Showing preview only (202K chars total). Download the full file or copy to clipboard to get everything.
Repository: grafoojs/grafoo
Branch: main
Commit: 93fac0521791
Files: 92
Total size: 181.2 KB
Directory structure:
gitextract_2ic31ndv/
├── .circleci/
│ └── config.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── changelog.md
├── lerna.json
├── package.json
├── packages/
│ ├── babel-plugin/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── __snapshots__/
│ │ │ │ └── index.js.snap
│ │ │ ├── compile-document.js
│ │ │ ├── index.js
│ │ │ ├── insert-fields.js
│ │ │ ├── schema.graphql
│ │ │ └── sort-query.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── src/
│ │ ├── compile-document.js
│ │ ├── index.js
│ │ ├── insert-fields.js
│ │ └── sort-query.js
│ ├── bindings/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── bundle/
│ │ ├── cli.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── core/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── build-query-tree.ts
│ │ │ ├── index.ts
│ │ │ ├── map-objects.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ ├── build-query-tree.ts
│ │ │ ├── index.ts
│ │ │ ├── map-objects.ts
│ │ │ └── util.ts
│ │ ├── tag.d.ts
│ │ ├── tag.js
│ │ └── tsconfig.json
│ ├── http-transport/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── preact/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.tsx
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ ├── consumer.ts
│ │ │ ├── index.ts
│ │ │ └── provider.ts
│ │ └── tsconfig.json
│ ├── react/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.tsx
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── test-utils/
│ │ ├── package.json
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ ├── db.ts
│ │ │ ├── index.ts
│ │ │ └── mock-server.ts
│ │ └── tsconfig.json
│ └── types/
│ ├── index.d.ts
│ ├── package.json
│ └── readme.md
├── readme.md
└── scripts/
├── build.js
├── jest-setup.js
└── resolver.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
build:
docker:
- image: cimg/node:14.15.1
steps:
- checkout
- restore_cache:
name: Restore yarn package cache
keys:
- yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-{{ .Branch }}
- yarn-packages-master
- yarn-packages-
- run:
name: Install dependencies and build packages
command: yarn
- save_cache:
name: Save yarn package cache
key: yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths:
- node_modules/
- run:
name: Run tests
command: yarn test:coverage
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# vscode
.vscode
# output directories
dist/
# Typescript
.rpt2_cache
# temp folder
temp
# mac
.DS_store
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at albernazmiguel@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
## Not sure where to start?
If you're not sure where to start? You'll probably want to learn a bit about a few topics before getting dirt in your hands.
- [ASTs](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (Abstract Syntax Tree): this project makes heavy use of code transformation with Babel and GraphQL. Check out [AST Explorer](http://astexplorer.net/) to learn more about ASTs interactively
- [Babel](https://github.com/babel/babel): I'd recommend a read to the [the Babel Plugin Handbook](https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md#babel-plugin-handbook) to understand how a plugin is written.
- [GraphQL](https://graphql.org/graphql-js/graphql): the GraphQL.js module is not only meant to build servers, it also exports a core subset of GraphQL functionality for creation of GraphQL type systems.
- [Lerna](https://github.com/lerna/lerna): this is mono repository and we use Lerna to manage our packages.
- [Yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/): Lerna is setup to be used with Yarn workspaces.
## Chat
Have read this contributing guide and still need some help? Feel free join our [slack channel](https://grafoo-slack.herokuapp.com).
## Disclaimer
**As Lerna is configured in this package to be used with Yarn, not using NPM will save you a lot of time.**
## Setup
```sh
$ git clone https://github.com/grafoojs/grafoo
$ cd grafoo
$ yarn # this command will install dependencies and automatically build every package
```
## Build packages
#### Build all packages
As mentioned above after every `yarn` install all the packages are built automatically. But if you want to build then anyway just run:
```sh
$ yarn prepare
```
#### Build single package
```sh
$ cd packages/[any-package]
$ yarn build
```
## Run tests
#### All tests
```sh
$ yarn test
```
#### All tests with coverage
I recomend the usage of [NPX](https://www.npmjs.com/package/npx) for any Lerna command if you don't want to install it globally.
```sh
$ npx lerna run test:coverage
```
#### Test individual package
```sh
$ cd packages/[any-package]
$ yarn test
```
#### Test individual package in watch mode
```sh
$ cd packages/[any-package]
$ yarn test --watch
```
#### Test individual package with coverage
```sh
$ cd packages/[any-package]
$ yarn test:coverage
```
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Miguel Albernaz <albernazmiguel@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: changelog.md
================================================
# CHANGELOG
## 1.4.0
### Features
- [babel-plugin, core] adds an option to babel-plugin to generate an id side by side with the query in Grafoo Object. This feature will enable persistent queries in the future.
### Contributors:
- @[adjourn](/adjourn)
## 1.3.0
### Features
- [core] add reset method to client to clear the cache.
### Fixes
- [babel-plugin] Correctly identify variables in arguments
- [babel-plugin] Don't throw error when encountering a Union node
- [babel-plugin] Prevent multiple instances of idFields to be added to the same node in a query
### Contributors:
- @[mogelbrod](/mogelbrod)
## 1.2.0
### Features
- [babel-plugin] allow schema to be omited on config
### Fixes
- [babel-plugin] fix client factory throwing when called with a variable as the second argument
## 1.1.1
### Fixes
- [preact] fix variables not being updated
## 1.1.0
### Features
- [bindings] enable load method to receive and update variables
## 1.0.9
### Fixes
- [core] fix bug preventing write to work when graphql payload wasn't destructured beforehand
## 1.0.8
### Fixes
- [bindings] for safety reusing `props` argument to avoid declaration of new variables
## 1.0.7
### Fixes
- [react, preact] reload when variables change
## 1.0.6
### Fixes
- [preact] correctly type children prop
## v0.0.1-beta.15
### Fixes
- [core] fix latest bug that afected also `client.read`
## v0.0.1-beta.14
### Fixes
- [core] allow queries to be partially cached on `client.write`
## v0.0.1-beta.13
### Features
- [preact, react] reload query on skip `Consumer` prop change to true
## v0.0.1-beta.12
### Fixes
- [preact] fix preact not rendering on nested componenents when data have been already received
## v0.0.1-beta.11
### Features
- [core] return a `partial` property in read to flag if a query result is only partially cached
### Fixes
- [bindings] fix bindings returning `loaded` equals to true if a query is only partially cached
## v0.0.1-beta.10
### Features
- [bindings, react, preact] revert latest release reintroducing `loaded` prop from bindings
## v0.0.1-beta.9
### Features
- [bindings, react, preact] remove `loaded` prop from bindings
## v0.0.1-beta.8
### Features
- [core] transport logic has been removed from core. This is a breaking change and here is the fix:
```diff
import createClient from "@grafoo/core";
+ function fetchQuery(query, variables) {
+ const init = {
+ method: "POST",
+ body: JSON.stringify({ query, variables }),
+ headers: {
+ "content-type": "application/json"
+ }
+ };
+
+ return fetch("http://some.graphql.api", init).then(res => res.json());
+ }
- const client = createClient("http://some.graphql.api");
+ const client = createClient(fetchQuery);
```
## v0.0.1-beta.7
### Features
- [core, transport] allow other fetch options to be set other then headers
## v0.0.1-beta.6
### Fixes
- building packages locally
## v0.0.1-beta.5
### Fixes
- last failed attempt to install packages on local install
## v0.0.1-beta.4
### Fixes
- [babel-plugin] add @babel/cli
## v0.0.1-beta.3
### Fixes
- fix further packages dependencies
## v0.0.1-beta.2
### Fixes
- [bundle] add mri to package dependecies
## v0.0.1-beta.1
### Fixes
- add coverage to packages .npmignore
## v0.0.1-beta.0
### Fixes
- [core] fix objects not being cleaned from the cache on removal
## v0.0.1-alpha.17
### Fixes
- [bindings] fix block scope bug due to the use of var instead of let
## v0.0.1-alpha.16
### Fixes
- [babel-plugin] fix fragments not being compiled correctly in babel-plugin;
- [bindings] fix shouldupdate logic in bindings
## v0.0.1-alpha.15
### Fixes
- [babel-plugin] fix bug the was preventing fragments to be compiled in babel-plugin `sort-query`
- [babel-plugin] improve coverage
## v0.0.1-alpha.14
### Fixes
- [react] same as before but now it's working
## v0.0.1-alpha.13
### Fixes
- [react] fix a bug that was preventing component setState to work within the consumer render function
## v0.0.1-alpha.12
### Features
- replace `@babel/preset-typescript` for `rollup-plugin-typescript2` in `grafoo-bundle`
## v0.0.1-alpha.11
### Features
- bindings generated mutation functions now resolve with the mutation response
- bindings mutations `prop` does not require the update hook anymore
### Fixes
- bindings `loading` flag is always false whenever the `load` is triggered
================================================
FILE: lerna.json
================================================
{
"packages": [
"packages/*"
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "1.4.2",
"command": {
"publish": {
"message": "chore(release): publish %s",
"npmClient": "npm"
}
}
}
================================================
FILE: package.json
================================================
{
"private": true,
"name": "grafoo",
"description": "a graphql client and toolkit",
"repository": "https://github.com/grafoojs/grafoo",
"author": "malbernaz <albernazmiguel@gmail.com>",
"license": "MIT",
"scripts": {
"bootstrap": "lerna bootstrap",
"test": "lerna run test",
"test:coverage": "lerna run test:coverage && codecov",
"prepare": "node scripts/build.js",
"clean": "rimraf packages/**/dist"
},
"husky": {
"hooks": {
"pre-push": "lerna run test",
"pre-commit": "lint-staged"
}
},
"workspaces": [
"packages/*"
],
"lint-staged": {
"*.{js,ts,tsx,json,graphql}": [
"eslint --fix",
"prettier --write"
]
},
"prettier": {
"printWidth": 100,
"trailingComma": "none"
},
"eslintConfig": {
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017,
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"@typescript-eslint",
"prefer-let"
],
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true,
"jest": true
},
"rules": {
"prefer-const": 0,
"prefer-let/prefer-let": 2,
"@typescript-eslint/ban-ts-comment": 1,
"@typescript-eslint/no-empty-function": 1
},
"ignorePatterns": [
"packages/bundle",
"scripts"
]
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.0.0",
"@babel/register": "^7.0.0",
"@graphql-tools/schema": "^8.2.0",
"@types/jest": "^27.0.2",
"@types/node": "^16.10.1",
"@types/react": "^17.0.24",
"@types/react-test-renderer": "^17.0.1",
"@types/uuid": "^8.3.1",
"@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"babel-plugin-jsx-pragmatic": "^1.0.2",
"babel-plugin-tester": "^10.0.0",
"casual": "^1.5.19",
"codecov": "^3.2.0",
"eslint": "^7.15.0",
"eslint-plugin-prefer-let": "^1.1.0",
"fetch-mock": "^9.11.0",
"graphql": "^15.4.0",
"husky": "^7.0.2",
"jest": "^27.2.2",
"lerna": "^4.0.0",
"lint-staged": "^11.1.2",
"lowdb": "^3.0.0",
"node-fetch": "^3.0.0",
"preact": "^8.3.0",
"preact-render-spy": "^1.3.0",
"prettier": "^2.2.1",
"react": "^16.8.2",
"react-test-renderer": "^16.8.2",
"resolve.exports": "^1.0.2",
"rimraf": "^3.0.2",
"typescript": "^4.1.2",
"uuid": "^8.3.2"
}
}
================================================
FILE: packages/babel-plugin/.babelrc
================================================
{
"presets": [["@babel/preset-env", { "targets": { "node": 4 } }]]
}
================================================
FILE: packages/babel-plugin/.npmignore
================================================
coverage
__tests__
.rpt2_cache
.babelrc
schema.graphql
================================================
FILE: packages/babel-plugin/__tests__/__snapshots__/index.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@grafoo/babel-plugin should compress the query string if the option compress is specified: should compress the query string if the option compress is specified 1`] = `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
↓ ↓ ↓ ↓ ↓ ↓
let query = {
query:
"query($id:ID!,$offset:Int!,$start:Int!){posts(offset:$offset,start:$start){authors{id name username}body createdAt id tags{id name}title}user(id:$id){id name username}}",
paths: {
"posts(offset:$offset,start:$start){authors{id name username}body createdAt id tags{id name}title}":
{
name: "posts",
args: ["offset", "start"]
},
"user(id:$id){id name username}": {
name: "user",
args: ["id"]
}
}
};
`;
exports[`@grafoo/babel-plugin should generate md5 hash and add it to object if the option generateIds is specified: should generate md5 hash and add it to object if the option generateIds is specified 1`] = `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
↓ ↓ ↓ ↓ ↓ ↓
let query = {
id: "6e0697df8f2453f2643bbd1e8a39c348",
query:
"query ($id: ID!, $offset: Int!, $start: Int!) {\\n posts(offset: $offset, start: $start) {\\n authors {\\n id\\n name\\n username\\n }\\n body\\n createdAt\\n id\\n tags {\\n id\\n name\\n }\\n title\\n }\\n user(id: $id) {\\n id\\n name\\n username\\n }\\n}",
paths: {
"posts(offset:$offset,start:$start){authors{id name username}body createdAt id tags{id name}title}":
{
name: "posts",
args: ["offset", "start"]
},
"user(id:$id){id name username}": {
name: "user",
args: ["id"]
}
}
};
`;
exports[`@grafoo/babel-plugin should include \`idFields\` in the client instantiation even if options are provided: should include \`idFields\` in the client instantiation even if options are provided 1`] = `
import createClient from "@grafoo/core";
let query = createClient(someTransport, {
headers: () => ({ authorization: "some-token" })
});
↓ ↓ ↓ ↓ ↓ ↓
import createClient from "@grafoo/core";
let query = createClient(someTransport, {
headers: () => ({
authorization: "some-token"
}),
idFields: ["id"]
});
`;
exports[`@grafoo/babel-plugin should include \`idFields\` in the client instantiation if not present in options: should include \`idFields\` in the client instantiation if not present in options 1`] = `
import createClient from "@grafoo/core";
let query = createClient(someTransport, {});
↓ ↓ ↓ ↓ ↓ ↓
import createClient from "@grafoo/core";
let query = createClient(someTransport, {
idFields: ["id"]
});
`;
exports[`@grafoo/babel-plugin should include \`idFields\` in the client instantiation if options are not provided: should include \`idFields\` in the client instantiation if options are not provided 1`] = `
import createClient from "@grafoo/core";
let query = createClient(someTransport);
↓ ↓ ↓ ↓ ↓ ↓
import createClient from "@grafoo/core";
let query = createClient(someTransport, {
idFields: ["id"]
});
`;
exports[`@grafoo/babel-plugin should include \`idFields\` in the client instantiation if options is a variable: should include \`idFields\` in the client instantiation if options is a variable 1`] = `
import createClient from "@grafoo/core";
let options = {};
let query = createClient(someTransport, options);
↓ ↓ ↓ ↓ ↓ ↓
import createClient from "@grafoo/core";
let options = {
idFields: ["id"]
};
let query = createClient(someTransport, options);
`;
exports[`@grafoo/babel-plugin should not generate md5 hash and add it to object if the option generateIds is falsey: should not generate md5 hash and add it to object if the option generateIds is falsey 1`] = `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
↓ ↓ ↓ ↓ ↓ ↓
let query = {
query:
"query ($id: ID!, $offset: Int!, $start: Int!) {\\n posts(offset: $offset, start: $start) {\\n authors {\\n id\\n name\\n username\\n }\\n body\\n createdAt\\n id\\n tags {\\n id\\n name\\n }\\n title\\n }\\n user(id: $id) {\\n id\\n name\\n username\\n }\\n}",
paths: {
"posts(offset:$offset,start:$start){authors{id name username}body createdAt id tags{id name}title}":
{
name: "posts",
args: ["offset", "start"]
},
"user(id:$id){id name username}": {
name: "user",
args: ["id"]
}
}
};
`;
exports[`@grafoo/babel-plugin should overide \`idFields\` in the client instantiation if options is a variable: should overide \`idFields\` in the client instantiation if options is a variable 1`] = `
import createClient from "@grafoo/core";
let options = { idFields: ["err"] };
let query = createClient(someTransport, options);
↓ ↓ ↓ ↓ ↓ ↓
import createClient from "@grafoo/core";
let options = {
idFields: ["id"]
};
let query = createClient(someTransport, options);
`;
exports[`@grafoo/babel-plugin should remove the imported path: should remove the imported path 1`] = `
import gql from "@grafoo/core/tag";
↓ ↓ ↓ ↓ ↓ ↓
`;
exports[`@grafoo/babel-plugin should replace a tagged template literal with the compiled grafoo object: should replace a tagged template literal with the compiled grafoo object 1`] = `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
↓ ↓ ↓ ↓ ↓ ↓
let query = {
query:
"query ($id: ID!, $offset: Int!, $start: Int!) {\\n posts(offset: $offset, start: $start) {\\n authors {\\n id\\n name\\n username\\n }\\n body\\n createdAt\\n id\\n tags {\\n id\\n name\\n }\\n title\\n }\\n user(id: $id) {\\n id\\n name\\n username\\n }\\n}",
paths: {
"posts(offset:$offset,start:$start){authors{id name username}body createdAt id tags{id name}title}":
{
name: "posts",
args: ["offset", "start"]
},
"user(id:$id){id name username}": {
name: "user",
args: ["id"]
}
}
};
`;
================================================
FILE: packages/babel-plugin/__tests__/compile-document.js
================================================
import * as babel from "@babel/core";
import plugin from "../src";
let transform = (program, opts) =>
babel.transform(program, {
plugins: [
[plugin, Object.assign({ schema: "__tests__/schema.graphql", idFields: ["id"] }, opts)],
],
});
describe("compile document", () => {
it("should throw if a schema path points to a inexistent file", () => {
let program = `
import gql from "@grafoo/core/tag";
let query = gql\`{ hello }\`;
`;
expect(() => transform(program, { schema: "?" })).toThrow();
});
it("should throw if more then one operation is specified", () => {
let program = `
import gql from "@grafoo/core/tag";
let query = gql\`
{ hello }
{ goodbye }
\`;
`;
expect(() => transform(program)).toThrow();
});
it("should accept fragments", () => {
let program = `
import gql from "@grafoo/core/tag";
let query = gql\`
fragment UserInfo on User {
name
bio
}
\`;
`;
expect(() => transform(program)).not.toThrow();
});
it("should accept named queries", () => {
let program = `
import gql from "@grafoo/core/tag";
let query = gql\`
query NamedQuery {
me { id }
}
\`;
`;
expect(() => transform(program)).not.toThrow();
});
it("should accept named queries with arguments", () => {
let program = `
import gql from "@grafoo/core/tag";
let query = gql\`
query NamedQuery($var: ID!) {
post(id: $var) {
id
title
}
}
\`;
`;
expect(() => transform(program)).not.toThrow();
});
});
================================================
FILE: packages/babel-plugin/__tests__/index.js
================================================
import pluginTester from "babel-plugin-tester";
import plugin from "../src";
pluginTester({
plugin,
pluginName: "@grafoo/babel-plugin",
pluginOptions: {
schema: "__tests__/schema.graphql",
idFields: ["id"],
},
tests: {
"should throw if a import is not default": {
code: 'import { gql } from "@grafoo/core/tag";',
error: true,
},
"should throw if a schema is not present on the root directory": {
pluginOptions: {
idFields: ["id"],
},
code: `
import gql from "@grafoo/core/tag";
let query = gql\`{ hello }\`;
`,
error: true,
},
"should throw if a tagged template string literal has expressions in it": {
code: `
import gql from "@grafoo/core/tag";
let query = gql\`{ user(id: "\${1}") { name } }\`;
`,
error: true,
},
"should remove the imported path": {
code: 'import gql from "@grafoo/core/tag";',
snapshot: true,
},
"should throw if idFields is not defined": {
pluginOptions: {
schema: "__tests__/schema.graphql",
},
code: `
import gql from "@grafoo/core/tag";
let query = gql\`{ hello }\`;
`,
error: true,
},
"should throw if during client instatiation options is passed with a type other then object": {
code: `
import createClient from "@grafoo/core";
let query = createClient(someTransport, "I AM ERROR");
`,
error: true,
},
"should throw if the type of some field in `idFields` is not of type string": {
pluginOptions: {
schema: "__tests__/schema.graphql",
idFields: ["id", true],
},
code: `
import createClient from "@grafoo/core";
let query = createClient(someTransport);
`,
error: true,
},
"should replace a tagged template literal with the compiled grafoo object": {
code: `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
`,
snapshot: true,
},
"should compress the query string if the option compress is specified": {
pluginOptions: {
schema: "__tests__/schema.graphql",
idFields: ["id"],
compress: true,
},
code: `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
`,
snapshot: true,
},
"should generate md5 hash and add it to object if the option generateIds is specified": {
pluginOptions: {
schema: "__tests__/schema.graphql",
idFields: ["id"],
generateIds: true,
},
code: `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
`,
snapshot: true,
},
"should not generate md5 hash and add it to object if the option generateIds is falsey": {
pluginOptions: {
schema: "__tests__/schema.graphql",
idFields: ["id"],
},
code: `
import gql from "@grafoo/core/tag";
let query = gql\`
query($start: Int!, $offset: Int!, $id: ID!) {
posts(start: $start, offset: $offset) {
title
body
createdAt
tags { name }
authors { name username }
}
user(id: $id) { name username }
}
\`;
`,
snapshot: true,
},
"should include `idFields` in the client instantiation if options are not provided": {
code: `
import createClient from "@grafoo/core";
let query = createClient(someTransport);
`,
snapshot: true,
},
"should include `idFields` in the client instantiation if not present in options": {
code: `
import createClient from "@grafoo/core";
let query = createClient(someTransport, {});
`,
snapshot: true,
},
"should include `idFields` in the client instantiation if options is a variable": {
code: `
import createClient from "@grafoo/core";
let options = {};
let query = createClient(someTransport, options);
`,
snapshot: true,
},
"should overide `idFields` in the client instantiation if options is a variable": {
code: `
import createClient from "@grafoo/core";
let options = { idFields: ["err"] };
let query = createClient(someTransport, options);
`,
snapshot: true,
},
"should throw if `idFields` in the client instantiation if options is not an object variable": {
code: `
import createClient from "@grafoo/core";
let options = [];
let query = createClient(someTransport, options);
`,
error: true,
},
"should include `idFields` in the client instantiation even if options are provided": {
code: `
import createClient from "@grafoo/core";
let query = createClient(someTransport, {
headers: () => ({ authorization: "some-token" })
});
`,
snapshot: true,
},
},
});
================================================
FILE: packages/babel-plugin/__tests__/insert-fields.js
================================================
import fs from "fs";
import { parse, print } from "graphql";
import path from "path";
import insertFields from "../src/insert-fields";
let schema = fs.readFileSync(path.join(__dirname, "schema.graphql"), "utf-8");
let cases = [
{
should: "should insert a field",
input: "{ author { name } }",
expectedOutput: "{ author { name id } }",
idFields: ["id"],
},
{
should: "should insert more then a field if specified",
input: "{ author { name } }",
expectedOutput: "{ author { name username email id } }",
idFields: ["username", "email", "id"],
},
{
should: "should insert `__typename` if specified",
input: "{ author { name } }",
expectedOutput: "{ author { name __typename } }",
idFields: ["__typename"],
},
{
should: "should insert props in queries with fragments",
input: `
{
user {
...UserFrag
}
}
fragment UserFrag on Author {
name
posts {
title
}
}
`,
expectedOutput: `
{
user {
...UserFrag
id
}
}
fragment UserFrag on Author {
name
posts {
title
id
}
}
`,
idFields: ["id"],
},
{
should: "should insert props in queries with inline fragments",
input: `
{
user {
name
...on Author {
posts {
title
}
}
}
}
`,
expectedOutput: `
{
user {
name
...on Author {
posts {
title
id
__typename
}
}
id
__typename
}
}
`,
idFields: ["id", "__typename"],
},
{
should: "should not insert `__typename` inside fragments",
input: `
{
user {
...UserFrag
}
}
fragment UserFrag on Author {
name
posts {
title
}
}
`,
expectedOutput: `
{
user {
...UserFrag
__typename
}
}
fragment UserFrag on Author {
name
posts {
title
__typename
}
}
`,
idFields: ["__typename"],
},
{
should: "should not insert `__typename` inside inline fragments",
input: `
{
user {
name
...on Author {
posts {
title
}
}
}
}
`,
expectedOutput: `
{
user {
name
...on Author {
posts {
title
__typename
}
}
__typename
}
}
`,
idFields: ["__typename"],
},
{
should: "should insert field present on a fragment",
input: `
{
user {
...UserFrag
}
}
fragment UserFrag on Author {
name
posts {
title
}
}
`,
expectedOutput: `
{
user {
...UserFrag
}
}
fragment UserFrag on Author {
name
posts {
title
}
bio
}
`,
idFields: ["bio"],
},
{
should: "should insert field present in an inline fragment",
input: `
{
user {
name
...on Author {
posts {
title
}
}
}
}
`,
expectedOutput: `
{
user {
name
...on Author {
posts {
title
}
bio
}
}
}
`,
idFields: ["bio"],
},
{
should: "should insert fields in inline fragments while leaving unions",
input: `
{
viewer {
...on Visitor {
ip
}
...on User {
username
}
}
}
`,
expectedOutput: `
{
viewer {
...on Visitor {
ip
id
}
...on User {
username
id
}
}
}
`,
idFields: ["id"],
},
{
should: "should not insert `__typename` in an operation definition",
input: `
mutation createPost($title: Int!, $body: Int!, $id: ID! $authors: [ID!]!) {
createPost(title: $title, body: $body, authors: $authors) {
title
body
createdAt
tags { name }
authors { name username }
}
}
`,
expectedOutput: `
mutation createPost($title: Int!, $body: Int!, $id: ID! $authors: [ID!]!) {
createPost(title: $title, body: $body, authors: $authors) {
title
body
createdAt
tags { name id __typename }
authors { name username id __typename }
id
__typename
}
}
`,
idFields: ["id", "__typename"],
},
];
describe("insert-fields", () => {
for (let { should, input, expectedOutput, idFields } of cases) {
it(should, () => {
expect(print(insertFields(schema, parse(input), idFields))).toBe(
print(parse(expectedOutput))
);
});
}
});
================================================
FILE: packages/babel-plugin/__tests__/schema.graphql
================================================
type Mutation {
createPost(title: String!, body: String!, authors: [ID!]!, tags: [String!]): Post
deletePost(id: ID): Post
createTag(name: String!): Tag
register(username: String!, email: String!, password: String!): User
login(email: String!, password: String!): String
updateUser(username: String, name: String, bio: String, email: String, password: String): User
}
type Post {
id: ID!
title: String!
slug: String!
body: String!
published: Boolean!
createdAt: String!
updateAt: String!
authors: [User!]!
tags: [Tag!]!
}
type Query {
author(id: ID!): Author
authors(start: Int!, offset: Int!): [Author]
posts(start: Int!, offset: Int!): [Post]
post(id: ID!): Post
tag(id: ID!): Tag
users(start: Int!, offset: Int!): [User]
user(id: ID!): User
me: User
viewer: Viewer
}
union Viewer = Visitor | User
type Tag {
id: ID!
name: String!
posts: [Post!]!
createdAt: String!
updateAt: String!
}
interface User {
id: ID!
username: String!
email: String!
createdAt: String!
updatedAt: String!
}
type Author implements User {
name: String
bio: String
posts: [Post!]!
}
type Visitor {
id: ID!
ip: String!
}
================================================
FILE: packages/babel-plugin/__tests__/sort-query.js
================================================
import { parse, print as graphqlPrint } from "graphql";
import sortQuery from "../src/sort-query";
let gql = String.raw;
function print(query, sort = false) {
return sort ? graphqlPrint(sortQuery(parse(query))) : graphqlPrint(parse(query));
}
describe("sort-query", () => {
it("should sort fields, variable declarations and arguments", () => {
let query = gql`
query($f: ID, $e: ID, $d: ID, $c: ID, $b: ID, $a: ID) {
f
e
d
c
b
a(f: $f, e: $e, d: $d, c: $c, b: $b, a: $a) {
f
e
d
c
b
a(f: $f, e: $e, d: $d, c: $c, b: $b, a: $a) {
f
e
d
c
b
}
}
}
`;
let expected = gql`
query($a: ID, $b: ID, $c: ID, $d: ID, $e: ID, $f: ID) {
a(a: $a, b: $b, c: $c, d: $d, e: $e, f: $f) {
a(a: $a, b: $b, c: $c, d: $d, e: $e, f: $f) {
b
c
d
e
f
}
b
c
d
e
f
}
b
c
d
e
f
}
`;
expect(print(query, true)).toBe(print(expected));
});
it("should sort fragments", () => {
let query = gql`
query {
user {
posts {
...PostInfo
}
...UserInfo
}
}
fragment UserInfo on User {
id
id
name
bio
}
fragment PostInfo on Post {
title
content
}
`;
let expected = gql`
fragment PostInfo on Post {
content
title
}
fragment UserInfo on User {
bio
id
id
name
}
query {
user {
...UserInfo
posts {
...PostInfo
}
}
}
`;
expect(print(query, true)).toBe(print(expected));
});
it("should sort inline fragments", () => {
let query = gql`
query {
user {
posts {
... on Post {
title
content
}
}
... on User {
id
name
bio
}
}
}
`;
let expected = gql`
{
user {
... on User {
bio
id
name
}
posts {
... on Post {
content
title
}
}
}
}
`;
expect(print(query, true)).toBe(print(expected));
});
it("should sort directives", () => {
let query = gql`
query($c: ID, $b: ID, $a: ID) {
someField @c(c: $c) @a(a: $a) @b(c: $b)
}
`;
let expected = gql`
query($a: ID, $b: ID, $c: ID) {
someField @a(a: $a) @b(c: $b) @c(c: $c)
}
`;
expect(print(query, true)).toBe(print(expected));
});
});
================================================
FILE: packages/babel-plugin/package.json
================================================
{
"name": "@grafoo/babel-plugin",
"version": "1.4.2",
"description": "grafoo client babel plugin",
"repository": "https://github.com/grafoojs/grafoo/tree/master/packages/babel-plugin",
"main": "dist/index.js",
"author": "malbernaz<albernazmiguel@gmail.com>",
"license": "MIT",
"keywords": [
"babel",
"babel-plugin",
"graphql",
"graphql-client",
"grafoo"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "babel src --out-dir dist",
"test": "jest",
"test:coverage": "jest --coverage"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx|js)$": "<rootDir>/../../scripts/jest-setup.js"
}
},
"dependencies": {
"babel-literal-to-ast": "^2.1.0",
"crypto-js": "^4.0.0",
"graphql": "^15.4.0",
"graphql-query-compress": "^1.0.0"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974",
"devDependencies": {
"graphql": "^15.4.0"
}
}
================================================
FILE: packages/babel-plugin/readme.md
================================================
# `@grafoo/babel-plugin`
<p><i>Grafoo Babel Plugin</i></p>
<p>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://www.npmjs.com/package/@grafoo/babel-plugin>
<img
src=https://img.shields.io/npm/v/@grafoo/babel-plugin.svg
alt=npm
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/babel-plugin>
<img
src=https://img.shields.io/npm/dm/@grafoo/babel-plugin.svg
alt=downloads
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
A premise Grafoo takes is that it should be able to extract an unique identifier from every node on the queries you write. It can be a GraphQL `ID` field, or more fields that together can form one (eg: an incremental integer and the GraphQL meta field `__typename`). It is `@grafoo/babel-plugin`'s responsibility to insert those fields on your queries automatically. If you have already used Apollo this should be very familiar to you, as our `idFields` configuration has the same pourpose of Apollo Cache's `dataIdFromObject`: to normalize your data.
## Install
```
$ npm i @grafoo/core && npm i -D @grafoo/babel-plugin
```
## Configuration
To configure the plugin is required to specify the option `idFields`, an array of strings that represent the fields that Grafoo will use to build object identifiers. The option `schema`, is a path to a GraphQL schema in your file system relative to the root of your project, if not specified the plugin will look for the schema in the root of your project:
```json
{
"plugins": [
[
"@grafoo/babel-plugin",
{
"schema": "schema.graphql",
"idFields": ["id"],
"generateIds": false
}
]
]
}
```
## How to get my schema?
The recommendation for now is to use the [`get-graphql-schema`](https://github.com/prismagraphql/get-graphql-schema), by [Prisma](https://www.prisma.io/). In the near future we are planning to introduce a `schemaUrl` option to this plugin so that this step won't be required anymore.
## Transformations
`@grafoo/babel-plugin` transforms your code in three ways:
- Template tag literals using the default export from submodule `@grafoo/core/tag` will be compiled to a special object that will assist the client on the caching process.
- Imports from submodule `@grafoo/core/tag` statements will be removed.
- `idFields` will be inserted automatically on client instantiation.
```diff
import createClient from "@grafoo/core";
- import graphql from "@grafoo/core/tag";
function fetchQuery(query, variables) {
const init = {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json"
}
};
return fetch("http://some.graphql.api", init).then(res => res.json());
}
- const client = createClient(fetchQuery);
+ const client = createClient(fetchQuery, {
+ idFields: ["id"]
+ });
- const USER_QUERY = graphql`
- query($id: ID!) {
- user(id: $id) {
- name
- posts {
- title
- }
- }
- }
- `;
+ const USER_QUERY = {
+ id: "d4b567cd2a8891aa4cd1840f1a53002e", // only if option "generateIds" is true
+ query: "query($id: ID!) { user(id: $id) { id name posts { id title } } }",
+ paths: {
+ "user(id:$id){id name posts{id title}}": {
+ name: "user"
+ args: ["id"]
+ }
+ }
+ };
```
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: packages/babel-plugin/src/compile-document.js
================================================
import fs from "fs";
import { parse, print } from "graphql";
import compress from "graphql-query-compress";
import md5Hash from "crypto-js/md5";
import path from "path";
import insertFields from "./insert-fields";
import sortDocument from "./sort-query";
let schema;
function getSchema(schemaPath) {
if (schema) return schema;
let fullPath;
if (!schemaPath) {
let schemaJson = path.join(process.cwd(), "schema.json");
let schemaGraphql = path.join(process.cwd(), "schema.graphql");
let schemaGql = path.join(process.cwd(), "schema.gql");
fullPath = fs.existsSync(schemaJson)
? schemaJson
: fs.existsSync(schemaGraphql)
? schemaGraphql
: fs.existsSync(schemaGql)
? schemaGql
: undefined;
} else {
fullPath = path.join(process.cwd(), schemaPath);
}
fs.accessSync(fullPath, fs.F_OK);
schema = fs.readFileSync(fullPath, "utf-8");
return schema;
}
export default function compileDocument(source, opts) {
let schema = getSchema(opts.schema);
let doc = sortDocument(insertFields(schema, parse(source), opts.idFields));
let oprs = doc.definitions.filter((d) => d.kind === "OperationDefinition");
let frags = doc.definitions.filter((d) => d.kind === "FragmentDefinition");
if (oprs.length > 1) {
throw new Error("@grafoo/core/tag: only one operation definition is accepted per tag.");
}
let grafooObj = {};
if (oprs.length) {
let printed = print(oprs[0]);
let compressed = compress(printed);
// Use compressed version to get same hash even if
// query has different whitespaces, newlines, etc
// Document is also sorted by "sortDocument" therefore
// selections, fields, etc order shouldn't matter either
if (opts.generateIds) {
grafooObj.id = md5Hash(compressed).toString();
}
grafooObj.query = opts.compress ? compressed : printed;
grafooObj.paths = oprs[0].selectionSet.selections.reduce(
(acc, s) =>
Object.assign(acc, {
// TODO: generate hashes as well
// based on compress(print(s))?
[compress(print(s))]: {
name: s.name.value,
args: s.arguments.map((a) => {
if (a.value && a.value.kind === "Variable") {
a = a.value;
}
return a.name.value;
}),
},
}),
{}
);
}
if (frags.length) {
grafooObj.frags = {};
for (let frag of frags) {
grafooObj.frags[frag.name.value] = opts.compress ? compress(print(frag)) : print(frag);
}
}
return grafooObj;
}
================================================
FILE: packages/babel-plugin/src/index.js
================================================
import parseLiteral from "babel-literal-to-ast";
import compileDocument from "./compile-document";
export default function transform({ types: t }) {
return {
visitor: {
Program(programPath, { opts }) {
let tagIdentifiers = [];
let clientFactoryIdentifiers = [];
if (typeof opts.compress !== "boolean") {
opts.compress = process.env.NODE_ENV === "production";
}
if (typeof opts.generateIds !== "boolean") {
opts.generateIds = false;
}
if (!opts.idFields) {
throw new Error("@grafoo/babel-plugin: the `idFields` option is required.");
}
if (
!Array.isArray(opts.idFields) ||
opts.idFields.some((field) => typeof field !== "string")
) {
throw new Error(
"@grafoo/babel-plugin: the `idFields` option must be declared as an array of strings."
);
}
programPath.traverse({
ImportDeclaration(path) {
let { source, specifiers } = path.node;
if (source.value === "@grafoo/core") {
let defaultSpecifier = specifiers.find((s) => t.isImportDefaultSpecifier(s));
clientFactoryIdentifiers.push(defaultSpecifier.local.name);
}
if (source.value === "@grafoo/core/tag") {
let defaultSpecifier = specifiers.find((specifier) =>
t.isImportDefaultSpecifier(specifier)
);
if (!defaultSpecifier) {
throw path.buildCodeFrameError("@grafoo/core/tag: no default import.");
}
tagIdentifiers.push(defaultSpecifier.local.name);
path.remove();
}
},
CallExpression(path) {
let { arguments: args, callee } = path.node;
let idFieldsArrayAst = t.arrayExpression(
opts.idFields.map((field) => t.stringLiteral(field))
);
let clientObjectAst = t.objectProperty(t.identifier("idFields"), idFieldsArrayAst);
if (clientFactoryIdentifiers.some((name) => t.isIdentifier(callee, { name }))) {
if (!args[1]) {
args[1] = t.objectExpression([clientObjectAst]);
}
if (t.isIdentifier(args[1])) {
let name = args[1].name;
let { init } = path.scope.bindings[name].path.node;
if (path.scope.hasBinding(name)) {
if (t.isObjectExpression(init)) {
let idFieldsProp = init.properties.find((arg) => arg.key.name === "idFields");
if (idFieldsProp) {
idFieldsProp.value = idFieldsArrayAst;
} else {
init.properties.push(clientObjectAst);
}
} else {
throw path.buildCodeFrameError(
callee.name +
" second argument must be of type object, instead got " +
args[1].type +
"."
);
}
}
} else if (t.isObjectExpression(args[1])) {
let idFieldsProp = args[1].properties.find((arg) => arg.key.name === "idFields");
if (idFieldsProp) {
idFieldsProp.value = idFieldsArrayAst;
} else {
args[1].properties.push(clientObjectAst);
}
} else {
throw path.buildCodeFrameError(
callee.name +
" second argument must be of type object, instead got " +
args[1].type +
"."
);
}
}
},
TaggedTemplateExpression(path) {
if (tagIdentifiers.some((name) => t.isIdentifier(path.node.tag, { name }))) {
try {
let quasi = path.get("quasi");
if (quasi.get("expressions").length) {
throw path.buildCodeFrameError(
"@grafoo/core/tag: interpolation is not supported in a graphql tagged template literal."
);
}
let source = quasi.node.quasis.reduce((src, q) => src + q.value.raw, "");
path.replaceWith(parseLiteral(compileDocument(source, opts)));
} catch (error) {
if (error.code === "ENOENT") {
throw new Error(
"Could not find a schema in the root directory! " +
"Please use the `schema` option to specify your schema path, " +
"or the `schemaUrl` to specify your graphql endpoint."
);
}
throw path.buildCodeFrameError(error.message);
}
}
},
});
},
},
};
}
================================================
FILE: packages/babel-plugin/src/insert-fields.js
================================================
import { TypeInfo, buildASTSchema, parse, visit, visitWithTypeInfo } from "graphql";
function getType(typeInfo) {
let currentType = typeInfo.getType();
while (currentType.ofType) currentType = currentType.ofType;
return currentType;
}
function insertField(selections, value) {
selections.push({ kind: "Field", name: { kind: "Name", value } });
}
export default function insertFields(schemaStr, documentAst, idFields) {
let typeInfo = new TypeInfo(buildASTSchema(parse(schemaStr)));
let isOperationDefinition = false;
let isFragment = false;
let visitor = {
OperationDefinition() {
isOperationDefinition = true;
},
InlineFragment() {
isFragment = true;
},
FragmentDefinition() {
isFragment = true;
},
SelectionSet({ selections }) {
if (isOperationDefinition) {
isOperationDefinition = false;
return;
}
let type = getType(typeInfo);
if (type.astNode.kind === "UnionTypeDefinition") {
return;
}
let typeFields = Object.keys(type.getFields());
let typeInterfaces = type.getInterfaces ? type.getInterfaces() : [];
let typeInterfacesFields = typeInterfaces.reduce(
(acc, next) => acc.concat(Object.keys(next.getFields())),
[]
);
for (let field of idFields) {
if (selections.some((_) => _.name && _.name.value === field)) {
continue; // Skip already declared fields
}
let typeHasField = typeFields.some((_) => _ === field);
let typeInterfacesHasField = typeInterfacesFields.some((_) => _ === field);
if (
typeHasField ||
(field === "__typename" && !isFragment) ||
(typeInterfacesHasField && !isFragment)
) {
insertField(selections, field);
}
}
isFragment = false;
},
};
return visit(documentAst, visitWithTypeInfo(typeInfo, visitor));
}
================================================
FILE: packages/babel-plugin/src/sort-query.js
================================================
import { visit } from "graphql";
function sort(array, fn) {
fn = fn || ((obj) => obj.name.value);
return (
array &&
array.sort((prev, next) => {
if (fn(prev) < fn(next)) return -1;
if (fn(prev) > fn(next)) return 1;
return 0;
})
);
}
export default function sortQuery(document) {
return visit(document, {
Document(node) {
node.definitions = [
...sort(node.definitions.filter((def) => def.kind === "FragmentDefinition")),
...node.definitions.filter((def) => def.kind !== "FragmentDefinition"),
];
},
OperationDefinition(node) {
sort(node.directives);
sort(node.variableDefinitions, (_) => _.variable.name.value);
},
SelectionSet(node) {
sort(node.selections, (_) => (_.alias || _.name || _.typeCondition.name).value);
},
Field(node) {
sort(node.directives);
sort(node.arguments);
},
InlineFragment(node) {
sort(node.directives);
},
FragmentSpread(node) {
sort(node.directives);
},
FragmentDefinition(node) {
sort(node.directives);
},
Directive(node) {
sort(node.arguments);
},
});
}
================================================
FILE: packages/bindings/.babelrc
================================================
{
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
"@babel/preset-typescript"
],
"plugins": [["module:@grafoo/babel-plugin", { "schema": "schema.graphql", "idFields": ["id"] }]]
}
================================================
FILE: packages/bindings/.npmignore
================================================
coverage
__tests__
.rpt2_cache
.babelrc
schema.graphql
================================================
FILE: packages/bindings/__tests__/index.ts
================================================
import createBindings from "../src";
import graphql from "@grafoo/core/tag";
import createClient from "@grafoo/core";
import { GrafooClient, GrafooMutations, Variables } from "@grafoo/types";
import { mockQueryRequest } from "@grafoo/test-utils";
import createTransport from "@grafoo/http-transport";
interface Post {
title: string;
content: string;
id: string;
__typename: string;
author: Author;
}
interface Author {
name: string;
id: string;
__typename: string;
posts?: Array<Post>;
}
interface Authors {
authors: Author[];
}
interface CreateAuthor {
createAuthor: {
name: string;
id: string;
__typename: string;
posts?: Array<Post>;
};
}
interface DeleteAuthor {
deleteAuthor: {
name: string;
id: string;
__typename: string;
posts?: Array<Post>;
};
}
interface UpdateAuthor {
updateAuthor: {
name: string;
id: string;
__typename: string;
posts?: Array<Post>;
};
}
let AUTHORS = graphql`
query {
authors {
name
posts {
title
body
}
}
}
`;
let AUTHOR = graphql`
query($id: ID!) {
author(id: $id) {
name
posts {
title
body
}
}
}
`;
let POSTS_AND_AUTHORS = graphql`
query {
posts {
title
body
author {
name
}
}
authors {
name
posts {
title
body
}
}
}
`;
let CREATE_AUTHOR = graphql`
mutation($name: String!) {
createAuthor(name: $name) {
name
posts {
title
body
}
}
}
`;
let DELETE_AUTHOR = graphql`
mutation($id: ID!) {
deleteAuthor(id: $id) {
name
posts {
title
body
}
}
}
`;
let UPDATE_AUTHOR = graphql`
mutation($id: ID!, $name: String) {
updateAuthor(id: $id, name: $name) {
name
posts {
title
body
}
}
}
`;
describe("@grafoo/bindings", () => {
let client: GrafooClient;
beforeEach(() => {
jest.resetAllMocks();
let transport = createTransport("https://some.graphql.api/");
client = createClient(transport, { idFields: ["id"] });
});
it("should be evocable given the minimal props", () => {
let bindings;
expect(() => (bindings = createBindings(client, {}, () => void 0))).not.toThrow();
Object.keys(bindings).forEach((fn) => {
expect(typeof bindings[fn]).toBe("function");
});
expect(bindings.unbind()).toBeUndefined();
});
it("should not provide any data if no query or mutation is given", () => {
let bindings = createBindings(client, {}, () => void 0);
let props = bindings.getState();
expect(props).toEqual({ client });
});
it("should execute a query", async () => {
let { data } = await mockQueryRequest<Authors>(AUTHORS);
let renderFn = jest.fn();
let bindings = createBindings<Authors>(client, { query: AUTHORS }, renderFn);
expect(bindings.getState()).toMatchObject({ loaded: false, loading: true });
await bindings.load();
expect(bindings.getState()).toMatchObject({ ...data, loaded: true, loading: false });
});
it("should notify a loading state", async () => {
let { data } = await mockQueryRequest<Authors>(AUTHORS);
let renderFn = jest.fn();
let bindings = createBindings<Authors>(client, { query: AUTHORS }, renderFn);
await bindings.load();
expect(renderFn).toHaveBeenCalledTimes(1);
expect(bindings.getState()).toMatchObject({ ...data, loaded: true, loading: false });
let reloadPromise = bindings.load();
expect(bindings.getState().loading).toBe(true);
await reloadPromise;
expect(bindings.getState().loading).toBe(false);
});
it("should provide the data if the query is already cached", async () => {
let { data } = await mockQueryRequest<Authors>(AUTHORS);
client.write(AUTHORS, data);
let bindings = createBindings<Authors>(client, { query: AUTHORS }, () => void 0);
expect(bindings.getState()).toMatchObject({ ...data, loaded: true, loading: false });
});
it("should provide the data if a query is partialy cached", async () => {
let { data } = await mockQueryRequest<Authors>(AUTHORS);
client.write(AUTHORS, data);
let bindings = createBindings<Authors>(client, { query: POSTS_AND_AUTHORS }, () => void 0);
expect(bindings.getState()).toMatchObject({ ...data, loaded: false, loading: true });
});
it("should trigger updater function if the cache has been updated", async () => {
let { data } = await mockQueryRequest<Authors>(AUTHORS);
let renderFn = jest.fn();
let bindings = createBindings<Authors>(client, { query: AUTHORS }, renderFn);
client.write(AUTHORS, data);
expect(renderFn).toHaveBeenCalled();
expect(bindings.getState()).toMatchObject(data);
});
it("should provide the state for a cached query", async () => {
let { data } = await mockQueryRequest<Authors>(AUTHORS);
client.write(AUTHORS, data);
let renderFn = jest.fn();
let bindings = createBindings<Authors>(client, { query: AUTHORS }, renderFn);
expect(bindings.getState()).toMatchObject(data);
});
it("should stop updating if unbind has been called", async () => {
let { data } = await mockQueryRequest<Authors>(AUTHORS);
let renderFn = jest.fn();
let bindings = createBindings<Authors>(client, { query: AUTHORS }, renderFn);
await bindings.load();
bindings.unbind();
client.write(AUTHORS, {
authors: data.authors.map((a, i) => (!i ? { ...a, name: "Homer" } : a)),
});
expect(client.read<Authors>(AUTHORS).data.authors[0].name).toBe("Homer");
expect(renderFn).toHaveBeenCalledTimes(1);
expect(bindings.getState()).toMatchObject(data);
});
it("should provide errors on bad request", async () => {
let FailAuthors = { ...AUTHORS, query: AUTHORS.query.substr(1) };
let { errors } = await mockQueryRequest(FailAuthors);
let renderFn = jest.fn();
let bindings = createBindings(client, { query: FailAuthors }, renderFn);
await bindings.load();
expect(renderFn).toHaveBeenCalledTimes(1);
expect(bindings.getState()).toMatchObject({ errors });
});
it("should perform a simple mutation", async () => {
interface Mutations {
createAuthor: CreateAuthor;
}
let mutations: GrafooMutations<Author, Mutations> = { createAuthor: { query: CREATE_AUTHOR } };
let bindings = createBindings(client, { mutations }, () => void 0);
let props = bindings.getState();
let variables = { name: "Bart" };
let { data } = await mockQueryRequest({ query: CREATE_AUTHOR.query, variables });
let { data: mutationData } = await props.createAuthor(variables);
expect(mutationData).toEqual(data);
});
it("should perform mutation with a cache update", async () => {
await mockQueryRequest<Authors>(AUTHORS);
interface Mutations {
createAuthor: CreateAuthor;
}
let mutations: GrafooMutations<Authors, Mutations> = {
createAuthor: {
query: CREATE_AUTHOR,
update: ({ authors }, data) => ({
authors: [data.createAuthor, ...authors],
}),
},
};
let update = jest.spyOn(mutations.createAuthor, "update");
let bindings = createBindings(client, { query: AUTHORS, mutations }, () => void 0);
let props = bindings.getState();
expect(typeof props.createAuthor).toBe("function");
await bindings.load();
let variables = { name: "Homer" };
let { data } = await mockQueryRequest<CreateAuthor>({ query: CREATE_AUTHOR.query, variables });
let { authors } = bindings.getState();
await props.createAuthor(variables);
expect(update).toHaveBeenCalledWith({ authors }, data);
});
it("should perform optimistic update", async () => {
await mockQueryRequest(AUTHORS);
interface Mutations {
createAuthor: CreateAuthor;
}
let mutations: GrafooMutations<Authors, Mutations> = {
createAuthor: {
query: CREATE_AUTHOR,
optimisticUpdate: ({ authors }, variables: Author) => ({
authors: [{ ...variables, id: "tempID" }, ...authors],
}),
update: ({ authors }, data) => ({
authors: authors.map((p) => (p.id === "tempID" ? data.createAuthor : p)),
}),
},
};
let optimisticUpdate = jest.spyOn(mutations.createAuthor, "optimisticUpdate");
let update = jest.spyOn(mutations.createAuthor, "update");
let bindings = createBindings(client, { query: AUTHORS, mutations }, () => void 0);
let props = bindings.getState();
expect(typeof props.createAuthor).toBe("function");
await bindings.load();
let variables = { name: "Peter" };
let { data } = await mockQueryRequest<CreateAuthor>({ query: CREATE_AUTHOR.query, variables });
let { authors } = bindings.getState();
let createAuthorPromise = props.createAuthor(variables);
expect(optimisticUpdate).toHaveBeenCalledWith({ authors }, variables);
let { authors: modifiedAuthors } = bindings.getState();
await createAuthorPromise;
expect(update).toHaveBeenCalledWith({ authors: modifiedAuthors }, data);
});
it("should update if query objects has less keys then nextObjects", async () => {
let { query } = CREATE_AUTHOR;
let author = (await mockQueryRequest<CreateAuthor>({ query, variables: { name: "gustav" } }))
.data.createAuthor;
let { data } = await mockQueryRequest(AUTHORS);
client.write(AUTHORS, data);
interface Mutations {
removeAuthor: DeleteAuthor;
}
let mutations: GrafooMutations<Authors, Mutations> = {
removeAuthor: {
query: DELETE_AUTHOR,
optimisticUpdate: ({ authors }, { id }: Author) => ({
authors: authors.filter((author) => author.id !== id),
}),
},
};
let renderFn = jest.fn();
let bindings = createBindings(client, { query: AUTHORS, mutations }, renderFn);
let { removeAuthor } = bindings.getState();
let variables = { id: author.id };
await removeAuthor(variables);
expect(renderFn).toHaveBeenCalled();
});
it("should update if query objects is modified", async () => {
let { query } = CREATE_AUTHOR;
let author = (
await mockQueryRequest<CreateAuthor>({
query,
variables: { name: "sven" },
})
).data.createAuthor;
let { data } = await mockQueryRequest(AUTHORS);
client.write(AUTHORS, data);
interface Mutations {
updateAuthor: UpdateAuthor;
}
let mutations: GrafooMutations<Authors, Mutations> = {
updateAuthor: {
query: UPDATE_AUTHOR,
optimisticUpdate: ({ authors }, variables: Author) => ({
authors: authors.map((author) => (author.id === variables.id ? variables : author)),
}),
},
};
let renderFn = jest.fn();
let bindings = createBindings(client, { query: AUTHORS, mutations }, renderFn);
let { updateAuthor } = bindings.getState();
let variables = { ...author, name: "johan" };
await mockQueryRequest({ query: UPDATE_AUTHOR.query, variables });
await updateAuthor(variables);
expect(renderFn).toHaveBeenCalled();
});
it("should not update if query objects is not modified", async () => {
let { data } = await mockQueryRequest(AUTHORS);
client.write(AUTHORS, data);
let renderFn = jest.fn();
createBindings(client, { query: AUTHORS }, renderFn);
client.write(AUTHORS, data);
expect(renderFn).not.toHaveBeenCalled();
});
it("should accept multiple mutations", async () => {
let { data } = await mockQueryRequest(AUTHORS);
client.write(AUTHORS, data);
interface Mutations {
createAuthor: CreateAuthor;
updateAuthor: UpdateAuthor;
deleteAuthor: DeleteAuthor;
}
let mutations: GrafooMutations<Authors, Mutations> = {
createAuthor: {
query: CREATE_AUTHOR,
optimisticUpdate: ({ authors }, variables: Author) => ({
authors: [{ ...variables, id: "tempID" }, ...authors],
}),
update: ({ authors }, data: CreateAuthor) => ({
authors: authors.map((author) => (author.id === "tempID" ? data.createAuthor : author)),
}),
},
updateAuthor: {
query: UPDATE_AUTHOR,
optimisticUpdate: ({ authors }, variables: Author) => ({
authors: authors.map((author) => (author.id === variables.id ? variables : author)),
}),
},
deleteAuthor: {
query: DELETE_AUTHOR,
optimisticUpdate: ({ authors }, variables: Author) => ({
authors: authors.map((author) => (author.id === variables.id ? variables : author)),
}),
},
};
let renderFn = jest.fn();
let bindings = createBindings(client, { query: AUTHORS, mutations }, renderFn);
let props = bindings.getState();
try {
let variables: Variables = { name: "mikel" };
let { data } = await mockQueryRequest<CreateAuthor>({
query: CREATE_AUTHOR.query,
variables,
});
expect(await mockQueryRequest({ query: CREATE_AUTHOR.query, variables })).toEqual(
await props.createAuthor(variables)
);
variables = { ...data.createAuthor, name: "miguel" };
expect(await mockQueryRequest({ query: UPDATE_AUTHOR.query, variables })).toEqual(
await props.updateAuthor(variables)
);
variables = data.createAuthor;
expect(await mockQueryRequest({ query: DELETE_AUTHOR.query, variables })).toEqual(
await props.deleteAuthor(data.createAuthor)
);
} catch (err) {
console.error(err);
}
});
it("should update variables when new variables are passed", async () => {
let {
data: { authors },
} = await mockQueryRequest<Authors>(AUTHORS);
let [author1, author2] = authors;
let author1Variables = { id: author1.id };
let author2Variables = { id: author2.id };
let bindings = createBindings<{ author: Author }>(
client,
{ query: AUTHOR, variables: author1Variables },
() => {}
);
await mockQueryRequest({ query: AUTHOR.query, variables: author1Variables });
await bindings.load();
expect(bindings.getState().author).toMatchObject(author1);
expect(client.read<{ author: Author }>(AUTHOR, author1Variables).data.author).toEqual(author1);
await mockQueryRequest({ query: AUTHOR.query, variables: author2Variables });
await bindings.load(author2Variables);
expect(bindings.getState().author).toMatchObject(author2);
expect(client.read<{ author: Author }>(AUTHOR, author2Variables).data.author).toEqual(author2);
});
});
================================================
FILE: packages/bindings/__tests__/tsconfig.json
================================================
{
"extends": "../tsconfig",
"include": ["."]
}
================================================
FILE: packages/bindings/package.json
================================================
{
"name": "@grafoo/bindings",
"version": "1.4.2",
"description": "grafoo client internal helper for building framework bindings",
"repository": "https://github.com/grafoojs/grafoo/tree/master/packages/bindings",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "malbernaz<albernazmiguel@gmail.com>",
"license": "MIT",
"keywords": [
"babel",
"babel-plugin",
"graphql",
"graphql-client",
"grafoo"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "grafoo-bundle --input src/index.ts",
"test": "jest",
"test:coverage": "jest --coverage"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx|js)$": "<rootDir>/../../scripts/jest-setup.js"
},
"resolver": "<rootDir>/../../scripts/resolver.js",
"transformIgnorePatterns": [
"node_modules/(?!(lowdb|steno|node-fetch|fetch-blob)/)"
]
},
"dependencies": {
"@grafoo/types": "^1.4.2"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974"
}
================================================
FILE: packages/bindings/readme.md
================================================
# `@grafoo/bindings`
<p><i>Grafoo Bindings for Frameworks</i></p>
<p>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://www.npmjs.com/package/@grafoo/bindings>
<img
src=https://img.shields.io/npm/v/@grafoo/bindings.svg
alt=npm
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/bindings>
<img
src=https://img.shields.io/npm/dm/@grafoo/bindings.svg
alt=downloads
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
This packages purpose is to standardize how view layer integrations are implemented for Grafoo. `@grafoo/bindings` has only a default export that is a `createBindings` factory function that returns an interface that provides data and notify for changes.
## API
### Arguments
| Argument | type | Description |
| -------- | ------------ | ----------------------------------------------------- |
| client | GrafooClient | a client nstance |
| props | object | a props object passed by the user (description below) |
| updater | function | a callback to notify for data changes |
#### Example
```js
import createBindings from "@grafoo/bindings";
import createClient from "@grafoo/core";
function fetchQuery(query, variables) {
const init = {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json"
}
};
return fetch("http://some.graphql.api", init).then(res => res.json());
}
const client = createClient(fetchQuery);
const props = {};
const updater = () => {};
const bindings = createBindings(client, props, updater);
```
### `props` argument
| Name | type | Descrition |
| --------- | ------- | ---------------------------------------------------------- |
| query | object | the query created with `@grafoo/core/tag`'s template tag |
| variables | object | GraphQL variables object for the query |
| mutations | object | an object where mutations are declared (description below) |
| skip | boolean | whether the client should skip the query request |
### Mutations
The `mutations` prop is a map of _mutation objects_ that are shaped like so:
```js
const createPost = {
query: CREATE_POST_MUTATION,
optimisticUpdate: ({ allPosts }, variables) => ({
allPosts: [{ ...variables.postInput, id: "tempID" }, ...allPosts]
}),
update: ({ allPosts }, response) => ({
allPosts: allPosts.map(p => (p.id === "tempID" ? response.post : p))
})
};
const mutations = { createPost };
```
A mutation object receives the following props:
| Name | Type | Required | Descrition |
| ---------------- | -------- | -------- | ------------------------------------------------------------------- |
| query | object | true | a mutation query created with `@grafoo/core/tag` |
| update | function | false | updates the cache when a request is completed (description below) |
| optimisticUpdate | function | false | updates the cache before a request is completed (description below) |
Each mutation will generate a single function that accepts a GraphQL variables object as argument and return a promise that will resolve with the mutation data or reject with GraphQL `errors`.
```ts
type MutationFn = (variables: Variables) => Promise<MutationData>;
```
### Mutation query dependency
**Important** to notice that to update the cache `update` and `optimistUpdate` hooks depend on a `query` and it's `variables` object props (they need to be passed in the `props` object argument). If you need to perform a mutation but updating the cache is not strictly important you can just use the mutation promise resolved data or use the client instance directly.
### `update`
```ts
type UpdateFn = (query: QueryData, data: MutationData) => CacheUpdate;
```
The mutation `update` function is resposible to update the cache when the request is completed. It receives as paremeters an object containing the data from the query it depends upon and the mutation result. `update` return type is an object that describes the changes to be made to the cache.
### `optimisticUpdate`
```ts
type OptimistcUpdateFn = (query: QueryData, variables: Variables) => CacheUpdate;
```
In modern UIs it's to be expected that every user interaction occur in a fraction of seconds. `optimisticUpdate` responsability is to skip the mutation network roundtrip and update the cache instantaneously, making sure such interactions are as fast as they can be. `optimisticUpdate` as in `update` takes as first paremater the depedent query data. As second paremater it receives the variables object with which it's correpondent generated mutation function was called. And again it should return an object that describes the changes to be made to cache.
If you want to perform an optimitic update you have to make sure that the data you are inserting contains the field or fields to extract a unique identifier. For instance, say `@grafoo/babel-plugin` `idFields` option is set to insert a property `id`. Is to be expected that your update has that field mocked.
### Bindings
The object returned by `createBindings` contains the following props.
| Name | type | Descrition |
| ------- | -------- | ------------------------------------------------------------ |
| client | object | the client instance |
| load | function | a method to execute a query with the `query` prop |
| loading | boolean | whether the client is executing a query or not |
| loaded | boolean | whether the query data is already cached |
| errors | string[] | an array of GraphQL errors from a failed request to your API |
The remaining props are:
- the data fetched by the client and shaped according to your `query`
- mutation functions generated by the `mutations` object prop
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: packages/bindings/schema.graphql
================================================
type Query {
author(id: ID!): Author!
authors: [Author!]!
post(id: ID!): Post!
posts: [Post!]!
}
type Mutation {
createAuthor(name: String!): Author!
updateAuthor(id: ID!, name: String): Author!
deleteAuthor(id: ID!): Author!
createPost(title: String!, body: String!, author: ID!): Post!
updatePost(id: ID!, title: String, body: String): Post!
deletePost(id: ID!): Post!
}
type Author {
id: ID!
name: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String!
author: Author!
}
================================================
FILE: packages/bindings/src/index.ts
================================================
import {
GrafooClient,
GrafooBindings,
GrafooBoundMutations,
GrafooConsumerProps,
ObjectsMap,
Variables,
} from "@grafoo/types";
export default function createBindings<T = unknown, U = unknown>(
client: GrafooClient,
props: GrafooConsumerProps<T, U>,
updater: () => void
): GrafooBindings<T, U> {
let { variables } = props;
let data: T;
let objects: ObjectsMap;
let boundMutations = {} as GrafooBoundMutations<U>;
let unbind = () => {};
let lockListenUpdate = 0;
let loaded = false;
let partial = false;
if (props.query) {
({ data, objects, partial } = client.read<T>(props.query, variables));
loaded = !!data && !partial;
unbind = client.listen((nextObjects) => {
if (lockListenUpdate) return (lockListenUpdate = 0);
objects = objects || {};
for (let i in nextObjects) {
// object has been inserted
if (!(i in objects)) return performUpdate();
for (let j in nextObjects[i]) {
// object has been updated
if (nextObjects[i][j] !== objects[i][j]) return performUpdate();
}
}
for (let i in objects) {
// object has been removed
if (!(i in nextObjects)) return performUpdate();
}
});
}
let boundState = props.query ? { load, loaded, loading: !props.skip && !loaded } : {};
if (props.mutations) {
for (let key in props.mutations) {
let { update, optimisticUpdate, query: mutationQuery } = props.mutations[key];
boundMutations[key] = (mutationVariables) => {
if (props.query && optimisticUpdate) {
writeToCache(optimisticUpdate(data, mutationVariables));
}
return client
.execute<U[typeof key]>(mutationQuery, mutationVariables)
.then((mutationResponse) => {
if (props.query && update && mutationResponse.data) {
writeToCache(update(data, mutationResponse.data));
}
return mutationResponse;
});
};
}
}
function writeToCache(dataUpdate: T) {
client.write(props.query, variables, dataUpdate);
}
function performUpdate(boundStateUpdate?) {
({ data, objects } = client.read<T>(props.query, variables));
Object.assign(boundState, boundStateUpdate);
updater();
}
function getState() {
return Object.assign({ client }, boundState, boundMutations, data);
}
function load(nextVariables?: Variables) {
if (nextVariables) {
variables = nextVariables;
}
if (!boundState.loading) {
Object.assign(boundState, { loading: true });
updater();
}
return client.execute<T>(props.query, variables).then(({ data, errors }) => {
if (data) {
lockListenUpdate = 1;
writeToCache(data);
}
performUpdate({ errors, loaded: !!data, loading: false });
});
}
return { getState, unbind, load };
}
================================================
FILE: packages/bindings/tsconfig.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"strict": false,
"lib": ["esnext", "dom"],
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": false,
"downlevelIteration": true
},
"include": ["src"]
}
================================================
FILE: packages/bundle/cli.js
================================================
#!/usr/bin/env node
/* eslint-disable no-console */
var mri = require("mri");
var build = require(".");
var opts = mri(process.argv.slice(2));
opts.skipCompression = !!opts["skip-compression"];
opts.rootPath = process.cwd();
build(opts).catch(console.error);
================================================
FILE: packages/bundle/index.js
================================================
var fs = require("fs");
var path = require("path");
var rollup = require("rollup").rollup;
var buble = require("rollup-plugin-buble");
var fileSize = require("rollup-plugin-filesize");
var nodeResolve = require("rollup-plugin-node-resolve");
var terser = require("rollup-plugin-terser").terser;
var typescript = require("rollup-plugin-typescript2");
var ts = require("typescript");
module.exports = function build(opts) {
var pkg = JSON.parse(fs.readFileSync(path.join(opts.rootPath, "package.json"), "utf-8"));
var tsconfig = JSON.parse(fs.readFileSync(path.join(opts.rootPath, "tsconfig.json"), "utf-8"));
var peerDependencies = pkg.peerDependencies || {};
tsconfig.compilerOptions.target = "esnext";
tsconfig.compilerOptions.module = "esnext";
tsconfig.compilerOptions.declaration = true;
tsconfig.compilerOptions.outDir = path.join(opts.rootPath, "dist");
return rollup({
input: path.join(opts.rootPath, opts.input),
external: Object.keys(peerDependencies),
sourcemap: true,
plugins: [
nodeResolve(),
typescript({
typescript: ts,
tsconfigOverride: tsconfig,
}),
buble({
transforms: {
dangerousForOf: true,
dangerousTaggedTemplateString: true,
},
}),
!opts.skipCompression &&
terser({
output: { comments: false },
compress: { keep_infinity: true, pure_getters: true },
warnings: true,
toplevel: true,
mangle: {},
}),
fileSize(),
].filter(Boolean),
}).then(function (bundle) {
return bundle.write({
file: path.join(opts.rootPath, "dist/index.js"),
sourcemap: true,
format: opts.format || "esm",
treeshake: {
propertyReadSideEffects: false,
},
});
});
};
================================================
FILE: packages/bundle/package.json
================================================
{
"name": "grafoo-bundle",
"version": "1.4.2",
"bin": "cli.js",
"main": "index.js",
"dependencies": {
"mri": "^1.1.1",
"rollup": "^2.34.2",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-filesize": "^9.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0",
"typescript": "^4.1.2"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974"
}
================================================
FILE: packages/bundle/readme.md
================================================
# `grafoo-bundle`
**This is and internal cli tool for [Grafoo](https://github.com/grafoojs/grafoo) and it's not meant to be used for anything else**. Basicaly a wrapper around rollup with some configuration already set.
## Usage
```
$ grafoo-bundle --input src/index.ts
```
## Options
```sh
--input # the entrypoint
--skip-compression # avoids minification
```
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: packages/core/.babelrc
================================================
{
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
"@babel/preset-typescript"
],
"plugins": [
[
"module:@grafoo/babel-plugin",
{ "schema": "schema.graphql", "idFields": ["id", "__typename"] }
]
]
}
================================================
FILE: packages/core/.npmignore
================================================
coverage
__tests__
.rpt2_cache
.babelrc
schema.graphql
================================================
FILE: packages/core/__tests__/build-query-tree.ts
================================================
import buildQueryTree from "../src/build-query-tree";
let tree = {
posts: [
{
title: "foo",
id: "1",
author: {
name: "miguel",
id: "2",
posts: [
{
id: "1",
content: "a post content",
author: {
name: "miguel",
lastName: "albernaz",
id: "2",
},
},
],
},
},
{ title: "bar", id: "3", author: { name: "vicente", id: "4" } },
{ title: "baz", id: "5", author: { name: "laura", id: "6" } },
],
};
let idFields = ["id"];
describe("build-query-tree", () => {
it("should update values of a resulting query tree", () => {
let objects = {
"1": { title: "foobar", id: "1", content: "a new post content" },
"2": { name: "miguel", id: "2", lastName: "coelho" },
};
let { posts } = buildQueryTree(tree, objects, idFields);
expect(posts[0].title).toBe("foobar");
expect(posts[0].content).toBe("a new post content");
expect(posts[0].author.lastName).toBe("coelho");
});
it("should add all properties of an object to its corresponding branch", () => {
let objects = {
"1": { title: "foo", id: "1", content: "a post content" },
"2": { name: "miguel", id: "2", lastName: "coelho" },
};
let [post] = buildQueryTree(tree, objects, idFields).posts;
expect(post.content).toBeTruthy();
expect(post.author.lastName).toBeTruthy();
expect(post.author.posts[0].title).toBeTruthy();
});
it("should not remove a property from a branch", () => {
let objects = {
"1": { id: "1" },
"2": { id: "2" },
"3": { id: "3" },
"4": { id: "4" },
"5": { id: "5" },
"6": { id: "6" },
};
let newTree = buildQueryTree(tree, objects, idFields);
expect(newTree).toEqual(tree);
});
});
================================================
FILE: packages/core/__tests__/index.ts
================================================
import graphql from "@grafoo/core/tag";
import { executeQuery } from "@grafoo/test-utils";
import { GrafooClient, Variables } from "@grafoo/types";
import createClient from "../src";
interface Post {
title: string;
content: string;
id: string;
__typename: string;
author: Author;
}
interface Author {
name: string;
id: string;
__typename: string;
posts?: Array<Post>;
}
interface AuthorsQuery {
authors: Author[];
}
interface PostQuery {
post: Post;
}
interface PostsAndAuthorsQuery {
authors: Author[];
posts: Post[];
}
interface PostsQuery {
posts: Post[];
}
let AUTHORS = graphql`
query {
authors {
name
posts {
title
body
}
}
}
`;
let SIMPLE_AUTHORS = graphql`
query {
authors {
name
}
}
`;
let POSTS_AND_AUTHORS = graphql`
query {
posts {
title
body
author {
name
}
}
authors {
name
posts {
title
body
}
}
}
`;
let POST = graphql`
query ($postId: ID!) {
post(id: $postId) {
title
body
author {
name
}
}
}
`;
let POST_WITH_FRAGMENT = graphql`
query ($postId: ID!) {
post(id: $postId) {
title
body
author {
...AuthorInfo
}
}
}
fragment AuthorInfo on Author {
name
}
`;
let POSTS = graphql`
query {
posts {
title
body
author {
name
}
}
}
`;
function mockTrasport<T>(query: string, variables: Variables) {
return executeQuery<T>({ query, variables });
}
describe("@grafoo/core", () => {
let client: GrafooClient;
beforeEach(() => {
client = createClient(mockTrasport, { idFields: ["id"] });
});
it("should be instantiable", () => {
expect(() => createClient(mockTrasport, { idFields: ["id"] })).not.toThrow();
expect(typeof client.execute).toBe("function");
expect(typeof client.listen).toBe("function");
expect(typeof client.write).toBe("function");
expect(typeof client.read).toBe("function");
expect(typeof client.flush).toBe("function");
expect(typeof client.reset).toBe("function");
});
it("should perform query requests", async () => {
let variables = { postId: "2c969ce7-02ae-42b1-a94d-7d0a38804c85" };
let { query, frags } = POST_WITH_FRAGMENT;
if (frags) for (let frag in frags) query += " " + frags[frag];
let data = await executeQuery({ query, variables });
expect(data).toEqual(await client.execute(POST_WITH_FRAGMENT, variables));
});
it("should perform query requests with fragments", async () => {
let data = await executeQuery({ query: SIMPLE_AUTHORS.query });
expect(data).toEqual(await client.execute(SIMPLE_AUTHORS));
});
it("should write queries to the client", async () => {
let data = await executeQuery<PostsAndAuthorsQuery>(POSTS_AND_AUTHORS);
client.write(POSTS_AND_AUTHORS, data);
let { authors, posts } = data.data;
let { objectsMap, pathsMap } = client.flush();
expect(authors).toEqual(
pathsMap["authors{__typename id name posts{__typename body id title}}"].data.authors
);
expect(posts).toEqual(
pathsMap["posts{__typename author{__typename id name}body id title}"].data.posts
);
expect(authors.every((author) => Boolean(objectsMap[author.id]))).toBe(true);
expect(posts.every((post) => Boolean(objectsMap[post.id]))).toBe(true);
});
it("should write queries partially to the client", async () => {
let { data } = await executeQuery<PostsQuery>(POSTS);
expect(() => client.write(POSTS_AND_AUTHORS, data)).not.toThrow();
expect(() => client.read(POSTS)).not.toThrow();
expect(() => client.read(AUTHORS)).not.toThrow();
});
it("should read queries from the client", async () => {
let { data } = await executeQuery<AuthorsQuery>(AUTHORS);
client.write(AUTHORS, data);
let result = client.read<AuthorsQuery>(AUTHORS);
let { authors } = data;
expect(authors).toEqual(result.data.authors);
expect(authors.every((author) => Boolean(result.objects[author.id]))).toBe(true);
expect(
authors.every((author) => author.posts.every((post) => Boolean(result.objects[post.id])))
).toBe(true);
});
it("should handle queries with variables", async () => {
let variables = { postId: "2c969ce7-02ae-42b1-a94d-7d0a38804c85" };
let { data } = await executeQuery<PostQuery>({ query: POST.query, variables });
client.write(POST, variables, data);
expect(client.read(POST, { postId: "123" })).toEqual({});
expect(client.read<PostQuery>(POST, variables).data.post.id).toBe(variables.postId);
});
it("should distinguish between calls to the same query with different variables", async () => {
let v1 = { postId: "2c969ce7-02ae-42b1-a94d-7d0a38804c85" };
let v2 = { postId: "77c483dd-6529-4c72-9bb6-bbfd69f65682" };
let { data: d1 } = await executeQuery<PostQuery>({ query: POST.query, variables: v1 });
client.write(POST, v1, d1);
expect(client.read(POST, { postId: "not found" })).toEqual({});
expect(client.read<PostQuery>(POST, v1).data.post.id).toBe(v1.postId);
let d2 = await executeQuery<PostQuery>({ query: POST.query, variables: v2 });
client.write(POST, v2, d2);
expect(client.read<PostQuery>(POST, v1).data.post.id).toBe(v1.postId);
expect(client.read<PostQuery>(POST, v2).data.post.id).toBe(v2.postId);
});
it("should flag if a query result is partial", async () => {
let { data } = await executeQuery<PostsQuery>({ query: POSTS.query });
client.write(POSTS, data);
expect(client.read<PostsAndAuthorsQuery>(POSTS_AND_AUTHORS).partial).toBe(true);
});
it("should remove unused objects from objectsMap", async () => {
let { data } = await executeQuery<AuthorsQuery>(SIMPLE_AUTHORS);
client.write(SIMPLE_AUTHORS, data);
let authorToBeRemoved: Author = data.authors[0];
let ids = Object.keys(client.flush().objectsMap);
expect(ids.some((id) => id === authorToBeRemoved.id)).toBe(true);
client.write(SIMPLE_AUTHORS, {
authors: data.authors.filter((author) => author.id !== authorToBeRemoved.id)
});
let nextIds = Object.keys(client.flush().objectsMap);
expect(nextIds.length).toBe(ids.length - 1);
expect(nextIds.some((id) => id === authorToBeRemoved.id)).toBe(false);
});
it("should perform update to client", async () => {
let variables = { postId: "2c969ce7-02ae-42b1-a94d-7d0a38804c85" };
let { data } = await executeQuery<PostQuery>({ query: POST.query, variables });
client.write(POST, variables, data);
let {
data: { post }
} = client.read<PostQuery>(POST, variables);
expect(post.title).toBe("Quam odit");
client.write(POST, variables, { post: { ...post, title: "updated title" } });
expect(client.read<PostQuery>(POST, variables).data.post.title).toBe("updated title");
});
it("should reflect updates on queries with shared objects", async () => {
let variables = { postId: "2c969ce7-02ae-42b1-a94d-7d0a38804c85" };
let postData = (await executeQuery<PostQuery>({ query: POST.query, variables })).data;
let postsData = (await executeQuery<PostsQuery>({ query: POSTS.query, variables })).data;
client.write(POSTS, postsData);
let { posts } = client.read<PostsQuery>(POSTS).data;
expect(posts.find((p) => p.id === variables.postId).title).toBe("Quam odit");
client.write(POST, variables, { post: { ...postData.post, title: "updated title" } });
let { posts: updatedPosts } = client.read<PostsQuery>(POSTS, variables).data;
expect(updatedPosts.find((p) => p.id === variables.postId).title).toBe("updated title");
});
it("should merge objects in the client when removing or adding properties", async () => {
let variables = { postId: "2c969ce7-02ae-42b1-a94d-7d0a38804c85" };
let data = (await executeQuery<PostQuery>({ query: POST.query, variables })).data;
client.write(POST, variables, data);
let post = JSON.parse(JSON.stringify(client.read<PostQuery>(POST, variables).data.post));
delete post.__typename;
post.foo = "bar";
client.write(POST, variables, { post });
expect(client.read<PostQuery>(POST, variables).data.post).toEqual({
__typename: "Post",
author: {
__typename: "Author",
id: "a1d3a2bc-e503-4640-9178-23cbd36b542c",
name: "Murphy Abshire"
},
body: "Ducimus harum delectus consectetur.",
id: "2c969ce7-02ae-42b1-a94d-7d0a38804c85",
title: "Quam odit",
foo: "bar"
});
});
it("should call client listeners on write with paths objects as arguments", async () => {
let variables = { postId: "2c969ce7-02ae-42b1-a94d-7d0a38804c85" };
let data = (await executeQuery<PostQuery>({ query: POST.query, variables })).data;
let listener = jest.fn();
let listener2 = jest.fn();
let unlisten = client.listen(listener);
client.listen(listener2);
client.write(POST, variables, data);
expect(listener).toHaveBeenCalledWith(client.read(POST, variables).objects);
unlisten();
client.write(POST, variables, data);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener2).toHaveBeenCalledTimes(2);
unlisten();
client.write(POST, variables, data);
expect(listener2).toHaveBeenCalledTimes(3);
});
it("should be able read from the client with a declared initialState", async () => {
let { data } = await executeQuery(POSTS_AND_AUTHORS);
client.write(POSTS_AND_AUTHORS, data);
client = createClient(mockTrasport, { idFields: ["id"], initialState: client.flush() });
expect(client.read(POSTS_AND_AUTHORS).data).toEqual(data);
});
it("should allow cache to be cleared using reset()", () => {
let data = { authors: [{ name: "deleteme" }] };
client.write(SIMPLE_AUTHORS, data);
expect(client.read(SIMPLE_AUTHORS).data).toEqual(data);
client.reset();
expect(client.read(SIMPLE_AUTHORS).data).toEqual(undefined);
expect(client.flush()).toEqual({
objectsMap: {},
pathsMap: {}
});
});
it("should accept `idFields` array in options", async () => {
let { data } = await executeQuery(AUTHORS);
let client = createClient(mockTrasport, { idFields: ["__typename", "id"] });
client.write(AUTHORS, data);
let cachedIds = Object.keys(client.flush().objectsMap);
expect(cachedIds.every((key) => /(Post|Author)/.test(key))).toBe(true);
});
});
================================================
FILE: packages/core/__tests__/map-objects.ts
================================================
import mapObjects from "../src/map-objects";
let tree = {
posts: [
{
title: "foo",
id: "1",
__typename: "Post",
author: {
name: "miguel",
id: "2",
__typename: "Author",
posts: [
{
title: "foo",
id: "1",
__typename: "Post",
content: "a post content",
author: {
name: "miguel",
lastName: "albernaz",
id: "2",
__typename: "Author",
},
},
],
},
},
{
title: "bar",
id: "3",
__typename: "Post",
author: { name: "vicente", id: "4", __typename: "Author" },
},
{
title: "baz",
id: "5",
__typename: "Post",
author: { name: "laura", id: "6", __typename: "Author" },
},
],
};
let idFields = ["id"];
describe("map-objects", () => {
it("should return the correct map of objects", () => {
let objects = mapObjects(tree, idFields);
let expected = {
"1": { title: "foo", id: "1", __typename: "Post", content: "a post content" },
"2": { name: "miguel", id: "2", __typename: "Author", lastName: "albernaz" },
"3": { title: "bar", __typename: "Post", id: "3" },
"4": { name: "vicente", id: "4", __typename: "Author" },
"5": { title: "baz", __typename: "Post", id: "5" },
"6": { name: "laura", id: "6", __typename: "Author" },
};
expect(objects).toEqual(expected);
});
it("should accept null values", () => {
let result = {
data: {
me: {
id: "5a3ab7e93f662a108d978a6e",
username: "malbernaz",
email: "albernazmiguel@gmail.com",
name: null,
bio: null,
},
},
};
expect(() => mapObjects(result, idFields)).not.toThrow();
});
it("should build an object identifier based on the `idFields` cache option", () => {
let idFields = ["__typename", "id"];
let objects = mapObjects(tree, idFields);
let expected = ["Post1", "Author2", "Post3", "Author4", "Post5", "Author6"];
expect(Object.keys(objects).every((obj) => expected.some((exp) => exp === obj))).toBe(true);
});
});
================================================
FILE: packages/core/__tests__/tsconfig.json
================================================
{
"extends": "../tsconfig",
"include": ["."]
}
================================================
FILE: packages/core/package.json
================================================
{
"name": "@grafoo/core",
"version": "1.4.2",
"description": "grafoo client core",
"repository": "https://github.com/grafoojs/grafoo/tree/master/packages/core",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "malbernaz<albernazmiguel@gmail.com>",
"license": "MIT",
"keywords": [
"babel",
"babel-plugin",
"graphql",
"graphql-client",
"grafoo"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "grafoo-bundle --input src/index.ts",
"test": "jest",
"test:coverage": "jest --coverage"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx|js)$": "<rootDir>/../../scripts/jest-setup.js"
},
"resolver": "<rootDir>/../../scripts/resolver.js",
"transformIgnorePatterns": [
"node_modules/(?!(lowdb|steno|node-fetch|fetch-blob)/)"
]
},
"dependencies": {
"@grafoo/types": "^1.4.2"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974"
}
================================================
FILE: packages/core/readme.md
================================================
# `@grafoo/core`
<p><i>Grafoo core</i></p>
<p>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://github.com/grafoojs/grafoo>
<img
src=https://img.shields.io/npm/v/@grafoo/core.svg
alt=npm
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/core>
<img
src=https://img.shields.io/npm/dm/@grafoo/core.svg
alt=downloads
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/core>
<img
src=https://img.shields.io/bundlephobia/minzip/@grafoo/core.svg?label=size
alt=size
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
## Install
```
$ npm i @grafoo/core && npm i -D @grafoo/babel-plugin
```
## Setup
Assuming you already have babel installed, the only additional step required to build an application with Grafoo is to configure [`@grafoo/babel-plugin`](https://github.com/grafoojs/grafoo/tree/master/packages/babel-plugin). The options it accepts are `idFields` - the fields Grafoo will take to build unique identifiers, and `schema`, which is a relative path to your schema file.
```json
{
"plugins": [
[
"@grafoo/babel-plugin",
{
"schema": "schema.graphql",
"idFields": ["id"]
}
]
]
}
```
## API
`@grafoo/core` consists of a module that exports as default function a factory to create the client intance and a submodule that exports that `graphql` template tag.
### `graphql` template tag
From `@grafoo/core/tag` is exported the `graphql` or `gql` tag that you'll use to create your queries. On build time every time you use that tag it will be replace with a special object that assists the client on the caching process. It is a dummy module and if you do not have `@grafoo/babel-plugin` it will thow you an error.
#### Example
```js
import gql from "@grafoo/core/tag";
const USER_QUERY = gql`
query($id: ID!) {
user(id: $id) {
name
}
}
`;
// will be transformed to this on build time
const USER_QUERY = {
query: "query($id: ID!) { user(id: $id) { name id } }"
paths: {
"user(id:$id){name id}": {
name: "user",
args: ["id"]
}
}
}
```
### `createClient` factory
`createClient` accepts as arguments a `transport` function to comunicate with your GraphQL API and an options object. This options are:
| Option | Type | Required | Description |
| ------------ | -------- | -------- | ------------------------------------------------------------------------------------- |
| idFields | string[] | false | fields Grafoo takes to build unique identifiers |
| initialState | object | false | a initial state to hydrate the cache. It can be produced by the `flush` client method |
#### Example
```js
import createClient from "@grafoo/core";
function fetchQuery(query, variables) {
const init = {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json"
}
};
return fetch("http://some.graphql.api", init).then(res => res.json());
}
const client = createClient(fetchQuery);
```
### IdFields
`IdFields` is homologous to the `@grafoo/babel-plugin` option with the same name. You don't have much to worry about it because it's **automatically inserted by `@grafoo/babel-plugin`** on every client instantiation. It is an array of fields that Grafoo will take to build unique identifiers.
Say you want to consume a query like so:
```graphql
{
me {
name
}
}
```
If `idFields` is configured with `["id"]`. This query will be transformed to this:
```graphql
{
me {
name
id
}
}
```
Then the client, when caching this data, will use this `id` field to store it.
#### Example
```js
const client = createClient(fetchQuery, {
idFields: ["id", "__typename"]
});
```
## `GrafooClient`
the `createClient` factory returns a client instance with some methods:
| Name | Description |
| ------- | ------------------------------------------------------ |
| execute | executes queries |
| read | reads queries from the cache |
| write | writes queries to the cache |
| listen | takes a listener callback and notify for cache changes |
| flush | dumps the internal state of the instance cache |
### `GrafooClient.execute`
This method receives as arguments a query object created with the `@grafoo/core/tag` template tag and optionally a GraphQL variables object. It returns a promise that will resolve with the data requested or reject with a list of GraphQL errors.
#### Example
```js
const variables = { id: 123 };
client.execute(USER_QUERY, variables).then(data => {
console.log(data); // { "user": { "name": "John Doe", "id": "123" } }
});
```
### `GrafooClient.write`
The write method as the name implies writes to the cache. It takes as argumets the query object, an optional variables object and the data to be stored.
#### Example
```js
client.execute(USER_QUERY, variables).then(data => {
client.write(USER_QUERY, variables, data);
});
```
### `GrafooClient.read`
The read method takes as arguments the query object and optionally a variables object. It returns an object with three properties: `data`, a tree structured object shaped according to your query tree, `objects` a flat structured object containing every node on your query indexed by a unique id created with the `idProps` option passed on client instantiation and a `partial` property that flags if the data is partially cached or not.
#### Example
```js
client.read(USER_QUERY, variables);
// {
// "data": {
// "user": {
// "name": "John Doe",
// "id": "123"
// }
// },
// "objects": {
// "123": {
// "name": "John Doe",
// "id": "123"
// }
// },
// partial: false
// }
```
### `GrafooClient.listen`
`listen` takes a _listener_ callback as argument. Whenever the cache is updated that _listener_ is called with the objects that were inserted, modified or removed.
#### Example
```js
function listener(objects) {
console.log(objects);
}
const unlisten = client.listen();
client.write(USER_QUERY, variables, data);
unlisten(); // detaches the listener from the client
```
### `GrafooClient.flush`
The `flush` method dumps all of the data inside the cache in it's raw state, producing a snapshot. It is to be used in mainly on the server producing, a initial state that can be passed as an option to `createClient` on client side.
#### Example
```js
// server.js
app.get("/", (req, res) => {
res.send(`<script>_GRAFOO_INITIAL_STATE_=${JSON.stringify(client.flush())}_</script>`);
});
// client.js
const client = createClient(fetchQuery, {
initialState: window._GRAFOO_INITIAL_STATE_
});
```
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: packages/core/schema.graphql
================================================
type Query {
author(id: ID!): Author!
authors: [Author!]!
post(id: ID!): Post!
posts: [Post!]!
}
type Mutation {
createAuthor(name: String!): Author!
updateAuthor(id: ID!, name: String): Author!
deleteAuthor(id: ID!): Author!
createPost(title: String!, body: String!, author: ID!): Post!
updatePost(id: ID!, title: String, body: String): Post!
deletePost(id: ID!): Post!
}
type Author {
id: ID!
name: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String!
author: Author!
}
================================================
FILE: packages/core/src/build-query-tree.ts
================================================
import { idFromProps, isNotNullObject } from "./util";
export default function buildQueryTree(tree, objects, idFields) {
// clone resulting query tree
let queryTree = tree;
let stack = [];
// populates stack with the properties of the query tree and the query tree it self
for (let i in queryTree) stack.push([i, queryTree]);
// will loop until the stack is empty
while (stack.length) {
// pops a stack entry extracting the current key of the tree's branch
// (eg: a node or an edge) and the branch it self
let [key, currentTree] = stack.pop();
// assigns nested branch
let branch = currentTree[key];
// get node identifier
let identifier = idFromProps(branch, idFields);
// possible node matching object
let branchObject = objects[identifier];
// iterates over the child branch properties
for (let i in Object.assign({}, branch, branchObject)) {
// assigns to the child branch all properties retrieved
// from the corresponding object retrieved from the objects cache
if (identifier && branchObject) branch[i] = branchObject[i] || branch[i];
// pushes properties of the child branch and the branch it self to the stack
if (isNotNullObject(branch[i])) stack.push([i, branch]);
}
}
return queryTree;
}
================================================
FILE: packages/core/src/index.ts
================================================
import {
GrafooClient,
GrafooClientOptions,
GrafooObject,
Listener,
ObjectsMap,
Variables,
GrafooTransport,
} from "@grafoo/types";
import buildQueryTree from "./build-query-tree";
import mapObjects from "./map-objects";
import { getPathId } from "./util";
export default function createClient(
transport: GrafooTransport,
options?: GrafooClientOptions
): GrafooClient {
let { initialState, idFields } = options;
let { pathsMap, objectsMap } = initialState || { pathsMap: {}, objectsMap: {} };
let listeners: Listener[] = [];
function execute<T>({ query, frags, id }: GrafooObject, variables?: Variables) {
if (frags) for (let frag in frags) query += frags[frag];
return transport<T>(query, variables, id);
}
function listen(listener: Listener) {
listeners.push(listener);
return () => {
let index = listeners.indexOf(listener);
if (index < 0) return;
listeners.splice(index, 1);
};
}
function write<T>({ paths }: GrafooObject, variables: Variables, data?: T | { data: T }) {
if (!data) {
data = variables as typeof data;
variables = undefined;
}
let objects: ObjectsMap = {};
for (let i in paths) {
let { name, args } = paths[i];
let pathData = {
[name]: (data as { data: T }).data ? (data as { data: T }).data[name] : data[name],
};
let pathObjects = mapObjects(pathData, idFields);
Object.assign(objects, pathObjects);
pathsMap[getPathId(i, args, variables)] = {
data: pathData,
objects: Object.keys(pathObjects),
};
}
// assign new values to objects in objectsMap
for (let i in objects) {
objectsMap[i] = objects[i] = Object.assign({}, objectsMap[i], objects[i]);
}
// clean cache
let pathsObjects = [];
for (let i in pathsMap) pathsObjects = pathsObjects.concat(pathsMap[i].objects);
let allObjects = new Set(pathsObjects);
for (let i in objectsMap) if (!allObjects.has(i)) delete objectsMap[i];
// run listeners
for (let i in listeners) listeners[i](objects);
}
function read({ paths }: GrafooObject, variables?: Variables) {
let data = {};
let objects: ObjectsMap = {};
let partial = false;
for (let i in paths) {
let { name, args } = paths[i];
let currentPath = pathsMap[getPathId(i, args, variables)];
if (currentPath) {
data[name] = currentPath.data[name];
for (let i of currentPath.objects) objects[i] = objectsMap[i];
} else {
partial = true;
}
}
return Object.keys(data).length
? { data: buildQueryTree(data, objectsMap, idFields), objects, partial }
: {};
}
function flush() {
return { objectsMap, pathsMap };
}
function reset() {
pathsMap = {};
objectsMap = {};
}
return { execute, listen, write, read, flush, reset };
}
================================================
FILE: packages/core/src/map-objects.ts
================================================
import { isNotNullObject, idFromProps } from "./util";
export default function mapObjects(tree, idFields) {
// map in which objects will be stored
// having their extracted ids from props as key
let map = {};
let stack = [];
// populates the stack with the tree branches
for (let i in tree) stack.push(tree[i]);
// will run until the stack is empty
while (stack.length) {
// pops the current branch from the stack
let branch = stack.pop();
// next node to be traversed. nested branches will be removed
let filteredBranch = {};
// iterate over branch properties
// if the property is a branch it will be added to the stack
// else if it is not a branch it will be added to filtered branch
for (let i in branch) {
let branchVal = branch[i];
(isNotNullObject(branchVal) && stack.push(branchVal)) || (filteredBranch[i] = branchVal);
}
// node identifier
let identifier = idFromProps(branch, idFields);
// if branch is a node, assign the value of filtered branch to it
if (identifier) map[identifier] = Object.assign({}, map[identifier], filteredBranch);
}
return map;
}
================================================
FILE: packages/core/src/util.ts
================================================
import { Variables } from "@grafoo/types";
export let idFromProps = (branch, idFields) => {
branch = branch || {};
let identifier = "";
for (let i = 0; i < idFields.length; i++) {
branch[idFields[i]] && (identifier += branch[idFields[i]]);
}
return identifier;
};
export let isNotNullObject = (obj) => obj && typeof obj == "object";
export let getPathId = (path: string, args: string[], variables?: Variables) => {
variables = variables || {};
let finalPath = path;
let i = args.length;
while (i--) finalPath += ":" + variables[args[i]];
return finalPath;
};
================================================
FILE: packages/core/tag.d.ts
================================================
declare module "@grafoo/core/tag" {
import { GrafooObject } from "@grafoo/types";
export default function graphql(strs: TemplateStringsArray): GrafooObject;
}
================================================
FILE: packages/core/tag.js
================================================
function graphql() {
throw new Error(
"@grafoo/core/tag: if you are getting this error it means your queries are not being transpiled"
);
}
module.exports = graphql;
module.exports.default = graphql;
================================================
FILE: packages/core/tsconfig.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"strict": false,
"lib": ["esnext", "dom"],
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": false,
"downlevelIteration": true
},
"include": ["src"]
}
================================================
FILE: packages/http-transport/.babelrc
================================================
{
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }],
"@babel/preset-typescript"
]
}
================================================
FILE: packages/http-transport/.npmignore
================================================
coverage
__tests__
.rpt2_cache
.babelrc
schema.graphql
================================================
FILE: packages/http-transport/__tests__/index.ts
================================================
/* eslint-disable @typescript-eslint/no-var-requires */
import { GrafooTransport } from "@grafoo/types";
import createTransport from "../src";
jest.mock("node-fetch", () => require("fetch-mock").sandbox());
let fetchMock = require("node-fetch");
global.fetch = fetchMock;
let fakeAPI = "http://fake-api.com/graphql";
let query = "{ hello }";
describe("@grafoo/http-transport", () => {
let request: GrafooTransport;
beforeEach(() => {
request = createTransport(fakeAPI);
fetchMock.restore();
});
it("should perform a simple request", async () => {
await mock(async () => {
await request(query);
let [, { body, headers, method }] = fetchMock.lastCall();
expect(method).toBe("POST");
expect(body).toBe(JSON.stringify({ query }));
expect(headers).toEqual({ "Content-Type": "application/json" });
});
});
it("should perform a request with variables", async () => {
await mock(async () => {
let variables = { some: "variable" };
await request(query, variables);
let [, { body }] = fetchMock.lastCall();
expect(JSON.parse(body as string).variables).toEqual(variables);
});
});
it("should accept fetchObjects as an object", async () => {
request = createTransport(fakeAPI, { headers: { authorization: "Bearer some-token" } });
await mock(async () => {
await request(query);
let [, { headers }] = fetchMock.lastCall();
expect(headers).toEqual({
authorization: "Bearer some-token",
"Content-Type": "application/json"
});
});
});
it("should accept fetchObjects as a function", async () => {
request = createTransport(fakeAPI, () => ({ headers: { authorization: "Bearer some-token" } }));
await mock(async () => {
await request(query);
let [, { headers }] = fetchMock.lastCall();
expect(headers).toEqual({
authorization: "Bearer some-token",
"Content-Type": "application/json"
});
});
});
it("should handle graphql errors", async () => {
let response = { data: null, errors: [{ message: "I AM ERROR!" }] };
await mock(
async () => expect(request(query)).resolves.toMatchObject({ errors: response.errors }),
response
);
});
});
async function mock(testFn, response?: any) {
fetchMock.mock(fakeAPI, response || { data: { hello: "world" } });
await testFn();
}
================================================
FILE: packages/http-transport/__tests__/tsconfig.json
================================================
{
"extends": "../tsconfig",
"include": ["."]
}
================================================
FILE: packages/http-transport/package.json
================================================
{
"name": "@grafoo/http-transport",
"description": "grafoo client standard transport",
"version": "1.4.2",
"repository": "https://github.com/grafoojs/grafoo/tree/master/packages/transport",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "malbernaz<albernazmiguel@gmail.com>",
"license": "MIT",
"keywords": [
"graphql",
"graphql-client",
"grafoo"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "grafoo-bundle --input src/index.ts",
"test": "jest",
"test:coverage": "jest --coverage"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx|js)$": "<rootDir>/../../scripts/jest-setup.js"
},
"resolver": "<rootDir>/../../scripts/resolver.js",
"transformIgnorePatterns": [
"node_modules/(?!(lowdb|steno|node-fetch|fetch-blob)/)"
]
},
"dependencies": {
"@grafoo/types": "^1.4.2",
"grafoo-bundle": "^1.4.2"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974"
}
================================================
FILE: packages/http-transport/readme.md
================================================
# `@grafoo/http-transport`
<p><i>A Simple HTTP Client for GraphQL Servers</i></p>
<p>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://www.npmjs.com/package/@grafoo/http-transport>
<img
src=https://img.shields.io/npm/v/@grafoo/http-transport.svg
alt=npm
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/http-transport>
<img
src=https://img.shields.io/npm/dm/@grafoo/http-transport.svg
alt=downloads
>
</a>
<a href=https://github.com/grafoojs/grafoo>
<img
src=https://img.shields.io/bundlephobia/minzip/@grafoo/http-transport.svg?label=size
alt=size
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
## Install
```
$ npm i @grafoo/http-transport
```
## Usage
`@grafoo/http-transport` default export is a factory that accepts as arguments `uri` and `fetchOptions` (that can be an object or a function):
```js
import createTransport from "@grafoo/http-transport";
const request = createTransport("http://some.graphql.api", () => ({
headers: {
authorization: storage.getItem("authorization")
}
}));
const USER_QUERY = `
query($id: ID!) {
user(id: $id) {
name
}
}
`;
const variables = { id: 123 };
request(USER_QUERY, variables).then(({ data }) => {
console.log(data.user);
});
```
## Warning
As this package uses `fetch` and `Object.assign` under the hood, make sure to install the proper polyfills if you want to use it in your project.
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: packages/http-transport/src/index.ts
================================================
import { GraphQlPayload, GrafooTransport, Variables } from "@grafoo/types";
export default function createTransport(
url: string,
options?: RequestInit | (() => RequestInit)
): GrafooTransport {
return <T>(query: string, variables?: Variables): Promise<GraphQlPayload<T>> => {
options = typeof options == "function" ? options() : options || {};
return fetch(
url,
Object.assign(options, {
body: JSON.stringify({ query, variables }),
method: "POST",
headers: Object.assign({ "Content-Type": "application/json" }, options.headers),
})
).then((response) => response.json());
};
}
================================================
FILE: packages/http-transport/tsconfig.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"strict": false,
"lib": ["esnext", "dom"],
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": false,
"downlevelIteration": true
},
"include": ["src"]
}
================================================
FILE: packages/preact/.babelrc
================================================
{
"presets": [
["@babel/preset-env", { "targets": { "node": 10 } }],
["@babel/preset-react", { "pragma": "h" }],
["@babel/preset-typescript", { "jsxPragma": "h" }]
],
"plugins": [
["babel-plugin-jsx-pragmatic", { "module": "preact", "export": "h", "import": "h" }],
[
"module:@grafoo/babel-plugin",
{ "schema": "schema.graphql", "idFields": ["id", "__typename"] }
]
]
}
================================================
FILE: packages/preact/.npmignore
================================================
coverage
__tests__
.rpt2_cache
.babelrc
schema.graphql
================================================
FILE: packages/preact/__tests__/index.tsx
================================================
/**
* @jest-environment jsdom
*/
import createClient from "@grafoo/core";
import graphql from "@grafoo/core/tag";
import createTransport from "@grafoo/http-transport";
import { mockQueryRequest } from "@grafoo/test-utils";
import { GrafooClient } from "@grafoo/types";
import { h, FunctionalComponent, Component } from "preact";
import { render } from "preact-render-spy";
import { Consumer, Provider } from "../src";
interface Post {
title: string;
content: string;
id: string;
__typename: string;
author: Author;
}
interface Author {
name: string;
id: string;
__typename: string;
posts?: Array<Post>;
}
interface Authors {
authors: Author[];
}
let AUTHOR = graphql`
query ($id: ID!) {
author(id: $id) {
name
}
}
`;
let AUTHORS = graphql`
{
authors {
name
posts {
title
body
}
}
}
`;
let CREATE_AUTHOR = graphql`
mutation ($name: String!) {
createAuthor(name: $name) {
name
}
}
`;
let POSTS_AND_AUTHORS = graphql`
{
posts {
title
body
author {
name
}
}
authors {
name
posts {
title
body
}
}
}
`;
describe("@grafoo/preact", () => {
let client: GrafooClient;
beforeEach(() => {
jest.resetAllMocks();
let transport = createTransport("https://some.graphql.api/");
client = createClient(transport, { idFields: ["id"] });
});
describe("<Provider />", () => {
it("should provide the client in it's context", (done) => {
let Comp = (_, context) => {
expect(context.client).toBe(client);
return null;
};
render(
<Provider client={client}>
<Comp />
</Provider>
);
done();
});
});
describe("<Consumer />", () => {
it("should not crash if a query is not given as prop", () => {
expect(() =>
render(
<Provider client={client}>
<Consumer>{() => null}</Consumer>
</Provider>
)
).not.toThrow();
});
it("should not fetch a query if skip prop is set to true", async () => {
await mockQueryRequest(AUTHORS);
let spy = jest.spyOn(window, "fetch");
render(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{() => null}
</Consumer>
</Provider>
);
expect(spy).not.toHaveBeenCalled();
});
it("should trigger listen on client instance", async () => {
await mockQueryRequest(AUTHORS);
let spy = jest.spyOn(client, "listen");
render(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{() => null}
</Consumer>
</Provider>
);
expect(spy).toHaveBeenCalled();
});
it("should not crash on unmount", () => {
let ctx = render(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{() => null}
</Consumer>
</Provider>
);
expect(() => ctx.render(null)).not.toThrow();
});
it("should execute render with default render argument", () => {
let mockRender = jest.fn();
render(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{mockRender}
</Consumer>
</Provider>
);
let [[call]] = mockRender.mock.calls;
expect(call).toMatchObject({ loading: false, loaded: false });
expect(typeof call.load).toBe("function");
});
it("should execute render with the right data if a query is specified", (done) => {
mockQueryRequest<Author>(AUTHORS).then(({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: true, loaded: false }),
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data })
]);
render(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
});
});
it("should render if skip changed value to true", (done) => {
mockQueryRequest<Author>(AUTHORS).then(async ({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: false, loaded: false }),
(props) => expect(props).toMatchObject({ loading: true, loaded: false }),
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data }),
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data })
]);
let App: FunctionalComponent<{ skip?: boolean }> = ({ skip = false }) => (
<Provider client={client}>
<Consumer query={AUTHORS} skip={skip}>
{mockRender}
</Consumer>
</Provider>
);
let ctx = render(<App skip />);
ctx.render(<App />);
await new Promise((resolve) => setTimeout(resolve, 10));
ctx.render(<App />);
});
});
it("should rerender if variables prop has changed", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(async ({ data }) => {
let mock = async (variables) => {
return (
await mockQueryRequest<{ author: Author }>({
query: AUTHOR.query,
variables
})
).data.author;
};
let firstVariables = { id: data.authors[0].id };
let secondVariables = { id: data.authors[1].id };
let firstAuthor = await mock(firstVariables);
let secondAuthor;
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: true, loaded: false }),
(props) => expect(props.author).toMatchObject(firstAuthor),
(props) =>
expect(props).toMatchObject({ loading: true, loaded: true, author: firstAuthor }),
(props) => expect(props.author).toMatchObject(secondAuthor)
]);
class AuthorComponent extends Component {
constructor(props, context) {
super(props, context);
this.state = firstVariables;
setTimeout(async () => {
secondAuthor = await mock(secondVariables);
this.setState(secondVariables);
}, 100);
}
render(_, variables) {
return (
<Consumer query={AUTHOR} variables={variables}>
{mockRender}
</Consumer>
);
}
}
render(
<Provider client={client}>
<AuthorComponent />
</Provider>
);
});
});
it("should not trigger a network request if the query is already cached", (done) => {
mockQueryRequest<Author>(AUTHORS).then(({ data }) => {
client.write(AUTHORS, data);
jest.resetAllMocks();
let spy = jest.spyOn(client, "execute");
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data })
]);
render(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
expect(spy).not.toHaveBeenCalled();
});
});
it("should handle simple mutations", (done) => {
let { query } = CREATE_AUTHOR;
let variables = { name: "Bart" };
mockQueryRequest({ query, variables }).then(({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => {
props.createAuthor(variables).then((res) => {
expect(res.data).toEqual(data);
});
}
]);
render(
<Provider client={client}>
<Consumer mutations={{ createAuthor: { query: CREATE_AUTHOR } }}>{mockRender}</Consumer>
</Provider>
);
});
});
it("should handle mutations with cache update", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => {
expect(props).toMatchObject({ loading: true, loaded: false });
expect(typeof props.createAuthor).toBe("function");
},
(props) => {
expect(props).toMatchObject({ loading: false, loaded: true, ...data });
let variables = { name: "Homer" };
mockQueryRequest({ query: CREATE_AUTHOR.query, variables }).then(() => {
props.createAuthor(variables);
});
},
(props) => {
expect(props.authors.length).toBe(data.authors.length + 1);
let newAuthor = props.authors.find((a) => a.id === "tempID");
expect(newAuthor).toMatchObject({ name: "Homer", id: "tempID" });
},
(props) => {
expect(props.authors.find((a) => a.id === "tempID")).toBeUndefined();
expect(props.authors.find((a) => a.name === "Homer")).toBeTruthy();
}
]);
render(
<Provider client={client}>
<Consumer
query={AUTHORS}
mutations={{
createAuthor: {
query: CREATE_AUTHOR,
optimisticUpdate: ({ authors }, variables) => ({
authors: [...authors, { ...variables, id: "tempID" }]
}),
update: ({ authors }, { createAuthor: author }) => ({
authors: authors.map((a) => (a.id === "tempID" ? author : a))
})
}
}}
>
{mockRender}
</Consumer>
</Provider>
);
});
});
it("should reflect updates that happen outside of the component", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(({ data }) => {
client.write(AUTHORS, data);
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data }),
(props) => expect(props.authors[0].name).toBe("Homer")
]);
render(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
client.write(AUTHORS, {
authors: data.authors.map((a, i) => (!i ? { ...a, name: "Homer" } : a))
});
});
});
it("should not trigger a network request if a query field is cached", (done) => {
mockQueryRequest<Authors>(POSTS_AND_AUTHORS).then(({ data }) => {
client.write(POSTS_AND_AUTHORS, data);
let spy = jest.spyOn(client, "execute");
let mockRender = createMockRenderFn(done, [
(props) => {
expect(props).toMatchObject({ authors: data.authors, loading: false, loaded: true });
expect(spy).not.toHaveBeenCalled();
}
]);
render(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
});
});
});
});
function createMockRenderFn(done, assertionsFns) {
let currentRender = 0;
return (props) => {
let assert = assertionsFns[currentRender];
if (assert) assertionsFns[currentRender](props);
if (currentRender++ === assertionsFns.length - 1) done();
return null;
};
}
================================================
FILE: packages/preact/__tests__/tsconfig.json
================================================
{
"extends": "../tsconfig",
"include": ["."]
}
================================================
FILE: packages/preact/package.json
================================================
{
"name": "@grafoo/preact",
"version": "1.4.2",
"description": "grafoo client preact bindings",
"repository": "https://github.com/grafoojs/grafoo/tree/master/packages/preact",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "malbernaz<albernazmiguel@gmail.com>",
"license": "MIT",
"keywords": [
"babel",
"babel-plugin",
"graphql",
"graphql-client",
"grafoo",
"preact",
"preactjs"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "grafoo-bundle --input src/index.ts",
"test": "jest",
"test:coverage": "jest --coverage"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx|js)$": "<rootDir>/../../scripts/jest-setup.js"
},
"resolver": "<rootDir>/../../scripts/resolver.js",
"transformIgnorePatterns": [
"node_modules/(?!(lowdb|steno|node-fetch|fetch-blob)/)"
]
},
"peerDependencies": {
"preact": ">=8.3"
},
"dependencies": {
"@grafoo/bindings": "^1.4.2",
"@grafoo/types": "^1.4.2"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974"
}
================================================
FILE: packages/preact/readme.md
================================================
# `@grafoo/preact`
<p><i>Grafoo Preact Bindings</i></p>
<p>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://github.com/grafoojs/grafoo>
<img
src=https://img.shields.io/npm/v/@grafoo/preact.svg
alt=npm
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/preact>
<img
src=https://img.shields.io/npm/dm/@grafoo/preact.svg
alt=downloads
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/preact>
<img
src=https://img.shields.io/bundlephobia/minzip/@grafoo/preact.svg?label=size
alt=size
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
## Install
```
$ npm i @grafoo/{core,react} && npm i -D @grafoo/babel-plugin
```
## API
For documentation please refer to [`@grafoo/react`](https://github.com/grafoojs/grafoo/tree/master/packages/react)'s page since both modules share the same API.
## Example
**`index.js`**
```jsx
import { h, render } from "preact";
import createClient from "@grafoo/core";
import { Provider } from "@grafoo/preact";
import Posts from "./Posts";
function fetchQuery(query, variables) {
const init = {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json"
}
};
return fetch("http://some.graphql.api", init).then(res => res.json());
}
const client = createClient(fetchQuery);
render(
<Provider client={client}>
<Posts />
</Provider>,
document.getElementById("mnt")
);
```
**`Posts.js`**
```jsx
import { h } from "preact";
import gql from "@grafoo/core/tag";
import { Consumer } from "@grafoo/preact";
const ALL_POSTS = gql`
query getPosts($orderBy: PostOrderBy) {
allPosts(orderBy: $orderBy) {
title
content
createdAt
updatedAt
}
}
`;
export default function Posts() {
return (
<Consumer query={ALL_POSTS} variables={{ orderBy: "createdAt_DESC" }}>
{({ client, load, loaded, loading, errors, allPosts }) => (
<h1>
<marquee>👆 do whatever you want with the variables above 👆</marquee>
</h1>
)}
</Consumer>
);
}
```
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: packages/preact/schema.graphql
================================================
type Query {
author(id: ID!): Author!
authors: [Author!]!
post(id: ID!): Post!
posts: [Post!]!
}
type Mutation {
createAuthor(name: String!): Author!
updateAuthor(id: ID!, name: String): Author!
deleteAuthor(id: ID!): Author!
createPost(title: String!, body: String!, author: ID!): Post!
updatePost(id: ID!, title: String, body: String): Post!
deletePost(id: ID!): Post!
}
type Author {
id: ID!
name: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String!
author: Author!
}
================================================
FILE: packages/preact/src/consumer.ts
================================================
import createBindings from "@grafoo/bindings";
import {
Context,
GrafooBoundState,
GrafooBoundMutations,
GrafooConsumerProps,
} from "@grafoo/types";
import { Component, VNode } from "preact";
/**
* T = Query
* U = Mutations
*/
type GrafooRenderFn<T, U> = (renderProps: GrafooBoundState & T & GrafooBoundMutations<U>) => VNode;
/**
* T = Query
* U = Mutations
*/
type GrafooPreactConsumerProps<T = unknown, U = unknown> = GrafooConsumerProps<T, U> & {
children?: GrafooRenderFn<T, U>;
};
/**
* T = Query
* U = Mutations
*/
export class Consumer<T = unknown, U = unknown> extends Component<GrafooPreactConsumerProps<T, U>> {
state: GrafooBoundState & T & GrafooBoundMutations<U>;
constructor(props: GrafooPreactConsumerProps<T, U>, context: Context) {
super(props, context);
let bindings = createBindings<T, U>(context.client, props, () => {
this.setState(bindings.getState());
});
this.state = bindings.getState();
this.componentDidMount = () => {
if (props.skip || !props.query || this.state.loaded) return;
this.state.load();
};
this.componentWillReceiveProps = (next) => {
if ((!this.state.loaded && !next.skip) || props.variables !== next.variables)
this.state.load(next.variables);
};
this.componentWillUnmount = () => {
bindings.unbind();
};
}
render(props, state): VNode {
return props.children[0](state);
}
}
================================================
FILE: packages/preact/src/index.ts
================================================
export * from "./provider";
export * from "./consumer";
================================================
FILE: packages/preact/src/provider.ts
================================================
import { Context } from "@grafoo/types";
import { Component } from "preact";
type GrafooPreactProviderProps = Context & { children?: JSX.Element };
export class Provider extends Component<GrafooPreactProviderProps> {
getChildContext(): Context {
return { client: this.props.client };
}
render(props: GrafooPreactProviderProps): JSX.Element {
return props.children[0];
}
}
================================================
FILE: packages/preact/tsconfig.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"strict": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": false,
"downlevelIteration": true,
"jsx": "react",
"jsxFactory": "h",
"lib": ["dom", "esnext"],
"types": ["preact", "jest"]
},
"include": ["src"]
}
================================================
FILE: packages/react/.babelrc
================================================
{
"presets": [
["@babel/preset-env", { "targets": { "node": 10 } }],
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
[
"module:@grafoo/babel-plugin",
{ "schema": "schema.graphql", "idFields": ["id"] }
]
]
}
================================================
FILE: packages/react/.npmignore
================================================
coverage
__tests__
.rpt2_cache
.babelrc
schema.graphql
================================================
FILE: packages/react/__tests__/index.tsx
================================================
/**
* @jest-environment jsdom
*/
import createClient from "@grafoo/core";
import graphql from "@grafoo/core/tag";
import createTrasport from "@grafoo/http-transport";
import { mockQueryRequest } from "@grafoo/test-utils";
import { GrafooClient, Variables } from "@grafoo/types";
import * as React from "react";
import * as TestRenderer from "react-test-renderer";
import { Consumer, Provider } from "../src";
interface Post {
title: string;
content: string;
id: string;
__typename: string;
author: Author;
}
interface Author {
name: string;
id: string;
__typename: string;
posts?: Array<Post>;
}
interface Authors {
authors: Author[];
}
let AUTHORS = graphql`
{
authors {
name
posts {
title
body
}
}
}
`;
let AUTHOR = graphql`
query ($id: ID!) {
author(id: $id) {
name
}
}
`;
let CREATE_AUTHOR = graphql`
mutation ($name: String!) {
createAuthor(name: $name) {
name
}
}
`;
let POSTS_AND_AUTHORS = graphql`
{
posts {
title
body
author {
name
}
}
authors {
name
posts {
title
body
}
}
}
`;
describe("@grafoo/react", () => {
let client: GrafooClient;
beforeEach(() => {
jest.resetAllMocks();
let transport = createTrasport("https://some.graphql.api/");
client = createClient(transport, { idFields: ["id"] });
});
it("should not crash if a query is not given as prop", () => {
expect(() =>
TestRenderer.create(
<Provider client={client}>
<Consumer>{() => null}</Consumer>
</Provider>
)
).not.toThrow();
});
it("should not fetch a query if skip prop is set to true", async () => {
await mockQueryRequest(AUTHORS);
let spy = jest.spyOn(window, "fetch");
TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{() => null}
</Consumer>
</Provider>
);
expect(spy).not.toHaveBeenCalled();
});
it("should trigger listen on client instance", async () => {
await mockQueryRequest(AUTHORS);
let spy = jest.spyOn(client, "listen");
TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{() => null}
</Consumer>
</Provider>
);
expect(spy).toHaveBeenCalled();
});
it("should not crash on unmount", () => {
let testRenderer = TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{() => null}
</Consumer>
</Provider>
);
expect(() => testRenderer.unmount()).not.toThrow();
});
it("should execute render with default render argument", () => {
let mockRender = jest.fn().mockReturnValue(null);
TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS} skip>
{mockRender}
</Consumer>
</Provider>
);
let [[call]] = mockRender.mock.calls;
expect(call).toMatchObject({ loading: false, loaded: false });
expect(typeof call.load).toBe("function");
});
it("should execute render with the right data if a query is specified", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: true, loaded: false }),
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data })
]);
TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
});
});
it("should render if skip changed value to true", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(async ({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: false, loaded: false }),
(props) => expect(props).toMatchObject({ loading: true, loaded: false }),
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data })
]);
let App: React.FC<{ skip?: boolean }> = ({ skip = false }) => (
<Provider client={client}>
<Consumer query={AUTHORS} skip={skip}>
{mockRender}
</Consumer>
</Provider>
);
let ctx = TestRenderer.create(<App skip />);
ctx.update(<App />);
await new Promise((resolve) => setTimeout(resolve, 10));
ctx.update(<App />);
});
});
it("should rerender if variables prop has changed", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(async ({ data }) => {
let mock = async (variables: Variables) => {
return (
await mockQueryRequest<{ author: Author }>({
query: AUTHOR.query,
variables
})
).data.author;
};
let firstVariables = { id: data.authors[0].id };
let secondVariables = { id: data.authors[1].id };
let firstAuthor = await mock(firstVariables);
let secondAuthor;
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: true, loaded: false }),
(props) => expect(props.author).toMatchObject(firstAuthor),
(props) =>
expect(props).toMatchObject({ loading: true, loaded: true, author: firstAuthor }),
(props) => expect(props.author).toMatchObject(secondAuthor)
]);
class AuthorComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.state = firstVariables;
setTimeout(async () => {
secondAuthor = await mock(secondVariables);
this.setState(secondVariables);
}, 100);
}
render() {
return (
<Consumer query={AUTHOR} variables={this.state}>
{mockRender}
</Consumer>
);
}
}
TestRenderer.create(
<Provider client={client}>
<AuthorComponent />
</Provider>
);
});
});
it("should not trigger a network request if the query is already cached", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(({ data }) => {
client.write(AUTHORS, data);
jest.resetAllMocks();
let spy = jest.spyOn(client, "execute");
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data })
]);
TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
expect(spy).not.toHaveBeenCalled();
});
});
it("should handle simple mutations", (done) => {
let variables = { name: "Bart" };
mockQueryRequest({ query: CREATE_AUTHOR.query, variables }).then(({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => {
props.createAuthor(variables).then((res) => {
expect(res.data).toEqual(data);
});
}
]);
let mutations = { createAuthor: { query: CREATE_AUTHOR } };
TestRenderer.create(
<Provider client={client}>
<Consumer mutations={mutations}>{mockRender}</Consumer>
</Provider>
);
});
});
it("should handle mutations with cache update", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(({ data }) => {
let mockRender = createMockRenderFn(done, [
(props) => {
expect(props).toMatchObject({ loading: true, loaded: false });
expect(typeof props.createAuthor).toBe("function");
},
(props) => {
expect(props).toMatchObject({ loading: false, loaded: true, ...data });
let variables = { name: "Homer" };
mockQueryRequest({ query: CREATE_AUTHOR.query, variables }).then(() => {
props.createAuthor(variables);
});
},
(props) => {
expect(props.authors.length).toBe(data.authors.length + 1);
let newAuthor = props.authors.find((a) => a.id === "tempID");
expect(newAuthor).toMatchObject({ name: "Homer", id: "tempID" });
},
(props) => {
expect(props.authors.find((a) => a.id === "tempID")).toBeUndefined();
expect(props.authors.find((a) => a.name === "Homer")).toBeTruthy();
}
]);
TestRenderer.create(
<Provider client={client}>
<Consumer
query={AUTHORS}
mutations={{
createAuthor: {
query: CREATE_AUTHOR,
optimisticUpdate: ({ authors }, variables) => ({
authors: [...authors, { ...variables, id: "tempID" }]
}),
update: ({ authors }, data) => ({
authors: authors.map((a) => (a.id === "tempID" ? (data as any).createAuthor : a))
})
}
}}
>
{mockRender}
</Consumer>
</Provider>
);
});
});
it("should reflect updates that happen outside of the component", (done) => {
mockQueryRequest<Authors>(AUTHORS).then(({ data }) => {
client.write(AUTHORS, data);
let mockRender = createMockRenderFn(done, [
(props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data }),
(props) => expect(props.authors[0].name).toBe("Homer")
]);
TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
client.write(AUTHORS, {
authors: data.authors.map((a, i) => (!i ? { ...a, name: "Homer" } : a))
});
});
});
it("should not trigger a network request if a query field is cached", (done) => {
mockQueryRequest<Authors>(POSTS_AND_AUTHORS).then(({ data }) => {
client.write(POSTS_AND_AUTHORS, data);
let spy = jest.spyOn(client, "execute");
let mockRender = createMockRenderFn(done, [
(props) => {
expect(props).toMatchObject({ authors: data.authors, loading: false, loaded: true });
expect(spy).not.toHaveBeenCalled();
}
]);
TestRenderer.create(
<Provider client={client}>
<Consumer query={AUTHORS}>{mockRender}</Consumer>
</Provider>
);
});
});
});
function createMockRenderFn(done, assertionsFns) {
let currentRender = 0;
return (props) => {
let assert = assertionsFns[currentRender];
if (assert) assertionsFns[currentRender](props);
if (currentRender++ === assertionsFns.length - 1) done();
return null;
};
}
================================================
FILE: packages/react/__tests__/tsconfig.json
================================================
{
"extends": "../tsconfig",
"include": ["."]
}
================================================
FILE: packages/react/package.json
================================================
{
"name": "@grafoo/react",
"version": "1.4.2",
"description": "grafoo client react bindings",
"repository": "https://github.com/grafoojs/grafoo/tree/master/packages/react",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "malbernaz<albernazmiguel@gmail.com>",
"license": "MIT",
"keywords": [
"babel",
"babel-plugin",
"graphql",
"graphql-client",
"grafoo",
"react",
"reactjs"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "grafoo-bundle --input src/index.ts",
"test": "jest",
"test:coverage": "jest --coverage",
"tsc": "tsc --noEmit"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx|js)$": "<rootDir>/../../scripts/jest-setup.js"
},
"resolver": "<rootDir>/../../scripts/resolver.js",
"transformIgnorePatterns": [
"node_modules/(?!(lowdb|steno|node-fetch|fetch-blob)/)"
]
},
"peerDependencies": {
"react": ">=16.4"
},
"dependencies": {
"@grafoo/bindings": "^1.4.2",
"@grafoo/types": "^1.4.2"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974"
}
================================================
FILE: packages/react/readme.md
================================================
# `@grafoo/react`
<p><i>Grafoo React Bindings</i></p>
<p>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://github.com/grafoojs/grafoo>
<img
src=https://img.shields.io/npm/v/@grafoo/bindings.svg
alt=npm
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/react>
<img
src=https://img.shields.io/npm/dm/@grafoo/bindings.svg
alt=downloads
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/react>
<img
src=https://img.shields.io/bundlephobia/minzip/@grafoo/react.svg?label=size
alt=size
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
## Install
```
$ npm i @grafoo/{core,react} && npm i -D @grafoo/babel-plugin
```
## Setup
Assuming you already have babel installed, the only additional step required to build an application with Grafoo is to configure [`@grafoo/babel-plugin`](https://github.com/grafoojs/grafoo/tree/master/packages/babel-plugin). The options it accepts are `idFields` - the fields Grafoo will take to build unique identifiers, and `schema`, which is a relative path to your schema file.
```json
{
"plugins": [
[
"@grafoo/babel-plugin",
{
"schema": "schema.graphql",
"idFields": ["id"]
}
]
]
}
```
## API
### `Provider`
`Provider` receives a single `client` instance prop that will be consumed by the `Consumer` components.
```jsx
import React from "react";
import createClient from "@grafoo/core";
import { Provider } from "@grafoo/react";
function fetchQuery(query, variables) {
const init = {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json"
}
};
return fetch("http://some.graphql.api", init).then(res => res.json());
}
const client = createClient(fetchQuery);
export default function App() {
return (
<Provider client={client}>
<SomeComponent />
</Provider>
);
}
```
### `Consumer`
`Consumer` is the component that performs query requests to your GraphQL API. It accepts the following props:
| Name | Type | Default | Required | Descrition |
| --------- | -------- | ------- | -------- | ------------------------------------------------------------ |
| query | object | - | false | a query created with `@grafoo/core/tag` |
| variables | object | - | false | a GraphQL variables object for the `query` prop |
| mutations | object | - | false | a map of mutations (description below) |
| skip | boolean | false | false | whether `Consumer` should skip the `query` request initially |
| children | function | - | false | a render function (description below) |
### Render parameter
The `Consumer` render function takes as parameter an object with the following props:
| Name | type | Descrition |
| ------- | -------- | ------------------------------------------------------------ |
| client | object | the client instance |
| load | function | a method to execute a query with the `query` prop |
| loading | boolean | whether the client is executing a query or not |
| loaded | boolean | whether the query data is already cached |
| errors | string[] | an array of GraphQL errors from a failed request to your API |
The remaining props are:
- the data fetched by the client and shaped according to your `query`
- mutation functions generated by the `mutations` object prop
#### Example
```jsx
import React from "react";
import gql from "@grafoo/core/tag";
import { Consumer } from "@grafoo/react";
const ALL_POSTS = gql`
query getPosts($orderBy: PostOrderBy) {
allPosts(orderBy: $orderBy) {
title
content
createdAt
updatedAt
}
}
`;
export default function Posts() {
return (
<Consumer query={ALL_POSTS} variables={{ orderBy: "createdAt_DESC" }}>
{({ client, load, loaded, loading, errors, allPosts }) => (
<h1>
<marquee>👆 do whatever you want with the variables above 👆</marquee>
</h1>
)}
</Consumer>
);
}
```
### Mutations
The `mutations` prop is a map of _mutation objects_ that are shaped like so:
```js
const createPost = {
query: CREATE_POST_MUTATION,
optimisticUpdate: ({ allPosts }, variables) => ({
allPosts: [{ ...variables.postInput, id: "tempID" }, ...allPosts]
}),
update: ({ allPosts }, response) => ({
allPosts: allPosts.map(p => (p.id === "tempID" ? response.post : p))
})
};
const mutations = { createPost };
```
A mutation object receives the following props:
| Name | Type | Required | Descrition |
| ---------------- | -------- | -------- | ------------------------------------------------------------------- |
| query | object | true | a mutation query created with `@grafoo/core/tag` |
| update | function | false | updates the cache when a request is completed (description below) |
| optimisticUpdate | function | false | updates the cache before a request is completed (description below) |
Each mutation will generate a single function that accepts a GraphQL variables object as argument and return a promise that will resolve with the mutation data or reject with GraphQL `errors`.
```ts
type MutationFn = (variables: Variables) => Promise<MutationResponse>;
```
### Mutation query dependency
**Important** to notice that to update the cache `update` and `optimistUpdate` hooks depend on a `query` and it's `variables` object props (so they need be declared in the `Consumer`). If you need to perform a mutation but updating the cache is not strictly important you can just use the mutation promise resolved data or use the client instance directly.
### `update`
```ts
type UpdateFn = (query: QueryData, response: MutationResponse) => CacheUpdate;
```
The mutation `update` function is resposible to update the cache when the request is completed. It receives as paremeters an object containing the data from the query it depends upon and the mutation response sent by the server. `update` return type is an object that describes the changes to be made to the cache.
### `optimisticUpdate`
```ts
type OptimistcUpdateFn = (query: QueryData, variables: Variables) => CacheUpdate;
```
In modern UIs it's to be expected that every user interaction occur in a fraction of seconds. `optimisticUpdate` responsability is to skip the mutation network roundtrip and update the cache instantaneously, making sure such interactions are as fast as they can be. `optimisticUpdate` as in `update` takes as first paremater the depedent query data. As second paremater it receives the variables object with which it's correpondent generated mutation function was called. And again it should return an object that describes the changes to be made to cache.
If you want to perform an optimitic update you have to make sure that the data you are inserting contains the field or fields to extract a unique identifier. For instance, say `@grafoo/babel-plugin` `idFields` option is set to insert a property `id`. Is to be expected that your update has that field mocked.
#### Example
```jsx
import React from "react";
import gql from "@grafoo/core/tag";
import { Consumer } from "@grafoo/react";
const ALL_POSTS = gql`
query getPosts($orderBy: PostOrderBy) {
allPosts(orderBy: $orderBy) {
title
content
createdAt
updatedAt
}
}
`;
const CREATE_POST = gql`
mutation createPost($content: String, $title: String, $authorId: ID) {
createPost(content: $content, title: $title, authorId: $authorId) {
title
content
createdAt
updatedAt
}
}
`;
const mutations = {
createPost: {
query: CREATE_POST,
optimisticUpdate: ({ allPosts }, variables) => ({
allPosts: [{ ...variables, id: "tempID" }, ...allPosts]
}),
update: ({ allPosts }, data) => ({
allPosts: allPosts.map(p => (p.id === "tempID" ? data.createPost : p))
})
}
};
const submit = mutate => event => {
event.preventDefault();
const { title, content } = event.target.elements;
mutate({ title: title.value, content: content.value }).then(mutationData => {
console.log(mutationData);
});
};
export default function PostForm() {
return (
<Consumer query={ALL_POSTS} mutations={mutations}>
{({ createPost }) => (
<form onSubmit={submit(createPost)}>
<input name="title" />
<textarea name="content" />
<button>submit</button>
</form>
)}
</Consumer>
);
}
```
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: packages/react/schema.graphql
================================================
type Query {
author(id: ID!): Author!
authors: [Author!]!
post(id: ID!): Post!
posts: [Post!]!
}
type Mutation {
createAuthor(name: String!): Author!
updateAuthor(id: ID!, name: String): Author!
deleteAuthor(id: ID!): Author!
createPost(title: String!, body: String!, author: ID!): Post!
updatePost(id: ID!, title: String, body: String): Post!
deletePost(id: ID!): Post!
}
type Author {
id: ID!
name: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String!
author: Author!
}
================================================
FILE: packages/react/src/index.ts
================================================
import createBindings from "@grafoo/bindings";
import {
Context,
GrafooConsumerProps,
GrafooBoundState,
GrafooBoundMutations,
} from "@grafoo/types";
import { Component, createContext, createElement, ReactElement, ReactNode, FC } from "react";
/**
* T = Query
* U = Mutations
*/
type GrafooRenderFn<T, U> = (
renderProps: GrafooBoundState & T & GrafooBoundMutations<U>
) => ReactNode;
/**
* T = Query
* U = Mutations
*/
type GrafooReactConsumerProps<T = unknown, U = unknown> = GrafooConsumerProps<T, U> & {
children: GrafooRenderFn<T, U>;
};
/**
* T = Query
* U = Mutations
*/
interface ConsumerType extends FC {
<T, U>(props: GrafooReactConsumerProps<T, U>): ReactElement | null;
}
let ctx = createContext({});
export let Provider: FC<Context> = (props) =>
createElement(ctx.Provider, { value: props.client }, props.children);
class GrafooConsumer<T, U> extends Component<GrafooReactConsumerProps<T, U>> {
state: GrafooBoundState & T & GrafooBoundMutations<U>;
constructor(props) {
super(props);
let bindings = createBindings<T, U>(props.client, props, () => {
this.setState(bindings.getState());
});
this.state = bindings.getState();
this.componentDidMount = () => {
if (props.skip || !props.query || this.state.loaded) return;
bindings.load();
};
this.componentWillReceiveProps = (next) => {
if ((!this.state.loaded && !next.skip) || props.variables !== next.variables) {
bindings.load(next.variables);
}
};
this.componentWillUnmount = () => {
bindings.unbind();
};
}
render() {
return this.props.children(this.state);
}
}
/**
* T = Query
* U = Mutations
*/
export let Consumer: ConsumerType = <T, U>(props: GrafooReactConsumerProps<T, U>) =>
createElement(ctx.Consumer, null, (client) =>
createElement(GrafooConsumer, Object.assign({ client }, props))
);
================================================
FILE: packages/react/tsconfig.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"strict": false,
"lib": ["esnext", "dom"],
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": false,
"downlevelIteration": true,
"jsx": "react"
},
"include": ["src"]
}
================================================
FILE: packages/test-utils/package.json
================================================
{
"private": true,
"name": "@grafoo/test-utils",
"version": "1.4.2",
"main": "dist/index.js",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc"
}
}
================================================
FILE: packages/test-utils/schema.graphql
================================================
type Query {
author(id: ID!): Author!
authors: [Author!]!
post(id: ID!): Post!
posts: [Post!]!
}
type Mutation {
createAuthor(name: String!): Author!
updateAuthor(id: ID!, name: String): Author!
deleteAuthor(id: ID!): Author!
createPost(title: String!, body: String!, author: ID!): Post!
updatePost(id: ID!, title: String, body: String): Post!
deletePost(id: ID!): Post!
}
type Author {
id: ID!
name: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String!
author: Author!
}
================================================
FILE: packages/test-utils/src/db.ts
================================================
import casual from "casual";
import { Low, Memory } from "lowdb";
casual.seed(666);
let times = (t: number, fn: (i: number) => void) => Array.from(Array(t), fn);
export default function setupDB() {
let db = new Low<{ posts: any[]; authors: any[] }>(new Memory());
db.data = { posts: [], authors: [] };
db.read;
times(2, () =>
db.data.authors.push({
id: casual.uuid,
name: casual.first_name + " " + casual.last_name,
})
);
db.data.authors.forEach(({ id }) => {
times(4, () =>
db.data.posts.push({
author: id,
id: casual.uuid,
title: casual.title,
body: casual.short_description,
})
);
let posts = db.data.posts.filter((post) => post.author === id).map((post) => post.id);
db.data.authors.find((author) => author.id === id).posts = posts;
});
db.write();
return db;
}
================================================
FILE: packages/test-utils/src/index.ts
================================================
export * from "./db";
export * from "./mock-server";
================================================
FILE: packages/test-utils/src/mock-server.ts
================================================
import { GraphQlPayload } from "@grafoo/types";
import fetchMock from "fetch-mock";
import fs from "fs";
import { graphql } from "graphql";
import { makeExecutableSchema } from "@graphql-tools/schema";
import path from "path";
import { v4 as uuid } from "uuid";
import setupDB from "./db";
let db = setupDB();
let typeDefs = fs.readFileSync(path.join(__dirname, "..", "schema.graphql"), "utf-8");
let Query = {
author(_, args) {
return db.data.authors.find((author) => author.id === args.id);
},
authors() {
return db.data.authors;
},
post(_, args) {
return db.data.posts.find((author) => author.id === args.id);
},
posts() {
return db.data.posts;
}
};
let Mutation = {
createAuthor(_, args) {
let newAuthor = Object.assign({}, args, { id: uuid() });
db.data.authors.push(newAuthor);
db.write();
return newAuthor;
},
updateAuthor(_, args) {
let author = Object.assign(
db.data.authors.find((author) => author.id === args.id),
args
);
db.write();
return author;
},
deleteAuthor(_, args) {
let author = db.data.authors.find(args);
db.data.authors = db.data.authors.filter((a) => a.id !== author.id);
db.data.posts = db.data.posts.filter((p) => p.author !== author.id);
db.write();
return author;
},
createPost(_, args) {
let newPost = Object.assign({}, args, { id: uuid() });
db.data.posts.push(newPost);
db.write();
return newPost;
},
updatePost(_, args) {
let post = Object.assign(
db.data.posts.find((author) => author.id === args.id),
args
);
db.write();
return post;
},
deletePost(_, args) {
let post = db.data.posts.find(args);
db.data.posts = db.data.posts.filter((p) => p.id !== args.id);
db.write();
return post;
}
};
let Author = {
posts(author) {
let s = author.posts
? author.posts.map((id) => db.data.posts.find((post) => post.id === id))
: null;
return s;
}
};
let Post = {
author(post) {
return db.data.authors.find((author) => author.id === post.author);
}
};
let resolvers = {
Query: Query,
Mutation: Mutation,
Author: Author,
Post: Post
};
let schema = makeExecutableSchema({ typeDefs: typeDefs, resolvers: resolvers });
interface ExecuteQueryArg {
query: string;
variables?: {
[key: string]: unknown;
};
}
export function executeQuery<T>({ query, variables }: ExecuteQueryArg): Promise<GraphQlPayload<T>> {
// @ts-ignore
return graphql({ schema: schema, source: query, variableValues: variables });
}
export async function mockQueryRequest<T>(request: ExecuteQueryArg): Promise<GraphQlPayload<T>> {
fetchMock.reset();
fetchMock.restore();
let response = await executeQuery<T>(request);
fetchMock.post("*", response);
return response;
}
================================================
FILE: packages/test-utils/tsconfig.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"strict": false,
"lib": ["esnext", "dom"],
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": false,
"downlevelIteration": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"outDir": "./dist",
"module": "esnext",
"target": "esnext"
},
"include": ["src"]
}
================================================
FILE: packages/types/index.d.ts
================================================
/**
* GrafooTransport
*/
export interface GraphQlError {
message: string;
locations: { line: number; column: number }[];
path: string[];
}
/**
* T = QueryData
*/
export interface GraphQlPayload<T> {
data: T;
errors?: GraphQlError[];
}
export interface Variables {
[key: string]: any;
}
/**
* T = QueryData
*/
export type GrafooTransport = <T>(
query: string,
variables?: Variables,
id?: string
) => Promise<GraphQlPayload<T>>;
/**
* Core
*/
export interface ObjectsMap {
[key: string]: Record<string, unknown>;
}
export interface PathsMap {
[key: string]: {
data: { [key: string]: any };
objects: string[];
};
}
export type Listener = (objects: ObjectsMap) => void;
export interface InitialState {
objectsMap: ObjectsMap;
pathsMap: PathsMap;
}
export interface GrafooClient {
execute: <T>(grafooObject: GrafooObject, variables?: Variables) => Promise<GraphQlPayload<T>>;
listen: (listener: Listener) => () => void;
write: {
<T>(grafooObject: GrafooObject, variables: Variables, data: T | { data: T }): void;
<T>(grafooObject: GrafooObject, data: T | { data: T }): void;
};
read: <T>(
grafooObject: GrafooObject,
variables?: Variables
) => { data?: T; objects?: ObjectsMap; partial?: boolean };
flush: () => InitialState;
reset: () => void;
}
export interface GrafooClientOptions {
initialState?: InitialState;
idFields?: Array<string>;
}
export interface GrafooObject {
frags?: {
[key: string]: string;
};
paths: {
[key: string]: {
name: string;
args: string[];
};
};
query: string;
id?: string;
}
/**
* Bindings
*/
/**
* T = Query
* U = Mutations
*/
export interface GrafooBindings<T, U> {
getState(): GrafooBoundState & T & GrafooBoundMutations<U>;
unbind(): void;
load(variables?: Variables): void;
}
export interface GrafooBoundState {
client: GrafooClient;
errors?: GraphQlError[];
load?: (variables?: Variables) => void;
loaded?: boolean;
loading?: boolean;
}
/**
* T = Query
* U = Mutations
*/
export type UpdateFn<T, U> = (props: T, data?: U) => T;
/**
* T = Query
*/
export type OptimisticUpdateFn<T> = (props: T, variables?: Variables) => T;
/**
* T = Query
* U = Mutations
* V = keyof Mutation
*/
export type GrafooMutations<T, U> = {
[V in keyof U]: {
query: GrafooObject;
optimisticUpdate?: OptimisticUpdateFn<T>;
update?: UpdateFn<T, U[V]>;
};
};
export interface Context {
client: GrafooClient;
}
/**
* T = Mutations
* U = keyof Mutations
*/
export type GrafooBoundMutations<T> = {
[U in keyof T]: (variables?: Variables) => Promise<GraphQlPayload<T[U]>>;
};
/**
* T = Query
* U = Mutations
*/
export interface GrafooConsumerProps<T, U> {
query?: GrafooObject;
variables?: Variables;
mutations?: GrafooMutations<T, U>;
skip?: boolean;
}
================================================
FILE: packages/types/package.json
================================================
{
"name": "@grafoo/types",
"version": "1.4.2",
"description": "grafoo client typescript definitions",
"repository": "https://github.com/grafoojs/grafoo/tree/master/packages/types",
"types": "index.d.ts",
"author": "malbernaz<albernazmiguel@gmail.com>",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"gitHead": "0bc67d8b398884a1f387a1813e485d2c5318b974"
}
================================================
FILE: packages/types/readme.md
================================================
# `@grafoo/types`
<p><i>Grafoo Typescript Definitions</i></p>
<p>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://www.npmjs.com/package/@grafoo/types>
<img
src=https://img.shields.io/npm/v/@grafoo/types.svg
alt=npm
>
</a>
<a href=https://www.npmjs.com/package/@grafoo/types>
<img
src=https://img.shields.io/npm/dm/@grafoo/types.svg
alt=downloads
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
## Install
```
$ npm i @grafoo/types
```
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: readme.md
================================================
<h1 align=center>
<br />
<img src="https://raw.githubusercontent.com/grafoojs/grafoo/master/logo.svg" alt=Grafoo />
<br />
<br />
</h1>
<p align=center><i>A GraphQL Client and Toolkit</i></p>
<p align=center>
<a href=https://circleci.com/gh/grafoojs/grafoo>
<img
src=https://img.shields.io/circleci/project/github/grafoojs/grafoo/master.svg?label=build
alt=build
/>
</a>
<a href=https://codecov.io/github/grafoojs/grafoo>
<img
src=https://img.shields.io/codecov/c/github/grafoojs/grafoo/master.svg
alt="coverage"
/>
</a>
<a href=https://github.com/grafoojs/grafoo>
<img
src=https://img.shields.io/npm/v/@grafoo/babel-plugin.svg
alt=npm
>
</a>
<a href=https://github.com/grafoojs/grafoo>
<img
src=https://img.shields.io/npm/dm/@grafoo/babel-plugin.svg
alt=downloads
>
</a>
<a href=https://prettier.io>
<img
src=https://img.shields.io/badge/code_style-prettier-ff69b4.svg
alt="code style: prettier"
/>
</a>
<a href=https://lernajs.io>
<img
src=https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
alt="mantained with: lerna"
/>
</a>
<a href=https://grafoo-slack.herokuapp.com>
<img
src=https://grafoo-slack.herokuapp.com/badge.svg
alt="slack"
/>
</a>
</p>
Grafoo is a GraphQL client that tries to be different by adopting a **simpler API**, without giving up of a **good caching strategy**.
## Some useful information
- **It's a multiple paradigm library**. So far we have **view layer integrations** for [react](https://github.com/grafoojs/grafoo/tree/master/packages/react) and [preact](https://github.com/grafoojs/grafoo/tree/master/packages/preact) and there are more to come.
- **It's not just a HTTP client**. It comes with a sophisticated caching system under the hood to make sure your data is consistent across your app.
- **It's build time dependent**. A important piece of Grafoo is it's **babel** plugin that compiles your queries based on the schema your app consumes.
- **It's environment agnostic**. Apart from the browser you can run Grafoo on the **server** and even on **native** with react.
## Why should I use this
Many of the work that has been put into this project came from borrowed ideas and concepts that are present in the GraphQL clients we have today. Grafoo wants to stand apart from the others trying to be in that sweet spot between **simplicity** and **usability**. Moreover, most of the benefits this library brings to the table are related to the fact that it does a lot at build time. It's **fast**, because it spares runtime computation and it's really **small** (something like **~1.6kb** for core and react) because it does not ship with a GraphQL parser.
## Example applications
You can refer to examples in [this repository](https://github.com/grafoojs/grafoo-examples).
## Basic usage
### Installation
The basic packages you'll have to install in order to use Grafoo are core and babel-plugin.
```
$ npm i @grafoo/core && npm i -D @grafoo/babel-plugin
```
### Configure babel
In `@grafoo/babel-plugin` the option `schema` is a path to a GraphQL schema in your file system relative to the root of your project and `idFields` is an array of strings that represent the fields that Grafoo will automatically insert on your queries to build unique identifiers in order to normalize the cache. **Both options are required**.
```json
{
"plugins": [
[
"@grafoo/babel-plugin",
{
"schema": "schema.graphql",
"idFields": ["id"]
}
]
]
}
```
### Writing your app
From `@grafoo/core` you will import the factory that creates the client instance and from submodule `@grafoo/core/tag` you'll import the `graphql` or `gql` tag that will be compiled at build time.
```js
import createClient from "@grafoo/core";
import gql from "@grafoo/core/tag";
function fetchQuery(query, variables) {
const init = {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json"
}
};
return fetch("http://some.graphql.api", init).then(res => res.json());
}
const client = createClient(fetchQuery);
const USER_QUERY = gql`
query($id: ID!) {
user(id: $id) {
name
}
}
`;
const variables = { id: 123 };
client.execute(USER_QUERY, variables).then(data => {
// Write to cache
client.write(USER_QUERY, variables, data);
// Do whatever with returned data
console.log(data);
// Read from cache at a later stage
console.log(client.read(USER_QUERY, variables));
});
// If you wish to reset (clear) the cache:
client.reset();
```
### With a framework
Here is how it would go for you to write a simple react app.
#### `index.js`
```jsx
import React from "react";
import ReactDom from "react-dom";
import createClient from "@grafoo/core";
import { Provider } from "@grafoo/react";
import Posts from "./Posts";
function fetchQuery(query, variables) {
const init = {
method: "POST",
body: JSON.stringify({ query, variables }),
headers: {
"content-type": "application/json"
}
};
return fetch("http://some.graphql.api", init).then(res => res.json());
}
const client = createClient(fetchQuery);
ReactDom.render(
<Provider client={client}>
<Posts />
</Provider>,
document.getElementById("mnt")
);
```
#### `Posts.js`
```jsx
import React from "react";
import gql from "@grafoo/core/tag";
import { Consumer } from "@grafoo/react";
const ALL_POSTS = gql`
query getPosts($orderBy: PostOrderBy) {
allPosts(orderBy: $orderBy) {
title
content
createdAt
updatedAt
}
}
`;
export default function Posts() {
return (
<Consumer query={ALL_POSTS} variables={{ orderBy: "createdAt_DESC" }}>
{({ client, load, loaded, loading, errors, allPosts }) => (
<marquee>👆 do whatever you want with the variables above 👆</marquee>
)}
</Consumer>
);
}
```
### Mutations
```jsx
import React from "react";
import gql from "@grafoo/core/tag";
import { Consumer } from "@grafoo/react";
const ALL_POSTS = gql`
query getPosts($orderBy: PostOrderBy) {
allPosts(orderBy: $orderBy) {
title
content
createdAt
updatedAt
}
}
`;
const CREATE_POST = gql`
mutation createPost($content: String, $title: String, $authorId: ID) {
createPost(content: $content, title: $title, authorId: $authorId) {
title
content
createdAt
updatedAt
}
}
`;
const mutations = {
createPost: {
query: CREATE_POST,
optimisticUpdate: ({ allPosts }, variables) => ({
allPosts: [{ ...variables, id: "tempID" }, ...allPosts]
}),
update: ({ allPosts }, data) => ({
allPosts: allPosts.map(p => (p.id === "tempID" ? data.createPost : p))
})
}
};
const submit = mutate => event => {
event.preventDefault();
const { title, content } = event.target.elements;
mutate({ title: title.value, content: content.value });
};
export default function PostForm() {
return (
<Consumer query={ALL_POSTS} variables={{ orderBy: "createdAt_DESC" }} mutations={mutations}>
{({ createPost }) => (
<form onSubmit={submit(createPost)}>
<input name="title" />
<textarea name="content" />
<button>submit</button>
</form>
)}
</Consumer>
);
}
```
## LICENSE
[MIT](https://github.com/grafoojs/grafoo/blob/master/LICENSE)
================================================
FILE: scripts/build.js
================================================
let { readdirSync } = require("fs");
let { join } = require("path");
let { exec } = require("child_process");
let pkgsRoot = join(__dirname, "..", "packages");
let withDeps = ["react", "preact"];
let noDeps = readdirSync(pkgsRoot).filter((x) => !withDeps.some((y) => y === x));
let command = exec(
[
`lerna run --scope "@grafoo/*(${noDeps.join("|")})" build`,
`lerna run --scope "@grafoo/*(${withDeps.join("|")})" build`,
].join(" && ")
);
command.stdout.pipe(process.stdout);
command.stderr.pipe(process.stderr);
================================================
FILE: scripts/jest-setup.js
================================================
let fs = require("fs");
let path = require("path");
let { transform } = require("@babel/core");
let config = Object.assign(
{ sourceMap: "inline", ast: false },
JSON.parse(fs.readFileSync(path.join(process.cwd(), ".babelrc"), "utf-8"))
);
module.exports.process = (src, path) =>
transform(src, Object.assign({}, config, { filename: path }));
================================================
FILE: scripts/resolver.js
================================================
let { resolve } = require("resolve.exports");
module.exports = (request, options) =>
options.defaultResolver(request, {
...options,
packageFilter: (package) => ({
...package,
main: package.main || resolve(package, ".")
})
});
gitextract_2ic31ndv/
├── .circleci/
│ └── config.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── changelog.md
├── lerna.json
├── package.json
├── packages/
│ ├── babel-plugin/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── __snapshots__/
│ │ │ │ └── index.js.snap
│ │ │ ├── compile-document.js
│ │ │ ├── index.js
│ │ │ ├── insert-fields.js
│ │ │ ├── schema.graphql
│ │ │ └── sort-query.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── src/
│ │ ├── compile-document.js
│ │ ├── index.js
│ │ ├── insert-fields.js
│ │ └── sort-query.js
│ ├── bindings/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── bundle/
│ │ ├── cli.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── core/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── build-query-tree.ts
│ │ │ ├── index.ts
│ │ │ ├── map-objects.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ ├── build-query-tree.ts
│ │ │ ├── index.ts
│ │ │ ├── map-objects.ts
│ │ │ └── util.ts
│ │ ├── tag.d.ts
│ │ ├── tag.js
│ │ └── tsconfig.json
│ ├── http-transport/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── preact/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.tsx
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ ├── consumer.ts
│ │ │ ├── index.ts
│ │ │ └── provider.ts
│ │ └── tsconfig.json
│ ├── react/
│ │ ├── .babelrc
│ │ ├── .npmignore
│ │ ├── __tests__/
│ │ │ ├── index.tsx
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── test-utils/
│ │ ├── package.json
│ │ ├── schema.graphql
│ │ ├── src/
│ │ │ ├── db.ts
│ │ │ ├── index.ts
│ │ │ └── mock-server.ts
│ │ └── tsconfig.json
│ └── types/
│ ├── index.d.ts
│ ├── package.json
│ └── readme.md
├── readme.md
└── scripts/
├── build.js
├── jest-setup.js
└── resolver.js
SYMBOL INDEX (119 symbols across 22 files)
FILE: packages/babel-plugin/__tests__/sort-query.js
function print (line 6) | function print(query, sort = false) {
FILE: packages/babel-plugin/src/compile-document.js
function getSchema (line 10) | function getSchema(schemaPath) {
function compileDocument (line 38) | function compileDocument(source, opts) {
FILE: packages/babel-plugin/src/index.js
function transform (line 4) | function transform({ types: t }) {
FILE: packages/babel-plugin/src/insert-fields.js
function getType (line 3) | function getType(typeInfo) {
function insertField (line 9) | function insertField(selections, value) {
function insertFields (line 13) | function insertFields(schemaStr, documentAst, idFields) {
FILE: packages/babel-plugin/src/sort-query.js
function sort (line 3) | function sort(array, fn) {
function sortQuery (line 16) | function sortQuery(document) {
FILE: packages/bindings/__tests__/index.ts
type Post (line 8) | interface Post {
type Author (line 16) | interface Author {
type Authors (line 23) | interface Authors {
type CreateAuthor (line 27) | interface CreateAuthor {
type DeleteAuthor (line 36) | interface DeleteAuthor {
type UpdateAuthor (line 45) | interface UpdateAuthor {
constant AUTHORS (line 54) | let AUTHORS = graphql`
constant AUTHOR (line 66) | let AUTHOR = graphql`
constant POSTS_AND_AUTHORS (line 78) | let POSTS_AND_AUTHORS = graphql`
constant CREATE_AUTHOR (line 98) | let CREATE_AUTHOR = graphql`
constant DELETE_AUTHOR (line 110) | let DELETE_AUTHOR = graphql`
constant UPDATE_AUTHOR (line 122) | let UPDATE_AUTHOR = graphql`
type Mutations (line 277) | interface Mutations {
type Mutations (line 299) | interface Mutations {
type Mutations (line 336) | interface Mutations {
type Mutations (line 388) | interface Mutations {
type Mutations (line 426) | interface Mutations {
type Mutations (line 472) | interface Mutations {
FILE: packages/bindings/src/index.ts
function createBindings (line 10) | function createBindings<T = unknown, U = unknown>(
FILE: packages/core/__tests__/index.ts
type Post (line 6) | interface Post {
type Author (line 14) | interface Author {
type AuthorsQuery (line 21) | interface AuthorsQuery {
type PostQuery (line 25) | interface PostQuery {
type PostsAndAuthorsQuery (line 29) | interface PostsAndAuthorsQuery {
type PostsQuery (line 34) | interface PostsQuery {
constant AUTHORS (line 38) | let AUTHORS = graphql`
constant SIMPLE_AUTHORS (line 50) | let SIMPLE_AUTHORS = graphql`
constant POSTS_AND_AUTHORS (line 58) | let POSTS_AND_AUTHORS = graphql`
constant POST (line 78) | let POST = graphql`
constant POST_WITH_FRAGMENT (line 90) | let POST_WITH_FRAGMENT = graphql`
constant POSTS (line 106) | let POSTS = graphql`
function mockTrasport (line 118) | function mockTrasport<T>(query: string, variables: Variables) {
FILE: packages/core/src/build-query-tree.ts
function buildQueryTree (line 3) | function buildQueryTree(tree, objects, idFields) {
FILE: packages/core/src/index.ts
function createClient (line 14) | function createClient(
FILE: packages/core/src/map-objects.ts
function mapObjects (line 3) | function mapObjects(tree, idFields) {
FILE: packages/core/tag.js
function graphql (line 1) | function graphql() {
FILE: packages/http-transport/__tests__/index.ts
function mock (line 84) | async function mock(testFn, response?: any) {
FILE: packages/http-transport/src/index.ts
function createTransport (line 3) | function createTransport(
FILE: packages/preact/__tests__/index.tsx
type Post (line 14) | interface Post {
type Author (line 22) | interface Author {
type Authors (line 29) | interface Authors {
constant AUTHOR (line 33) | let AUTHOR = graphql`
constant AUTHORS (line 41) | let AUTHORS = graphql`
constant CREATE_AUTHOR (line 53) | let CREATE_AUTHOR = graphql`
constant POSTS_AND_AUTHORS (line 61) | let POSTS_AND_AUTHORS = graphql`
class AuthorComponent (line 247) | class AuthorComponent extends Component {
method constructor (line 248) | constructor(props, context) {
method render (line 260) | render(_, variables) {
function createMockRenderFn (line 412) | function createMockRenderFn(done, assertionsFns) {
FILE: packages/preact/src/consumer.ts
type GrafooRenderFn (line 14) | type GrafooRenderFn<T, U> = (renderProps: GrafooBoundState & T & GrafooB...
type GrafooPreactConsumerProps (line 20) | type GrafooPreactConsumerProps<T = unknown, U = unknown> = GrafooConsume...
class Consumer (line 28) | class Consumer<T = unknown, U = unknown> extends Component<GrafooPreactC...
method constructor (line 31) | constructor(props: GrafooPreactConsumerProps<T, U>, context: Context) {
method render (line 56) | render(props, state): VNode {
FILE: packages/preact/src/provider.ts
type GrafooPreactProviderProps (line 4) | type GrafooPreactProviderProps = Context & { children?: JSX.Element };
class Provider (line 6) | class Provider extends Component<GrafooPreactProviderProps> {
method getChildContext (line 7) | getChildContext(): Context {
method render (line 11) | render(props: GrafooPreactProviderProps): JSX.Element {
FILE: packages/react/__tests__/index.tsx
type Post (line 14) | interface Post {
type Author (line 22) | interface Author {
type Authors (line 29) | interface Authors {
constant AUTHORS (line 33) | let AUTHORS = graphql`
constant AUTHOR (line 45) | let AUTHOR = graphql`
constant CREATE_AUTHOR (line 53) | let CREATE_AUTHOR = graphql`
constant POSTS_AND_AUTHORS (line 61) | let POSTS_AND_AUTHORS = graphql`
class AuthorComponent (line 224) | class AuthorComponent extends React.Component {
method constructor (line 225) | constructor(props, context) {
method render (line 237) | render() {
function createMockRenderFn (line 389) | function createMockRenderFn(done, assertionsFns) {
FILE: packages/react/src/index.ts
type GrafooRenderFn (line 14) | type GrafooRenderFn<T, U> = (
type GrafooReactConsumerProps (line 22) | type GrafooReactConsumerProps<T = unknown, U = unknown> = GrafooConsumer...
type ConsumerType (line 30) | interface ConsumerType extends FC {
class GrafooConsumer (line 39) | class GrafooConsumer<T, U> extends Component<GrafooReactConsumerProps<T,...
method constructor (line 42) | constructor(props) {
method render (line 68) | render() {
FILE: packages/test-utils/src/db.ts
function setupDB (line 8) | function setupDB() {
FILE: packages/test-utils/src/mock-server.ts
method author (line 15) | author(_, args) {
method authors (line 18) | authors() {
method post (line 21) | post(_, args) {
method posts (line 24) | posts() {
method createAuthor (line 30) | createAuthor(_, args) {
method updateAuthor (line 39) | updateAuthor(_, args) {
method deleteAuthor (line 49) | deleteAuthor(_, args) {
method createPost (line 59) | createPost(_, args) {
method updatePost (line 68) | updatePost(_, args) {
method deletePost (line 78) | deletePost(_, args) {
method posts (line 90) | posts(author) {
method author (line 100) | author(post) {
type ExecuteQueryArg (line 114) | interface ExecuteQueryArg {
function executeQuery (line 121) | function executeQuery<T>({ query, variables }: ExecuteQueryArg): Promise...
function mockQueryRequest (line 126) | async function mockQueryRequest<T>(request: ExecuteQueryArg): Promise<Gr...
FILE: packages/types/index.d.ts
type GraphQlError (line 5) | interface GraphQlError {
type GraphQlPayload (line 14) | interface GraphQlPayload<T> {
type Variables (line 19) | interface Variables {
type GrafooTransport (line 26) | type GrafooTransport = <T>(
type ObjectsMap (line 36) | interface ObjectsMap {
type PathsMap (line 40) | interface PathsMap {
type Listener (line 47) | type Listener = (objects: ObjectsMap) => void;
type InitialState (line 49) | interface InitialState {
type GrafooClient (line 54) | interface GrafooClient {
type GrafooClientOptions (line 69) | interface GrafooClientOptions {
type GrafooObject (line 74) | interface GrafooObject {
type GrafooBindings (line 96) | interface GrafooBindings<T, U> {
type GrafooBoundState (line 102) | interface GrafooBoundState {
type UpdateFn (line 114) | type UpdateFn<T, U> = (props: T, data?: U) => T;
type OptimisticUpdateFn (line 119) | type OptimisticUpdateFn<T> = (props: T, variables?: Variables) => T;
type GrafooMutations (line 126) | type GrafooMutations<T, U> = {
type Context (line 134) | interface Context {
type GrafooBoundMutations (line 142) | type GrafooBoundMutations<T> = {
type GrafooConsumerProps (line 150) | interface GrafooConsumerProps<T, U> {
Condensed preview — 92 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (201K chars).
[
{
"path": ".circleci/config.yml",
"chars": 706,
"preview": "version: 2\njobs:\n build:\n docker:\n - image: cimg/node:14.15.1\n\n steps:\n - checkout\n\n - restore_cac"
},
{
"path": ".gitignore",
"chars": 1938,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directo"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3356,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 2362,
"preview": "# Contributing\n\n## Not sure where to start?\n\nIf you're not sure where to start? You'll probably want to learn a bit abou"
},
{
"path": "LICENSE",
"chars": 1099,
"preview": "MIT License\n\nCopyright (c) 2018 Miguel Albernaz <albernazmiguel@gmail.com>\n\nPermission is hereby granted, free of charge"
},
{
"path": "changelog.md",
"chars": 4415,
"preview": "# CHANGELOG\n\n## 1.4.0\n\n### Features\n\n- [babel-plugin, core] adds an option to babel-plugin to generate an id side by sid"
},
{
"path": "lerna.json",
"chars": 226,
"preview": "{\n \"packages\": [\n \"packages/*\"\n ],\n \"npmClient\": \"yarn\",\n \"useWorkspaces\": true,\n \"version\": \"1.4.2\",\n \"command"
},
{
"path": "package.json",
"chars": 2791,
"preview": "{\n \"private\": true,\n \"name\": \"grafoo\",\n \"description\": \"a graphql client and toolkit\",\n \"repository\": \"https://githu"
},
{
"path": "packages/babel-plugin/.babelrc",
"chars": 71,
"preview": "{\n \"presets\": [[\"@babel/preset-env\", { \"targets\": { \"node\": 4 } }]]\n}\n"
},
{
"path": "packages/babel-plugin/.npmignore",
"chars": 55,
"preview": "coverage\n__tests__\n.rpt2_cache\n.babelrc\nschema.graphql\n"
},
{
"path": "packages/babel-plugin/__tests__/__snapshots__/index.js.snap",
"chars": 7006,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`@grafoo/babel-plugin should compress the query string if the option"
},
{
"path": "packages/babel-plugin/__tests__/compile-document.js",
"chars": 1698,
"preview": "import * as babel from \"@babel/core\";\nimport plugin from \"../src\";\n\nlet transform = (program, opts) =>\n babel.transform"
},
{
"path": "packages/babel-plugin/__tests__/index.js",
"chars": 5974,
"preview": "import pluginTester from \"babel-plugin-tester\";\nimport plugin from \"../src\";\n\npluginTester({\n plugin,\n pluginName: \"@g"
},
{
"path": "packages/babel-plugin/__tests__/insert-fields.js",
"chars": 5281,
"preview": "import fs from \"fs\";\nimport { parse, print } from \"graphql\";\nimport path from \"path\";\nimport insertFields from \"../src/i"
},
{
"path": "packages/babel-plugin/__tests__/schema.graphql",
"chars": 1181,
"preview": "type Mutation {\n createPost(title: String!, body: String!, authors: [ID!]!, tags: [String!]): Post\n deletePost(id: ID)"
},
{
"path": "packages/babel-plugin/__tests__/sort-query.js",
"chars": 2964,
"preview": "import { parse, print as graphqlPrint } from \"graphql\";\nimport sortQuery from \"../src/sort-query\";\n\nlet gql = String.raw"
},
{
"path": "packages/babel-plugin/package.json",
"chars": 940,
"preview": "{\n \"name\": \"@grafoo/babel-plugin\",\n \"version\": \"1.4.2\",\n \"description\": \"grafoo client babel plugin\",\n \"repository\":"
},
{
"path": "packages/babel-plugin/readme.md",
"chars": 4152,
"preview": "# `@grafoo/babel-plugin`\n\n<p><i>Grafoo Babel Plugin</i></p>\n\n<p>\n <a href=https://circleci.com/gh/grafoojs/grafoo>\n "
},
{
"path": "packages/babel-plugin/src/compile-document.js",
"chars": 2584,
"preview": "import fs from \"fs\";\nimport { parse, print } from \"graphql\";\nimport compress from \"graphql-query-compress\";\nimport md5Ha"
},
{
"path": "packages/babel-plugin/src/index.js",
"chars": 4983,
"preview": "import parseLiteral from \"babel-literal-to-ast\";\nimport compileDocument from \"./compile-document\";\n\nexport default funct"
},
{
"path": "packages/babel-plugin/src/insert-fields.js",
"chars": 1932,
"preview": "import { TypeInfo, buildASTSchema, parse, visit, visitWithTypeInfo } from \"graphql\";\n\nfunction getType(typeInfo) {\n let"
},
{
"path": "packages/babel-plugin/src/sort-query.js",
"chars": 1171,
"preview": "import { visit } from \"graphql\";\n\nfunction sort(array, fn) {\n fn = fn || ((obj) => obj.name.value);\n\n return (\n arr"
},
{
"path": "packages/bindings/.babelrc",
"chars": 219,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\", { \"targets\": { \"node\": \"current\" } }],\n \"@babel/preset-typescript\"\n ],\n "
},
{
"path": "packages/bindings/.npmignore",
"chars": 55,
"preview": "coverage\n__tests__\n.rpt2_cache\n.babelrc\nschema.graphql\n"
},
{
"path": "packages/bindings/__tests__/index.ts",
"chars": 14686,
"preview": "import createBindings from \"../src\";\nimport graphql from \"@grafoo/core/tag\";\nimport createClient from \"@grafoo/core\";\nim"
},
{
"path": "packages/bindings/__tests__/tsconfig.json",
"chars": 51,
"preview": "{\n \"extends\": \"../tsconfig\",\n \"include\": [\".\"]\n}\n"
},
{
"path": "packages/bindings/package.json",
"chars": 1011,
"preview": "{\n \"name\": \"@grafoo/bindings\",\n \"version\": \"1.4.2\",\n \"description\": \"grafoo client internal helper for building frame"
},
{
"path": "packages/bindings/readme.md",
"chars": 6996,
"preview": "# `@grafoo/bindings`\n\n<p><i>Grafoo Bindings for Frameworks</i></p>\n\n<p>\n <a href=https://circleci.com/gh/grafoojs/grafo"
},
{
"path": "packages/bindings/schema.graphql",
"chars": 530,
"preview": "type Query {\n author(id: ID!): Author!\n authors: [Author!]!\n post(id: ID!): Post!\n posts: [Post!]!\n}\n\ntype Mutation "
},
{
"path": "packages/bindings/src/index.ts",
"chars": 2901,
"preview": "import {\n GrafooClient,\n GrafooBindings,\n GrafooBoundMutations,\n GrafooConsumerProps,\n ObjectsMap,\n Variables,\n} f"
},
{
"path": "packages/bindings/tsconfig.json",
"chars": 249,
"preview": "{\n \"compilerOptions\": {\n \"moduleResolution\": \"node\",\n \"strict\": false,\n \"lib\": [\"esnext\", \"dom\"],\n \"noUnuse"
},
{
"path": "packages/bundle/cli.js",
"chars": 264,
"preview": "#!/usr/bin/env node\n\n/* eslint-disable no-console */\n\nvar mri = require(\"mri\");\nvar build = require(\".\");\n\nvar opts = mr"
},
{
"path": "packages/bundle/index.js",
"chars": 1805,
"preview": "var fs = require(\"fs\");\nvar path = require(\"path\");\nvar rollup = require(\"rollup\").rollup;\nvar buble = require(\"rollup-p"
},
{
"path": "packages/bundle/package.json",
"chars": 452,
"preview": "{\n \"name\": \"grafoo-bundle\",\n \"version\": \"1.4.2\",\n \"bin\": \"cli.js\",\n \"main\": \"index.js\",\n \"dependencies\": {\n \"mri"
},
{
"path": "packages/bundle/readme.md",
"chars": 452,
"preview": "# `grafoo-bundle`\n\n**This is and internal cli tool for [Grafoo](https://github.com/grafoojs/grafoo) and it's not meant t"
},
{
"path": "packages/core/.babelrc",
"chars": 259,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\", { \"targets\": { \"node\": \"current\" } }],\n \"@babel/preset-typescript\"\n ],\n "
},
{
"path": "packages/core/.npmignore",
"chars": 55,
"preview": "coverage\n__tests__\n.rpt2_cache\n.babelrc\nschema.graphql\n"
},
{
"path": "packages/core/__tests__/build-query-tree.ts",
"chars": 1866,
"preview": "import buildQueryTree from \"../src/build-query-tree\";\n\nlet tree = {\n posts: [\n {\n title: \"foo\",\n id: \"1\",\n"
},
{
"path": "packages/core/__tests__/index.ts",
"chars": 10518,
"preview": "import graphql from \"@grafoo/core/tag\";\nimport { executeQuery } from \"@grafoo/test-utils\";\nimport { GrafooClient, Variab"
},
{
"path": "packages/core/__tests__/map-objects.ts",
"chars": 2218,
"preview": "import mapObjects from \"../src/map-objects\";\n\nlet tree = {\n posts: [\n {\n title: \"foo\",\n id: \"1\",\n __t"
},
{
"path": "packages/core/__tests__/tsconfig.json",
"chars": 51,
"preview": "{\n \"extends\": \"../tsconfig\",\n \"include\": [\".\"]\n}\n"
},
{
"path": "packages/core/package.json",
"chars": 960,
"preview": "{\n \"name\": \"@grafoo/core\",\n \"version\": \"1.4.2\",\n \"description\": \"grafoo client core\",\n \"repository\": \"https://github"
},
{
"path": "packages/core/readme.md",
"chars": 7721,
"preview": "# `@grafoo/core`\n\n<p><i>Grafoo core</i></p>\n\n<p>\n <a href=https://circleci.com/gh/grafoojs/grafoo>\n <img\n src=h"
},
{
"path": "packages/core/schema.graphql",
"chars": 530,
"preview": "type Query {\n author(id: ID!): Author!\n authors: [Author!]!\n post(id: ID!): Post!\n posts: [Post!]!\n}\n\ntype Mutation "
},
{
"path": "packages/core/src/build-query-tree.ts",
"chars": 1303,
"preview": "import { idFromProps, isNotNullObject } from \"./util\";\n\nexport default function buildQueryTree(tree, objects, idFields) "
},
{
"path": "packages/core/src/index.ts",
"chars": 2893,
"preview": "import {\n GrafooClient,\n GrafooClientOptions,\n GrafooObject,\n Listener,\n ObjectsMap,\n Variables,\n GrafooTransport"
},
{
"path": "packages/core/src/map-objects.ts",
"chars": 1157,
"preview": "import { isNotNullObject, idFromProps } from \"./util\";\n\nexport default function mapObjects(tree, idFields) {\n // map in"
},
{
"path": "packages/core/src/util.ts",
"chars": 590,
"preview": "import { Variables } from \"@grafoo/types\";\n\nexport let idFromProps = (branch, idFields) => {\n branch = branch || {};\n "
},
{
"path": "packages/core/tag.d.ts",
"chars": 164,
"preview": "declare module \"@grafoo/core/tag\" {\n import { GrafooObject } from \"@grafoo/types\";\n\n export default function graphql(s"
},
{
"path": "packages/core/tag.js",
"chars": 209,
"preview": "function graphql() {\n throw new Error(\n \"@grafoo/core/tag: if you are getting this error it means your queries are n"
},
{
"path": "packages/core/tsconfig.json",
"chars": 249,
"preview": "{\n \"compilerOptions\": {\n \"moduleResolution\": \"node\",\n \"strict\": false,\n \"lib\": [\"esnext\", \"dom\"],\n \"noUnuse"
},
{
"path": "packages/http-transport/.babelrc",
"chars": 119,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\", { \"targets\": { \"node\": \"current\" } }],\n \"@babel/preset-typescript\"\n ]\n}\n"
},
{
"path": "packages/http-transport/.npmignore",
"chars": 55,
"preview": "coverage\n__tests__\n.rpt2_cache\n.babelrc\nschema.graphql\n"
},
{
"path": "packages/http-transport/__tests__/index.ts",
"chars": 2405,
"preview": "/* eslint-disable @typescript-eslint/no-var-requires */\n\nimport { GrafooTransport } from \"@grafoo/types\";\nimport createT"
},
{
"path": "packages/http-transport/__tests__/tsconfig.json",
"chars": 51,
"preview": "{\n \"extends\": \"../tsconfig\",\n \"include\": [\".\"]\n}\n"
},
{
"path": "packages/http-transport/package.json",
"chars": 987,
"preview": "{\n \"name\": \"@grafoo/http-transport\",\n \"description\": \"grafoo client standard transport\",\n \"version\": \"1.4.2\",\n \"repo"
},
{
"path": "packages/http-transport/readme.md",
"chars": 2275,
"preview": "# `@grafoo/http-transport`\n\n<p><i>A Simple HTTP Client for GraphQL Servers</i></p>\n\n<p>\n <a href=https://circleci.com/g"
},
{
"path": "packages/http-transport/src/index.ts",
"chars": 641,
"preview": "import { GraphQlPayload, GrafooTransport, Variables } from \"@grafoo/types\";\n\nexport default function createTransport(\n "
},
{
"path": "packages/http-transport/tsconfig.json",
"chars": 249,
"preview": "{\n \"compilerOptions\": {\n \"moduleResolution\": \"node\",\n \"strict\": false,\n \"lib\": [\"esnext\", \"dom\"],\n \"noUnuse"
},
{
"path": "packages/preact/.babelrc",
"chars": 414,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\", { \"targets\": { \"node\": 10 } }],\n [\"@babel/preset-react\", { \"pragma\": \"h\" }"
},
{
"path": "packages/preact/.npmignore",
"chars": 55,
"preview": "coverage\n__tests__\n.rpt2_cache\n.babelrc\nschema.graphql\n"
},
{
"path": "packages/preact/__tests__/index.tsx",
"chars": 11506,
"preview": "/**\n * @jest-environment jsdom\n */\n\nimport createClient from \"@grafoo/core\";\nimport graphql from \"@grafoo/core/tag\";\nimp"
},
{
"path": "packages/preact/__tests__/tsconfig.json",
"chars": 51,
"preview": "{\n \"extends\": \"../tsconfig\",\n \"include\": [\".\"]\n}\n"
},
{
"path": "packages/preact/package.json",
"chars": 1090,
"preview": "{\n \"name\": \"@grafoo/preact\",\n \"version\": \"1.4.2\",\n \"description\": \"grafoo client preact bindings\",\n \"repository\": \"h"
},
{
"path": "packages/preact/readme.md",
"chars": 2923,
"preview": "# `@grafoo/preact`\n\n<p><i>Grafoo Preact Bindings</i></p>\n\n<p>\n <a href=https://circleci.com/gh/grafoojs/grafoo>\n <im"
},
{
"path": "packages/preact/schema.graphql",
"chars": 530,
"preview": "type Query {\n author(id: ID!): Author!\n authors: [Author!]!\n post(id: ID!): Post!\n posts: [Post!]!\n}\n\ntype Mutation "
},
{
"path": "packages/preact/src/consumer.ts",
"chars": 1439,
"preview": "import createBindings from \"@grafoo/bindings\";\nimport {\n Context,\n GrafooBoundState,\n GrafooBoundMutations,\n GrafooC"
},
{
"path": "packages/preact/src/index.ts",
"chars": 56,
"preview": "export * from \"./provider\";\nexport * from \"./consumer\";\n"
},
{
"path": "packages/preact/src/provider.ts",
"chars": 391,
"preview": "import { Context } from \"@grafoo/types\";\nimport { Component } from \"preact\";\n\ntype GrafooPreactProviderProps = Context &"
},
{
"path": "packages/preact/tsconfig.json",
"chars": 325,
"preview": "{\n \"compilerOptions\": {\n \"moduleResolution\": \"node\",\n \"strict\": false,\n \"noUnusedLocals\": true,\n \"noUnusedP"
},
{
"path": "packages/react/.babelrc",
"chars": 265,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\", { \"targets\": { \"node\": 10 } }],\n \"@babel/preset-react\",\n \"@babel/preset"
},
{
"path": "packages/react/.npmignore",
"chars": 55,
"preview": "coverage\n__tests__\n.rpt2_cache\n.babelrc\nschema.graphql\n"
},
{
"path": "packages/react/__tests__/index.tsx",
"chars": 10748,
"preview": "/**\n * @jest-environment jsdom\n */\n\nimport createClient from \"@grafoo/core\";\nimport graphql from \"@grafoo/core/tag\";\nimp"
},
{
"path": "packages/react/__tests__/tsconfig.json",
"chars": 51,
"preview": "{\n \"extends\": \"../tsconfig\",\n \"include\": [\".\"]\n}\n"
},
{
"path": "packages/react/package.json",
"chars": 1112,
"preview": "{\n \"name\": \"@grafoo/react\",\n \"version\": \"1.4.2\",\n \"description\": \"grafoo client react bindings\",\n \"repository\": \"htt"
},
{
"path": "packages/react/readme.md",
"chars": 9693,
"preview": "# `@grafoo/react`\n\n<p><i>Grafoo React Bindings</i></p>\n\n<p>\n <a href=https://circleci.com/gh/grafoojs/grafoo>\n <img\n"
},
{
"path": "packages/react/schema.graphql",
"chars": 530,
"preview": "type Query {\n author(id: ID!): Author!\n authors: [Author!]!\n post(id: ID!): Post!\n posts: [Post!]!\n}\n\ntype Mutation "
},
{
"path": "packages/react/src/index.ts",
"chars": 1909,
"preview": "import createBindings from \"@grafoo/bindings\";\nimport {\n Context,\n GrafooConsumerProps,\n GrafooBoundState,\n GrafooBo"
},
{
"path": "packages/react/tsconfig.json",
"chars": 269,
"preview": "{\n \"compilerOptions\": {\n \"moduleResolution\": \"node\",\n \"strict\": false,\n \"lib\": [\"esnext\", \"dom\"],\n \"noUnuse"
},
{
"path": "packages/test-utils/package.json",
"chars": 191,
"preview": "{\n \"private\": true,\n \"name\": \"@grafoo/test-utils\",\n \"version\": \"1.4.2\",\n \"main\": \"dist/index.js\",\n \"publishConfig\":"
},
{
"path": "packages/test-utils/schema.graphql",
"chars": 530,
"preview": "type Query {\n author(id: ID!): Author!\n authors: [Author!]!\n post(id: ID!): Post!\n posts: [Post!]!\n}\n\ntype Mutation "
},
{
"path": "packages/test-utils/src/db.ts",
"chars": 874,
"preview": "import casual from \"casual\";\nimport { Low, Memory } from \"lowdb\";\n\ncasual.seed(666);\n\nlet times = (t: number, fn: (i: nu"
},
{
"path": "packages/test-utils/src/index.ts",
"chars": 53,
"preview": "export * from \"./db\";\nexport * from \"./mock-server\";\n"
},
{
"path": "packages/test-utils/src/mock-server.ts",
"chars": 2827,
"preview": "import { GraphQlPayload } from \"@grafoo/types\";\nimport fetchMock from \"fetch-mock\";\nimport fs from \"fs\";\nimport { graphq"
},
{
"path": "packages/test-utils/tsconfig.json",
"chars": 388,
"preview": "{\n \"compilerOptions\": {\n \"moduleResolution\": \"node\",\n \"strict\": false,\n \"lib\": [\"esnext\", \"dom\"],\n \"noUnuse"
},
{
"path": "packages/types/index.d.ts",
"chars": 2853,
"preview": "/**\n * GrafooTransport\n */\n\nexport interface GraphQlError {\n message: string;\n locations: { line: number; column: numb"
},
{
"path": "packages/types/package.json",
"chars": 392,
"preview": "{\n \"name\": \"@grafoo/types\",\n \"version\": \"1.4.2\",\n \"description\": \"grafoo client typescript definitions\",\n \"repositor"
},
{
"path": "packages/types/readme.md",
"chars": 1314,
"preview": "# `@grafoo/types`\n\n<p><i>Grafoo Typescript Definitions</i></p>\n\n<p>\n <a href=https://circleci.com/gh/grafoojs/grafoo>\n "
},
{
"path": "readme.md",
"chars": 7486,
"preview": "<h1 align=center>\n <br />\n <img src=\"https://raw.githubusercontent.com/grafoojs/grafoo/master/logo.svg\" alt=Grafoo />\n"
},
{
"path": "scripts/build.js",
"chars": 530,
"preview": "let { readdirSync } = require(\"fs\");\nlet { join } = require(\"path\");\nlet { exec } = require(\"child_process\");\n\nlet pkgsR"
},
{
"path": "scripts/jest-setup.js",
"chars": 350,
"preview": "let fs = require(\"fs\");\nlet path = require(\"path\");\nlet { transform } = require(\"@babel/core\");\n\nlet config = Object.ass"
},
{
"path": "scripts/resolver.js",
"chars": 255,
"preview": "let { resolve } = require(\"resolve.exports\");\n\nmodule.exports = (request, options) =>\n options.defaultResolver(request,"
}
]
About this extraction
This page contains the full source code of the grafoojs/grafoo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 92 files (181.2 KB), approximately 49.9k tokens, and a symbol index with 119 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.