graphql-defaults v0.7.17
GraphQL Query Defaults
Generate default JS Objects from GraphQL query using a provided schema. The result is much like the result the query will produce from the server howbeit with scalar type defaults.
About
Developed by Emmanuel Mahuni to solve a problem of creating a JS object of scalar defaults, based on a graphql query and a schema. see this post for more details: StackOverflow
Installation:
Installing the package is very simple:
yarn add graphql-defaultsnpm install graphql-defaultsThat's it.
Usage
The point of this library is to generate a js object with defaults equivalent to the object that the server will respond with so that you can use the resulting object before the server responds, then later update/merge with the response when ready. It provides a set of tools to get this done easily.
Initialize and prepare
Let's say there is a schema that was introspected from some server, use that to initialise the lib (optionally exclude any types you don't want to use that may otherwise mess with defaults generating using regex on the type name):
import { genGraphQLDefaults, genTypesDefinitionsMaps, mergeWithDefaults } from 'graphql-defaults';
genTypesDefinitionsMaps(schema, [/.{1,}Aggregation.*/i, /.*Connection/i]);If you are using commonjs, without a bundler, then do this:
- install
esmpackage into your projectyarn add esmornpm install esm --save. - substitute
requirewith one from esm package like so:
const requireEsm = require("esm")(module);
const { genGraphQLDefaults, genTypesDefinitionsMaps, mergeWithDefaults } = requireEsm( 'graphql-defaults').default;
genTypesDefinitionsMaps(schema, [/.{1,}Aggregation.*/i, /.*Connection/i]);That is done once at boot time.
Then get the defaults from a Graphql query that may even have fragments eg:
// defined elsewhere
let addressFiledsFragment = `
fragment addressFields on Address {
line1
line2
street {
name
location {
name
city {
name
}
}
}
}
`Generate Defaults
Finally generate the defaults for a query.
Don't forget the fragment at the end of the query... see graphql-tag docs on https://github.com/apollographql/graphql-tag
const profileContext = genGraphQLDefaults({ operation: `
query fetchProfile ($id: ID!){
profile(id: $id){
firstname
contact {
addresses {
...addressFields
}
mobiles {
number
confirmed
}
fixed {
number
}
faxes {
number
}
emailaddresses {
address
}
}
}
}
${addressFieldsFragment}
` });Result
The point of this library is to generate equivalent js object of what the server will respond with similar to the following:
profileContext.profile === {
__typename: 'Profile',
firstname: '',
contact: {
__typename: 'Contact',
addresses: [
{
__typename: 'Address',
line1: '',
line2: '',
street: {
__typename: 'Street',
name: '',
location: {
__typename: 'Location',
name: '',
city: {
__typename: 'City',
name: '',
},
},
},
},
],
mobiles: [{ __typename: 'Phonenumber', number: 0, confirmed: false }],
fixed: [{ __typename: 'Phonenumber', number: 0 }],
faxes: [{ __typename: 'Phonenumber', number: 0 }],
emailAddresses: [{ __typename: 'Emailaddress', address: '' }],
},
}This object is useful for immediate use in frameworks like VueJS and React or anywhere you need to immediately use an object whilst waiting for the server to finally give the actual data. You can plug this in right into a component's data prop and use with v-model without bloating your code. VueJS example:
<template>
<div>
firstname: <input v-model="profile.firstname" />
addresses:
<div v-for="addr of profile.contact.addresses">
line1: <input v-model="addr.line1"/>
line2: <input v-model="addr.line2"/>
...
</div>
</div>
</template>
<script>
// let's say all that script code was here and we modify it so that it works properly here
export default {
data(){
return {
// assign the defaults to profile for use now.
profile: profileContext.profile
}
},
created(){
// request server data through apollo or whatever method you are doing it. In the meantime before the server returns the data, the defaults will be used.
apollo.query({
query: fetchProfile
}).then(({data: {profile}})=>{
// merge the defaults with the server data to finally display the actual result
this.profile = mergeWithDefaults({ path: 'profile', data: profile, context: profileContext });
})
},
}
</script>Usage in VueJS (I am a VueJS user)
The following examples uses the above template and query.
Without Vue mixin:
This whole script block illustrates the use of this lib without the Vue mixin.
<script>
// you will have to import genGraphQLDefaults and mergeWithDefaults in every component file, but not geTypesDefinitionsMaps
import { genGraphQLDefaults, genTypesDefinitionsMaps } from 'graphql-defaults';
// get app schema
import schema from 'app/schema.graphql';
// genTypesDefinitionsMaps should be placed where it is run once or when the schema changes
// In a large app it makes no sense to put it here unless this is the root instance or only component
genTypesDefinitionsMaps(schema);
// you can save your queries in files and import them for much cleaner and modular code
import query from './profileQuery.graphql';
// yes you can pass a query as AST tags.
const profileDefaults = genGraphQLDefaults(query).profile;
export default {
data () {
return {
profile: profileDefaults,
}
},
// usage with vue apollo, mergeWithDefaults will patch up the data with the defaults generated earlier
apollo: {
profile: {
query: query,
variables: { id: 1},
update ({profile}) { return mergeWithDefaults({path: 'profile', data: profile, defaults: profileDefaults}) }
}
}
}
</script>The following script block illustrates the use of this lib with the Vue mixin (recommended).
<script>
// import and use it in root instance only
import { graphQLDefaultsVueMixin, genTypesDefinitionsMaps} from 'graphql-defaults';
// get app schema
import schema from 'app/schema.graphql';
// genTypesDefinitionsMaps should be placed where it is run once or when the schema changes
// In a large app it makes no sense to put it here unless this is the root instance or only component
genTypesDefinitionsMaps(schema);
// you can save your queries in files and import them for much cleaner and modular code
import query from './profileQuery.graphql';
export default {
data () {
return {
profile: undefined,
}
},
// this mixin injects utils for extra manipulation of defaults, should only be done in root instance (other components will have the utils)
mixins: [graphQLDefaultsVueMixin],
created(){
// now generate your defaults. This can be done in any component as long as that mixin was loaded in the root instance
// now profile has defaults that the template can use right away
this.profile = this.$genGraphQLDefaults({ operation: query }).profile;
},
// usage with vue apollo, mergeWithDefaults will patch up the data with the defaults generated earlier
apollo: {
profile: {
query: query,
variables: { id: 1},
update ({profile}) {
profile = this.$mergeWithDefaults({ path: 'profile' });
/* maybe modify profile somehow */
return profile;
}
},
// or if we wanted to supply the prop defaults ourselves
profile: {
query: query,
variables: { id: 1},
update ({profile}) { return this.$mergeWithDefaults({ defaults: this.$defaults$.profile, data: profile }) }
},
}
}
</script>Example profileQuery.graphql file See grapql-tag docs for webpack loader!
# profileQuery.graphql
query getProfile($id: ID!){
profile(id: $id) {
firstname
contact {
addresses {
line1
line2
street {
name
location {
name
city {
name
}
}
}
}
mobiles {
number
}
fixed {
number
}
faxes {
number
}
emailaddresses {
address
}
}
}
}That example will not cause any problems during fetching or writing of data to server. It is clean and doesn't require you to do many checks on data to avoid errors. If you change the query in the graphql file, then the defaults are updated and everything work perfectly without any problems.
Here vue-apollo will execute that query and get profile data for id 1. the data is then merged into the defaults before being given to Vue for use in the vm. the merge defaults part makes sure there are no nulls or undefined's that mess the structure up when the data is updated. eg: if there was no profile on the server then it will respond with an {} or null. So to mitigate that the defaults are then used again to patch the response. Another example is of missing information like phone numbers in that example. Merge with defaults will patch it up and it will work.
Vue Mixin and Utils
The mixin injects the $genGraphQLDefaults function, and a few helpers utils to manage defaults. This is meant to be injected into of your app through Vue.mixin({...graphQLDefaultsVueMixin}) to have these handy utils available in all components:
$initDefaults([context: Object] = this): Initializes defaults on context. This is performed automatically in VuebeforeCreatehook if you use the mixin. The idea is to get each property default as it was in its pristine condition.$genGraphQLDefaults(operation: GraphqlOperation): It returns scalar representation of the given Graphql operation. Query, Mutation, or Subscription. It returns the js default object as if it's a server response. See above examples. You can dothis.profile = this.$genDefaults(profileQuery).profilein thecreated()lifecycle hook or where you need to generate the defaults. Aliases$genGraphqlDefaults/$genDefaults.$mergeWithDefaults({[data: Object], [operation: Object], [defaults: Object], [path: String], [context: Object = this], [debug = false]}): it's useful in keeping the defaults clean without any nulls or undefineds. It merges those initial defaults with the given data object and put defaults, where there was supposed to be a null or undefined. This is mostly meant to be used on graphql operation resulting data.data- data that is returned by graphql operation (one used to generate the target defaults).operation- useful for skippinggenGraphQLDefaultsand just passing the operation here. This will causemergeWithDefaultsto generate graphql defaults for the operation, store it for future use and use the defaults to merge with data. Caution; If you don't pass path, it will grab the defaults as is without drilling into the appropriate data defaults path. ie: use{ data: { profile }, operation: profileQ }instead of{ data: profile, operation: profileQ }or if you specify the path, use{ path: 'profile', data: profile, operation: profileQ }instead of{ path: 'profile', data: { profile }, operation: profileQ }.defaults- defaults to merge with data. Object is defaults directly related to data that was generated earlier by$genDefaults. This is automatically figured out by method if you providepathandcontextboth of which are redundant if you providedataanddefaults.path- is the dot notation 'key' for the property on thecontextit's being used on, used primarily to figure out the defaults to merge with datacontext- where to find the defaults and probably data if not provideddebug- provides debug information on how the merge is happening, allows you to fix merging issues if any (very helpful)@return- returns the given data merged with graphql operation defaults
$resetToDefaults([path: String], [context: Object] = this): Resets the givencontext's property atpathto its initial defaults (before$initDefaultswas run, usually inbeforeCreatehook). The wholecontext's data props are reset to their defaults if there is no context given.$isDefault([path: String], [context: Object] = this): check if the data property at key path is a default or was modified. return boolean.$getDefault([path: String], [context: Object] = this): Get the default for specifiedpathoncontext. Returns all defaults if nopathis specified.$hasDefault(path: String, [context: Object] = this): Check if the default for specifiedpathoncontextexists. Returns true if so, false otherwise.
Debug can now be globally turned on or off by setting GraphqlDefaults.debug of the default export. Each method with debug option can override this option.
Testing & Examples
You can find complete examples in the test.
The tests are run using Mocha. You can install mocha npm i -g mocha and run the tests with npm run test or mocha
Changes
v0.7.6 ... v0.7.11
- change how imports and exports are done (path-finder to sweet-spot btw legacy and modern js)
- change src to typescript (WIP) and transpile to ES6 - commonjs, (works without issue on ESM and ES5 or Nodejs).
v0.7.5
- add global debug option to
GraphqlDefaultsdefault export for easier usage.
v0.7.4
- fix
mergeWithDefaultsjson default tonullinstead of'{}'
v0.7.1
- add operation option to
mergeWithDefaultsfor easier usage.
v0.7.0
- BREAKING CHANGE: remove all deprecated params and refactor all mixin methods to use standard proto naming conventions.
- various improvements and API usage.
v0.6.2
- fix: bug with resetToDefaults
- fix: mergeWithDefaults old keyPath param that was being ignored completely
v0.6.1
- perf: add
pathanddefaultsfor a much simpler api
v0.6.0
- refactor: don't export genGraphQLDefaults as default but move it into default export object
v0.5.3
- feat: add support for __typename in defaults
v0.5.1
- feat: add new hasDefault method
v0.5.0
- BREAKING CHANGE: changed
mergeWithDefaultssignature to use object parameters because of too many optional parameters. This makes it more flexible and powerful, but is a breaking change. perf: Correctly use any object as context
Contributing
Please follow the Felix's Node.js Style Guide.
We use semantic versioning for the NPM package.
Contributors
- Author: Emmanuel Mahuni
License
2020 MIT
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
