From 159cd9aa479cd5558032d5f91558467c1a08acac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Wed, 4 Nov 2020 16:28:31 +0100 Subject: [PATCH 01/14] Add caching and fetching --- README.md | 63 ++++- index.js | 220 +++++++++++++++-- package.json | 4 +- test.js | 658 +++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 870 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 3fcdd2b..d74e95c 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ [![npm version][2]][3] [![build status][4]][5] [![downloads][8]][9] [![js-standard-style][10]][11] -Tiny graphQL client library. Does everything you need with GraphQL 15 lines of -code. +Tiny GraphQL client library. Compiles queries, fetches them and caches them, all +in one tiny pacakge. ## Usage ```js -var gql = require('nanographql') +const { gql, nanographql } = require('nanographql') -var query = gql` +const Query = gql` query($name: String!) { movie (name: $name) { releaseDate @@ -17,15 +17,52 @@ var query = gql` } ` -try { - var res = await fetch('/query', { - body: query({ name: 'Back to the Future' }), - method: 'POST' - }) - var json = res.json() - console.log(json) -} catch (err) { - console.error(err) +const graphql = nanographql('/graphql') +const { errors, data } = graphql(Query({ name: 'Back to the Future' })) + +``` + +```js +const { gql, nanographql } = require('nanographql') + +const { user } = gql` + fragment user on User { + id + name + } +` + +const { GetUser, SaveUser } = gql` + query GetUser($id: ID!) { + user: getUser(id: $id) { + ...${user} + } + } + mutation SaveUser($id: ID!, $name: String) { + user: saveUser(id: $id, name: $name) { + ...${user} + } + } +` + +const graphql = nanographql('/graphql', render) + +function render () { + const { errors, data } = graphql(GetUser({ id: 'abc123' })) + if (errors) return html`

User not found

` + if (!data) return html`

Loading

` + + return html` +
+ Name: + + ` + + function onsubmit (event) { + graphql(SaveUser({ id: 'abc123', name: this.username })) + event.preventDefault() + } } ``` diff --git a/index.js b/index.js index df2afef..37fb122 100644 --- a/index.js +++ b/index.js @@ -1,17 +1,209 @@ -module.exports = nanographql - -var getOpname = /(query|mutation) ?([\w\d-_]+)? ?\(.*?\)? \{/ - -function nanographql (str) { - str = Array.isArray(str) ? str.join('') : str - var name = getOpname.exec(str) - return function (variables) { - var data = { query: str } - if (variables) data.variables = JSON.stringify(variables) - if (name && name.length) { - var operationName = name[2] - if (operationName) data.operationName = name[2] +const parsed = new WeakMap() +const bypass = ['no-store', 'reload', 'no-cache', 'default'] +const getPlaceholders = /(\.\.\.)?\0(\d+)\0/g +const getOperations = /\s*(query|subscription|mutation|fragment)\s*(\w+)?\s*(?:(?:\(.*?\))|on\s*\w+)?\s*\{/g + +class Operation { + constructor ({ key, type, query, variables, operationName }) { + this.operationName = operationName + this.variables = variables + this.query = query + this.type = type + this.key = key + } + + * [Symbol.iterator] () { + yield ['query', this.query] + yield ['variables', this.variables] + if (this.operationName) yield ['operationName', this.operationName] + } + + toString () { + const { query, variables, operationName } = this + const parts = [ + `query=${encodeURIComponent(query.replace(/\s+/g, ' ').trim())}`, + `variables=${encodeURIComponent(JSON.stringify(variables))}` + ] + if (operationName) parts.push(`operationName=${operationName}`) + return parts.join('&') + } + + toJSON () { + let { query, variables, operationName } = this + query = query.replace(/\s+/g, ' ').trim() + return { query, variables, operationName } + } +} + +exports.gql = gql +exports.Operation = Operation +exports.nanographql = nanographql + +function nanographql (url, opts = {}) { + let { cache, fetch } = opts + + if (!cache) cache = new Map() + if (typeof fetch !== 'function') { + fetch = function (url, opts, cb) { + window.fetch(url, opts).then((res) => res.json()).then((res) => cb(null, res), cb) } - return JSON.stringify(data) } + + return function (operation, opts = {}, cb = Function.prototype) { + if (typeof opts === 'function') { + cb = opts + opts = {} + } + + let { method, body, headers } = opts + const { variables, type } = operation + const querystring = operation.toString() + let href = url.toString() + + let key = opts.key || (variables ? serialize(variables) : querystring) + if (typeof key === 'function') key = opts.key(variables) + let useCache = !body && type !== 'mutation' && !bypass.includes(opts.cache) + let store = cache.get(operation.key) + if (!store) cache.set(operation.key, store = {}) + let cached = store[key] + + if (opts.mutate || useCache) { + if (opts.mutate) cached = store[key] = opts.mutate(cached) + if (cached != null && useCache) return cached + } + + if (body || type === 'mutation' || (href + querystring).length >= 2000) { + method = method || 'POST' + if (!body) { // Don't bother with custom bodies + body = JSON.stringify(operation) + headers = { ...headers, 'Content-Type': 'application/json' } + } + } else { + let [domainpath, query] = href.split('?') + query = query ? query + `&${querystring}` : querystring + href = `${domainpath}?${query}` + } + + let errored = false + fetch(href, { ...opts, method, headers, body }, function (err, res) { + useCache = true // it's not really cached when resolved sync + if (err) { + delete store[key] + errored = true + return cb(err) + } + if (typeof opts.key === 'function') key = opts.key(variables, res) + if (typeof opts.parse === 'function') res = opts.parse(res, store[key]) + if (opts.cache !== 'no-store') store[key] = res + cb(null, res) + }) + + cached = store[key] + if (!cached && !errored) store[key] = {} + if (errored || !useCache) return {} + return store[key] || {} + } +} + +function gql (strings, ...values) { + let operation = parsed.get(strings) + if (operation) return operation + operation = parse(strings, values) + parsed.set(strings, operation) + return operation +} + +function parse (strings, values) { + // Compile query with placeholders for partials + const template = strings.reduce(function (query, str, index) { + query += str + if (values[index] != null) query += `\u0000${index}\u0000` + return query + }, '') + + let match + const operations = [] + + // Extract individual operations from template + while ((match = getOperations.exec(template))) { + const index = getOperations.lastIndex + const [query, type, name] = match + const prev = operations[operations.length - 1] + if (prev) { + prev.query += template.substring(prev.index, index - query.length) + } + operations.push({ type, name, query, index }) + } + + // Add on trailing piece of template + const last = operations[operations.length - 1] + if (last) last.query += template.substring(last.index) + + // Inject fragment into operation query + const fragments = operations.filter((operation) => operation.type === 'fragment') + if (fragments.length) { + for (const operation of operations) { + if (operation.type === 'fragment') continue + for (const fragment of fragments) { + if (operation.query.includes(`...${fragment.name}`)) { + operation.query += fragment.query + } + } + } + } + + // Decorate base operation + for (const operation of operations) { + const name = operation.name || operation.type + Object.defineProperty(createOperation, name, { + value (variables) { + return new Operation({ + variables, + key: template, + type: operation.type, + operationName: operation.name, + query: compile(operation.query, variables, values) + }) + } + }) + } + + return createOperation + + function createOperation (variables) { + return new Operation({ + variables, + key: template, + query: compile(template, variables, values) + }) + } +} + +function compile (template, variables, values) { + const external = new Set() + let query = template.replace(getPlaceholders, function (_, spread, index) { + let value = values[+index] + if (typeof value === 'function') value = value(variables) + if (value instanceof Operation) { + if (value.type === 'fragment') { + external.add(value.query) + if (spread) return `...${value.operationName}` + } + return '' + } + if (value == null) return '' + return value + }) + query += Array.from(external).join(' ') + return query +} + +// Serialize object into a predictable (sorted by key) string representation +function serialize (obj, prefix = '') { + return Object.keys(obj).sort().map(function (key) { + const value = obj[key] + const name = prefix ? `${prefix}.${key}` : key + if (value && typeof value === 'object') return serialize(obj, key) + return `${name}=${value}` + }).join('&') } diff --git a/package.json b/package.json index bda448d..abcbe03 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,8 @@ "start": "node .", "test": "standard && node test" }, - "dependencies": {}, "devDependencies": { - "spok": "^0.8.1", - "standard": "^10.0.2", + "standard": "^16.0.1", "tape": "^4.7.0" }, "keywords": [ diff --git a/test.js b/test.js index 4117c19..f4bbe39 100644 --- a/test.js +++ b/test.js @@ -1,67 +1,635 @@ -var spok = require('spok') -var tape = require('tape') -var gql = require('./') - -tape('should create a query', function (assert) { - var query = gql` - query($number_of_repos:Int!) { - viewer { +const tape = require('tape') +const querystring = require('querystring') +const { gql, nanographql, Operation } = require('./') + +tape('operation interface', function (t) { + t.plan(5) + const expected = { + operationName: 'Greeting', + variables: { hello: 'world' }, + query: 'query Greeting { hello }' + } + const operation = new Operation({ ...expected, key: 'hi' }) + + t.equal(operation.toString(), [ + `query=${encodeURIComponent('query Greeting { hello }')}`, + `variables=${encodeURIComponent('{"hello":"world"}')}`, + 'operationName=Greeting' + ].join('&'), 'string representation as query string') + t.deepEqual(operation.toJSON(), expected, 'json representation match') + + for (const [key, value] of operation) { + t.equal(value, expected[key], `iterator expose ${key}`) + } + + t.end() +}) + +tape('should resolve to operation', function (t) { + const query = gql` + { + hello + } + ` + t.equal(typeof query, 'function', 'is function') + t.doesNotThrow(query, 'does not throw') + + const operation = query({ value: 1 }) + t.ok(operation instanceof Operation, 'resolves to Operation') + t.deepEqual(operation.variables, { value: 1 }, 'operation has variables') + t.equal(operation.query, ` + { + hello + } + `, 'operation has query') + t.end() +}) + +tape('should expose operations by type', function (t) { + const { query, mutation, subscription } = gql` + query { + hello { name - repositories(last: $number_of_repos) { - nodes { - name - } - } + } + } + mutation { + greet(name: $name) { + name + } + } + subscription { + friendship(name: $name) { + relation } } ` - var variables = { number_of_repos: 3 } - var data = query(variables) - spok(assert, JSON.parse(data), { - query: spok.string, - variables: JSON.stringify(variables) - }) - assert.end() + t.equal(typeof query, 'function', 'query is exposed') + t.equal(typeof mutation, 'function', 'mutation is exposed') + t.equal(typeof subscription, 'function', 'subscription is exposed') + + const operation = mutation({ name: 'Jane Doe' }) + t.ok(operation instanceof Operation, 'mutation resolves to Operation') + t.equal(operation.query.trim(), ` + mutation { + greet(name: $name) { + name + } + } + `.trim(), 'mutation has query') + + t.end() }) -tape('should have a name', function (assert) { - var query = gql` - query foo ($number_of_repos:Int!) { - viewer { +tape('should expose named operations', function (t) { + const query = gql` + query Introduction { + hello { name - repositories(last: $number_of_repos) { - nodes { - name - } + } + } + mutation Handshake($name: String!) { + greet(name: $name) { + ...person + } + } + subscription Friendship($name: String!) { + friendship(name: $name) { + relation + person { + ...person } } } + fragment person on Friend { + name + } ` - var variables = { number_of_repos: 3 } - var data = query(variables) - spok(assert, JSON.parse(data), { - query: spok.string, - operationName: 'foo', - variables: JSON.stringify(variables) + t.equal(typeof query.Introduction, 'function', 'Introduction is exposed') + t.equal(typeof query.Handshake, 'function', 'Handshake is exposed') + t.equal(typeof query.Friendship, 'function', 'Friendship is exposed') + t.equal(typeof query.person, 'function', 'fragment is exposed') + + const introduction = query.Introduction() + t.ok(introduction instanceof Operation, 'introduction resolves to Operation') + t.equal(introduction.query.trim(), ` + query Introduction { + hello { + name + } + } + `.trim(), 'introduction has query') + + const handshake = query.Handshake({ name: 'Jane Doe' }) + t.deepEqual(handshake.variables, { name: 'Jane Doe' }, 'handshake has variables') + t.equal(handshake.query.trim(), ` + mutation Handshake($name: String!) { + greet(name: $name) { + ...person + } + } + fragment person on Friend { + name + } + `.trim(), 'handshake has fragment') + + t.end() +}) + +tape('should support expressions', function (t) { + const { person } = gql` + fragment person on Person { + name + } + ` + const { GetPerson, GetPeople, UpdatePerson } = gql` + query GetPerson { + getPerson(name: "${'Jane Doe'}") { + name + } + } + query GetPeople { + getPeople { + ...${person} + } + } + mutation UpdatePerson { + updatePerson(name: "${(variables) => variables.name}") { + name + } + } + ` + + let operation = GetPerson() + t.equal(operation.query.trim(), ` + query GetPerson { + getPerson(name: "Jane Doe") { + name + } + } + `.trim(), 'interpolates string expressions') + + operation = GetPeople() + t.equal(operation.query.trim(), ` + query GetPeople { + getPeople { + ...person + } + } + fragment person on Person { + name + } + `.trim(), 'interpolates fragment operation') + + operation = UpdatePerson({ name: 'Jane Doe' }) + t.equal(operation.query.trim(), ` + mutation UpdatePerson { + updatePerson(name: "Jane Doe") { + name + } + } + `.trim(), 'interpolates function expressions') + + t.end() +}) + +tape('fetch handler', function (t) { + t.test('with query', function (t) { + t.plan(7) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello + } + ` + + let shouldFail = true + graphql(Query({ hello: 'world' }), function (err, res) { + t.ok(err, 'callback received error') + }) + + shouldFail = false + graphql(Query({ hello: 'world' }), function (err, res) { + t.notOk(err) + t.deepEqual(res, { data: { hello: 'hi!' } }, 'callback received data') + }) + + function fetch (url, opts, cb) { + t.equal(url, '/graphql?query=query%20Query%20%7B%20hello%20%7D&variables=%7B%22hello%22%3A%22world%22%7D&operationName=Query', 'payload is encoded as query string') + t.equal(typeof cb, 'function', 'forwards callback') + if (shouldFail) cb(new Error('fail')) + else cb(null, { data: { hello: 'hi!' } }) + } + }) + + t.test('with large query', function (t) { + t.plan(4) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello + } + ` + + const variables = { value: '' } + for (let i = 0; i < 2000; i++) variables.value += 'a' + const operation = Query(variables) + graphql(operation) + + function fetch (url, opts, cb) { + t.ok(opts.body, 'body is set') + const body = JSON.parse(opts.body) + t.deepEqual(body, operation.toJSON(), 'operation is encoded as json body') + t.equal(opts.headers?.['Content-Type'], 'application/json', 'header is set to json') + t.equal(opts.method, 'POST', 'method is POST') + cb(null) + } + }) + + t.test('with mutation', function (t) { + t.plan(6) + + const graphql = nanographql('/graphql', { fetch }) + const { Mutation } = gql` + mutation Mutation { + hello + } + ` + + const operation = Mutation({ hello: 'world' }) + graphql(operation, function (err, res) { + t.notOk(err) + }) + + function fetch (url, opts, cb) { + t.ok(opts.body, 'body is set') + const body = JSON.parse(opts.body) + t.equal(url, '/graphql', 'url is untouched') + t.deepEqual(body, operation.toJSON(), 'payload is json encoded') + t.equal(opts.headers?.['Content-Type'], 'application/json', 'header is set to json') + t.equal(opts.method, 'POST', 'method is POST') + cb(null) + } + }) + + t.test('with body', function (t) { + t.plan(4) + + const graphql = nanographql('/graphql', { fetch }) + const { Mutation } = gql` + mutation Mutation { + hello + } + ` + + const method = 'UPDATE' + const body = 'hello=world' + const contentType = 'application/x-www-form-urlencoded' + const operation = Mutation({ hello: 'world' }) + graphql(operation, { + body, + method, + headers: { 'Content-Type': contentType } + }, function (err, res) { + t.notOk(err) + }) + + function fetch (url, opts, cb) { + t.equal(opts.body, body, 'body is preserved') + t.equal(opts.headers?.['Content-Type'], contentType, 'content type is preserved') + t.equal(opts.method, method, 'method is preserved') + cb(null) + } + }) + + t.test('synchronous resolution', function (t) { + t.plan(4) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello + } + ` + + let shouldFail = true + let res = graphql(Query({ hello: 'world' }), function (err, res) { + t.ok(err, 'callback received error') + }) + t.deepEqual(res, {}, 'resolves to empty object on error') + + shouldFail = false + res = graphql(Query({ hello: 'world' }), function (err, res) { + t.notOk(err) + }) + t.deepEqual(res, { data: { hello: 'hi!' } }, 'synchronously resolved result') + + function fetch (url, opts, cb) { + if (shouldFail) cb(new Error('fail')) + else cb(null, { data: { hello: 'hi!' } }) + } + }) + + t.test('asynchronous resolution', function (t) { + t.plan(3) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello + } + ` + + let shouldFail = true + const sequence = init() + sequence.next() + + function * init () { + let res = graphql(Query({ hello: 'world' })) + t.deepEqual(res, {}, 'resolves to empty object while loading') + yield + shouldFail = false + res = graphql(Query({ hello: 'world' })) + t.deepEqual(res, {}, 'resolves to empty object while loading after error') + yield + res = graphql(Query({ hello: 'world' })) + t.deepEqual(res, { data: { hello: 'hi!' } }, 'resolved result') + } + + function fetch (url, opts, cb) { + setTimeout(function () { + if (shouldFail) cb(new Error('fail')) + else cb(null, { data: { hello: 'hi!' } }) + sequence.next() + }, 100) + } }) - assert.end() }) -tape('should have a name for mutations also', function (assert) { - var query = gql` - mutation CreateSomethingBig($input: Idea!) { - createSomething(input: $input) { - result +tape('cache handler', function (t) { + t.test('cache by query', function (t) { + t.plan(3) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello + } + ` + + let shouldFail = true + const sequence = init() + sequence.next() + + function * init () { + let res = graphql(Query()) + t.deepEqual(res, {}, 'resolves to empty result while loading') + yield + shouldFail = false + res = graphql(Query()) + t.deepEqual(res, {}, 'error was not cached') + yield + res = graphql(Query()) + t.deepEqual(res, { data: { hello: 'world' } }, 'result was cached') + } + + function fetch (url, opts, cb) { + setTimeout(function () { + if (shouldFail) cb(new Error('fail')) + else cb(null, { data: { hello: 'world' } }) + sequence.next() + }, 100) + } + }) + + t.test('cache by variables', function (t) { + t.plan(4) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query($value: String!) { + echo(value: $value) } + ` + + const sequence = init() + sequence.next() + + function * init () { + let foo = graphql(Query({ value: 'foo' })) + t.deepEqual(foo, {}, 'resolves to empty result while loading') + yield + let bar = graphql(Query({ value: 'bar' })) + t.deepEqual(bar, {}, 'resolves to empty result while loading') + yield + foo = graphql(Query({ value: 'foo' })) + t.deepEqual(foo, { data: { echo: { value: 'foo' } } }, 'result was cached by foo value') + bar = graphql(Query({ value: 'bar' })) + t.deepEqual(bar, { data: { echo: { value: 'bar' } } }, 'result was cached by bar value') + } + + function fetch (url, opts, cb) { + setTimeout(function () { + const query = querystring.parse(url.split('?')[1]) + cb(null, { data: { echo: JSON.parse(query.variables) } }) + sequence.next() + }, 100) + } + }) + + t.test('cache by key option', function (t) { + t.plan(8) + + const graphql = nanographql('/graphql', { fetch }) + const { Query, Mutation } = gql` + query Query($value: String!) { + key(value: $value) + } + mutation Mutation($value: String!) { + key(value: $value) + } + ` + + const sequence = init() + sequence.next() + + function * init () { + let foo = graphql(Query({ value: 'foo' }), { key: keyFn }) + t.deepEqual(foo, {}, 'resolves to empty result while loading') + yield + graphql(Mutation({ value: 'bin' }, { + key (res) { + return res?.data.key + } + })) + let bar = graphql(Query({ value: 'bar' }), { key: 'baz' }) + t.deepEqual(bar, { data: { key: 'baz' } }, 'mutation resolved to same key') + yield + foo = graphql(Query({ value: 'foo' }), { key: keyFn }) + bar = graphql(Query({ value: 'bar' }), { key: 'baz' }) + t.deepEqual(foo, { data: { key: 'baz' } }, 'result match') + t.deepEqual(bar, { data: { key: 'baz' } }, 'result match') + } + + function fetch (url, opts, cb) { + setTimeout(function () { + cb(null, { data: { key: 'baz' } }) + sequence.next() + }, 100) + } + + function keyFn (variables, cached) { + t.deepEqual(variables, { value: 'foo' }, 'key function called w/ variables') + if (cached) { + t.deepEqual(cached, { data: { key: 'baz' } }, 'key function called w/ cached value') + } + return 'baz' + } + }) + + t.test('respect cache option', function (t) { + t.plan(12) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello + } + ` + + const bypass = ['no-store', 'reload', 'no-cache', 'default'] + let sequence = init(0) + sequence.next() + + function * init (index) { + const cache = bypass[index] + + let res = graphql(Query(), { cache, key: cache }) + t.deepEqual(res, {}, `empty result while loading using ${cache}`) + yield + res = graphql(Query(), { cache, key: cache }) + t.deepEqual(res, {}, `was not retrieved from cache using ${cache}`) + yield + res = graphql(Query(), { key: cache }) + if (cache === 'no-store') { + t.deepEqual(res, {}, `was not stored in cache using ${cache}`) + } else { + t.deepEqual(res, { data: { hello: 'hi' } }, `was stored in cache using ${cache}`) + } + + if (index < bypass.length - 1) { + sequence = init(index + 1) + sequence.next() + } + } + + function fetch (url, opts, cb) { + setTimeout(function () { + cb(null, { data: { hello: 'hi' } }) + sequence.next() + }, 100) + } + }) + + t.test('custom cache', function (t) { + const cache = new Map() + const graphql = nanographql('/graphql', { fetch, cache }) + const { Query } = gql` + query Query { + hello + } + ` + + const operation = Query() + graphql(operation, { key: 'key' }) + t.ok(cache.has(operation.key)) + t.deepEqual(cache.get(operation.key).key, { data: { hello: 'hi' } }) + t.end() + + function fetch (url, opts, cb) { + cb(null, { data: { hello: 'hi' } }) + } + }) +}) + +tape('parse', function (t) { + t.plan(5) + + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello } ` - var data = query() - spok(assert, JSON.parse(data), { - query: spok.string, - operationName: 'CreateSomethingBig' + let res = graphql(Query(), { + parse (res, cached) { + t.deepEqual(res, { data: { hello: 'hi' } }, 'parse got original response') + t.notOk(cached, 'nothing cached on first run') + return { data: { hello: 'hey' } } + } + }) + t.deepEqual(res, { data: { hello: 'hey' } }, 'response was parsed') + + res = graphql(Query(), { + cache: 'no-cache', + parse (res, cached) { + t.deepEqual(cached, { data: { hello: 'hey' } }, 'parse got cached response') + return { data: { hello: 'greetings' } } + } }) - assert.end() + t.deepEqual(res, { data: { hello: 'greetings' } }, 'response was parsed') + + function fetch (url, opts, cb) { + cb(null, { data: { hello: 'hi' } }) + } +}) + +tape('mutate', function (t) { + t.plan(3) + + const graphql = nanographql('/graphql', { fetch }) + const { Query, Mutation } = gql` + query Query { + hello + } + mutation Mutation { + hello + } + ` + + const sequence = init() + sequence.next() + + function * init () { + graphql(Query(), { key: 'foo' }) // Populate cache + + yield + + let res = graphql(Mutation(), { + key: 'foo', + mutate (cached) { + t.deepEqual(cached, { data: { hello: 'hi' } }, 'mutate got cached value') + return { data: { hello: 'hey' } } + } + }) + + res = graphql(Query(), { key: 'foo' }) + t.deepEqual(res, { data: { hello: 'hey' } }, 'got mutated value') + + yield + + res = graphql(Query(), { key: 'foo' }) + t.deepEqual(res, { data: { hello: 'hi' } }, 'mutation was overwritten') + } + + function fetch (url, opts, cb) { + setTimeout(function () { + cb(null, { data: { hello: 'hi' } }) + sequence.next() + }, 100) + } }) From be8bc0f1ccf06ca6ab7d7fbfe2a6d1755daa75aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:20:26 +0100 Subject: [PATCH 02/14] Update readme --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++------- index.js | 18 ++++++++++---- 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d74e95c..8a3554d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![downloads][8]][9] [![js-standard-style][10]][11] Tiny GraphQL client library. Compiles queries, fetches them and caches them, all -in one tiny pacakge. +in one tiny package. ## Usage ```js @@ -22,6 +22,65 @@ const { errors, data } = graphql(Query({ name: 'Back to the Future' })) ``` +## API +### ``query = gql`[query]` `` +Create a new graphql query function. + +### `operation = query([data])` +Create a new operation object that holds all data necessary to execute the query +against an endpoint. An operation can be stringified to a query (`toString`), +serialized to a plain object (`toJSON`) or iterated over. + +### `cache = nanographql(string[, opts])` +Create a managed cache which fetches data as it is requested. + +#### Options +- **`cache`:** a custom cache store. Should implement `get` and `set` methods. + *Default: `new Map()`*. +- **`fetch`:** a custom [`fetch`]([12]) implementation. + *Default: `window.fetch`*. + +### `result = cache(operation[, opts])` +Query the cache and fetch query if necessary. The arguments match that of +[`fetch`]([12]) with a couple extra options. + +#### Options +The options are forwarded to the [`fetch`]([12]) implementation but a few are +also used to determine when to use the cache and how to format the request. + +##### Default options +- **`cache`:** The default behavior of nanographql mimics that of `force-cache` + as it will always try and read from the cache unless specified otherwise. Any + of the values `no-store`, `reload`, `no-cache`, `default` will cause + nanographql to bypass the cache and call the fetch implmentation. The value + `no-store` will also prevent the response from being cached locally. +- **`body`:** If a body is defined, nanographql will make no changes to headers + or the body itself. You'll have to append the operation to the body yourself. +- **`method`:** If the operation is a `mutation` or if the stringified + operation is too long to be transferred as `GET` parameters, the method will + be set to `POST`, unless specified otherwise. + +##### Extra options +- **`key|key(variables, cached)`:** A unique identifier for the requested data. + Can be a string or a function. Functions will be called with the variables and + the cached data, if there is any. This can be used to determine the key of + e.g. a mutation where the key is not known untill a response is retrieved. The + default is the `id` variable, if deined, otherwise all variables as a + serialized string, or a stringified representation of the query if no + variables are provided. +- **`parse(response, cached)`:** Parse the incoming data before comitting to the + cache. +- **`mutate(cached)`:** Mutate the cached data prior to reading from cache or + fetching data. This is useful for e.g. immedately updating the UI while + submitting changes to the back end. + +## Advanced usage +One of the benefits of GraphQL is the strucuted format of the queries. When +passing a query to the `gql` tag, nanographql will parse the string identifying +individual queries, mutations, subscritions and fragments and expose these as +individual functions. It will also mix in interpolated fragments from other +queries. + ```js const { gql, nanographql } = require('nanographql') @@ -60,18 +119,12 @@ function render () { ` function onsubmit (event) { - graphql(SaveUser({ id: 'abc123', name: this.username })) + graphql(SaveUser({ id: 'abc123', name: this.username.value })) event.preventDefault() } } ``` -## API -### `query = gql(string)` -Create a new graphql query function. - -### `json = query([data])` -Create a new query object that can be sent as `application/json` to a server. ## License [MIT](https://tldrlegal.com/license/mit-license) @@ -88,3 +141,4 @@ Create a new query object that can be sent as `application/json` to a server. [9]: https://npmjs.org/package/nanographql [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square [11]: https://github.com/feross/standard +[12]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters diff --git a/index.js b/index.js index 37fb122..bb955b4 100644 --- a/index.js +++ b/index.js @@ -45,7 +45,11 @@ function nanographql (url, opts = {}) { if (!cache) cache = new Map() if (typeof fetch !== 'function') { fetch = function (url, opts, cb) { - window.fetch(url, opts).then((res) => res.json()).then((res) => cb(null, res), cb) + window.fetch(url, opts).then(function (res) { + return res.json() + }).then(function (res) { + return cb(null, res) + }, cb) } } @@ -60,8 +64,10 @@ function nanographql (url, opts = {}) { const querystring = operation.toString() let href = url.toString() - let key = opts.key || (variables ? serialize(variables) : querystring) - if (typeof key === 'function') key = opts.key(variables) + let key = opts.key + if (!key) key = variables ? variables.id || serialize(variables) : querystring + else if (typeof key === 'function') key = opts.key(variables) + let useCache = !body && type !== 'mutation' && !bypass.includes(opts.cache) let store = cache.get(operation.key) if (!store) cache.set(operation.key, store = {}) @@ -80,7 +86,7 @@ function nanographql (url, opts = {}) { } } else { let [domainpath, query] = href.split('?') - query = query ? query + `&${querystring}` : querystring + query = query ? `${query}&${querystring}` : querystring href = `${domainpath}?${query}` } @@ -140,7 +146,9 @@ function parse (strings, values) { if (last) last.query += template.substring(last.index) // Inject fragment into operation query - const fragments = operations.filter((operation) => operation.type === 'fragment') + const fragments = operations.filter(function (operation) { + return operation.type === 'fragment' + }) if (fragments.length) { for (const operation of operations) { if (operation.type === 'fragment') continue From 04de63ccb227dffda2ead83a9788e051f031bf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:30:55 +0100 Subject: [PATCH 03/14] Ditch id as default key --- README.md | 15 ++++++++++----- index.js | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8a3554d..f4dbc09 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,8 @@ also used to determine when to use the cache and how to format the request. Can be a string or a function. Functions will be called with the variables and the cached data, if there is any. This can be used to determine the key of e.g. a mutation where the key is not known untill a response is retrieved. The - default is the `id` variable, if deined, otherwise all variables as a - serialized string, or a stringified representation of the query if no - variables are provided. + default is the variables as a serialized string, or a stringified + representation of the query if no variables are provided. - **`parse(response, cached)`:** Parse the incoming data before comitting to the cache. - **`mutate(cached)`:** Mutate the cached data prior to reading from cache or @@ -107,7 +106,7 @@ const { GetUser, SaveUser } = gql` const graphql = nanographql('/graphql', render) function render () { - const { errors, data } = graphql(GetUser({ id: 'abc123' })) + const { errors, data } = graphql(GetUser({ id: 'abc123' }), { key: 'id' }) if (errors) return html`

User not found

` if (!data) return html`

Loading

` @@ -119,7 +118,13 @@ function render () { ` function onsubmit (event) { - graphql(SaveUser({ id: 'abc123', name: this.username.value })) + graphql(SaveUser({ id: 'abc123', name: this.username.value }), { + key: 'id', + mutate (cached) { + const user = { ...data.user, name } + return { data: { user } } + } + }) event.preventDefault() } } diff --git a/index.js b/index.js index bb955b4..e792751 100644 --- a/index.js +++ b/index.js @@ -65,7 +65,7 @@ function nanographql (url, opts = {}) { let href = url.toString() let key = opts.key - if (!key) key = variables ? variables.id || serialize(variables) : querystring + if (!key) key = variables ? serialize(variables) : querystring else if (typeof key === 'function') key = opts.key(variables) let useCache = !body && type !== 'mutation' && !bypass.includes(opts.cache) From b9057650a2c39487bddc0a7374febed8245f2ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:32:58 +0100 Subject: [PATCH 04/14] Fix argument name in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4dbc09..8c9f054 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Create a new operation object that holds all data necessary to execute the query against an endpoint. An operation can be stringified to a query (`toString`), serialized to a plain object (`toJSON`) or iterated over. -### `cache = nanographql(string[, opts])` +### `cache = nanographql(url[, opts])` Create a managed cache which fetches data as it is requested. #### Options From 78ba7f620ed8d5fe3bf11cb51b26b133f13b3973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:34:51 +0100 Subject: [PATCH 05/14] Fix fetch link --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8c9f054..85280c8 100644 --- a/README.md +++ b/README.md @@ -37,15 +37,15 @@ Create a managed cache which fetches data as it is requested. #### Options - **`cache`:** a custom cache store. Should implement `get` and `set` methods. *Default: `new Map()`*. -- **`fetch`:** a custom [`fetch`]([12]) implementation. +- **`fetch`:** a custom [`fetch`][12] implementation. *Default: `window.fetch`*. ### `result = cache(operation[, opts])` Query the cache and fetch query if necessary. The arguments match that of -[`fetch`]([12]) with a couple extra options. +[`fetch`][12] with a couple extra options. #### Options -The options are forwarded to the [`fetch`]([12]) implementation but a few are +The options are forwarded to the [`fetch`][12] implementation but a few are also used to determine when to use the cache and how to format the request. ##### Default options From 864a5235d33b571f28c3fc5a1082c4c81da4ab8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:39:38 +0100 Subject: [PATCH 06/14] Add cache mode link --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 85280c8..d283bdb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Create a managed cache which fetches data as it is requested. ### `result = cache(operation[, opts])` Query the cache and fetch query if necessary. The arguments match that of -[`fetch`][12] with a couple extra options. +[`fetch`][13] with a couple extra options. #### Options The options are forwarded to the [`fetch`][12] implementation but a few are @@ -52,8 +52,9 @@ also used to determine when to use the cache and how to format the request. - **`cache`:** The default behavior of nanographql mimics that of `force-cache` as it will always try and read from the cache unless specified otherwise. Any of the values `no-store`, `reload`, `no-cache`, `default` will cause - nanographql to bypass the cache and call the fetch implmentation. The value - `no-store` will also prevent the response from being cached locally. + nanographql to bypass the cache and call the fetch implementation. The value + `no-store` will also prevent the response from being cached locally. See + [cache][14] for more details on cache mode. - **`body`:** If a body is defined, nanographql will make no changes to headers or the body itself. You'll have to append the operation to the body yourself. - **`method`:** If the operation is a `mutation` or if the stringified @@ -146,4 +147,6 @@ function render () { [9]: https://npmjs.org/package/nanographql [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square [11]: https://github.com/feross/standard -[12]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters +[12]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch +[13]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters +[14]: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache From 22e74bf0ee6b38ce3f0a21cbb7ab0a9df4933c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:41:25 +0100 Subject: [PATCH 07/14] Fix cache mode link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d283bdb..59a2e91 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ also used to determine when to use the cache and how to format the request. of the values `no-store`, `reload`, `no-cache`, `default` will cause nanographql to bypass the cache and call the fetch implementation. The value `no-store` will also prevent the response from being cached locally. See - [cache][14] for more details on cache mode. + [cache mode][14] for more details. - **`body`:** If a body is defined, nanographql will make no changes to headers or the body itself. You'll have to append the operation to the body yourself. - **`method`:** If the operation is a `mutation` or if the stringified From 26d39148f7ed3f4663b36f9b77f751c334de9234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:58:44 +0100 Subject: [PATCH 08/14] Add only-if-cached check --- index.js | 5 ++++- test.js | 20 ++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index e792751..06b0941 100644 --- a/index.js +++ b/index.js @@ -75,7 +75,10 @@ function nanographql (url, opts = {}) { if (opts.mutate || useCache) { if (opts.mutate) cached = store[key] = opts.mutate(cached) - if (cached != null && useCache) return cached + if (useCache) { + if (cached != null) return cached + if (opts.cache === 'only-if-cached') return {} + } } if (body || type === 'mutation' || (href + querystring).length >= 2000) { diff --git a/test.js b/test.js index f4bbe39..134b159 100644 --- a/test.js +++ b/test.js @@ -490,7 +490,24 @@ tape('cache handler', function (t) { } }) - t.test('respect cache option', function (t) { + t.test('respect only-if-cached option', function (t) { + const graphql = nanographql('/graphql', { fetch }) + const { Query } = gql` + query Query { + hello + } + ` + + const res = graphql(Query(), { cache: 'only-if-cached' }) + t.deepEqual(res, {}, 'empty result when not cached') + t.end() + + function fetch (url, opts, cb) { + t.fail('should not fetch') + } + }) + + t.test('respect cache bypass option', function (t) { t.plan(12) const graphql = nanographql('/graphql', { fetch }) @@ -506,7 +523,6 @@ tape('cache handler', function (t) { function * init (index) { const cache = bypass[index] - let res = graphql(Query(), { cache, key: cache }) t.deepEqual(res, {}, `empty result while loading using ${cache}`) yield From 8c342bc78f91318155c251e2cfffd18be1da60bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 09:59:11 +0100 Subject: [PATCH 09/14] Fix typos --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 59a2e91..cfcd5d0 100644 --- a/README.md +++ b/README.md @@ -62,23 +62,23 @@ also used to determine when to use the cache and how to format the request. be set to `POST`, unless specified otherwise. ##### Extra options -- **`key|key(variables, cached)`:** A unique identifier for the requested data. - Can be a string or a function. Functions will be called with the variables and - the cached data, if there is any. This can be used to determine the key of - e.g. a mutation where the key is not known untill a response is retrieved. The - default is the variables as a serialized string, or a stringified - representation of the query if no variables are provided. -- **`parse(response, cached)`:** Parse the incoming data before comitting to the - cache. +- **`key|key(variables[, cached])`:** A unique identifier for the requested + data. Can be a string or a function. Functions will be called with the + variables and the cached data, if there is any. This can be used to determine + the key of e.g. a mutation where the key is not known untill a response is + retrieved. The default is the variables as a serialized string, or a + stringified representation of the query if no variables are provided. +- **`parse(response[, cached])`:** Parse the incoming data before comitting to + the cache. - **`mutate(cached)`:** Mutate the cached data prior to reading from cache or fetching data. This is useful for e.g. immedately updating the UI while submitting changes to the back end. -## Advanced usage +## Expressions and Operations One of the benefits of GraphQL is the strucuted format of the queries. When passing a query to the `gql` tag, nanographql will parse the string identifying individual queries, mutations, subscritions and fragments and expose these as -individual functions. It will also mix in interpolated fragments from other +individual operations. It will also mix in interpolated fragments from other queries. ```js @@ -107,20 +107,20 @@ const { GetUser, SaveUser } = gql` const graphql = nanographql('/graphql', render) function render () { - const { errors, data } = graphql(GetUser({ id: 'abc123' }), { key: 'id' }) + const { errors, data } = graphql(GetUser({ id: 'abc123' }), { key: 'abc123' }) if (errors) return html`

User not found

` if (!data) return html`

Loading

` return html` Name: - Save ` function onsubmit (event) { graphql(SaveUser({ id: 'abc123', name: this.username.value }), { - key: 'id', + key: 'abc123', mutate (cached) { const user = { ...data.user, name } return { data: { user } } From 8c4744e00ffd09b124109bd538ee2c5b4346dff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 10:34:30 +0100 Subject: [PATCH 10/14] Update mutate example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfcd5d0..5b8ef18 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ function render () { graphql(SaveUser({ id: 'abc123', name: this.username.value }), { key: 'abc123', mutate (cached) { - const user = { ...data.user, name } + const user = { ...cached.data.user, name } return { data: { user } } } }) From c9e98ea10019c205aba289005a11ae61a7838dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 10:38:38 +0100 Subject: [PATCH 11/14] Fix travis config, node compat --- .travis.yml | 7 +++---- test.js | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3ea5d4f..0f02967 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ node_js: -- "4" -- "5" -- "6" -- "7" +- "10" +- "12" +- "14" sudo: false language: node_js script: "npm run test" diff --git a/test.js b/test.js index 134b159..80da9d5 100644 --- a/test.js +++ b/test.js @@ -243,7 +243,7 @@ tape('fetch handler', function (t) { t.ok(opts.body, 'body is set') const body = JSON.parse(opts.body) t.deepEqual(body, operation.toJSON(), 'operation is encoded as json body') - t.equal(opts.headers?.['Content-Type'], 'application/json', 'header is set to json') + t.equal(opts.headers['Content-Type'], 'application/json', 'header is set to json') t.equal(opts.method, 'POST', 'method is POST') cb(null) } @@ -269,7 +269,7 @@ tape('fetch handler', function (t) { const body = JSON.parse(opts.body) t.equal(url, '/graphql', 'url is untouched') t.deepEqual(body, operation.toJSON(), 'payload is json encoded') - t.equal(opts.headers?.['Content-Type'], 'application/json', 'header is set to json') + t.equal(opts.headers['Content-Type'], 'application/json', 'header is set to json') t.equal(opts.method, 'POST', 'method is POST') cb(null) } @@ -299,7 +299,7 @@ tape('fetch handler', function (t) { function fetch (url, opts, cb) { t.equal(opts.body, body, 'body is preserved') - t.equal(opts.headers?.['Content-Type'], contentType, 'content type is preserved') + t.equal(opts.headers['Content-Type'], contentType, 'content type is preserved') t.equal(opts.method, method, 'method is preserved') cb(null) } @@ -462,7 +462,7 @@ tape('cache handler', function (t) { yield graphql(Mutation({ value: 'bin' }, { key (res) { - return res?.data.key + return res && res.data.key } })) let bar = graphql(Query({ value: 'bar' }), { key: 'baz' }) From 5395959da98950aee080d4120d2007d22697eaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 11:21:26 +0100 Subject: [PATCH 12/14] Update example w/ choo --- README.md | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5b8ef18..72ec681 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ individual operations. It will also mix in interpolated fragments from other queries. ```js +const choo = require('choo') +const html = require('choo/html') const { gql, nanographql } = require('nanographql') const { user } = gql` @@ -104,22 +106,40 @@ const { GetUser, SaveUser } = gql` } ` -const graphql = nanographql('/graphql', render) +const app = choo() -function render () { - const { errors, data } = graphql(GetUser({ id: 'abc123' }), { key: 'abc123' }) - if (errors) return html`

User not found

` - if (!data) return html`

Loading

` +app.route('/', main) + +app.use(function (state, emitter) { + const graphql = nanographql('/graphql') + + state.api = (...args) => graphql(...args, render) + + function render () { + emitter.emit('render') + } +}) + +app.mount(document.body) + +function main (state, emit) { + const { api } = state + const { errors, data } = api(GetUser({ id: 'abc123' }), { key: 'abc123' }) + + if (errors) return html`

User not found

` + if (!data) return html`

Loading

` return html` -
- Name: - -
+ +
+ Name: + +
+ ` function onsubmit (event) { - graphql(SaveUser({ id: 'abc123', name: this.username.value }), { + api(SaveUser({ id: 'abc123', name: this.username.value }), { key: 'abc123', mutate (cached) { const user = { ...cached.data.user, name } From 2363a6bf40cc7525e54b6086bbb3be5182c1ca46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Thu, 5 Nov 2020 11:36:02 +0100 Subject: [PATCH 13/14] Fix irregularities in readme --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 72ec681..28cef93 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,16 @@ Create a managed cache which fetches data as it is requested. #### Options - **`cache`:** a custom cache store. Should implement `get` and `set` methods. - *Default: `new Map()`*. -- **`fetch`:** a custom [`fetch`][12] implementation. - *Default: `window.fetch`*. + The default is a [`Map`][15]. +- **`fetch`:** a custom [`fetch`][12] implementation. The `fetch` option should + be a function which takes three arguments, `url`, `opts` and a callback + function. The callback function should be called whenever there is an error or + new data available. The default is an implementation of `window.fetch`. -### `result = cache(operation[, opts])` -Query the cache and fetch query if necessary. The arguments match that of -[`fetch`][13] with a couple extra options. +### `result = cache(operation[, opts][, cb])` +Query the cache and fetch query if necessary. The options match that of +[`fetch`][13] with a couple extra options. The callback will be called whenever +an error or new data becomes available. #### Options The options are forwarded to the [`fetch`][12] implementation but a few are @@ -170,3 +173,4 @@ function main (state, emit) { [12]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch [13]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters [14]: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache +[15]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map From ee765df31a428cb3090e373c0d5dbd4b86bcef98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Fri, 6 Nov 2020 09:06:08 +0100 Subject: [PATCH 14/14] Update example --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 28cef93..88a1ffa 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,11 @@ const { GetUser, SaveUser } = gql` const app = choo() +app.use(store) app.route('/', main) +app.mount(document.body) -app.use(function (state, emitter) { +function store (state, emitter) { const graphql = nanographql('/graphql') state.api = (...args) => graphql(...args, render) @@ -121,9 +123,7 @@ app.use(function (state, emitter) { function render () { emitter.emit('render') } -}) - -app.mount(document.body) +} function main (state, emit) { const { api } = state