graphql-defaults
v0.7.17
Published
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 type defaults. eg: string will be "", number: 0 etc
Downloads
265
Readme
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-defaults
npm install graphql-defaults
That'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
esm
package into your projectyarn add esm
ornpm install esm --save
. - substitute
require
with 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 VuebeforeCreate
hook 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).profile
in 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 skippinggenGraphQLDefaults
and just passing the operation here. This will causemergeWithDefaults
to 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 providepath
andcontext
both of which are redundant if you providedata
anddefaults
.path
- is the dot notation 'key' for the property on thecontext
it'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 atpath
to its initial defaults (before$initDefaults
was run, usually inbeforeCreate
hook). 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 specifiedpath
oncontext
. Returns all defaults if nopath
is specified.$hasDefault(path: String, [context: Object] = this)
: Check if the default for specifiedpath
oncontext
exists. 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
GraphqlDefaults
default export for easier usage.
v0.7.4
- fix
mergeWithDefaults
json default tonull
instead of'{}'
v0.7.1
- add operation option to
mergeWithDefaults
for 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
path
anddefaults
for 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
mergeWithDefaults
signature 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