graphql-defaults
TypeScript icon, indicating that this package has built-in type declarations

0.7.17 • Public • Published

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.

NPM

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

Donate using Liberapay

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 project yarn add esm or npm 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 Vue beforeCreate 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 do this.profile = this.$genDefaults(profileQuery).profile in the created() 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 skipping genGraphQLDefaults and just passing the operation here. This will cause mergeWithDefaults 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 provide path and context both of which are redundant if you provide data and defaults.
    • path - is the dot notation 'key' for the property on the context it's being used on, used primarily to figure out the defaults to merge with data
    • context - where to find the defaults and probably data if not provided
    • debug - 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 given context's property at path to its initial defaults (before $initDefaults was run, usually in beforeCreate hook). The whole context'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 specified path on context. Returns all defaults if no path is specified.
  • $hasDefault(path: String, [context: Object] = this): Check if the default for specified path on context 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 to null 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 and defaults 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

License

2020 MIT

Readme

Keywords

none

Package Sidebar

Install

npm i graphql-defaults

Weekly Downloads

0

Version

0.7.17

License

MIT

Unpacked Size

59.1 kB

Total Files

4

Last publish

Collaborators

  • emahuni