├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .tool-versions ├── .travis.yml ├── LICENSE.md ├── README.md ├── codecov.yml ├── deploy-docs.sh ├── dist ├── actions │ ├── action.d.ts │ ├── destroy.d.ts │ ├── fetch.d.ts │ ├── index.d.ts │ ├── mutate.d.ts │ ├── persist.d.ts │ ├── push.d.ts │ ├── query.d.ts │ ├── simple-mutation.d.ts │ └── simple-query.d.ts ├── adapters │ ├── adapter.d.ts │ └── builtin │ │ └── default-adapter.d.ts ├── common │ ├── context.d.ts │ └── logger.d.ts ├── graphql │ ├── apollo.d.ts │ ├── introspection-query.d.ts │ ├── query-builder.d.ts │ ├── schema.d.ts │ └── transformer.d.ts ├── index.d.ts ├── orm │ ├── model.d.ts │ └── store.d.ts ├── plugin.d.ts ├── support │ ├── interfaces.d.ts │ └── utils.d.ts ├── test-utils.d.ts ├── vuex-orm-graphql.cjs.js ├── vuex-orm-graphql.d.ts ├── vuex-orm-graphql.esm-bundler.js ├── vuex-orm-graphql.esm.js ├── vuex-orm-graphql.esm.prod.js ├── vuex-orm-graphql.global.js └── vuex-orm-graphql.global.prod.js ├── docs ├── .vuepress │ ├── config.js │ └── public │ │ └── logo-vuex-orm.png ├── README.md └── guide │ ├── README.md │ ├── adapters.md │ ├── connection-mode.md │ ├── contribution.md │ ├── custom-queries.md │ ├── destroy.md │ ├── eager-loading.md │ ├── faq.md │ ├── fetch.md │ ├── meta-fields.md │ ├── nuxt.md │ ├── persist.md │ ├── push.md │ ├── relationships.md │ ├── setup.md │ ├── testing.md │ └── virtual-fields.md ├── package.json ├── rollup.config.js ├── scripts └── build.js ├── src ├── actions │ ├── action.ts │ ├── destroy.ts │ ├── fetch.ts │ ├── index.ts │ ├── mutate.ts │ ├── persist.ts │ ├── push.ts │ ├── query.ts │ ├── simple-mutation.ts │ └── simple-query.ts ├── adapters │ ├── adapter.ts │ └── builtin │ │ └── default-adapter.ts ├── common │ ├── context.ts │ └── logger.ts ├── graphql │ ├── apollo.ts │ ├── introspection-query.ts │ ├── query-builder.ts │ ├── schema.ts │ └── transformer.ts ├── index.ts ├── orm │ ├── model.ts │ └── store.ts ├── plugin.ts ├── support │ ├── interfaces.ts │ └── utils.ts ├── test-utils.ts └── vuex-orm-graphql.ts ├── test ├── bootstrap.js ├── integration │ ├── actions │ │ ├── customMutation.spec.ts │ │ ├── customQuery.spec.ts │ │ ├── deleteAndDestroy.spec.ts │ │ ├── destroy.spec.ts │ │ ├── fetch.spec.ts │ │ ├── persist.spec.ts │ │ ├── push.spec.ts │ │ ├── simpleMutation.spec.ts │ │ └── simpleQuery.spec.ts │ ├── plugin.spec.ts │ ├── relations │ │ ├── has-many-through.spec.ts │ │ ├── many-to-many.spec.ts │ │ ├── one-to-many.spec.ts │ │ ├── one-to-one.spec.ts │ │ ├── polymorphic-has-many.spec.ts │ │ ├── polymorphic-has-one.spec.ts │ │ └── polymorphic-many-to-many.spec.ts │ └── test-utils.spec.ts ├── support │ ├── helpers.ts │ ├── mock-apollo-client.ts │ ├── mock-data.ts │ ├── mock-schema.ts │ └── test-adapter.ts └── unit │ ├── action.spec.ts │ ├── adapter │ └── builtin │ │ └── default-adapter.spec.ts │ ├── apollo.spec.ts │ ├── context.spec.ts │ ├── model.spec.ts │ ├── query-builder.spec.ts │ ├── transformer.spec.ts │ └── utils.spec.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 2 9 | charset = utf-8 10 | max_line_length = 100 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | /.tmp 61 | /lib 62 | 63 | /.idea 64 | 65 | # docs 66 | /docs/.vuepress/dist 67 | 68 | 69 | compiled 70 | .awcache 71 | .rpt2_cache 72 | 73 | /dist/lib 74 | /dist/types 75 | 76 | .DS_Store 77 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 12.16.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | cache: 5 | yarn: true 6 | directories: 7 | - node_modules 8 | script: 9 | - npm test && npm run build 10 | after_success: 11 | - npm run docs:deploy 12 | 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 phortx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Vuex ORM 3 |

4 | 5 |

Vuex ORM Plugin: GraphQL

6 | 7 |

This project is powered by i22 Digitalagentur GmbH

8 | 9 |

10 | 11 | Travis CI 12 | 13 | 14 | JavaScript Style Guide 15 | 16 | 17 | License 18 | 19 |

20 | 21 | Vuex-ORM-GraphQL is a plugin for the amazing [Vuex-ORM](https://github.com/vuex-orm/vuex-orm), which brings 22 | Object-Relational Mapping access to the Vuex Store. Vuex-ORM-GraphQL enhances Vuex-ORM to let you sync your Vuex state 23 | via the Vuex-ORM models with your server via a [GraphQL API](http://graphql.org/). 24 | 25 | The plugin will automatically generate GraphQL queries and mutations based on your model definitions and by 26 | reading your and GraphQL schema from your server. Thus it hides the specifics of Network Communication, GraphQL, 27 | Caching, De- and Serialization of your Data and so on from the developer. Getting a record of a model from the server 28 | is as easy as calling `Product.fetch()`. This allows you to write sophisticated Single-Page Applications fast and 29 | efficient without worrying about GraphQL. 30 | 31 | 32 | ## Documentation 33 | 34 | You can find the complete documentation at https://vuex-orm.github.io/plugin-graphql/. 35 | 36 | 37 | ## Questions & Discussions 38 | 39 | Join us on our [Slack Channel](https://join.slack.com/t/vuex-orm/shared_invite/enQtNDQ0NjE3NTgyOTY2LTc1YTI2N2FjMGRlNGNmMzBkMGZlMmYxOTgzYzkzZDM2OTQ3OGExZDRkN2FmMGQ1MGJlOWM1NjU0MmRiN2VhYzQ) for any questions and discussions. 40 | 41 | While there is the Slack Channel, do not hesitate to open an issue for any question you might have. 42 | We're always more than happy to hear any feedback, and we don't care what kind of form they are. 43 | 44 | 45 | ## Donations 46 | 47 | Support this project by sending a small donation to the developer. 48 | 49 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MF6ST3SXPC4G8) 50 | 51 | 52 | ## License 53 | 54 | Vuex ORM GraphQL is open-sourced software licensed under the [MIT license](https://github.com/phortx/plugin-graphql/blob/master/LICENSE.md). 55 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /deploy-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | yarn run build:docs 8 | 9 | # navigate into the build output directory 10 | cd docs/.vuepress/dist 11 | 12 | # if you are deploying to a custom domain 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # if you are deploying to https://.github.io 20 | # git push -f git@github.com:/.github.io.git master 21 | 22 | # if you are deploying to https://.github.io/ 23 | git push -f git@github.com:vuex-orm/plugin-graphql.git master:gh-pages 24 | 25 | cd - 26 | -------------------------------------------------------------------------------- /dist/actions/action.d.ts: -------------------------------------------------------------------------------- 1 | import { Arguments, Data, DispatchFunction } from "../support/interfaces"; 2 | import Model from "../orm/model"; 3 | import RootState from "@vuex-orm/core/lib/modules/contracts/RootState"; 4 | /** 5 | * Base class for all Vuex actions. Contains some utility and convenience methods. 6 | */ 7 | export default class Action { 8 | /** 9 | * Sends a mutation. 10 | * 11 | * @param {string} name Name of the mutation like 'createUser' 12 | * @param {Data | undefined} variables Variables to send with the mutation 13 | * @param {Function} dispatch Vuex Dispatch method for the model 14 | * @param {Model} model The model this mutation affects. 15 | * @param {boolean} multiple Tells if we're requesting a single record or multiple. 16 | * @returns {Promise} 17 | */ 18 | protected static mutation(name: string, variables: Data | undefined, dispatch: DispatchFunction, model: Model): Promise; 19 | /** 20 | * Convenience method to get the model from the state. 21 | * @param {RootState} state Vuex state 22 | * @returns {Model} 23 | */ 24 | static getModelFromState(state: RootState): Model; 25 | /** 26 | * Makes sure args is a hash. 27 | * 28 | * @param {Arguments|undefined} args 29 | * @param {any} id When not undefined, it's added to the args 30 | * @returns {Arguments} 31 | */ 32 | static prepareArgs(args?: Arguments, id?: any): Arguments; 33 | /** 34 | * Adds the record itself to the args and sends it through transformOutgoingData. Key is named by the singular name 35 | * of the model. 36 | * 37 | * @param {Arguments} args 38 | * @param {Model} model 39 | * @param {Data} data 40 | * @returns {Arguments} 41 | */ 42 | static addRecordToArgs(args: Arguments, model: Model, data: Data): Arguments; 43 | /** 44 | * Transforms each field of the args which contains a model. 45 | * @param {Arguments} args 46 | * @returns {Arguments} 47 | */ 48 | protected static transformArgs(args: Arguments): Arguments; 49 | } 50 | -------------------------------------------------------------------------------- /dist/actions/destroy.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * Destroy action for sending a delete mutation. Will be used for record.$destroy(). 5 | */ 6 | export default class Destroy extends Action { 7 | /** 8 | * Registers the record.$destroy() and record.$deleteAndDestroy() methods and 9 | * the destroy Vuex Action. 10 | */ 11 | static setup(): void; 12 | /** 13 | * @param {State} state The Vuex state 14 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 15 | * @param {number} id ID of the record to delete 16 | * @returns {Promise} true 17 | */ 18 | static call({ state, dispatch }: ActionParams, { id, args }: ActionParams): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /dist/actions/fetch.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, Data } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * Fetch action for sending a query. Will be used for Model.fetch(). 5 | */ 6 | export default class Fetch extends Action { 7 | /** 8 | * Registers the Model.fetch() method and the fetch Vuex Action. 9 | */ 10 | static setup(): void; 11 | /** 12 | * @param {any} state The Vuex state 13 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 14 | * @param {ActionParams} params Optional params to send with the query 15 | * @returns {Promise} The fetched records as hash 16 | */ 17 | static call({ state, dispatch }: ActionParams, params?: ActionParams): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /dist/actions/index.d.ts: -------------------------------------------------------------------------------- 1 | import Action from "./action"; 2 | import Destroy from "./destroy"; 3 | import Fetch from "./fetch"; 4 | import Mutate from "./mutate"; 5 | import Persist from "./persist"; 6 | import Push from "./push"; 7 | import Query from "./query"; 8 | import SimpleQuery from "./simple-query"; 9 | import SimpleMutation from "./simple-mutation"; 10 | export { SimpleQuery, SimpleMutation, Query, Action, Destroy, Fetch, Mutate, Persist, Push }; 11 | -------------------------------------------------------------------------------- /dist/actions/mutate.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, Data } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * Mutate action for sending a custom mutation. Will be used for Model.mutate() and record.$mutate(). 5 | */ 6 | export default class Mutate extends Action { 7 | /** 8 | * Registers the Model.mutate() and the record.$mutate() methods and the mutate Vuex Action. 9 | */ 10 | static setup(): void; 11 | /** 12 | * @param {any} state The Vuex state 13 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 14 | * @param {string} name Name of the query 15 | * @param {boolean} multiple Fetch one or multiple? 16 | * @param {Arguments} args Arguments for the mutation. Must contain a 'mutation' field. 17 | * @returns {Promise} The new record if any 18 | */ 19 | static call({ state, dispatch }: ActionParams, { args, name }: ActionParams): Promise; 20 | } 21 | -------------------------------------------------------------------------------- /dist/actions/persist.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, Data } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * Persist action for sending a create mutation. Will be used for record.$persist(). 5 | */ 6 | export default class Persist extends Action { 7 | /** 8 | * Registers the record.$persist() method and the persist Vuex Action. 9 | */ 10 | static setup(): void; 11 | /** 12 | * @param {any} state The Vuex state 13 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 14 | * @param {number|string} id ID of the record to persist 15 | * @returns {Promise} The saved record 16 | */ 17 | static call({ state, dispatch }: ActionParams, { id, args }: ActionParams): Promise; 18 | /** 19 | * It's very likely that the server generated different ID for this record. 20 | * In this case Action.mutation has inserted a new record instead of updating the existing one. 21 | * 22 | * @param {Model} model 23 | * @param {Data} record 24 | * @returns {Promise} 25 | */ 26 | private static deleteObsoleteRecord; 27 | } 28 | -------------------------------------------------------------------------------- /dist/actions/push.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, Data } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * Push action for sending a update mutation. Will be used for record.$push(). 5 | */ 6 | export default class Push extends Action { 7 | /** 8 | * Registers the record.$push() method and the push Vuex Action. 9 | */ 10 | static setup(): void; 11 | /** 12 | * @param {any} state The Vuex state 13 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 14 | * @param {Arguments} data New data to save 15 | * @param {Arguments} args Additional arguments 16 | * @returns {Promise} The updated record 17 | */ 18 | static call({ state, dispatch }: ActionParams, { data, args }: ActionParams): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /dist/actions/query.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, Data } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * Query action for sending a custom query. Will be used for Model.customQuery() and record.$customQuery. 5 | */ 6 | export default class Query extends Action { 7 | /** 8 | * Registers the Model.customQuery and the record.$customQuery() methods and the 9 | * query Vuex Action. 10 | */ 11 | static setup(): void; 12 | /** 13 | * @param {any} state The Vuex state 14 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 15 | * @param {string} name Name of the query 16 | * @param {boolean} multiple Fetch one or multiple? 17 | * @param {object} filter Filter object (arguments) 18 | * @param {boolean} bypassCache Whether to bypass the cache 19 | * @returns {Promise} The fetched records as hash 20 | */ 21 | static call({ state, dispatch }: ActionParams, { name, filter, bypassCache }: ActionParams): Promise; 22 | } 23 | -------------------------------------------------------------------------------- /dist/actions/simple-mutation.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * SimpleMutation action for sending a model unrelated simple mutation. 5 | */ 6 | export default class SimpleMutation extends Action { 7 | /** 8 | * Registers the Model.simpleMutation() Vuex Root Action. 9 | */ 10 | static setup(): void; 11 | /** 12 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 13 | * @param {string} query The query to send 14 | * @param {Arguments} variables 15 | * @returns {Promise} The result 16 | */ 17 | static call({ dispatch }: ActionParams, { query, variables }: ActionParams): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /dist/actions/simple-query.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams } from "../support/interfaces"; 2 | import Action from "./action"; 3 | /** 4 | * SimpleQuery action for sending a model unrelated simple query. 5 | */ 6 | export default class SimpleQuery extends Action { 7 | /** 8 | * Registers the Model.simpleQuery() Vuex Root Action. 9 | */ 10 | static setup(): void; 11 | /** 12 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 13 | * @param {string} query The query to send 14 | * @param {Arguments} variables 15 | * @param {boolean} bypassCache Whether to bypass the cache 16 | * @returns {Promise} The result 17 | */ 18 | static call({ dispatch }: ActionParams, { query, bypassCache, variables }: ActionParams): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /dist/adapters/adapter.d.ts: -------------------------------------------------------------------------------- 1 | import Model from "../orm/model"; 2 | export declare enum ConnectionMode { 3 | AUTO = 0, 4 | PLAIN = 1, 5 | NODES = 2, 6 | EDGES = 3, 7 | ITEMS = 4 8 | } 9 | export declare enum ArgumentMode { 10 | TYPE = 0, 11 | LIST = 1 12 | } 13 | export default interface Adapter { 14 | getRootQueryName(): string; 15 | getRootMutationName(): string; 16 | getNameForPersist(model: Model): string; 17 | getNameForPush(model: Model): string; 18 | getNameForDestroy(model: Model): string; 19 | getNameForFetch(model: Model, plural: boolean): string; 20 | getConnectionMode(): ConnectionMode; 21 | getArgumentMode(): ArgumentMode; 22 | getFilterTypeName(model: Model): string; 23 | getInputTypeName(model: Model, action?: string): string; 24 | prepareSchemaTypeName(name: string): string; 25 | } 26 | -------------------------------------------------------------------------------- /dist/adapters/builtin/default-adapter.d.ts: -------------------------------------------------------------------------------- 1 | import Adapter, { ConnectionMode, ArgumentMode } from "../adapter"; 2 | import Model from "../../orm/model"; 3 | export default class DefaultAdapter implements Adapter { 4 | getRootMutationName(): string; 5 | getRootQueryName(): string; 6 | getConnectionMode(): ConnectionMode; 7 | getArgumentMode(): ArgumentMode; 8 | getFilterTypeName(model: Model): string; 9 | getInputTypeName(model: Model, action?: string): string; 10 | getNameForDestroy(model: Model): string; 11 | getNameForFetch(model: Model, plural: boolean): string; 12 | getNameForPersist(model: Model): string; 13 | getNameForPush(model: Model): string; 14 | prepareSchemaTypeName(name: string): string; 15 | } 16 | -------------------------------------------------------------------------------- /dist/common/context.d.ts: -------------------------------------------------------------------------------- 1 | import Logger from "./logger"; 2 | import Model from "../orm/model"; 3 | import { PluginComponents } from "@vuex-orm/core/lib/plugins/use"; 4 | import Apollo from "../graphql/apollo"; 5 | import Database from "@vuex-orm/core/lib/database/Database"; 6 | import { Options } from "../support/interfaces"; 7 | import Schema from "../graphql/schema"; 8 | import { Mock, MockOptions } from "../test-utils"; 9 | import Adapter, { ConnectionMode } from "../adapters/adapter"; 10 | /** 11 | * Internal context of the plugin. This class contains all information, the models, database, logger and so on. 12 | * 13 | * It's a singleton class, so just call Context.getInstance() anywhere you need the context. 14 | */ 15 | export default class Context { 16 | /** 17 | * Contains the instance for the singleton pattern. 18 | * @type {Context} 19 | */ 20 | static instance: Context; 21 | /** 22 | * Components collection of Vuex-ORM 23 | * @type {PluginComponents} 24 | */ 25 | readonly components: PluginComponents; 26 | /** 27 | * The options which have been passed to VuexOrm.install 28 | * @type {Options} 29 | */ 30 | readonly options: Options; 31 | /** 32 | * GraphQL Adapter. 33 | * @type {Adapter} 34 | */ 35 | readonly adapter: Adapter; 36 | /** 37 | * The Vuex-ORM database 38 | * @type {Database} 39 | */ 40 | readonly database: Database; 41 | /** 42 | * Collection of all Vuex-ORM models wrapped in a Model instance. 43 | * @type {Map} 44 | */ 45 | readonly models: Map; 46 | /** 47 | * When true, the logging is enabled. 48 | * @type {boolean} 49 | */ 50 | readonly debugMode: boolean; 51 | /** 52 | * Our nice Vuex-ORM-GraphQL logger 53 | * @type {Logger} 54 | */ 55 | readonly logger: Logger; 56 | /** 57 | * Instance of Apollo which cares about the communication with the graphql endpoint. 58 | * @type {Apollo} 59 | */ 60 | apollo: Apollo; 61 | /** 62 | * The graphql schema. Is null until the first request. 63 | * @type {Schema} 64 | */ 65 | schema: Schema | undefined; 66 | /** 67 | * Tells if the schema is already loaded or the loading is currently processed. 68 | * @type {boolean} 69 | */ 70 | private schemaWillBeLoaded; 71 | /** 72 | * Defines how to query connections. 'auto' | 'nodes' | 'edges' | 'plain' | 'items' 73 | */ 74 | connectionMode: ConnectionMode; 75 | /** 76 | * Container for the global mocks. 77 | * @type {Object} 78 | */ 79 | private globalMocks; 80 | /** 81 | * Private constructor, called by the setup method 82 | * 83 | * @constructor 84 | * @param {PluginComponents} components The Vuex-ORM Components collection 85 | * @param {Options} options The options passed to VuexORM.install 86 | */ 87 | private constructor(); 88 | /** 89 | * Get the singleton instance of the context. 90 | * @returns {Context} 91 | */ 92 | static getInstance(): Context; 93 | /** 94 | * This is called only once and creates a new instance of the Context. 95 | * @param {PluginComponents} components The Vuex-ORM Components collection 96 | * @param {Options} options The options passed to VuexORM.install 97 | * @returns {Context} 98 | */ 99 | static setup(components: PluginComponents, options: Options): Context; 100 | loadSchema(): Promise; 101 | processSchema(): void; 102 | /** 103 | * Returns a model from the model collection by it's name 104 | * 105 | * @param {Model|string} model A Model instance, a singular or plural name of the model 106 | * @param {boolean} allowNull When true this method returns null instead of throwing an exception when no model was 107 | * found. Default is false 108 | * @returns {Model} 109 | */ 110 | getModel(model: Model | string, allowNull?: boolean): Model; 111 | /** 112 | * Will add a mock for simple mutations or queries. These are model unrelated and have to be 113 | * handled globally. 114 | * 115 | * @param {Mock} mock - Mock config. 116 | */ 117 | addGlobalMock(mock: Mock): boolean; 118 | /** 119 | * Finds a global mock for the given action and options. 120 | * 121 | * @param {string} action - Name of the action like 'simpleQuery' or 'simpleMutation'. 122 | * @param {MockOptions} options - MockOptions like { name: 'example' }. 123 | * @returns {Mock | null} null when no mock was found. 124 | */ 125 | findGlobalMock(action: string, options: MockOptions | undefined): Mock | null; 126 | /** 127 | * Hook to be called by simpleMutation and simpleQuery actions in order to get the global mock 128 | * returnValue. 129 | * 130 | * @param {string} action - Name of the action like 'simpleQuery' or 'simpleMutation'. 131 | * @param {MockOptions} options - MockOptions. 132 | * @returns {any} null when no mock was found. 133 | */ 134 | globalMockHook(action: string, options: MockOptions): any; 135 | /** 136 | * Wraps all Vuex-ORM entities in a Model object and saves them into this.models 137 | */ 138 | private collectModels; 139 | } 140 | -------------------------------------------------------------------------------- /dist/common/logger.d.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from "graphql/language/ast"; 2 | import { Arguments } from "../support/interfaces"; 3 | import { FetchPolicy } from "apollo-client"; 4 | /** 5 | * Vuex-ORM-Apollo Debug Logger. 6 | * Wraps console and only logs if enabled. 7 | * 8 | * Also contains some methods to format graphql queries for the output 9 | */ 10 | export default class Logger { 11 | /** 12 | * Tells if any logging should happen 13 | * @type {boolean} 14 | */ 15 | private readonly enabled; 16 | /** 17 | * Fancy Vuex-ORM-Apollo prefix for all log messages. 18 | * @type {string[]} 19 | */ 20 | private readonly PREFIX; 21 | /** 22 | * @constructor 23 | * @param {boolean} enabled Tells if any logging should happen 24 | */ 25 | constructor(enabled: boolean); 26 | /** 27 | * Wraps console.group. In TEST env console.log is used instead because console.group doesn't work on CLI. 28 | * If available console.groupCollapsed will be used instead. 29 | * @param {Array} messages 30 | */ 31 | group(...messages: Array): void; 32 | /** 33 | * Wrapper for console.groupEnd. In TEST env nothing happens because console.groupEnd doesn't work on CLI. 34 | */ 35 | groupEnd(): void; 36 | /** 37 | * Wrapper for console.log. 38 | * @param {Array} messages 39 | */ 40 | log(...messages: Array): void; 41 | /** 42 | * Wrapper for console.warn. 43 | * @param {Array} messages 44 | */ 45 | warn(...messages: Array): void; 46 | /** 47 | * Logs a graphql query in a readable format and with all information like fetch policy and variables. 48 | * @param {string | DocumentNode} query 49 | * @param {Arguments} variables 50 | * @param {FetchPolicy} fetchPolicy 51 | */ 52 | logQuery(query: string | DocumentNode, variables?: Arguments, fetchPolicy?: FetchPolicy): void; 53 | } 54 | -------------------------------------------------------------------------------- /dist/graphql/apollo.d.ts: -------------------------------------------------------------------------------- 1 | import { Arguments, Data } from "../support/interfaces"; 2 | import Model from "../orm/model"; 3 | /** 4 | * This class takes care of the communication with the graphql endpoint by leveraging the awesome apollo-client lib. 5 | */ 6 | export default class Apollo { 7 | /** 8 | * The http link instance to use. 9 | * @type {HttpLink} 10 | */ 11 | private readonly httpLink; 12 | /** 13 | * The ApolloClient instance 14 | * @type {ApolloClient} 15 | */ 16 | private readonly apolloClient; 17 | /** 18 | * @constructor 19 | */ 20 | constructor(); 21 | /** 22 | * Sends a request to the GraphQL API via apollo 23 | * @param model 24 | * @param {any} query The query to send (result from gql()) 25 | * @param {Arguments} variables Optional. The variables to send with the query 26 | * @param {boolean} mutation Optional. If this is a mutation (true) or a query (false, default) 27 | * @param {boolean} bypassCache If true the query will be send to the server without using the cache. For queries only 28 | * @returns {Promise} The new records 29 | */ 30 | request(model: Model, query: any, variables?: Arguments, mutation?: boolean, bypassCache?: boolean): Promise; 31 | simpleQuery(query: string, variables: Arguments, bypassCache?: boolean, context?: Data): Promise; 32 | simpleMutation(query: string, variables: Arguments, context?: Data): Promise; 33 | private static getHeaders; 34 | } 35 | -------------------------------------------------------------------------------- /dist/graphql/introspection-query.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: "\nquery Introspection {\n __schema {\n types {\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n name\n description\n type {\n name\n kind\n\n ofType {\n kind\n\n name\n ofType {\n kind\n name\n\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n\n type {\n name\n kind\n\n ofType {\n kind\n\n name\n ofType {\n kind\n name\n\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n\n inputFields {\n name\n description\n type {\n name\n kind\n\n ofType {\n kind\n\n name\n ofType {\n kind\n name\n\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /dist/graphql/query-builder.d.ts: -------------------------------------------------------------------------------- 1 | import Model from "../orm/model"; 2 | import { Arguments, GraphQLField } from "../support/interfaces"; 3 | /** 4 | * Contains all logic to build GraphQL queries/mutations. 5 | */ 6 | export default class QueryBuilder { 7 | /** 8 | * Builds a field for the GraphQL query and a specific model 9 | * 10 | * @param {Model|string} model The model to use 11 | * @param {boolean} multiple Determines whether plural/nodes syntax or singular syntax is used. 12 | * @param {Arguments} args The args that will be passed to the query field ( user(role: $role) ) 13 | * @param {Array} path The relations in this list are ignored (while traversing relations). 14 | * Mainly for recursion 15 | * @param {string} name Optional name of the field. If not provided, this will be the model name 16 | * @param filter 17 | * @param {boolean} allowIdFields Optional. Determines if id fields will be ignored for the argument generation. 18 | * See buildArguments 19 | * @returns {string} 20 | * 21 | * @todo Do we need the allowIdFields param? 22 | */ 23 | static buildField(model: Model | string, multiple?: boolean, args?: Arguments, path?: Array, name?: string, filter?: boolean, allowIdFields?: boolean): string; 24 | /** 25 | * Generates a query. 26 | * Currently only one root field for the query is possible. 27 | * @param {string} type 'mutation' or 'query' 28 | * @param {Model | string} model The model this query or mutation affects. This mainly determines the query fields. 29 | * @param {string} name Optional name of the query/mutation. Will overwrite the name from the model. 30 | * @param {Arguments} args Arguments for the query 31 | * @param {boolean} multiple Determines if the root query field is a connection or not (will be passed to buildField) 32 | * @param {boolean} filter When true the query arguments are passed via a filter object. 33 | * @returns {any} Whatever gql() returns 34 | */ 35 | static buildQuery(type: string, model: Model | string, name?: string, args?: Arguments, multiple?: boolean, filter?: boolean): import("graphql").DocumentNode; 36 | /** 37 | * Generates the arguments string for a graphql query based on a given map. 38 | * 39 | * There are three types of arguments: 40 | * 41 | * 1) Signatures with primitive types (signature = true) 42 | * => 'mutation createUser($name: String!)' 43 | * 44 | * 2) Signatures with object types (signature = true, args = { user: { __type: 'User' }}) 45 | * => 'mutation createUser($user: UserInput!)' 46 | * 47 | * 3) Fields with variables (signature = false) 48 | * => 'user(id: $id)' 49 | * 50 | * 4) Filter fields with variables (signature = false, filter = true) 51 | * => 'users(filter: { active: $active })' 52 | * 53 | * @param model 54 | * @param {Arguments | undefined} args 55 | * @param {boolean} signature When true, then this method generates a query signature instead of key/value pairs 56 | * @param filter 57 | * @param {boolean} allowIdFields If true, ID fields will be included in the arguments list 58 | * @param {GraphQLField} field Optional. The GraphQL mutation or query field 59 | * @returns {String} 60 | */ 61 | static buildArguments(model: Model, args?: Arguments, signature?: boolean, filter?: boolean, allowIdFields?: boolean, field?: GraphQLField | null): string; 62 | /** 63 | * Determines the GraphQL primitive type of a field in the variables hash by the field type or (when 64 | * the field type is generic attribute) by the variable type. 65 | * @param {Model} model 66 | * @param {string} key 67 | * @param {string} value 68 | * @param {GraphQLField} query Pass when we have to detect the type of an argument 69 | * @returns {string} 70 | */ 71 | static determineAttributeType(model: Model, key: string, value: any, query?: GraphQLField): string; 72 | private static findSchemaFieldForArgument; 73 | /** 74 | * Generates the fields for all related models. 75 | * 76 | * @param {Model} model 77 | * @param {Array} path 78 | * @returns {string} 79 | */ 80 | static buildRelationsQuery(model: null | Model, path?: Array): string; 81 | private static prepareArguments; 82 | } 83 | -------------------------------------------------------------------------------- /dist/graphql/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLField, GraphQLSchema, GraphQLType, GraphQLTypeDefinition } from "../support/interfaces"; 2 | import { ConnectionMode } from "../adapters/adapter"; 3 | export default class Schema { 4 | private schema; 5 | private types; 6 | private mutations; 7 | private queries; 8 | constructor(schema: GraphQLSchema); 9 | determineQueryMode(): ConnectionMode; 10 | getType(name: string, allowNull?: boolean): GraphQLType | null; 11 | getMutation(name: string, allowNull?: boolean): GraphQLField | null; 12 | getQuery(name: string, allowNull?: boolean): GraphQLField | null; 13 | static returnsConnection(field: GraphQLField): boolean; 14 | static getRealType(type: GraphQLTypeDefinition): GraphQLTypeDefinition; 15 | static getTypeNameOfField(field: GraphQLField): string; 16 | } 17 | -------------------------------------------------------------------------------- /dist/graphql/transformer.d.ts: -------------------------------------------------------------------------------- 1 | import { Data } from "../support/interfaces"; 2 | import Model from "../orm/model"; 3 | /** 4 | * This class provides methods to transform incoming data from GraphQL in to a format Vuex-ORM understands and 5 | * vice versa. 6 | */ 7 | export default class Transformer { 8 | /** 9 | * Transforms outgoing data. Use for variables param. 10 | * 11 | * @param {Model} model Base model of the mutation/query 12 | * @param {Data} data Data to transform 13 | * @param {boolean} read Tells if this is a write or a read action. read is fetch, write is push and persist. 14 | * @param {Array} whitelist of fields 15 | * @param {Map>} outgoingRecords List of record IDs that are already added to the 16 | * outgoing data in order to detect recursion. 17 | * @param {boolean} recursiveCall Tells if it's a recursive call. 18 | * @returns {Data} 19 | */ 20 | static transformOutgoingData(model: Model, data: Data, read: boolean, whitelist?: Array, outgoingRecords?: Map>, recursiveCall?: boolean): Data; 21 | /** 22 | * Transforms a set of incoming data to the format vuex-orm requires. 23 | * 24 | * @param {Data | Array} data 25 | * @param model 26 | * @param mutation required to transform something like `disableUserAddress` to the actual model name. 27 | * @param {boolean} recursiveCall 28 | * @returns {Data} 29 | */ 30 | static transformIncomingData(data: Data | Array, model: Model, mutation?: boolean, recursiveCall?: boolean): Data; 31 | /** 32 | * Tells if a field should be included in the outgoing data. 33 | * @param {boolean} forFilter Tells whether a filter is constructed or not. 34 | * @param {string} fieldName Name of the field to check. 35 | * @param {any} value Value of the field. 36 | * @param {Model} model Model class which contains the field. 37 | * @param {Array|undefined} whitelist Contains a list of fields which should always be included. 38 | * @returns {boolean} 39 | */ 40 | static shouldIncludeOutgoingField(forFilter: boolean, fieldName: string, value: any, model: Model, whitelist?: Array): boolean; 41 | /** 42 | * Tells whether a field is in the input type. 43 | * @param {Model} model 44 | * @param {string} fieldName 45 | */ 46 | private static inputTypeContainsField; 47 | /** 48 | * Registers a record for recursion detection. 49 | * @param {Map>} records Map of IDs. 50 | * @param {ORMModel} record The record to register. 51 | */ 52 | private static addRecordForRecursionDetection; 53 | /** 54 | * Detects recursions. 55 | * @param {Map>} records Map of IDs. 56 | * @param {ORMModel} record The record to check. 57 | * @return {boolean} true when the record is already included in the records. 58 | */ 59 | private static isRecursion; 60 | } 61 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import VuexORMGraphQLPlugin from "./plugin"; 2 | import DefaultAdapter from "./adapters/builtin/default-adapter"; 3 | import Adapter, { ConnectionMode, ArgumentMode } from "./adapters/adapter"; 4 | import Model from "./orm/model"; 5 | export default VuexORMGraphQLPlugin; 6 | export { setupTestUtils, mock, clearORMStore, Mock, MockOptions, ReturnValue } from "./test-utils"; 7 | export { DefaultAdapter, Adapter, ConnectionMode, ArgumentMode, Model }; 8 | -------------------------------------------------------------------------------- /dist/orm/model.d.ts: -------------------------------------------------------------------------------- 1 | import { Model as ORMModel, Relation } from "@vuex-orm/core"; 2 | import { Field, PatchedModel } from "../support/interfaces"; 3 | import { Mock, MockOptions } from "../test-utils"; 4 | /** 5 | * Wrapper around a Vuex-ORM model with some useful methods. 6 | * 7 | * Also provides a mock system, to define mocking responses for actions. 8 | */ 9 | export default class Model { 10 | /** 11 | * The singular name of a model like `blogPost` 12 | * @type {string} 13 | */ 14 | readonly singularName: string; 15 | /** 16 | * The plural name of a model like `blogPosts` 17 | * @type {string} 18 | */ 19 | readonly pluralName: string; 20 | /** 21 | * The original Vuex-ORM model 22 | */ 23 | readonly baseModel: typeof PatchedModel; 24 | /** 25 | * The fields of the model 26 | * @type {Map} 27 | */ 28 | readonly fields: Map; 29 | /** 30 | * Container for the mocks. 31 | * @type {Object} 32 | */ 33 | private mocks; 34 | /** 35 | * @constructor 36 | * @param {Model} baseModel The original Vuex-ORM model 37 | */ 38 | constructor(baseModel: typeof ORMModel); 39 | /** 40 | * Tells if a field is a numeric field. 41 | * 42 | * @param {Field | undefined} field 43 | * @returns {boolean} 44 | */ 45 | static isFieldNumber(field: Field | undefined): boolean; 46 | /** 47 | * Tells if a field is a attribute (and thus not a relation) 48 | * @param {Field} field 49 | * @returns {boolean} 50 | */ 51 | static isFieldAttribute(field: Field): boolean; 52 | /** 53 | * Tells if a field which represents a relation is a connection (multiple). 54 | * @param {Field} field 55 | * @returns {boolean} 56 | */ 57 | static isConnection(field: Field): boolean; 58 | /** 59 | * Adds $isPersisted and other meta fields to the model. 60 | * 61 | * TODO: This feels rather hacky currently and may break anytime the internal structure of 62 | * the core changes. Maybe in the future there will be a feature in the core that allows to add 63 | * those meta fields by plugins. 64 | * @param {Model} model 65 | */ 66 | static augment(model: Model): void; 67 | /** 68 | * Returns the related model for a relation. 69 | * @param {Field|undefined} relation Relation field. 70 | * @returns {Model|null} 71 | */ 72 | static getRelatedModel(relation?: Relation): Model | null; 73 | /** 74 | * Returns all fields which should be included in a graphql query: All attributes which are not included in the 75 | * skipFields array or start with $. 76 | * @returns {Array} field names which should be queried 77 | */ 78 | getQueryFields(): Array; 79 | /** 80 | * Tells if a field should be ignored. This is true for fields that start with a `$` or is it is within the skipField 81 | * property or is the foreignKey of a belongsTo/hasOne relation. 82 | * 83 | * @param {string} field 84 | * @returns {boolean} 85 | */ 86 | skipField(field: string): boolean; 87 | /** 88 | * @returns {Map} all relations of the model. 89 | */ 90 | getRelations(): Map; 91 | /** 92 | * This accepts a field like `subjectType` and checks if this is just randomly named `...Type` or it is part 93 | * of a polymorphic relation. 94 | * @param {string} name 95 | * @returns {boolean} 96 | */ 97 | isTypeFieldOfPolymorphicRelation(name: string): boolean; 98 | /** 99 | * Returns a record of this model with the given ID. 100 | * @param {number|string} id 101 | * @returns {any} 102 | */ 103 | getRecordWithId(id: number | string): import("@vuex-orm/core").Item; 104 | /** 105 | * Determines if we should eager load (means: add as a field in the graphql query) a related entity. belongsTo, 106 | * hasOne and morphOne related entities are always eager loaded. Others can be added to the `eagerLoad` array 107 | * or `eagerSync` of the model. 108 | * 109 | * @param {string} fieldName Name of the field 110 | * @param {Relation} relation Relation field 111 | * @param {Model} relatedModel Related model 112 | * @returns {boolean} 113 | */ 114 | shouldEagerLoadRelation(fieldName: string, relation: Relation, relatedModel: Model): boolean; 115 | /** 116 | * Determines if we should eager save (means: add as a field in the graphql mutation) a related entity. belongsTo 117 | * related entities are always eager saved. Others can be added to the `eagerSave` or `eagerSync` array of the model. 118 | * 119 | * @param {string} fieldName Name of the field 120 | * @param {Relation} relation Relation field 121 | * @param {Model} relatedModel Related model 122 | * @returns {boolean} 123 | */ 124 | shouldEagerSaveRelation(fieldName: string, relation: Relation, relatedModel: Model): boolean; 125 | /** 126 | * Adds a mock. 127 | * 128 | * @param {Mock} mock - Mock config. 129 | * @returns {boolean} 130 | */ 131 | $addMock(mock: Mock): boolean; 132 | /** 133 | * Finds a mock for the given action and options. 134 | * 135 | * @param {string} action - Name of the action like 'fetch'. 136 | * @param {MockOptions} options - MockOptions like { variables: { id: 42 } }. 137 | * @returns {Mock | null} null when no mock was found. 138 | */ 139 | $findMock(action: string, options: MockOptions | undefined): Mock | null; 140 | /** 141 | * Hook to be called by all actions in order to get the mock returnValue. 142 | * 143 | * @param {string} action - Name of the action like 'fetch'. 144 | * @param {MockOptions} options - MockOptions. 145 | * @returns {any} null when no mock was found. 146 | */ 147 | $mockHook(action: string, options: MockOptions): any; 148 | } 149 | -------------------------------------------------------------------------------- /dist/orm/store.d.ts: -------------------------------------------------------------------------------- 1 | import { Data, DispatchFunction } from "../support/interfaces"; 2 | /** 3 | * Provides some helper methods to interact with the Vuex-ORM store 4 | */ 5 | export declare class Store { 6 | /** 7 | * Inserts incoming data into the store. Existing data will be updated. 8 | * 9 | * @param {Data} data New data to insert/update 10 | * @param {Function} dispatch Vuex Dispatch method for the model 11 | * @return {Promise} Inserted data as hash 12 | */ 13 | static insertData(data: Data, dispatch: DispatchFunction): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /dist/plugin.d.ts: -------------------------------------------------------------------------------- 1 | import VuexORMGraphQL from "./vuex-orm-graphql"; 2 | import { PluginComponents, Plugin } from "@vuex-orm/core/lib/plugins/use"; 3 | import { Options } from "./support/interfaces"; 4 | /** 5 | * Plugin class. This just provides a static install method for Vuex-ORM and stores the instance of the model 6 | * within this.instance. 7 | */ 8 | export default class VuexORMGraphQLPlugin implements Plugin { 9 | /** 10 | * Contains the instance of VuexORMGraphQL 11 | */ 12 | static instance: VuexORMGraphQL; 13 | /** 14 | * This is called, when VuexORM.install(VuexOrmGraphQL, options) is called. 15 | * 16 | * @param {PluginComponents} components The Vuex-ORM Components collection 17 | * @param {Options} options The options passed to VuexORM.install 18 | * @returns {VuexORMGraphQL} 19 | */ 20 | static install(components: PluginComponents, options: Options): VuexORMGraphQL; 21 | } 22 | -------------------------------------------------------------------------------- /dist/support/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | import { Database, Model as ORMModel } from "@vuex-orm/core"; 2 | import ORMInstance from "@vuex-orm/core/lib/data/Instance"; 3 | import RootState from "@vuex-orm/core/lib/modules/contracts/RootState"; 4 | import { ApolloLink } from "apollo-link"; 5 | import { DocumentNode } from "graphql/language/ast"; 6 | import Adapter from "../adapters/adapter"; 7 | export declare type DispatchFunction = (action: string, data: Data) => Promise; 8 | export interface Options { 9 | apolloClient: any; 10 | database: Database; 11 | url?: string; 12 | headers?: { 13 | [index: string]: any; 14 | }; 15 | credentials?: string; 16 | useGETForQueries?: boolean; 17 | debug?: boolean; 18 | link?: ApolloLink; 19 | adapter?: Adapter; 20 | } 21 | export interface ActionParams { 22 | commit?: any; 23 | dispatch?: DispatchFunction; 24 | getters?: any; 25 | rootGetters?: any; 26 | rootState?: any; 27 | state?: RootState; 28 | filter?: Filter; 29 | id?: string; 30 | data?: Data; 31 | args?: Arguments; 32 | variables?: Arguments; 33 | bypassCache?: boolean; 34 | query?: string | DocumentNode; 35 | multiple?: boolean; 36 | name?: string; 37 | } 38 | export interface Data extends ORMInstance { 39 | [index: string]: any; 40 | } 41 | export interface Filter { 42 | [index: string]: any; 43 | } 44 | export interface Arguments { 45 | [index: string]: any; 46 | } 47 | export interface GraphQLType { 48 | description: string; 49 | name: string; 50 | fields?: Array; 51 | inputFields?: Array; 52 | } 53 | export interface GraphQLField { 54 | description: string; 55 | name: string; 56 | args: Array; 57 | type: GraphQLTypeDefinition; 58 | } 59 | export interface GraphQLTypeDefinition { 60 | kind: string; 61 | name?: string; 62 | ofType: GraphQLTypeDefinition; 63 | } 64 | export interface GraphQLSchema { 65 | types: Array; 66 | } 67 | export interface Field { 68 | related?: typeof ORMModel; 69 | parent?: typeof ORMModel; 70 | localKey?: string; 71 | foreignKey?: string; 72 | } 73 | export declare class PatchedModel extends ORMModel { 74 | static eagerLoad?: Array; 75 | static eagerSave?: Array; 76 | static eagerSync?: Array; 77 | static skipFields?: Array; 78 | $isPersisted: boolean; 79 | static fetch(filter?: any, bypassCache?: boolean): Promise; 80 | static mutate(params: ActionParams): Promise; 81 | static customQuery(params: ActionParams): Promise; 82 | $mutate(params: ActionParams): Promise; 83 | $customQuery(params: ActionParams): Promise; 84 | $persist(args?: any): Promise; 85 | $push(args?: any): Promise; 86 | $destroy(): Promise; 87 | $deleteAndDestroy(): Promise; 88 | } 89 | -------------------------------------------------------------------------------- /dist/support/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from "graphql/language/ast"; 2 | export declare const pluralize: any; 3 | export declare const singularize: any; 4 | /** 5 | * Capitalizes the first letter of the given string. 6 | * 7 | * @param {string} input 8 | * @returns {string} 9 | */ 10 | export declare function upcaseFirstLetter(input: string): string; 11 | /** 12 | * Down cases the first letter of the given string. 13 | * 14 | * @param {string} input 15 | * @returns {string} 16 | */ 17 | export declare function downcaseFirstLetter(input: string): string; 18 | /** 19 | * Takes a string with a graphql query and formats it. Useful for debug output and the tests. 20 | * @param {string} query 21 | * @returns {string} 22 | */ 23 | export declare function prettify(query: string | DocumentNode): string; 24 | /** 25 | * Returns a parsed query as GraphQL AST DocumentNode. 26 | * 27 | * @param {string | DocumentNode} query - Query as string or GraphQL AST DocumentNode. 28 | * 29 | * @returns {DocumentNode} Query as GraphQL AST DocumentNode. 30 | */ 31 | export declare function parseQuery(query: string | DocumentNode): DocumentNode; 32 | /** 33 | * @param {DocumentNode} query - The GraphQL AST DocumentNode. 34 | * 35 | * @returns {string} the GraphQL query within a DocumentNode as a plain string. 36 | */ 37 | export declare function graphQlDocumentToString(query: DocumentNode): string; 38 | /** 39 | * Tells if a object is just a simple object. 40 | * 41 | * @param {any} obj - Value to check. 42 | */ 43 | export declare function isPlainObject(obj: any): boolean; 44 | /** 45 | * Creates an object composed of the picked `object` properties. 46 | * @param {object} object - Object. 47 | * @param {array} props - Properties to pick. 48 | */ 49 | export declare function pick(object: any, props: Array): {}; 50 | export declare function isEqual(a: object, b: object): boolean; 51 | export declare function clone(input: any): any; 52 | export declare function takeWhile(array: Array, predicate: (x: any, idx: number, array: Array) => any): any[]; 53 | export declare function matches(source: any): (object: any) => boolean; 54 | export declare function removeSymbols(input: any): any; 55 | /** 56 | * Converts the argument into a number. 57 | */ 58 | export declare function toPrimaryKey(input: string | number | null): number | string; 59 | export declare function isGuid(value: string): boolean; 60 | -------------------------------------------------------------------------------- /dist/test-utils.d.ts: -------------------------------------------------------------------------------- 1 | import { Model as ORMModel } from "@vuex-orm/core"; 2 | import VuexORMGraphQLPlugin from "./index"; 3 | export declare function setupTestUtils(plugin: typeof VuexORMGraphQLPlugin): void; 4 | export interface MockOptions { 5 | [key: string]: any; 6 | } 7 | declare type ReturnObject = { 8 | [key: string]: any; 9 | }; 10 | export declare type ReturnValue = (() => ReturnObject | Array) | ReturnObject | Array; 11 | export declare class Mock { 12 | readonly action: string; 13 | readonly options?: MockOptions; 14 | modelClass?: typeof ORMModel; 15 | returnValue?: ReturnValue; 16 | constructor(action: string, options?: MockOptions); 17 | for(modelClass: typeof ORMModel): Mock; 18 | andReturn(returnValue: ReturnValue): Mock; 19 | private installMock; 20 | } 21 | export declare function clearORMStore(): Promise; 22 | export declare function mock(action: string, options?: MockOptions): Mock; 23 | export {}; 24 | -------------------------------------------------------------------------------- /dist/vuex-orm-graphql.d.ts: -------------------------------------------------------------------------------- 1 | import { Options } from "./support/interfaces"; 2 | import Context from "./common/context"; 3 | import { PluginComponents } from "@vuex-orm/core/lib/plugins/use"; 4 | /** 5 | * Main class of the plugin. Setups the internal context, Vuex actions and model methods 6 | */ 7 | export default class VuexORMGraphQL { 8 | /** 9 | * @constructor 10 | * @param {PluginComponents} components The Vuex-ORM Components collection 11 | * @param {Options} options The options passed to VuexORM.install 12 | */ 13 | constructor(components: PluginComponents, options: Options); 14 | /** 15 | * Allow everything to read the context. 16 | */ 17 | getContext(): Context; 18 | /** 19 | * This method will setup: 20 | * - Vuex actions: fetch, persist, push, destroy, mutate 21 | * - Model methods: fetch(), mutate(), customQuery() 22 | * - Record method: $mutate(), $persist(), $push(), $destroy(), $deleteAndDestroy(), $customQuery() 23 | */ 24 | private static setupActions; 25 | } 26 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Vuex-ORM GraphQL Plugin', 3 | description: 'Vue + Vuex-ORM + GraphQL = <3', 4 | 5 | base: '/plugin-graphql/', 6 | 7 | themeConfig: { 8 | logo: '/logo-vuex-orm.png', 9 | 10 | repo: 'https://github.com/vuex-orm/plugin-graphql', 11 | docsDir: 'docs', 12 | docsBranch: 'master', 13 | editLinks: true, 14 | 15 | markdown: { 16 | lineNumbers: true 17 | }, 18 | 19 | nav: [ 20 | { text: 'Home', link: '/' }, 21 | { text: 'Guide', link: '/guide/' }, 22 | { text: 'Vuex-ORM', link: 'https://vuex-orm.github.io/vuex-orm/' }, 23 | ], 24 | 25 | sidebar: [ 26 | ['/guide/', 'Introduction'], 27 | 28 | { 29 | title: 'Basics', 30 | collapsable: false, 31 | children: [ 32 | '/guide/setup', 33 | '/guide/fetch', 34 | '/guide/persist', 35 | '/guide/push', 36 | '/guide/destroy', 37 | '/guide/relationships', 38 | ] 39 | }, 40 | 41 | { 42 | title: 'Advanced Topics', 43 | collapsable: false, 44 | children: [ 45 | '/guide/nuxt', 46 | '/guide/adapters', 47 | '/guide/connection-mode', 48 | '/guide/custom-queries', 49 | '/guide/eager-loading', 50 | '/guide/virtual-fields', 51 | '/guide/meta-fields', 52 | '/guide/testing', 53 | ] 54 | }, 55 | 56 | { 57 | title: 'Meta', 58 | collapsable: false, 59 | children: [ 60 | '/guide/faq', 61 | '/guide/contribution', 62 | ] 63 | }, 64 | ], 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /docs/.vuepress/public/logo-vuex-orm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuex-orm/plugin-graphql/b6a878d4d8aed66a8634eb846f3894cac4d2e65f/docs/.vuepress/public/logo-vuex-orm.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /logo-vuex-orm.png 4 | actionText: Get Started → 5 | actionLink: /guide/ 6 | features: 7 | - title: GraphQL 8 | details: This Vuex-ORM plugin uses the apollo-client to let you sync your Vuex-ORM state with a GraphQL API. 9 | - title: Vuex-ORM 10 | details: The plugin keeps up with the API and design of Vuex-ORM with full reactivity. 11 | - title: No boilerplate 12 | details: Setup and usage of this plugin is kept as simple as possible, no clutter in your components. 13 | footer: MIT Licensed 14 | --- 15 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Vuex-ORM/Plugin-GraphQL is a plugin for the amazing [Vuex-ORM](https://github.com/vuex-orm/vuex-orm), which brings 4 | Object-Relational Mapping access to the Vuex Store. Vuex-ORM-GraphQL enhances Vuex-ORM to let you sync your Vuex state 5 | via the Vuex-ORM models with your server via a [GraphQL API](http://graphql.org/). 6 | 7 | The plugin will automatically generate GraphQL queries and mutations on the fly based on your model definitions and by 8 | reading the GraphQL schema from your server. Thus it hides the specifics of Network Communication, GraphQL, 9 | Caching, De- and Serialization of your data, and so on from the developer. Getting a record of a model from the server 10 | is as easy as calling `Product.fetch()`. This allows you to write sophisticated Single-Page Applications fast and 11 | efficient without worrying about GraphQL. 12 | 13 | 14 | ::: warning 15 | You should have basic knowledge of [GraphQL](http://graphql.org/), [Vue](https://vuejs.org/), 16 | [Vuex](https://vuex.vuejs.org/) and [Vuex-ORM](https://vuex-orm.github.io/vuex-orm/) before reading this documentation. 17 | ::: 18 | 19 | 20 | --- 21 | 22 | 23 | [[toc]] 24 | 25 | 26 | ## Actions 27 | 28 | While using Vuex-ORM with the GraphQL plugin you have to distinguish between two types of Vuex actions: 29 | 30 | - Vuex-ORM actions: Retrieve data from or save data to Vuex (`Vue Component <--> Vuex Store`) 31 | - Persistence actions: Load data from or persist data to the GraphQL API (`Vuex Store <--> GraphQL Server`) 32 | 33 | The following table lists all actions and what they do: 34 | 35 | CRUD | Vuex Only | Persist to GraphQL API 36 | --| -- | -- 37 | **R**EAD | [`find()`](https://vuex-orm.github.io/vuex-orm/guide/store/retrieving-data.html#get-single-data), [`all()`](https://vuex-orm.github.io/vuex-orm/guide/store/retrieving-data.html#get-all-data), [`query()`](https://vuex-orm.github.io/vuex-orm/guide/store/retrieving-data.html#query-builder) | [`fetch()`](fetch.md) 38 | **C**REATE | [`create()`](https://vuex-orm.github.io/vuex-orm/guide/store/inserting-and-updating-data.html#inserts) or [`insert()`](https://vuex-orm.github.io/vuex-orm/guide/store/inserting-and-updating-data.html#inserts) | [`$persist()`](persist.md) 39 | **U**PDATE | [`$update()`](https://vuex-orm.github.io/vuex-orm/guide/store/inserting-and-updating-data.html#updates) | [`$push()`](push.md) 40 | **D**ELETE | [`$delete()`](https://vuex-orm.github.io/vuex-orm/guide/store/deleting-data.html) | [`$destroy()`](destroy.md) 41 | 42 | See the example below to get an idea of how this plugin interacts with Vuex-ORM. 43 | 44 | 45 | 46 | 47 | ## Example usage 48 | 49 | After [installing](setup.md) this plugin you can load data in your component: 50 | 51 | ```vue 52 | 61 | 62 | 63 | 91 | ``` 92 | 93 | ::: tip 94 | This plugin works with the [Apollo Dev Tools](https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm)! 95 | ::: 96 | 97 | 98 | ## Adapters 99 | 100 | It seems that there are several standards within the GraphQL community how the schema is designed. 101 | Some do connections via a `nodes` field, some via a `edges { nodes }` query and some do neither of them. 102 | Some work with Input Types and some with plain parameter lists. This plugin supports all of them via 103 | a adapter pattern. There is a default adapter and all example queries in this documentation are 104 | generated by the DefaultAdapter but that doesn't mean the queries are tied to this format. 105 | When you have to customize it, you'll find how to do so in the [respective chapter](adapters.md). 106 | 107 | 108 | ## License 109 | 110 | Vuex-ORM-GraphQL is open-sourced software licensed under the 111 | [MIT license](https://github.com/phortx/vuex-orm-graphql/blob/master/LICENSE.md). 112 | -------------------------------------------------------------------------------- /docs/guide/adapters.md: -------------------------------------------------------------------------------- 1 | # Adapters 2 | 3 | [[toc]] 4 | 5 | There is not single true way to design a GraphQL schema and thus there are 6 | some small differences between the implementations, however this plugin has to automatically 7 | generate GraphQL queries, has to parse the schema and de-/serialize the data. Thus we needed a way 8 | to customize how this plugin should behave and communicate with the API. For this we implemented an 9 | adapter pattern, which allows you to setup your own adapter and customize it. 10 | 11 | 12 | ## Basics 13 | 14 | Every adapter has to implement the `Adapter` interface (when your're working with TypeScript). 15 | However it's easier to just inherit from the DefaultAdapter: 16 | 17 | `data/custom-adapter.js`: 18 | ```javascript 19 | import { DefaultAdapter, ConnectionMode } from '@vuex-orm/plugin-graphql'; 20 | 21 | export default class CustomAdapter extends DefaultAdapter { 22 | // Your code here 23 | 24 | // Example 25 | getConnectionMode() { 26 | return ConnectionMode.PLAIN 27 | } 28 | } 29 | ``` 30 | 31 | Then register this adapter when setting up the plugin: 32 | 33 | `data/store.js`: 34 | ```javascript 35 | import CustomAdapter from './custom-adapter.ts'; 36 | 37 | // ... 38 | 39 | VuexORM.use(VuexORMGraphQL, { 40 | database, 41 | adapter: new CustomAdapter(), 42 | }); 43 | ``` 44 | 45 | 46 | That's it. In the next sections you can read what and how you can customize the adapter. 47 | 48 | 49 | ## Methods 50 | 51 | Each Adapter has to implement a bunch of methods. Here is the list of the currently supported 52 | method signatures and their value in the default adapter: 53 | 54 | - `getRootQueryName(): string;` 55 | - Returns the name of the type all query types inherit. 56 | - Default adapter value: `Query` 57 | 58 | - `getRootMutationName(): string;` 59 | - Returns the name of the type all mutation types inherit. 60 | - Default adapter value: `Mutation` 61 | 62 | - `getNameForPersist(model: Model): string;` 63 | - Returns the mutation name for persisting (creating) a record. 64 | - Default adapter value example: `createPost` 65 | - `model` is a instance of [Model](https://github.com/vuex-orm/plugin-graphql/blob/master/src/orm/model.ts) 66 | 67 | - `getNameForPush(model: Model): string;` 68 | - Returns the mutation name for pushing (updating) a record. 69 | - Default adapter value example: `updatePost` 70 | - `model` is a instance of [Model](https://github.com/vuex-orm/plugin-graphql/blob/master/src/orm/model.ts) 71 | 72 | - `getNameForDestroy(model: Model): string;` 73 | - Returns the mutation name for destroying a record. 74 | - Default adapter value example: `deletePost` 75 | - `model` is a instance of [Model](https://github.com/vuex-orm/plugin-graphql/blob/master/src/orm/model.ts) 76 | 77 | - `getNameForFetch(model: Model, plural: boolean): string;` 78 | - Returns the query field for fetching a record. 79 | - Default adapter value example: `posts` or `post` 80 | - `model` is a instance of [Model](https://github.com/vuex-orm/plugin-graphql/blob/master/src/orm/model.ts) 81 | - `plural` tells if one or multiple records (connection) are fetched. 82 | 83 | - `getConnectionMode(): ConnectionMode;` 84 | - Returns the [ConnectionMode](connection-mode.md). 85 | - Default adapter value: `AUTO` 86 | 87 | - `getArgumentMode(): ArgumentMode;` 88 | - Returns the ArgumentMode for filtering and inputs (push, persist). 89 | - Default adapter value: `TYPE` 90 | 91 | - `getFilterTypeName(model: Model): string;` 92 | - Returns the name of the filter type for a model. 93 | - `model` is a instance of [Model](https://github.com/vuex-orm/plugin-graphql/blob/master/src/orm/model.ts) 94 | - Default adapter value example: `PostFilter` 95 | 96 | - `getInputTypeName(model: Model, action?: string): string;` 97 | - Returns the name of the input type for a model. 98 | - `model` is a instance of [Model](https://github.com/vuex-orm/plugin-graphql/blob/master/src/orm/model.ts) 99 | - Default adapter value example: `PostInput` 100 | 101 | 102 | ## ArgumentMode 103 | 104 | The `getArgumentMode()` methods determines the ArgumentMode, which knows to options: `LIST` and `TYPE`. 105 | It tells the plugin how arguments should be passed to queries and mutations. 106 | 107 | 108 | ### `TYPE` 109 | 110 | `TYPE` is the value in the default adapter and causes the plugin to use a `Input` or `Filter` type: 111 | 112 | For `$persist()`: 113 | ``` 114 | mutation CreatePost($post: PostInput!) { 115 | createPost(post: $post) { 116 | ... 117 | } 118 | } 119 | ``` 120 | 121 | For `fetch()`: 122 | ``` 123 | query Posts($title: String!) { 124 | posts(filter: {title: $title}) { 125 | ... 126 | } 127 | } 128 | ``` 129 | 130 | 131 | ### `LIST` 132 | 133 | `LIST` causes the plugin to use plain lists: 134 | 135 | For `$persist()`: 136 | ``` 137 | mutation CreatePost($id: ID!, $authorId: ID!, $title: String!, $content: String!) { 138 | createPost(id: $id, authorId: $authorId, title: $title, content: $content) { 139 | ... 140 | } 141 | } 142 | ``` 143 | 144 | For `fetch()`: 145 | ``` 146 | query Posts($title: String!) { 147 | posts(title: $title) { 148 | ... 149 | } 150 | } 151 | ``` 152 | -------------------------------------------------------------------------------- /docs/guide/connection-mode.md: -------------------------------------------------------------------------------- 1 | # Connection Mode 2 | 3 | [[toc]] 4 | 5 | It seems that there are several standards within the GraphQL community how connections (fields that returns multiple 6 | records) are designed. Some do this via a `nodes` field, some via a `edges { nodes }` query and some do neither of them. 7 | Vuex-ORM-GraphQL tries to be flexible and supports all of them. 8 | 9 | There are four possible modes: `AUTO`, `NODES`, `EDGES`, `PLAIN`, `ITEMS`. The Adapter you use will tell the 10 | plugin which ConnectionMode to use. In the DefaultAdapter this is `AUTO`. 11 | 12 | 13 | ## Automatic detection 14 | 15 | The plugin will try to detect automatically which mode should be used by analyzing the GraphQL 16 | Schema. In the best case you don't have to bother with this at all. 17 | 18 | 19 | ## Manual setting 20 | 21 | In rare cases the automatic detection might fail or report the wrong mode. In this case, you can 22 | manually set the connection mode via a custom adapter. The modes and the resulting 23 | queries are explained in the next sections. 24 | 25 | ## Mode 1: `plain` 26 | 27 | The third mode is the less preferred one due to the lack of meta information. In this case we just plain pass the field 28 | queries: 29 | 30 | ``` 31 | query Users { 32 | users { 33 | id 34 | email 35 | name 36 | } 37 | } 38 | ``` 39 | 40 | ## Mode 2: `nodes` 41 | 42 | This is the preferred mode and used for the example queries in this documentation. Setting the connection mode to 43 | `nodes` (or letting the plugin auto detect this mode) will lead to the following query when calling `User.fetch()`: 44 | 45 | ``` 46 | query Users { 47 | users { 48 | nodes { 49 | id 50 | email 51 | name 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | 58 | ## Mode 3: `edges` 59 | 60 | This mode uses a `edges` not to query the edge an then query the `node` within that edge: 61 | 62 | ``` 63 | query Users { 64 | users { 65 | edges { 66 | node { 67 | id 68 | email 69 | name 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## Mode 4: `items` 77 | 78 | This is the mode used for handling the shape of AWS AppSync queries. Using `items` (or letting the plugin auto detect this mode) will lead to the following query when calling `User.fetch()`: 79 | 80 | ``` 81 | query Users { 82 | users { 83 | items { 84 | id 85 | email 86 | name 87 | } 88 | } 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/guide/contribution.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | [[toc]] 4 | 5 | To test this plugin in your existing project, you can use `yarn link` functionality. Run `yarn link` 6 | in your local plugin-graphql directory and run `yarn link @vuex-orm/plugin-graphql` in your project dir. 7 | 8 | Remember to run `yarn build` in your plugin-graphql directory and then again `yarn link` in your project after you have 9 | made changes to the plugin code. You probably have also to restart your webpack server. 10 | 11 | 12 | ```bash 13 | $ yarn build 14 | ``` 15 | 16 | Compile files and generate bundles in dist directory. 17 | 18 | ```bash 19 | $ yarn lint 20 | ``` 21 | 22 | Lint files using a rule of Standard JS. 23 | 24 | ```bash 25 | $ yarn test 26 | ``` 27 | 28 | Run the test using Jest and prints a coverage report. 29 | 30 | 31 | ```bash 32 | yarn build:docs 33 | ``` 34 | 35 | Builds the documentation. 36 | 37 | 38 | ```bash 39 | yarn docs:dev 40 | ``` 41 | 42 | Spawns a server for the documentation. 43 | 44 | 45 | ```bash 46 | yarn docs:depoy 47 | ``` 48 | 49 | Deploys the documentation. 50 | -------------------------------------------------------------------------------- /docs/guide/destroy.md: -------------------------------------------------------------------------------- 1 | # destroy 2 | 3 | [[toc]] 4 | 5 | 6 | ## $destroy() 7 | 8 | Last thing you can do with a record is to delete it on the server after deleting (`delete` action) it on the client via 9 | Vuex-ORM. For this use case we have the `destroy` action. 10 | 11 | Via calling 12 | 13 | ```javascript 14 | await post.$destroy(); 15 | // or 16 | await post.$dispatch('destroy', { id: post.id }); 17 | ``` 18 | 19 | the following GraphQL query is generated: 20 | 21 | 22 | ```graphql 23 | mutation DeletePost($id: ID!) { 24 | deletePost(id: $id) { 25 | id 26 | title 27 | content 28 | 29 | user { 30 | id 31 | email 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | Variables: 38 | 39 | ```json 40 | { 41 | "id": "42" 42 | } 43 | ``` 44 | 45 | ## $deleteAndDestroy() 46 | 47 | You can also use the `$deleteAndDestroy()` action to delete the record from the store and from the server. It's just a 48 | short convenience method for `$delete()` and `$destroy()`. 49 | -------------------------------------------------------------------------------- /docs/guide/eager-loading.md: -------------------------------------------------------------------------------- 1 | # Eager Loading and Saving 2 | 3 | [[toc]] 4 | 5 | 6 | ## Eager Loading 7 | 8 | All `belongsTo`, `hasOne` and `morphOne` related records are eager loaded when `fetch` is called. 9 | All other related records have to be added to a static field in the model called `eagerLoad` to 10 | have them eagerly loaded with fetch. 11 | 12 | Example: 13 | 14 | ```javascript{3} 15 | class User extends Model { 16 | static entity = 'users'; 17 | static eagerLoad = ['posts']; 18 | 19 | static fields () { 20 | return { 21 | id: this.uid(), 22 | name: this.string(''), 23 | 24 | posts: this.hasMany(Post, 'userId') 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | ## Eager Saving 31 | 32 | Similar to the eager loading there is a "eager saving". When saving (via `$persist` or `$push`) a 33 | record will automatically sends all `belongsTo` related records too to the server. 34 | 35 | All other related records have to be added to a static field in the model called `eagerSave` to 36 | have them eagerly saved with persist and push. 37 | 38 | ```javascript{4} 39 | class User extends Model { 40 | static entity = 'users'; 41 | static eagerLoad = ['posts']; 42 | static eagerSave = ['posts']; 43 | 44 | static fields () { 45 | return { 46 | id: this.uid(), 47 | name: this.string(''), 48 | 49 | posts: this.hasMany(Post, 'userId') 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | 56 | ## Eager Syncing 57 | 58 | `eagerSync` combines these two fields. Adding a relation to this array will make it eagerly loaded 59 | and saved: 60 | 61 | 62 | ```javascript{3} 63 | class User extends Model { 64 | static entity = 'users'; 65 | static eagerSync = ['posts']; 66 | 67 | static fields () { 68 | return { 69 | id: this.uid(), 70 | name: this.string(''), 71 | 72 | posts: this.hasMany(Post, 'userId') 73 | } 74 | } 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/guide/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | [[toc]] 4 | 5 | ## WTF? 6 | 7 | Good question, glad you asked! Maybe [this article](https://dev.to/phortx/vue-3-graphql-kj6) helps you. If not, feel 8 | free to join us on our [Slack Channel](https://join.slack.com/t/vuex-orm/shared_invite/enQtMzY5MzczMzI2OTgyLTAwOWEwOGRjOTFmMzZlYzdmZTJhZGU2NGFiY2U2NzBjOWE4Y2FiMWJkMjYxMTAzZDk0ZjAxNTgzZjZhY2VkZDQ) 9 | for any questions and discussions. 10 | 11 | 12 | ## Does Vuex-ORM-GraphQL know my GraphQL Schema? 13 | 14 | Yes, it does! Before the first query is sent, the plugin loads the schema from the GraphQL server and extracts different 15 | information like the types of the fields to use, which fields to ignore, because they're not in the schema and whether 16 | custom queries and mutations return a connection or a record. 17 | 18 | Further it detects differences between the Vuex-ORM model definitions and the schema. 19 | 20 | In the future there will probably be more smart consulting of your GraphQL schema, we're open for suggestions. 21 | 22 | 23 | ## Is this plugin nativescript-vue compatible? 24 | 25 | Yes, since version `0.0.38`. 26 | 27 | 28 | ## What is `await`? 29 | 30 | It's a nice way to work with promises. See https://javascript.info/async-await. 31 | 32 | 33 | ## Does it support Nuxt/SSR? 34 | 35 | Since Version 1.0.0.RC.21 there is experimental support for SSR. You will need 36 | [node-fetch](https://www.npmjs.com/package/node-fetch) and register it via `global.fetch = fetch;` 37 | in order to make it work. 38 | -------------------------------------------------------------------------------- /docs/guide/fetch.md: -------------------------------------------------------------------------------- 1 | # fetch 2 | 3 | [[toc]] 4 | 5 | The `fetch` action is for loading data from the GraphQL API into your Vuex-Store. 6 | 7 | 8 | ## Fetching all records 9 | 10 | The simplest way to call the fetch 11 | method is without any arguments. This will query all records from the GraphQL API: 12 | 13 | ```javascript 14 | await Comment.fetch(); 15 | // or 16 | await Comment.dispatch('fetch') 17 | ``` 18 | 19 | This produces the following GraphQL query: 20 | 21 | ```graphql 22 | query Comments { 23 | comments { 24 | nodes { 25 | id 26 | content 27 | postId 28 | userId 29 | 30 | user { 31 | id 32 | email 33 | } 34 | 35 | post { 36 | id 37 | content 38 | title 39 | 40 | user { 41 | id 42 | email 43 | } 44 | } 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | When you look in the store after a fetch, you'll find that there is the comment, comment author user, post and posts 51 | author user loaded. 52 | 53 | So using the regular Vuex-ORM getters should work out of the box now: 54 | 55 | ```javascript 56 | const comments = Comment.query().withAll().all(); 57 | ``` 58 | 59 | When fetching all returned records replace the respective existing records in the Vuex-ORM database. 60 | 61 | 62 | ## Fetching a single record 63 | 64 | You can also fetch single records via ID: 65 | 66 | ```javascript 67 | await Comment.fetch('42'); 68 | // or 69 | await Comment.fetch({ id: '42' }); 70 | // or 71 | await Comment.dispatch('fetch', { filter: { id: '42' }}) 72 | ``` 73 | 74 | It automatically recognizes, that you're requesting a single record and sends a GraphQL Query for a single record: 75 | 76 | ```graphql 77 | query Comment($id: ID!) { 78 | comment(id: $id) { 79 | id 80 | content 81 | postId 82 | userId 83 | 84 | user { 85 | id 86 | email 87 | } 88 | 89 | post { 90 | id 91 | content 92 | title 93 | 94 | user { 95 | id 96 | email 97 | } 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | 104 | ## Filtering 105 | 106 | Additionally you can pass a filter object to the fetch action like this: 107 | 108 | ```javascript 109 | await Comment.fetch({ postId: '15', deleted: false }); 110 | // or 111 | await Comment.dispatch('fetch', { filter: { postId: '15', deleted: false }}) 112 | ``` 113 | 114 | This will generate the following GraphQL query: 115 | 116 | ```graphql 117 | query Comments($postId: ID!, $deleted: Boolean!) { 118 | comments(filter: { postId: $postId, deleted: $deleted }) { 119 | nodes { 120 | id 121 | content 122 | postId 123 | userId 124 | 125 | user { 126 | id 127 | email 128 | } 129 | 130 | post { 131 | id 132 | content 133 | title 134 | 135 | user { 136 | id 137 | email 138 | } 139 | } 140 | } 141 | } 142 | } 143 | ``` 144 | 145 | 146 | ## Usage with vue-router 147 | 148 | When you use vue-router, you should call your fetch actions in the page component after the navigation. Also we highly 149 | recommend the usage of async/await. 150 | 151 | ```vue 152 | 158 | 159 | 185 | ``` 186 | 187 | 188 | ## Caching 189 | 190 | The plugin caches same queries. To bypass caching set the second param of the `fetch` action to `true` 191 | when using the convenience method or add `bypassCache: true` to the arguments of the `dispatch()` call 192 | 193 | ```javascript 194 | await Comment.fetch({ id: '42' }, true ); 195 | // Or 196 | await Comment.dispatch('fetch', { filter: { id: '42' }, bypassCache: true }) 197 | ``` 198 | -------------------------------------------------------------------------------- /docs/guide/meta-fields.md: -------------------------------------------------------------------------------- 1 | # Meta Fields 2 | 3 | [[toc]] 4 | 5 | This plugin currently adds a special property to your models: `$isPersisted`, which represents if this record is 6 | persisted on the server. It's true for all records except newly created ones. 7 | 8 | More fields may come in future releases. 9 | -------------------------------------------------------------------------------- /docs/guide/nuxt.md: -------------------------------------------------------------------------------- 1 | # Nuxt.js / SSR Integration 2 | 3 | [[toc]] 4 | 5 | Since Version 1.0.0.RC.21 there is support for SSR. The following example shows how to setup 6 | Vuex-ORM and Plugin GraphQL with Nuxt. 7 | 8 | `nuxt.config.js`: 9 | 10 | ```javascript 11 | export default { 12 | plugins: [ 13 | '~/plugins/graphql', 14 | '~/plugins/vuex-orm' 15 | ] 16 | }; 17 | ``` 18 | 19 | `/store/index.js`: 20 | 21 | ```javascript 22 | export const state = () => ({}); 23 | ``` 24 | 25 | 26 | `/plugins/vuex-orm.js`: 27 | 28 | ```javascript 29 | import VuexORM from '@vuex-orm/core'; 30 | import database from '~/data/database'; 31 | 32 | export default ({ store }) => { 33 | VuexORM.install(database)(store); 34 | }; 35 | 36 | ``` 37 | 38 | `/data/database.js`: 39 | 40 | ```javascript 41 | import { Database } from '@vuex-orm/core'; 42 | import User from '~/data/models/user'; 43 | // ... 44 | 45 | const database = new Database(); 46 | database.register(User); 47 | // ... 48 | 49 | export default database; 50 | 51 | ``` 52 | 53 | 54 | `/plugins/graphql.js`: 55 | 56 | ```javascript 57 | import VuexORM from '@vuex-orm/core'; 58 | import VuexORMGraphQL from '@vuex-orm/plugin-graphql'; 59 | import { HttpLink } from 'apollo-link-http'; 60 | import fetch from 'node-fetch'; 61 | import database from '~/data/database'; 62 | 63 | // The url can be anything, in this example we use the value from dotenv 64 | export default function({ app, env: { url } }) { 65 | const apolloClient = app?.apolloProvider?.defaultClient 66 | const options = { database, url }; 67 | 68 | if (apolloClient) { 69 | options.apolloClient = apolloClient 70 | } else { 71 | options.link = new HttpLink({ uri: options.url, fetch }); 72 | } 73 | 74 | VuexORM.use(VuexORMGraphQL, options); 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/guide/persist.md: -------------------------------------------------------------------------------- 1 | # persist 2 | 3 | [[toc]] 4 | 5 | 6 | After creating a new record via Vuex-ORM you may want to save it to your server via GraphQL. For this use case we have 7 | the `persist` action. 8 | 9 | Via calling 10 | 11 | ```javascript 12 | await Post.create({ data: { 13 | content: 'Lorem Ipsum dolor sit amet', 14 | title: 'Example Post', 15 | user: User.query().first() 16 | }}); 17 | 18 | const post = Post.query().first(); 19 | 20 | await post.$persist(); 21 | // or 22 | await post.$dispatch('persist', { id: post.id }); 23 | ``` 24 | 25 | the post record is send to the GraphQL by generating the following query: 26 | 27 | 28 | ```graphql 29 | mutation CreatePost($post: PostInput!) { 30 | createPost(post: $post) { 31 | id 32 | userId 33 | content 34 | title 35 | 36 | user { 37 | id 38 | email 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | Variables: 45 | 46 | ```json 47 | { 48 | "post": { 49 | "id": "42", 50 | "userId": "15", 51 | "content": "Lorem Ipsum dolor sit amet", 52 | "title": "Example Post", 53 | "user": { 54 | "id": "15", 55 | "email": "example@example.com" 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | Like when pushing, all records which are returned replace the respective existing records in the Vuex-ORM database. 62 | 63 | 64 | ## Additional variables 65 | 66 | You can pass a object like this: `$perist({ captchaToken: 'asdfasdf' })`. All fields in the object will be passed as 67 | variables to the mutation. 68 | 69 | 70 | ## Relationships 71 | 72 | When persisting a record, all `belongsTo` relations are sent to the server too. `hasMany`/`hasOne` 73 | relations on the other hand are filtered out and have to be persisted on their own. 74 | -------------------------------------------------------------------------------- /docs/guide/push.md: -------------------------------------------------------------------------------- 1 | # push 2 | 3 | [[toc]] 4 | 5 | After fetching (`fetch` action) and changing (`save` action) a record via Vuex-ORM you probably want to save it back to 6 | your server via GraphQL. For this use case we have the `push` action. 7 | 8 | Via calling 9 | 10 | ```javascript 11 | const post = Post.query().first(); 12 | await post.$push(); 13 | // or 14 | await post.$dispatch('push', { data: post }); 15 | ``` 16 | 17 | the post record is send to the GraphQL by generating the following query: 18 | 19 | 20 | ```graphql 21 | mutation UpdatePost($id: ID!, $post: PostInput!) { 22 | updatePost(id: $id, post: $post) { 23 | id 24 | userId 25 | content 26 | title 27 | 28 | user { 29 | id 30 | email 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | Variables: 37 | 38 | ```json 39 | { 40 | "id": "42", 41 | "post": { 42 | "id": "42", 43 | "userId": "15", 44 | "content": "Some more exciting content!", 45 | "title": "Not a example post", 46 | "user": { 47 | "id": "15", 48 | "email": "example@example.com" 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | Like when persisting, all records which are returned replace the respective existing records in the Vuex-ORM database. 55 | 56 | 57 | 58 | ## Additional variables 59 | 60 | You can pass a object like this: `$push({ captchaToken: 'asdfasdf' })`. All fields in the object will be passed as 61 | variables to the mutation. 62 | 63 | 64 | ## Relationships 65 | 66 | When pushing a record, all `belongsTo` relations are sent to the server too. `hasMany`/`hasOne` 67 | relations on the other hand are filtered out and have to be persisted on their own. 68 | -------------------------------------------------------------------------------- /docs/guide/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | [[toc]] 4 | 5 | 6 | ## Installation 7 | 8 | Installation of the GraphQL plugin is easy. First add the package to your dependencies: 9 | 10 | ```bash 11 | $ yarn add @vuex-orm/plugin-graphql 12 | ``` 13 | 14 | or 15 | 16 | ```bash 17 | $ npm install --save @vuex-orm/plugin-graphql 18 | ``` 19 | 20 | 21 | After that we setup the plugin. Add this after [registering your models to the database](https://vuex-orm.github.io/vuex-orm/guide/prologue/getting-started.html#register-models-and-modules-to-the-vuex-store): 22 | 23 | ```javascript 24 | import VuexORMGraphQL from '@vuex-orm/plugin-graphql'; 25 | VuexORM.use(VuexORMGraphQL, { database }); 26 | ``` 27 | 28 | ## Possible options 29 | 30 | These are the possible options to pass when calling `VuexORM.use()`: 31 | - `apolloClient` (optional): Provide a preconfigured instance of the Apollo client. See [client](#client) 32 | - `database` (required): The Vuex-ORM database. 33 | - `debug` (optional, default: `false`): Set to true to activate the debug logging. 34 | - `url` (optional, default: `/graphql`): The URL to the graphql api. Will be passed to apollo-client. 35 | - `headers` (optional, default: `{}`) HTTP Headers. See 36 | [apollo-link-http](https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-http#options). 37 | This can be a static object or a function, returning a object, which will be called before a request is made. 38 | - `credentials` (optional, default: `same-origin`) Credentials Policy. See [apollo-link-http](https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-http#options) 39 | - `useGETForQueries` (optional, default: `false`) Use GET for queries (not for mutations). See [apollo-link-http](https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-http#options) 40 | - `adapter` (optional, default: `DefaultAdapter`). See [Adapters](adapters.md) 41 | 42 | ::: tip 43 | We recommend to activate the debug mode in development env automatically via: 44 | ```javascript 45 | { debug: process.env.NODE_ENV !== 'production' } 46 | ``` 47 | ::: 48 | 49 | ## Client 50 | 51 | You can inject your own instance of the Apollo Client using `option.apolloClient`. This is useful if 52 | the app requires a more complex configuration, such as integration with AWS AppSync. When `apolloClient` 53 | is used, `plugin-graphql` ignores any other options to configure Apollo client. 54 | 55 | Here is an example configuration for AWS AppSync: 56 | 57 | ```js 58 | import VuexORM from '@vuex-orm/core' 59 | import AWSAppSyncClient from 'aws-appsync' 60 | import { Auth } from 'aws-amplify' 61 | import VuexORMGraphQL from '@vuex-orm/plugin-graphql' 62 | 63 | import database from '../database' 64 | import awsexports from '../aws-exports' 65 | 66 | const options = { 67 | defaultOptions: { 68 | watchQuery: { 69 | fetchPolicy: 'cache-and-network' 70 | } 71 | }, 72 | connectionQueryMode: 'items', 73 | database: database, 74 | url: awsexports.aws_appsync_graphqlEndpoint, 75 | includeExtensions: true, 76 | debug: process.env.NODE_ENV !== 'production' 77 | } 78 | 79 | const config = { 80 | url: awsexports.aws_appsync_graphqlEndpoint, 81 | region: awsexports.aws_appsync_region, 82 | auth: { 83 | type: awsexports.aws_appsync_authenticationType, 84 | jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken() 85 | } 86 | } 87 | 88 | const client = new AWSAppSyncClient(config, options) 89 | 90 | options.apolloClient = client 91 | 92 | VuexORM.use(VuexORMGraphQL, options) 93 | 94 | export const plugins = [ 95 | VuexORM.install(database) 96 | ] 97 | ``` 98 | -------------------------------------------------------------------------------- /docs/guide/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | [[toc]] 4 | 5 | ## Unit Testing 6 | 7 | To unit test vue components which use the persistence actions of this plugin, you need to mock 8 | the results of the GraphQL queries. The GraphQL plugin offers some utils to do this. 9 | 10 | First we have to import the mock method from the test utils via 11 | 12 | ```js 13 | import VuexORMGraphQL from '@vuex-orm/plugin-graphql'; 14 | import { setupTestUtils, mock } from '@vuex-orm/plugin-graphql'; 15 | ``` 16 | 17 | After that we have to setup the test utils, this is very easy, just pass the imported VuexORMGraphQL 18 | plugin like this: 19 | 20 | ``` 21 | setupTestUtils(VuexORMGraphQL); 22 | ``` 23 | 24 | Now we're ready to go. In the next step we can setup mocks via 25 | 26 | ```js 27 | mock('fetch').for(User).andReturn({ id: '1', name: 'Charlie Brown' }); 28 | ``` 29 | 30 | This means: Whenever `User.fetch()` is called, insert `{ id: '1', name: 'Charlie Brown' }` in the Vuex-ORM 31 | store. 32 | 33 | The mock method also accepts a second param which allows to match specific calls. Only those 34 | properties which are within the given object are considered while matching. Example: 35 | 36 | ```js 37 | // Will only trigger when `User.fetch('17')` is called 38 | mock('fetch', { filter: { id: '17' } }).for(User).andReturn({ id: '17', name: 'Charlie Brown' }); 39 | 40 | // Will only trigger when `User.fetch({ filter: { active: true }})` is called 41 | mock('fetch', { filter: { active: true } }).for(User).andReturn([ 42 | { id: '17', name: 'Charlie Brown' }, 43 | { id: '18', name: 'Snoopy' } 44 | ]); 45 | ``` 46 | 47 | Additionally the argument of `andReturn` can be a function, which will be called each time the mock 48 | is triggered. 49 | 50 | The following examples describe how each action type can be mocked. 51 | 52 | 53 | ### Fetch 54 | 55 | ```js 56 | // This mock call 57 | mock('fetch', { filter: { id: '42' }}).for(User).andReturn(userData); 58 | 59 | // will be triggerd by 60 | User.fetch('42'); 61 | ``` 62 | 63 | ### Persist 64 | 65 | ```js 66 | // This mock call 67 | mock('persist', { id: '17' }).for(User).andReturn({ id: '17', name: 'Charlie Brown' }); 68 | 69 | // will be triggerd by 70 | user.$persist(); 71 | ``` 72 | 73 | ### Push 74 | 75 | ```js 76 | // This mock call 77 | mock('push', { data: { ... } }).for(User).andReturn({ id: '17', name: 'Charlie Brown' }); 78 | 79 | // will be triggerd by 80 | user.$push(); 81 | ``` 82 | 83 | ### Destroy 84 | 85 | ```js 86 | // This mock call 87 | mock('destroy', { id: '17' }).for(User).andReturn({ id: '17', name: 'Charlie Brown' }); 88 | 89 | // will be triggerd by 90 | user.$destroy(); 91 | ``` 92 | 93 | ### Custom query 94 | 95 | ```js 96 | // This mock call 97 | mock('query', { name: 'status' }).for(Post).andReturn({ ... }); 98 | 99 | // will be triggerd by 100 | Post.customQuery({ name: 'status' }); 101 | ``` 102 | 103 | 104 | ### Mutate 105 | 106 | ```js 107 | // This mock call 108 | mock('mutate', { name: 'upvote', args: { id: '4' }}).for(Post).andReturn({ ... }); 109 | 110 | // will be triggerd by 111 | post.$mutate({ name: 'upvote' }); 112 | ``` 113 | 114 | ### Simple Query 115 | 116 | Mocking simple queries works slightly different then the other actions, because these are not model 117 | related. Thus we have to mock these globally by omiting the model (`for`): 118 | 119 | ```js 120 | // This mock call 121 | mock('simpleQuery', { name: 'example' }).andReturn({ success: true }); 122 | 123 | // will be triggered by 124 | store.dispatch('entities/simpleQuery', { query: 'query example { success }' }); 125 | ``` 126 | 127 | ### Simple Mutation 128 | 129 | Works just like the simple queries: 130 | 131 | ```js 132 | // This mock call 133 | mock('simpleMutation', { 134 | name: 'SendSms', 135 | variables: { to: '+4912345678', text: 'GraphQL is awesome!' } 136 | }).andReturn({ sendSms: { delivered: true }}); 137 | 138 | // will be triggered by 139 | const query = ` 140 | mutation SendSms($to: string!, $text: string!) { 141 | sendSms(to: $to, text: $text) { 142 | delivered 143 | } 144 | }`; 145 | 146 | const result = await store.dispatch('entities/simpleMutation', { 147 | query, 148 | variables: { to: '+4912345678', text: 'GraphQL is awesome!' } 149 | }); 150 | ``` 151 | 152 | 153 | ### Resetting a mock 154 | 155 | ::: warning 156 | Support for resetting a mock is currently work in progress and will be added soon. 157 | 158 | See https://github.com/vuex-orm/plugin-graphql/issues/61 159 | ::: 160 | 161 | 162 | ## Misc 163 | 164 | The testing utils also provide some other useful functions, which are listed here: 165 | 166 | - `async clearORMStore()`: Will remove all records from the Vuex-ORM store to clean up while testing. 167 | 168 | 169 | ## Integration Testing 170 | 171 | ::: warning 172 | Support for integration testing is currently work in progress and will be added soon. 173 | 174 | See https://github.com/vuex-orm/plugin-graphql/issues/59 175 | ::: 176 | -------------------------------------------------------------------------------- /docs/guide/virtual-fields.md: -------------------------------------------------------------------------------- 1 | # Virtual Fields 2 | 3 | [[toc]] 4 | 5 | It may happen that you want fields in your Vuex-ORM model, which are not in the respective GraphQL Type. 6 | We call these "virtual fields" because they are only known to the Vuex-ORM and not to your backend or database. 7 | 8 | This plugin will automatically detect which fields are not included in the schema and will not query them at all. 9 | 10 | Let's assume we have a product model with the field `parsedMarkdownContent` which is not known to our GraphQL server: 11 | 12 | ```javascript{4} 13 | export default class Product extends Model { 14 | static entity = 'products'; 15 | 16 | static fields () { 17 | return { 18 | id: this.uid(), 19 | title: this.string(''), 20 | content: this.string(''), 21 | parsedMarkdownContent: this.string('') 22 | }; 23 | } 24 | } 25 | ``` 26 | 27 | With this model definition, the GraphQL plugin will produce the following GraphQL Query when `fetch` is called: 28 | 29 | ```graphql 30 | query Posts() { 31 | posts { 32 | nodes { 33 | id 34 | title 35 | content 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | As you see the `parsedMarkdownContent` field is not queried due to the fact that it's not in the GraphQL Schema. 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vuex-orm/plugin-graphql", 3 | "version": "1.0.0-rc.42", 4 | "description": "Vuex-ORM persistence plugin to sync the store against a GraphQL API.", 5 | "main": "dist/vuex-orm-graphql.cjs.js", 6 | "browser": "dist/vuex-orm-graphql.esm.js", 7 | "module": "dist/vuex-orm-graphql.esm-bundler.js", 8 | "unpkg": "dist/vuex-orm-graphql.global.js", 9 | "typings": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 15 | "build": "node scripts/build.js && npm run build:docs", 16 | "build:docs": "vuepress build docs", 17 | "start": "rollup -c rollup.config.ts -w", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:prod": "npm run lint && npm run test -- --coverage --no-cache", 21 | "test:full": "npm lint && npm test && npm build", 22 | "docs:deploy": "npm run build:docs && ./deploy-docs.sh", 23 | "docs:dev": "vuepress dev docs", 24 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 25 | "commit": "git-cz", 26 | "precommit": "lint-staged" 27 | }, 28 | "lint-staged": { 29 | "{src,test}/**/*.ts": [ 30 | "prettier --write", 31 | "git add" 32 | ] 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/vuex-orm/plugin-graphql.git" 37 | }, 38 | "keywords": [ 39 | "vue", 40 | "vuex", 41 | "vuex-plugin", 42 | "orm", 43 | "vuex-orm", 44 | "vuex-orm-plugin", 45 | "graphql" 46 | ], 47 | "author": "Benjamin Klein", 48 | "license": "MIT", 49 | "engines": { 50 | "node": ">=10.0.0" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/vuex-orm/plugin-graphql/issues" 54 | }, 55 | "dependencies": { 56 | "apollo-cache-inmemory": "^1.6.5", 57 | "app": "latest" 58 | }, 59 | "peerDependencies": { 60 | "@vuex-orm/core": "^0.36.3" 61 | }, 62 | "devDependencies": { 63 | "@commitlint/cli": "^8.3.5", 64 | "@commitlint/config-conventional": "^8.3.4", 65 | "@types/jest": "^25.2.1", 66 | "@types/node": "^13.11.0", 67 | "@types/sinon": "^9.0.0", 68 | "@vuex-orm/core": "^0.36.3", 69 | "apollo-client": "^2.6.8", 70 | "apollo-link": "^1.2.0", 71 | "apollo-link-http": "^1.3.2", 72 | "apollo-link-schema": "^1.1.0", 73 | "babel-plugin-external-helpers": "^6.22.0", 74 | "babel-preset-env": "^1.7.0", 75 | "colors": "^1.3.2", 76 | "commitizen": "^3.0.0", 77 | "coveralls": "^3.0.2", 78 | "cross-env": "^5.2.0", 79 | "cross-fetch": "^3.0.2", 80 | "cz-conventional-changelog": "^2.1.0", 81 | "graphql": "^14.6.0", 82 | "graphql-tag": "^2.10.2", 83 | "graphql-tools": "^4.0.7", 84 | "husky": "^1.0.1", 85 | "jest": "^25.2.7", 86 | "jest-config": "^25.2.7", 87 | "lint-staged": "^7.3.0", 88 | "lodash.clone": "^4.5.0", 89 | "lodash.isequal": "^4.5.0", 90 | "node-fetch": "^2.1.1", 91 | "normalizr": "^3.2.4", 92 | "pluralize": "^7.0.0", 93 | "prettier": "^1.14.3", 94 | "prompt": "^1.0.0", 95 | "replace-in-file": "^3.4.2", 96 | "rimraf": "^2.6.2", 97 | "rollup": "^2.3.2", 98 | "@rollup/plugin-commonjs": "^11.0.2", 99 | "@rollup/plugin-node-resolve": "^7.1.1", 100 | "rollup-plugin-terser": "^5.3.0", 101 | "rollup-plugin-typescript2": "^0.27.0", 102 | "brotli": "^1.3.2", 103 | "sinon": "^6.0.0", 104 | "ts-jest": "^25.3.1", 105 | "ts-node": "^8.8.2", 106 | "tslint": "^6.1.1", 107 | "tslint-config-prettier": "^1.15.0", 108 | "tslint-config-standard": "^9.0.0", 109 | "typescript": "^3.8.3", 110 | "vue": "^2.6.11", 111 | "vue-server-renderer": "^2.6.11", 112 | "vuepress": "^1.3.0", 113 | "vuex": "^3.1.3" 114 | }, 115 | "jest": { 116 | "transform": { 117 | ".(ts|tsx)": "ts-jest" 118 | }, 119 | "testEnvironment": "node", 120 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 121 | "moduleFileExtensions": [ 122 | "ts", 123 | "tsx", 124 | "js" 125 | ], 126 | "testPathIgnorePatterns": [ 127 | "/node_modules/", 128 | "/src/", 129 | "/dist/" 130 | ], 131 | "coveragePathIgnorePatterns": [ 132 | "/node_modules/", 133 | "/test/", 134 | "/dist/" 135 | ], 136 | "coverageThreshold": { 137 | "global": { 138 | "statements": 92, 139 | "branches": 80, 140 | "functions": 96, 141 | "lines": 93 142 | } 143 | }, 144 | "collectCoverage": true, 145 | "coverageReporters": [ 146 | "json", 147 | "lcov", 148 | "html" 149 | ], 150 | "collectCoverageFrom": [ 151 | "src/**/*.{js,ts}" 152 | ], 153 | "globals": { 154 | "ts-jest": { 155 | "diagnostics": { 156 | "ignoreCodes": [ 157 | 2339, 158 | 2576 159 | ] 160 | } 161 | } 162 | } 163 | }, 164 | "commitlint": { 165 | "extends": [ 166 | "@commitlint/config-conventional" 167 | ] 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | import ts from 'rollup-plugin-typescript2' 5 | import { terser } from 'rollup-plugin-terser' 6 | 7 | const configs = [ 8 | { input: 'src/index.ts', file: 'dist/vuex-orm-graphql.esm.js', format: 'es', browser: true, env: 'development' }, 9 | { input: 'src/index.ts', file: 'dist/vuex-orm-graphql.esm.prod.js', format: 'es', browser: true, env: 'production' }, 10 | { input: 'src/index.ts', file: 'dist/vuex-orm-graphql.esm-bundler.js', format: 'es', env: 'development' }, 11 | { input: 'src/index.ts', file: 'dist/vuex-orm-graphql.global.js', format: 'iife', env: 'development' }, 12 | { input: 'src/index.ts', file: 'dist/vuex-orm-graphql.global.prod.js', format: 'iife', minify: true, env: 'production' }, 13 | { input: 'src/index.ts', file: 'dist/vuex-orm-graphql.cjs.js', format: 'cjs', env: 'development' } 14 | ] 15 | 16 | function createEntries() { 17 | return configs.map((c) => createEntry(c)) 18 | } 19 | 20 | function createEntry(config) { 21 | const c = { 22 | input: config.input, 23 | plugins: [], 24 | output: { 25 | file: config.file, 26 | format: config.format, 27 | globals: { 28 | vue: 'Vue' 29 | } 30 | }, 31 | onwarn: (msg, warn) => { 32 | if (!/Circular/.test(msg)) { 33 | warn(msg) 34 | } 35 | } 36 | } 37 | 38 | if (config.format === 'iife') { 39 | c.output.name = 'VuexORMGraphQLPlugin' 40 | } 41 | 42 | c.plugins.push(resolve()) 43 | c.plugins.push( commonjs({ 44 | exclude: ['node_modules/symbol-observable/es/*.js'], 45 | })) 46 | 47 | c.plugins.push(ts({ 48 | check: config.format === 'es' && config.browser && config.env === 'development', 49 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 50 | cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'), 51 | tsconfigOverride: { 52 | compilerOptions: { 53 | declaration: config.format === 'es' && config.browser && config.env === 'development', 54 | target: config.format === 'iife' || config.format === 'cjs' ? 'es5' : 'es2018' 55 | }, 56 | exclude: ['test'] 57 | } 58 | })) 59 | 60 | if (config.minify) { 61 | c.plugins.push(terser({ module: config.format === 'es' })) 62 | } 63 | 64 | return c 65 | } 66 | 67 | export default createEntries() 68 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const chalk = require('chalk') 3 | const execa = require('execa') 4 | const { gzipSync } = require('zlib') 5 | const { compress } = require('brotli') 6 | 7 | const files = [ 8 | 'dist/vuex-orm-graphql.esm.js', 9 | 'dist/vuex-orm-graphql.esm.prod.js', 10 | 'dist/vuex-orm-graphql.esm-bundler.js', 11 | 'dist/vuex-orm-graphql.global.js', 12 | 'dist/vuex-orm-graphql.global.prod.js', 13 | 'dist/vuex-orm-graphql.cjs.js' 14 | ] 15 | 16 | async function run() { 17 | await build() 18 | checkAllSizes() 19 | } 20 | 21 | async function build() { 22 | await fs.remove('dist') 23 | 24 | await execa('rollup', ['-c', 'rollup.config.js'], { stdio: 'inherit' }) 25 | } 26 | 27 | function checkAllSizes() { 28 | console.log() 29 | files.map((f) => checkSize(f)) 30 | console.log() 31 | } 32 | 33 | function checkSize(file) { 34 | const f = fs.readFileSync(file) 35 | const minSize = (f.length / 1024).toFixed(2) + 'kb' 36 | const gzipped = gzipSync(f) 37 | const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb' 38 | const compressed = compress(f) 39 | const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb' 40 | console.log( 41 | `${chalk.gray( 42 | chalk.bold(file) 43 | )} size:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}` 44 | ) 45 | } 46 | 47 | run() 48 | -------------------------------------------------------------------------------- /src/actions/action.ts: -------------------------------------------------------------------------------- 1 | import QueryBuilder from "../graphql/query-builder"; 2 | import Context from "../common/context"; 3 | import { Store } from "../orm/store"; 4 | import { Arguments, Data, DispatchFunction } from "../support/interfaces"; 5 | import Model from "../orm/model"; 6 | import RootState from "@vuex-orm/core/lib/modules/contracts/RootState"; 7 | import Transformer from "../graphql/transformer"; 8 | import Schema from "../graphql/schema"; 9 | import { singularize, toPrimaryKey } from "../support/utils"; 10 | 11 | /** 12 | * Base class for all Vuex actions. Contains some utility and convenience methods. 13 | */ 14 | export default class Action { 15 | /** 16 | * Sends a mutation. 17 | * 18 | * @param {string} name Name of the mutation like 'createUser' 19 | * @param {Data | undefined} variables Variables to send with the mutation 20 | * @param {Function} dispatch Vuex Dispatch method for the model 21 | * @param {Model} model The model this mutation affects. 22 | * @param {boolean} multiple Tells if we're requesting a single record or multiple. 23 | * @returns {Promise} 24 | */ 25 | protected static async mutation( 26 | name: string, 27 | variables: Data | undefined, 28 | dispatch: DispatchFunction, 29 | model: Model 30 | ): Promise { 31 | if (variables) { 32 | const context: Context = Context.getInstance(); 33 | const schema: Schema = await context.loadSchema(); 34 | 35 | const multiple: boolean = Schema.returnsConnection(schema.getMutation(name)!); 36 | const query = QueryBuilder.buildQuery("mutation", model, name, variables, multiple); 37 | 38 | // Send GraphQL Mutation 39 | let newData = await context.apollo.request(model, query, variables, true); 40 | 41 | // When this was not a destroy action, we get new data, which we should insert in the store 42 | if (name !== context.adapter.getNameForDestroy(model)) { 43 | newData = newData[Object.keys(newData)[0]]; 44 | 45 | // IDs as String cause terrible issues, so we convert them to integers. 46 | newData.id = toPrimaryKey(newData.id); 47 | 48 | const insertedData: Data = await Store.insertData( 49 | { [model.pluralName]: newData } as Data, 50 | dispatch 51 | ); 52 | 53 | // Try to find the record to return 54 | const records = insertedData[model.pluralName]; 55 | const newRecord = records[records.length - 1]; 56 | if (newRecord) { 57 | return newRecord; 58 | } else { 59 | Context.getInstance().logger.log( 60 | "Couldn't find the record of type '", 61 | model.pluralName, 62 | "' within", 63 | insertedData, 64 | ". Falling back to find()" 65 | ); 66 | return model.baseModel.query().last(); 67 | } 68 | } 69 | 70 | return true; 71 | } 72 | } 73 | 74 | /** 75 | * Convenience method to get the model from the state. 76 | * @param {RootState} state Vuex state 77 | * @returns {Model} 78 | */ 79 | static getModelFromState(state: RootState): Model { 80 | return Context.getInstance().getModel(state.$name); 81 | } 82 | 83 | /** 84 | * Makes sure args is a hash. 85 | * 86 | * @param {Arguments|undefined} args 87 | * @param {any} id When not undefined, it's added to the args 88 | * @returns {Arguments} 89 | */ 90 | static prepareArgs(args?: Arguments, id?: any): Arguments { 91 | args = args || {}; 92 | if (id) args["id"] = id; 93 | 94 | return args; 95 | } 96 | 97 | /** 98 | * Adds the record itself to the args and sends it through transformOutgoingData. Key is named by the singular name 99 | * of the model. 100 | * 101 | * @param {Arguments} args 102 | * @param {Model} model 103 | * @param {Data} data 104 | * @returns {Arguments} 105 | */ 106 | static addRecordToArgs(args: Arguments, model: Model, data: Data): Arguments { 107 | args[model.singularName] = Transformer.transformOutgoingData(model, data, false); 108 | return args; 109 | } 110 | 111 | /** 112 | * Transforms each field of the args which contains a model. 113 | * @param {Arguments} args 114 | * @returns {Arguments} 115 | */ 116 | protected static transformArgs(args: Arguments): Arguments { 117 | const context = Context.getInstance(); 118 | 119 | Object.keys(args).forEach((key: string) => { 120 | const value: Data = args[key]; 121 | 122 | if (value instanceof context.components.Model) { 123 | const model = context.getModel(singularize(value.$self().entity)); 124 | const transformedValue = Transformer.transformOutgoingData(model, value, false); 125 | context.logger.log( 126 | "A", 127 | key, 128 | "model was found within the variables and will be transformed from", 129 | value, 130 | "to", 131 | transformedValue 132 | ); 133 | args[key] = transformedValue; 134 | } 135 | }); 136 | 137 | return args; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/actions/destroy.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, Data, PatchedModel } from "../support/interfaces"; 2 | import Action from "./action"; 3 | import { Store } from "../orm/store"; 4 | import Context from "../common/context"; 5 | import { toPrimaryKey } from "../support/utils"; 6 | 7 | /** 8 | * Destroy action for sending a delete mutation. Will be used for record.$destroy(). 9 | */ 10 | export default class Destroy extends Action { 11 | /** 12 | * Registers the record.$destroy() and record.$deleteAndDestroy() methods and 13 | * the destroy Vuex Action. 14 | */ 15 | public static setup() { 16 | const context = Context.getInstance(); 17 | const record: PatchedModel = context.components.Model.prototype as PatchedModel; 18 | 19 | context.components.Actions.destroy = Destroy.call.bind(Destroy); 20 | 21 | record.$destroy = async function() { 22 | return this.$dispatch("destroy", { id: toPrimaryKey(this.$id) }); 23 | }; 24 | 25 | record.$deleteAndDestroy = async function() { 26 | await this.$delete(); 27 | return this.$destroy(); 28 | }; 29 | } 30 | 31 | /** 32 | * @param {State} state The Vuex state 33 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 34 | * @param {number} id ID of the record to delete 35 | * @returns {Promise} true 36 | */ 37 | public static async call( 38 | { state, dispatch }: ActionParams, 39 | { id, args }: ActionParams 40 | ): Promise { 41 | if (id) { 42 | const model = this.getModelFromState(state!); 43 | const mutationName = Context.getInstance().adapter.getNameForDestroy(model); 44 | 45 | const mockReturnValue = model.$mockHook("destroy", { id }); 46 | 47 | if (mockReturnValue) { 48 | await Store.insertData(mockReturnValue, dispatch!); 49 | return true; 50 | } 51 | 52 | args = this.prepareArgs(args, id); 53 | 54 | await Action.mutation(mutationName, args as Data, dispatch!, model); 55 | return true; 56 | } else { 57 | /* istanbul ignore next */ 58 | throw new Error("The destroy action requires the 'id' to be set"); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/actions/fetch.ts: -------------------------------------------------------------------------------- 1 | import QueryBuilder from "../graphql/query-builder"; 2 | import Context from "../common/context"; 3 | import { Store } from "../orm/store"; 4 | import Transformer from "../graphql/transformer"; 5 | import { ActionParams, Data, PatchedModel } from "../support/interfaces"; 6 | import Action from "./action"; 7 | import { isPlainObject } from "../support/utils"; 8 | 9 | /** 10 | * Fetch action for sending a query. Will be used for Model.fetch(). 11 | */ 12 | export default class Fetch extends Action { 13 | /** 14 | * Registers the Model.fetch() method and the fetch Vuex Action. 15 | */ 16 | public static setup() { 17 | const context = Context.getInstance(); 18 | const model: typeof PatchedModel = context.components.Model as typeof PatchedModel; 19 | 20 | context.components.Actions.fetch = Fetch.call.bind(Fetch); 21 | 22 | model.fetch = async function(filter: any, bypassCache = false) { 23 | let filterObj = filter; 24 | if (!isPlainObject(filterObj)) filterObj = { id: filter }; 25 | 26 | return this.dispatch("fetch", { filter: filterObj, bypassCache }); 27 | }; 28 | } 29 | 30 | /** 31 | * @param {any} state The Vuex state 32 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 33 | * @param {ActionParams} params Optional params to send with the query 34 | * @returns {Promise} The fetched records as hash 35 | */ 36 | public static async call( 37 | { state, dispatch }: ActionParams, 38 | params?: ActionParams 39 | ): Promise { 40 | const context = Context.getInstance(); 41 | const model = this.getModelFromState(state!); 42 | 43 | const mockReturnValue = model.$mockHook("fetch", { 44 | filter: params ? params.filter || {} : {} 45 | }); 46 | 47 | if (mockReturnValue) { 48 | return Store.insertData(mockReturnValue, dispatch!); 49 | } 50 | 51 | await context.loadSchema(); 52 | 53 | // Filter 54 | let filter = {}; 55 | 56 | if (params && params.filter) { 57 | filter = Transformer.transformOutgoingData( 58 | model, 59 | params.filter as Data, 60 | true, 61 | Object.keys(params.filter) 62 | ); 63 | } 64 | 65 | const bypassCache = params && params.bypassCache; 66 | 67 | // When the filter contains an id, we query in singular mode 68 | const multiple: boolean = !filter["id"]; 69 | const name: string = context.adapter.getNameForFetch(model, multiple); 70 | const query = QueryBuilder.buildQuery("query", model, name, filter, multiple, multiple); 71 | 72 | // Send the request to the GraphQL API 73 | const data = await context.apollo.request(model, query, filter, false, bypassCache as boolean); 74 | 75 | // Insert incoming data into the store 76 | return Store.insertData(data, dispatch!); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import Action from "./action"; 2 | import Destroy from "./destroy"; 3 | import Fetch from "./fetch"; 4 | import Mutate from "./mutate"; 5 | import Persist from "./persist"; 6 | import Push from "./push"; 7 | import Query from "./query"; 8 | import SimpleQuery from "./simple-query"; 9 | import SimpleMutation from "./simple-mutation"; 10 | 11 | export { SimpleQuery, SimpleMutation, Query, Action, Destroy, Fetch, Mutate, Persist, Push }; 12 | -------------------------------------------------------------------------------- /src/actions/mutate.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, PatchedModel, Data } from "../support/interfaces"; 2 | import Action from "./action"; 3 | import Context from "../common/context"; 4 | import Schema from "../graphql/schema"; 5 | import { Store } from "../orm/store"; 6 | import { toPrimaryKey } from "../support/utils"; 7 | 8 | /** 9 | * Mutate action for sending a custom mutation. Will be used for Model.mutate() and record.$mutate(). 10 | */ 11 | export default class Mutate extends Action { 12 | /** 13 | * Registers the Model.mutate() and the record.$mutate() methods and the mutate Vuex Action. 14 | */ 15 | public static setup() { 16 | const context = Context.getInstance(); 17 | const model: typeof PatchedModel = context.components.Model as typeof PatchedModel; 18 | const record: PatchedModel = context.components.Model.prototype as PatchedModel; 19 | 20 | context.components.Actions.mutate = Mutate.call.bind(Mutate); 21 | 22 | model.mutate = async function(params: ActionParams) { 23 | return this.dispatch("mutate", params); 24 | }; 25 | 26 | record.$mutate = async function({ name, args, multiple }: ActionParams) { 27 | args = args || {}; 28 | if (!args["id"]) args["id"] = toPrimaryKey(this.$id); 29 | 30 | return this.$dispatch("mutate", { name, args, multiple }); 31 | }; 32 | } 33 | 34 | /** 35 | * @param {any} state The Vuex state 36 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 37 | * @param {string} name Name of the query 38 | * @param {boolean} multiple Fetch one or multiple? 39 | * @param {Arguments} args Arguments for the mutation. Must contain a 'mutation' field. 40 | * @returns {Promise} The new record if any 41 | */ 42 | public static async call( 43 | { state, dispatch }: ActionParams, 44 | { args, name }: ActionParams 45 | ): Promise { 46 | if (name) { 47 | const context: Context = Context.getInstance(); 48 | const model = this.getModelFromState(state!); 49 | 50 | const mockReturnValue = model.$mockHook("mutate", { 51 | name, 52 | args: args || {} 53 | }); 54 | 55 | if (mockReturnValue) { 56 | return Store.insertData(mockReturnValue, dispatch!); 57 | } 58 | 59 | await context.loadSchema(); 60 | args = this.prepareArgs(args); 61 | 62 | // There could be anything in the args, but we have to be sure that all records are gone through 63 | // transformOutgoingData() 64 | this.transformArgs(args); 65 | 66 | // Send the mutation 67 | return Action.mutation(name, args as Data, dispatch!, model); 68 | } else { 69 | /* istanbul ignore next */ 70 | throw new Error("The mutate action requires the mutation name ('mutation') to be set"); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/actions/persist.ts: -------------------------------------------------------------------------------- 1 | import Context from "../common/context"; 2 | import { ActionParams, Data, PatchedModel } from "../support/interfaces"; 3 | import Action from "./action"; 4 | import Model from "../orm/model"; 5 | import { Store } from "../orm/store"; 6 | import { toPrimaryKey } from "../support/utils"; 7 | 8 | /** 9 | * Persist action for sending a create mutation. Will be used for record.$persist(). 10 | */ 11 | export default class Persist extends Action { 12 | /** 13 | * Registers the record.$persist() method and the persist Vuex Action. 14 | */ 15 | public static setup() { 16 | const context = Context.getInstance(); 17 | const record: PatchedModel = context.components.Model.prototype as PatchedModel; 18 | 19 | context.components.Actions.persist = Persist.call.bind(Persist); 20 | 21 | record.$persist = async function(args: any) { 22 | return this.$dispatch("persist", { id: this.$id, args }); 23 | }; 24 | } 25 | 26 | /** 27 | * @param {any} state The Vuex state 28 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 29 | * @param {number|string} id ID of the record to persist 30 | * @returns {Promise} The saved record 31 | */ 32 | public static async call( 33 | { state, dispatch }: ActionParams, 34 | { id, args }: ActionParams 35 | ): Promise { 36 | if (id) { 37 | const model = this.getModelFromState(state!); 38 | const mutationName = Context.getInstance().adapter.getNameForPersist(model); 39 | const oldRecord = model.getRecordWithId(id)!; 40 | 41 | const mockReturnValue = model.$mockHook("persist", { 42 | id: toPrimaryKey(id), 43 | args: args || {} 44 | }); 45 | 46 | if (mockReturnValue) { 47 | const newRecord = await Store.insertData(mockReturnValue, dispatch!); 48 | await this.deleteObsoleteRecord(model, newRecord, oldRecord); 49 | return newRecord; 50 | } 51 | 52 | // Arguments 53 | await Context.getInstance().loadSchema(); 54 | args = this.prepareArgs(args); 55 | this.addRecordToArgs(args, model, oldRecord); 56 | 57 | // Send mutation 58 | const newRecord = await Action.mutation(mutationName, args as Data, dispatch!, model); 59 | 60 | // Delete the old record if necessary 61 | await this.deleteObsoleteRecord(model, newRecord, oldRecord); 62 | 63 | return newRecord; 64 | } else { 65 | /* istanbul ignore next */ 66 | throw new Error("The persist action requires the 'id' to be set"); 67 | } 68 | } 69 | 70 | /** 71 | * It's very likely that the server generated different ID for this record. 72 | * In this case Action.mutation has inserted a new record instead of updating the existing one. 73 | * 74 | * @param {Model} model 75 | * @param {Data} record 76 | * @returns {Promise} 77 | */ 78 | private static async deleteObsoleteRecord(model: Model, newRecord: Data, oldRecord: Data) { 79 | if (newRecord && oldRecord && newRecord.id !== oldRecord.id) { 80 | Context.getInstance().logger.log("Dropping deprecated record", oldRecord); 81 | return oldRecord.$delete(); 82 | } 83 | 84 | return null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/actions/push.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams, Data, PatchedModel } from "../support/interfaces"; 2 | import Action from "./action"; 3 | import { Store } from "../orm/store"; 4 | import Context from "../common/context"; 5 | 6 | /** 7 | * Push action for sending a update mutation. Will be used for record.$push(). 8 | */ 9 | export default class Push extends Action { 10 | /** 11 | * Registers the record.$push() method and the push Vuex Action. 12 | */ 13 | public static setup() { 14 | const context = Context.getInstance(); 15 | const model: PatchedModel = context.components.Model.prototype as PatchedModel; 16 | 17 | context.components.Actions.push = Push.call.bind(Push); 18 | 19 | model.$push = async function(args: any) { 20 | return this.$dispatch("push", { data: this, args }); 21 | }; 22 | } 23 | 24 | /** 25 | * @param {any} state The Vuex state 26 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 27 | * @param {Arguments} data New data to save 28 | * @param {Arguments} args Additional arguments 29 | * @returns {Promise} The updated record 30 | */ 31 | public static async call( 32 | { state, dispatch }: ActionParams, 33 | { data, args }: ActionParams 34 | ): Promise { 35 | if (data) { 36 | const model = this.getModelFromState(state!); 37 | const mutationName = Context.getInstance().adapter.getNameForPush(model); 38 | 39 | const mockReturnValue = model.$mockHook("push", { 40 | data, 41 | args: args || {} 42 | }); 43 | 44 | if (mockReturnValue) { 45 | return Store.insertData(mockReturnValue, dispatch!); 46 | } 47 | 48 | // Arguments 49 | await Context.getInstance().loadSchema(); 50 | args = this.prepareArgs(args, data.id); 51 | this.addRecordToArgs(args, model, data); 52 | 53 | // Send the mutation 54 | return Action.mutation(mutationName, args as Data, dispatch!, model); 55 | } else { 56 | /* istanbul ignore next */ 57 | throw new Error("The persist action requires the 'data' to be set"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/actions/query.ts: -------------------------------------------------------------------------------- 1 | import QueryBuilder from "../graphql/query-builder"; 2 | import Context from "../common/context"; 3 | import { Store } from "../orm/store"; 4 | import Transformer from "../graphql/transformer"; 5 | import { ActionParams, Data, PatchedModel } from "../support/interfaces"; 6 | import Action from "./action"; 7 | import Schema from "../graphql/schema"; 8 | import { toPrimaryKey } from "../support/utils"; 9 | 10 | /** 11 | * Query action for sending a custom query. Will be used for Model.customQuery() and record.$customQuery. 12 | */ 13 | export default class Query extends Action { 14 | /** 15 | * Registers the Model.customQuery and the record.$customQuery() methods and the 16 | * query Vuex Action. 17 | */ 18 | public static setup() { 19 | const context = Context.getInstance(); 20 | const model: typeof PatchedModel = context.components.Model as typeof PatchedModel; 21 | const record: PatchedModel = context.components.Model.prototype as PatchedModel; 22 | 23 | context.components.Actions.query = Query.call.bind(Query); 24 | 25 | model.customQuery = async function({ name, filter, multiple, bypassCache }: ActionParams) { 26 | return this.dispatch("query", { name, filter, multiple, bypassCache }); 27 | }; 28 | 29 | record.$customQuery = async function({ name, filter, multiple, bypassCache }: ActionParams) { 30 | filter = filter || {}; 31 | if (!filter["id"]) filter["id"] = toPrimaryKey(this.$id); 32 | 33 | return this.$dispatch("query", { name, filter, multiple, bypassCache }); 34 | }; 35 | } 36 | 37 | /** 38 | * @param {any} state The Vuex state 39 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 40 | * @param {string} name Name of the query 41 | * @param {boolean} multiple Fetch one or multiple? 42 | * @param {object} filter Filter object (arguments) 43 | * @param {boolean} bypassCache Whether to bypass the cache 44 | * @returns {Promise} The fetched records as hash 45 | */ 46 | public static async call( 47 | { state, dispatch }: ActionParams, 48 | { name, filter, bypassCache }: ActionParams 49 | ): Promise { 50 | if (name) { 51 | const context: Context = Context.getInstance(); 52 | const model = this.getModelFromState(state!); 53 | 54 | const mockReturnValue = model.$mockHook("query", { 55 | name, 56 | filter: filter || {} 57 | }); 58 | 59 | if (mockReturnValue) { 60 | return Store.insertData(mockReturnValue, dispatch!); 61 | } 62 | 63 | const schema: Schema = await context.loadSchema(); 64 | 65 | // Filter 66 | filter = filter ? Transformer.transformOutgoingData(model, filter as Data, true) : {}; 67 | 68 | // Multiple? 69 | const multiple: boolean = Schema.returnsConnection(schema.getQuery(name)!); 70 | 71 | // Build query 72 | const query = QueryBuilder.buildQuery("query", model, name, filter, multiple, false); 73 | 74 | // Send the request to the GraphQL API 75 | const data = await context.apollo.request( 76 | model, 77 | query, 78 | filter, 79 | false, 80 | bypassCache as boolean 81 | ); 82 | 83 | // Insert incoming data into the store 84 | return Store.insertData(data, dispatch!); 85 | } else { 86 | /* istanbul ignore next */ 87 | throw new Error("The customQuery action requires the query name ('name') to be set"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/actions/simple-mutation.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams } from "../support/interfaces"; 2 | import Action from "./action"; 3 | import Context from "../common/context"; 4 | import { clone, graphQlDocumentToString, parseQuery } from "../support/utils"; 5 | 6 | /** 7 | * SimpleMutation action for sending a model unrelated simple mutation. 8 | */ 9 | export default class SimpleMutation extends Action { 10 | /** 11 | * Registers the Model.simpleMutation() Vuex Root Action. 12 | */ 13 | public static setup() { 14 | const context = Context.getInstance(); 15 | context.components.RootActions.simpleMutation = SimpleMutation.call.bind(SimpleMutation); 16 | } 17 | 18 | /** 19 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 20 | * @param {string} query The query to send 21 | * @param {Arguments} variables 22 | * @returns {Promise} The result 23 | */ 24 | public static async call( 25 | { dispatch }: ActionParams, 26 | { query, variables }: ActionParams 27 | ): Promise { 28 | const context: Context = Context.getInstance(); 29 | 30 | if (query) { 31 | const parsedQuery = parseQuery(query); 32 | 33 | const mockReturnValue = context.globalMockHook("simpleMutation", { 34 | name: parsedQuery.definitions[0]["name"].value, 35 | variables 36 | }); 37 | 38 | if (mockReturnValue) { 39 | return mockReturnValue; 40 | } 41 | 42 | variables = this.prepareArgs(variables); 43 | const result = await context.apollo.simpleMutation( 44 | graphQlDocumentToString(parsedQuery), 45 | variables 46 | ); 47 | 48 | // remove the symbols 49 | return clone(result.data); 50 | } else { 51 | /* istanbul ignore next */ 52 | throw new Error("The simpleMutation action requires the 'query' to be set"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/actions/simple-query.ts: -------------------------------------------------------------------------------- 1 | import { ActionParams } from "../support/interfaces"; 2 | import Action from "./action"; 3 | import Context from "../common/context"; 4 | import { clone, graphQlDocumentToString, parseQuery, removeSymbols } from "../support/utils"; 5 | 6 | /** 7 | * SimpleQuery action for sending a model unrelated simple query. 8 | */ 9 | export default class SimpleQuery extends Action { 10 | /** 11 | * Registers the Model.simpleQuery() Vuex Root Action. 12 | */ 13 | public static setup() { 14 | const context = Context.getInstance(); 15 | context.components.RootActions.simpleQuery = SimpleQuery.call.bind(SimpleQuery); 16 | } 17 | 18 | /** 19 | * @param {DispatchFunction} dispatch Vuex Dispatch method for the model 20 | * @param {string} query The query to send 21 | * @param {Arguments} variables 22 | * @param {boolean} bypassCache Whether to bypass the cache 23 | * @returns {Promise} The result 24 | */ 25 | public static async call( 26 | { dispatch }: ActionParams, 27 | { query, bypassCache, variables }: ActionParams 28 | ): Promise { 29 | const context: Context = Context.getInstance(); 30 | 31 | if (query) { 32 | const parsedQuery = parseQuery(query); 33 | 34 | const mockReturnValue = context.globalMockHook("simpleQuery", { 35 | name: parsedQuery.definitions[0]["name"].value, 36 | variables 37 | }); 38 | 39 | if (mockReturnValue) { 40 | return mockReturnValue; 41 | } 42 | 43 | variables = this.prepareArgs(variables); 44 | 45 | const result = await context.apollo.simpleQuery( 46 | graphQlDocumentToString(parsedQuery), 47 | variables, 48 | bypassCache 49 | ); 50 | 51 | // remove the symbols 52 | return removeSymbols(clone(result.data)); 53 | } else { 54 | /* istanbul ignore next */ 55 | throw new Error("The simpleQuery action requires the 'query' to be set"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/adapters/adapter.ts: -------------------------------------------------------------------------------- 1 | import Model from "../orm/model"; 2 | 3 | export enum ConnectionMode { 4 | AUTO, 5 | PLAIN, 6 | NODES, 7 | EDGES, 8 | ITEMS 9 | } 10 | 11 | export enum ArgumentMode { 12 | TYPE, 13 | LIST 14 | } 15 | 16 | export default interface Adapter { 17 | getRootQueryName(): string; 18 | getRootMutationName(): string; 19 | 20 | getNameForPersist(model: Model): string; 21 | getNameForPush(model: Model): string; 22 | getNameForDestroy(model: Model): string; 23 | getNameForFetch(model: Model, plural: boolean): string; 24 | 25 | getConnectionMode(): ConnectionMode; 26 | 27 | getArgumentMode(): ArgumentMode; 28 | 29 | getFilterTypeName(model: Model): string; 30 | getInputTypeName(model: Model, action?: string): string; 31 | 32 | prepareSchemaTypeName(name: string): string; 33 | } 34 | -------------------------------------------------------------------------------- /src/adapters/builtin/default-adapter.ts: -------------------------------------------------------------------------------- 1 | import Adapter, { ConnectionMode, ArgumentMode } from "../adapter"; 2 | import Model from "../../orm/model"; 3 | import { upcaseFirstLetter } from "../../support/utils"; 4 | 5 | export default class DefaultAdapter implements Adapter { 6 | getRootMutationName(): string { 7 | return "Mutation"; 8 | } 9 | 10 | getRootQueryName(): string { 11 | return "Query"; 12 | } 13 | 14 | getConnectionMode(): ConnectionMode { 15 | return ConnectionMode.NODES; 16 | } 17 | 18 | getArgumentMode(): ArgumentMode { 19 | return ArgumentMode.TYPE; 20 | } 21 | 22 | getFilterTypeName(model: Model): string { 23 | return `${upcaseFirstLetter(model.singularName)}Filter`; 24 | } 25 | 26 | getInputTypeName(model: Model, action?: string): string { 27 | return `${upcaseFirstLetter(model.singularName)}Input`; 28 | } 29 | 30 | getNameForDestroy(model: Model): string { 31 | return `delete${upcaseFirstLetter(model.singularName)}`; 32 | } 33 | 34 | getNameForFetch(model: Model, plural: boolean): string { 35 | return plural ? model.pluralName : model.singularName; 36 | } 37 | 38 | getNameForPersist(model: Model): string { 39 | return `create${upcaseFirstLetter(model.singularName)}`; 40 | } 41 | 42 | getNameForPush(model: Model): string { 43 | return `update${upcaseFirstLetter(model.singularName)}`; 44 | } 45 | 46 | prepareSchemaTypeName(name: string): string { 47 | return upcaseFirstLetter(name); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/common/logger.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from "graphql/language/ast"; 2 | import { Arguments } from "../support/interfaces"; 3 | import { FetchPolicy } from "apollo-client"; 4 | import { isPlainObject, prettify } from "../support/utils"; 5 | 6 | /** 7 | * Vuex-ORM-Apollo Debug Logger. 8 | * Wraps console and only logs if enabled. 9 | * 10 | * Also contains some methods to format graphql queries for the output 11 | */ 12 | export default class Logger { 13 | /** 14 | * Tells if any logging should happen 15 | * @type {boolean} 16 | */ 17 | private readonly enabled: boolean; 18 | 19 | /** 20 | * Fancy Vuex-ORM-Apollo prefix for all log messages. 21 | * @type {string[]} 22 | */ 23 | private readonly PREFIX = [ 24 | "%c Vuex-ORM: GraphQL Plugin %c", 25 | "background: #35495e; padding: 1px 0; border-radius: 3px; color: #eee;", 26 | "background: transparent;" 27 | ]; 28 | 29 | /** 30 | * @constructor 31 | * @param {boolean} enabled Tells if any logging should happen 32 | */ 33 | public constructor(enabled: boolean) { 34 | this.enabled = enabled; 35 | this.log("Logging is enabled."); 36 | } 37 | 38 | /** 39 | * Wraps console.group. In TEST env console.log is used instead because console.group doesn't work on CLI. 40 | * If available console.groupCollapsed will be used instead. 41 | * @param {Array} messages 42 | */ 43 | public group(...messages: Array): void { 44 | if (this.enabled) { 45 | if (console.groupCollapsed) { 46 | console.groupCollapsed(...this.PREFIX, ...messages); 47 | } else { 48 | console.log(...this.PREFIX, ...messages); 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Wrapper for console.groupEnd. In TEST env nothing happens because console.groupEnd doesn't work on CLI. 55 | */ 56 | public groupEnd(): void { 57 | if (this.enabled && console.groupEnd) console.groupEnd(); 58 | } 59 | 60 | /** 61 | * Wrapper for console.log. 62 | * @param {Array} messages 63 | */ 64 | public log(...messages: Array): void { 65 | if (this.enabled) { 66 | console.log(...this.PREFIX, ...messages); 67 | } 68 | } 69 | 70 | /** 71 | * Wrapper for console.warn. 72 | * @param {Array} messages 73 | */ 74 | public warn(...messages: Array): void { 75 | if (this.enabled) { 76 | console.warn(...this.PREFIX, ...messages); 77 | } 78 | } 79 | 80 | /** 81 | * Logs a graphql query in a readable format and with all information like fetch policy and variables. 82 | * @param {string | DocumentNode} query 83 | * @param {Arguments} variables 84 | * @param {FetchPolicy} fetchPolicy 85 | */ 86 | public logQuery(query: string | DocumentNode, variables?: Arguments, fetchPolicy?: FetchPolicy) { 87 | if (this.enabled) { 88 | try { 89 | let prettified = ""; 90 | if (isPlainObject(query) && (query as DocumentNode).loc) { 91 | prettified = prettify((query as DocumentNode).loc!.source.body); 92 | } else { 93 | prettified = prettify(query as string); 94 | } 95 | 96 | this.group( 97 | "Sending query:", 98 | prettified 99 | .split("\n")[1] 100 | .replace("{", "") 101 | .trim() 102 | ); 103 | console.log(prettified); 104 | 105 | if (variables) console.log("VARIABLES:", variables); 106 | if (fetchPolicy) console.log("FETCH POLICY:", fetchPolicy); 107 | 108 | this.groupEnd(); 109 | } catch (e) { 110 | console.error("[Vuex-ORM-Apollo] There is a syntax error in the query!", e, query); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/graphql/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, FetchPolicy } from "apollo-client"; 2 | import { InMemoryCache } from "apollo-cache-inmemory"; 3 | import { HttpLink } from "apollo-link-http"; 4 | import { ApolloLink } from "apollo-link"; 5 | import Context from "../common/context"; 6 | import { Arguments, Data } from "../support/interfaces"; 7 | import Transformer from "./transformer"; 8 | import Model from "../orm/model"; 9 | import gql from "graphql-tag"; 10 | 11 | /** 12 | * This class takes care of the communication with the graphql endpoint by leveraging the awesome apollo-client lib. 13 | */ 14 | export default class Apollo { 15 | /** 16 | * The http link instance to use. 17 | * @type {HttpLink} 18 | */ 19 | private readonly httpLink: ApolloLink; 20 | 21 | /** 22 | * The ApolloClient instance 23 | * @type {ApolloClient} 24 | */ 25 | private readonly apolloClient: ApolloClient; 26 | 27 | /** 28 | * @constructor 29 | */ 30 | public constructor() { 31 | const context = Context.getInstance(); 32 | 33 | // This allows the test suite to pass a custom link 34 | if (!context.options.apolloClient && context.options.link) { 35 | this.httpLink = context.options.link; 36 | } else { 37 | /* istanbul ignore next */ 38 | this.httpLink = new HttpLink({ 39 | uri: context.options.url ? context.options.url : "/graphql", 40 | credentials: context.options.credentials ? context.options.credentials : "same-origin", 41 | useGETForQueries: Boolean(context.options.useGETForQueries) 42 | }); 43 | } 44 | 45 | if (context.options.apolloClient) { 46 | this.apolloClient = (context => { 47 | return context.options.apolloClient; 48 | })(context); 49 | } else { 50 | this.apolloClient = new ApolloClient({ 51 | link: this.httpLink, 52 | cache: new InMemoryCache(), 53 | connectToDevTools: context.debugMode 54 | }); 55 | } 56 | } 57 | 58 | /** 59 | * Sends a request to the GraphQL API via apollo 60 | * @param model 61 | * @param {any} query The query to send (result from gql()) 62 | * @param {Arguments} variables Optional. The variables to send with the query 63 | * @param {boolean} mutation Optional. If this is a mutation (true) or a query (false, default) 64 | * @param {boolean} bypassCache If true the query will be send to the server without using the cache. For queries only 65 | * @returns {Promise} The new records 66 | */ 67 | public async request( 68 | model: Model, 69 | query: any, 70 | variables?: Arguments, 71 | mutation: boolean = false, 72 | bypassCache: boolean = false 73 | ): Promise { 74 | const fetchPolicy: FetchPolicy = bypassCache ? "network-only" : "cache-first"; 75 | Context.getInstance().logger.logQuery(query, variables, fetchPolicy); 76 | 77 | const context = { headers: Apollo.getHeaders() }; 78 | 79 | let response; 80 | if (mutation) { 81 | response = await this.apolloClient.mutate({ mutation: query, variables, context }); 82 | } else { 83 | response = await this.apolloClient.query({ query, variables, fetchPolicy, context }); 84 | } 85 | 86 | // Transform incoming data into something useful 87 | return Transformer.transformIncomingData(response.data as Data, model, mutation); 88 | } 89 | 90 | public async simpleQuery( 91 | query: string, 92 | variables: Arguments, 93 | bypassCache: boolean = false, 94 | context?: Data 95 | ): Promise { 96 | const fetchPolicy: FetchPolicy = bypassCache ? "network-only" : "cache-first"; 97 | return this.apolloClient.query({ 98 | query: gql(query), 99 | variables, 100 | fetchPolicy, 101 | context: { headers: Apollo.getHeaders() } 102 | }); 103 | } 104 | 105 | public async simpleMutation(query: string, variables: Arguments, context?: Data): Promise { 106 | return this.apolloClient.mutate({ 107 | mutation: gql(query), 108 | variables, 109 | context: { headers: Apollo.getHeaders() } 110 | }); 111 | } 112 | 113 | private static getHeaders() { 114 | const context = Context.getInstance(); 115 | 116 | let headers: any = context.options.headers ? context.options.headers : {}; 117 | 118 | if (typeof headers === "function") { 119 | headers = headers(context); 120 | } 121 | 122 | return headers; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/graphql/introspection-query.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | query Introspection { 3 | __schema { 4 | types { 5 | name 6 | description 7 | fields(includeDeprecated: true) { 8 | name 9 | description 10 | args { 11 | name 12 | description 13 | type { 14 | name 15 | kind 16 | 17 | ofType { 18 | kind 19 | 20 | name 21 | ofType { 22 | kind 23 | name 24 | 25 | ofType { 26 | kind 27 | name 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | type { 35 | name 36 | kind 37 | 38 | ofType { 39 | kind 40 | 41 | name 42 | ofType { 43 | kind 44 | name 45 | 46 | ofType { 47 | kind 48 | name 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | inputFields { 56 | name 57 | description 58 | type { 59 | name 60 | kind 61 | 62 | ofType { 63 | kind 64 | 65 | name 66 | ofType { 67 | kind 68 | name 69 | 70 | ofType { 71 | kind 72 | name 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | `; 82 | -------------------------------------------------------------------------------- /src/graphql/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLField, 3 | GraphQLSchema, 4 | GraphQLType, 5 | GraphQLTypeDefinition 6 | } from "../support/interfaces"; 7 | import { upcaseFirstLetter } from "../support/utils"; 8 | import { ConnectionMode } from "../adapters/adapter"; 9 | import Context from "../common/context"; 10 | 11 | export default class Schema { 12 | private schema: GraphQLSchema; 13 | private types: Map; 14 | private mutations: Map; 15 | private queries: Map; 16 | 17 | public constructor(schema: GraphQLSchema) { 18 | const context = Context.getInstance(); 19 | 20 | this.schema = schema; 21 | this.types = new Map(); 22 | this.mutations = new Map(); 23 | this.queries = new Map(); 24 | 25 | this.schema.types.forEach((t: GraphQLType) => this.types.set(t.name, t)); 26 | 27 | this.getType(context.adapter.getRootQueryName())!.fields!.forEach(f => 28 | this.queries.set(f.name, f) 29 | ); 30 | this.getType(context.adapter.getRootMutationName())!.fields!.forEach(f => 31 | this.mutations.set(f.name, f) 32 | ); 33 | } 34 | 35 | public determineQueryMode(): ConnectionMode { 36 | let connection: GraphQLType | null = null; 37 | 38 | this.queries.forEach(query => { 39 | const typeName = Schema.getTypeNameOfField(query); 40 | if (typeName.endsWith("Connection")) { 41 | connection = this.getType(typeName); 42 | return false; // break 43 | } 44 | return true; 45 | }); 46 | 47 | /* istanbul ignore next */ 48 | if (!connection) { 49 | throw new Error( 50 | "Can't determine the connection mode due to the fact that here are no connection types in the schema. Please set the connectionMode via Vuex-ORM-GraphQL options!" 51 | ); 52 | } 53 | 54 | if (connection!.fields!.find(f => f.name === "nodes")) { 55 | return ConnectionMode.NODES; 56 | } else if (connection!.fields!.find(f => f.name === "edges")) { 57 | return ConnectionMode.EDGES; 58 | } else if (connection!.fields!.find(f => f.name === "items")) { 59 | return ConnectionMode.ITEMS; 60 | } else { 61 | return ConnectionMode.PLAIN; 62 | } 63 | } 64 | 65 | public getType(name: string, allowNull: boolean = false): GraphQLType | null { 66 | name = Context.getInstance().adapter.prepareSchemaTypeName(name); 67 | const type = this.types.get(name); 68 | 69 | if (!allowNull && !type) { 70 | throw new Error(`Couldn't find Type of name ${name} in the GraphQL Schema.`); 71 | } 72 | 73 | return type || null; 74 | } 75 | 76 | public getMutation(name: string, allowNull: boolean = false): GraphQLField | null { 77 | const mutation = this.mutations.get(name); 78 | 79 | /* istanbul ignore next */ 80 | if (!allowNull && !mutation) { 81 | throw new Error(`Couldn't find Mutation of name ${name} in the GraphQL Schema.`); 82 | } 83 | 84 | return mutation || null; 85 | } 86 | 87 | public getQuery(name: string, allowNull: boolean = false): GraphQLField | null { 88 | const query = this.queries.get(name); 89 | 90 | /* istanbul ignore next */ 91 | if (!allowNull && !query) { 92 | throw new Error(`Couldn't find Query of name ${name} in the GraphQL Schema.`); 93 | } 94 | 95 | return query || null; 96 | } 97 | 98 | static returnsConnection(field: GraphQLField): boolean { 99 | return Schema.getTypeNameOfField(field).endsWith("Connection"); 100 | } 101 | 102 | static getRealType(type: GraphQLTypeDefinition): GraphQLTypeDefinition { 103 | if (type.kind === "NON_NULL") { 104 | return this.getRealType(type.ofType); 105 | } else { 106 | return type; 107 | } 108 | } 109 | 110 | static getTypeNameOfField(field: GraphQLField): string { 111 | let type = this.getRealType(field.type); 112 | 113 | if (type.kind === "LIST") { 114 | while (!type.name) type = type.ofType; 115 | return `[${type.name}]`; 116 | } else { 117 | while (!type.name) type = type.ofType; 118 | 119 | /* istanbul ignore next */ 120 | if (!type.name) throw new Error(`Can't find type name for field ${field.name}`); 121 | 122 | return type.name; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import VuexORMGraphQLPlugin from "./plugin"; 2 | import DefaultAdapter from "./adapters/builtin/default-adapter"; 3 | import Adapter, { ConnectionMode, ArgumentMode } from "./adapters/adapter"; 4 | import Model from "./orm/model"; 5 | export default VuexORMGraphQLPlugin; 6 | 7 | export { setupTestUtils, mock, clearORMStore, Mock, MockOptions, ReturnValue } from "./test-utils"; 8 | export { DefaultAdapter, Adapter, ConnectionMode, ArgumentMode, Model }; 9 | -------------------------------------------------------------------------------- /src/orm/store.ts: -------------------------------------------------------------------------------- 1 | import { Data, DispatchFunction } from "../support/interfaces"; 2 | import Context from "../common/context"; 3 | 4 | /** 5 | * Provides some helper methods to interact with the Vuex-ORM store 6 | */ 7 | export class Store { 8 | /** 9 | * Inserts incoming data into the store. Existing data will be updated. 10 | * 11 | * @param {Data} data New data to insert/update 12 | * @param {Function} dispatch Vuex Dispatch method for the model 13 | * @return {Promise} Inserted data as hash 14 | */ 15 | public static async insertData(data: Data, dispatch: DispatchFunction): Promise { 16 | let insertedData: Data = {} as Data; 17 | 18 | await Promise.all( 19 | Object.keys(data).map(async key => { 20 | const value = data[key]; 21 | Context.getInstance().logger.log("Inserting records", value); 22 | const newData = await dispatch("insertOrUpdate", ({ data: value } as unknown) as Data); 23 | 24 | Object.keys(newData).forEach(dataKey => { 25 | if (!insertedData[dataKey]) insertedData[dataKey] = []; 26 | insertedData[dataKey] = insertedData[dataKey].concat(newData[dataKey]); 27 | }); 28 | }) 29 | ); 30 | 31 | return insertedData; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import VuexORMGraphQL from "./vuex-orm-graphql"; 2 | import { PluginComponents, Plugin } from "@vuex-orm/core/lib/plugins/use"; 3 | import { Options } from "./support/interfaces"; 4 | 5 | /** 6 | * Plugin class. This just provides a static install method for Vuex-ORM and stores the instance of the model 7 | * within this.instance. 8 | */ 9 | export default class VuexORMGraphQLPlugin implements Plugin { 10 | /** 11 | * Contains the instance of VuexORMGraphQL 12 | */ 13 | public static instance: VuexORMGraphQL; 14 | 15 | /** 16 | * This is called, when VuexORM.install(VuexOrmGraphQL, options) is called. 17 | * 18 | * @param {PluginComponents} components The Vuex-ORM Components collection 19 | * @param {Options} options The options passed to VuexORM.install 20 | * @returns {VuexORMGraphQL} 21 | */ 22 | public static install(components: PluginComponents, options: Options): VuexORMGraphQL { 23 | VuexORMGraphQLPlugin.instance = new VuexORMGraphQL(components, options); 24 | return VuexORMGraphQLPlugin.instance; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/support/interfaces.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { Database, Model as ORMModel } from "@vuex-orm/core"; 3 | import ORMInstance from "@vuex-orm/core/lib/data/Instance"; 4 | import RootState from "@vuex-orm/core/lib/modules/contracts/RootState"; 5 | import { ApolloLink } from "apollo-link"; 6 | import { DocumentNode } from "graphql/language/ast"; 7 | import Adapter from "../adapters/adapter"; 8 | 9 | export type DispatchFunction = (action: string, data: Data) => Promise; 10 | 11 | export interface Options { 12 | apolloClient: any; 13 | database: Database; 14 | url?: string; 15 | headers?: { [index: string]: any }; 16 | credentials?: string; 17 | useGETForQueries?: boolean; 18 | debug?: boolean; 19 | link?: ApolloLink; 20 | adapter?: Adapter; 21 | } 22 | 23 | export interface ActionParams { 24 | commit?: any; 25 | dispatch?: DispatchFunction; 26 | getters?: any; 27 | rootGetters?: any; 28 | rootState?: any; 29 | state?: RootState; 30 | filter?: Filter; 31 | id?: string; 32 | data?: Data; 33 | args?: Arguments; 34 | variables?: Arguments; 35 | bypassCache?: boolean; 36 | query?: string | DocumentNode; 37 | multiple?: boolean; 38 | name?: string; 39 | } 40 | 41 | export interface Data extends ORMInstance { 42 | [index: string]: any; 43 | } 44 | 45 | export interface Filter { 46 | [index: string]: any; 47 | } 48 | 49 | export interface Arguments { 50 | [index: string]: any; 51 | } 52 | 53 | export interface GraphQLType { 54 | description: string; 55 | name: string; 56 | fields?: Array; 57 | inputFields?: Array; 58 | } 59 | 60 | export interface GraphQLField { 61 | description: string; 62 | name: string; 63 | args: Array; 64 | type: GraphQLTypeDefinition; 65 | } 66 | 67 | export interface GraphQLTypeDefinition { 68 | kind: string; 69 | name?: string; 70 | ofType: GraphQLTypeDefinition; 71 | } 72 | 73 | export interface GraphQLSchema { 74 | types: Array; 75 | } 76 | 77 | export interface Field { 78 | related?: typeof ORMModel; 79 | parent?: typeof ORMModel; 80 | localKey?: string; 81 | foreignKey?: string; 82 | } 83 | 84 | export class PatchedModel extends ORMModel { 85 | static eagerLoad?: Array; 86 | static eagerSave?: Array; 87 | static eagerSync?: Array; 88 | static skipFields?: Array; 89 | 90 | $isPersisted: boolean = false; 91 | 92 | static async fetch(filter?: any, bypassCache: boolean = false): Promise { 93 | return undefined; 94 | } 95 | static async mutate(params: ActionParams): Promise { 96 | return undefined; 97 | } 98 | static async customQuery(params: ActionParams): Promise { 99 | return undefined; 100 | } 101 | 102 | async $mutate(params: ActionParams): Promise { 103 | return undefined; 104 | } 105 | 106 | async $customQuery(params: ActionParams): Promise { 107 | return undefined; 108 | } 109 | 110 | async $persist(args?: any): Promise { 111 | return undefined; 112 | } 113 | 114 | async $push(args?: any): Promise { 115 | return undefined; 116 | } 117 | 118 | async $destroy(): Promise { 119 | return undefined; 120 | } 121 | 122 | async $deleteAndDestroy(): Promise { 123 | return undefined; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/support/utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "graphql/language/parser"; 2 | import { print } from "graphql/language/printer"; 3 | import { DocumentNode } from "graphql/language/ast"; 4 | 5 | // @ts-ignore 6 | import lodashIsEqual from "lodash.isequal"; 7 | 8 | // @ts-ignore 9 | import lodashClone from "lodash.clone"; 10 | 11 | // @ts-ignore 12 | import pluralizeLib from "pluralize"; 13 | export const pluralize = pluralizeLib.plural; 14 | export const singularize = pluralizeLib.singular; 15 | 16 | /** 17 | * Capitalizes the first letter of the given string. 18 | * 19 | * @param {string} input 20 | * @returns {string} 21 | */ 22 | export function upcaseFirstLetter(input: string) { 23 | return input.charAt(0).toUpperCase() + input.slice(1); 24 | } 25 | 26 | /** 27 | * Down cases the first letter of the given string. 28 | * 29 | * @param {string} input 30 | * @returns {string} 31 | */ 32 | export function downcaseFirstLetter(input: string) { 33 | return input.charAt(0).toLowerCase() + input.slice(1); 34 | } 35 | 36 | /** 37 | * Takes a string with a graphql query and formats it. Useful for debug output and the tests. 38 | * @param {string} query 39 | * @returns {string} 40 | */ 41 | export function prettify(query: string | DocumentNode): string { 42 | return print(parseQuery(query)); 43 | } 44 | 45 | /** 46 | * Returns a parsed query as GraphQL AST DocumentNode. 47 | * 48 | * @param {string | DocumentNode} query - Query as string or GraphQL AST DocumentNode. 49 | * 50 | * @returns {DocumentNode} Query as GraphQL AST DocumentNode. 51 | */ 52 | export function parseQuery(query: string | DocumentNode): DocumentNode { 53 | return typeof query === "string" ? parse(query) : query; 54 | } 55 | 56 | /** 57 | * @param {DocumentNode} query - The GraphQL AST DocumentNode. 58 | * 59 | * @returns {string} the GraphQL query within a DocumentNode as a plain string. 60 | */ 61 | export function graphQlDocumentToString(query: DocumentNode): string { 62 | return query.loc!.source.body; 63 | } 64 | 65 | /** 66 | * Tells if a object is just a simple object. 67 | * 68 | * @param {any} obj - Value to check. 69 | */ 70 | export function isPlainObject(obj: any): boolean { 71 | // Basic check for Type object that's not null 72 | return obj !== null && !(obj instanceof Array) && typeof obj === "object"; 73 | } 74 | 75 | /** 76 | * Creates an object composed of the picked `object` properties. 77 | * @param {object} object - Object. 78 | * @param {array} props - Properties to pick. 79 | */ 80 | export function pick(object: any, props: Array) { 81 | if (!object) { 82 | return {}; 83 | } 84 | 85 | let index = -1; 86 | const length = props.length; 87 | const result = {}; 88 | 89 | while (++index < length) { 90 | const prop = props[index]; 91 | result[prop] = object[prop]; 92 | } 93 | 94 | return result; 95 | } 96 | 97 | export function isEqual(a: object, b: object): boolean { 98 | // Couldn' find a simpler working implementation yet. 99 | return lodashIsEqual(a, b); 100 | } 101 | 102 | export function clone(input: any): any { 103 | // Couldn' find a simpler working implementation yet. 104 | return lodashClone(input); 105 | } 106 | 107 | export function takeWhile( 108 | array: Array, 109 | predicate: (x: any, idx: number, array: Array) => any 110 | ) { 111 | let index = -1; 112 | 113 | while (++index < array.length && predicate(array[index], index, array)) { 114 | // just increase index 115 | } 116 | 117 | return array.slice(0, index); 118 | } 119 | 120 | export function matches(source: any) { 121 | source = clone(source); 122 | 123 | return (object: any) => isEqual(object, source); 124 | } 125 | 126 | export function removeSymbols(input: any) { 127 | return JSON.parse(JSON.stringify(input)); 128 | } 129 | 130 | /** 131 | * Converts the argument into a number. 132 | */ 133 | export function toPrimaryKey(input: string | number | null): number | string { 134 | if (input === null) return 0; 135 | 136 | if (typeof input === "string" && (input.startsWith("$uid") || isGuid(input))) { 137 | return input; 138 | } 139 | 140 | return parseInt(input.toString(), 10); 141 | } 142 | 143 | export function isGuid(value: string): boolean { 144 | const regex = /[a-f0-9]{8}(?:-?[a-f0-9]{4}){3}-?[a-f0-9]{12}/i; 145 | const match = regex.exec(value); 146 | return match !== null; 147 | } 148 | -------------------------------------------------------------------------------- /src/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { Model as ORMModel } from "@vuex-orm/core"; 2 | import Context from "./common/context"; 3 | import Model from "./orm/model"; 4 | import VuexORMGraphQLPlugin from "./index"; 5 | 6 | let context: Context | null = null; 7 | 8 | export function setupTestUtils(plugin: typeof VuexORMGraphQLPlugin): void { 9 | /* istanbul ignore next */ 10 | if (!plugin.instance) { 11 | throw new Error("Please call this function after setting up the store!"); 12 | } 13 | 14 | context = plugin.instance.getContext(); 15 | } 16 | 17 | export interface MockOptions { 18 | [key: string]: any; 19 | } 20 | 21 | type ReturnObject = { [key: string]: any }; 22 | 23 | export type ReturnValue = 24 | | (() => ReturnObject | Array) 25 | | ReturnObject 26 | | Array; 27 | 28 | export class Mock { 29 | public readonly action: string; 30 | public readonly options?: MockOptions; 31 | public modelClass?: typeof ORMModel; 32 | public returnValue?: ReturnValue; 33 | 34 | constructor(action: string, options?: MockOptions) { 35 | this.action = action; 36 | this.options = options; 37 | } 38 | 39 | public for(modelClass: typeof ORMModel): Mock { 40 | this.modelClass = modelClass; 41 | return this; 42 | } 43 | 44 | public andReturn(returnValue: ReturnValue): Mock { 45 | this.returnValue = returnValue; 46 | this.installMock(); 47 | return this; 48 | } 49 | 50 | private installMock(): void { 51 | if (this.action === "simpleQuery" || this.action === "simpleMutation") { 52 | context!.addGlobalMock(this); 53 | } else { 54 | const model: Model = context!.getModel(this.modelClass!.entity); 55 | model.$addMock(this); 56 | } 57 | } 58 | } 59 | 60 | export async function clearORMStore() { 61 | /* istanbul ignore next */ 62 | if (!context) { 63 | throw new Error("Please call setupTestUtils() before!"); 64 | } 65 | 66 | await context.database.store.dispatch("entities/deleteAll"); 67 | } 68 | 69 | export function mock(action: string, options?: MockOptions): Mock { 70 | /* istanbul ignore next */ 71 | if (!context) { 72 | throw new Error("Please call setupTestUtils() before!"); 73 | } 74 | 75 | return new Mock(action, options); 76 | } 77 | -------------------------------------------------------------------------------- /src/vuex-orm-graphql.ts: -------------------------------------------------------------------------------- 1 | import { Options } from "./support/interfaces"; 2 | import Context from "./common/context"; 3 | import { PluginComponents } from "@vuex-orm/core/lib/plugins/use"; 4 | import { 5 | Destroy, 6 | Fetch, 7 | Mutate, 8 | Persist, 9 | Push, 10 | Query, 11 | SimpleQuery, 12 | SimpleMutation 13 | } from "./actions"; 14 | 15 | /** 16 | * Main class of the plugin. Setups the internal context, Vuex actions and model methods 17 | */ 18 | export default class VuexORMGraphQL { 19 | /** 20 | * @constructor 21 | * @param {PluginComponents} components The Vuex-ORM Components collection 22 | * @param {Options} options The options passed to VuexORM.install 23 | */ 24 | public constructor(components: PluginComponents, options: Options) { 25 | Context.setup(components, options); 26 | VuexORMGraphQL.setupActions(); 27 | } 28 | 29 | /** 30 | * Allow everything to read the context. 31 | */ 32 | public getContext(): Context { 33 | return Context.getInstance(); 34 | } 35 | 36 | /** 37 | * This method will setup: 38 | * - Vuex actions: fetch, persist, push, destroy, mutate 39 | * - Model methods: fetch(), mutate(), customQuery() 40 | * - Record method: $mutate(), $persist(), $push(), $destroy(), $deleteAndDestroy(), $customQuery() 41 | */ 42 | private static setupActions() { 43 | Fetch.setup(); 44 | Persist.setup(); 45 | Push.setup(); 46 | Destroy.setup(); 47 | Mutate.setup(); 48 | Query.setup(); 49 | SimpleQuery.setup(); 50 | SimpleMutation.setup(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | global.expect = require('expect'); 2 | global.sinon = require('sinon'); 3 | -------------------------------------------------------------------------------- /test/integration/actions/customMutation.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | import { recordGraphQLRequest } from "../../support/helpers"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("custom mutation", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | describe("via Model.mutate", () => { 14 | test("sends the correct query to the API", async () => { 15 | // @ts-ignore 16 | await Post.fetch(1); 17 | const post: Data = Post.find(1)! as Data; 18 | 19 | const request = await recordGraphQLRequest(async () => { 20 | // @ts-ignore 21 | await Post.mutate({ name: "upvotePost", args: { captchaToken: "15", id: post.id } }); 22 | }); 23 | 24 | expect(request!.variables.captchaToken).toEqual("15"); 25 | expect(request!.variables.id).toEqual(post.id); 26 | expect(request!.query).toEqual( 27 | ` 28 | mutation UpvotePost($captchaToken: String!, $id: ID!) { 29 | upvotePost(captchaToken: $captchaToken, id: $id) { 30 | id 31 | content 32 | title 33 | otherId 34 | published 35 | author { 36 | id 37 | name 38 | role 39 | profile { 40 | id 41 | email 42 | age 43 | sex 44 | } 45 | } 46 | comments { 47 | nodes { 48 | id 49 | content 50 | subjectId 51 | subjectType 52 | author { 53 | id 54 | name 55 | role 56 | profile { 57 | id 58 | email 59 | age 60 | sex 61 | } 62 | } 63 | } 64 | } 65 | tags { 66 | nodes { 67 | id 68 | name 69 | } 70 | } 71 | } 72 | } 73 | `.trim() + "\n" 74 | ); 75 | }); 76 | }); 77 | 78 | describe("via post.$mutate", () => { 79 | test("sends the correct query to the API", async () => { 80 | // @ts-ignore 81 | await Post.fetch(1); 82 | const post: Data = Post.find(1)! as Data; 83 | 84 | const request = await recordGraphQLRequest(async () => { 85 | // @ts-ignore 86 | await post.$mutate({ name: "upvotePost", args: { captchaToken: "15" } }); 87 | }); 88 | 89 | expect(request!.variables.captchaToken).toEqual("15"); 90 | expect(request!.variables.id).toEqual(post.id); 91 | expect(request!.query).toEqual( 92 | ` 93 | mutation UpvotePost($captchaToken: String!, $id: ID!) { 94 | upvotePost(captchaToken: $captchaToken, id: $id) { 95 | id 96 | content 97 | title 98 | otherId 99 | published 100 | author { 101 | id 102 | name 103 | role 104 | profile { 105 | id 106 | email 107 | age 108 | sex 109 | } 110 | } 111 | comments { 112 | nodes { 113 | id 114 | content 115 | subjectId 116 | subjectType 117 | author { 118 | id 119 | name 120 | role 121 | profile { 122 | id 123 | email 124 | age 125 | sex 126 | } 127 | } 128 | } 129 | } 130 | tags { 131 | nodes { 132 | id 133 | name 134 | } 135 | } 136 | } 137 | } 138 | `.trim() + "\n" 139 | ); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/integration/actions/customQuery.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | import { recordGraphQLRequest } from "../../support/helpers"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("custom query", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | test("via Model method sends the correct query to the API", async () => { 14 | const request = await recordGraphQLRequest(async () => { 15 | // @ts-ignore 16 | await Post.customQuery({ name: "unpublishedPosts", filter: { authorId: 3 } }); 17 | }); 18 | 19 | expect(request!.variables.authorId).toEqual(3); 20 | expect(request!.query).toEqual( 21 | ` 22 | query UnpublishedPosts($authorId: ID!) { 23 | unpublishedPosts(authorId: $authorId) { 24 | nodes { 25 | id 26 | content 27 | title 28 | otherId 29 | published 30 | author { 31 | id 32 | name 33 | role 34 | profile { 35 | id 36 | email 37 | age 38 | sex 39 | } 40 | } 41 | comments { 42 | nodes { 43 | id 44 | content 45 | subjectId 46 | subjectType 47 | author { 48 | id 49 | name 50 | role 51 | profile { 52 | id 53 | email 54 | age 55 | sex 56 | } 57 | } 58 | } 59 | } 60 | tags { 61 | nodes { 62 | id 63 | name 64 | } 65 | } 66 | } 67 | } 68 | } 69 | `.trim() + "\n" 70 | ); 71 | }); 72 | 73 | test("via record method sends the correct query to the API", async () => { 74 | // @ts-ignore 75 | await Post.fetch(1); 76 | const post: Data = Post.find(1)! as Data; 77 | 78 | const request = await recordGraphQLRequest(async () => { 79 | await post.$customQuery({ name: "unpublishedPosts", filter: { authorId: 2 } }); 80 | }); 81 | 82 | expect(request!.variables.authorId).toEqual(2); 83 | expect(request!.variables.id).toEqual(1); 84 | expect(request!.query).toEqual( 85 | ` 86 | query UnpublishedPosts($authorId: ID!, $id: ID!) { 87 | unpublishedPosts(authorId: $authorId, id: $id) { 88 | nodes { 89 | id 90 | content 91 | title 92 | otherId 93 | published 94 | author { 95 | id 96 | name 97 | role 98 | profile { 99 | id 100 | email 101 | age 102 | sex 103 | } 104 | } 105 | comments { 106 | nodes { 107 | id 108 | content 109 | subjectId 110 | subjectType 111 | author { 112 | id 113 | name 114 | role 115 | profile { 116 | id 117 | email 118 | age 119 | sex 120 | } 121 | } 122 | } 123 | } 124 | tags { 125 | nodes { 126 | id 127 | name 128 | } 129 | } 130 | } 131 | } 132 | } 133 | `.trim() + "\n" 134 | ); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/integration/actions/deleteAndDestroy.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | import { recordGraphQLRequest } from "../../support/helpers"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("deleteAndDestroy", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | test("sends the correct query to the API and deletes the record", async () => { 14 | // @ts-ignore 15 | await Post.fetch(1); 16 | const post: Data = Post.find(1)! as Data; 17 | 18 | const request = await recordGraphQLRequest(async () => { 19 | await post.$deleteAndDestroy(); 20 | }); 21 | 22 | expect(request!.variables).toEqual({ id: 1 }); 23 | expect(request!.query).toEqual( 24 | ` 25 | mutation DeletePost($id: ID!) { 26 | deletePost(id: $id) { 27 | id 28 | content 29 | title 30 | otherId 31 | published 32 | author { 33 | id 34 | name 35 | role 36 | profile { 37 | id 38 | email 39 | age 40 | sex 41 | } 42 | } 43 | comments { 44 | nodes { 45 | id 46 | content 47 | subjectId 48 | subjectType 49 | author { 50 | id 51 | name 52 | role 53 | profile { 54 | id 55 | email 56 | age 57 | sex 58 | } 59 | } 60 | } 61 | } 62 | tags { 63 | nodes { 64 | id 65 | name 66 | } 67 | } 68 | } 69 | } 70 | `.trim() + "\n" 71 | ); 72 | 73 | expect(await Post.find(1)).toEqual(null); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/integration/actions/destroy.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | import { recordGraphQLRequest } from "../../support/helpers"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("destroy", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | test("sends the correct query to the API", async () => { 14 | // @ts-ignore 15 | await Post.fetch(1); 16 | const post: Data = Post.find(1)! as Data; 17 | 18 | const request = await recordGraphQLRequest(async () => { 19 | await post.$destroy(); 20 | }); 21 | 22 | expect(request!.variables).toEqual({ id: 1 }); 23 | expect(request!.query).toEqual( 24 | ` 25 | mutation DeletePost($id: ID!) { 26 | deletePost(id: $id) { 27 | id 28 | content 29 | title 30 | otherId 31 | published 32 | author { 33 | id 34 | name 35 | role 36 | profile { 37 | id 38 | email 39 | age 40 | sex 41 | } 42 | } 43 | comments { 44 | nodes { 45 | id 46 | content 47 | subjectId 48 | subjectType 49 | author { 50 | id 51 | name 52 | role 53 | profile { 54 | id 55 | email 56 | age 57 | sex 58 | } 59 | } 60 | } 61 | } 62 | tags { 63 | nodes { 64 | id 65 | name 66 | } 67 | } 68 | } 69 | } 70 | `.trim() + "\n" 71 | ); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/integration/actions/fetch.spec.ts: -------------------------------------------------------------------------------- 1 | import { recordGraphQLRequest } from "../../support/helpers"; 2 | import { Category, Post, Profile, setupMockData, User } from "../../support/mock-data"; 3 | import { Data } from "../../../src/support/interfaces"; 4 | import Context from "../../../src/common/context"; 5 | 6 | let store: any; 7 | let vuexOrmGraphQL; 8 | 9 | describe("fetch", () => { 10 | beforeEach(async () => { 11 | [store, vuexOrmGraphQL] = await setupMockData(); 12 | }); 13 | 14 | test("also requests the otherId field", async () => { 15 | const request = await recordGraphQLRequest(async () => { 16 | // @ts-ignore 17 | await Post.fetch(1); 18 | }); 19 | 20 | expect(request!.query).toEqual( 21 | ` 22 | query Post($id: ID!) { 23 | post(id: $id) { 24 | id 25 | content 26 | title 27 | otherId 28 | published 29 | author { 30 | id 31 | name 32 | role 33 | profile { 34 | id 35 | email 36 | age 37 | sex 38 | } 39 | } 40 | comments { 41 | nodes { 42 | id 43 | content 44 | subjectId 45 | subjectType 46 | author { 47 | id 48 | name 49 | role 50 | profile { 51 | id 52 | email 53 | age 54 | sex 55 | } 56 | } 57 | } 58 | } 59 | tags { 60 | nodes { 61 | id 62 | name 63 | } 64 | } 65 | } 66 | } 67 | `.trim() + "\n" 68 | ); 69 | 70 | const post: Data = Post.query() 71 | .withAll() 72 | .where("id", 1) 73 | .first()! as Data; 74 | 75 | expect(post.title).toEqual("GraphQL"); 76 | expect(post.content).toEqual("GraphQL is so nice!"); 77 | expect(post.comments.length).toEqual(1); 78 | expect(post.comments[0].content).toEqual("Yes!!!!"); 79 | }); 80 | 81 | describe("with ID", () => { 82 | test("doesn't cache when bypassCache = true", async () => { 83 | let request1 = await recordGraphQLRequest(async () => { 84 | // @ts-ignore 85 | await User.fetch(1); 86 | }, true); 87 | expect(request1).not.toEqual(null); 88 | 89 | let request2 = await recordGraphQLRequest(async () => { 90 | // @ts-ignore 91 | await User.fetch(1); 92 | }, true); 93 | expect(request2).toEqual(null); 94 | 95 | let request3 = await recordGraphQLRequest(async () => { 96 | // @ts-ignore 97 | await User.fetch(1, true); 98 | }, true); 99 | expect(request3).not.toEqual(null); 100 | }); 101 | 102 | test("sends the correct query to the API", async () => { 103 | const request = await recordGraphQLRequest(async () => { 104 | // @ts-ignore 105 | await User.fetch(1); 106 | }); 107 | 108 | expect(request!.variables).toEqual({ id: 1 }); 109 | expect(request!.query).toEqual( 110 | ` 111 | query User($id: ID!) { 112 | user(id: $id) { 113 | id 114 | name 115 | role 116 | profile { 117 | id 118 | email 119 | age 120 | sex 121 | } 122 | } 123 | } 124 | `.trim() + "\n" 125 | ); 126 | }); 127 | }); 128 | 129 | describe("without ID but with filter with array", () => { 130 | test("sends the correct query to the API", async () => { 131 | // @ts-ignore 132 | await User.fetch(1); 133 | 134 | const insertedData = await Post.insert({ 135 | data: { 136 | title: "It works!", 137 | content: "This is a test!", 138 | published: false, 139 | otherId: 15, 140 | author: User.find(1) 141 | } 142 | }); 143 | 144 | const post = insertedData.posts[0]; 145 | 146 | const request = await recordGraphQLRequest(async () => { 147 | // @ts-ignore 148 | await User.fetch({ profileId: 2, posts: [post] }); 149 | }); 150 | 151 | expect(request!.variables).toMatchObject({ 152 | profileId: 2, 153 | posts: [ 154 | { 155 | id: expect.stringMatching(/(\$uid\d+)/), 156 | authorId: 1, 157 | content: "This is a test!", 158 | otherId: 15, 159 | published: false, 160 | title: "It works!" 161 | } 162 | ] 163 | }); 164 | expect(request!.query).toEqual( 165 | ` 166 | query Users($profileId: ID!, $posts: [PostFilter]!) { 167 | users(filter: {profileId: $profileId, posts: $posts}) { 168 | nodes { 169 | id 170 | name 171 | role 172 | profile { 173 | id 174 | email 175 | age 176 | sex 177 | } 178 | } 179 | } 180 | } 181 | `.trim() + "\n" 182 | ); 183 | }); 184 | }); 185 | 186 | describe("without ID but with filter with object", () => { 187 | test("sends the correct query to the API", async () => { 188 | // @ts-ignore 189 | await Profile.fetch(2); 190 | const profile: Data = Context.getInstance() 191 | .getModel("profile") 192 | .getRecordWithId(2)! as Data; 193 | 194 | const request = await recordGraphQLRequest(async () => { 195 | // @ts-ignore 196 | await User.fetch({ profile }); 197 | }); 198 | 199 | expect(request!.variables).toEqual({ 200 | profile: { 201 | id: profile.id, 202 | email: profile.email, 203 | age: profile.age, 204 | sex: profile.sex 205 | } 206 | }); 207 | 208 | expect(request!.query).toEqual( 209 | ` 210 | query Users($profile: ProfileInput!) { 211 | users(filter: {profile: $profile}) { 212 | nodes { 213 | id 214 | name 215 | role 216 | profile { 217 | id 218 | email 219 | age 220 | sex 221 | } 222 | } 223 | } 224 | } 225 | `.trim() + "\n" 226 | ); 227 | }); 228 | }); 229 | 230 | describe("without ID or filter", () => { 231 | test("sends the correct query to the API", async () => { 232 | const request = await recordGraphQLRequest(async () => { 233 | // @ts-ignore 234 | await User.fetch(); 235 | }); 236 | 237 | expect(request!.variables).toEqual({}); 238 | expect(request!.query).toEqual( 239 | ` 240 | query Users { 241 | users { 242 | nodes { 243 | id 244 | name 245 | role 246 | profile { 247 | id 248 | email 249 | age 250 | sex 251 | } 252 | } 253 | } 254 | } 255 | `.trim() + "\n" 256 | ); 257 | }); 258 | }); 259 | 260 | describe("without ID or filter and no FilterType exists", () => { 261 | test("sends the correct query to the API", async () => { 262 | const request = await recordGraphQLRequest(async () => { 263 | // @ts-ignore 264 | await Category.fetch(); 265 | }); 266 | 267 | expect(request!.variables).toEqual({}); 268 | expect(request!.query).toEqual( 269 | ` 270 | query Categories { 271 | categories { 272 | nodes { 273 | id 274 | name 275 | parent { 276 | id 277 | name 278 | parent { 279 | id 280 | name 281 | parent { 282 | id 283 | name 284 | parent { 285 | id 286 | name 287 | parent { 288 | id 289 | name 290 | } 291 | } 292 | } 293 | } 294 | } 295 | } 296 | } 297 | } 298 | `.trim() + "\n" 299 | ); 300 | }); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /test/integration/actions/persist.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData, User } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | import { recordGraphQLRequest } from "../../support/helpers"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("persist", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | // Skipped due to https://github.com/vuex-orm/vuex-orm/issues/367 14 | test.skip("sends the correct query to the API", async () => { 15 | // @ts-ignore 16 | await User.fetch(1); 17 | 18 | const insertedData = await Post.insert({ 19 | data: { 20 | title: "It works!", 21 | content: "This is a test!", 22 | published: false, 23 | otherId: 15, 24 | author: User.find(1), 25 | tags: [{ name: "Foo" }, { name: "Bar" }] 26 | } 27 | }); 28 | 29 | let post: Data = insertedData.posts[0] as Data; 30 | 31 | expect(post.tags.length).toEqual(2); 32 | 33 | const request = await recordGraphQLRequest(async () => { 34 | post = await post.$persist(); 35 | }); 36 | 37 | expect(post.id).toEqual(4); // was set from the server 38 | 39 | expect(request!.variables).toEqual({ 40 | post: { 41 | content: "This is a test!", 42 | id: 1, 43 | otherId: 15, 44 | published: false, 45 | title: "It works!", 46 | authorId: 1, 47 | tags: [{ name: "Foo" }, { name: "Bar" }], 48 | author: { 49 | id: 1, 50 | name: "Charlie Brown", 51 | profileId: 1, 52 | profile: { 53 | id: 1, 54 | sex: true, 55 | age: 8, 56 | email: "charlie@peanuts.com" 57 | } 58 | } 59 | } 60 | }); 61 | 62 | expect(request!.query).toEqual( 63 | ` 64 | mutation CreatePost($post: PostInput!) { 65 | createPost(post: $post) { 66 | id 67 | content 68 | title 69 | otherId 70 | published 71 | author { 72 | id 73 | name 74 | profile { 75 | id 76 | email 77 | age 78 | sex 79 | } 80 | } 81 | comments { 82 | nodes { 83 | id 84 | content 85 | subjectId 86 | subjectType 87 | author { 88 | id 89 | name 90 | profile { 91 | id 92 | email 93 | age 94 | sex 95 | } 96 | } 97 | } 98 | } 99 | tags { 100 | nodes { 101 | id 102 | name 103 | } 104 | } 105 | } 106 | } 107 | `.trim() + "\n" 108 | ); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/integration/actions/push.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData, User } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | import { recordGraphQLRequest } from "../../support/helpers"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("push", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | test("sends the correct query to the API", async () => { 14 | // @ts-ignore 15 | await User.fetch(1); 16 | const user: Data = User.find(1)! as Data; 17 | user.name = "Snoopy"; 18 | 19 | const request = await recordGraphQLRequest(async () => { 20 | await user.$push(); 21 | }); 22 | 23 | expect(request!.variables).toEqual({ 24 | id: 1, 25 | user: { id: 1, name: "Snoopy", profileId: 1 } 26 | }); 27 | expect(request!.query).toEqual( 28 | ` 29 | mutation UpdateUser($id: ID!, $user: UserInput!) { 30 | updateUser(id: $id, user: $user) { 31 | id 32 | name 33 | role 34 | profile { 35 | id 36 | email 37 | age 38 | sex 39 | } 40 | } 41 | } 42 | `.trim() + "\n" 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/integration/actions/simpleMutation.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData } from "../../support/mock-data"; 2 | import { recordGraphQLRequest } from "../../support/helpers"; 3 | import gql from "graphql-tag"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("simple mutation", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | test("sends my query to the api", async () => { 14 | let result; 15 | 16 | const query = ` 17 | mutation SendSms($to: String!, $text: String!) { 18 | sendSms(to: $to, text: $text) { 19 | delivered 20 | } 21 | }`; 22 | 23 | const request = await recordGraphQLRequest(async () => { 24 | result = await store.dispatch("entities/simpleMutation", { 25 | query, 26 | variables: { to: "+4912345678", text: "GraphQL is awesome!" } 27 | }); 28 | }); 29 | 30 | expect(request!.variables).toEqual({ to: "+4912345678", text: "GraphQL is awesome!" }); 31 | expect(result).toEqual({ 32 | sendSms: { 33 | __typename: "SmsStatus", // TODO: this could removed by Vuex-ORM-GraphQL IMHO 34 | delivered: true 35 | } 36 | }); 37 | expect(request!.query).toEqual( 38 | ` 39 | mutation SendSms($to: String!, $text: String!) { 40 | sendSms(to: $to, text: $text) { 41 | delivered 42 | } 43 | } 44 | `.trim() + "\n" 45 | ); 46 | }); 47 | 48 | test("also accepts GraphQL AST DocumentNode", async () => { 49 | let result; 50 | 51 | const query = gql` 52 | mutation SendSms($to: String!, $text: String!) { 53 | sendSms(to: $to, text: $text) { 54 | delivered 55 | } 56 | } 57 | `; 58 | 59 | const request = await recordGraphQLRequest(async () => { 60 | result = await store.dispatch("entities/simpleMutation", { 61 | query, 62 | variables: { to: "+4912345678", text: "GraphQL is awesome!" } 63 | }); 64 | }); 65 | 66 | expect(request!.variables).toEqual({ to: "+4912345678", text: "GraphQL is awesome!" }); 67 | expect(result).toEqual({ 68 | sendSms: { 69 | __typename: "SmsStatus", // TODO: this could removed by Vuex-ORM-GraphQL IMHO 70 | delivered: true 71 | } 72 | }); 73 | expect(request!.query).toEqual( 74 | ` 75 | mutation SendSms($to: String!, $text: String!) { 76 | sendSms(to: $to, text: $text) { 77 | delivered 78 | } 79 | } 80 | `.trim() + "\n" 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/integration/actions/simpleQuery.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData } from "../../support/mock-data"; 2 | import { recordGraphQLRequest } from "../../support/helpers"; 3 | import gql from "graphql-tag"; 4 | 5 | let store: any; 6 | let vuexOrmGraphQL; 7 | 8 | describe("simple query", () => { 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | }); 12 | 13 | test("sends my query to the api", async () => { 14 | let result; 15 | 16 | const query = ` 17 | query Status { 18 | status { 19 | backend 20 | smsGateway 21 | paypalIntegration 22 | } 23 | }`; 24 | 25 | const request = await recordGraphQLRequest(async () => { 26 | result = await store.dispatch("entities/simpleQuery", { query, variables: {} }); 27 | }); 28 | 29 | expect(result).toEqual({ 30 | status: { 31 | __typename: "Status", 32 | backend: true, 33 | paypalIntegration: true, 34 | smsGateway: false 35 | } 36 | }); 37 | 38 | expect(request!.query).toEqual( 39 | ` 40 | query Status { 41 | status { 42 | backend 43 | smsGateway 44 | paypalIntegration 45 | } 46 | } 47 | `.trim() + "\n" 48 | ); 49 | }); 50 | test("also accepts GraphQL AST DocumentNode", async () => { 51 | let result; 52 | 53 | const query = gql` 54 | query Status { 55 | status { 56 | backend 57 | smsGateway 58 | paypalIntegration 59 | } 60 | } 61 | `; 62 | 63 | const request = await recordGraphQLRequest(async () => { 64 | result = await store.dispatch("entities/simpleQuery", { query, variables: {} }); 65 | }); 66 | 67 | expect(result).toEqual({ 68 | status: { 69 | __typename: "Status", 70 | backend: true, 71 | paypalIntegration: true, 72 | smsGateway: false 73 | } 74 | }); 75 | 76 | expect(request!.query).toEqual( 77 | ` 78 | query Status { 79 | status { 80 | backend 81 | smsGateway 82 | paypalIntegration 83 | } 84 | } 85 | `.trim() + "\n" 86 | ); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/integration/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData, User, Post, Tariff, Tag } from "../support/mock-data"; 2 | import Context from "../../src/common/context"; 3 | import { recordGraphQLRequest } from "../support/helpers"; 4 | import { Data } from "../../src/support/interfaces"; 5 | 6 | let store: any; 7 | let vuexOrmGraphQL; 8 | 9 | describe("Plugin GraphQL", () => { 10 | beforeEach(async () => { 11 | [store, vuexOrmGraphQL] = await setupMockData(); 12 | }); 13 | 14 | test("fetches the schema on the first action", async () => { 15 | let result; 16 | 17 | const request = await recordGraphQLRequest(async () => { 18 | // @ts-ignore 19 | result = await Post.fetch(2); 20 | }); 21 | 22 | expect(request).not.toEqual(null); 23 | expect(result).not.toEqual(null); 24 | 25 | const context = Context.getInstance(); 26 | expect(!!context.schema).not.toEqual(false); 27 | expect(context.schema!.getType("Post")!.name).toEqual("Post"); 28 | expect(context.schema!.getQuery("post")!.name).toEqual("post"); 29 | expect(context.schema!.getMutation("createPost")!.name).toEqual("createPost"); 30 | }); 31 | 32 | describe("$isPersisted", () => { 33 | test("is false for newly created records", async () => { 34 | await Context.getInstance().loadSchema(); 35 | 36 | const insertedData = await User.insert({ data: { name: "Snoopy" } }); 37 | let user: Data = insertedData.users[0] as Data; 38 | expect(user.$isPersisted).toBeFalsy(); 39 | 40 | user = User.find(user.id)! as Data; 41 | expect(user.$isPersisted).toBeFalsy(); 42 | }); 43 | 44 | test("is true for persisted records", async () => { 45 | await Context.getInstance().loadSchema(); 46 | 47 | const insertedData = await User.insert({ data: { name: "Snoopy" } }); 48 | let user: Data = insertedData.users[0] as Data; 49 | 50 | expect(user.$isPersisted).toBeFalsy(); 51 | 52 | const result = await user.$persist(); 53 | user = User.query().last() as Data; 54 | 55 | expect(user.$isPersisted).toBeTruthy(); 56 | }); 57 | 58 | test("is true for single fetched records", async () => { 59 | // @ts-ignore 60 | await Post.fetch(1); 61 | 62 | let post: Data = Post.query().first()! as Data; 63 | expect(post.$isPersisted).toBeTruthy(); 64 | }); 65 | 66 | test("is true for multiple fetched records", async () => { 67 | await Post.fetch(); 68 | 69 | let post: Data = Post.query().first()! as Data; 70 | expect(post.$isPersisted).toBeTruthy(); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/integration/relations/has-many-through.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData } from "../../support/mock-data"; 2 | 3 | let store: any; 4 | let vuexOrmGraphQL; 5 | 6 | describe("Has Many Through", () => { 7 | beforeEach(async () => { 8 | [store, vuexOrmGraphQL] = await setupMockData(); 9 | }); 10 | 11 | test("works", async () => { 12 | // TODO 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/integration/relations/many-to-many.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData, Tariff } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | 4 | let store: any; 5 | let vuexOrmGraphQL; 6 | 7 | describe("Many To Many Relation", () => { 8 | beforeEach(async () => { 9 | [store, vuexOrmGraphQL] = await setupMockData(); 10 | }); 11 | 12 | test("works", async () => { 13 | // @ts-ignore 14 | await Tariff.fetch(); 15 | 16 | const tariff: Data = Tariff.query() 17 | .withAllRecursive() 18 | .find("ED5F2379-6A8B-4E1D-A4E3-A2C03057C2FC")! as Data; 19 | 20 | expect(tariff.name).toEqual("Super DSL S"); 21 | expect(tariff.tariffOptions).not.toEqual(null); 22 | expect(tariff.tariffOptions.length).not.toEqual(0); 23 | expect(tariff.tariffOptions[0].name).toEqual("Installation"); 24 | expect(tariff.tariffOptions[0].tariffs).not.toEqual(null); 25 | expect(tariff.tariffOptions[0].tariffs[0].name).toEqual("Super DSL S"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/integration/relations/one-to-many.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData, User } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | 4 | let store: any; 5 | let vuexOrmGraphQL; 6 | 7 | describe("One To Many Relation", () => { 8 | beforeEach(async () => { 9 | [store, vuexOrmGraphQL] = await setupMockData(); 10 | }); 11 | 12 | test("works", async () => { 13 | // @ts-ignore 14 | await User.fetch(); 15 | 16 | // @ts-ignore 17 | await Post.fetch(); 18 | 19 | const user: Data = User.query() 20 | .withAllRecursive() 21 | .first()! as Data; 22 | 23 | expect(user.name).toEqual("Charlie Brown"); 24 | expect(user.posts.length).not.toEqual(0); 25 | expect(user.posts[0].title).toEqual("GraphQL"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/integration/relations/one-to-one.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData, User } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | 4 | let store: any; 5 | let vuexOrmGraphQL; 6 | 7 | describe("One To One Relation", () => { 8 | beforeEach(async () => { 9 | [store, vuexOrmGraphQL] = await setupMockData(); 10 | }); 11 | 12 | test("works", async () => { 13 | // @ts-ignore 14 | await User.fetch(1, true); 15 | 16 | const user: Data = User.query() 17 | .withAllRecursive() 18 | .find(1)! as Data; 19 | 20 | expect(user.name).toEqual("Charlie Brown"); 21 | expect(user.profile).not.toEqual(null); 22 | expect(user.profile.sex).toEqual(true); 23 | expect(user.profile.email).toEqual("charlie@peanuts.com"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/integration/relations/polymorphic-has-many.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData, Tariff, User } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | 4 | let store: any; 5 | let vuexOrmGraphQL; 6 | 7 | describe("Polymorphic Has Many", () => { 8 | beforeEach(async () => { 9 | [store, vuexOrmGraphQL] = await setupMockData(); 10 | }); 11 | 12 | test("works", async () => { 13 | // @ts-ignore 14 | const result = await Post.fetch(1, true); 15 | 16 | const post: Data = Post.query() 17 | .withAllRecursive() 18 | .find(1)! as Data; 19 | expect(post.author).not.toEqual(null); 20 | expect(post.comments).not.toEqual(null); 21 | expect(post.comments.length).not.toEqual(0); 22 | expect(post.author.name).toEqual("Charlie Brown"); 23 | expect(post.comments[0].content).toEqual("Yes!!!!"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/integration/relations/polymorphic-has-one.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData, Tariff, User } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | 4 | let store: any; 5 | let vuexOrmGraphQL; 6 | 7 | describe("Polymorphic Has One", () => { 8 | beforeEach(async () => { 9 | [store, vuexOrmGraphQL] = await setupMockData(); 10 | }); 11 | 12 | test("works", async () => { 13 | // FIXME 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/integration/relations/polymorphic-many-to-many.spec.ts: -------------------------------------------------------------------------------- 1 | import { Post, setupMockData, Tag } from "../../support/mock-data"; 2 | import { Data } from "../../../src/support/interfaces"; 3 | 4 | let store: any; 5 | let vuexOrmGraphQL; 6 | 7 | describe("Polymorphic Many To Many", () => { 8 | beforeEach(async () => { 9 | [store, vuexOrmGraphQL] = await setupMockData(); 10 | }); 11 | 12 | test("works", async () => { 13 | // @ts-ignore 14 | await Tag.fetch(); 15 | 16 | // @ts-ignore 17 | await Post.fetch(1); 18 | 19 | const tag: Data = Tag.query() 20 | .withAllRecursive() 21 | .find(1)! as Data; 22 | 23 | const post: Data = Post.query() 24 | .withAllRecursive() 25 | .find(1)! as Data; 26 | 27 | expect(post!.tags.length).toEqual(2); 28 | expect(post!.tags[0].name).toEqual("GraphQL"); 29 | expect(post!.tags[1].name).toEqual("Ruby"); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/support/helpers.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import VuexORM, { Database, Model } from "@vuex-orm/core"; 4 | import VuexORMGraphQLPlugin from "../../src"; 5 | import { SchemaLink } from "apollo-link-schema"; 6 | import { ApolloLink } from "apollo-link"; 7 | import { makeExecutableSchema } from "graphql-tools"; 8 | import { prettify } from "../../src/support/utils"; 9 | import { typeDefs, resolvers } from "./mock-schema"; 10 | import sinon from "sinon"; 11 | import Adapter from "../../src/adapters/adapter"; 12 | 13 | // @ts-ignore 14 | Vue.use(Vuex); 15 | 16 | export let link: ApolloLink; 17 | 18 | export interface Entity { 19 | model: typeof Model; 20 | module?: object; 21 | } 22 | 23 | /** 24 | * Create a new Vuex Store. 25 | */ 26 | export function createStore(entities: Array, headers?: any, adapter?: Adapter) { 27 | const database = new Database(); 28 | 29 | entities.forEach(entity => { 30 | database.register(entity.model, entity.module || {}); 31 | }); 32 | 33 | // @ts-ignore 34 | const executableSchema = makeExecutableSchema({ 35 | typeDefs, 36 | resolvers 37 | }); 38 | 39 | link = new SchemaLink({ schema: executableSchema }); 40 | 41 | VuexORM.use(VuexORMGraphQLPlugin, { 42 | database: database, 43 | link, 44 | headers, 45 | adapter, 46 | debug: false 47 | }); 48 | 49 | const store = new Vuex.Store({ 50 | plugins: [VuexORM.install(database)] 51 | }); 52 | 53 | return [store, VuexORMGraphQLPlugin.instance]; 54 | } 55 | 56 | export async function recordGraphQLRequest(callback: Function, allowToFail: boolean = false) { 57 | const spy = sinon.spy(link, "request"); 58 | 59 | try { 60 | await callback(); 61 | } catch (e) { 62 | console.log(JSON.stringify(e, null, 2)); 63 | throw e; 64 | } 65 | 66 | spy.restore(); 67 | 68 | if (spy.notCalled) { 69 | if (allowToFail) { 70 | return null; 71 | } else { 72 | throw new Error("No GraphQL request was made."); 73 | } 74 | } 75 | 76 | const relevantCall = spy.getCalls().find(c => c.args[0].operationName !== "Introspection"); 77 | 78 | return { 79 | operationName: relevantCall!.args[0].operationName, 80 | variables: relevantCall!.args[0].variables, 81 | query: prettify(relevantCall!.args[0].query.loc!.source.body) 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /test/support/mock-apollo-client.ts: -------------------------------------------------------------------------------- 1 | import * as fetch from "cross-fetch"; 2 | import { ApolloClient, FetchPolicy } from "apollo-client"; 3 | import { InMemoryCache } from "apollo-cache-inmemory"; 4 | import { HttpLink } from "apollo-link-http"; 5 | import { ApolloLink } from "apollo-link"; 6 | 7 | export default class MockApolloClient { 8 | /** 9 | * The http link instance to use. 10 | * @type {HttpLink} 11 | */ 12 | private readonly httpLink: ApolloLink; 13 | /** 14 | * The ApolloClient instance 15 | * @type {ApolloClient} 16 | */ 17 | private readonly apolloClient: ApolloClient; 18 | /** 19 | * @constructor 20 | */ 21 | public constructor() { 22 | /* istanbul ignore next */ 23 | this.httpLink = new HttpLink({ 24 | ...fetch, 25 | uri: "/graphql", 26 | credentials: "same-origin", 27 | useGETForQueries: false 28 | }); 29 | 30 | this.apolloClient = new ApolloClient({ 31 | link: this.httpLink, 32 | cache: new InMemoryCache(), 33 | connectToDevTools: true 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/support/mock-data.ts: -------------------------------------------------------------------------------- 1 | import { Attribute, Model as ORMModel } from "@vuex-orm/core"; 2 | import { createStore } from "./helpers"; 3 | import { setupTestUtils } from "../../src/test-utils"; 4 | import VuexORMGraphQLPlugin from "../../src"; 5 | import Adapter from "../../src/adapters/adapter"; 6 | import TestAdapter from "./test-adapter"; 7 | 8 | export interface Fields { 9 | [key: string]: Attribute; 10 | } 11 | 12 | export class User extends ORMModel { 13 | static entity = "users"; 14 | 15 | static fields(): Fields { 16 | return { 17 | id: this.uid(), 18 | name: this.string(""), 19 | profileId: this.number(0), 20 | role: this.string(""), 21 | posts: this.hasMany(Post, "authorId"), 22 | comments: this.hasMany(Comment, "authorId"), 23 | profile: this.belongsTo(Profile, "profileId") 24 | }; 25 | } 26 | } 27 | 28 | export class Profile extends ORMModel { 29 | static entity = "profiles"; 30 | static eagerSave = ["user"]; 31 | 32 | static fields(): Fields { 33 | return { 34 | id: this.uid(), 35 | email: this.string(""), 36 | age: this.number(0), 37 | sex: this.boolean(true), 38 | user: this.hasOne(User, "profileId") 39 | }; 40 | } 41 | } 42 | 43 | export class Video extends ORMModel { 44 | static entity = "videos"; 45 | static eagerLoad = ["comments", "tags"]; 46 | 47 | static fields(): Fields { 48 | return { 49 | id: this.uid(), 50 | content: this.string(""), 51 | title: this.string(""), 52 | authorId: this.number(0), 53 | otherId: this.number(0), // This is a field which ends with `Id` but doesn't belong to any relation 54 | ignoreMe: this.string(""), 55 | author: this.belongsTo(User, "authorId"), 56 | comments: this.morphMany(Comment, "subjectId", "subjectType"), 57 | tags: this.morphToMany(Tag, Taggable, "tagId", "subjectId", "subjectType") 58 | }; 59 | } 60 | } 61 | 62 | export class Post extends ORMModel { 63 | static entity = "posts"; 64 | static eagerLoad = ["comments"]; 65 | static eagerSync = ["tags"]; 66 | 67 | static fields(): Fields { 68 | return { 69 | id: this.uid(), 70 | content: this.string(""), 71 | title: this.string(""), 72 | authorId: this.number(0), 73 | otherId: this.number(0), // This is a field which ends with `Id` but doesn't belong to any relation 74 | published: this.boolean(true), 75 | author: this.belongsTo(User, "authorId"), 76 | comments: this.morphMany(Comment, "subjectId", "subjectType"), 77 | tags: this.morphToMany(Tag, Taggable, "tagId", "subjectId", "subjectType") 78 | }; 79 | } 80 | } 81 | 82 | export class Comment extends ORMModel { 83 | static entity = "comments"; 84 | 85 | static fields(): Fields { 86 | return { 87 | id: this.uid(), 88 | content: this.string(""), 89 | authorId: this.number(0), 90 | author: this.belongsTo(User, "authorId"), 91 | subjectId: this.number(0), 92 | subjectType: this.string("") 93 | }; 94 | } 95 | } 96 | 97 | export class TariffTariffOption extends ORMModel { 98 | static entity = "tariffTariffOptions"; 99 | static primaryKey = ["tariffUuid", "tariffOptionId"]; 100 | 101 | static fields(): Fields { 102 | return { 103 | tariffUuid: this.string(""), 104 | tariffOptionId: this.number(0) 105 | }; 106 | } 107 | } 108 | 109 | export class Tariff extends ORMModel { 110 | static entity = "tariffs"; 111 | static eagerLoad = ["tariffOptions"]; 112 | static primaryKey = "uuid"; 113 | 114 | static fields(): Fields { 115 | return { 116 | uuid: this.string(""), 117 | name: this.string(""), 118 | displayName: this.string(""), 119 | tariffType: this.string(""), 120 | slug: this.string(""), 121 | 122 | tariffOptions: this.belongsToMany( 123 | TariffOption, 124 | TariffTariffOption, 125 | "tariffUuid", 126 | "tariffOptionId" 127 | ) 128 | }; 129 | } 130 | } 131 | 132 | export class TariffOption extends ORMModel { 133 | static entity = "tariffOptions"; 134 | static eagerLoad = ["tariffs"]; 135 | 136 | static fields(): Fields { 137 | return { 138 | id: this.uid(), 139 | name: this.string(""), 140 | description: this.string(""), 141 | 142 | tariffs: this.belongsToMany(Tariff, TariffTariffOption, "tariffOptionId", "tariffUuid") 143 | }; 144 | } 145 | } 146 | 147 | export class Category extends ORMModel { 148 | static entity = "categories"; 149 | 150 | static fields(): Fields { 151 | return { 152 | id: this.uid(), 153 | name: this.string(""), 154 | 155 | parentId: this.number(0), 156 | parent: this.belongsTo(Category, "parentId") 157 | }; 158 | } 159 | } 160 | 161 | export class Taggable extends ORMModel { 162 | static entity = "taggables"; 163 | 164 | static fields(): Fields { 165 | return { 166 | id: this.uid(), 167 | tagId: this.number(0), 168 | subjectId: this.number(0), 169 | subjectType: this.string("") 170 | }; 171 | } 172 | } 173 | 174 | export class Tag extends ORMModel { 175 | static entity = "tags"; 176 | 177 | static fields(): Fields { 178 | return { 179 | id: this.uid(), 180 | name: this.string("") 181 | }; 182 | } 183 | } 184 | 185 | export class Media extends ORMModel { 186 | static entity = "media"; 187 | static singularName = "media"; 188 | static pluralName = "media"; 189 | 190 | static fields(): Fields { 191 | return { 192 | id: this.uid() 193 | }; 194 | } 195 | } 196 | 197 | export async function setupMockData(headers?: any, adapter?: Adapter) { 198 | let store; 199 | let vuexOrmGraphQL; 200 | 201 | if (!adapter) adapter = new TestAdapter(); 202 | 203 | [store, vuexOrmGraphQL] = createStore( 204 | [ 205 | { model: User }, 206 | { model: Profile }, 207 | { model: Post }, 208 | { model: Video }, 209 | { model: Comment }, 210 | { model: TariffOption }, 211 | { model: Tariff }, 212 | { model: TariffTariffOption }, 213 | { model: Category }, 214 | { model: Taggable }, 215 | { model: Tag }, 216 | { model: Media } 217 | ], 218 | headers, 219 | adapter 220 | ); 221 | 222 | setupTestUtils(VuexORMGraphQLPlugin); 223 | 224 | return [store, vuexOrmGraphQL]; 225 | } 226 | -------------------------------------------------------------------------------- /test/support/test-adapter.ts: -------------------------------------------------------------------------------- 1 | import DefaultAdapter from "../../src/adapters/builtin/default-adapter"; 2 | import { ConnectionMode, ArgumentMode } from "../../src/adapters/adapter"; 3 | 4 | /** 5 | * Adapter implementation that allows to change ConnectionMode, ArgumentMode and so on in runtime. 6 | */ 7 | export default class TestAdapter extends DefaultAdapter { 8 | public connectionMode: ConnectionMode = ConnectionMode.NODES; 9 | public argumentMode: ArgumentMode = ArgumentMode.TYPE; 10 | 11 | getConnectionMode(): ConnectionMode { 12 | return this.connectionMode; 13 | } 14 | 15 | getArgumentMode(): ArgumentMode { 16 | return this.argumentMode; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/unit/action.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData, Post } from "../support/mock-data"; 2 | import Context from "../../src/common/context"; 3 | import Action from "../../src/actions/action"; 4 | 5 | let store; 6 | let vuexOrmGraphQL; 7 | let context: Context; 8 | 9 | beforeEach(async () => { 10 | [store, vuexOrmGraphQL] = await setupMockData(); 11 | context = Context.getInstance(); 12 | }); 13 | 14 | describe("Action", () => { 15 | describe(".getModelFromState", () => { 16 | test("returns the model", () => { 17 | expect(Action.getModelFromState({ $name: "post" })).toEqual(context.getModel("post")); 18 | }); 19 | }); 20 | 21 | describe(".prepareArgs", () => { 22 | test("returns a args object without the id", () => { 23 | expect(Action.prepareArgs(undefined, 15)).toEqual({ id: 15 }); 24 | expect(Action.prepareArgs({}, 42)).toEqual({ id: 42 }); 25 | }); 26 | 27 | test("returns a args object with the id", () => { 28 | expect(Action.prepareArgs(undefined)).toEqual({}); 29 | expect(Action.prepareArgs({ test: 15 })).toEqual({ test: 15 }); 30 | }); 31 | }); 32 | 33 | describe(".addRecordToArgs", () => { 34 | test("returns a args object with the record", async () => { 35 | const model = context.getModel("post"); 36 | // @ts-ignore 37 | await Post.fetch(1); 38 | 39 | const record = model.getRecordWithId(1)!; 40 | 41 | expect(Action.addRecordToArgs({ test: 2 }, model, record)).toEqual({ 42 | post: { 43 | id: 1, 44 | content: "GraphQL is so nice!", 45 | otherId: 123, 46 | published: true, 47 | title: "GraphQL", 48 | author: { 49 | id: 1, 50 | name: "Charlie Brown", 51 | profile: { 52 | id: 1, 53 | age: 8, 54 | email: "charlie@peanuts.com", 55 | sex: true 56 | }, 57 | profileId: 1 58 | }, 59 | tags: [ 60 | { 61 | id: 1, 62 | name: "GraphQL" 63 | }, 64 | { 65 | id: 2, 66 | name: "Ruby" 67 | } 68 | ], 69 | authorId: 1 70 | }, 71 | 72 | test: 2 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/unit/adapter/builtin/default-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import DefaultAdapter from "../../../../src/adapters/builtin/default-adapter"; 2 | import { setupMockData } from "../../../support/mock-data"; 3 | import Context from "../../../../src/common/context"; 4 | import Model from "../../../../src/orm/model"; 5 | import { ConnectionMode } from "../../../../src/adapters/adapter"; 6 | 7 | let model: Model; 8 | let store; 9 | let vuexOrmGraphQL; 10 | let context; 11 | 12 | describe("DefaultAdapter", () => { 13 | beforeEach(async () => { 14 | [store, vuexOrmGraphQL] = await setupMockData(null, new DefaultAdapter()); 15 | context = Context.getInstance(); 16 | await context.loadSchema(); 17 | model = context.getModel("post"); 18 | }); 19 | 20 | describe(".getNameForPersist", () => { 21 | test("returns a correct create mutation name", () => { 22 | expect(Context.getInstance().adapter.getNameForPersist(model)).toEqual("createPost"); 23 | }); 24 | }); 25 | 26 | describe(".getNameForPush", () => { 27 | test("returns a correct update mutation name", () => { 28 | expect(Context.getInstance().adapter.getNameForPush(model)).toEqual("updatePost"); 29 | }); 30 | }); 31 | 32 | describe(".getNameForDestroy", () => { 33 | test("returns a correct delete mutation name", () => { 34 | expect(Context.getInstance().adapter.getNameForDestroy(model)).toEqual("deletePost"); 35 | }); 36 | }); 37 | 38 | describe(".getNameForFetch", () => { 39 | test("returns a correct fetch query name", () => { 40 | expect(Context.getInstance().adapter.getNameForFetch(model, true)).toEqual("posts"); 41 | expect(Context.getInstance().adapter.getNameForFetch(model, false)).toEqual("post"); 42 | }); 43 | }); 44 | 45 | describe(".getFilterTypeName", () => { 46 | test("returns a correct filter type name", () => { 47 | expect(Context.getInstance().adapter.getFilterTypeName(model)).toEqual("PostFilter"); 48 | }); 49 | }); 50 | 51 | describe(".getConnectionMode", () => { 52 | test("returns a correct connection mode", () => { 53 | expect(Context.getInstance().adapter.getConnectionMode()).toEqual(ConnectionMode.NODES); 54 | }); 55 | }); 56 | 57 | describe(".getRootMutationName", () => { 58 | test("returns a correct name", () => { 59 | expect(Context.getInstance().adapter.getRootMutationName()).toEqual("Mutation"); 60 | }); 61 | }); 62 | 63 | describe(".getRootQueryName", () => { 64 | test("returns a correct name", () => { 65 | expect(Context.getInstance().adapter.getRootQueryName()).toEqual("Query"); 66 | }); 67 | }); 68 | 69 | describe(".getInputTypeName", () => { 70 | test("returns a correct name", () => { 71 | expect(Context.getInstance().adapter.getInputTypeName(model)).toEqual("PostInput"); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/unit/apollo.spec.ts: -------------------------------------------------------------------------------- 1 | import Apollo from "../../src/graphql/apollo"; 2 | import { setupMockData } from "../support/mock-data"; 3 | 4 | describe("Apollo", () => { 5 | describe(".getHeaders", () => { 6 | it("allows to set headers as object", async () => { 7 | await setupMockData({ "X-TEST": 42 }); 8 | 9 | expect(Apollo["getHeaders"]()).toEqual({ "X-TEST": 42 }); 10 | }); 11 | 12 | it("allows to set headers as function", async () => { 13 | await setupMockData(() => ({ "X-TEST-FN": 43 })); 14 | 15 | expect(Apollo["getHeaders"]()).toEqual({ "X-TEST-FN": 43 }); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/unit/context.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupMockData } from "../support/mock-data"; 2 | import Context from "../../src/common/context"; 3 | import MockApolloClient from "../support/mock-apollo-client"; 4 | 5 | let store; 6 | let vuexOrmGraphQL; 7 | let context: Context; 8 | 9 | describe("Context", () => { 10 | beforeEach(async () => { 11 | [store, vuexOrmGraphQL] = await setupMockData(); 12 | context = Context.getInstance(); 13 | }); 14 | 15 | describe(".debugMode", () => { 16 | test("to be false", () => { 17 | expect(context.debugMode).toEqual(false); 18 | }); 19 | }); 20 | 21 | describe(".apolloClient", () => { 22 | test("returns an apollo client", () => { 23 | context.options.apolloClient = new MockApolloClient(); 24 | expect(context.options.apolloClient instanceof MockApolloClient).toBe(true); 25 | }); 26 | }); 27 | 28 | describe(".getModel", () => { 29 | test("returns a model", () => { 30 | expect(context.getModel("post")).toEqual(context.models.get("post")); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | prettify, 3 | downcaseFirstLetter, 4 | upcaseFirstLetter, 5 | pick, 6 | isGuid, 7 | toPrimaryKey 8 | } from "../../src/support/utils"; 9 | 10 | describe("capitalizeFirstLetter", () => { 11 | test("capitalizes the first letter of a string", () => { 12 | expect(upcaseFirstLetter("testFooBar")).toEqual("TestFooBar"); 13 | expect(upcaseFirstLetter("TestFooBar")).toEqual("TestFooBar"); 14 | }); 15 | }); 16 | 17 | describe("downcaseFirstLetter", () => { 18 | test("down cases the first letter of a string", () => { 19 | expect(downcaseFirstLetter("testFooBar")).toEqual("testFooBar"); 20 | expect(downcaseFirstLetter("TestFooBar")).toEqual("testFooBar"); 21 | }); 22 | }); 23 | 24 | describe("prettify", () => { 25 | test("formats a graphql query", () => { 26 | const query = 27 | "query Posts($deleted:Boolean!){posts(deleted: $deleted){id, name author{id, email, firstname}}}"; 28 | 29 | const formattedQuery = 30 | ` 31 | query Posts($deleted: Boolean!) { 32 | posts(deleted: $deleted) { 33 | id 34 | name 35 | author { 36 | id 37 | email 38 | firstname 39 | } 40 | } 41 | } 42 | `.trim() + "\n"; 43 | 44 | expect(prettify(query)).toEqual(formattedQuery); 45 | }); 46 | }); 47 | 48 | describe("pick", () => { 49 | test("picks stuff", () => { 50 | const input = { 51 | foo: 1, 52 | bar: 2, 53 | test: 3, 54 | hello: 4, 55 | world: 5 56 | }; 57 | 58 | const expectedOutput = { 59 | bar: 2, 60 | hello: 4 61 | }; 62 | 63 | expect(pick(input, ["bar", "hello"])).toEqual(expectedOutput); 64 | expect(pick(0, ["bar", "hello"])).toEqual({}); 65 | }); 66 | }); 67 | 68 | describe("toPrimaryKey", () => { 69 | test("should return 0 for null", () => { 70 | const input = null; 71 | const expectedOutput = 0; 72 | 73 | expect(toPrimaryKey(input)).toEqual(expectedOutput); 74 | }); 75 | 76 | test("return GUID for string GUIDs", () => { 77 | const input = "149ae8b4-cc84-49f5-bf97-b6ce6c09c8e2"; 78 | const expectedOutput = "149ae8b4-cc84-49f5-bf97-b6ce6c09c8e2"; 79 | 80 | expect(toPrimaryKey(input)).toEqual(expectedOutput); 81 | }); 82 | 83 | test("return $uid for string $uids", () => { 84 | const input = "$uid:1"; 85 | const expectedOutput = "$uid:1"; 86 | 87 | expect(toPrimaryKey(input)).toEqual(expectedOutput); 88 | }); 89 | 90 | test("return int for string numbers", () => { 91 | const input = "100"; 92 | const expectedOutput = 100; 93 | 94 | expect(toPrimaryKey(input)).toEqual(expectedOutput); 95 | }); 96 | 97 | test("return int for int numbers", () => { 98 | const input = 100; 99 | const expectedOutput = 100; 100 | 101 | expect(toPrimaryKey(input)).toEqual(expectedOutput); 102 | }); 103 | }); 104 | 105 | describe("isGuid", () => { 106 | test("returns true if it's a GUID", () => { 107 | const input = "149ae8b4-cc84-49f5-bf97-b6ce6c09c8e2"; 108 | const expectedOutput = true; 109 | 110 | expect(isGuid(input)).toEqual(expectedOutput); 111 | }); 112 | 113 | test("returns false if it's not a GUID", () => { 114 | const input = "this-is-not-a-guid"; 115 | 116 | const expectedOutput = false; 117 | 118 | expect(isGuid(input)).toEqual(expectedOutput); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "lib": ["esnext.asynciterable", "dom", "es2015", "es2016", "es2017"], 7 | "strict": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "strictNullChecks": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "declarationDir": "dist/types", 19 | "outDir": "dist/lib", 20 | "rootDir": "src", 21 | "typeRoots": [ 22 | "node_modules/@types" 23 | ] 24 | }, 25 | 26 | "include": [ 27 | "src" 28 | ], 29 | 30 | "exclude": [ 31 | "node_modules", 32 | "test" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ], 6 | "rules": { 7 | "semicolon": [true, "always"], 8 | "no-unused-variable": false 9 | } 10 | } 11 | --------------------------------------------------------------------------------