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 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 ", "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", "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)$": "/../../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`

Grafoo Babel Plugin

build coverage npm downloads code style: prettier mantained with: lerna slack

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; } interface Authors { authors: Author[]; } interface CreateAuthor { createAuthor: { name: string; id: string; __typename: string; posts?: Array; }; } interface DeleteAuthor { deleteAuthor: { name: string; id: string; __typename: string; posts?: Array; }; } interface UpdateAuthor { updateAuthor: { name: string; id: string; __typename: string; posts?: Array; }; } 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); let renderFn = jest.fn(); let bindings = createBindings(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); let renderFn = jest.fn(); let bindings = createBindings(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); client.write(AUTHORS, data); let bindings = createBindings(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); client.write(AUTHORS, data); let bindings = createBindings(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); let renderFn = jest.fn(); let bindings = createBindings(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); client.write(AUTHORS, data); let renderFn = jest.fn(); let bindings = createBindings(client, { query: AUTHORS }, renderFn); expect(bindings.getState()).toMatchObject(data); }); it("should stop updating if unbind has been called", async () => { let { data } = await mockQueryRequest(AUTHORS); let renderFn = jest.fn(); let bindings = createBindings(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).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 = { 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); interface Mutations { createAuthor: CreateAuthor; } let mutations: GrafooMutations = { 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({ 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 = { 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({ 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({ query, variables: { name: "gustav" } })) .data.createAuthor; let { data } = await mockQueryRequest(AUTHORS); client.write(AUTHORS, data); interface Mutations { removeAuthor: DeleteAuthor; } let mutations: GrafooMutations = { 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({ query, variables: { name: "sven" }, }) ).data.createAuthor; let { data } = await mockQueryRequest(AUTHORS); client.write(AUTHORS, data); interface Mutations { updateAuthor: UpdateAuthor; } let mutations: GrafooMutations = { 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 = { 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({ 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); 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", "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)$": "/../../scripts/jest-setup.js" }, "resolver": "/../../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`

Grafoo Bindings for Frameworks

build coverage npm downloads code style: prettier mantained with: lerna slack

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; ``` ### 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( client: GrafooClient, props: GrafooConsumerProps, updater: () => void ): GrafooBindings { let { variables } = props; let data: T; let objects: ObjectsMap; let boundMutations = {} as GrafooBoundMutations; let unbind = () => {}; let lockListenUpdate = 0; let loaded = false; let partial = false; if (props.query) { ({ data, objects, partial } = client.read(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(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(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(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; } 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(query: string, variables: Variables) { return executeQuery({ 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(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(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(AUTHORS); client.write(AUTHORS, data); let result = client.read(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({ query: POST.query, variables }); client.write(POST, variables, data); expect(client.read(POST, { postId: "123" })).toEqual({}); expect(client.read(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({ query: POST.query, variables: v1 }); client.write(POST, v1, d1); expect(client.read(POST, { postId: "not found" })).toEqual({}); expect(client.read(POST, v1).data.post.id).toBe(v1.postId); let d2 = await executeQuery({ query: POST.query, variables: v2 }); client.write(POST, v2, d2); expect(client.read(POST, v1).data.post.id).toBe(v1.postId); expect(client.read(POST, v2).data.post.id).toBe(v2.postId); }); it("should flag if a query result is partial", async () => { let { data } = await executeQuery({ query: POSTS.query }); client.write(POSTS, data); expect(client.read(POSTS_AND_AUTHORS).partial).toBe(true); }); it("should remove unused objects from objectsMap", async () => { let { data } = await executeQuery(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({ query: POST.query, variables }); client.write(POST, variables, data); let { data: { post } } = client.read(POST, variables); expect(post.title).toBe("Quam odit"); client.write(POST, variables, { post: { ...post, title: "updated title" } }); expect(client.read(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({ query: POST.query, variables })).data; let postsData = (await executeQuery({ query: POSTS.query, variables })).data; client.write(POSTS, postsData); let { posts } = client.read(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(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({ query: POST.query, variables })).data; client.write(POST, variables, data); let post = JSON.parse(JSON.stringify(client.read(POST, variables).data.post)); delete post.__typename; post.foo = "bar"; client.write(POST, variables, { post }); expect(client.read(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({ 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", "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)$": "/../../scripts/jest-setup.js" }, "resolver": "/../../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`

Grafoo core

build coverage npm downloads size code style: prettier mantained with: lerna slack

## 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(``); }); // 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({ query, frags, id }: GrafooObject, variables?: Variables) { if (frags) for (let frag in frags) query += frags[frag]; return transport(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({ 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", "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)$": "/../../scripts/jest-setup.js" }, "resolver": "/../../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`

A Simple HTTP Client for GraphQL Servers

build coverage npm downloads size code style: prettier mantained with: lerna slack

## 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 (query: string, variables?: Variables): Promise> => { 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; } 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("", () => { it("should provide the client in it's context", (done) => { let Comp = (_, context) => { expect(context.client).toBe(client); return null; }; render( ); done(); }); }); describe("", () => { it("should not crash if a query is not given as prop", () => { expect(() => render( {() => null} ) ).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( {() => null} ); expect(spy).not.toHaveBeenCalled(); }); it("should trigger listen on client instance", async () => { await mockQueryRequest(AUTHORS); let spy = jest.spyOn(client, "listen"); render( {() => null} ); expect(spy).toHaveBeenCalled(); }); it("should not crash on unmount", () => { let ctx = render( {() => null} ); expect(() => ctx.render(null)).not.toThrow(); }); it("should execute render with default render argument", () => { let mockRender = jest.fn(); render( {mockRender} ); 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).then(({ data }) => { let mockRender = createMockRenderFn(done, [ (props) => expect(props).toMatchObject({ loading: true, loaded: false }), (props) => expect(props).toMatchObject({ loading: false, loaded: true, ...data }) ]); render( {mockRender} ); }); }); it("should render if skip changed value to true", (done) => { mockQueryRequest(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 }) => ( {mockRender} ); let ctx = render(); ctx.render(); await new Promise((resolve) => setTimeout(resolve, 10)); ctx.render(); }); }); it("should rerender if variables prop has changed", (done) => { mockQueryRequest(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 ( {mockRender} ); } } render( ); }); }); it("should not trigger a network request if the query is already cached", (done) => { mockQueryRequest(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( {mockRender} ); 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( {mockRender} ); }); }); it("should handle mutations with cache update", (done) => { mockQueryRequest(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( ({ authors: [...authors, { ...variables, id: "tempID" }] }), update: ({ authors }, { createAuthor: author }) => ({ authors: authors.map((a) => (a.id === "tempID" ? author : a)) }) } }} > {mockRender} ); }); }); it("should reflect updates that happen outside of the component", (done) => { mockQueryRequest(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( {mockRender} ); 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(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( {mockRender} ); }); }); }); }); 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", "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)$": "/../../scripts/jest-setup.js" }, "resolver": "/../../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`

Grafoo Preact Bindings

build coverage npm downloads size code style: prettier mantained with: lerna slack

## 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( , 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 ( {({ client, load, loaded, loading, errors, allPosts }) => (

👆 do whatever you want with the variables above 👆

)}
); } ``` ## 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 = (renderProps: GrafooBoundState & T & GrafooBoundMutations) => VNode; /** * T = Query * U = Mutations */ type GrafooPreactConsumerProps = GrafooConsumerProps & { children?: GrafooRenderFn; }; /** * T = Query * U = Mutations */ export class Consumer extends Component> { state: GrafooBoundState & T & GrafooBoundMutations; constructor(props: GrafooPreactConsumerProps, context: Context) { super(props, context); let bindings = createBindings(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 { 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; } 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( {() => null} ) ).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( {() => null} ); expect(spy).not.toHaveBeenCalled(); }); it("should trigger listen on client instance", async () => { await mockQueryRequest(AUTHORS); let spy = jest.spyOn(client, "listen"); TestRenderer.create( {() => null} ); expect(spy).toHaveBeenCalled(); }); it("should not crash on unmount", () => { let testRenderer = TestRenderer.create( {() => null} ); expect(() => testRenderer.unmount()).not.toThrow(); }); it("should execute render with default render argument", () => { let mockRender = jest.fn().mockReturnValue(null); TestRenderer.create( {mockRender} ); 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).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( {mockRender} ); }); }); it("should render if skip changed value to true", (done) => { mockQueryRequest(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 }) => ( {mockRender} ); let ctx = TestRenderer.create(); ctx.update(); await new Promise((resolve) => setTimeout(resolve, 10)); ctx.update(); }); }); it("should rerender if variables prop has changed", (done) => { mockQueryRequest(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 ( {mockRender} ); } } TestRenderer.create( ); }); }); it("should not trigger a network request if the query is already cached", (done) => { mockQueryRequest(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( {mockRender} ); 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( {mockRender} ); }); }); it("should handle mutations with cache update", (done) => { mockQueryRequest(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( ({ authors: [...authors, { ...variables, id: "tempID" }] }), update: ({ authors }, data) => ({ authors: authors.map((a) => (a.id === "tempID" ? (data as any).createAuthor : a)) }) } }} > {mockRender} ); }); }); it("should reflect updates that happen outside of the component", (done) => { mockQueryRequest(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( {mockRender} ); 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(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( {mockRender} ); }); }); }); 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", "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)$": "/../../scripts/jest-setup.js" }, "resolver": "/../../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`

Grafoo React Bindings

build coverage npm downloads size code style: prettier mantained with: lerna slack

## 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 ( ); } ``` ### `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 ( {({ client, load, loaded, loading, errors, allPosts }) => (

👆 do whatever you want with the variables above 👆

)}
); } ``` ### 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; ``` ### 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 ( {({ createPost }) => (