├── .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 | [](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 |
--------------------------------------------------------------------------------