Compare commits
276 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c38617df77 | |||
| 61c387113f | |||
| 7b6e7c3018 | |||
| 015b51a598 | |||
| f4417a2a58 | |||
| 0faf1ac6ba | |||
| 2f53b7f101 | |||
| 753d4c8a6e | |||
| d8182a9580 | |||
| 2630d0b401 | |||
| 151d51f6e8 | |||
| b970d3c454 | |||
| 19512cb6b2 | |||
| a228eaa04d | |||
| a592182e1c | |||
| 7970865f2a | |||
| dc92f7c24b | |||
| 384235bed9 | |||
| 4987a48fec | |||
| 619b76efcf | |||
| da427d5542 | |||
| c9d101b99a | |||
| 21cfccbfac | |||
| f91748e8d7 | |||
| 7e2f480a6c | |||
| 71c5140462 | |||
| aa8be3e99d | |||
| 2940dcff42 | |||
| accd28fc19 | |||
| 660baabe33 | |||
| 315c31389d | |||
| 34336f9ece | |||
| 4e30667a14 | |||
| 8e01a115ba | |||
| c3138c7001 | |||
| 6f8e30598c | |||
| 024c546345 | |||
| ce6fcb3fe6 | |||
| a37735daa5 | |||
| f0591c0aa2 | |||
| 06d70f250e | |||
| bf915d7215 | |||
| 92bc6a5932 | |||
| d304a20ba2 | |||
| 0734e360f6 | |||
| 70e79bfbf5 | |||
| 2c51549ef7 | |||
| a6933cdb66 | |||
| 927726fae6 | |||
| 62f86064dc | |||
| 0610835df7 | |||
| 883a9da071 | |||
| e541af8201 | |||
| 1fa908d859 | |||
| 39aa1b430e | |||
| 29cb45392f | |||
| 441c897442 | |||
| 5baa304add | |||
| c160171ec9 | |||
| e229c69421 | |||
| 8ab911f4dd | |||
| b767ff94ff | |||
| 05d4e1f117 | |||
| 492a9fc70b | |||
| 4faf126fa3 | |||
| ce0a7c3d6d | |||
| db56bc097d | |||
| d61bb4da4f | |||
| 1b7eec07be | |||
| ea3798f1ec | |||
| a78c36250b | |||
| bfcfd90bf5 | |||
| b6e3fd9a55 | |||
| e67951c0c1 | |||
| e9280799c9 | |||
| 3f036a1faa | |||
| 841eb0ef30 | |||
| 4305ee261c | |||
| de5ed10fcf | |||
| 0d327b14ec | |||
| 985c4d5b3f | |||
| bbed27395a | |||
| e3ca73d34f | |||
| fb20387229 | |||
| 068b3cc87e | |||
| 364081a24c | |||
| b629c232b2 | |||
| 1d2afb8f03 | |||
| 9776978f50 | |||
| 08c0686f05 | |||
| a45711f58b | |||
| 419d3880d2 | |||
| ad85560e70 | |||
| 8e4f74dc0d | |||
| 48b88f2989 | |||
| 2c6f91d225 | |||
| 26ba4f1678 | |||
| de80521bfa | |||
| 29414d8e33 | |||
| 1d9238c5d6 | |||
| 1ae2c62237 | |||
| f17372e08d | |||
| 50d5f68848 | |||
| 67400160a4 | |||
| d7e0132e23 | |||
| e9e6625df5 | |||
| ecf949e05a | |||
| eb3f046325 | |||
| d40772caf7 | |||
| a06db8d24b | |||
| 67f82cfb51 | |||
| 01b93675d8 | |||
| 93262362a3 | |||
| 50d550a350 | |||
| d880a87225 | |||
| c9e5f39b7a | |||
| 16565770ea | |||
| 9e927b6aac | |||
| be65185b76 | |||
| 956bd7878c | |||
| 42d0681ff3 | |||
| a7f8fbae9a | |||
| f64c30f3f3 | |||
| 8cd72c6359 | |||
| b1cda614c3 | |||
| 94730ccee8 | |||
| a8107594a8 | |||
| 385679b4a4 | |||
| 563f1ce2eb | |||
| 8f3500295f | |||
| 8be0b8e07f | |||
| 2f6f9b4f13 | |||
| 81c8820b80 | |||
| 94ba867b00 | |||
| ae4ccedcc2 | |||
| 05ec76bf91 | |||
| 6923ead0e6 | |||
| 9b0d0e06e4 | |||
| e7396ee638 | |||
| 766bd54a10 | |||
| e4950fdfec | |||
| 6c154c0773 | |||
| bd1a290007 | |||
| 734182fedf | |||
| 54f9852313 | |||
| 8f3626ce47 | |||
| 2805cad639 | |||
| 254afbda73 | |||
| cddb5a39c5 | |||
| 9ab2eca760 | |||
| db6f22584c | |||
| b79df5fa2d | |||
| 672b5e2a1a | |||
| a8095edd6b | |||
| 1b84662e46 | |||
| 255aaa2652 | |||
| c3e2a19938 | |||
| 7c25ebdd65 | |||
| a1eff91095 | |||
| 7ace51e44f | |||
| d89b0e9afd | |||
| 1212ed8d04 | |||
| b2786ee424 | |||
| b10d7ee06b | |||
| fe931e4f40 | |||
| 17eaef1d9b | |||
| 2c4da47c7f | |||
| 92b9aa69d5 | |||
| fe8383adc8 | |||
| 40f5d48edf | |||
| 38262bdbb9 | |||
| 18b2dc3f80 | |||
| 8ad83a4fc6 | |||
| 5e99a3a354 | |||
| cbe2c251fb | |||
| ce94951550 | |||
| f1969bc88c | |||
| 7be078995e | |||
| f8b9bbd403 | |||
| 656eef7bcd | |||
| 6326ec8211 | |||
| 512eb9328b | |||
| 8f4c8cd88f | |||
| 0f312478bc | |||
| b022822a97 | |||
| ee396004a5 | |||
| 2077acddcb | |||
| e34cfec074 | |||
| 595733435b | |||
| e610d9dff0 | |||
| f5b28b0dc0 | |||
| c8f8c6c7b5 | |||
| 6ce30d8063 | |||
| c489613d12 | |||
| e835aac336 | |||
| a318b10e97 | |||
| 1ca08f363c | |||
| 0d68177297 | |||
| 47a9363624 | |||
| 02a95b0068 | |||
| 5f77e78dce | |||
| a55c4df8ac | |||
| ffe674e179 | |||
| 5316dffc79 | |||
| 2c102ffd86 | |||
| 653651af9e | |||
| 2579221ef3 | |||
| 1671a2a641 | |||
| 271af35ff3 | |||
| 38efa7f5fd | |||
| a184514f2c | |||
| 7a79a5c1fa | |||
| 4d55c37949 | |||
| 62e5db0159 | |||
| efe3394cc7 | |||
| 631f1c8836 | |||
| b0060fa347 | |||
| 51af6f30e4 | |||
| 0325a5bd2e | |||
| 46569adf84 | |||
| 1ada9125b7 | |||
| 0a260bd78e | |||
| f9415e58ba | |||
| 1abd6285a2 | |||
| 93d1f2b3fb | |||
| 3baa30e4b8 | |||
| b76d2b89f8 | |||
| 7593629c61 | |||
| 3fc3e664f4 | |||
| 2a24baba26 | |||
| 7008d8a401 | |||
| 75679dc539 | |||
| d6a4af0adf | |||
| 1e4bb13b14 | |||
| 345bb7e0b6 | |||
| fe75aaaf03 | |||
| 4f6b27c3cc | |||
| 60e04d13a2 | |||
| a5af67e962 | |||
| 70831ef914 | |||
| 3e20a074bf | |||
| 711ad93254 | |||
| 95a4720a24 | |||
| 2f26bfd45a | |||
| a00b05b352 | |||
| 5802d949e5 | |||
| 860db84f51 | |||
| 9ec02d15f0 | |||
| cb7ef32616 | |||
| 2e160a6f29 | |||
| 99f815dfbd | |||
| b10f95bd02 | |||
| a56f0168dd | |||
| 3426e2aff4 | |||
| 8571bc04ab | |||
| bb4b2b875e | |||
| f3382b7d76 | |||
| b0fc7d5506 | |||
| 33068e663b | |||
| 6ed1e9bdb1 | |||
| e6a80a4d88 | |||
| 2fe8c6b582 | |||
| 6bc078dbe4 | |||
| 7fd1b3977b | |||
| cce1dbd5a9 | |||
| 1c314402c5 | |||
| 2dae141096 | |||
| b58b07cf9b | |||
| 3b15071a3f | |||
| 770c2e3b98 | |||
| 6f33c7d785 | |||
| ced75f75ae | |||
| 3815b97099 | |||
| 5626dc129b | |||
| 7cacc37645 | |||
| 9f15bc22c5 |
+1
-6
@@ -7,12 +7,7 @@ module.exports = {
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/essential',
|
||||
'prettier/vue',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:vue/essential', 'prettier'],
|
||||
plugins: ['@typescript-eslint', 'vue'],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Test
|
||||
name: Build
|
||||
|
||||
on: [push]
|
||||
|
||||
@@ -10,37 +10,35 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '16.x'
|
||||
always-auth: true
|
||||
registry-url: https://registry.npmjs.org
|
||||
- id: cache_npm
|
||||
name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
./node_modules.tar.zstd
|
||||
key: ${{ runner.OS }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
key: ${{ runner.OS }}-node16.x-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-npm-
|
||||
${{ runner.OS }}-
|
||||
${{ runner.OS }}-node16.x-
|
||||
- name: Install dependencies with NPM
|
||||
if: steps.cache_npm.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
- name: Archive node_modules
|
||||
if: steps.cache_npm.outputs.cache-hit != 'true'
|
||||
run: tar --use-compress-program "zstd -T0 --long=31 -1" -cf node_modules.tar.zstd -P node_modules
|
||||
- name: Persisting node_modules artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: node_modules.tar.zstd
|
||||
path: node_modules.tar.zstd
|
||||
retention-days: 2
|
||||
|
||||
#
|
||||
# lint job
|
||||
@@ -51,22 +49,32 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
node-version: '12.x'
|
||||
token: ${{ secrets.BUILD_USER_TOKEN || github.token }} # allows commit of any fixes to trigger a new workflow run
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
always-auth: true
|
||||
registry-url: https://registry.npmjs.org
|
||||
- name: Restore node_modules artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: node_modules.tar.zstd
|
||||
- name: Unarchive node_modules
|
||||
run: tar --use-compress-program "zstd -d --long=31" -xf node_modules.tar.zstd
|
||||
# ESLint
|
||||
- name: Lint source code
|
||||
run: npm run lint
|
||||
# Lint
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v2
|
||||
with:
|
||||
prettier: true
|
||||
eslint: true
|
||||
eslint_args: "--ext '.ts,.js' --ignore-path '.gitignore' --ignore-pattern '.github/*'"
|
||||
continue_on_error: false
|
||||
auto_fix: ${{ secrets.BUILD_USER_TOKEN && 'true' || 'false' }}
|
||||
git_name: equabot
|
||||
git_email: git@equalogic.com
|
||||
|
||||
#
|
||||
# build job
|
||||
@@ -77,15 +85,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '16.x'
|
||||
always-auth: true
|
||||
registry-url: https://registry.npmjs.org
|
||||
- name: Restore node_modules artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: node_modules.tar.zstd
|
||||
- name: Unarchive node_modules
|
||||
@@ -113,9 +121,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
always-auth: true
|
||||
registry-url: https://registry.npmjs.org
|
||||
- name: Restore node_modules artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: node_modules.tar.zstd
|
||||
- name: Unarchive node_modules
|
||||
@@ -125,9 +139,3 @@ jobs:
|
||||
run: node_modules/.bin/dotenv -e .env.ci -- npm run test:ci
|
||||
env:
|
||||
CI: true
|
||||
- if: always()
|
||||
name: Persisting test-results.html artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results.html
|
||||
path: test/.results/test-results.html
|
||||
@@ -1,18 +0,0 @@
|
||||
name: Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *' # every day at 1am
|
||||
|
||||
jobs:
|
||||
remove-old-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Remove old artifacts
|
||||
uses: c-hive/gha-remove-artifacts@v1
|
||||
with:
|
||||
age: '1 days' # delete artifacts older than this
|
||||
skip-tags: false # don't treat artifacts created by runs on tagged commits any differently
|
||||
skip-recent: 2 # keep the last 2 runs, regardless of age
|
||||
@@ -3,6 +3,7 @@ name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
#
|
||||
@@ -12,30 +13,27 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '16.x'
|
||||
always-auth: true
|
||||
registry-url: https://registry.npmjs.org
|
||||
# Dependencies
|
||||
- id: cache_npm
|
||||
name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
./node_modules.tar.zstd
|
||||
key: ${{ runner.OS }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
key: ${{ runner.OS }}-node16.x-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-npm-
|
||||
${{ runner.OS }}-
|
||||
${{ runner.OS }}-node16.x-
|
||||
- name: Install dependencies with NPM
|
||||
if: steps.cache_npm.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
- name: Unarchive node_modules
|
||||
if: steps.cache_npm.outputs.cache-hit == 'true'
|
||||
run: tar --use-compress-program "zstd -d --long=31" -xf node_modules.tar.zstd
|
||||
@@ -56,6 +54,6 @@ jobs:
|
||||
- name: Copy extra files into dist directory
|
||||
run: cp package.json README* dist/
|
||||
- name: Publish release to NPM registry
|
||||
run: npm publish dist
|
||||
run: npm publish dist/
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_PUBLISHING }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Ignore workflows
|
||||
.github
|
||||
|
||||
# Node modules
|
||||
node_modules
|
||||
|
||||
# Test results
|
||||
test/.results
|
||||
test/.coverage
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
@@ -1,3 +1,156 @@
|
||||
# vue-apollo-smart-ops
|
||||
|
||||
Create TypeScript-typed operation functions for your Vue Apollo queries and mutations.
|
||||
Creates TypeScript-typed operation functions for GraphQL queries and mutations compatible with
|
||||
[Vue Apollo](https://apollo.vuejs.org/).
|
||||
|
||||
This library is intended to be used together with the
|
||||
[`typescript-vue-apollo-smart-ops` plugin](https://www.graphql-code-generator.com/docs/plugins/typescript-vue-apollo-smart-ops)
|
||||
for [GraphQL Code Generator](https://www.graphql-code-generator.com/), but it may also be useful standalone.
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
npm install --save vue-apollo-smart-ops
|
||||
```
|
||||
|
||||
## Smart Query Usage
|
||||
|
||||
### `createSmartQueryOptionsFunction(query, onError?)`
|
||||
|
||||
Returns a generated function which returns a [Vue Apollo Smart Query options object](https://apollo.vuejs.org/api/smart-query.html#options)
|
||||
for the given query when called.
|
||||
|
||||
> ⚠️ Note: The returned function is not meant to execute the query itself. It is only used to configure a Vue Apollo
|
||||
> Smart Query. The responsibility for executing the query lies with Vue Apollo.
|
||||
|
||||
The returned function accepts an options object as its first argument, allowing variables and other parameters to be
|
||||
customised in runtime usage. Compared with creating an options object directly, the advantage here is that the options
|
||||
accepted by the generated function are fully type-checked based on the query definition - and without needing to pass
|
||||
type arguments at every usage.
|
||||
|
||||
Using the [`@graphql-codegen/typescript-vue-apollo-smart-ops` plugin](https://www.graphql-code-generator.com/docs/plugins/typescript-vue-apollo-smart-ops)
|
||||
you can automatically generate options functions for all the query operations in your project.
|
||||
|
||||
The following example manually creates an options function and assigns it to the variable `useTodoListQuery`:
|
||||
|
||||
```typescript
|
||||
const useTodoListQuery = createSmartQueryOptionsFunction<TodosQuery, TodosQueryVariables>(gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`);
|
||||
```
|
||||
|
||||
This function can subsequently be called inside the `apollo` options of a Vue component to configure a Smart Query. The
|
||||
following example adds a `todos` Smart Query to a component, in Vue class component style:
|
||||
|
||||
```typescript
|
||||
@Component<TodoList>({
|
||||
apollo: {
|
||||
todos: useTodoListQuery<TodoList>({
|
||||
skip() {
|
||||
return this.foo === 'bar';
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
class TodoList extends Vue {
|
||||
todos: Todo[] | null = null;
|
||||
|
||||
get foo(): string {
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `@SmartQuery(options)`
|
||||
|
||||
The `@SmartQuery()` decorator works with your generated options functions to enable the declaration of Smart Queries
|
||||
within the body of a Vue class component, instead of in the component options. This helps to keep the data property and
|
||||
its query options together in one place.
|
||||
|
||||
The following example is equivalent to the previous example but using the decorated syntax style:
|
||||
|
||||
```typescript
|
||||
@Component
|
||||
class TodoList extends Vue {
|
||||
@SmartQuery(
|
||||
useTodoListQuery<TodoList>({
|
||||
skip() {
|
||||
return this.foo === 'bar';
|
||||
},
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
}),
|
||||
)
|
||||
todos: Todo[] | null = null;
|
||||
|
||||
get foo(): string {
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Mutations Usage
|
||||
|
||||
### `createMutationFunction(mutation, onError?)`
|
||||
|
||||
Returns a generated function which executes a mutation and returns the result when called.
|
||||
|
||||
The returned function requires a Vue app instance as its first argument (from which the `$apollo` client will be
|
||||
accessed), and accepts an options object as its second argument, allowing variables and other parameters to be
|
||||
customised in runtime usage. Compared with executing a mutation using the Vue Apollo client directly, the advantage here
|
||||
is that the options accepted by the generated function are fully type-checked based on the operation definition - and
|
||||
without needing to pass type arguments at every usage.
|
||||
|
||||
Using the [`@graphql-codegen/typescript-vue-apollo-smart-ops` plugin](https://www.graphql-code-generator.com/docs/plugins/typescript-vue-apollo-smart-ops)
|
||||
you can automatically generate mutation functions for all the mutation operations in your project.
|
||||
|
||||
The following example manually creates a mutation function and assigns it to the variable `todoCreateMutation`:
|
||||
|
||||
```typescript
|
||||
const todoCreateMutation = createMutationFunction<TodoCreateMutation, TodoCreateMutationVariables>(gql`
|
||||
mutation TodoCreate($input: TodoInput!) {
|
||||
todoCreate(input: $input) {
|
||||
todo {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
```
|
||||
|
||||
The following example demonstrates how to call this mutation from a method on a component:
|
||||
|
||||
```typescript
|
||||
@Component
|
||||
class TodoList extends Vue {
|
||||
async onClickCreateTodoButton(): Promise<void> {
|
||||
const result = await todoCreateMutation(this, {
|
||||
variables: {
|
||||
title: 'Bake a cake',
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Failed to create Todo!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
- `@SmartQuery()` decorator is based on the [vue-apollo-decorator](https://github.com/chanlito/vue-apollo-decorator)
|
||||
package by chanlito, with some alteration to the types.
|
||||
|
||||
+1
-6
@@ -1,11 +1,6 @@
|
||||
{
|
||||
"directory": "src/",
|
||||
"exclude": [
|
||||
"\\.(spec|e2e-spec)\\.ts$",
|
||||
"src/cli/",
|
||||
"src/database/",
|
||||
"src/testing/"
|
||||
],
|
||||
"exclude": ["\\.(spec|e2e-spec)\\.ts$", "src/cli/", "src/database/", "src/testing/"],
|
||||
"location": "all",
|
||||
"structure": "flat",
|
||||
"singleQuotes": true,
|
||||
|
||||
+15
-22
@@ -1,24 +1,17 @@
|
||||
const { pathsToModuleNameMapper } = require('ts-jest/utils');
|
||||
const { compilerOptions } = require('./tsconfig');
|
||||
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/$1',
|
||||
'^~/(.*)$': '<rootDir>/$1',
|
||||
'^vue$': 'vue/dist/vue.common.js',
|
||||
},
|
||||
moduleFileExtensions: ['js', 'ts', 'vue', 'json'],
|
||||
transform: {
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
'^.+\\.ts?$': 'ts-jest',
|
||||
'.*\\.(vue)$': 'vue-jest',
|
||||
},
|
||||
collectCoverage: false,
|
||||
collectCoverageFrom: ['<rootDir>/components/**/*.vue', '<rootDir>/pages/**/*.vue'],
|
||||
reporters: [
|
||||
'default',
|
||||
[
|
||||
'jest-html-reporters',
|
||||
{
|
||||
filename: 'test/.results/test-results.html',
|
||||
},
|
||||
],
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src/', '<rootDir>/test/'],
|
||||
testRegex: '\\.(spec|e2e-spec)\\.ts$',
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
testTimeout: 20000,
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
|
||||
prefix: '<rootDir>/',
|
||||
}),
|
||||
coveragePathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/test/'],
|
||||
coverageDirectory: '<rootDir>/test/.coverage',
|
||||
reporters: ['default'],
|
||||
};
|
||||
|
||||
Generated
+12818
-5227
File diff suppressed because it is too large
Load Diff
+35
-41
@@ -1,25 +1,26 @@
|
||||
{
|
||||
"name": "vue-apollo-smart-ops",
|
||||
"version": "0.0.3",
|
||||
"version": "0.2.0-beta.1",
|
||||
"description": "Create TypeScript-typed operation functions for your Vue Apollo queries and mutations.",
|
||||
"author": "Madscience Ltd",
|
||||
"author": "Equalogic Ltd",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/madscience/vue-apollo-smart-ops.git"
|
||||
"url": "https://github.com/equalogic/vue-apollo-smart-ops.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist && npm run barrels:generate",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"postbuild": "cp package.json README.md dist/",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --ext .ts --ignore-path .gitignore .",
|
||||
"lint:fix": "eslint --ext .ts --ignore-path .gitignore . --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:ci": "rimraf test/.results && mkdirp test/.results && jest --ci --runInBand --passWithNoTests",
|
||||
"test:ci": "jest --ci --runInBand",
|
||||
"version": "npm run build",
|
||||
"postversion": "npm run postbuild",
|
||||
"release": "np --contents dist/",
|
||||
@@ -29,58 +30,51 @@
|
||||
"lodash.isplainobject": "^4.0",
|
||||
"lodash.mapvalues": "^4.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"vue-class-component": "^7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"apollo-client": ">=2.6",
|
||||
"apollo-link": ">=1.2",
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0",
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
|
||||
"vue": ">=2.6",
|
||||
"vue-apollo": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "26.0.22",
|
||||
"@types/lodash.isplainobject": "4.0.6",
|
||||
"@types/lodash.mapvalues": "4.6.6",
|
||||
"@typescript-eslint/eslint-plugin": "4.21.0",
|
||||
"@typescript-eslint/parser": "4.21.0",
|
||||
"@vue/test-utils": "1.1.3",
|
||||
"@types/jest": "27.5.2",
|
||||
"@types/lodash.isplainobject": "4.0.7",
|
||||
"@types/lodash.mapvalues": "4.6.7",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.5",
|
||||
"@typescript-eslint/parser": "5.30.5",
|
||||
"@vue/test-utils": "1.3.0",
|
||||
"apollo-client": "2.6.10",
|
||||
"apollo-link": "1.2.14",
|
||||
"barrelsby": "2.2.0",
|
||||
"dotenv-cli": "4.0.0",
|
||||
"eslint": "7.23.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jest": "24.3.4",
|
||||
"barrelsby": "2.3.4",
|
||||
"dotenv-cli": "6.0.0",
|
||||
"eslint": "8.19.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-jest": "26.5.3",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"eslint-plugin-promise": "4.3.1",
|
||||
"eslint-plugin-vue": "7.8.0",
|
||||
"graphql": "15.5.0",
|
||||
"husky": "6.0.0",
|
||||
"jest": "26.6.3",
|
||||
"jest-html-reporters": "2.1.3",
|
||||
"lint-staged": "10.5.4",
|
||||
"eslint-plugin-promise": "6.0.0",
|
||||
"eslint-plugin-vue": "9.2.0",
|
||||
"graphql": "15.8.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"jest": "27.5.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"np": "7.4.0",
|
||||
"prettier": "2.2.1",
|
||||
"np": "7.6.1",
|
||||
"prettier": "2.7.1",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-jest": "26.5.4",
|
||||
"typescript": "4.2.4",
|
||||
"vue": "2.6.12",
|
||||
"vue-apollo": "3.0.7",
|
||||
"vue-jest": "3.0.7"
|
||||
"ts-jest": "27.1.5",
|
||||
"typescript": "4.7.4",
|
||||
"vue": "2.6.14",
|
||||
"vue-apollo": "3.1.0",
|
||||
"vue-class-component": "7.2.6",
|
||||
"vue-property-decorator": "9.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
},
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,js}": "eslint --ext .ts,.js --ignore-path .gitignore --fix --cache"
|
||||
}
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
|
||||
+10
-7
@@ -2,7 +2,7 @@
|
||||
"extends": [
|
||||
"config:js-lib",
|
||||
":automergeMajor",
|
||||
":automergePr",
|
||||
":automergeBranch",
|
||||
":automergeRequireAllStatusChecks",
|
||||
":dependencyDashboard",
|
||||
":semanticCommitsDisabled"
|
||||
@@ -12,12 +12,9 @@
|
||||
"prCreation": "not-pending",
|
||||
"stabilityDays": 3,
|
||||
"rebaseWhen": "conflicted",
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"reviewers": [
|
||||
"sgarner"
|
||||
],
|
||||
"lockFileMaintenance": true,
|
||||
"labels": ["dependencies"],
|
||||
"reviewers": ["sgarner"],
|
||||
"packageRules": [
|
||||
{
|
||||
"packagePatterns": ["eslint"],
|
||||
@@ -26,6 +23,12 @@
|
||||
{
|
||||
"packagePatterns": ["jest"],
|
||||
"groupName": "jest"
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["nvm", "npm"],
|
||||
"matchPackageNames": ["node", "@types/node"],
|
||||
"groupName": "Node.js",
|
||||
"allowedVersions": "^16"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import {
|
||||
ApolloError,
|
||||
ApolloErrorType,
|
||||
ApolloOperationContext,
|
||||
GraphQLError,
|
||||
InputValidationError,
|
||||
ProcessedApolloError,
|
||||
ServerError,
|
||||
UnauthorizedError,
|
||||
} from './types';
|
||||
|
||||
export function isApolloError(error: ApolloError | any): error is ApolloError {
|
||||
return error.graphQLErrors !== undefined;
|
||||
}
|
||||
|
||||
export function isGraphQLError(error: GraphQLError | any): error is GraphQLError {
|
||||
return error.extensions !== undefined;
|
||||
}
|
||||
|
||||
export class ApolloErrorProcessor<TApp = Vue, TContext = ApolloOperationContext> {
|
||||
public static FriendlyMessages: Record<string, string> = {
|
||||
FAILED_TO_FETCH:
|
||||
'Unable to communicate with server. The service may be down or you may be offline. Try again in a moment.',
|
||||
INTERNAL_SERVER_ERROR: `A server error has occurred.`,
|
||||
};
|
||||
|
||||
public processedErrors: ProcessedApolloError[];
|
||||
|
||||
protected readonly originalError: Error;
|
||||
protected readonly app: TApp;
|
||||
protected readonly context: TContext;
|
||||
|
||||
public constructor(error: ApolloError, app: TApp, context: TContext) {
|
||||
this.originalError = error;
|
||||
this.app = app;
|
||||
this.context = context;
|
||||
|
||||
this.processedErrors = this.processApolloError(error);
|
||||
}
|
||||
|
||||
public showErrorNotifications(): void {
|
||||
// This is just an example - to do something else (e.g. showing a visible notification to the user), you should
|
||||
// implement your own class that extends ApolloErrorProcessor and replace this showErrorNotifications method.
|
||||
this.processedErrors.forEach(error => {
|
||||
console.error(`${error.type}: ${error.message}`, error.error);
|
||||
});
|
||||
}
|
||||
|
||||
public cleanError(error: ApolloError | GraphQLError | Record<string, any>): Error {
|
||||
if (error instanceof Error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// the `error` object we have may not be an actual Error instance
|
||||
// create a new one suitable for e.g. capturing to Sentry
|
||||
const cleanError = new Error(error.message);
|
||||
|
||||
if (isGraphQLError(error)) {
|
||||
cleanError.name = 'GraphQLError' + (error.extensions?.code != null ? `[${error.extensions.code}]` : '');
|
||||
cleanError.stack = this.originalError.stack;
|
||||
|
||||
Object.defineProperty(cleanError, 'nodes', { value: error.nodes });
|
||||
Object.defineProperty(cleanError, 'source', { value: error.source });
|
||||
Object.defineProperty(cleanError, 'positions', { value: error.positions });
|
||||
Object.defineProperty(cleanError, 'path', { value: error.path });
|
||||
Object.defineProperty(cleanError, 'extensions', { value: JSON.stringify(error.extensions) });
|
||||
Object.defineProperty(cleanError, 'originalError', { value: this.originalError });
|
||||
} else {
|
||||
Object.keys(error).forEach(key => Object.defineProperty(cleanError, key, { value: error[key] }));
|
||||
}
|
||||
|
||||
return cleanError;
|
||||
}
|
||||
|
||||
protected isUnauthorizedError(error: GraphQLError): boolean {
|
||||
return (
|
||||
error.message === 'Unauthorized' ||
|
||||
error.extensions?.code === 'FORBIDDEN' ||
|
||||
error.extensions?.exception?.status === 401
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
|
||||
protected onUnauthorizedError(error: UnauthorizedError): void {
|
||||
// extending classes can take action here, e.g. go to log in page
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
|
||||
protected onServerError(error: ServerError): void {
|
||||
// extending classes can take action here, e.g. capture to Sentry
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
|
||||
protected onInputValidationError(error: InputValidationError): void {
|
||||
// extending classes can take action here, e.g. capture to Sentry
|
||||
}
|
||||
|
||||
protected getFriendlyMessage(errorCode: string, errorMessage: string): string;
|
||||
protected getFriendlyMessage(errorCode: string): string | undefined;
|
||||
protected getFriendlyMessage(errorCode: string, errorMessage?: string): string | undefined {
|
||||
return (this.constructor as typeof ApolloErrorProcessor).FriendlyMessages[errorCode] ?? errorMessage;
|
||||
}
|
||||
|
||||
private processApolloError(error: ApolloError): ProcessedApolloError[] {
|
||||
if (error.graphQLErrors != null && error.graphQLErrors.length > 0) {
|
||||
// Successful request but with errors from the resolver
|
||||
return error.graphQLErrors.flatMap(graphQLError => this.processGraphQLError(graphQLError));
|
||||
}
|
||||
|
||||
if (
|
||||
error.networkError != null &&
|
||||
error.networkError.result != null &&
|
||||
error.networkError.result.errors != null &&
|
||||
error.networkError.result.errors.length > 0
|
||||
) {
|
||||
// Network error that contains GraphQL errors inside it. Can occur when server responds with a non-200 status code
|
||||
return error.networkError.result.errors.flatMap(graphQLError => this.processGraphQLError(graphQLError));
|
||||
}
|
||||
|
||||
if (error.networkError != null) {
|
||||
// Network error, e.g. server is not responding or some other exception occurs
|
||||
return this.processNetworkError(error);
|
||||
}
|
||||
|
||||
// Some other internal server error
|
||||
const processedError: ServerError = {
|
||||
type: ApolloErrorType.SERVER_ERROR,
|
||||
error,
|
||||
message: error.message,
|
||||
};
|
||||
|
||||
this.onServerError(processedError);
|
||||
|
||||
return [processedError];
|
||||
}
|
||||
|
||||
private processGraphQLError(error: GraphQLError): ProcessedApolloError[] {
|
||||
if (this.isUnauthorizedError(error)) {
|
||||
// Unauthorized (not logged in, or not allowed) error
|
||||
const processedError: UnauthorizedError = {
|
||||
type: ApolloErrorType.UNAUTHORIZED_ERROR,
|
||||
error,
|
||||
message: error.message,
|
||||
path: error.path,
|
||||
};
|
||||
|
||||
this.onUnauthorizedError(processedError);
|
||||
|
||||
return [processedError];
|
||||
}
|
||||
|
||||
if (error.extensions?.validationErrors != null) {
|
||||
// User input validation error
|
||||
const processedError: InputValidationError = {
|
||||
type: ApolloErrorType.INPUT_VALIDATION_ERROR,
|
||||
error,
|
||||
message: error.message,
|
||||
path: error.path,
|
||||
invalidArgs: error.extensions.invalidArgs,
|
||||
violations: error.extensions.validationErrors,
|
||||
};
|
||||
|
||||
this.onInputValidationError(processedError);
|
||||
|
||||
return [processedError];
|
||||
}
|
||||
|
||||
// Other GraphQL resolver error - probably a bug
|
||||
const processedError: ServerError = {
|
||||
type: error.extensions?.code != null ? error.extensions.code : ApolloErrorType.SERVER_ERROR,
|
||||
error,
|
||||
path: error.path,
|
||||
message: this.getFriendlyMessage('INTERNAL_SERVER_ERROR', error.message),
|
||||
};
|
||||
|
||||
this.onServerError(processedError);
|
||||
|
||||
return [processedError];
|
||||
}
|
||||
|
||||
private processNetworkError(error: ApolloError): ProcessedApolloError[] {
|
||||
const errors: ProcessedApolloError[] = [];
|
||||
let message: string;
|
||||
|
||||
if (error.networkError != null && error.networkError.message != null) {
|
||||
message = this.processErrorMessage(error.networkError.message);
|
||||
} else {
|
||||
message = this.processErrorMessage(error.message);
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case 'Failed to fetch':
|
||||
message = this.getFriendlyMessage('FAILED_TO_FETCH', message);
|
||||
break;
|
||||
}
|
||||
|
||||
errors.push({
|
||||
type: ApolloErrorType.NETWORK_ERROR,
|
||||
error,
|
||||
statusCode: error.networkError != null ? error.networkError.statusCode : undefined,
|
||||
message,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private processErrorMessage(message: GraphQLError['message']): string {
|
||||
if (typeof message === 'object') {
|
||||
if (message.error != null) {
|
||||
return message.error;
|
||||
}
|
||||
|
||||
return 'Unknown error: ' + JSON.stringify(message);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import { ApolloQueryResult } from 'apollo-client';
|
||||
import gql from 'graphql-tag';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { createSmartQueryOptionsFunction } from '../query';
|
||||
import { SmartQuery } from './SmartQuery';
|
||||
|
||||
interface Todo {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface QueryResult {
|
||||
todos: Todo[];
|
||||
__typename?: 'Query';
|
||||
}
|
||||
|
||||
interface QueryVariables {
|
||||
skip?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
describe('@SmartQuery() decorator', () => {
|
||||
it('Adds basic query options', () => {
|
||||
@Component
|
||||
class TodoList extends Vue {
|
||||
@SmartQuery({
|
||||
query: gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
})
|
||||
todos!: Todo[];
|
||||
|
||||
get vars(): QueryVariables {
|
||||
return {
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TodoList();
|
||||
|
||||
expect(instance.$options.apollo?.todos).toEqual(
|
||||
expect.objectContaining({
|
||||
query: expect.objectContaining({ definitions: expect.arrayContaining([]) }),
|
||||
variables: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('Works with a query options function', () => {
|
||||
const useTodoListQuery = createSmartQueryOptionsFunction<QueryResult, QueryVariables>(gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@Component<TodoList>({})
|
||||
class TodoList extends Vue {
|
||||
@SmartQuery(
|
||||
useTodoListQuery<TodoList>({
|
||||
skip() {
|
||||
return this.foo === 'bar';
|
||||
},
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
loadingKey: 'loading',
|
||||
}),
|
||||
)
|
||||
todos!: Todo[];
|
||||
|
||||
loading: number = 0;
|
||||
|
||||
get vars(): QueryVariables {
|
||||
return {
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
}
|
||||
|
||||
get foo(): string {
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TodoList();
|
||||
|
||||
expect(instance.$options.apollo?.todos).toEqual(
|
||||
expect.objectContaining({
|
||||
query: expect.objectContaining({ definitions: expect.arrayContaining([]) }),
|
||||
skip: expect.any(Function),
|
||||
variables: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('Works with class inheritance and update, result methods', () => {
|
||||
@Component
|
||||
class TodoList extends Vue {
|
||||
@SmartQuery({
|
||||
query: gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
})
|
||||
todos!: Todo[];
|
||||
|
||||
get vars(): QueryVariables {
|
||||
return {
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
class TodoList2 extends TodoList {
|
||||
@SmartQuery<TodoList2, QueryResult>({
|
||||
query: gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
update(data: QueryResult) {
|
||||
// data: QueryResult
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
result({ data, errors, loading }) {
|
||||
this.doThings();
|
||||
},
|
||||
subscribeToMore: {
|
||||
document: gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
},
|
||||
})
|
||||
todos!: Todo[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
doThings() {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TodoList2();
|
||||
|
||||
expect(instance.$options.apollo?.todos).toEqual(
|
||||
expect.objectContaining({
|
||||
query: expect.objectContaining({ definitions: expect.arrayContaining([]) }),
|
||||
variables: expect.any(Function),
|
||||
update: expect.any(Function),
|
||||
result: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('Supports subscribeToMore with updateQuery method', () => {
|
||||
@Component
|
||||
class TodoList extends Vue {
|
||||
@SmartQuery({
|
||||
query: gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
})
|
||||
todos!: Todo[];
|
||||
|
||||
get vars(): QueryVariables {
|
||||
return {
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
class TodoList3 extends TodoList {
|
||||
@SmartQuery<TodoList3, QueryResult, QueryVariables>({
|
||||
query: gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
update(data: QueryResult) {
|
||||
// data: QueryResult
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
||||
result(data: ApolloQueryResult<QueryResult>) {},
|
||||
subscribeToMore: [
|
||||
{
|
||||
document: gql`
|
||||
query Todos($skip: Int, $limit: Int) {
|
||||
todos(skip: $skip, limit: $limit) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return this.vars;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
updateQuery(prev, { subscriptionData: { data }, variables }) {
|
||||
return {
|
||||
...prev,
|
||||
todos: [
|
||||
{ id: '1', title: 'Int' },
|
||||
{ id: '2', title: 'Float' },
|
||||
{ id: '3', title: 'String' },
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
todos!: Todo[];
|
||||
}
|
||||
|
||||
const instance = new TodoList3();
|
||||
|
||||
expect(instance.$options.apollo?.todos).toEqual(
|
||||
expect.objectContaining({
|
||||
query: expect.objectContaining({ definitions: expect.arrayContaining([]) }),
|
||||
variables: expect.any(Function),
|
||||
update: expect.any(Function),
|
||||
result: expect.any(Function),
|
||||
subscribeToMore: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
document: expect.objectContaining({ definitions: expect.arrayContaining([]) }),
|
||||
variables: expect.any(Function),
|
||||
updateQuery: expect.any(Function),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Based on https://github.com/chanlito/vue-apollo-decorator by chanlito ❤️
|
||||
*/
|
||||
|
||||
import { ApolloError, OperationVariables } from 'apollo-client';
|
||||
import { DocumentNode } from 'graphql';
|
||||
import Vue from 'vue';
|
||||
import { createDecorator, VueDecorator } from 'vue-class-component';
|
||||
import { VueApolloSmartQueryOptions } from '../query';
|
||||
|
||||
export function SmartQuery<TApp = any, TResult = any, TVariables = OperationVariables, TError = ApolloError>(
|
||||
options: TApp extends Vue ? VueApolloSmartQueryOptions<TResult, TVariables, TError, TApp> : DocumentNode,
|
||||
): VueDecorator {
|
||||
return createDecorator((componentOptions: any, k: string) => {
|
||||
componentOptions.apollo = componentOptions.apollo || {};
|
||||
componentOptions.apollo[k] = options;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from './SmartQuery';
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
ApolloErrorType,
|
||||
InputValidationError,
|
||||
NetworkError,
|
||||
ProcessedApolloError,
|
||||
ServerError,
|
||||
UnauthorizedError,
|
||||
UserInputError,
|
||||
ValidationRuleViolation,
|
||||
} from './types';
|
||||
|
||||
export interface ApolloErrorHandlerResultInterface {
|
||||
allErrors: ProcessedApolloError[];
|
||||
validationRuleViolations?: ValidationRuleViolation[];
|
||||
}
|
||||
|
||||
export class ApolloErrorHandlerResult implements ApolloErrorHandlerResultInterface {
|
||||
public readonly allErrors: ProcessedApolloError[];
|
||||
|
||||
public constructor(
|
||||
public readonly unhandledErrors: ProcessedApolloError[],
|
||||
public readonly handledErrors: ProcessedApolloError[],
|
||||
) {
|
||||
this.allErrors = [...unhandledErrors, ...handledErrors];
|
||||
}
|
||||
|
||||
public get networkErrors(): NetworkError[] {
|
||||
return this.allErrors.filter((e): e is NetworkError => e.type === ApolloErrorType.NETWORK_ERROR);
|
||||
}
|
||||
|
||||
public get serverErrors(): ServerError[] {
|
||||
return this.allErrors.filter((e): e is ServerError => e.type === ApolloErrorType.SERVER_ERROR);
|
||||
}
|
||||
|
||||
public get unauthorizedErrors(): UnauthorizedError[] {
|
||||
return this.allErrors.filter((e): e is UnauthorizedError => e.type === ApolloErrorType.UNAUTHORIZED_ERROR);
|
||||
}
|
||||
|
||||
public get userInputErrors(): UserInputError[] {
|
||||
return this.allErrors.filter((e): e is UserInputError => e.type === ApolloErrorType.BAD_USER_INPUT);
|
||||
}
|
||||
|
||||
public get inputValidationErrors(): InputValidationError[] {
|
||||
return this.allErrors.filter((e): e is InputValidationError => e.type === ApolloErrorType.INPUT_VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
public get validationRuleViolations(): ValidationRuleViolation[] {
|
||||
return this.inputValidationErrors.flatMap(e => e.violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { processApolloError } from './processApolloError';
|
||||
import { ApolloOperationContext } from '../types';
|
||||
import { Vue } from 'vue/types/vue';
|
||||
import { ApolloError, ApolloOperationErrorHandlerFunction } from './types';
|
||||
import { ApolloErrorHandlerResult } from './ApolloErrorHandlerResult';
|
||||
|
||||
/**
|
||||
* This is a simple example of an error handler function. You can copy this and implement your own in your application.
|
||||
*/
|
||||
export const handleApolloError: ApolloOperationErrorHandlerFunction<
|
||||
ApolloError,
|
||||
Vue,
|
||||
ApolloOperationContext,
|
||||
ApolloErrorHandlerResult
|
||||
> = (error: ApolloError, app: Vue, context?: ApolloOperationContext): ApolloErrorHandlerResult => {
|
||||
const { unhandledErrors, handledErrors } = processApolloError(error, {
|
||||
app,
|
||||
context,
|
||||
// Example of a handler function for a particular type of error:
|
||||
onUnauthorizedError: error => {
|
||||
console.warn('Unauthorized! Logging you out...', error);
|
||||
//logout();
|
||||
|
||||
// Returning true indicates to the processor that we've handled this error
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
unhandledErrors.forEach(error => {
|
||||
console.error(`${error.type}: ${error.message}`, error.error);
|
||||
});
|
||||
|
||||
return new ApolloErrorHandlerResult(unhandledErrors, handledErrors);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from './ApolloErrorHandlerResult';
|
||||
export * from './handleApolloError';
|
||||
export * from './processApolloError';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,213 @@
|
||||
import Vue from 'vue';
|
||||
import { ApolloOperationContext } from '../types';
|
||||
import {
|
||||
ApolloError,
|
||||
ApolloErrorType,
|
||||
GraphQLError,
|
||||
InputValidationError,
|
||||
NetworkError,
|
||||
ProcessedApolloError,
|
||||
ServerError,
|
||||
UnauthorizedError,
|
||||
} from './types';
|
||||
|
||||
export function isApolloError(error: ApolloError | any): error is ApolloError {
|
||||
return error.graphQLErrors !== undefined;
|
||||
}
|
||||
|
||||
export function isGraphQLError(error: GraphQLError | any): error is GraphQLError {
|
||||
return error.extensions !== undefined;
|
||||
}
|
||||
|
||||
function normalizeErrorMessage(message: GraphQLError['message']): string {
|
||||
if (typeof message === 'object') {
|
||||
if (message.error != null) {
|
||||
return message.error;
|
||||
}
|
||||
|
||||
return 'Unknown error: ' + JSON.stringify(message);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function normalizeError(error: GraphQLError | Error): Error {
|
||||
if (isGraphQLError(error)) {
|
||||
return {
|
||||
...error,
|
||||
message: normalizeErrorMessage(error.message),
|
||||
};
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
function translateErrorMessage(messageOrCode: string, translations: Record<string, string>): string {
|
||||
return translations[messageOrCode] ?? messageOrCode;
|
||||
}
|
||||
|
||||
export interface ApolloErrorProcessorOptions<TApp = Vue, TContext = ApolloOperationContext> {
|
||||
app?: TApp;
|
||||
context?: TContext;
|
||||
isUnauthorizedError?: (error: GraphQLError) => boolean;
|
||||
onUnauthorizedError?: (error: UnauthorizedError) => boolean | void;
|
||||
onInputValidationError?: (error: InputValidationError) => boolean | void;
|
||||
onServerError?: (error: ServerError) => boolean | void;
|
||||
onNetworkError?: (error: NetworkError) => boolean | void;
|
||||
translations?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const defaultErrorMessageTranslations: Record<string, string> = {
|
||||
FAILED_TO_FETCH:
|
||||
'Unable to communicate with server. The service may be down or you may be offline. Try again in a moment.',
|
||||
INTERNAL_SERVER_ERROR: `A server error has occurred.`,
|
||||
};
|
||||
|
||||
const defaultProcessorOptions: ApolloErrorProcessorOptions = {
|
||||
isUnauthorizedError: (error: GraphQLError) =>
|
||||
error.message === 'Unauthorized' ||
|
||||
error.extensions?.code === 'FORBIDDEN' ||
|
||||
error.extensions?.exception?.status === 401,
|
||||
translations: defaultErrorMessageTranslations,
|
||||
};
|
||||
|
||||
export interface ApolloErrorProcessorResult {
|
||||
unhandledErrors: ProcessedApolloError[];
|
||||
handledErrors: ProcessedApolloError[];
|
||||
}
|
||||
|
||||
function processGraphQLError(
|
||||
error: GraphQLError,
|
||||
options: ApolloErrorProcessorOptions = {},
|
||||
): ApolloErrorProcessorResult {
|
||||
options = { ...defaultProcessorOptions, ...options };
|
||||
const { isUnauthorizedError, onUnauthorizedError, onInputValidationError, onServerError, translations } = options;
|
||||
|
||||
if (isUnauthorizedError != null && isUnauthorizedError(error)) {
|
||||
// Unauthorized (not logged in, or not allowed) error
|
||||
const processedError: UnauthorizedError = {
|
||||
type: ApolloErrorType.UNAUTHORIZED_ERROR,
|
||||
error: normalizeError(error),
|
||||
message: normalizeErrorMessage(error.message),
|
||||
path: error.path,
|
||||
};
|
||||
|
||||
if (onUnauthorizedError != null && onUnauthorizedError(processedError)) {
|
||||
return { unhandledErrors: [], handledErrors: [processedError] };
|
||||
}
|
||||
|
||||
return { unhandledErrors: [processedError], handledErrors: [] };
|
||||
}
|
||||
|
||||
if (error.extensions?.validationErrors != null) {
|
||||
// User input validation error
|
||||
const processedError: InputValidationError = {
|
||||
type: ApolloErrorType.INPUT_VALIDATION_ERROR,
|
||||
error: normalizeError(error),
|
||||
message: normalizeErrorMessage(error.message),
|
||||
path: error.path,
|
||||
invalidArgs: error.extensions.invalidArgs,
|
||||
violations: error.extensions.validationErrors,
|
||||
};
|
||||
|
||||
if (onInputValidationError != null && onInputValidationError(processedError)) {
|
||||
return { unhandledErrors: [], handledErrors: [processedError] };
|
||||
}
|
||||
|
||||
return { unhandledErrors: [processedError], handledErrors: [] };
|
||||
}
|
||||
|
||||
// Other GraphQL resolver error - probably a bug
|
||||
const processedError: ServerError = {
|
||||
type: error.extensions?.code != null ? error.extensions.code : ApolloErrorType.SERVER_ERROR,
|
||||
error: normalizeError(error),
|
||||
message: translateErrorMessage('INTERNAL_SERVER_ERROR', translations ?? defaultErrorMessageTranslations),
|
||||
path: error.path,
|
||||
};
|
||||
|
||||
if (onServerError != null && onServerError(processedError)) {
|
||||
return { unhandledErrors: [], handledErrors: [processedError] };
|
||||
}
|
||||
|
||||
return { unhandledErrors: [processedError], handledErrors: [] };
|
||||
}
|
||||
|
||||
function processNetworkError(
|
||||
error: ApolloError,
|
||||
options: ApolloErrorProcessorOptions = {},
|
||||
): ApolloErrorProcessorResult {
|
||||
options = { ...defaultProcessorOptions, ...options };
|
||||
const { onNetworkError, translations } = options;
|
||||
|
||||
let message: string =
|
||||
error.networkError != null && error.networkError.message != null
|
||||
? normalizeErrorMessage(error.networkError.message)
|
||||
: normalizeErrorMessage(error.message);
|
||||
|
||||
switch (message) {
|
||||
case 'Failed to fetch':
|
||||
message = translateErrorMessage('FAILED_TO_FETCH', translations ?? defaultErrorMessageTranslations);
|
||||
break;
|
||||
}
|
||||
|
||||
const processedError: NetworkError = {
|
||||
type: ApolloErrorType.NETWORK_ERROR,
|
||||
error,
|
||||
statusCode: error.networkError != null ? error.networkError.statusCode : undefined,
|
||||
message,
|
||||
};
|
||||
|
||||
if (onNetworkError != null && onNetworkError(processedError)) {
|
||||
return { unhandledErrors: [], handledErrors: [processedError] };
|
||||
}
|
||||
|
||||
return { unhandledErrors: [processedError], handledErrors: [processedError] };
|
||||
}
|
||||
|
||||
export function processApolloError(
|
||||
error: ApolloError,
|
||||
options: ApolloErrorProcessorOptions = {},
|
||||
): ApolloErrorProcessorResult {
|
||||
options = { ...defaultProcessorOptions, ...options };
|
||||
const { onServerError } = options;
|
||||
|
||||
if (error.graphQLErrors != null && error.graphQLErrors.length > 0) {
|
||||
// Successful request but with errors from the resolver
|
||||
const errorProcessorResults = error.graphQLErrors.map(graphQLError => processGraphQLError(graphQLError, options));
|
||||
|
||||
return {
|
||||
unhandledErrors: errorProcessorResults.flatMap(result => result.unhandledErrors),
|
||||
handledErrors: errorProcessorResults.flatMap(result => result.handledErrors),
|
||||
};
|
||||
}
|
||||
|
||||
if (error.networkError?.result?.errors != null && error.networkError.result.errors.length > 0) {
|
||||
// Network error that contains GraphQL errors inside it. Can occur when server responds with a non-200 status code
|
||||
const errorProcessorResults = error.networkError.result.errors.map(graphQLError =>
|
||||
processGraphQLError(graphQLError, options),
|
||||
);
|
||||
|
||||
return {
|
||||
unhandledErrors: errorProcessorResults.flatMap(result => result.unhandledErrors),
|
||||
handledErrors: errorProcessorResults.flatMap(result => result.handledErrors),
|
||||
};
|
||||
}
|
||||
|
||||
if (error.networkError != null) {
|
||||
// Network error, e.g. server is not responding or some other exception occurs
|
||||
return processNetworkError(error, options);
|
||||
}
|
||||
|
||||
// Some other internal server error
|
||||
const processedError: ServerError = {
|
||||
type: ApolloErrorType.SERVER_ERROR,
|
||||
error,
|
||||
message: error.message,
|
||||
};
|
||||
|
||||
if (onServerError != null && onServerError(processedError)) {
|
||||
return { unhandledErrors: [], handledErrors: [processedError] };
|
||||
}
|
||||
|
||||
return { unhandledErrors: [processedError], handledErrors: [] };
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { GraphQLError as BaseGraphQLError } from 'graphql';
|
||||
import { ApolloError as BaseApolloError } from 'apollo-client';
|
||||
import { Vue } from 'vue/types/vue';
|
||||
import { ApolloOperationContext } from '../types';
|
||||
import { ApolloErrorHandlerResultInterface } from './ApolloErrorHandlerResult';
|
||||
|
||||
export enum ApolloErrorType {
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
SERVER_ERROR = 'SERVER_ERROR',
|
||||
UNAUTHORIZED_ERROR = 'UNAUTHORIZED_ERROR',
|
||||
BAD_USER_INPUT = 'BAD_USER_INPUT',
|
||||
INPUT_VALIDATION_ERROR = 'INPUT_VALIDATION_ERROR',
|
||||
}
|
||||
|
||||
// Upstream type declares networkError as Error, but it can contain additional properties, we override to add these
|
||||
export type ApolloError = BaseApolloError & {
|
||||
networkError: ApolloNetworkError | null;
|
||||
graphQLErrors: GraphQLError[];
|
||||
};
|
||||
|
||||
export interface ApolloNetworkError extends Error {
|
||||
statusCode?: number;
|
||||
response?: any;
|
||||
result?: {
|
||||
errors: GraphQLError[];
|
||||
};
|
||||
}
|
||||
|
||||
export type GraphQLError = Omit<BaseGraphQLError, 'message'> & {
|
||||
message: string | Record<string, any>;
|
||||
};
|
||||
export type ProcessedApolloError =
|
||||
| NetworkError
|
||||
| ServerError
|
||||
| UnauthorizedError
|
||||
| UserInputError
|
||||
| InputValidationError;
|
||||
|
||||
export interface NetworkError {
|
||||
type: ApolloErrorType.NETWORK_ERROR;
|
||||
error: Error;
|
||||
message: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
export interface ServerError {
|
||||
type: ApolloErrorType.SERVER_ERROR;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UnauthorizedError {
|
||||
type: ApolloErrorType.UNAUTHORIZED_ERROR;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UserInputError {
|
||||
type: ApolloErrorType.BAD_USER_INPUT;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface InputValidationError {
|
||||
type: ApolloErrorType.INPUT_VALIDATION_ERROR;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
invalidArgs: string[];
|
||||
violations: ValidationRuleViolation[];
|
||||
}
|
||||
|
||||
export interface ValidationRuleViolation {
|
||||
path: string[];
|
||||
message: string;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export type ApolloOperationErrorHandlerFunction<
|
||||
TError = BaseApolloError,
|
||||
TApp extends Vue = Vue,
|
||||
TContext = ApolloOperationContext,
|
||||
TResult = ApolloErrorHandlerResultInterface,
|
||||
> = (error: TError, app: TApp, context?: TContext) => TResult;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ApolloErrorProcessor } from './ApolloErrorProcessor';
|
||||
import {
|
||||
ApolloError,
|
||||
ApolloErrorHandlerResult,
|
||||
ApolloOperationContext,
|
||||
ApolloOperationErrorHandlerFunction,
|
||||
} from './types';
|
||||
import { Vue } from 'vue/types/vue';
|
||||
|
||||
/**
|
||||
* This is a simple example of an error handler function. You can copy this and implement your own in your application.
|
||||
*/
|
||||
export const handleApolloError: ApolloOperationErrorHandlerFunction<ApolloError, Vue> = (
|
||||
error: ApolloError,
|
||||
app: Vue,
|
||||
context?: ApolloOperationContext,
|
||||
): ApolloErrorHandlerResult => {
|
||||
const processor = new ApolloErrorProcessor(error, app, context ?? {});
|
||||
|
||||
processor.showErrorNotifications();
|
||||
|
||||
return {
|
||||
processedErrors: processor.processedErrors,
|
||||
};
|
||||
};
|
||||
+2
-1
@@ -2,8 +2,9 @@
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from './ApolloErrorProcessor';
|
||||
export * from './mutation';
|
||||
export * from './query';
|
||||
export * from './subscription';
|
||||
export * from './types';
|
||||
export * from './decorator/index';
|
||||
export * from './error/index';
|
||||
|
||||
+24
-21
@@ -6,13 +6,13 @@ import { FetchPolicy, MutationBaseOptions } from 'apollo-client/core/watchQueryO
|
||||
import { Vue } from 'vue/types/vue';
|
||||
import mapValues from 'lodash.mapvalues';
|
||||
import isPlainObject from 'lodash.isplainobject';
|
||||
import { ApolloOperationContext } from './types';
|
||||
import {
|
||||
ApolloErrorHandlerResult,
|
||||
ApolloOperationContext,
|
||||
ApolloErrorHandlerResultInterface,
|
||||
ApolloOperationErrorHandlerFunction,
|
||||
ProcessedApolloError,
|
||||
ValidationRuleViolation,
|
||||
} from './types';
|
||||
} from './error';
|
||||
|
||||
export type ApolloComponentMutationFunction<R = any, TVariables = OperationVariables> = (
|
||||
options: MutationBaseOptions<R, TVariables> &
|
||||
@@ -33,18 +33,17 @@ export type ApolloMutationClient<TResult, TVariables extends OperationVariables>
|
||||
// Mutation function with typings
|
||||
export type MutationOperationFunction<TResult, TVariables extends OperationVariables, TError = ApolloError> = (
|
||||
app: VueAppWithApollo,
|
||||
params: Omit<MutationOperationParams<TVariables, TError>, 'mutation'>,
|
||||
params: Omit<MutationOperationParams<TResult, TVariables, TError>, 'mutation'>,
|
||||
client?: ApolloMutationClient<TResult, TVariables>,
|
||||
) => Promise<MutationResult<TResult>>;
|
||||
|
||||
// Parameters given to a MutationOperationFunction
|
||||
export interface MutationOperationParams<
|
||||
TResult,
|
||||
TVariables extends OperationVariables,
|
||||
TError = ApolloError,
|
||||
TContext = ApolloOperationContext
|
||||
> {
|
||||
mutation: DocumentNode;
|
||||
variables: TVariables;
|
||||
TContext = ApolloOperationContext,
|
||||
> extends MutationOptions<TResult, TVariables> {
|
||||
context?: TContext;
|
||||
onError?: ApolloOperationErrorHandlerFunction<TError>;
|
||||
}
|
||||
@@ -83,19 +82,23 @@ export async function mutateWithErrorHandling<
|
||||
TResult,
|
||||
TVariables extends OperationVariables,
|
||||
TError,
|
||||
TApp extends VueAppWithApollo = VueAppWithApollo
|
||||
TApp extends VueAppWithApollo = VueAppWithApollo,
|
||||
>(
|
||||
app: TApp,
|
||||
{ mutation, variables, onError, context }: MutationOperationParams<TVariables, TError>,
|
||||
params: MutationOperationParams<TResult, TVariables, TError>,
|
||||
client?: ApolloMutationClient<TResult, TVariables>,
|
||||
): Promise<MutationResult<TResult>> {
|
||||
const mutate =
|
||||
client === undefined ? app.$apollo : typeof client === 'function' ? client : client.mutate.bind(client);
|
||||
const mutate: ApolloClientMutationFunction | ApolloComponentMutationFunction =
|
||||
client === undefined
|
||||
? app.$apollo.mutate.bind(app.$apollo)
|
||||
: typeof client === 'function'
|
||||
? client
|
||||
: client.mutate.bind(client);
|
||||
|
||||
try {
|
||||
const result = await mutate({
|
||||
mutation,
|
||||
variables: cleanInput(variables),
|
||||
...params,
|
||||
variables: params.variables != null ? cleanInput(params.variables) : undefined,
|
||||
});
|
||||
|
||||
if (result == null) {
|
||||
@@ -104,12 +107,13 @@ export async function mutateWithErrorHandling<
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
const errorHandlerResult: ApolloErrorHandlerResult | undefined =
|
||||
const { onError, context } = params;
|
||||
const errorHandlerResult: ApolloErrorHandlerResultInterface | undefined =
|
||||
onError != null ? onError(error, app, context) : undefined;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errors: errorHandlerResult?.processedErrors,
|
||||
errors: errorHandlerResult?.allErrors,
|
||||
validationRuleViolations: errorHandlerResult?.validationRuleViolations,
|
||||
};
|
||||
}
|
||||
@@ -119,23 +123,22 @@ export function createMutationFunction<
|
||||
TResult,
|
||||
TVariables extends OperationVariables,
|
||||
TError = ApolloError,
|
||||
TApp extends VueAppWithApollo = VueAppWithApollo
|
||||
TApp extends VueAppWithApollo = VueAppWithApollo,
|
||||
>(
|
||||
mutation: DocumentNode,
|
||||
onError?: ApolloOperationErrorHandlerFunction<TError, TApp>,
|
||||
): MutationOperationFunction<TResult, TVariables, TError> {
|
||||
return (
|
||||
app: TApp,
|
||||
params: Omit<MutationOperationParams<TVariables, TError>, 'mutation'>,
|
||||
params: Omit<MutationOperationParams<TResult, TVariables, TError>, 'mutation'>,
|
||||
client?: ApolloMutationClient<TResult, TVariables>,
|
||||
): Promise<MutationResult<TResult>> => {
|
||||
return mutateWithErrorHandling(
|
||||
app,
|
||||
{
|
||||
mutation,
|
||||
variables: params.variables,
|
||||
onError: params.onError ?? onError,
|
||||
context: params.context,
|
||||
onError,
|
||||
...params,
|
||||
},
|
||||
client,
|
||||
);
|
||||
|
||||
+34
-15
@@ -1,39 +1,58 @@
|
||||
import { VueApolloQueryDefinition, ErrorHandler } from 'vue-apollo/types/options';
|
||||
import { ApolloError, OperationVariables } from 'apollo-client';
|
||||
import { DocumentNode } from 'graphql';
|
||||
import { ErrorHandler, VueApolloQueryDefinition } from 'vue-apollo/types/options';
|
||||
import { Vue } from 'vue/types/vue';
|
||||
import { ApolloOperationErrorHandlerFunction } from './types';
|
||||
import { ApolloOperationErrorHandlerFunction } from './error';
|
||||
import { OverrideAllThis, SubscribeToMoreOptionsPatched } from './types';
|
||||
|
||||
export interface VueApolloQueryDefinitionPatched<TComponent extends Vue = Vue, TResult = any, TVariables = any>
|
||||
extends OverrideAllThis<
|
||||
Omit<VueApolloQueryDefinition<TResult, TVariables>, 'subscribeToMore' | 'variables' | 'loadingKey'>,
|
||||
TComponent
|
||||
> {
|
||||
variables?: ((this: TComponent) => TVariables) | TVariables;
|
||||
subscribeToMore?:
|
||||
| SubscribeToMoreOptionsPatched<TComponent, TResult, TVariables>
|
||||
| Array<SubscribeToMoreOptionsPatched<TComponent, TResult, TVariables>>;
|
||||
loadingKey?: keyof TComponent;
|
||||
|
||||
// added here pending upstream fix for missing types https://github.com/vuejs/vue-apollo/pull/1257
|
||||
throttle?: number;
|
||||
debounce?: number;
|
||||
}
|
||||
|
||||
export type VueApolloSmartQueryErrorHandler<
|
||||
TResult = any,
|
||||
TVariables = OperationVariables,
|
||||
TError = ApolloError,
|
||||
TApp extends Vue = Vue
|
||||
TComponent extends Vue = Vue,
|
||||
> = (
|
||||
error: TError,
|
||||
vm: TApp,
|
||||
vm: TComponent,
|
||||
key: string,
|
||||
type: 'query',
|
||||
options: VueApolloSmartQueryOptions<TResult, TVariables, TError, TApp>,
|
||||
options: VueApolloSmartQueryOptions<TResult, TVariables, TError, TComponent>,
|
||||
) => void;
|
||||
|
||||
// Type of VueApolloQueryDefinition['subscribeToMore'] is incompatible with generated QueryVariables types.
|
||||
// Omitting it here since we don't use it anyway.
|
||||
export type VueApolloSmartQueryOptions<
|
||||
TResult = any,
|
||||
TVariables = OperationVariables,
|
||||
TError = ApolloError,
|
||||
TApp extends Vue = Vue
|
||||
> = Omit<VueApolloQueryDefinition<TResult, TVariables>, 'subscribeToMore' | 'error'> & {
|
||||
error?: VueApolloSmartQueryErrorHandler<TResult, TVariables, TError, TApp>;
|
||||
TComponent extends Vue = Vue,
|
||||
> = VueApolloQueryDefinitionPatched<TComponent, TResult, TVariables> & {
|
||||
error?: VueApolloSmartQueryErrorHandler<TResult, TVariables, TError, TComponent>;
|
||||
};
|
||||
|
||||
export type VueApolloSmartQueryOptionsFunction<TResult, TVariables, TError = ApolloError, TApp extends Vue = Vue> = <
|
||||
TComponent extends Vue = TApp,
|
||||
>(
|
||||
options?: Partial<Omit<VueApolloSmartQueryOptions<TResult, TVariables, TError, TComponent>, 'query'>>,
|
||||
) => VueApolloSmartQueryOptions<TResult, TVariables, TError, TComponent>;
|
||||
|
||||
export function createSmartQueryOptionsFunction<TResult, TVariables, TError = ApolloError, TApp extends Vue = Vue>(
|
||||
query: DocumentNode,
|
||||
onError?: ApolloOperationErrorHandlerFunction<TError, TApp>,
|
||||
): (
|
||||
options?: Partial<VueApolloSmartQueryOptions<TResult, TVariables, TError, TApp>>,
|
||||
) => VueApolloQueryDefinition<TResult, TVariables> {
|
||||
): VueApolloSmartQueryOptionsFunction<TResult, TVariables> {
|
||||
return (options = {}) => {
|
||||
const defaultErrorHandlerFn: VueApolloSmartQueryErrorHandler<TResult, TVariables, TError, TApp> = (
|
||||
error: TError,
|
||||
@@ -54,8 +73,8 @@ export function createSmartQueryOptionsFunction<TResult, TVariables, TError = Ap
|
||||
...options,
|
||||
error:
|
||||
// we have to override vue-apollo types because they are incorrect
|
||||
((options.error as unknown) as ErrorHandler | undefined) ?? onError !== undefined
|
||||
? ((defaultErrorHandlerFn as unknown) as ErrorHandler)
|
||||
(options.error as unknown as ErrorHandler | undefined) ?? onError !== undefined
|
||||
? (defaultErrorHandlerFn as unknown as ErrorHandler)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
+6
-6
@@ -2,13 +2,13 @@ import { ErrorHandler, VueApolloSubscriptionDefinition } from 'vue-apollo/types/
|
||||
import { ApolloError, OperationVariables } from 'apollo-client';
|
||||
import { DocumentNode } from 'graphql';
|
||||
import { Vue } from 'vue/types/vue';
|
||||
import { ApolloOperationErrorHandlerFunction } from './types';
|
||||
import { ApolloOperationErrorHandlerFunction } from './error';
|
||||
|
||||
export type VueApolloSmartSubscriptionErrorHandler<
|
||||
TResult = any,
|
||||
TVariables = OperationVariables,
|
||||
TError = ApolloError,
|
||||
TApp extends Vue = Vue
|
||||
TApp extends Vue = Vue,
|
||||
> = (
|
||||
error: TError,
|
||||
vm: TApp,
|
||||
@@ -21,7 +21,7 @@ export type VueApolloSmartSubscriptionOptions<
|
||||
TResult = any,
|
||||
TVariables = OperationVariables,
|
||||
TError = ApolloError,
|
||||
TApp extends Vue = Vue
|
||||
TApp extends Vue = Vue,
|
||||
> = Omit<VueApolloSubscriptionDefinition<TVariables>, 'error'> & {
|
||||
error?: VueApolloSmartSubscriptionErrorHandler<TResult, TVariables, TError, TApp>;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export function createSmartSubscriptionOptionsFunction<
|
||||
TResult,
|
||||
TVariables,
|
||||
TError = ApolloError,
|
||||
TApp extends Vue = Vue
|
||||
TApp extends Vue = Vue,
|
||||
>(
|
||||
query: DocumentNode,
|
||||
onError?: ApolloOperationErrorHandlerFunction<TError, TApp>,
|
||||
@@ -57,8 +57,8 @@ export function createSmartSubscriptionOptionsFunction<
|
||||
...options,
|
||||
error:
|
||||
// we have to override vue-apollo types because they are incorrect
|
||||
((options.error as unknown) as ErrorHandler | undefined) ?? onError !== undefined
|
||||
? ((defaultErrorHandlerFn as unknown) as ErrorHandler)
|
||||
(options.error as unknown as ErrorHandler | undefined) ?? onError !== undefined
|
||||
? (defaultErrorHandlerFn as unknown as ErrorHandler)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
+26
-90
@@ -1,92 +1,28 @@
|
||||
import { ApolloError as BaseApolloError } from 'apollo-client';
|
||||
import { Vue } from 'vue/types/vue';
|
||||
import { GraphQLError as BaseGraphQLError } from 'graphql';
|
||||
import { VueApolloSubscribeToMoreOptions } from 'vue-apollo/types/options';
|
||||
|
||||
type OverrideThis<F, T> = F extends (...args: infer A) => infer B ? (this: T, ...args: A) => B : F;
|
||||
|
||||
export type OverrideAllThis<O, T> = {
|
||||
[key in keyof O]: OverrideThis<O[key], T>;
|
||||
};
|
||||
|
||||
export type SubscribeToMoreOptionsPatched<TComponent, TResult, TVariables> = OverrideAllThis<
|
||||
Omit<VueApolloSubscribeToMoreOptions<TResult, TVariables>, 'updateQuery' | 'variables'>,
|
||||
TComponent
|
||||
> & {
|
||||
variables?: (this: TComponent) => any;
|
||||
updateQuery?: UpdateQueryFn<TComponent, TResult, any, any>; // TODO: How should we pass subscript data & variables types?
|
||||
};
|
||||
|
||||
type UpdateQueryFn<TComponent = any, TResult = any, TSubscriptionVariables = any, TSubscriptionData = any> = (
|
||||
this: TComponent,
|
||||
previousQueryResult: TResult,
|
||||
options: {
|
||||
subscriptionData: {
|
||||
data: TSubscriptionData;
|
||||
};
|
||||
variables?: TSubscriptionVariables;
|
||||
},
|
||||
) => TResult;
|
||||
|
||||
export type ApolloOperationContext<TAttrs = Record<string, any>> = TAttrs;
|
||||
|
||||
export enum ApolloErrorType {
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
SERVER_ERROR = 'SERVER_ERROR',
|
||||
UNAUTHORIZED_ERROR = 'UNAUTHORIZED_ERROR',
|
||||
BAD_USER_INPUT = 'BAD_USER_INPUT',
|
||||
INPUT_VALIDATION_ERROR = 'INPUT_VALIDATION_ERROR',
|
||||
}
|
||||
|
||||
// Upstream type declares networkError as Error, but it can contain additional properties, we override to add these
|
||||
export type ApolloError = BaseApolloError & {
|
||||
networkError: ApolloNetworkError | null;
|
||||
graphQLErrors: GraphQLError[];
|
||||
};
|
||||
|
||||
export interface ApolloNetworkError extends Error {
|
||||
statusCode?: number;
|
||||
response?: any;
|
||||
result?: {
|
||||
errors: GraphQLError[];
|
||||
};
|
||||
}
|
||||
|
||||
export type GraphQLError = BaseGraphQLError & {
|
||||
message: string | Record<string, any>;
|
||||
};
|
||||
|
||||
export type ProcessedApolloError =
|
||||
| NetworkError
|
||||
| ServerError
|
||||
| UnauthorizedError
|
||||
| UserInputError
|
||||
| InputValidationError;
|
||||
|
||||
export interface NetworkError {
|
||||
type: ApolloErrorType.NETWORK_ERROR;
|
||||
error: Error;
|
||||
message: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
export interface ServerError {
|
||||
type: ApolloErrorType.SERVER_ERROR;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UnauthorizedError {
|
||||
type: ApolloErrorType.UNAUTHORIZED_ERROR;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UserInputError {
|
||||
type: ApolloErrorType.BAD_USER_INPUT;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface InputValidationError {
|
||||
type: ApolloErrorType.INPUT_VALIDATION_ERROR;
|
||||
error: Error;
|
||||
path?: readonly (string | number)[];
|
||||
message: string;
|
||||
invalidArgs: string[];
|
||||
violations: ValidationRuleViolation[];
|
||||
}
|
||||
|
||||
export interface ValidationRuleViolation {
|
||||
path: string[];
|
||||
message: string;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export interface ApolloErrorHandlerResult {
|
||||
processedErrors: ProcessedApolloError[];
|
||||
validationRuleViolations?: ValidationRuleViolation[];
|
||||
}
|
||||
|
||||
export type ApolloOperationErrorHandlerFunction<
|
||||
TError = BaseApolloError,
|
||||
TApp extends Vue = Vue,
|
||||
TContext = ApolloOperationContext
|
||||
> = (error: TError, app: TApp, context?: TContext) => ApolloErrorHandlerResult;
|
||||
|
||||
+4
-13
@@ -15,18 +15,9 @@
|
||||
"strictNullChecks": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./@types"
|
||||
]
|
||||
"paths": {},
|
||||
"types": ["@types/node", "@types/jest", "vue-apollo/types"]
|
||||
},
|
||||
"include":[
|
||||
"src/**/*",
|
||||
"test/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["src/**/*", "test/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user