├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── src ├── formatters │ ├── connect.test.ts │ ├── connect.ts │ ├── create.test.ts │ ├── create.ts │ ├── save.test.ts │ └── save.ts ├── index.ts ├── parseFormToMutation.test.ts ├── parseFormToMutation.ts ├── parseQueryToForm.test.ts └── parseQueryToForm.ts ├── tsconfig.json ├── tslint.json ├── types.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: 4 | node_js 5 | 6 | node_js: 7 | - '8' 8 | 9 | cache: 10 | yarn: true 11 | directories: 12 | - node_modules 13 | 14 | script: 15 | npm run ci 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Pull requests and issues are very welcome! 2 | 3 | # Prerequisites 4 | 5 | * Node v8+ 6 | 7 | # Getting started 8 | 9 | To get started, clone this repository and run `yarn` or `npm i`. 10 | 11 | After that, you can run the tests with `yarn test`. 12 | 13 | # Adding a feature or changing behavior 14 | 15 | For features or changes, please create a new issue first. Since this is a very opinionated package, it is possible I don’t like the change. By discussing it first you can prevent wasted time. But please do! I am very open to improvements. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, Volst 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Form Helpers 2 | 3 | [![codecov](https://codecov.io/gh/Volst/graphql-form-helpers/branch/master/graph/badge.svg)](https://codecov.io/gh/Volst/graphql-form-helpers) 4 | 5 | A light-weight (1kb) package for dealing with complicated forms that have nested data and use GraphQL. 6 | 7 | We use it in combination with [Formik](https://github.com/jaredpalmer/formik), [Apollo Client](https://github.com/apollographql/apollo-client) and [Prisma](https://www.prisma.io/), but it is not specific to one of those. 8 | 9 | **Features:** 10 | 11 | * Convert data from a form to a GraphQL mutation ([docs](#from-form-to-mutation)) 12 | * Convert data from a GraphQL query to suitable form data ([docs](#from-query-to-form)) 13 | 14 | # Motivation 15 | 16 | When dealing with GraphQL you often have to write boilerplate code to load the fetched data in a form and some code so the form data becomes a correct mutation. It might not be so much boilerplate if you have a basic form, but it can quickly become a lot for more complicated forms with nested data. This package intents to reduce that boilerplate code. 17 | 18 | # Install 19 | 20 | Install with Yarn or npm: 21 | 22 | ``` 23 | yarn add @volst/graphql-form-helpers 24 | npm i @volst/graphql-form-helpers 25 | ``` 26 | 27 | # Usage 28 | 29 | ## From form to mutation 30 | 31 | Imagine you have a form which, when the `onSubmit` event is called, outputs this data: 32 | 33 | ```js 34 | const data = { 35 | restaurant: 'kjrrqxy08001309', 36 | categories: [ 37 | { 38 | id: 'dgfrqxfaf000v', 39 | name: 'Drinks' 40 | }, 41 | { 42 | name: 'Burgers' 43 | } 44 | ] 45 | }; 46 | ``` 47 | 48 | First I'll explain what's going on here: 49 | 50 | * The `restaurant` field refers to an `ID`, so it already exists and should be connected to the given restaurant. 51 | * The `categories` field is an array of categories, the first one already exists (since it has an `id` field), and the second one doesn't exist yet. 52 | 53 | Now we need to write a mutation which saves this data to your backend. Your GraphQL scheme probably looks different from this `data` scheme. For example, if you use [Prisma](https://www.prisma.io/), your mutation data would need to look like this: 54 | 55 | ```js 56 | const graphqlData = { 57 | restaurant: { connect: { id: 'kjrrqxy08001309' } }, 58 | categories: { 59 | create: [ 60 | { 61 | name: 'Burgers' 62 | } 63 | ], 64 | update: [ 65 | { 66 | where: { id: 'dgfrqxfaf000v' }, 67 | data: { name: 'Drinks' } 68 | } 69 | ] 70 | } 71 | }; 72 | ``` 73 | 74 | As you can see this is a lot different to the data we have above. You could write some custom code everytime to make it look the same, but I'm already sweating even thinking about that. That's where `parseFormToMutation` comes in: 75 | 76 | ```js 77 | import { 78 | create, 79 | connect, 80 | save, 81 | parseFormToMutation 82 | } from '@volst/graphql-form-helpers'; 83 | 84 | const scheme = { 85 | restaurant: connect, 86 | categories: save 87 | }; 88 | 89 | const graphqlData = parseFormToMutation(values, scheme); 90 | ``` 91 | 92 | But what if you have nested data? Imagine that a category can have items and subitems, the schema would look like this: 93 | 94 | ```js 95 | const scheme = { 96 | restaurant: connect, 97 | categories: { 98 | __format: save, 99 | items: { 100 | __format: save, 101 | subitems: save 102 | } 103 | } 104 | }; 105 | ``` 106 | 107 | > The `__format` property applies the formatter (`save`) on the parent property. 108 | 109 | As you can see, it is now very easy to mutate nested data, even if it's an array of objects. 110 | 111 | Currently there are three formatters out of the box: 112 | 113 | * `connect` - wraps an object around a `connect` mutation. 114 | * `create` - wraps an object around a `create` mutation. 115 | * `save` - wraps an object around an `update` mutation if there is an `id` field, or `create` if there is none. 116 | 117 | > **Psst, want to see a [real-world example](https://github.com/Volst/new-food-order/blob/64a8ccd7c7ffd437016d88d1fe394bb53739e636/frontend/src/screen/RestaurantCardEdit.tsx#L55-L80)?** 118 | 119 | ### Writing a custom formatter 120 | 121 | Writing a custom formatter is very easy! 122 | 123 | ```js 124 | function decimalToFloat(value: string | number) { 125 | return parseFloat(value); 126 | } 127 | 128 | const scheme = { 129 | items: { 130 | price: decimalToFloat // In this example you could even pass `parseFloat` directly 131 | } 132 | }; 133 | ``` 134 | 135 | ## From query to form 136 | 137 | When performing a GraphQL query, you can't just load the results of the query directly into a form. The results contain some extra info which is not relevant to the form, like `__typename` (Apollo Client adds this field automatically for caching purposes). Stripping this field recursively is painful. Also, the top-level `id` is not relevant since you already have that `id`. 138 | 139 | `parseQueryToForm` removes this irrelevant data for you. An example with Formik: 140 | 141 | ```jsx 142 | import { Query } from 'react-apollo'; 143 | import { Formik } from 'formik'; 144 | import { parseQueryToForm } from '@volst/graphql-form-helpers'; 145 | 146 | const GET_RESTAURANT = gql` 147 | query($id: ID!) { 148 | restaurant(where: { id: $id }) { 149 | id 150 | name 151 | } 152 | } 153 | `; 154 | 155 | ... 156 | 157 | 158 | {({ data }) => ( 159 | 162 | )} 163 | 164 | ``` 165 | 166 | > **Checkout this [real-world example](https://github.com/Volst/new-food-order/blob/64a8ccd7c7ffd437016d88d1fe394bb53739e636/frontend/src/screen/RestaurantCardEdit.tsx#L55-L80)** 167 | 168 | # Contributing 169 | 170 | This project is still in early phases. Please don't hesistate to create an issue with feedback or send a PR! See [contributing guide](./CONTRIBUTING.md) for more info. 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@volst/graphql-form-helpers", 3 | "version": "0.2.5", 4 | "description": "Makes it easy to work with nested data in forms with GraphQL.", 5 | "author": "kees@volst.nl", 6 | "repository": "Volst/graphql-form-helpers", 7 | "license": "ISC", 8 | "private": false, 9 | "main": "dist/index.js", 10 | "typings": "dist/index.d.ts", 11 | "engines": { 12 | "node": ">=8.0" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "keywords": [ 18 | "graphql", 19 | "forms", 20 | "prisma" 21 | ], 22 | "scripts": { 23 | "build": "rm -rf dist && tsc", 24 | "lint": "tslint -p .", 25 | "prepublishOnly": "npm run -s build", 26 | "precommit": "pretty-quick --staged", 27 | "test": "jest --watch", 28 | "test-coverage": "jest --coverage", 29 | "ci": "npm run -s lint && npm run -s build && npm run -s size && npm run -s test-coverage && codecov", 30 | "size": "size-limit" 31 | }, 32 | "devDependencies": { 33 | "@types/jest": "^22.2.3", 34 | "@volst/tslint-config": "^0.2.1", 35 | "codecov": "^3.0.1", 36 | "husky": "^0.14.3", 37 | "jest": "^22.4.3", 38 | "prettier": "^1.12.1", 39 | "pretty-quick": "^1.4.1", 40 | "size-limit": "^0.17.0", 41 | "ts-jest": "^22.4.5", 42 | "tslint": "^5.10.0", 43 | "typescript": "^2.8.3" 44 | }, 45 | "dependencies": { 46 | "deepmerge": "^2.1.0", 47 | "is-pojo": "^1.0.2" 48 | }, 49 | "peerDependencies": {}, 50 | "jest": { 51 | "roots": [ 52 | "./src" 53 | ], 54 | "transform": { 55 | "^.+\\.tsx?$": "ts-jest" 56 | }, 57 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 58 | "moduleFileExtensions": [ 59 | "ts", 60 | "js", 61 | "json", 62 | "node" 63 | ], 64 | "globals": { 65 | "ts-jest": { 66 | "enableTsDiagnostics": true 67 | } 68 | } 69 | }, 70 | "size-limit": [ 71 | { 72 | "limit": "2 KB", 73 | "path": "dist/index.js" 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/formatters/connect.test.ts: -------------------------------------------------------------------------------- 1 | import { connect } from '..'; 2 | 3 | test('connect - handle empty case correctly', () => { 4 | expect(connect('')).toEqual(undefined); 5 | }); 6 | 7 | test('connect - accept arrays', () => { 8 | const values = ['category-1', 'category-2']; 9 | const formatted = connect(values); 10 | 11 | expect(formatted).toEqual({ 12 | connect: [{ id: 'category-1' }, { id: 'category-2' }] 13 | }); 14 | }); 15 | 16 | test('connect - accept string', () => { 17 | const formatted = connect('category-1'); 18 | 19 | expect(formatted).toEqual({ 20 | connect: { id: 'category-1' } 21 | }); 22 | }); 23 | 24 | test('save - throw error on invalid value', () => { 25 | expect(() => connect({} as any)).toThrow('Illegal value for connect given'); 26 | }); 27 | -------------------------------------------------------------------------------- /src/formatters/connect.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../parseFormToMutation'; 2 | 3 | export function connect(ids?: string | string[] | null) { 4 | invariant( 5 | ids == null || Array.isArray(ids) || typeof ids === 'string', 6 | 'Illegal value for connect given' 7 | ); 8 | if (ids) { 9 | if (Array.isArray(ids)) { 10 | return { connect: ids.map(id => ({ id })) }; 11 | } 12 | return { connect: { id: ids } }; 13 | } 14 | return undefined; 15 | } 16 | -------------------------------------------------------------------------------- /src/formatters/create.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '..'; 2 | 3 | test('create - handle empty case correctly', () => { 4 | expect(create(undefined)).toEqual(undefined); 5 | }); 6 | 7 | test('connect - accept object', () => { 8 | const formatted = create({ name: 'Drinks' }); 9 | 10 | expect(formatted).toEqual({ 11 | create: { name: 'Drinks' } 12 | }); 13 | }); 14 | 15 | test('save - throw error on invalid value', () => { 16 | expect(() => create(0 as any)).toThrow('Illegal value for create given'); 17 | }); 18 | -------------------------------------------------------------------------------- /src/formatters/create.ts: -------------------------------------------------------------------------------- 1 | import * as isPlainObject from 'is-pojo'; 2 | import { invariant } from '../parseFormToMutation'; 3 | 4 | export function create(values?: object | object[] | null) { 5 | invariant( 6 | values == null || Array.isArray(values) || isPlainObject(values), 7 | 'Illegal value for create given' 8 | ); 9 | if (values) { 10 | return { create: values }; 11 | } 12 | return undefined; 13 | } 14 | -------------------------------------------------------------------------------- /src/formatters/save.test.ts: -------------------------------------------------------------------------------- 1 | import { parseFormToMutation, save } from '..'; 2 | 3 | test('save - handle empty case correctly', () => { 4 | expect(save(null)).toEqual(undefined); 5 | }); 6 | 7 | test('save - accept object', () => { 8 | const values = { 9 | restaurant: { 10 | id: 'restaurant-1', 11 | name: 'Red Wheelbarrow', 12 | organization: { name: 'fsociety' } 13 | } 14 | }; 15 | const scheme = { 16 | restaurant: { 17 | __format: save, 18 | organization: save 19 | } 20 | }; 21 | const formatted = parseFormToMutation(values, scheme); 22 | 23 | expect(formatted).toEqual({ 24 | restaurant: { 25 | update: { 26 | name: 'Red Wheelbarrow', 27 | organization: { create: { name: 'fsociety' } } 28 | } 29 | } 30 | }); 31 | }); 32 | 33 | test('save - accept array', () => { 34 | const values = { 35 | categories: [ 36 | { 37 | id: 'category-1', 38 | name: 'Drinks', 39 | items: [ 40 | { 41 | name: 'Coca cola' 42 | }, 43 | { 44 | id: 'beer-1', 45 | name: 'Beer' 46 | } 47 | ] 48 | } 49 | ] 50 | }; 51 | const scheme = { 52 | categories: { 53 | __format: save, 54 | items: save 55 | } 56 | }; 57 | const formatted = parseFormToMutation(values, scheme); 58 | 59 | expect(formatted).toEqual({ 60 | categories: { 61 | update: [ 62 | { 63 | where: { id: 'category-1' }, 64 | data: { 65 | name: 'Drinks', 66 | items: { 67 | create: [ 68 | { 69 | name: 'Coca cola' 70 | } 71 | ], 72 | update: [ 73 | { 74 | where: { id: 'beer-1' }, 75 | data: { 76 | name: 'Beer' 77 | } 78 | } 79 | ] 80 | } 81 | } 82 | } 83 | ] 84 | } 85 | }); 86 | }); 87 | 88 | test('save - throw error on invalid value', () => { 89 | expect(() => save(1 as any)).toThrow('Illegal value for save given'); 90 | }); 91 | -------------------------------------------------------------------------------- /src/formatters/save.ts: -------------------------------------------------------------------------------- 1 | import * as isPlainObject from 'is-pojo'; 2 | import { invariant } from '../parseFormToMutation'; 3 | 4 | export interface IValues { 5 | id?: string; 6 | [key: string]: any; 7 | } 8 | 9 | // We don't call it `upsert`, because Prisma has a mutation named that way and we don't want to imply it is that. 10 | export function save(values?: IValues[] | IValues | null) { 11 | invariant( 12 | values == null || Array.isArray(values) || isPlainObject(values), 13 | 'Illegal value for save given' 14 | ); 15 | 16 | if (values) { 17 | const output: { create?: any; update?: any } = {}; 18 | const creates: any[] = []; 19 | const updates: any[] = []; 20 | if (Array.isArray(values)) { 21 | values.forEach(value => { 22 | const id = value.id; 23 | delete value.id; 24 | if (id) { 25 | updates.push({ where: { id }, data: value }); 26 | } else { 27 | creates.push(value); 28 | } 29 | }); 30 | if (creates.length > 0) { 31 | output.create = creates; 32 | } 33 | if (updates.length > 0) { 34 | output.update = updates; 35 | } 36 | } else { 37 | // Assuming `values` is a plain object 38 | const id = values.id; 39 | delete values.id; 40 | if (id) { 41 | output.update = values; 42 | } else { 43 | output.create = values; 44 | } 45 | } 46 | return output; 47 | } 48 | return undefined; 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { parseFormToMutation } from './parseFormToMutation'; 2 | export { parseQueryToForm } from './parseQueryToForm'; 3 | export { connect } from './formatters/connect'; 4 | export { create } from './formatters/create'; 5 | export { save } from './formatters/save'; 6 | -------------------------------------------------------------------------------- /src/parseFormToMutation.test.ts: -------------------------------------------------------------------------------- 1 | import { create, connect, save, parseFormToMutation } from '.'; 2 | 3 | test('parseFormToMutation - basic', () => { 4 | const values = { 5 | name: 'Summer season', 6 | organization: 'organization-1' 7 | }; 8 | const scheme = { 9 | organization: connect 10 | }; 11 | const formatted = parseFormToMutation(values, scheme); 12 | 13 | expect(formatted).toEqual({ 14 | name: 'Summer season', 15 | organization: { connect: { id: 'organization-1' } } 16 | }); 17 | }); 18 | 19 | test('parseFormToMutation - Nested Scheme with null root', () => { 20 | const values = { 21 | name: 'Summer season', 22 | organization: null 23 | }; 24 | const scheme = { 25 | organization: { 26 | __format: create, 27 | address: create 28 | } 29 | }; 30 | const formatted = parseFormToMutation(values, scheme); 31 | 32 | expect(formatted).toEqual({ 33 | name: 'Summer season', 34 | organization: null 35 | }); 36 | }); 37 | 38 | test('parseFormToMutation - advanced', () => { 39 | const values = { 40 | categories: [ 41 | { 42 | id: 'category-1', 43 | name: 'Drinks', 44 | items: [ 45 | { 46 | name: 'Coca cola' 47 | }, 48 | { 49 | name: 'Beer', 50 | toppings: [ 51 | { 52 | name: 'Lemon' 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | }; 60 | const scheme = { 61 | categories: { 62 | __format: create, 63 | items: { 64 | __format: create, 65 | toppings: create 66 | } 67 | } 68 | }; 69 | const formatted = parseFormToMutation(values, scheme); 70 | 71 | expect(formatted).toEqual({ 72 | categories: { 73 | create: [ 74 | { 75 | id: 'category-1', 76 | name: 'Drinks', 77 | items: { 78 | create: [ 79 | { 80 | name: 'Coca cola' 81 | }, 82 | { 83 | name: 'Beer', 84 | toppings: { 85 | create: [{ name: 'Lemon' }] 86 | } 87 | } 88 | ] 89 | } 90 | } 91 | ] 92 | } 93 | }); 94 | }); 95 | 96 | test('parseFormToMutation - should create new object', () => { 97 | const values = { 98 | name: 'Summer season', 99 | categories: [{ id: 'category-1' }] 100 | }; 101 | const scheme = { 102 | categories: create 103 | }; 104 | const formatted = parseFormToMutation(values, scheme); 105 | 106 | expect(formatted).not.toBe(values); 107 | expect(values.categories[0].id).toBe('category-1'); 108 | }); 109 | 110 | test('parseFormToMutation - formatting should work without a __format present', () => { 111 | const values = { 112 | categories: [ 113 | { 114 | items: [{ name: 'Beer' }] 115 | } 116 | ] 117 | }; 118 | const scheme = { 119 | categories: { 120 | items: save 121 | } 122 | }; 123 | const formatted = parseFormToMutation(values, scheme); 124 | 125 | expect(formatted).toEqual({ 126 | categories: [{ items: { create: [{ name: 'Beer' }] } }] 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /src/parseFormToMutation.ts: -------------------------------------------------------------------------------- 1 | import * as _merge from 'deepmerge'; 2 | import * as isPlainObject from 'is-pojo'; 3 | 4 | const merge = _merge.default || _merge; 5 | 6 | export type ActionFn = ((value: any) => any); 7 | export interface Scheme { 8 | [x: string]: ActionFn | Scheme; 9 | } 10 | 11 | export function parseFormToMutation(values: object, scheme: Scheme): object { 12 | const myValues = merge({}, values); 13 | 14 | function applyParentAction( 15 | _values: object, 16 | _scheme: Scheme, 17 | parentKey: string, 18 | parentValues: object 19 | ) { 20 | if (!_scheme.__format) { 21 | return; 22 | } 23 | parentValues[parentKey] = (_scheme.__format as ActionFn)(_values); 24 | } 25 | 26 | function traverseScheme( 27 | _values: object, 28 | _scheme: Scheme, 29 | parentKey?: string, 30 | parentValues?: object 31 | ) { 32 | Object.keys(_scheme) 33 | .filter(s => s !== '__format') 34 | .forEach(key => { 35 | const action = _scheme[key]; 36 | const currentValue = _values[key]; 37 | if (isPlainObject(action)) { 38 | if (Array.isArray(currentValue)) { 39 | applyParentAction(currentValue, action as Scheme, key, _values); 40 | currentValue.forEach(value => 41 | traverseScheme(value, action as Scheme, key, _values) 42 | ); 43 | } else if (currentValue) { 44 | applyParentAction(currentValue, action as Scheme, key, _values); 45 | traverseScheme(currentValue, action as Scheme, key, _values); 46 | } 47 | } else if (_values) { 48 | _values[key] = (action as ActionFn)(currentValue); 49 | } 50 | }); 51 | } 52 | 53 | traverseScheme(myValues, scheme); 54 | 55 | return myValues; 56 | } 57 | 58 | export function invariant( 59 | condition: boolean, 60 | message: string = 'Illegal state' 61 | ) { 62 | if (!condition) { 63 | throw new Error(`[graphql-form-helpers] ${message}`); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/parseQueryToForm.test.ts: -------------------------------------------------------------------------------- 1 | import { parseQueryToForm } from '.'; 2 | 3 | test('parseQueryToForm - advanced', () => { 4 | const input = { 5 | __typename: 'Card', 6 | id: 'card-1', 7 | name: 'Summer season', 8 | restaurant: { id: 'restaurant-1', __typename: 'Restaurant' }, 9 | categories: [ 10 | { 11 | id: 'category-1', 12 | name: 'Burgers', 13 | items: [], 14 | __typename: 'CardCategory' 15 | }, 16 | { 17 | id: 'category-2', 18 | name: 'Drinks', 19 | items: [ 20 | { 21 | id: 'item-1', 22 | name: 'Beer', 23 | subitems: [], 24 | __typename: 'CardItem' 25 | } 26 | ], 27 | __typename: 'CardCategory' 28 | } 29 | ] 30 | }; 31 | const output = parseQueryToForm(input, {}); 32 | 33 | expect(output).toEqual({ 34 | restaurant: { id: 'restaurant-1' }, 35 | categories: [ 36 | { id: 'category-1', items: [], name: 'Burgers' }, 37 | { 38 | id: 'category-2', 39 | items: [{ id: 'item-1', name: 'Beer', subitems: [] }], 40 | name: 'Drinks' 41 | } 42 | ], 43 | name: 'Summer season' 44 | }); 45 | }); 46 | 47 | test('parseQueryToForm - empty', () => { 48 | const defaults = { hoi: true }; 49 | const output = parseQueryToForm(null, defaults); 50 | expect(output).toBe(defaults); 51 | 52 | // Completely empty 53 | const output2 = parseQueryToForm(null); 54 | expect(output2).toEqual({}); 55 | }); 56 | -------------------------------------------------------------------------------- /src/parseQueryToForm.ts: -------------------------------------------------------------------------------- 1 | import * as _merge from 'deepmerge'; 2 | import * as isPlainObject from 'is-pojo'; 3 | 4 | const merge = _merge.default || _merge; 5 | 6 | export function parseQueryToForm(data: any, defaults?: object): object { 7 | function removeProps(obj: object, keys: string[]) { 8 | if (obj instanceof Array) { 9 | obj.forEach(item => removeProps(item, keys)); 10 | } else if (isPlainObject(obj)) { 11 | Object.getOwnPropertyNames(obj).forEach(key => { 12 | if (keys.indexOf(key) !== -1) delete obj[key]; 13 | else removeProps(obj[key], keys); 14 | }); 15 | } 16 | } 17 | if (data) { 18 | const myData = merge({}, data); 19 | removeProps(myData, ['__typename']); 20 | delete myData.id; 21 | return myData; 22 | } 23 | 24 | return defaults || {}; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "lib": ["esnext"], 8 | "strict": true, 9 | // Disabled because this doesn't work correctly with "declaration": true 10 | "noUnusedLocals": false, 11 | "noImplicitAny": false, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@volst/tslint-config" 3 | } 4 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'deepmerge' { 2 | interface Options { 3 | clone?: boolean; 4 | arrayMerge?(destination: any[], source: any[], options?: Options): any[]; 5 | isMergeableObject?(value: object): boolean; 6 | } 7 | function deepmerge(x: Partial, y: Partial, options?: Options): T; 8 | function deepmerge(x: T1, y: T2, options?: Options): T1 & T2; 9 | export default deepmerge; 10 | export function all(objects: Array>, options?: Options): T; 11 | } 12 | --------------------------------------------------------------------------------