├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── data.sql ├── helpers.js ├── integration │ ├── __snapshots__ │ │ ├── create.test.js.snap │ │ ├── options.test.js.snap │ │ ├── plurals.test.js.snap │ │ ├── smartComments.test.js.snap │ │ └── update.test.js.snap │ ├── create.test.js │ ├── options.test.js │ ├── plurals.test.js │ ├── schema │ │ ├── __snapshots__ │ │ │ └── schema.test.js.snap │ │ ├── core.js │ │ └── schema.test.js │ ├── smartComments.test.js │ └── update.test.js ├── printSchemaOrdered.js └── schema.sql ├── index.js ├── package.json ├── scripts └── test ├── src ├── PostgraphileNestedConnectorsPlugin.js ├── PostgraphileNestedDeletersPlugin.js ├── PostgraphileNestedMutationsPlugin.js ├── PostgraphileNestedTypesPlugin.js └── PostgraphileNestedUpdatersPlugin.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:12 11 | - image: circleci/postgres:9.6-alpine 12 | environment: 13 | POSTGRES_USER: circleci 14 | POSTGRES_DB: circle_test 15 | 16 | # Specify service dependencies here if necessary 17 | # CircleCI maintains a library of pre-built images 18 | # documented at https://circleci.com/docs/2.0/circleci-images/ 19 | # - image: circleci/mongo:3.4.4 20 | 21 | working_directory: ~/repo 22 | 23 | steps: 24 | - checkout 25 | 26 | # Download and cache dependencies 27 | - restore_cache: 28 | keys: 29 | - v1-dependencies-{{ checksum "package.json" }} 30 | # fallback to using the latest cache if no exact match is found 31 | - v1-dependencies- 32 | 33 | - run: 34 | name: Installing dependencies 35 | command: | 36 | yarn install 37 | sudo apt-get install postgresql-client 38 | 39 | - save_cache: 40 | paths: 41 | - node_modules 42 | key: v1-dependencies-{{ checksum "package.json" }} 43 | 44 | - run: 45 | name: Waiting for Postgres to be ready 46 | command: | 47 | for i in `seq 1 10`; 48 | do 49 | nc -z localhost 5432 && echo Success && exit 0 50 | echo -n . 51 | sleep 1 52 | done 53 | echo Failed waiting for Postgres && exit 1 54 | 55 | # run tests! 56 | - run: 57 | name: Run linter 58 | command: yarn lint --format junit -o reports/junit/js-lint-results.xml 59 | 60 | - run: 61 | name: Run unit tests 62 | environment: 63 | TEST_DATABASE_URL: postgres://circleci@localhost:5432/circle_test 64 | JEST_JUNIT_OUTPUT: 'reports/junit/js-test-results.xml' 65 | command: yarn test -- --ci --testResultsProcessor="jest-junit" 66 | 67 | - store_test_results: 68 | path: reports/junit 69 | 70 | - store_artifacts: 71 | path: reports/junit 72 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | 10 | [Makefile] 11 | indent_style = tab 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # testing 5 | coverage 6 | # production 7 | build 8 | .netlify 9 | dist 10 | reports/ 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log 15 | yarn-error.log 16 | 17 | #IDE 18 | .idea/* 19 | *.swp 20 | .vscode 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## v1.1.0 4 | 5 | - Add support for --classic-ids. 6 | - Re-order operations so that deleteOthers is done before inserting a 7 | new row. 8 | - Fix parenthesis wrapping for tables with multiple keys. 9 | 10 | ## v1.0.1 11 | 12 | - Correctly release the savepoint on error (thanks @sijad). 13 | 14 | ## v1.0.0 15 | 16 | - Bump dependencies. 17 | - Guard against creating updater fields where constraint is not available. 18 | 19 | ## v1.0.0-alpha.22 20 | 21 | - Fix bug where if an update mutation was called that did not locate 22 | a row, we'd still try and extract the PKs. 23 | 24 | ## v1.0.0-alpha.21 25 | 26 | - Really fix `updateByNodeId`. Thanks for the report @ken0x0a! 27 | 28 | ## v1.0.0-alpha.20 29 | 30 | - Fix case where a mutation specified `nestedMutationField: null`. 31 | 32 | ## v1.0.0-alpha.19 33 | 34 | - `deleteOthers` now is not in the schema where the foreign table 35 | has `@omit delete`. 36 | - Fixed error that prevented `updateByNodeId` from working. 37 | 38 | ## v1.0.0-alpha.18 39 | 40 | - Correctly handle `null` values to connect and update fields. 41 | 42 | ## v1.0.0-alpha.17 43 | 44 | - Add support for `@fieldName` and `@foreignFieldName` smart comments on 45 | foreign keys to match those used in PostGraphile. The original 46 | `@forwardMutationName` and `@reverseMutationName` smart comments will 47 | remain to allow for renaming the fields just for nested mutations. 48 | 49 | ## v1.0.0-alpha.16 50 | 51 | - Support `deleteOthers` where there are no other records modified. Thanks 52 | @srp. 53 | 54 | ## v1.0.0-alpha.15 55 | 56 | - The patch type for nested updates now correctly omits the keys that are 57 | the subject of the nested mutation. 58 | 59 | ## v1.0.0-alpha.14 60 | 61 | - Support for updating nested records. 62 | 63 | ## v1.0.0-alpha.13 64 | 65 | - 1:1 relationships no longer allow a list of objects, or allow multiple 66 | operations, preventing a constraint violation. 67 | 68 | ## v1.0.0-alpha.11 69 | 70 | - _BREAKING_ One-to-one relationships are now correctly named in the singular. To 71 | keep using the old behaviour, use the `nestedMutationsOldUniqueFields` option. 72 | - _BREAKING_ The `connect` field has been removed. In its place is `connectByNodeId` 73 | which takes a nodeId, and `connnectBy` for the table's primary key and 74 | each unique key. 75 | - Nested mutations on update mutations are now supported. 76 | - Existing rows can be now be `connected`. 77 | - Multiple actions per nested type may now be specified (i.e. create some records 78 | and connect others). 79 | - A new field has been added on nested mutations: `deleteOthers`. When set to `true`, 80 | any related rows not updated or created in the nested mutation will be deleted. To 81 | keep a row that is not being created or updated, specify it for update with no 82 | modified fields. 83 | - Relationships between two tables that have multiple relationships are now supported. 84 | Previously, the last constraint would overwrite the others. These will usually end 85 | up with some pretty awkward names, so the use of smart comments to name the relationships 86 | is recommended. 87 | - Improved test suite. 88 | 89 | ## v1.0.0-alpha.7 90 | 91 | Relationships using composite keys are now supported, and this has meant creating 92 | a custom field name for the nested mutation, rather than piggybacking an existing ID 93 | field. See the examples below for the new GraphQL schema that is generated. 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2021 Mark Lipscombe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Package on npm](https://img.shields.io/npm/v/postgraphile-plugin-nested-mutations.svg)](https://www.npmjs.com/package/postgraphile-plugin-nested-mutations) 2 | [![CircleCI](https://circleci.com/gh/mlipscombe/postgraphile-plugin-nested-mutations/tree/master.svg?style=svg)](https://circleci.com/gh/mlipscombe/postgraphile-plugin-nested-mutations/tree/master) 3 | 4 | # postgraphile-plugin-nested-mutations 5 | This plugin implements nested mutations based on both forward and reverse foreign 6 | key relationships in PostGraphile v4. Nested mutations can be of infinite depth. 7 | 8 | ## Getting Started 9 | 10 | ### CLI 11 | 12 | ``` bash 13 | postgraphile --append-plugins postgraphile-plugin-nested-mutations 14 | ``` 15 | 16 | See [here](https://www.graphile.org/postgraphile/extending/#loading-additional-plugins) for 17 | more information about loading plugins with PostGraphile. 18 | 19 | ### Library 20 | 21 | ``` js 22 | const express = require('express'); 23 | const { postgraphile } = require('postgraphile'); 24 | const PostGraphileNestedMutations = require('postgraphile-plugin-nested-mutations'); 25 | 26 | const app = express(); 27 | 28 | app.use( 29 | postgraphile(pgConfig, schema, { 30 | appendPlugins: [ 31 | PostGraphileNestedMutations, 32 | ], 33 | }) 34 | ); 35 | 36 | app.listen(5000); 37 | ``` 38 | 39 | ### Plugin Options 40 | 41 | When using PostGraphile as a library, the following plugin options can be passed 42 | via `graphileBuildOptions`: 43 | 44 |
45 | 46 | nestedMutationsSimpleFieldNames 47 | 48 | Use simple field names for nested mutations. Instead of names suffixed with 49 | `tableBy` and `tableUsing`, tables with a single foreign key relationship 50 | between them will have their nested relation fields named `table`. Defaults to 51 | `false`. 52 | 53 | ```js 54 | postgraphile(pgConfig, schema, { 55 | graphileBuildOptions: { 56 | nestedMutationsSimpleFieldNames: true, 57 | } 58 | }); 59 | ``` 60 |
61 | 62 |
63 | 64 | nestedMutationsDeleteOthers 65 | 66 | Controls whether the `deleteOthers` field is available on nested mutations. Defaults 67 | to `true`. 68 | 69 | ```js 70 | postgraphile(pgConfig, schema, { 71 | graphileBuildOptions: { 72 | nestedMutationsDeleteOthers: false, 73 | } 74 | }); 75 | ``` 76 |
77 | 78 |
79 | 80 | nestedMutationsOldUniqueFields 81 | 82 | If enabled, plural names for one-to-one relations will be used. For backwards 83 | compatibility. Defaults to `false`. 84 | 85 | ```js 86 | postgraphile(pgConfig, schema, { 87 | graphileBuildOptions: { 88 | nestedMutationsOldUniqueFields: false, 89 | } 90 | }); 91 | ``` 92 | 93 |
94 | 95 | ## Usage 96 | 97 | This plugin creates an additional field on each GraphQL `Input` type for every forward 98 | and reverse foreign key relationship on a table, with the same name as the foreign table. 99 | 100 | Each nested mutation field will have the following fields. They will accept an array if 101 | the relationship is a one-to-many relationship, or a single input if they are one-to-one. 102 | 103 | ### Connect to Existing Record 104 | #### `connectByNodeId` 105 | Connect using a `nodeId` from the nested table. 106 | 107 | #### `connectBy` 108 | Connect using any readable primary key or unique constraint on the nested table. 109 | 110 | ### Creating New Records 111 | #### `create` 112 | Create a new record in the nested table. 113 | 114 | ### Delete existing Record 115 | #### `deleteByNodeId` 116 | Delete using a `nodeId` from the nested table. 117 | 118 | #### `deleteBy` 119 | Delete using any readable primary key or unique constraint on the nested table. 120 | 121 | ### Updating Records 122 | #### `updateByNodeId` 123 | Update a record using a `nodeId` from the nested table. 124 | 125 | #### `updatedBy` 126 | Update a record using any readable primary key or unique constraint on the nested table. 127 | 128 | ## Example 129 | 130 | ```sql 131 | create table parent ( 132 | id serial primary key, 133 | name text not null 134 | ); 135 | 136 | create table child ( 137 | id serial primary key, 138 | parent_id integer, 139 | name text not null, 140 | constraint child_parent_fkey foreign key (parent_id) 141 | references p.parent (id) 142 | ); 143 | ``` 144 | 145 | A nested mutation against this schema, using `Parent` as the base mutation 146 | would look like this: 147 | 148 | ``` graphql 149 | mutation { 150 | createParent(input: { 151 | parent: { 152 | name: "Parent 1" 153 | childrenUsingId: { 154 | connectById: [{ 155 | id: 1 156 | }] 157 | create: [{ 158 | name: "Child 1" 159 | }, { 160 | name: "Child 2" 161 | }] 162 | } 163 | } 164 | }) { 165 | parent { 166 | id 167 | name 168 | childrenByParentId { 169 | nodes { 170 | id 171 | name 172 | } 173 | } 174 | } 175 | } 176 | } 177 | ``` 178 | 179 | Or using `Child` as the base mutation: 180 | 181 | ``` graphql 182 | mutation { 183 | createChild(input: { 184 | child: { 185 | name: "Child 1" 186 | parentToParentId: { 187 | create: { 188 | name: "Parent of Child 1" 189 | } 190 | } 191 | }, 192 | }) { 193 | child { 194 | id 195 | name 196 | parentByParentId { 197 | id 198 | name 199 | } 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | ## Smart Comments 206 | 207 | [Smart comments](https://www.graphile.org/postgraphile/smart-comments/) are supported for 208 | renaming the nested mutation fields. 209 | 210 | ```sql 211 | comment on constraint child_parent_fkey on child is 212 | E'@fieldName parent\n@foreignFieldName children'; 213 | ``` 214 | -------------------------------------------------------------------------------- /__tests__/data.sql: -------------------------------------------------------------------------------- 1 | insert into p.parent (id, name) values 2 | (1000, 'A'), 3 | (1001, 'B'), 4 | (1002, 'C'); 5 | 6 | insert into p.child (id, parent_id, name) values 7 | (100, 1000, 'A'); 8 | -------------------------------------------------------------------------------- /__tests__/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const pg = require('pg'); 3 | const { readFile } = require('fs'); 4 | const pgConnectionString = require('pg-connection-string'); 5 | const { createPostGraphileSchema } = require('postgraphile-core'); 6 | 7 | function readFilePromise(filename, encoding) { 8 | return new Promise((resolve, reject) => { 9 | readFile(filename, encoding, (err, res) => { 10 | if (err) reject(err); 11 | else resolve(res); 12 | }); 13 | }); 14 | } 15 | 16 | const kitchenSinkData = () => readFilePromise(`${__dirname}/data.sql`, 'utf8'); 17 | 18 | const withPgClient = async (url, fn) => { 19 | if (!fn) { 20 | fn = url; 21 | url = process.env.TEST_DATABASE_URL; 22 | } 23 | const pgPool = new pg.Pool(pgConnectionString.parse(url)); 24 | let client; 25 | try { 26 | client = await pgPool.connect(); 27 | await client.query('begin'); 28 | await client.query('set local timezone to \'+04:00\''); 29 | const result = await fn(client); 30 | await client.query('rollback'); 31 | return result; 32 | } finally { 33 | try { 34 | await client.release(); 35 | } catch (e) { 36 | console.error('Error releasing pgClient', e); 37 | throw e; 38 | } 39 | await pgPool.end(); 40 | } 41 | }; 42 | 43 | const withDbFromUrl = async (url, fn) => withPgClient(url, async (client) => { 44 | try { 45 | await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE;'); 46 | return fn(client); 47 | } finally { 48 | await client.query('COMMIT;'); 49 | } 50 | }); 51 | 52 | 53 | const withRootDb = fn => withDbFromUrl(process.env.TEST_DATABASE_URL, fn); 54 | 55 | let prepopulatedDBKeepalive; 56 | 57 | const populateDatabase = async (client) => { 58 | await client.query(await readFilePromise(`${__dirname}/data.sql`, 'utf8')); 59 | return {}; 60 | }; 61 | 62 | const withPrepopulatedDb = async (fn) => { 63 | if (!prepopulatedDBKeepalive) { 64 | throw new Error('You must call setup and teardown to use this'); 65 | } 66 | const { client, vars } = prepopulatedDBKeepalive; 67 | if (!vars) { 68 | throw new Error('No prepopulated vars'); 69 | } 70 | let err; 71 | try { 72 | await fn(client, vars); 73 | } catch (e) { 74 | err = e; 75 | } 76 | try { 77 | await client.query('ROLLBACK TO SAVEPOINT pristine;'); 78 | } catch (e) { 79 | err = err || e; 80 | console.error('ERROR ROLLING BACK', e.message); // eslint-disable-line no-console 81 | } 82 | if (err) { 83 | throw err; 84 | } 85 | }; 86 | 87 | withPrepopulatedDb.setup = async () => { 88 | if (prepopulatedDBKeepalive) { 89 | throw new Error("There's already a prepopulated DB running"); 90 | } 91 | let res; 92 | let rej; 93 | prepopulatedDBKeepalive = new Promise((resolve, reject) => { 94 | res = resolve; 95 | rej = reject; 96 | }); 97 | prepopulatedDBKeepalive.resolve = res; 98 | prepopulatedDBKeepalive.reject = rej; 99 | withRootDb(async (client) => { 100 | prepopulatedDBKeepalive.client = client; 101 | try { 102 | prepopulatedDBKeepalive.vars = await populateDatabase(client); 103 | } catch (e) { 104 | console.error('FAILED TO PREPOPULATE DB!', e.message); // eslint-disable-line no-console 105 | throw e; 106 | } 107 | await client.query('SAVEPOINT pristine;'); 108 | return prepopulatedDBKeepalive; 109 | }); 110 | }; 111 | 112 | withPrepopulatedDb.teardown = () => { 113 | if (!prepopulatedDBKeepalive) { 114 | throw new Error('Cannot tear down null!'); 115 | } 116 | prepopulatedDBKeepalive.resolve(); // Release DB transaction 117 | prepopulatedDBKeepalive = null; 118 | }; 119 | 120 | const withSchema = ({ 121 | setup, 122 | test, 123 | options = {}, 124 | }) => () => withPgClient(async (client) => { 125 | if (setup) { 126 | if (typeof setup === 'function') { 127 | await setup(client); 128 | } else { 129 | await client.query(setup); 130 | } 131 | } 132 | 133 | const schemaOptions = Object.assign( 134 | { 135 | appendPlugins: [require('../index.js')], 136 | showErrorStack: true, 137 | }, 138 | options, 139 | ); 140 | 141 | const schema = await createPostGraphileSchema(client, ['p'], schemaOptions); 142 | return test({ 143 | schema, 144 | pgClient: client, 145 | }); 146 | }); 147 | 148 | const loadQuery = fn => readFilePromise(`${__dirname}/fixtures/queries/${fn}`, 'utf8'); 149 | 150 | exports.withRootDb = withRootDb; 151 | exports.withPrepopulatedDb = withPrepopulatedDb; 152 | exports.withPgClient = withPgClient; 153 | exports.withSchema = withSchema; 154 | exports.loadQuery = loadQuery; 155 | -------------------------------------------------------------------------------- /__tests__/integration/__snapshots__/options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`id fields are renamed rowId when classicIds is enabled 1`] = ` 4 | GraphQLSchema { 5 | "__validationErrors": Array [], 6 | "_directives": Array [ 7 | "@include", 8 | "@skip", 9 | "@deprecated", 10 | "@specifiedBy", 11 | ], 12 | "_implementationsMap": Object { 13 | "Node": Object { 14 | "interfaces": Array [], 15 | "objects": Array [ 16 | "Query", 17 | "Child", 18 | "Parent", 19 | ], 20 | }, 21 | }, 22 | "_mutationType": "Mutation", 23 | "_queryType": "Query", 24 | "_subTypeMap": Object {}, 25 | "_subscriptionType": undefined, 26 | "_typeMap": Object { 27 | "Boolean": "Boolean", 28 | "Child": "Child", 29 | "ChildChildPkeyConnect": "ChildChildPkeyConnect", 30 | "ChildChildPkeyDelete": "ChildChildPkeyDelete", 31 | "ChildCondition": "ChildCondition", 32 | "ChildInput": "ChildInput", 33 | "ChildNodeIdConnect": "ChildNodeIdConnect", 34 | "ChildNodeIdDelete": "ChildNodeIdDelete", 35 | "ChildOnChildForChildParentIdFkeyNodeIdUpdate": "ChildOnChildForChildParentIdFkeyNodeIdUpdate", 36 | "ChildOnChildForChildParentIdFkeyUsingChildPkeyUpdate": "ChildOnChildForChildParentIdFkeyUsingChildPkeyUpdate", 37 | "ChildParentIdFkeyChildCreateInput": "ChildParentIdFkeyChildCreateInput", 38 | "ChildParentIdFkeyInput": "ChildParentIdFkeyInput", 39 | "ChildParentIdFkeyInverseInput": "ChildParentIdFkeyInverseInput", 40 | "ChildParentIdFkeyParentCreateInput": "ChildParentIdFkeyParentCreateInput", 41 | "ChildPatch": "ChildPatch", 42 | "ChildrenConnection": "ChildrenConnection", 43 | "ChildrenEdge": "ChildrenEdge", 44 | "ChildrenOrderBy": "ChildrenOrderBy", 45 | "CreateChildInput": "CreateChildInput", 46 | "CreateChildPayload": "CreateChildPayload", 47 | "CreateParentInput": "CreateParentInput", 48 | "CreateParentPayload": "CreateParentPayload", 49 | "Cursor": "Cursor", 50 | "DeleteChildByRowIdInput": "DeleteChildByRowIdInput", 51 | "DeleteChildInput": "DeleteChildInput", 52 | "DeleteChildPayload": "DeleteChildPayload", 53 | "DeleteParentByRowIdInput": "DeleteParentByRowIdInput", 54 | "DeleteParentInput": "DeleteParentInput", 55 | "DeleteParentPayload": "DeleteParentPayload", 56 | "ID": "ID", 57 | "Int": "Int", 58 | "Mutation": "Mutation", 59 | "Node": "Node", 60 | "PageInfo": "PageInfo", 61 | "Parent": "Parent", 62 | "ParentCondition": "ParentCondition", 63 | "ParentInput": "ParentInput", 64 | "ParentNodeIdConnect": "ParentNodeIdConnect", 65 | "ParentNodeIdDelete": "ParentNodeIdDelete", 66 | "ParentOnChildForChildParentIdFkeyNodeIdUpdate": "ParentOnChildForChildParentIdFkeyNodeIdUpdate", 67 | "ParentOnChildForChildParentIdFkeyUsingParentPkeyUpdate": "ParentOnChildForChildParentIdFkeyUsingParentPkeyUpdate", 68 | "ParentParentPkeyConnect": "ParentParentPkeyConnect", 69 | "ParentParentPkeyDelete": "ParentParentPkeyDelete", 70 | "ParentPatch": "ParentPatch", 71 | "ParentsConnection": "ParentsConnection", 72 | "ParentsEdge": "ParentsEdge", 73 | "ParentsOrderBy": "ParentsOrderBy", 74 | "Query": "Query", 75 | "String": "String", 76 | "UUID": "UUID", 77 | "UpdateChildByRowIdInput": "UpdateChildByRowIdInput", 78 | "UpdateChildInput": "UpdateChildInput", 79 | "UpdateChildPayload": "UpdateChildPayload", 80 | "UpdateParentByRowIdInput": "UpdateParentByRowIdInput", 81 | "UpdateParentInput": "UpdateParentInput", 82 | "UpdateParentPayload": "UpdateParentPayload", 83 | "__Directive": "__Directive", 84 | "__DirectiveLocation": "__DirectiveLocation", 85 | "__EnumValue": "__EnumValue", 86 | "__Field": "__Field", 87 | "__InputValue": "__InputValue", 88 | "__Schema": "__Schema", 89 | "__Type": "__Type", 90 | "__TypeKind": "__TypeKind", 91 | "updateChildOnChildForChildParentIdFkeyPatch": "updateChildOnChildForChildParentIdFkeyPatch", 92 | "updateParentOnChildForChildParentIdFkeyPatch": "updateParentOnChildForChildParentIdFkeyPatch", 93 | }, 94 | "astNode": undefined, 95 | "description": undefined, 96 | "extensionASTNodes": undefined, 97 | "extensions": undefined, 98 | } 99 | `; 100 | 101 | exports[`simple names, plural when one-to-many, singular in reverse 1`] = ` 102 | GraphQLSchema { 103 | "__validationErrors": Array [], 104 | "_directives": Array [ 105 | "@include", 106 | "@skip", 107 | "@deprecated", 108 | "@specifiedBy", 109 | ], 110 | "_implementationsMap": Object { 111 | "Node": Object { 112 | "interfaces": Array [], 113 | "objects": Array [ 114 | "Query", 115 | "Child", 116 | "Parent", 117 | ], 118 | }, 119 | }, 120 | "_mutationType": "Mutation", 121 | "_queryType": "Query", 122 | "_subTypeMap": Object {}, 123 | "_subscriptionType": undefined, 124 | "_typeMap": Object { 125 | "Boolean": "Boolean", 126 | "Child": "Child", 127 | "ChildChildPkeyConnect": "ChildChildPkeyConnect", 128 | "ChildChildPkeyDelete": "ChildChildPkeyDelete", 129 | "ChildCondition": "ChildCondition", 130 | "ChildInput": "ChildInput", 131 | "ChildNodeIdConnect": "ChildNodeIdConnect", 132 | "ChildNodeIdDelete": "ChildNodeIdDelete", 133 | "ChildOnChildForChildParentFkeyNodeIdUpdate": "ChildOnChildForChildParentFkeyNodeIdUpdate", 134 | "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate": "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate", 135 | "ChildParentFkeyChildCreateInput": "ChildParentFkeyChildCreateInput", 136 | "ChildParentFkeyInput": "ChildParentFkeyInput", 137 | "ChildParentFkeyInverseInput": "ChildParentFkeyInverseInput", 138 | "ChildParentFkeyParentCreateInput": "ChildParentFkeyParentCreateInput", 139 | "ChildPatch": "ChildPatch", 140 | "ChildrenConnection": "ChildrenConnection", 141 | "ChildrenEdge": "ChildrenEdge", 142 | "ChildrenOrderBy": "ChildrenOrderBy", 143 | "CreateChildInput": "CreateChildInput", 144 | "CreateChildPayload": "CreateChildPayload", 145 | "CreateParentInput": "CreateParentInput", 146 | "CreateParentPayload": "CreateParentPayload", 147 | "Cursor": "Cursor", 148 | "DeleteChildByIdInput": "DeleteChildByIdInput", 149 | "DeleteChildInput": "DeleteChildInput", 150 | "DeleteChildPayload": "DeleteChildPayload", 151 | "DeleteParentByIdInput": "DeleteParentByIdInput", 152 | "DeleteParentInput": "DeleteParentInput", 153 | "DeleteParentPayload": "DeleteParentPayload", 154 | "ID": "ID", 155 | "Int": "Int", 156 | "Mutation": "Mutation", 157 | "Node": "Node", 158 | "PageInfo": "PageInfo", 159 | "Parent": "Parent", 160 | "ParentCondition": "ParentCondition", 161 | "ParentInput": "ParentInput", 162 | "ParentNodeIdConnect": "ParentNodeIdConnect", 163 | "ParentNodeIdDelete": "ParentNodeIdDelete", 164 | "ParentOnChildForChildParentFkeyNodeIdUpdate": "ParentOnChildForChildParentFkeyNodeIdUpdate", 165 | "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate": "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate", 166 | "ParentParentPkeyConnect": "ParentParentPkeyConnect", 167 | "ParentParentPkeyDelete": "ParentParentPkeyDelete", 168 | "ParentPatch": "ParentPatch", 169 | "ParentsConnection": "ParentsConnection", 170 | "ParentsEdge": "ParentsEdge", 171 | "ParentsOrderBy": "ParentsOrderBy", 172 | "Query": "Query", 173 | "String": "String", 174 | "UpdateChildByIdInput": "UpdateChildByIdInput", 175 | "UpdateChildInput": "UpdateChildInput", 176 | "UpdateChildPayload": "UpdateChildPayload", 177 | "UpdateParentByIdInput": "UpdateParentByIdInput", 178 | "UpdateParentInput": "UpdateParentInput", 179 | "UpdateParentPayload": "UpdateParentPayload", 180 | "__Directive": "__Directive", 181 | "__DirectiveLocation": "__DirectiveLocation", 182 | "__EnumValue": "__EnumValue", 183 | "__Field": "__Field", 184 | "__InputValue": "__InputValue", 185 | "__Schema": "__Schema", 186 | "__Type": "__Type", 187 | "__TypeKind": "__TypeKind", 188 | "updateChildOnChildForChildParentFkeyPatch": "updateChildOnChildForChildParentFkeyPatch", 189 | "updateParentOnChildForChildParentFkeyPatch": "updateParentOnChildForChildParentFkeyPatch", 190 | }, 191 | "astNode": undefined, 192 | "description": undefined, 193 | "extensionASTNodes": undefined, 194 | "extensions": undefined, 195 | } 196 | `; 197 | 198 | exports[`simple names, singular when one-to-one 1`] = ` 199 | GraphQLSchema { 200 | "__validationErrors": Array [], 201 | "_directives": Array [ 202 | "@include", 203 | "@skip", 204 | "@deprecated", 205 | "@specifiedBy", 206 | ], 207 | "_implementationsMap": Object { 208 | "Node": Object { 209 | "interfaces": Array [], 210 | "objects": Array [ 211 | "Query", 212 | "Child", 213 | "Parent", 214 | ], 215 | }, 216 | }, 217 | "_mutationType": "Mutation", 218 | "_queryType": "Query", 219 | "_subTypeMap": Object {}, 220 | "_subscriptionType": undefined, 221 | "_typeMap": Object { 222 | "Boolean": "Boolean", 223 | "Child": "Child", 224 | "ChildChildPkeyConnect": "ChildChildPkeyConnect", 225 | "ChildChildPkeyDelete": "ChildChildPkeyDelete", 226 | "ChildCondition": "ChildCondition", 227 | "ChildInput": "ChildInput", 228 | "ChildNodeIdConnect": "ChildNodeIdConnect", 229 | "ChildNodeIdDelete": "ChildNodeIdDelete", 230 | "ChildOnChildForChildParentFkeyNodeIdUpdate": "ChildOnChildForChildParentFkeyNodeIdUpdate", 231 | "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate": "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate", 232 | "ChildParentFkeyChildCreateInput": "ChildParentFkeyChildCreateInput", 233 | "ChildParentFkeyInput": "ChildParentFkeyInput", 234 | "ChildParentFkeyInverseInput": "ChildParentFkeyInverseInput", 235 | "ChildParentFkeyParentCreateInput": "ChildParentFkeyParentCreateInput", 236 | "ChildPatch": "ChildPatch", 237 | "ChildrenConnection": "ChildrenConnection", 238 | "ChildrenEdge": "ChildrenEdge", 239 | "ChildrenOrderBy": "ChildrenOrderBy", 240 | "CreateChildInput": "CreateChildInput", 241 | "CreateChildPayload": "CreateChildPayload", 242 | "CreateParentInput": "CreateParentInput", 243 | "CreateParentPayload": "CreateParentPayload", 244 | "Cursor": "Cursor", 245 | "DeleteChildByParentIdInput": "DeleteChildByParentIdInput", 246 | "DeleteChildInput": "DeleteChildInput", 247 | "DeleteChildPayload": "DeleteChildPayload", 248 | "DeleteParentByIdInput": "DeleteParentByIdInput", 249 | "DeleteParentInput": "DeleteParentInput", 250 | "DeleteParentPayload": "DeleteParentPayload", 251 | "ID": "ID", 252 | "Int": "Int", 253 | "Mutation": "Mutation", 254 | "Node": "Node", 255 | "PageInfo": "PageInfo", 256 | "Parent": "Parent", 257 | "ParentCondition": "ParentCondition", 258 | "ParentInput": "ParentInput", 259 | "ParentNodeIdConnect": "ParentNodeIdConnect", 260 | "ParentNodeIdDelete": "ParentNodeIdDelete", 261 | "ParentOnChildForChildParentFkeyNodeIdUpdate": "ParentOnChildForChildParentFkeyNodeIdUpdate", 262 | "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate": "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate", 263 | "ParentParentPkeyConnect": "ParentParentPkeyConnect", 264 | "ParentParentPkeyDelete": "ParentParentPkeyDelete", 265 | "ParentPatch": "ParentPatch", 266 | "ParentsConnection": "ParentsConnection", 267 | "ParentsEdge": "ParentsEdge", 268 | "ParentsOrderBy": "ParentsOrderBy", 269 | "Query": "Query", 270 | "String": "String", 271 | "UpdateChildByParentIdInput": "UpdateChildByParentIdInput", 272 | "UpdateChildInput": "UpdateChildInput", 273 | "UpdateChildPayload": "UpdateChildPayload", 274 | "UpdateParentByIdInput": "UpdateParentByIdInput", 275 | "UpdateParentInput": "UpdateParentInput", 276 | "UpdateParentPayload": "UpdateParentPayload", 277 | "__Directive": "__Directive", 278 | "__DirectiveLocation": "__DirectiveLocation", 279 | "__EnumValue": "__EnumValue", 280 | "__Field": "__Field", 281 | "__InputValue": "__InputValue", 282 | "__Schema": "__Schema", 283 | "__Type": "__Type", 284 | "__TypeKind": "__TypeKind", 285 | "updateChildOnChildForChildParentFkeyPatch": "updateChildOnChildForChildParentFkeyPatch", 286 | "updateParentOnChildForChildParentFkeyPatch": "updateParentOnChildForChildParentFkeyPatch", 287 | }, 288 | "astNode": undefined, 289 | "description": undefined, 290 | "extensionASTNodes": undefined, 291 | "extensions": undefined, 292 | } 293 | `; 294 | 295 | exports[`still plural when one-to-one if nestedMutationsOldUniqueFields is enabled 1`] = ` 296 | GraphQLSchema { 297 | "__validationErrors": Array [], 298 | "_directives": Array [ 299 | "@include", 300 | "@skip", 301 | "@deprecated", 302 | "@specifiedBy", 303 | ], 304 | "_implementationsMap": Object { 305 | "Node": Object { 306 | "interfaces": Array [], 307 | "objects": Array [ 308 | "Query", 309 | "Child", 310 | "Parent", 311 | ], 312 | }, 313 | }, 314 | "_mutationType": "Mutation", 315 | "_queryType": "Query", 316 | "_subTypeMap": Object {}, 317 | "_subscriptionType": undefined, 318 | "_typeMap": Object { 319 | "Boolean": "Boolean", 320 | "Child": "Child", 321 | "ChildChildPkeyConnect": "ChildChildPkeyConnect", 322 | "ChildChildPkeyDelete": "ChildChildPkeyDelete", 323 | "ChildCondition": "ChildCondition", 324 | "ChildInput": "ChildInput", 325 | "ChildNodeIdConnect": "ChildNodeIdConnect", 326 | "ChildNodeIdDelete": "ChildNodeIdDelete", 327 | "ChildOnChildForChildParentFkeyNodeIdUpdate": "ChildOnChildForChildParentFkeyNodeIdUpdate", 328 | "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate": "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate", 329 | "ChildParentFkeyChildCreateInput": "ChildParentFkeyChildCreateInput", 330 | "ChildParentFkeyInput": "ChildParentFkeyInput", 331 | "ChildParentFkeyInverseInput": "ChildParentFkeyInverseInput", 332 | "ChildParentFkeyParentCreateInput": "ChildParentFkeyParentCreateInput", 333 | "ChildPatch": "ChildPatch", 334 | "ChildrenConnection": "ChildrenConnection", 335 | "ChildrenEdge": "ChildrenEdge", 336 | "ChildrenOrderBy": "ChildrenOrderBy", 337 | "CreateChildInput": "CreateChildInput", 338 | "CreateChildPayload": "CreateChildPayload", 339 | "CreateParentInput": "CreateParentInput", 340 | "CreateParentPayload": "CreateParentPayload", 341 | "Cursor": "Cursor", 342 | "DeleteChildByParentIdInput": "DeleteChildByParentIdInput", 343 | "DeleteChildInput": "DeleteChildInput", 344 | "DeleteChildPayload": "DeleteChildPayload", 345 | "DeleteParentByIdInput": "DeleteParentByIdInput", 346 | "DeleteParentInput": "DeleteParentInput", 347 | "DeleteParentPayload": "DeleteParentPayload", 348 | "ID": "ID", 349 | "Int": "Int", 350 | "Mutation": "Mutation", 351 | "Node": "Node", 352 | "PageInfo": "PageInfo", 353 | "Parent": "Parent", 354 | "ParentCondition": "ParentCondition", 355 | "ParentInput": "ParentInput", 356 | "ParentNodeIdConnect": "ParentNodeIdConnect", 357 | "ParentNodeIdDelete": "ParentNodeIdDelete", 358 | "ParentOnChildForChildParentFkeyNodeIdUpdate": "ParentOnChildForChildParentFkeyNodeIdUpdate", 359 | "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate": "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate", 360 | "ParentParentPkeyConnect": "ParentParentPkeyConnect", 361 | "ParentParentPkeyDelete": "ParentParentPkeyDelete", 362 | "ParentPatch": "ParentPatch", 363 | "ParentsConnection": "ParentsConnection", 364 | "ParentsEdge": "ParentsEdge", 365 | "ParentsOrderBy": "ParentsOrderBy", 366 | "Query": "Query", 367 | "String": "String", 368 | "UpdateChildByParentIdInput": "UpdateChildByParentIdInput", 369 | "UpdateChildInput": "UpdateChildInput", 370 | "UpdateChildPayload": "UpdateChildPayload", 371 | "UpdateParentByIdInput": "UpdateParentByIdInput", 372 | "UpdateParentInput": "UpdateParentInput", 373 | "UpdateParentPayload": "UpdateParentPayload", 374 | "__Directive": "__Directive", 375 | "__DirectiveLocation": "__DirectiveLocation", 376 | "__EnumValue": "__EnumValue", 377 | "__Field": "__Field", 378 | "__InputValue": "__InputValue", 379 | "__Schema": "__Schema", 380 | "__Type": "__Type", 381 | "__TypeKind": "__TypeKind", 382 | "updateChildOnChildForChildParentFkeyPatch": "updateChildOnChildForChildParentFkeyPatch", 383 | "updateParentOnChildForChildParentFkeyPatch": "updateParentOnChildForChildParentFkeyPatch", 384 | }, 385 | "astNode": undefined, 386 | "description": undefined, 387 | "extensionASTNodes": undefined, 388 | "extensions": undefined, 389 | } 390 | `; 391 | -------------------------------------------------------------------------------- /__tests__/integration/__snapshots__/plurals.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`1:1 relationship does not allow multiple nested rows 1`] = ` 4 | GraphQLSchema { 5 | "__validationErrors": Array [], 6 | "_directives": Array [ 7 | "@include", 8 | "@skip", 9 | "@deprecated", 10 | "@specifiedBy", 11 | ], 12 | "_implementationsMap": Object { 13 | "Node": Object { 14 | "interfaces": Array [], 15 | "objects": Array [ 16 | "Query", 17 | "Post", 18 | "PostImage", 19 | ], 20 | }, 21 | }, 22 | "_mutationType": "Mutation", 23 | "_queryType": "Query", 24 | "_subTypeMap": Object {}, 25 | "_subscriptionType": undefined, 26 | "_typeMap": Object { 27 | "Boolean": "Boolean", 28 | "CreatePostImageInput": "CreatePostImageInput", 29 | "CreatePostImagePayload": "CreatePostImagePayload", 30 | "CreatePostInput": "CreatePostInput", 31 | "CreatePostPayload": "CreatePostPayload", 32 | "Cursor": "Cursor", 33 | "DeletePostByIdInput": "DeletePostByIdInput", 34 | "DeletePostImageByIdInput": "DeletePostImageByIdInput", 35 | "DeletePostImageByPostIdInput": "DeletePostImageByPostIdInput", 36 | "DeletePostImageInput": "DeletePostImageInput", 37 | "DeletePostImagePayload": "DeletePostImagePayload", 38 | "DeletePostInput": "DeletePostInput", 39 | "DeletePostPayload": "DeletePostPayload", 40 | "ID": "ID", 41 | "Int": "Int", 42 | "Mutation": "Mutation", 43 | "Node": "Node", 44 | "PageInfo": "PageInfo", 45 | "Post": "Post", 46 | "PostCondition": "PostCondition", 47 | "PostImage": "PostImage", 48 | "PostImageCondition": "PostImageCondition", 49 | "PostImageInput": "PostImageInput", 50 | "PostImageNodeIdConnect": "PostImageNodeIdConnect", 51 | "PostImageNodeIdDelete": "PostImageNodeIdDelete", 52 | "PostImageOnPostImageForPostImagePostIdFkeyNodeIdUpdate": "PostImageOnPostImageForPostImagePostIdFkeyNodeIdUpdate", 53 | "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePkeyUpdate": "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePkeyUpdate", 54 | "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePostIdKeyUpdate": "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePostIdKeyUpdate", 55 | "PostImagePatch": "PostImagePatch", 56 | "PostImagePostIdFkeyInput": "PostImagePostIdFkeyInput", 57 | "PostImagePostIdFkeyInverseInput": "PostImagePostIdFkeyInverseInput", 58 | "PostImagePostIdFkeyPostCreateInput": "PostImagePostIdFkeyPostCreateInput", 59 | "PostImagePostIdFkeyPostImageCreateInput": "PostImagePostIdFkeyPostImageCreateInput", 60 | "PostImagePostImagePkeyConnect": "PostImagePostImagePkeyConnect", 61 | "PostImagePostImagePkeyDelete": "PostImagePostImagePkeyDelete", 62 | "PostImagePostImagePostIdKeyConnect": "PostImagePostImagePostIdKeyConnect", 63 | "PostImagePostImagePostIdKeyDelete": "PostImagePostImagePostIdKeyDelete", 64 | "PostImagesConnection": "PostImagesConnection", 65 | "PostImagesEdge": "PostImagesEdge", 66 | "PostImagesOrderBy": "PostImagesOrderBy", 67 | "PostInput": "PostInput", 68 | "PostNodeIdConnect": "PostNodeIdConnect", 69 | "PostNodeIdDelete": "PostNodeIdDelete", 70 | "PostOnPostImageForPostImagePostIdFkeyNodeIdUpdate": "PostOnPostImageForPostImagePostIdFkeyNodeIdUpdate", 71 | "PostOnPostImageForPostImagePostIdFkeyUsingPostPkeyUpdate": "PostOnPostImageForPostImagePostIdFkeyUsingPostPkeyUpdate", 72 | "PostPatch": "PostPatch", 73 | "PostPostPkeyConnect": "PostPostPkeyConnect", 74 | "PostPostPkeyDelete": "PostPostPkeyDelete", 75 | "PostsConnection": "PostsConnection", 76 | "PostsEdge": "PostsEdge", 77 | "PostsOrderBy": "PostsOrderBy", 78 | "Query": "Query", 79 | "String": "String", 80 | "UpdatePostByIdInput": "UpdatePostByIdInput", 81 | "UpdatePostImageByIdInput": "UpdatePostImageByIdInput", 82 | "UpdatePostImageByPostIdInput": "UpdatePostImageByPostIdInput", 83 | "UpdatePostImageInput": "UpdatePostImageInput", 84 | "UpdatePostImagePayload": "UpdatePostImagePayload", 85 | "UpdatePostInput": "UpdatePostInput", 86 | "UpdatePostPayload": "UpdatePostPayload", 87 | "__Directive": "__Directive", 88 | "__DirectiveLocation": "__DirectiveLocation", 89 | "__EnumValue": "__EnumValue", 90 | "__Field": "__Field", 91 | "__InputValue": "__InputValue", 92 | "__Schema": "__Schema", 93 | "__Type": "__Type", 94 | "__TypeKind": "__TypeKind", 95 | "updatePostImageOnPostImageForPostImagePostIdFkeyPatch": "updatePostImageOnPostImageForPostImagePostIdFkeyPatch", 96 | "updatePostOnPostImageForPostImagePostIdFkeyPatch": "updatePostOnPostImageForPostImagePostIdFkeyPatch", 97 | }, 98 | "astNode": undefined, 99 | "description": undefined, 100 | "extensionASTNodes": undefined, 101 | "extensions": undefined, 102 | } 103 | `; 104 | 105 | exports[`1:1 relationship mutation fails when multiple operators are specified 1`] = ` 106 | GraphQLSchema { 107 | "__validationErrors": Array [], 108 | "_directives": Array [ 109 | "@include", 110 | "@skip", 111 | "@deprecated", 112 | "@specifiedBy", 113 | ], 114 | "_implementationsMap": Object { 115 | "Node": Object { 116 | "interfaces": Array [], 117 | "objects": Array [ 118 | "Query", 119 | "Post", 120 | "PostImage", 121 | ], 122 | }, 123 | }, 124 | "_mutationType": "Mutation", 125 | "_queryType": "Query", 126 | "_subTypeMap": Object {}, 127 | "_subscriptionType": undefined, 128 | "_typeMap": Object { 129 | "Boolean": "Boolean", 130 | "CreatePostImageInput": "CreatePostImageInput", 131 | "CreatePostImagePayload": "CreatePostImagePayload", 132 | "CreatePostInput": "CreatePostInput", 133 | "CreatePostPayload": "CreatePostPayload", 134 | "Cursor": "Cursor", 135 | "DeletePostByIdInput": "DeletePostByIdInput", 136 | "DeletePostImageByIdInput": "DeletePostImageByIdInput", 137 | "DeletePostImageByPostIdInput": "DeletePostImageByPostIdInput", 138 | "DeletePostImageInput": "DeletePostImageInput", 139 | "DeletePostImagePayload": "DeletePostImagePayload", 140 | "DeletePostInput": "DeletePostInput", 141 | "DeletePostPayload": "DeletePostPayload", 142 | "ID": "ID", 143 | "Int": "Int", 144 | "Mutation": "Mutation", 145 | "Node": "Node", 146 | "PageInfo": "PageInfo", 147 | "Post": "Post", 148 | "PostCondition": "PostCondition", 149 | "PostImage": "PostImage", 150 | "PostImageCondition": "PostImageCondition", 151 | "PostImageInput": "PostImageInput", 152 | "PostImageNodeIdConnect": "PostImageNodeIdConnect", 153 | "PostImageNodeIdDelete": "PostImageNodeIdDelete", 154 | "PostImageOnPostImageForPostImagePostIdFkeyNodeIdUpdate": "PostImageOnPostImageForPostImagePostIdFkeyNodeIdUpdate", 155 | "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePkeyUpdate": "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePkeyUpdate", 156 | "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePostIdKeyUpdate": "PostImageOnPostImageForPostImagePostIdFkeyUsingPostImagePostIdKeyUpdate", 157 | "PostImagePatch": "PostImagePatch", 158 | "PostImagePostIdFkeyInput": "PostImagePostIdFkeyInput", 159 | "PostImagePostIdFkeyInverseInput": "PostImagePostIdFkeyInverseInput", 160 | "PostImagePostIdFkeyPostCreateInput": "PostImagePostIdFkeyPostCreateInput", 161 | "PostImagePostIdFkeyPostImageCreateInput": "PostImagePostIdFkeyPostImageCreateInput", 162 | "PostImagePostImagePkeyConnect": "PostImagePostImagePkeyConnect", 163 | "PostImagePostImagePkeyDelete": "PostImagePostImagePkeyDelete", 164 | "PostImagePostImagePostIdKeyConnect": "PostImagePostImagePostIdKeyConnect", 165 | "PostImagePostImagePostIdKeyDelete": "PostImagePostImagePostIdKeyDelete", 166 | "PostImagesConnection": "PostImagesConnection", 167 | "PostImagesEdge": "PostImagesEdge", 168 | "PostImagesOrderBy": "PostImagesOrderBy", 169 | "PostInput": "PostInput", 170 | "PostNodeIdConnect": "PostNodeIdConnect", 171 | "PostNodeIdDelete": "PostNodeIdDelete", 172 | "PostOnPostImageForPostImagePostIdFkeyNodeIdUpdate": "PostOnPostImageForPostImagePostIdFkeyNodeIdUpdate", 173 | "PostOnPostImageForPostImagePostIdFkeyUsingPostPkeyUpdate": "PostOnPostImageForPostImagePostIdFkeyUsingPostPkeyUpdate", 174 | "PostPatch": "PostPatch", 175 | "PostPostPkeyConnect": "PostPostPkeyConnect", 176 | "PostPostPkeyDelete": "PostPostPkeyDelete", 177 | "PostsConnection": "PostsConnection", 178 | "PostsEdge": "PostsEdge", 179 | "PostsOrderBy": "PostsOrderBy", 180 | "Query": "Query", 181 | "String": "String", 182 | "UpdatePostByIdInput": "UpdatePostByIdInput", 183 | "UpdatePostImageByIdInput": "UpdatePostImageByIdInput", 184 | "UpdatePostImageByPostIdInput": "UpdatePostImageByPostIdInput", 185 | "UpdatePostImageInput": "UpdatePostImageInput", 186 | "UpdatePostImagePayload": "UpdatePostImagePayload", 187 | "UpdatePostInput": "UpdatePostInput", 188 | "UpdatePostPayload": "UpdatePostPayload", 189 | "__Directive": "__Directive", 190 | "__DirectiveLocation": "__DirectiveLocation", 191 | "__EnumValue": "__EnumValue", 192 | "__Field": "__Field", 193 | "__InputValue": "__InputValue", 194 | "__Schema": "__Schema", 195 | "__Type": "__Type", 196 | "__TypeKind": "__TypeKind", 197 | "updatePostImageOnPostImageForPostImagePostIdFkeyPatch": "updatePostImageOnPostImageForPostImagePostIdFkeyPatch", 198 | "updatePostOnPostImageForPostImagePostIdFkeyPatch": "updatePostOnPostImageForPostImagePostIdFkeyPatch", 199 | }, 200 | "astNode": undefined, 201 | "description": undefined, 202 | "extensionASTNodes": undefined, 203 | "extensions": undefined, 204 | } 205 | `; 206 | 207 | exports[`plural when one-to-many, singular in reverse 1`] = ` 208 | GraphQLSchema { 209 | "__validationErrors": Array [], 210 | "_directives": Array [ 211 | "@include", 212 | "@skip", 213 | "@deprecated", 214 | "@specifiedBy", 215 | ], 216 | "_implementationsMap": Object { 217 | "Node": Object { 218 | "interfaces": Array [], 219 | "objects": Array [ 220 | "Query", 221 | "Child", 222 | "Parent", 223 | ], 224 | }, 225 | }, 226 | "_mutationType": "Mutation", 227 | "_queryType": "Query", 228 | "_subTypeMap": Object {}, 229 | "_subscriptionType": undefined, 230 | "_typeMap": Object { 231 | "Boolean": "Boolean", 232 | "Child": "Child", 233 | "ChildChildPkeyConnect": "ChildChildPkeyConnect", 234 | "ChildChildPkeyDelete": "ChildChildPkeyDelete", 235 | "ChildCondition": "ChildCondition", 236 | "ChildInput": "ChildInput", 237 | "ChildNodeIdConnect": "ChildNodeIdConnect", 238 | "ChildNodeIdDelete": "ChildNodeIdDelete", 239 | "ChildOnChildForChildParentFkeyNodeIdUpdate": "ChildOnChildForChildParentFkeyNodeIdUpdate", 240 | "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate": "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate", 241 | "ChildParentFkeyChildCreateInput": "ChildParentFkeyChildCreateInput", 242 | "ChildParentFkeyInput": "ChildParentFkeyInput", 243 | "ChildParentFkeyInverseInput": "ChildParentFkeyInverseInput", 244 | "ChildParentFkeyParentCreateInput": "ChildParentFkeyParentCreateInput", 245 | "ChildPatch": "ChildPatch", 246 | "ChildrenConnection": "ChildrenConnection", 247 | "ChildrenEdge": "ChildrenEdge", 248 | "ChildrenOrderBy": "ChildrenOrderBy", 249 | "CreateChildInput": "CreateChildInput", 250 | "CreateChildPayload": "CreateChildPayload", 251 | "CreateParentInput": "CreateParentInput", 252 | "CreateParentPayload": "CreateParentPayload", 253 | "Cursor": "Cursor", 254 | "DeleteChildByIdInput": "DeleteChildByIdInput", 255 | "DeleteChildInput": "DeleteChildInput", 256 | "DeleteChildPayload": "DeleteChildPayload", 257 | "DeleteParentByIdInput": "DeleteParentByIdInput", 258 | "DeleteParentInput": "DeleteParentInput", 259 | "DeleteParentPayload": "DeleteParentPayload", 260 | "ID": "ID", 261 | "Int": "Int", 262 | "Mutation": "Mutation", 263 | "Node": "Node", 264 | "PageInfo": "PageInfo", 265 | "Parent": "Parent", 266 | "ParentCondition": "ParentCondition", 267 | "ParentInput": "ParentInput", 268 | "ParentNodeIdConnect": "ParentNodeIdConnect", 269 | "ParentNodeIdDelete": "ParentNodeIdDelete", 270 | "ParentOnChildForChildParentFkeyNodeIdUpdate": "ParentOnChildForChildParentFkeyNodeIdUpdate", 271 | "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate": "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate", 272 | "ParentParentPkeyConnect": "ParentParentPkeyConnect", 273 | "ParentParentPkeyDelete": "ParentParentPkeyDelete", 274 | "ParentPatch": "ParentPatch", 275 | "ParentsConnection": "ParentsConnection", 276 | "ParentsEdge": "ParentsEdge", 277 | "ParentsOrderBy": "ParentsOrderBy", 278 | "Query": "Query", 279 | "String": "String", 280 | "UpdateChildByIdInput": "UpdateChildByIdInput", 281 | "UpdateChildInput": "UpdateChildInput", 282 | "UpdateChildPayload": "UpdateChildPayload", 283 | "UpdateParentByIdInput": "UpdateParentByIdInput", 284 | "UpdateParentInput": "UpdateParentInput", 285 | "UpdateParentPayload": "UpdateParentPayload", 286 | "__Directive": "__Directive", 287 | "__DirectiveLocation": "__DirectiveLocation", 288 | "__EnumValue": "__EnumValue", 289 | "__Field": "__Field", 290 | "__InputValue": "__InputValue", 291 | "__Schema": "__Schema", 292 | "__Type": "__Type", 293 | "__TypeKind": "__TypeKind", 294 | "updateChildOnChildForChildParentFkeyPatch": "updateChildOnChildForChildParentFkeyPatch", 295 | "updateParentOnChildForChildParentFkeyPatch": "updateParentOnChildForChildParentFkeyPatch", 296 | }, 297 | "astNode": undefined, 298 | "description": undefined, 299 | "extensionASTNodes": undefined, 300 | "extensions": undefined, 301 | } 302 | `; 303 | 304 | exports[`singular when one-to-one 1`] = ` 305 | GraphQLSchema { 306 | "__validationErrors": Array [], 307 | "_directives": Array [ 308 | "@include", 309 | "@skip", 310 | "@deprecated", 311 | "@specifiedBy", 312 | ], 313 | "_implementationsMap": Object { 314 | "Node": Object { 315 | "interfaces": Array [], 316 | "objects": Array [ 317 | "Query", 318 | "Child", 319 | "Parent", 320 | ], 321 | }, 322 | }, 323 | "_mutationType": "Mutation", 324 | "_queryType": "Query", 325 | "_subTypeMap": Object {}, 326 | "_subscriptionType": undefined, 327 | "_typeMap": Object { 328 | "Boolean": "Boolean", 329 | "Child": "Child", 330 | "ChildChildPkeyConnect": "ChildChildPkeyConnect", 331 | "ChildChildPkeyDelete": "ChildChildPkeyDelete", 332 | "ChildCondition": "ChildCondition", 333 | "ChildInput": "ChildInput", 334 | "ChildNodeIdConnect": "ChildNodeIdConnect", 335 | "ChildNodeIdDelete": "ChildNodeIdDelete", 336 | "ChildOnChildForChildParentFkeyNodeIdUpdate": "ChildOnChildForChildParentFkeyNodeIdUpdate", 337 | "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate": "ChildOnChildForChildParentFkeyUsingChildPkeyUpdate", 338 | "ChildParentFkeyChildCreateInput": "ChildParentFkeyChildCreateInput", 339 | "ChildParentFkeyInput": "ChildParentFkeyInput", 340 | "ChildParentFkeyInverseInput": "ChildParentFkeyInverseInput", 341 | "ChildParentFkeyParentCreateInput": "ChildParentFkeyParentCreateInput", 342 | "ChildPatch": "ChildPatch", 343 | "ChildrenConnection": "ChildrenConnection", 344 | "ChildrenEdge": "ChildrenEdge", 345 | "ChildrenOrderBy": "ChildrenOrderBy", 346 | "CreateChildInput": "CreateChildInput", 347 | "CreateChildPayload": "CreateChildPayload", 348 | "CreateParentInput": "CreateParentInput", 349 | "CreateParentPayload": "CreateParentPayload", 350 | "Cursor": "Cursor", 351 | "DeleteChildByParentIdInput": "DeleteChildByParentIdInput", 352 | "DeleteChildInput": "DeleteChildInput", 353 | "DeleteChildPayload": "DeleteChildPayload", 354 | "DeleteParentByIdInput": "DeleteParentByIdInput", 355 | "DeleteParentInput": "DeleteParentInput", 356 | "DeleteParentPayload": "DeleteParentPayload", 357 | "ID": "ID", 358 | "Int": "Int", 359 | "Mutation": "Mutation", 360 | "Node": "Node", 361 | "PageInfo": "PageInfo", 362 | "Parent": "Parent", 363 | "ParentCondition": "ParentCondition", 364 | "ParentInput": "ParentInput", 365 | "ParentNodeIdConnect": "ParentNodeIdConnect", 366 | "ParentNodeIdDelete": "ParentNodeIdDelete", 367 | "ParentOnChildForChildParentFkeyNodeIdUpdate": "ParentOnChildForChildParentFkeyNodeIdUpdate", 368 | "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate": "ParentOnChildForChildParentFkeyUsingParentPkeyUpdate", 369 | "ParentParentPkeyConnect": "ParentParentPkeyConnect", 370 | "ParentParentPkeyDelete": "ParentParentPkeyDelete", 371 | "ParentPatch": "ParentPatch", 372 | "ParentsConnection": "ParentsConnection", 373 | "ParentsEdge": "ParentsEdge", 374 | "ParentsOrderBy": "ParentsOrderBy", 375 | "Query": "Query", 376 | "String": "String", 377 | "UpdateChildByParentIdInput": "UpdateChildByParentIdInput", 378 | "UpdateChildInput": "UpdateChildInput", 379 | "UpdateChildPayload": "UpdateChildPayload", 380 | "UpdateParentByIdInput": "UpdateParentByIdInput", 381 | "UpdateParentInput": "UpdateParentInput", 382 | "UpdateParentPayload": "UpdateParentPayload", 383 | "__Directive": "__Directive", 384 | "__DirectiveLocation": "__DirectiveLocation", 385 | "__EnumValue": "__EnumValue", 386 | "__Field": "__Field", 387 | "__InputValue": "__InputValue", 388 | "__Schema": "__Schema", 389 | "__Type": "__Type", 390 | "__TypeKind": "__TypeKind", 391 | "updateChildOnChildForChildParentFkeyPatch": "updateChildOnChildForChildParentFkeyPatch", 392 | "updateParentOnChildForChildParentFkeyPatch": "updateParentOnChildForChildParentFkeyPatch", 393 | }, 394 | "astNode": undefined, 395 | "description": undefined, 396 | "extensionASTNodes": undefined, 397 | "extensions": undefined, 398 | } 399 | `; 400 | -------------------------------------------------------------------------------- /__tests__/integration/create.test.js: -------------------------------------------------------------------------------- 1 | const { graphql } = require('graphql'); 2 | const { withSchema } = require('../helpers'); 3 | 4 | test( 5 | 'table with no relations is not affected by plugin', 6 | withSchema({ 7 | setup: ` 8 | create table p.parent ( 9 | id serial primary key, 10 | name text not null 11 | );`, 12 | test: async ({ schema, pgClient }) => { 13 | const query = ` 14 | mutation { 15 | createParent( 16 | input: { 17 | parent: { 18 | name: "test f1" 19 | } 20 | } 21 | ) { 22 | parent { 23 | id 24 | name 25 | } 26 | } 27 | } 28 | `; 29 | expect(schema).toMatchSnapshot(); 30 | 31 | const result = await graphql(schema, query, null, { pgClient }); 32 | expect(result).not.toHaveProperty('errors'); 33 | }, 34 | }), 35 | ); 36 | 37 | test( 38 | 'forward nested mutation creates records', 39 | withSchema({ 40 | setup: ` 41 | create table p.parent ( 42 | id serial primary key, 43 | name text not null 44 | ); 45 | 46 | create table p.child ( 47 | id serial primary key, 48 | parent_id integer, 49 | name text not null, 50 | constraint child_parent_fkey foreign key (parent_id) 51 | references p.parent (id) 52 | ); 53 | `, 54 | test: async ({ schema, pgClient }) => { 55 | const query = ` 56 | mutation { 57 | createParent( 58 | input: { 59 | parent: { 60 | name: "test f1" 61 | childrenUsingId: { 62 | create: [{ 63 | name: "child 1 of test f1" 64 | }, { 65 | name: "child 2 of test f1" 66 | }] 67 | } 68 | } 69 | } 70 | ) { 71 | parent { 72 | id 73 | name 74 | childrenByParentId { 75 | nodes { 76 | id 77 | parentId 78 | name 79 | } 80 | } 81 | } 82 | } 83 | } 84 | `; 85 | expect(schema).toMatchSnapshot(); 86 | 87 | const result = await graphql(schema, query, null, { pgClient }); 88 | expect(result).not.toHaveProperty('errors'); 89 | 90 | const data = result.data.createParent.parent; 91 | expect(data.childrenByParentId.nodes).toHaveLength(2); 92 | data.childrenByParentId.nodes.map((n) => 93 | expect(n.parentId).toBe(data.id), 94 | ); 95 | }, 96 | }), 97 | ); 98 | 99 | test( 100 | 'forward nested mutation with null nested fields succeeds', 101 | withSchema({ 102 | setup: ` 103 | create table p.parent ( 104 | id serial primary key, 105 | name text not null 106 | ); 107 | 108 | create table p.child ( 109 | id serial primary key, 110 | parent_id integer, 111 | name text not null, 112 | constraint child_parent_fkey foreign key (parent_id) 113 | references p.parent (id) 114 | ); 115 | 116 | insert into p.child values(1, null, 'test child 1'); 117 | `, 118 | test: async ({ schema, pgClient }) => { 119 | const query = ` 120 | mutation { 121 | createParent( 122 | input: { 123 | parent: { 124 | name: "test f1" 125 | childrenUsingId: { 126 | create: null 127 | connectByNodeId: null 128 | connectById: null 129 | deleteByNodeId: null 130 | deleteById: null 131 | updateByNodeId: null 132 | updateById: null 133 | } 134 | } 135 | } 136 | ) { 137 | parent { 138 | id 139 | name 140 | childrenByParentId { 141 | nodes { 142 | id 143 | parentId 144 | name 145 | } 146 | } 147 | } 148 | } 149 | } 150 | `; 151 | expect(schema).toMatchSnapshot(); 152 | 153 | const result = await graphql(schema, query, null, { pgClient }); 154 | expect(result).not.toHaveProperty('errors'); 155 | 156 | const data = result.data.createParent.parent; 157 | expect(data.childrenByParentId.nodes).toHaveLength(0); 158 | }, 159 | }), 160 | ); 161 | 162 | test( 163 | 'forward nested mutation with empty nested fields succeeds', 164 | withSchema({ 165 | setup: ` 166 | create table p.parent ( 167 | id serial primary key, 168 | name text not null 169 | ); 170 | 171 | create table p.child ( 172 | id serial primary key, 173 | parent_id integer, 174 | name text not null, 175 | constraint child_parent_fkey foreign key (parent_id) 176 | references p.parent (id) 177 | ); 178 | 179 | insert into p.child values(1, null, 'test child 1'); 180 | `, 181 | test: async ({ schema, pgClient }) => { 182 | const query = ` 183 | mutation { 184 | createParent( 185 | input: { 186 | parent: { 187 | name: "test f1" 188 | childrenUsingId: { 189 | create: [] 190 | connectByNodeId: [] 191 | connectById: [] 192 | deleteByNodeId: [] 193 | deleteById: [] 194 | updateByNodeId: [] 195 | updateById: [] 196 | } 197 | } 198 | } 199 | ) { 200 | parent { 201 | id 202 | name 203 | childrenByParentId { 204 | nodes { 205 | id 206 | parentId 207 | name 208 | } 209 | } 210 | } 211 | } 212 | } 213 | `; 214 | expect(schema).toMatchSnapshot(); 215 | 216 | const result = await graphql(schema, query, null, { pgClient }); 217 | expect(result).not.toHaveProperty('errors'); 218 | 219 | const data = result.data.createParent.parent; 220 | expect(data.childrenByParentId.nodes).toHaveLength(0); 221 | }, 222 | }), 223 | ); 224 | 225 | test( 226 | 'forward nested mutation with null outer nested field succeeds', 227 | withSchema({ 228 | setup: ` 229 | create table p.parent ( 230 | id serial primary key, 231 | name text not null 232 | ); 233 | 234 | create table p.child ( 235 | id serial primary key, 236 | parent_id integer, 237 | name text not null, 238 | constraint child_parent_fkey foreign key (parent_id) 239 | references p.parent (id) 240 | ); 241 | 242 | insert into p.child values(6, null, 'test child 1'); 243 | `, 244 | test: async ({ schema, pgClient }) => { 245 | const query = ` 246 | mutation { 247 | a1: createParent( 248 | input: { 249 | parent: { 250 | name: "test f1" 251 | childrenUsingId: null 252 | } 253 | } 254 | ) { 255 | parent { 256 | id 257 | name 258 | childrenByParentId { 259 | nodes { 260 | id 261 | parentId 262 | name 263 | } 264 | } 265 | } 266 | } 267 | a2: createChild( 268 | input: { 269 | child: { 270 | name: "test child 2" 271 | parentToParentId: null 272 | } 273 | } 274 | ) { 275 | child { 276 | id 277 | } 278 | } 279 | } 280 | `; 281 | expect(schema).toMatchSnapshot(); 282 | 283 | const result = await graphql(schema, query, null, { pgClient }); 284 | expect(result).not.toHaveProperty('errors'); 285 | }, 286 | }), 287 | ); 288 | test( 289 | 'forward deeply nested mutation creates records', 290 | withSchema({ 291 | setup: ` 292 | create table p.parent ( 293 | id serial primary key, 294 | name text not null 295 | ); 296 | 297 | create table p.child ( 298 | id serial primary key, 299 | parent_id integer, 300 | name text not null, 301 | constraint child_parent_fkey foreign key (parent_id) 302 | references p.parent (id) 303 | ); 304 | 305 | create table p.grandchild ( 306 | id serial primary key, 307 | child_id integer not null, 308 | name text not null, 309 | constraint grandchild_child_fkey foreign key (child_id) 310 | references p.child (id) 311 | ); 312 | `, 313 | test: async ({ schema, pgClient }) => { 314 | const query = ` 315 | mutation { 316 | createParent( 317 | input: { 318 | parent: { 319 | name: "test f1" 320 | childrenUsingId: { 321 | create: [{ 322 | name: "child 1 of test f1" 323 | grandchildrenUsingId: { 324 | create: [{ 325 | name: "grandchild 1 of child 1" 326 | }] 327 | } 328 | }, { 329 | name: "child 2 of test f1" 330 | grandchildrenUsingId: { 331 | create: [{ 332 | name: "grandchild 1 of child 2" 333 | }] 334 | } 335 | }] 336 | } 337 | } 338 | } 339 | ) { 340 | parent { 341 | id 342 | name 343 | childrenByParentId { 344 | nodes { 345 | id 346 | parentId 347 | name 348 | grandchildrenByChildId { 349 | nodes { 350 | id 351 | childId 352 | name 353 | } 354 | } 355 | } 356 | } 357 | } 358 | } 359 | } 360 | `; 361 | expect(schema).toMatchSnapshot(); 362 | 363 | const result = await graphql(schema, query, null, { pgClient }); 364 | expect(result).not.toHaveProperty('errors'); 365 | 366 | const data = result.data.createParent.parent; 367 | expect(data.childrenByParentId.nodes).toHaveLength(2); 368 | data.childrenByParentId.nodes.map((n) => 369 | expect(n.parentId).toBe(data.id), 370 | ); 371 | }, 372 | }), 373 | ); 374 | 375 | test( 376 | 'forward nested mutation connects existing records and creates simultaneously', 377 | withSchema({ 378 | setup: ` 379 | create table p.parent ( 380 | id serial primary key, 381 | name text not null 382 | ); 383 | 384 | create table p.child ( 385 | id serial primary key, 386 | parent_id integer, 387 | name text not null, 388 | constraint child_parent_fkey foreign key (parent_id) 389 | references p.parent (id) 390 | ); 391 | 392 | insert into p.child values (123, null, 'unattached child'); 393 | insert into p.child values (124, null, 'unattached child'); 394 | insert into p.child values (125, null, 'unattached child'); 395 | `, 396 | test: async ({ schema, pgClient }) => { 397 | const lookupQuery = ` 398 | query { 399 | childById(id: 125) { 400 | nodeId 401 | } 402 | } 403 | `; 404 | const lookupResult = await graphql(schema, lookupQuery, null, { 405 | pgClient, 406 | }); 407 | const { nodeId } = lookupResult.data.childById; 408 | expect(nodeId).not.toBeUndefined(); 409 | 410 | const query = ` 411 | mutation { 412 | createParent( 413 | input: { 414 | parent: { 415 | name: "test f1" 416 | childrenUsingId: { 417 | connectById: [{ 418 | id: 123 419 | }, { 420 | id: 124 421 | }] 422 | connectByNodeId: [{ 423 | nodeId: "${nodeId}" 424 | }] 425 | create: [{ 426 | name: "child 1" 427 | }, { 428 | name: "child 2" 429 | }] 430 | } 431 | } 432 | } 433 | ) { 434 | parent { 435 | id 436 | name 437 | childrenByParentId { 438 | nodes { 439 | id 440 | parentId 441 | name 442 | } 443 | } 444 | } 445 | } 446 | } 447 | `; 448 | expect(schema).toMatchSnapshot(); 449 | 450 | const result = await graphql(schema, query, null, { pgClient }); 451 | expect(result).not.toHaveProperty('errors'); 452 | 453 | const data = result.data.createParent.parent; 454 | expect(data.childrenByParentId.nodes).toHaveLength(5); 455 | data.childrenByParentId.nodes.map((n) => 456 | expect(n.parentId).toBe(data.id), 457 | ); 458 | }, 459 | }), 460 | ); 461 | 462 | test( 463 | 'invalid nodeId fails', 464 | withSchema({ 465 | setup: ` 466 | create table p.parent ( 467 | id serial primary key, 468 | name text not null 469 | ); 470 | 471 | create table p.child ( 472 | id serial primary key, 473 | parent_id integer, 474 | name text not null, 475 | constraint child_parent_fkey foreign key (parent_id) 476 | references p.parent (id) 477 | ); 478 | `, 479 | test: async ({ schema, pgClient }) => { 480 | const query = ` 481 | mutation { 482 | createParent( 483 | input: { 484 | parent: { 485 | name: "test f1" 486 | childrenUsingId: { 487 | connectByNodeId: [{ 488 | nodeId: "W10=" 489 | }] 490 | } 491 | } 492 | } 493 | ) { 494 | parent { 495 | id 496 | name 497 | childrenByParentId { 498 | nodes { 499 | id 500 | parentId 501 | name 502 | } 503 | } 504 | } 505 | } 506 | } 507 | `; 508 | expect(schema).toMatchSnapshot(); 509 | 510 | const result = await graphql(schema, query, null, { pgClient }); 511 | expect(result).toHaveProperty('errors'); 512 | expect(result.errors[0].message).toMatch(/Mismatched type/); 513 | }, 514 | }), 515 | ); 516 | 517 | test( 518 | 'reverse nested mutation creates records', 519 | withSchema({ 520 | setup: ` 521 | create table p.parent ( 522 | id serial primary key, 523 | name text not null 524 | ); 525 | 526 | create table p.child ( 527 | id serial primary key, 528 | parent_id integer, 529 | name text not null, 530 | constraint child_parent_fkey foreign key (parent_id) 531 | references p.parent (id) 532 | ); 533 | `, 534 | test: async ({ schema, pgClient }) => { 535 | const query = ` 536 | mutation { 537 | createChild( 538 | input: { 539 | child: { 540 | name: "test f1" 541 | parentToParentId: { 542 | create: { 543 | name: "parent of f1" 544 | } 545 | } 546 | } 547 | } 548 | ) { 549 | child { 550 | id 551 | name 552 | parentId 553 | parentByParentId { 554 | id 555 | name 556 | } 557 | } 558 | } 559 | } 560 | `; 561 | expect(schema).toMatchSnapshot(); 562 | 563 | const result = await graphql(schema, query, null, { pgClient }); 564 | expect(result).not.toHaveProperty('errors'); 565 | 566 | const data = result.data.createChild.child; 567 | expect(data.parentByParentId.id).toEqual(data.parentId); 568 | }, 569 | }), 570 | ); 571 | 572 | test( 573 | 'reverse nested mutation connects to existing records', 574 | withSchema({ 575 | setup: ` 576 | create table p.parent ( 577 | id serial primary key, 578 | name text not null 579 | ); 580 | 581 | create table p.child ( 582 | id serial primary key, 583 | parent_id integer, 584 | name text not null, 585 | constraint child_parent_fkey foreign key (parent_id) 586 | references p.parent (id) 587 | ); 588 | insert into p.parent values (1000, 'parent 1'); 589 | `, 590 | test: async ({ schema, pgClient }) => { 591 | const query = ` 592 | mutation { 593 | createChild( 594 | input: { 595 | child: { 596 | name: "test f1" 597 | parentToParentId: { 598 | connectById: { 599 | id: 1000 600 | } 601 | } 602 | } 603 | } 604 | ) { 605 | child { 606 | id 607 | name 608 | parentId 609 | parentByParentId { 610 | id 611 | name 612 | } 613 | } 614 | } 615 | } 616 | `; 617 | expect(schema).toMatchSnapshot(); 618 | 619 | const result = await graphql(schema, query, null, { pgClient }); 620 | expect(result).not.toHaveProperty('errors'); 621 | 622 | const data = result.data.createChild.child; 623 | expect(data.parentId).toEqual(1000); 624 | expect(data.parentByParentId.id).toEqual(data.parentId); 625 | }, 626 | }), 627 | ); 628 | 629 | test( 630 | 'reverse nested mutation connects by nodeId to existing records', 631 | withSchema({ 632 | setup: ` 633 | create table p.parent ( 634 | id serial primary key, 635 | name text not null 636 | ); 637 | 638 | create table p.child ( 639 | id serial primary key, 640 | parent_id integer, 641 | name text not null, 642 | constraint child_parent_fkey foreign key (parent_id) 643 | references p.parent (id) 644 | ); 645 | insert into p.parent values (1000, 'parent 1'); 646 | `, 647 | test: async ({ schema, pgClient }) => { 648 | const lookupQuery = ` 649 | query { 650 | parentById(id: 1000) { 651 | nodeId 652 | } 653 | } 654 | `; 655 | const lookupResult = await graphql(schema, lookupQuery, null, { 656 | pgClient, 657 | }); 658 | const { nodeId } = lookupResult.data.parentById; 659 | expect(nodeId).not.toBeUndefined(); 660 | 661 | const query = ` 662 | mutation { 663 | createChild( 664 | input: { 665 | child: { 666 | name: "test f1" 667 | parentToParentId: { 668 | connectByNodeId: { 669 | nodeId: "${nodeId}" 670 | } 671 | } 672 | } 673 | } 674 | ) { 675 | child { 676 | id 677 | name 678 | parentId 679 | parentByParentId { 680 | id 681 | name 682 | } 683 | } 684 | } 685 | } 686 | `; 687 | expect(schema).toMatchSnapshot(); 688 | 689 | const result = await graphql(schema, query, null, { pgClient }); 690 | expect(result).not.toHaveProperty('errors'); 691 | 692 | const data = result.data.createChild.child; 693 | expect(data.parentId).toEqual(1000); 694 | expect(data.parentByParentId.id).toEqual(data.parentId); 695 | }, 696 | }), 697 | ); 698 | 699 | test( 700 | 'forward nested mutation using uuid pkey creates records', 701 | withSchema({ 702 | setup: ` 703 | create table p.parent ( 704 | id uuid not null primary key default uuid_generate_v4(), 705 | name text not null 706 | ); 707 | 708 | create table p.child ( 709 | id uuid not null primary key default uuid_generate_v4(), 710 | parent_id uuid not null, 711 | name text not null, 712 | constraint child_parent_fkey foreign key (parent_id) 713 | references p.parent (id) 714 | ); 715 | `, 716 | test: async ({ schema, pgClient }) => { 717 | const query = ` 718 | mutation { 719 | createParent( 720 | input: { 721 | parent: { 722 | id: "0609e1cc-4f01-4c33-a7c0-aee402e9d043" 723 | name: "test f1" 724 | childrenUsingId: { 725 | create: [{ 726 | id: "dbb34d5a-c4e1-4b42-9d0d-a3e546f02a94" 727 | name: "child 1" 728 | }, { 729 | id: "d9deb95b-1a69-4178-aa7c-834ed54edb91" 730 | name: "child 2" 731 | }] 732 | } 733 | } 734 | } 735 | ) { 736 | parent { 737 | id 738 | name 739 | childrenByParentId { 740 | nodes { 741 | id 742 | parentId 743 | name 744 | } 745 | } 746 | } 747 | } 748 | } 749 | `; 750 | expect(schema).toMatchSnapshot(); 751 | 752 | const result = await graphql(schema, query, null, { pgClient }); 753 | expect(result).not.toHaveProperty('errors'); 754 | 755 | const data = result.data.createParent.parent; 756 | expect(data.childrenByParentId.nodes).toHaveLength(2); 757 | data.childrenByParentId.nodes.map((n) => 758 | expect(n.parentId).toEqual(data.id), 759 | ); 760 | }, 761 | }), 762 | ); 763 | 764 | test( 765 | 'forward nested mutation using composite pkey creates records', 766 | withSchema({ 767 | setup: ` 768 | create table p.parent ( 769 | id serial, 770 | name text not null, 771 | constraint parent_pkey primary key (id, name) 772 | ); 773 | 774 | create table p.child ( 775 | id serial primary key, 776 | name text not null, 777 | parent_id integer not null, 778 | parent_name text not null, 779 | constraint child_parent_fkey foreign key (parent_id, parent_name) 780 | references p.parent (id, name) 781 | ); 782 | `, 783 | test: async ({ schema, pgClient }) => { 784 | const query = ` 785 | mutation { 786 | createParent( 787 | input: { 788 | parent: { 789 | name: "test f1" 790 | childrenUsingIdAndName: { 791 | create: [{ 792 | name: "child 1" 793 | }, { 794 | name: "child 2" 795 | }] 796 | } 797 | } 798 | } 799 | ) { 800 | parent { 801 | id 802 | name 803 | childrenByParentIdAndParentName { 804 | nodes { 805 | id 806 | parentId 807 | parentName 808 | name 809 | } 810 | } 811 | } 812 | } 813 | } 814 | `; 815 | expect(schema).toMatchSnapshot(); 816 | 817 | const result = await graphql(schema, query, null, { pgClient }); 818 | expect(result).not.toHaveProperty('errors'); 819 | 820 | const data = result.data.createParent.parent; 821 | expect(data.childrenByParentIdAndParentName.nodes).toHaveLength(2); 822 | data.childrenByParentIdAndParentName.nodes.map((n) => 823 | expect([n.parentId, n.parentName]).toEqual([data.id, data.name]), 824 | ); 825 | }, 826 | }), 827 | ); 828 | 829 | // https://github.com/mlipscombe/postgraphile-plugin-nested-mutations/issues/1 830 | test( 831 | 'forward nested mutation with composite key on child table creates records', 832 | withSchema({ 833 | setup: ` 834 | create table p.parent ( 835 | id uuid default uuid_generate_v4(), 836 | primary key (id) 837 | ); 838 | 839 | create table p.child ( 840 | parent_id uuid not null, 841 | service_id varchar(50) not null, 842 | name varchar(50) not null, 843 | val varchar(50) not null, 844 | primary key (parent_id, service_id, name, val), 845 | foreign key (parent_id) references p.parent (id) 846 | ); 847 | `, 848 | test: async ({ schema, pgClient }) => { 849 | const query = ` 850 | mutation { 851 | createParent( 852 | input: { 853 | parent: { 854 | childrenUsingId: { 855 | create: [{ 856 | name: "child 1 of test f1" 857 | serviceId: "test" 858 | val: "test" 859 | }, { 860 | name: "child 2 of test f1" 861 | serviceId: "test" 862 | val: "test" 863 | }] 864 | } 865 | } 866 | } 867 | ) { 868 | parent { 869 | id 870 | childrenByParentId { 871 | nodes { 872 | parentId 873 | serviceId 874 | val 875 | } 876 | } 877 | } 878 | } 879 | } 880 | `; 881 | expect(schema).toMatchSnapshot(); 882 | 883 | const result = await graphql(schema, query, null, { pgClient }); 884 | expect(result).not.toHaveProperty('errors'); 885 | 886 | const data = result.data.createParent.parent; 887 | expect(data.childrenByParentId.nodes).toHaveLength(2); 888 | data.childrenByParentId.nodes.map((n) => 889 | expect(n.parentId).toBe(data.id), 890 | ); 891 | }, 892 | }), 893 | ); 894 | 895 | // https://github.com/mlipscombe/postgraphile-plugin-nested-mutations/issues/9 896 | test( 897 | 'works with multiple fkeys to the same related table', 898 | withSchema({ 899 | setup: ` 900 | create table p.job ( 901 | id serial primary key 902 | ); 903 | 904 | create table p.job_relationship ( 905 | type text, 906 | from_job_id integer references p.job(id), 907 | to_job_id integer references p.job(id) 908 | ); 909 | `, 910 | test: async ({ schema, pgClient }) => { 911 | const query = ` 912 | mutation { 913 | createJob( 914 | input: { 915 | job: { 916 | jobRelationshipsToToJobIdUsingId: { 917 | create: [{ 918 | type: "test" 919 | }] 920 | } 921 | } 922 | } 923 | ) { 924 | job { 925 | id 926 | jobRelationshipsByToJobId { 927 | nodes { 928 | type 929 | toJobId 930 | fromJobId 931 | } 932 | } 933 | jobRelationshipsByFromJobId { 934 | nodes { 935 | type 936 | toJobId 937 | fromJobId 938 | } 939 | } 940 | } 941 | } 942 | } 943 | `; 944 | expect(schema).toMatchSnapshot(); 945 | 946 | const result = await graphql(schema, query, null, { pgClient }); 947 | expect(result).not.toHaveProperty('errors'); 948 | 949 | const data = result.data.createJob.job; 950 | expect(data.jobRelationshipsByToJobId.nodes).toHaveLength(1); 951 | expect(data.jobRelationshipsByFromJobId.nodes).toHaveLength(0); 952 | }, 953 | }), 954 | ); 955 | 956 | // https://github.com/mlipscombe/postgraphile-plugin-nested-mutations/issues/33 957 | test( 958 | 'works with tables that relate to themselves - child > parent', 959 | withSchema({ 960 | setup: ` 961 | create table p.person ( 962 | id serial primary key, 963 | parent_id integer, 964 | name text not null, 965 | constraint person_parent_fkey foreign key (parent_id) 966 | references p.person(id) 967 | ); 968 | `, 969 | test: async ({ schema, pgClient }) => { 970 | const query = ` 971 | mutation { 972 | createPerson( 973 | input: { 974 | person: { 975 | name: "test child" 976 | personToParentId: { 977 | create: { 978 | name: "test parent" 979 | personToParentId: { 980 | create: { 981 | name: "test parent of parent" 982 | } 983 | } 984 | } 985 | } 986 | } 987 | } 988 | ) { 989 | person { 990 | id 991 | name 992 | parentId 993 | personByParentId { 994 | id 995 | name 996 | parentId 997 | personByParentId { 998 | id 999 | name 1000 | parentId 1001 | } 1002 | } 1003 | } 1004 | } 1005 | } 1006 | `; 1007 | expect(schema).toMatchSnapshot(); 1008 | 1009 | const result = await graphql(schema, query, null, { pgClient }); 1010 | expect(result).not.toHaveProperty('errors'); 1011 | 1012 | const data = result.data.createPerson.person; 1013 | expect(data.personByParentId).toHaveProperty('id'); 1014 | }, 1015 | }), 1016 | ); 1017 | 1018 | // https://github.com/mlipscombe/postgraphile-plugin-nested-mutations/issues/31 1019 | test( 1020 | 'deleteBy works', 1021 | withSchema({ 1022 | setup: ` 1023 | CREATE TABLE p.a (a_id SERIAL PRIMARY KEY, name TEXT); 1024 | CREATE TABLE p.b (b_id SERIAL PRIMARY KEY, name TEXT); 1025 | CREATE TABLE p.a_b (a_id INT REFERENCES p.a(a_id), b_id INT REFERENCES p.b(b_id), PRIMARY KEY (a_id, b_id)); 1026 | `, 1027 | test: async ({ schema, pgClient }) => { 1028 | const query = ` 1029 | mutation { 1030 | createA( 1031 | input: { a: { aBsUsingAId: { deleteByAIdAndBId: { aId: 10, bId: 10 } } } } 1032 | ) { 1033 | clientMutationId 1034 | } 1035 | } 1036 | `; 1037 | expect(schema).toMatchSnapshot(); 1038 | 1039 | const result = await graphql(schema, query, null, { pgClient }); 1040 | expect(result).not.toHaveProperty('errors'); 1041 | 1042 | // const data = result.data.createPerson.person; 1043 | // expect(data.personByParentId).toHaveProperty('id'); 1044 | }, 1045 | }), 1046 | ); 1047 | 1048 | test( 1049 | 'reverse nested mutation with non-serial / default parent PK creates record', 1050 | withSchema({ 1051 | setup: ` 1052 | create table p.parent ( 1053 | id text primary key, 1054 | name text not null 1055 | ); 1056 | 1057 | create table p.child ( 1058 | id serial primary key, 1059 | parent_id text, 1060 | name text not null, 1061 | constraint child_parent_fkey foreign key (parent_id) 1062 | references p.parent (id) 1063 | ); 1064 | `, 1065 | test: async ({ schema, pgClient }) => { 1066 | const query = ` 1067 | mutation { 1068 | createChild( 1069 | input: { 1070 | child: { 1071 | name: "test f1" 1072 | parentId: "123" 1073 | parentToParentId: { 1074 | create: { 1075 | id: "123" 1076 | name: "parent of f1" 1077 | } 1078 | } 1079 | } 1080 | } 1081 | ) { 1082 | child { 1083 | id 1084 | name 1085 | parentId 1086 | parentByParentId { 1087 | id 1088 | name 1089 | } 1090 | } 1091 | } 1092 | } 1093 | `; 1094 | expect(schema).toMatchSnapshot(); 1095 | 1096 | const result = await graphql(schema, query, null, { pgClient }); 1097 | expect(result).not.toHaveProperty('errors'); 1098 | 1099 | const data = result.data.createChild.child; 1100 | expect(data.parentByParentId.id).toEqual(data.parentId); 1101 | }, 1102 | }), 1103 | ); 1104 | 1105 | test( 1106 | 'reverse nested mutation with same column names creates record', 1107 | withSchema({ 1108 | setup: ` 1109 | create table p.parent ( 1110 | parent_id text primary key, 1111 | name text not null 1112 | ); 1113 | 1114 | create table p.child ( 1115 | id serial primary key, 1116 | parent_id text, 1117 | name text not null, 1118 | constraint child_parent_fkey foreign key (parent_id) 1119 | references p.parent (parent_id) 1120 | ); 1121 | `, 1122 | test: async ({ schema, pgClient }) => { 1123 | const query = ` 1124 | mutation { 1125 | createChild( 1126 | input: { 1127 | child: { 1128 | name: "test f1" 1129 | parentToParentId: { 1130 | create: { 1131 | parentId: "123" 1132 | name: "parent of f1" 1133 | } 1134 | } 1135 | } 1136 | } 1137 | ) { 1138 | child { 1139 | id 1140 | name 1141 | parentId 1142 | parentByParentId { 1143 | parentId 1144 | name 1145 | } 1146 | } 1147 | } 1148 | } 1149 | `; 1150 | expect(schema).toMatchSnapshot(); 1151 | 1152 | const result = await graphql(schema, query, null, { pgClient }); 1153 | expect(result).not.toHaveProperty('errors'); 1154 | 1155 | const data = result.data.createChild.child; 1156 | expect(data.parentByParentId.parentId).toEqual(data.parentId); 1157 | }, 1158 | }), 1159 | ); 1160 | -------------------------------------------------------------------------------- /__tests__/integration/options.test.js: -------------------------------------------------------------------------------- 1 | const { graphql } = require('graphql'); 2 | const { withSchema } = require('../helpers'); 3 | 4 | test( 5 | 'simple names, plural when one-to-many, singular in reverse', 6 | withSchema({ 7 | setup: ` 8 | create table p.parent ( 9 | id serial primary key, 10 | name text not null 11 | ); 12 | 13 | create table p.child ( 14 | id serial primary key, 15 | parent_id integer, 16 | name text not null, 17 | constraint child_parent_fkey foreign key (parent_id) 18 | references p.parent (id) 19 | ); 20 | `, 21 | options: { 22 | graphileBuildOptions: { 23 | nestedMutationsSimpleFieldNames: true, 24 | }, 25 | }, 26 | test: async ({ schema, pgClient }) => { 27 | const query = ` 28 | mutation { 29 | c1: createParent( 30 | input: { 31 | parent: { 32 | name: "test" 33 | children: { 34 | create: [{ 35 | name: "test child" 36 | }] 37 | } 38 | } 39 | } 40 | ) { 41 | parent { 42 | id 43 | } 44 | } 45 | 46 | c2: createChild( 47 | input: { 48 | child: { 49 | name: "child" 50 | parent: { 51 | create: { 52 | name: "child's parent" 53 | } 54 | } 55 | } 56 | } 57 | ) { 58 | child { 59 | id 60 | } 61 | } 62 | } 63 | `; 64 | expect(schema).toMatchSnapshot(); 65 | 66 | const result = await graphql(schema, query, null, { pgClient }); 67 | expect(result).not.toHaveProperty('errors'); 68 | }, 69 | }), 70 | ); 71 | 72 | test( 73 | 'simple names, singular when one-to-one', 74 | withSchema({ 75 | setup: ` 76 | create table p.parent ( 77 | id serial primary key, 78 | name text not null 79 | ); 80 | 81 | create table p.child ( 82 | parent_id serial primary key, 83 | name text not null, 84 | constraint child_parent_fkey foreign key (parent_id) 85 | references p.parent (id) 86 | ); 87 | `, 88 | options: { 89 | graphileBuildOptions: { 90 | nestedMutationsSimpleFieldNames: true, 91 | }, 92 | }, 93 | test: async ({ schema, pgClient }) => { 94 | const query = ` 95 | mutation { 96 | c1: createParent( 97 | input: { 98 | parent: { 99 | name: "test" 100 | child: { 101 | create: { 102 | name: "test child" 103 | } 104 | } 105 | } 106 | } 107 | ) { 108 | parent { 109 | id 110 | } 111 | } 112 | 113 | c2: createChild( 114 | input: { 115 | child: { 116 | name: "child" 117 | parent: { 118 | create: { 119 | name: "child's parent" 120 | } 121 | } 122 | } 123 | } 124 | ) { 125 | child { 126 | parentId 127 | } 128 | } 129 | } 130 | `; 131 | expect(schema).toMatchSnapshot(); 132 | 133 | const result = await graphql(schema, query, null, { pgClient }); 134 | expect(result).not.toHaveProperty('errors'); 135 | }, 136 | }), 137 | ); 138 | 139 | test( 140 | 'deleteOthers is not avaialble when disabled', 141 | withSchema({ 142 | setup: ` 143 | create table p.parent ( 144 | id serial primary key, 145 | name text not null 146 | ); 147 | 148 | create table p.child ( 149 | id serial primary key, 150 | parent_id integer, 151 | name text not null, 152 | constraint child_parent_fkey foreign key (parent_id) 153 | references p.parent (id) 154 | ); 155 | insert into p.parent values(1, 'test'); 156 | insert into p.child values(99, 1, 'test child'); 157 | `, 158 | options: { 159 | graphileBuildOptions: { 160 | nestedMutationsDeleteOthers: false, 161 | }, 162 | }, 163 | test: async ({ schema, pgClient }) => { 164 | const query = ` 165 | mutation { 166 | updateParentById( 167 | input: { 168 | id: 1 169 | parentPatch: { 170 | childrenUsingId: { 171 | deleteOthers: true 172 | create: [{ 173 | name: "test child 2" 174 | }, { 175 | name: "test child 3" 176 | }] 177 | } 178 | } 179 | } 180 | ) { 181 | parent { 182 | id 183 | name 184 | childrenByParentId { 185 | nodes { 186 | id 187 | parentId 188 | name 189 | } 190 | } 191 | } 192 | } 193 | } 194 | `; 195 | const result = await graphql(schema, query, null, { pgClient }); 196 | expect(result).toHaveProperty('errors'); 197 | expect(result.errors[0].message).toMatch(/"deleteOthers" is not defined/); 198 | }, 199 | }), 200 | ); 201 | 202 | test( 203 | 'still plural when one-to-one if nestedMutationsOldUniqueFields is enabled', 204 | withSchema({ 205 | setup: ` 206 | create table p.parent ( 207 | id serial primary key, 208 | name text not null 209 | ); 210 | 211 | create table p.child ( 212 | parent_id serial primary key, 213 | name text not null, 214 | constraint child_parent_fkey foreign key (parent_id) 215 | references p.parent (id) 216 | ); 217 | `, 218 | options: { 219 | graphileBuildOptions: { 220 | nestedMutationsOldUniqueFields: true, 221 | }, 222 | }, 223 | test: async ({ schema, pgClient }) => { 224 | const query = ` 225 | mutation { 226 | createParent( 227 | input: { 228 | parent: { 229 | name: "test" 230 | childrenUsingId: { 231 | create: { 232 | name: "test child" 233 | } 234 | } 235 | } 236 | } 237 | ) { 238 | parent { 239 | id 240 | } 241 | } 242 | } 243 | `; 244 | expect(schema).toMatchSnapshot(); 245 | 246 | const result = await graphql(schema, query, null, { pgClient }); 247 | expect(result).not.toHaveProperty('errors'); 248 | }, 249 | }), 250 | ); 251 | 252 | // from https://github.com/mlipscombe/postgraphile-plugin-nested-mutations/issues/40 253 | test( 254 | 'id fields are renamed rowId when classicIds is enabled', 255 | withSchema({ 256 | setup: ` 257 | create table p.parent ( 258 | id uuid primary key default public.uuid_generate_v4(), 259 | name text not null 260 | ); 261 | create table p.child ( 262 | id uuid primary key default public.uuid_generate_v4(), 263 | parent_id uuid references p.parent on delete set null, 264 | name text not null 265 | ); 266 | `, 267 | options: { 268 | classicIds: true, 269 | }, 270 | test: async ({ schema, pgClient }) => { 271 | const query = ` 272 | mutation { 273 | createParent( 274 | input: { 275 | parent: { 276 | name: "test" 277 | childrenUsingRowId: { 278 | create: { 279 | name: "test child" 280 | } 281 | } 282 | } 283 | } 284 | ) { 285 | parent { 286 | id 287 | rowId 288 | childrenByParentId { 289 | nodes { 290 | id 291 | rowId 292 | } 293 | } 294 | } 295 | } 296 | } 297 | `; 298 | expect(schema).toMatchSnapshot(); 299 | 300 | const result = await graphql(schema, query, null, { pgClient }); 301 | expect(result).not.toHaveProperty('errors'); 302 | }, 303 | }), 304 | ); 305 | -------------------------------------------------------------------------------- /__tests__/integration/plurals.test.js: -------------------------------------------------------------------------------- 1 | const { graphql } = require('graphql'); 2 | const { withSchema } = require('../helpers'); 3 | 4 | test( 5 | 'plural when one-to-many, singular in reverse', 6 | withSchema({ 7 | setup: ` 8 | create table p.parent ( 9 | id serial primary key, 10 | name text not null 11 | ); 12 | 13 | create table p.child ( 14 | id serial primary key, 15 | parent_id integer, 16 | name text not null, 17 | constraint child_parent_fkey foreign key (parent_id) 18 | references p.parent (id) 19 | ); 20 | `, 21 | test: async ({ schema, pgClient }) => { 22 | const query = ` 23 | mutation { 24 | c1: createParent( 25 | input: { 26 | parent: { 27 | name: "test" 28 | childrenUsingId: { 29 | create: [{ 30 | name: "test child" 31 | }] 32 | } 33 | } 34 | } 35 | ) { 36 | parent { 37 | id 38 | } 39 | } 40 | 41 | c2: createChild( 42 | input: { 43 | child: { 44 | name: "child" 45 | parentToParentId: { 46 | create: { 47 | name: "child's parent" 48 | } 49 | } 50 | } 51 | } 52 | ) { 53 | child { 54 | id 55 | } 56 | } 57 | } 58 | `; 59 | expect(schema).toMatchSnapshot(); 60 | 61 | const result = await graphql(schema, query, null, { pgClient }); 62 | expect(result).not.toHaveProperty('errors'); 63 | }, 64 | }), 65 | ); 66 | 67 | test( 68 | 'singular when one-to-one', 69 | withSchema({ 70 | setup: ` 71 | create table p.parent ( 72 | id serial primary key, 73 | name text not null 74 | ); 75 | 76 | create table p.child ( 77 | parent_id serial primary key, 78 | name text not null, 79 | constraint child_parent_fkey foreign key (parent_id) 80 | references p.parent (id) 81 | ); 82 | `, 83 | test: async ({ schema, pgClient }) => { 84 | const query = ` 85 | mutation { 86 | c1: createParent( 87 | input: { 88 | parent: { 89 | name: "test" 90 | childUsingId: { 91 | create: { 92 | name: "test child" 93 | } 94 | } 95 | } 96 | } 97 | ) { 98 | parent { 99 | id 100 | } 101 | } 102 | 103 | c2: createChild( 104 | input: { 105 | child: { 106 | name: "child" 107 | parentToParentId: { 108 | create: { 109 | name: "child's parent" 110 | } 111 | } 112 | } 113 | } 114 | ) { 115 | child { 116 | parentId 117 | } 118 | } 119 | } 120 | `; 121 | expect(schema).toMatchSnapshot(); 122 | 123 | const result = await graphql(schema, query, null, { pgClient }); 124 | expect(result).not.toHaveProperty('errors'); 125 | }, 126 | }), 127 | ); 128 | 129 | // https://github.com/mlipscombe/postgraphile-plugin-nested-mutations/issues/7 130 | test( 131 | '1:1 relationship does not allow multiple nested rows', 132 | withSchema({ 133 | setup: ` 134 | CREATE TABLE p.post ( 135 | id SERIAL PRIMARY KEY, 136 | text TEXT NOT NULL 137 | ); 138 | 139 | CREATE TABLE p.post_image ( 140 | id SERIAL PRIMARY KEY, 141 | post_id INTEGER UNIQUE NOT NULL REFERENCES p.post(id) ON DELETE CASCADE, 142 | url TEXT NOT NULL 143 | ); 144 | `, 145 | test: async ({ schema, pgClient }) => { 146 | const query = ` 147 | mutation { 148 | c1: createPost( 149 | input: { 150 | post: { 151 | text: "test" 152 | postImageUsingId: { 153 | create: { 154 | url: "test child" 155 | } 156 | } 157 | } 158 | } 159 | ) { 160 | post { 161 | id 162 | } 163 | } 164 | 165 | c2: createPostImage( 166 | input: { 167 | postImage: { 168 | url: "child" 169 | postToPostId: { 170 | create: { 171 | text: "child's parent" 172 | } 173 | } 174 | } 175 | } 176 | ) { 177 | postImage { 178 | postId 179 | } 180 | } 181 | } 182 | `; 183 | expect(schema).toMatchSnapshot(); 184 | 185 | const result = await graphql(schema, query, null, { pgClient }); 186 | expect(result).not.toHaveProperty('errors'); 187 | }, 188 | }), 189 | ); 190 | 191 | test( 192 | '1:1 relationship mutation fails when multiple operators are specified', 193 | withSchema({ 194 | setup: ` 195 | CREATE TABLE p.post ( 196 | id SERIAL PRIMARY KEY, 197 | text TEXT NOT NULL 198 | ); 199 | 200 | CREATE TABLE p.post_image ( 201 | id SERIAL PRIMARY KEY, 202 | post_id INTEGER UNIQUE NOT NULL REFERENCES p.post(id) ON DELETE CASCADE, 203 | url TEXT NOT NULL 204 | ); 205 | `, 206 | test: async ({ schema, pgClient }) => { 207 | const query = ` 208 | mutation { 209 | c1: createPost( 210 | input: { 211 | post: { 212 | text: "test" 213 | postImageUsingId: { 214 | create: { 215 | url: "test child" 216 | } 217 | connectById: { 218 | id: 1 219 | } 220 | } 221 | } 222 | } 223 | ) { 224 | post { 225 | id 226 | } 227 | } 228 | 229 | c2: createPostImage( 230 | input: { 231 | postImage: { 232 | url: "child" 233 | postToPostId: { 234 | create: { 235 | text: "child's parent" 236 | } 237 | connectById: { 238 | id: 1 239 | } 240 | } 241 | } 242 | } 243 | ) { 244 | postImage { 245 | postId 246 | } 247 | } 248 | } 249 | `; 250 | expect(schema).toMatchSnapshot(); 251 | 252 | const result = await graphql(schema, query, null, { pgClient }); 253 | expect(result).toHaveProperty('errors'); 254 | expect(result.errors[0].message).toMatch( 255 | /may only create or connect a single row/, 256 | ); 257 | }, 258 | }), 259 | ); 260 | -------------------------------------------------------------------------------- /__tests__/integration/schema/__snapshots__/schema.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`prints a schema with the nested mutations plugin 1`] = ` 4 | "\\"\\"\\"The root query type which gives access points into the data universe.\\"\\"\\" 5 | type Query implements Node { 6 | \\"\\"\\"Fetches an object given its globally unique \`ID\`.\\"\\"\\" 7 | node( 8 | \\"\\"\\"The globally unique \`ID\`.\\"\\"\\" 9 | nodeId: ID! 10 | ): Node 11 | 12 | \\"\\"\\" 13 | The root query type must be a \`Node\` to work well with Relay 1 mutations. This just resolves to \`query\`. 14 | \\"\\"\\" 15 | nodeId: ID! 16 | 17 | \\"\\"\\" 18 | Exposes the root query type nested one level down. This is helpful for Relay 1 19 | which can only query top level fields if they are in a particular form. 20 | \\"\\"\\" 21 | query: Query! 22 | } 23 | 24 | \\"\\"\\"An object with a globally unique \`ID\`.\\"\\"\\" 25 | interface Node { 26 | \\"\\"\\" 27 | A globally unique identifier. Can be used in various places throughout the system to identify this single value. 28 | \\"\\"\\" 29 | nodeId: ID! 30 | } 31 | " 32 | `; 33 | 34 | exports[`prints a schema with the nested mutations plugin in simple names mode 1`] = ` 35 | "\\"\\"\\"The root query type which gives access points into the data universe.\\"\\"\\" 36 | type Query implements Node { 37 | \\"\\"\\"Fetches an object given its globally unique \`ID\`.\\"\\"\\" 38 | node( 39 | \\"\\"\\"The globally unique \`ID\`.\\"\\"\\" 40 | nodeId: ID! 41 | ): Node 42 | 43 | \\"\\"\\" 44 | The root query type must be a \`Node\` to work well with Relay 1 mutations. This just resolves to \`query\`. 45 | \\"\\"\\" 46 | nodeId: ID! 47 | 48 | \\"\\"\\" 49 | Exposes the root query type nested one level down. This is helpful for Relay 1 50 | which can only query top level fields if they are in a particular form. 51 | \\"\\"\\" 52 | query: Query! 53 | } 54 | 55 | \\"\\"\\"An object with a globally unique \`ID\`.\\"\\"\\" 56 | interface Node { 57 | \\"\\"\\" 58 | A globally unique identifier. Can be used in various places throughout the system to identify this single value. 59 | \\"\\"\\" 60 | nodeId: ID! 61 | } 62 | " 63 | `; 64 | -------------------------------------------------------------------------------- /__tests__/integration/schema/core.js: -------------------------------------------------------------------------------- 1 | const { createPostGraphileSchema } = require('postgraphile-core'); 2 | const printSchemaOrdered = require('../../printSchemaOrdered'); 3 | const { withPgClient } = require('../../helpers'); 4 | 5 | exports.test = (schemas, options, setup) => () => 6 | withPgClient(async (client) => { 7 | if (setup) { 8 | if (typeof setup === 'function') { 9 | await setup(client); 10 | } else { 11 | await client.query(setup); 12 | } 13 | } 14 | const schema = await createPostGraphileSchema(client, schemas, options); 15 | expect(printSchemaOrdered(schema)).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/integration/schema/schema.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const core = require('./core'); 3 | 4 | test( 5 | 'prints a schema with the nested mutations plugin', 6 | core.test(['p'], { 7 | appendPlugins: [require('../../../index.js')], 8 | }), 9 | ); 10 | 11 | test( 12 | 'prints a schema with the nested mutations plugin in simple names mode', 13 | core.test(['p'], { 14 | graphileBuildOptions: { 15 | nestedMutationsSimpleFieldNames: true, 16 | }, 17 | appendPlugins: [require('../../../index.js')], 18 | }), 19 | ); 20 | -------------------------------------------------------------------------------- /__tests__/integration/smartComments.test.js: -------------------------------------------------------------------------------- 1 | const { graphql } = require('graphql'); 2 | const { withSchema } = require('../helpers'); 3 | 4 | test( 5 | '@omit create on child table inhibits nested create', 6 | withSchema({ 7 | setup: ` 8 | create table p.parent ( 9 | id serial primary key, 10 | name text not null 11 | ); 12 | 13 | create table p.child ( 14 | id serial primary key, 15 | parent_id integer, 16 | name text not null, 17 | constraint child_parent_fkey foreign key (parent_id) 18 | references p.parent (id) 19 | ); 20 | comment on table p.child is E'@omit create'; 21 | `, 22 | test: async ({ schema, pgClient }) => { 23 | const query = ` 24 | mutation { 25 | createParent( 26 | input: { 27 | parent: { 28 | name: "test f1" 29 | childrenUsingId: { 30 | create: [{ 31 | name: "child 1 of test f1" 32 | }, { 33 | name: "child 2 of test f1" 34 | }] 35 | } 36 | } 37 | } 38 | ) { 39 | parent { 40 | id 41 | name 42 | childrenByParentId { 43 | nodes { 44 | id 45 | parentId 46 | name 47 | } 48 | } 49 | } 50 | } 51 | } 52 | `; 53 | expect(schema).toMatchSnapshot(); 54 | 55 | const result = await graphql(schema, query, null, { pgClient }); 56 | expect(result).toHaveProperty('errors'); 57 | expect(result.errors[0].message).toMatch(/"create" is not defined/); 58 | }, 59 | }), 60 | ); 61 | 62 | test( 63 | '@omit on foreign key inhibits nested mutation', 64 | withSchema({ 65 | setup: ` 66 | create table p.parent ( 67 | id serial primary key, 68 | name text not null 69 | ); 70 | 71 | create table p.child ( 72 | id serial primary key, 73 | parent_id integer, 74 | name text not null, 75 | constraint child_parent_fkey foreign key (parent_id) 76 | references p.parent (id) 77 | ); 78 | comment on constraint child_parent_fkey on p.child is E'@omit'; 79 | `, 80 | test: async ({ schema, pgClient }) => { 81 | const query = ` 82 | mutation { 83 | createParent( 84 | input: { 85 | parent: { 86 | name: "test f1" 87 | childrenUsingId: { 88 | create: [{ 89 | name: "child 1 of test f1" 90 | }, { 91 | name: "child 2 of test f1" 92 | }] 93 | } 94 | } 95 | } 96 | ) { 97 | parent { 98 | id 99 | name 100 | childrenByParentId { 101 | nodes { 102 | id 103 | parentId 104 | name 105 | } 106 | } 107 | } 108 | } 109 | } 110 | `; 111 | expect(schema).toMatchSnapshot(); 112 | 113 | const result = await graphql(schema, query, null, { pgClient }); 114 | expect(result).toHaveProperty('errors'); 115 | expect(result.errors[0].message).toMatch( 116 | /"childrenUsingId" is not defined/, 117 | ); 118 | }, 119 | }), 120 | ); 121 | 122 | test( 123 | '@omit create on column in foreign key inhibits nested mutation', 124 | withSchema({ 125 | setup: ` 126 | create table p.parent ( 127 | id serial primary key, 128 | name text not null 129 | ); 130 | 131 | create table p.child ( 132 | id serial primary key, 133 | parent_id integer, 134 | name text not null, 135 | constraint child_parent_fkey foreign key (parent_id) 136 | references p.parent (id) 137 | ); 138 | comment on column p.child.parent_id is E'@omit create'; 139 | `, 140 | test: async ({ schema, pgClient }) => { 141 | const query = ` 142 | mutation { 143 | createParent( 144 | input: { 145 | parent: { 146 | name: "test f1" 147 | childrenUsingId: { 148 | create: [{ 149 | name: "child 1 of test f1" 150 | }, { 151 | name: "child 2 of test f1" 152 | }] 153 | } 154 | } 155 | } 156 | ) { 157 | parent { 158 | id 159 | name 160 | childrenByParentId { 161 | nodes { 162 | id 163 | parentId 164 | name 165 | } 166 | } 167 | } 168 | } 169 | } 170 | `; 171 | expect(schema).toMatchSnapshot(); 172 | 173 | const result = await graphql(schema, query, null, { pgClient }); 174 | expect(result).toHaveProperty('errors'); 175 | expect(result.errors[0].message).toMatch(/"create" is not defined/); 176 | }, 177 | }), 178 | ); 179 | 180 | test( 181 | '@omit create on column referenced column parent table does not inhibit nested mutation', 182 | withSchema({ 183 | setup: ` 184 | create table p.parent ( 185 | id serial primary key, 186 | name text not null 187 | ); 188 | comment on column p.parent.id is E'@omit create'; 189 | 190 | create table p.child ( 191 | id serial primary key, 192 | parent_id integer, 193 | name text not null, 194 | constraint child_parent_fkey foreign key (parent_id) 195 | references p.parent (id) 196 | ); 197 | `, 198 | test: async ({ schema, pgClient }) => { 199 | const query = ` 200 | mutation { 201 | createParent( 202 | input: { 203 | parent: { 204 | name: "test f1" 205 | childrenUsingId: { 206 | create: [{ 207 | name: "child 1 of test f1" 208 | }, { 209 | name: "child 2 of test f1" 210 | }] 211 | } 212 | } 213 | } 214 | ) { 215 | parent { 216 | id 217 | name 218 | childrenByParentId { 219 | nodes { 220 | id 221 | parentId 222 | name 223 | } 224 | } 225 | } 226 | } 227 | } 228 | `; 229 | expect(schema).toMatchSnapshot(); 230 | 231 | const result = await graphql(schema, query, null, { pgClient }); 232 | expect(result).not.toHaveProperty('errors'); 233 | 234 | const data = result.data.createParent.parent; 235 | expect(data.childrenByParentId.nodes).toHaveLength(2); 236 | data.childrenByParentId.nodes.map((n) => 237 | expect(n.parentId).toBe(data.id), 238 | ); 239 | }, 240 | }), 241 | ); 242 | 243 | test( 244 | 'setting @name on relation does not affect field names, but changes type names', 245 | withSchema({ 246 | setup: ` 247 | create table p.parent ( 248 | id serial primary key, 249 | name text not null 250 | ); 251 | 252 | create table p.child ( 253 | id serial primary key, 254 | parent_id integer, 255 | name text not null, 256 | constraint child_parent_fkey foreign key (parent_id) 257 | references p.parent (id) 258 | ); 259 | comment on constraint child_parent_fkey on p.child is E'@name parentChildRelation'; 260 | `, 261 | test: async ({ schema, pgClient }) => { 262 | const query = ` 263 | mutation { 264 | createParent( 265 | input: { 266 | parent: { 267 | name: "test f1" 268 | childrenUsingId: { 269 | create: [{ 270 | name: "child 1 of test f1" 271 | }, { 272 | name: "child 2 of test f1" 273 | }] 274 | } 275 | } 276 | } 277 | ) { 278 | parent { 279 | id 280 | name 281 | childrenByParentId { 282 | nodes { 283 | id 284 | parentId 285 | name 286 | } 287 | } 288 | } 289 | } 290 | } 291 | `; 292 | expect(schema).toMatchSnapshot(); 293 | 294 | const result = await graphql(schema, query, null, { pgClient }); 295 | expect(result).not.toHaveProperty('errors'); 296 | 297 | const data = result.data.createParent.parent; 298 | expect(data.childrenByParentId.nodes).toHaveLength(2); 299 | data.childrenByParentId.nodes.map((n) => 300 | expect(n.parentId).toBe(data.id), 301 | ); 302 | }, 303 | }), 304 | ); 305 | 306 | test( 307 | 'setting @forwardMutationName changes field name', 308 | withSchema({ 309 | setup: ` 310 | create table p.parent ( 311 | id serial primary key, 312 | name text not null 313 | ); 314 | 315 | create table p.child ( 316 | id serial primary key, 317 | parent_id integer, 318 | name text not null, 319 | constraint child_parent_fkey foreign key (parent_id) 320 | references p.parent (id) 321 | ); 322 | comment on constraint child_parent_fkey on p.child is E'@forwardMutationName susan'; 323 | `, 324 | test: async ({ schema, pgClient }) => { 325 | const query = ` 326 | mutation { 327 | createChild ( 328 | input: { 329 | child: { 330 | name: "child 1" 331 | susan: { 332 | create: { 333 | name: "parent 1" 334 | } 335 | } 336 | } 337 | } 338 | ) { 339 | child { 340 | id 341 | parentId 342 | name 343 | parentByParentId { 344 | id 345 | name 346 | } 347 | } 348 | } 349 | } 350 | `; 351 | expect(schema).toMatchSnapshot(); 352 | 353 | const result = await graphql(schema, query, null, { pgClient }); 354 | expect(result).not.toHaveProperty('errors'); 355 | 356 | const data = result.data.createChild.child; 357 | expect(data.parentByParentId.id).toEqual(data.parentId); 358 | }, 359 | }), 360 | ); 361 | 362 | test( 363 | 'setting @fieldName changes field name', 364 | withSchema({ 365 | setup: ` 366 | create table p.parent ( 367 | id serial primary key, 368 | name text not null 369 | ); 370 | 371 | create table p.child ( 372 | id serial primary key, 373 | parent_id integer, 374 | name text not null, 375 | constraint child_parent_fkey foreign key (parent_id) 376 | references p.parent (id) 377 | ); 378 | comment on constraint child_parent_fkey on p.child is E'@fieldName susan'; 379 | `, 380 | test: async ({ schema, pgClient }) => { 381 | const query = ` 382 | mutation { 383 | createChild ( 384 | input: { 385 | child: { 386 | name: "child 1" 387 | susan: { 388 | create: { 389 | name: "parent 1" 390 | } 391 | } 392 | } 393 | } 394 | ) { 395 | child { 396 | id 397 | parentId 398 | name 399 | susan { 400 | id 401 | name 402 | } 403 | } 404 | } 405 | } 406 | `; 407 | expect(schema).toMatchSnapshot(); 408 | 409 | const result = await graphql(schema, query, null, { pgClient }); 410 | expect(result).not.toHaveProperty('errors'); 411 | 412 | const data = result.data.createChild.child; 413 | expect(data.susan.id).toEqual(data.parentId); 414 | }, 415 | }), 416 | ); 417 | 418 | test( 419 | 'setting @reverseMutationName changes field name', 420 | withSchema({ 421 | setup: ` 422 | create table p.parent ( 423 | id serial primary key, 424 | name text not null 425 | ); 426 | 427 | create table p.child ( 428 | id serial primary key, 429 | parent_id integer, 430 | name text not null, 431 | constraint child_parent_fkey foreign key (parent_id) 432 | references p.parent (id) 433 | ); 434 | comment on constraint child_parent_fkey on p.child is E'@reverseMutationName jane'; 435 | `, 436 | test: async ({ schema, pgClient }) => { 437 | const query = ` 438 | mutation { 439 | createParent( 440 | input: { 441 | parent: { 442 | name: "test f1" 443 | jane: { 444 | create: [{ 445 | name: "child 1 of test f1" 446 | }, { 447 | name: "child 2 of test f1" 448 | }] 449 | } 450 | } 451 | } 452 | ) { 453 | parent { 454 | id 455 | name 456 | childrenByParentId { 457 | nodes { 458 | id 459 | parentId 460 | name 461 | } 462 | } 463 | } 464 | } 465 | } 466 | `; 467 | expect(schema).toMatchSnapshot(); 468 | 469 | const result = await graphql(schema, query, null, { pgClient }); 470 | expect(result).not.toHaveProperty('errors'); 471 | 472 | const data = result.data.createParent.parent; 473 | expect(data.childrenByParentId.nodes).toHaveLength(2); 474 | data.childrenByParentId.nodes.map((n) => 475 | expect(n.parentId).toBe(data.id), 476 | ); 477 | }, 478 | }), 479 | ); 480 | 481 | test( 482 | 'setting @foreignFieldName changes field name', 483 | withSchema({ 484 | setup: ` 485 | create table p.parent ( 486 | id serial primary key, 487 | name text not null 488 | ); 489 | 490 | create table p.child ( 491 | id serial primary key, 492 | parent_id integer, 493 | name text not null, 494 | constraint child_parent_fkey foreign key (parent_id) 495 | references p.parent (id) 496 | ); 497 | comment on constraint child_parent_fkey on p.child is E'@foreignFieldName jane'; 498 | `, 499 | test: async ({ schema, pgClient }) => { 500 | const query = ` 501 | mutation { 502 | createParent( 503 | input: { 504 | parent: { 505 | name: "test f1" 506 | jane: { 507 | create: [{ 508 | name: "child 1 of test f1" 509 | }, { 510 | name: "child 2 of test f1" 511 | }] 512 | } 513 | } 514 | } 515 | ) { 516 | parent { 517 | id 518 | name 519 | jane { 520 | nodes { 521 | id 522 | parentId 523 | name 524 | } 525 | } 526 | } 527 | } 528 | } 529 | `; 530 | expect(schema).toMatchSnapshot(); 531 | 532 | const result = await graphql(schema, query, null, { pgClient }); 533 | expect(result).not.toHaveProperty('errors'); 534 | 535 | const data = result.data.createParent.parent; 536 | expect(data.jane.nodes).toHaveLength(2); 537 | data.jane.nodes.map((n) => expect(n.parentId).toBe(data.id)); 538 | }, 539 | }), 540 | ); 541 | 542 | test( 543 | 'unreadable foreign table inhibits nested mutation', 544 | withSchema({ 545 | setup: ` 546 | create table p.parent ( 547 | id serial primary key, 548 | name text not null 549 | ); 550 | 551 | create table p.child ( 552 | id serial primary key, 553 | parent_id integer, 554 | name text not null, 555 | constraint child_parent_fkey foreign key (parent_id) 556 | references p.parent (id) 557 | ); 558 | comment on table p.child is E'@omit read,update,create,delete,all,many'; 559 | `, 560 | test: async ({ schema, pgClient }) => { 561 | const query = ` 562 | mutation { 563 | createParent( 564 | input: { 565 | parent: { 566 | name: "test f1" 567 | childrenUsingId: { 568 | create: [{ 569 | name: "child 1 of test f1" 570 | }, { 571 | name: "child 2 of test f1" 572 | }] 573 | } 574 | } 575 | } 576 | ) { 577 | parent { 578 | id 579 | name 580 | childrenByParentId { 581 | nodes { 582 | id 583 | parentId 584 | name 585 | } 586 | } 587 | } 588 | } 589 | } 590 | `; 591 | expect(schema).toMatchSnapshot(); 592 | 593 | const result = await graphql(schema, query, null, { pgClient }); 594 | expect(result).toHaveProperty('errors'); 595 | expect(result.errors[0].message).toMatch( 596 | /"childrenUsingId" is not defined/, 597 | ); 598 | }, 599 | }), 600 | ); 601 | -------------------------------------------------------------------------------- /__tests__/printSchemaOrdered.js: -------------------------------------------------------------------------------- 1 | const { parse, buildASTSchema } = require('graphql'); 2 | const { printSchema } = require('graphql/utilities'); 3 | 4 | module.exports = function printSchemaOrdered(originalSchema) { 5 | // Clone schema so we don't damage anything 6 | const schema = buildASTSchema(parse(printSchema(originalSchema))); 7 | 8 | const typeMap = schema.getTypeMap(); 9 | Object.keys(typeMap).forEach((name) => { 10 | const gqlType = typeMap[name]; 11 | 12 | // Object? 13 | if (gqlType.getFields) { 14 | const fields = gqlType.getFields(); 15 | const keys = Object.keys(fields).sort(); 16 | keys.forEach((key) => { 17 | const value = fields[key]; 18 | 19 | // Move the key to the end of the object 20 | delete fields[key]; 21 | fields[key] = value; 22 | 23 | // Sort args 24 | if (value.args) { 25 | value.args.sort((a, b) => a.name.localeCompare(b.name)); 26 | } 27 | }); 28 | } 29 | 30 | // Enum? 31 | if (gqlType.getValues) { 32 | gqlType.getValues().sort((a, b) => a.name.localeCompare(b.name)); 33 | } 34 | }); 35 | 36 | return printSchema(schema); 37 | }; 38 | -------------------------------------------------------------------------------- /__tests__/schema.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists "uuid-ossp"; 2 | drop schema if exists p cascade; 3 | 4 | create schema p; 5 | 6 | /* create table p.child_no_pk ( 7 | parent_id integer, 8 | name text not null, 9 | constraint child_no_pk_parent_fkey foreign key (parent_id) 10 | references p.parent (id) 11 | ); */ 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | module.exports = function PostgraphileNestedMutationsPlugin(builder, options) { 3 | require('./src/PostgraphileNestedConnectorsPlugin.js')(builder, options); 4 | require('./src/PostgraphileNestedDeletersPlugin')(builder, options); 5 | require('./src/PostgraphileNestedUpdatersPlugin')(builder, options); 6 | require('./src/PostgraphileNestedTypesPlugin')(builder, options); 7 | require('./src/PostgraphileNestedMutationsPlugin')(builder, options); 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postgraphile-plugin-nested-mutations", 3 | "version": "1.2.0", 4 | "description": "Nested mutations plugin for PostGraphile", 5 | "main": "index.js", 6 | "repository": { 7 | "url": "git+https://github.com/mlipscombe/postgraphile-plugin-nested-mutations.git", 8 | "type": "git" 9 | }, 10 | "author": "Mark Lipscombe", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/mlipscombe/postgraphile-plugin-nested-mutations/issues" 14 | }, 15 | "scripts": { 16 | "test": "scripts/test jest -i", 17 | "lint": "eslint index.js src/**/*.js" 18 | }, 19 | "dependencies": { 20 | "graphile-build-pg": "^4.11.2" 21 | }, 22 | "peerDependencies": { 23 | "postgraphile-core": "^4.2.0" 24 | }, 25 | "devDependencies": { 26 | "@graphile-contrib/pg-simplify-inflector": "^6.1.0", 27 | "eslint": "^7.22.0", 28 | "eslint-config-airbnb-base": "^14.2.1", 29 | "eslint-config-prettier": "^8.1.0", 30 | "eslint-plugin-import": "^2.22.1", 31 | "eslint-plugin-prettier": "^3.3.1", 32 | "graphql": "^15.5.0", 33 | "jest": "^26.6.3", 34 | "jest-junit": "^12.0.0", 35 | "pg": "^8.5.1", 36 | "postgraphile-core": "^4.11.2", 37 | "prettier": "^2.2.1" 38 | }, 39 | "jest": { 40 | "testRegex": "__tests__/.*\\.test\\.js$", 41 | "collectCoverageFrom": [ 42 | "src/*.js", 43 | "index.js" 44 | ] 45 | }, 46 | "files": [ 47 | "src" 48 | ], 49 | "prettier": { 50 | "trailingComma": "all", 51 | "semi": true, 52 | "singleQuote": true, 53 | "arrowParens": "always" 54 | }, 55 | "eslintConfig": { 56 | "extends": [ 57 | "airbnb-base", 58 | "prettier" 59 | ], 60 | "plugins": [ 61 | "prettier" 62 | ], 63 | "env": { 64 | "jest": true 65 | }, 66 | "globals": { 67 | "expect": false, 68 | "jasmine": false 69 | }, 70 | "rules": { 71 | "prettier/prettier": "error", 72 | "import/no-unresolved": 0, 73 | "import/no-extraneous-dependencies": 0, 74 | "import/extensions": 0, 75 | "import/prefer-default-export": 0, 76 | "prefer-object-spread": 0, 77 | "max-len": 0, 78 | "symbol-description": 0, 79 | "no-nested-ternary": 0, 80 | "no-alert": 0, 81 | "no-console": 0, 82 | "no-plusplus": 0, 83 | "no-restricted-globals": 0, 84 | "no-underscore-dangle": [ 85 | "error", 86 | { 87 | "allow": [ 88 | "_fields" 89 | ] 90 | } 91 | ], 92 | "no-return-assign": [ 93 | "error", 94 | "except-parens" 95 | ], 96 | "class-methods-use-this": 0, 97 | "prefer-destructuring": [ 98 | "error", 99 | { 100 | "object": true, 101 | "array": false 102 | } 103 | ] 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ -x ".env" ]; then 5 | set -a 6 | . ./.env 7 | set +a 8 | fi; 9 | 10 | if [ "$TEST_DATABASE_URL" == "" ]; then 11 | echo "ERROR: No test database configured; aborting" 12 | echo 13 | echo "To resolve this, ensure environmental variable TEST_DATABASE_URL is set" 14 | exit 1; 15 | fi; 16 | 17 | # Import latest schema (throw on error) 18 | psql -Xqv ON_ERROR_STOP=1 -f __tests__/schema.sql "$TEST_DATABASE_URL" 19 | echo "Database reset successfully ✅" 20 | 21 | # Now run the tests 22 | $@ 23 | -------------------------------------------------------------------------------- /src/PostgraphileNestedConnectorsPlugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function PostGraphileNestedConnectorsPlugin(builder) { 2 | builder.hook('inflection', (inflection, build) => 3 | build.extend(inflection, { 4 | nestedConnectByNodeIdField() { 5 | return this.camelCase(`connect_by_${build.nodeIdFieldName}`); 6 | }, 7 | nestedConnectByKeyField(options) { 8 | const { constraint } = options; 9 | return this.camelCase( 10 | `connect_by_${constraint.keyAttributes 11 | .map((k) => this.column(k)) 12 | .join('_and_')}`, 13 | ); 14 | }, 15 | nestedConnectByNodeIdInputType(options) { 16 | const { table } = options; 17 | 18 | const tableFieldName = inflection.tableFieldName(table); 19 | 20 | return this.upperCamelCase(`${tableFieldName}_node_id_connect`); 21 | }, 22 | nestedConnectByKeyInputType(options) { 23 | const { 24 | table, 25 | constraint: { 26 | name, 27 | tags: { name: tagName }, 28 | }, 29 | } = options; 30 | 31 | const tableFieldName = this.tableFieldName(table); 32 | 33 | return this.upperCamelCase( 34 | `${tableFieldName}_${tagName || name}_connect`, 35 | ); 36 | }, 37 | }), 38 | ); 39 | 40 | builder.hook('build', (build) => { 41 | const { 42 | extend, 43 | inflection, 44 | pgSql: sql, 45 | gql2pg, 46 | nodeIdFieldName, 47 | pgGetGqlTypeByTypeIdAndModifier, 48 | } = build; 49 | 50 | return extend(build, { 51 | pgNestedTableConnectorFields: {}, 52 | pgNestedTableConnect: async ({ 53 | nestedField, 54 | connectorField, 55 | input, 56 | pgClient, 57 | parentRow, 58 | }) => { 59 | const { foreignTable, keys, foreignKeys } = nestedField; 60 | const { isNodeIdConnector, constraint } = connectorField; 61 | 62 | const ForeignTableType = pgGetGqlTypeByTypeIdAndModifier( 63 | foreignTable.type.id, 64 | null, 65 | ); 66 | let where = ''; 67 | 68 | if (isNodeIdConnector) { 69 | const nodeId = input[nodeIdFieldName]; 70 | const primaryKeys = foreignTable.primaryKeyConstraint.keyAttributes; 71 | const { Type, identifiers } = build.getTypeAndIdentifiersFromNodeId( 72 | nodeId, 73 | ); 74 | if (Type !== ForeignTableType) { 75 | throw new Error('Mismatched type'); 76 | } 77 | if (identifiers.length !== primaryKeys.length) { 78 | throw new Error('Invalid ID'); 79 | } 80 | where = sql.fragment`(${sql.join( 81 | primaryKeys.map( 82 | (key, idx) => 83 | sql.fragment`${sql.identifier(key.name)} = ${gql2pg( 84 | identifiers[idx], 85 | key.type, 86 | key.typeModifier, 87 | )}`, 88 | ), 89 | ') and (', 90 | )})`; 91 | } else { 92 | const foreignPrimaryKeys = constraint.keyAttributes; 93 | where = sql.fragment`(${sql.join( 94 | foreignPrimaryKeys.map( 95 | (k) => sql.fragment` 96 | ${sql.identifier(k.name)} = ${gql2pg( 97 | input[inflection.column(k)], 98 | k.type, 99 | k.typeModifier, 100 | )} 101 | `, 102 | ), 103 | ') and (', 104 | )})`; 105 | } 106 | const select = foreignKeys.map((k) => sql.identifier(k.name)); 107 | const query = parentRow 108 | ? sql.query` 109 | update ${sql.identifier( 110 | foreignTable.namespace.name, 111 | foreignTable.name, 112 | )} 113 | set ${sql.join( 114 | keys.map( 115 | (k, i) => 116 | sql.fragment`${sql.identifier(k.name)} = ${sql.value( 117 | parentRow[foreignKeys[i].name], 118 | )}`, 119 | ), 120 | ', ', 121 | )} 122 | where ${where} 123 | returning *` 124 | : sql.query` 125 | select ${sql.join(select, ', ')} 126 | from ${sql.identifier( 127 | foreignTable.namespace.name, 128 | foreignTable.name, 129 | )} 130 | where ${where}`; 131 | 132 | const { text, values } = sql.compile(query); 133 | const { rows } = await pgClient.query(text, values); 134 | return rows[0]; 135 | }, 136 | }); 137 | }); 138 | 139 | builder.hook('GraphQLObjectType:fields', (fields, build, context) => { 140 | const { 141 | inflection, 142 | newWithHooks, 143 | describePgEntity, 144 | nodeIdFieldName, 145 | pgIntrospectionResultsByKind: introspectionResultsByKind, 146 | pgGetGqlInputTypeByTypeIdAndModifier: getGqlInputTypeByTypeIdAndModifier, 147 | pgOmit: omit, 148 | pgNestedTableConnectorFields, 149 | graphql: { GraphQLNonNull, GraphQLInputObjectType, GraphQLID }, 150 | } = build; 151 | const { 152 | scope: { isRootMutation }, 153 | } = context; 154 | 155 | if (!isRootMutation) { 156 | return fields; 157 | } 158 | 159 | introspectionResultsByKind.class 160 | .filter((cls) => cls.namespace && cls.isSelectable) 161 | .forEach((table) => { 162 | const tableFieldName = inflection.tableFieldName(table); 163 | 164 | pgNestedTableConnectorFields[table.id] = table.constraints 165 | .filter((con) => con.type === 'u' || con.type === 'p') 166 | .filter((con) => !omit(con)) 167 | .filter((con) => !con.keyAttributes.some((key) => omit(key, 'read'))) 168 | .map((constraint) => { 169 | const keys = constraint.keyAttributes; 170 | 171 | // istanbul ignore next 172 | if (!keys.every((_) => _)) { 173 | throw new Error( 174 | `Consistency error: could not find an attribute in the constraint when building nested connection type for ${describePgEntity( 175 | table, 176 | )}!`, 177 | ); 178 | } 179 | 180 | return { 181 | constraint, 182 | keys: constraint.keyAttributes, 183 | isNodeIdConnector: false, 184 | fieldName: inflection.nestedConnectByKeyField({ 185 | table, 186 | constraint, 187 | }), 188 | field: newWithHooks( 189 | GraphQLInputObjectType, 190 | { 191 | name: inflection.nestedConnectByKeyInputType({ 192 | table, 193 | constraint, 194 | }), 195 | description: `The fields on \`${tableFieldName}\` to look up the row to connect.`, 196 | fields: () => 197 | keys 198 | .map((k) => 199 | Object.assign( 200 | {}, 201 | { 202 | [inflection.column(k)]: { 203 | description: k.description, 204 | type: new GraphQLNonNull( 205 | getGqlInputTypeByTypeIdAndModifier( 206 | k.typeId, 207 | k.typeModifier, 208 | ), 209 | ), 210 | }, 211 | }, 212 | ), 213 | ) 214 | .reduce((res, o) => Object.assign(res, o), {}), 215 | }, 216 | { 217 | isNestedMutationInputType: true, 218 | isNestedMutationConnectInputType: true, 219 | pgInflection: table, 220 | pgFieldInflection: constraint, 221 | }, 222 | ), 223 | }; 224 | }); 225 | 226 | const { primaryKeyConstraint } = table; 227 | if (nodeIdFieldName && primaryKeyConstraint) { 228 | pgNestedTableConnectorFields[table.id].push({ 229 | constraint: null, 230 | keys: null, 231 | isNodeIdConnector: true, 232 | fieldName: inflection.nestedConnectByNodeIdField(), 233 | field: newWithHooks( 234 | GraphQLInputObjectType, 235 | { 236 | name: inflection.nestedConnectByNodeIdInputType({ table }), 237 | description: 238 | 'The globally unique `ID` look up for the row to connect.', 239 | fields: { 240 | [nodeIdFieldName]: { 241 | description: `The globally unique \`ID\` which identifies a single \`${tableFieldName}\` to be connected.`, 242 | type: new GraphQLNonNull(GraphQLID), 243 | }, 244 | }, 245 | }, 246 | { 247 | isNestedMutationInputType: true, 248 | isNestedMutationConnectInputType: true, 249 | isNestedMutationConnectByNodeIdType: true, 250 | pgInflection: table, 251 | }, 252 | ), 253 | }); 254 | } 255 | }); 256 | return fields; 257 | }); 258 | }; 259 | -------------------------------------------------------------------------------- /src/PostgraphileNestedDeletersPlugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function PostGraphileNestedDeletersPlugin(builder) { 2 | builder.hook('inflection', (inflection, build) => 3 | build.extend(inflection, { 4 | nestedDeleteByNodeIdField() { 5 | return this.camelCase(`delete_by_${build.nodeIdFieldName}`); 6 | }, 7 | nestedDeleteByKeyField(options) { 8 | const { constraint } = options; 9 | return this.camelCase( 10 | `delete_by_${constraint.keyAttributes 11 | .map((k) => this.column(k)) 12 | .join('_and_')}`, 13 | ); 14 | }, 15 | nestedDeleteByNodeIdInputType(options) { 16 | const { table } = options; 17 | 18 | const tableFieldName = inflection.tableFieldName(table); 19 | 20 | return this.upperCamelCase(`${tableFieldName}_node_id_delete`); 21 | }, 22 | nestedDeleteByKeyInputType(options) { 23 | const { 24 | table, 25 | constraint: { 26 | name, 27 | tags: { name: tagName }, 28 | }, 29 | } = options; 30 | 31 | const tableFieldName = this.tableFieldName(table); 32 | 33 | return this.upperCamelCase( 34 | `${tableFieldName}_${tagName || name}_delete`, 35 | ); 36 | }, 37 | }), 38 | ); 39 | 40 | builder.hook('build', (build) => { 41 | const { 42 | extend, 43 | inflection, 44 | pgSql: sql, 45 | gql2pg, 46 | nodeIdFieldName, 47 | pgGetGqlTypeByTypeIdAndModifier, 48 | } = build; 49 | 50 | return extend(build, { 51 | pgNestedTableDeleterFields: {}, 52 | pgNestedTableDelete: async ({ 53 | nestedField, 54 | deleterField, 55 | input, 56 | pgClient, 57 | parentRow, 58 | }) => { 59 | const { foreignTable, foreignKeys } = nestedField; 60 | const { isNodeIdDeleter, constraint } = deleterField; 61 | 62 | const ForeignTableType = pgGetGqlTypeByTypeIdAndModifier( 63 | foreignTable.type.id, 64 | null, 65 | ); 66 | let where = ''; 67 | 68 | if (isNodeIdDeleter) { 69 | const nodeId = input[nodeIdFieldName]; 70 | const primaryKeys = foreignTable.primaryKeyConstraint.keyAttributes; 71 | const { Type, identifiers } = build.getTypeAndIdentifiersFromNodeId( 72 | nodeId, 73 | ); 74 | if (Type !== ForeignTableType) { 75 | throw new Error('Mismatched type'); 76 | } 77 | if (identifiers.length !== primaryKeys.length) { 78 | throw new Error('Invalid ID'); 79 | } 80 | where = sql.fragment`(${sql.join( 81 | primaryKeys.map( 82 | (key, idx) => 83 | sql.fragment`${sql.identifier(key.name)} = ${gql2pg( 84 | identifiers[idx], 85 | key.type, 86 | key.typeModifier, 87 | )}`, 88 | ), 89 | ') and (', 90 | )})`; 91 | } else { 92 | const foreignPrimaryKeys = constraint.keyAttributes; 93 | where = sql.fragment`(${sql.join( 94 | foreignPrimaryKeys.map( 95 | (k) => sql.fragment` 96 | ${sql.identifier(k.name)} = ${gql2pg( 97 | input[inflection.column(k)], 98 | k.type, 99 | k.typeModifier, 100 | )} 101 | `, 102 | ), 103 | ') and (', 104 | )})`; 105 | } 106 | const select = foreignKeys.map((k) => sql.identifier(k.name)); 107 | const query = parentRow 108 | ? sql.query` 109 | delete from ${sql.identifier( 110 | foreignTable.namespace.name, 111 | foreignTable.name, 112 | )} 113 | where ${where}` 114 | : sql.query` 115 | select ${sql.join(select, ', ')} 116 | from ${sql.identifier( 117 | foreignTable.namespace.name, 118 | foreignTable.name, 119 | )} 120 | where ${where}`; 121 | 122 | const { text, values } = sql.compile(query); 123 | const { rows } = await pgClient.query(text, values); 124 | return rows[0]; 125 | }, 126 | }); 127 | }); 128 | 129 | builder.hook('GraphQLObjectType:fields', (fields, build, context) => { 130 | const { 131 | inflection, 132 | newWithHooks, 133 | describePgEntity, 134 | nodeIdFieldName, 135 | pgIntrospectionResultsByKind: introspectionResultsByKind, 136 | pgGetGqlInputTypeByTypeIdAndModifier: getGqlInputTypeByTypeIdAndModifier, 137 | pgOmit: omit, 138 | pgNestedTableDeleterFields, 139 | graphql: { GraphQLNonNull, GraphQLInputObjectType, GraphQLID }, 140 | } = build; 141 | const { 142 | scope: { isRootMutation }, 143 | } = context; 144 | 145 | if (!isRootMutation) { 146 | return fields; 147 | } 148 | 149 | introspectionResultsByKind.class 150 | .filter((cls) => cls.namespace && cls.isSelectable) 151 | .forEach((table) => { 152 | const tableFieldName = inflection.tableFieldName(table); 153 | 154 | pgNestedTableDeleterFields[table.id] = table.constraints 155 | .filter((con) => con.type === 'u' || con.type === 'p') 156 | .filter((con) => !omit(con)) 157 | .filter((con) => !con.keyAttributes.some((key) => omit(key, 'read'))) 158 | .map((constraint) => { 159 | const keys = constraint.keyAttributes; 160 | 161 | // istanbul ignore next 162 | if (!keys.every((_) => _)) { 163 | throw new Error( 164 | `Consistency error: could not find an attribute in the constraint when building nested connection type for ${describePgEntity( 165 | table, 166 | )}!`, 167 | ); 168 | } 169 | 170 | return { 171 | constraint, 172 | keys: constraint.keyAttributes, 173 | isNodeIdDeleter: false, 174 | fieldName: inflection.nestedDeleteByKeyField({ 175 | table, 176 | constraint, 177 | }), 178 | field: newWithHooks( 179 | GraphQLInputObjectType, 180 | { 181 | name: inflection.nestedDeleteByKeyInputType({ 182 | table, 183 | constraint, 184 | }), 185 | description: `The fields on \`${tableFieldName}\` to look up the row to delete.`, 186 | fields: () => 187 | keys 188 | .map((k) => 189 | Object.assign( 190 | {}, 191 | { 192 | [inflection.column(k)]: { 193 | description: k.description, 194 | type: new GraphQLNonNull( 195 | getGqlInputTypeByTypeIdAndModifier( 196 | k.typeId, 197 | k.typeModifier, 198 | ), 199 | ), 200 | }, 201 | }, 202 | ), 203 | ) 204 | .reduce((res, o) => Object.assign(res, o), {}), 205 | }, 206 | { 207 | isNestedMutationInputType: true, 208 | isNestedMutationDeleteInputType: true, 209 | pgInflection: table, 210 | pgFieldInflection: constraint, 211 | }, 212 | ), 213 | }; 214 | }); 215 | 216 | const { primaryKeyConstraint } = table; 217 | if (nodeIdFieldName && primaryKeyConstraint) { 218 | pgNestedTableDeleterFields[table.id].push({ 219 | constraint: null, 220 | keys: null, 221 | isNodeIdDeleter: true, 222 | fieldName: inflection.nestedDeleteByNodeIdField(), 223 | field: newWithHooks( 224 | GraphQLInputObjectType, 225 | { 226 | name: inflection.nestedDeleteByNodeIdInputType({ table }), 227 | description: 228 | 'The globally unique `ID` look up for the row to delete.', 229 | fields: { 230 | [nodeIdFieldName]: { 231 | description: `The globally unique \`ID\` which identifies a single \`${tableFieldName}\` to be deleted.`, 232 | type: new GraphQLNonNull(GraphQLID), 233 | }, 234 | }, 235 | }, 236 | { 237 | isNestedMutationInputType: true, 238 | isNestedMutationDeleteInputType: true, 239 | isNestedMutationDeleteByNodeIdType: true, 240 | pgInflection: table, 241 | }, 242 | ), 243 | }); 244 | } 245 | }); 246 | return fields; 247 | }); 248 | }; 249 | -------------------------------------------------------------------------------- /src/PostgraphileNestedMutationsPlugin.js: -------------------------------------------------------------------------------- 1 | const debugFactory = require('debug'); 2 | 3 | const debug = debugFactory('postgraphile-plugin-nested-mutations'); 4 | 5 | module.exports = function PostGraphileNestedMutationPlugin(builder) { 6 | builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => { 7 | const { 8 | inflection, 9 | pgGetGqlInputTypeByTypeIdAndModifier: getGqlInputTypeByTypeIdAndModifier, 10 | pgNestedPluginForwardInputTypes, 11 | pgNestedPluginReverseInputTypes, 12 | } = build; 13 | 14 | const { 15 | scope: { isInputType, isPgRowType, isPgPatch, pgIntrospection: table }, 16 | } = context; 17 | 18 | const nestedFields = {}; 19 | 20 | if ( 21 | (!isPgPatch && (!isInputType || !isPgRowType)) || 22 | (!pgNestedPluginForwardInputTypes[table.id] && 23 | !pgNestedPluginReverseInputTypes[table.id]) 24 | ) { 25 | return fields; 26 | } 27 | 28 | pgNestedPluginForwardInputTypes[table.id].forEach( 29 | ({ name, keys, connectorInputField }) => { 30 | // Allow nulls on keys that have forward mutations available. 31 | keys.forEach((k) => { 32 | const keyFieldName = inflection.column(k); 33 | nestedFields[keyFieldName] = Object.assign({}, fields[keyFieldName], { 34 | type: getGqlInputTypeByTypeIdAndModifier(k.typeId, k.typeModifier), 35 | }); 36 | }); 37 | 38 | nestedFields[name] = Object.assign({}, fields[name], { 39 | type: connectorInputField, 40 | }); 41 | }, 42 | ); 43 | 44 | pgNestedPluginReverseInputTypes[table.id].forEach( 45 | ({ name, connectorInputField }) => { 46 | nestedFields[name] = Object.assign({}, fields[name], { 47 | type: connectorInputField, 48 | }); 49 | }, 50 | ); 51 | 52 | return Object.assign({}, fields, nestedFields); 53 | }); 54 | 55 | builder.hook('GraphQLObjectType:fields:field', (field, build, context) => { 56 | const { 57 | inflection, 58 | nodeIdFieldName, 59 | pgSql: sql, 60 | pgOmit: omit, 61 | gql2pg, 62 | parseResolveInfo, 63 | getTypeByName, 64 | getTypeAndIdentifiersFromNodeId, 65 | pgColumnFilter, 66 | pgQueryFromResolveData: queryFromResolveData, 67 | pgNestedPluginForwardInputTypes, 68 | pgNestedPluginReverseInputTypes, 69 | pgNestedCreateResolvers, 70 | pgNestedUpdateResolvers, 71 | pgNestedTableConnectorFields, 72 | pgNestedTableConnect, 73 | pgNestedTableDeleterFields, 74 | pgNestedTableDelete, 75 | pgNestedTableUpdaterFields, 76 | pgNestedTableUpdate, 77 | pgViaTemporaryTable: viaTemporaryTable, 78 | pgGetGqlTypeByTypeIdAndModifier, 79 | } = build; 80 | 81 | const { 82 | scope: { 83 | isPgCreateMutationField, 84 | isPgUpdateMutationField, 85 | pgFieldIntrospection: table, 86 | pgFieldConstraint, 87 | }, 88 | addArgDataGenerator, 89 | getDataFromParsedResolveInfoFragment, 90 | } = context; 91 | 92 | if (!isPgCreateMutationField && !isPgUpdateMutationField) { 93 | return field; 94 | } 95 | 96 | if ( 97 | !pgNestedPluginForwardInputTypes[table.id] && 98 | !pgNestedPluginReverseInputTypes[table.id] 99 | ) { 100 | pgNestedCreateResolvers[table.id] = field.resolve; 101 | return field; 102 | } 103 | 104 | const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null); 105 | 106 | // Ensure the table's primary keys are always available in a query. 107 | const tablePrimaryKey = table.constraints.find((con) => con.type === 'p'); 108 | if (tablePrimaryKey) { 109 | addArgDataGenerator(() => ({ 110 | pgQuery: (queryBuilder) => { 111 | tablePrimaryKey.keyAttributes.forEach((key) => { 112 | queryBuilder.select( 113 | sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( 114 | key.name, 115 | )}`, 116 | `__pk__${key.name}`, 117 | ); 118 | }); 119 | }, 120 | })); 121 | } 122 | 123 | const recurseForwardNestedMutations = async ( 124 | data, 125 | { input }, 126 | { pgClient }, 127 | resolveInfo, 128 | ) => { 129 | const nestedFields = pgNestedPluginForwardInputTypes[table.id]; 130 | const output = Object.assign({}, input); 131 | await Promise.all( 132 | nestedFields 133 | .filter((k) => input[k.name]) 134 | .map(async (nestedField) => { 135 | const { 136 | constraint, 137 | foreignTable, 138 | keys, 139 | foreignKeys, 140 | name: fieldName, 141 | } = nestedField; 142 | const fieldValue = input[fieldName]; 143 | 144 | if (fieldValue.updateById || fieldValue.updateByNodeId) { 145 | await Promise.all( 146 | Object.keys(fieldValue).map(async (k) => { 147 | (Array.isArray(fieldValue[k]) 148 | ? fieldValue[k] 149 | : [fieldValue[k]] 150 | ).map(async (rowData) => { 151 | const updateData = Object.assign( 152 | {}, 153 | rowData, 154 | await recurseForwardNestedMutations( 155 | data, 156 | { input: rowData }, 157 | { pgClient }, 158 | resolveInfo, 159 | ), 160 | ); 161 | 162 | const resolver = pgNestedUpdateResolvers[foreignTable.id]; 163 | const resolveResult = await resolver( 164 | data, 165 | { input: updateData }, 166 | { pgClient }, 167 | resolveInfo, 168 | ); 169 | 170 | foreignKeys.forEach((pk, idx) => { 171 | output[inflection.column(keys[idx])] = 172 | resolveResult.data[`__pk__${pk.name}`]; 173 | }); 174 | }); 175 | }), 176 | ); 177 | } 178 | 179 | await Promise.all( 180 | pgNestedTableConnectorFields[foreignTable.id] 181 | .filter((f) => fieldValue[f.fieldName]) 182 | .map(async (connectorField) => { 183 | const row = await pgNestedTableConnect({ 184 | nestedField, 185 | connectorField, 186 | input: fieldValue[connectorField.fieldName], 187 | pgClient, 188 | }); 189 | 190 | if (!row) { 191 | throw new Error('invalid connect keys'); 192 | } 193 | 194 | foreignKeys.forEach((k, idx) => { 195 | output[inflection.column(keys[idx])] = row[k.name]; 196 | }); 197 | }), 198 | ); 199 | 200 | await Promise.all( 201 | pgNestedTableDeleterFields[foreignTable.id] 202 | .filter((f) => fieldValue[f.fieldName]) 203 | .map(async (deleterField) => { 204 | const row = await pgNestedTableDelete({ 205 | nestedField, 206 | deleterField, 207 | input: fieldValue[deleterField.fieldName], 208 | pgClient, 209 | }); 210 | 211 | if (!row) { 212 | throw new Error('invalid connect keys'); 213 | } 214 | 215 | foreignKeys.forEach((k, idx) => { 216 | output[inflection.column(keys[idx])] = row[k.name]; 217 | }); 218 | }), 219 | ); 220 | 221 | await Promise.all( 222 | pgNestedTableUpdaterFields[table.id][constraint.id] 223 | .filter((f) => fieldValue[f.fieldName]) 224 | .map(async (connectorField) => { 225 | const row = await pgNestedTableUpdate({ 226 | nestedField, 227 | connectorField, 228 | input: fieldValue[connectorField.fieldName], 229 | pgClient, 230 | context, 231 | }); 232 | 233 | if (!row) { 234 | throw new Error('unmatched row for update'); 235 | } 236 | 237 | foreignKeys.forEach((k, idx) => { 238 | output[inflection.column(keys[idx])] = row[k.name]; 239 | }); 240 | }), 241 | ); 242 | 243 | if (fieldValue.create) { 244 | const createData = fieldValue.create; 245 | const resolver = pgNestedCreateResolvers[foreignTable.id]; 246 | const tableVar = inflection.tableFieldName(foreignTable); 247 | 248 | const insertData = Object.assign( 249 | {}, 250 | createData, 251 | await recurseForwardNestedMutations( 252 | data, 253 | { input: { [tableVar]: createData } }, 254 | { pgClient }, 255 | resolveInfo, 256 | ), 257 | ); 258 | 259 | const resolveResult = await resolver( 260 | data, 261 | { input: { [tableVar]: insertData } }, 262 | { pgClient }, 263 | resolveInfo, 264 | ); 265 | foreignKeys.forEach((k, idx) => { 266 | output[inflection.column(keys[idx])] = 267 | resolveResult.data[`__pk__${k.name}`]; 268 | }); 269 | } 270 | }), 271 | ); 272 | 273 | return output; 274 | }; 275 | 276 | const mutationResolver = async ( 277 | data, 278 | { input }, 279 | { pgClient }, 280 | resolveInfo, 281 | ) => { 282 | const PayloadType = getTypeByName( 283 | isPgUpdateMutationField 284 | ? inflection.updatePayloadType(table) 285 | : inflection.createPayloadType(table), 286 | ); 287 | const tableFieldName = isPgUpdateMutationField 288 | ? inflection.patchField(inflection.tableFieldName(table)) 289 | : inflection.tableFieldName(table); 290 | const parsedResolveInfoFragment = parseResolveInfo(resolveInfo); 291 | const resolveData = getDataFromParsedResolveInfoFragment( 292 | parsedResolveInfoFragment, 293 | PayloadType, 294 | ); 295 | const insertedRowAlias = sql.identifier(Symbol()); 296 | const query = queryFromResolveData( 297 | insertedRowAlias, 298 | insertedRowAlias, 299 | resolveData, 300 | {}, 301 | ); 302 | 303 | try { 304 | await pgClient.query('SAVEPOINT graphql_nested_mutation'); 305 | 306 | // run forward nested mutations 307 | const forwardOutput = await recurseForwardNestedMutations( 308 | data, 309 | { input: input[tableFieldName] }, 310 | { pgClient }, 311 | resolveInfo, 312 | ); 313 | 314 | const inputData = Object.assign( 315 | {}, 316 | input[tableFieldName], 317 | forwardOutput, 318 | ); 319 | 320 | let mutationQuery = null; 321 | 322 | if (isPgCreateMutationField) { 323 | const sqlColumns = []; 324 | const sqlValues = []; 325 | table.attributes 326 | .filter((attr) => pgColumnFilter(attr, build, context)) 327 | .filter((attr) => !omit(attr, 'create')) 328 | .forEach((attr) => { 329 | const fieldName = inflection.column(attr); 330 | const val = inputData[fieldName]; 331 | if (Object.prototype.hasOwnProperty.call(inputData, fieldName)) { 332 | sqlColumns.push(sql.identifier(attr.name)); 333 | sqlValues.push(gql2pg(val, attr.type, attr.typeModifier)); 334 | } 335 | }); 336 | 337 | /* eslint indent: 0 */ 338 | mutationQuery = sql.query` 339 | insert into ${sql.identifier(table.namespace.name, table.name)} 340 | ${ 341 | sqlColumns.length 342 | ? sql.fragment`( 343 | ${sql.join(sqlColumns, ', ')} 344 | ) values(${sql.join(sqlValues, ', ')})` 345 | : sql.fragment`default values` 346 | } returning *`; 347 | } else if (isPgUpdateMutationField) { 348 | const sqlColumns = []; 349 | const sqlValues = []; 350 | let condition = null; 351 | const nodeId = input[nodeIdFieldName]; 352 | 353 | if (nodeId) { 354 | try { 355 | const { Type, identifiers } = getTypeAndIdentifiersFromNodeId( 356 | nodeId, 357 | ); 358 | const primaryKeys = table.primaryKeyConstraint.keyAttributes; 359 | if (Type !== TableType) { 360 | throw new Error('Mismatched type'); 361 | } 362 | if (identifiers.length !== primaryKeys.length) { 363 | throw new Error('Invalid ID'); 364 | } 365 | condition = sql.fragment`(${sql.join( 366 | table.primaryKeyConstraint.keyAttributes.map( 367 | (key, idx) => 368 | sql.fragment`${sql.identifier(key.name)} = ${gql2pg( 369 | identifiers[idx], 370 | key.type, 371 | key.typeModifier, 372 | )}`, 373 | ), 374 | ') and (', 375 | )})`; 376 | } catch (e) { 377 | debug(e); 378 | throw e; 379 | } 380 | } else { 381 | const { keyAttributes: keys } = pgFieldConstraint; 382 | condition = sql.fragment`(${sql.join( 383 | keys.map( 384 | (key) => 385 | sql.fragment`${sql.identifier(key.name)} = ${gql2pg( 386 | input[inflection.column(key)], 387 | key.type, 388 | key.typeModifier, 389 | )}`, 390 | ), 391 | ') and (', 392 | )})`; 393 | } 394 | table.attributes 395 | .filter((attr) => pgColumnFilter(attr, build, context)) 396 | .filter((attr) => !omit(attr, 'update')) 397 | .forEach((attr) => { 398 | const fieldName = inflection.column(attr); 399 | if (fieldName in inputData) { 400 | const val = inputData[fieldName]; 401 | sqlColumns.push(sql.identifier(attr.name)); 402 | sqlValues.push(gql2pg(val, attr.type, attr.typeModifier)); 403 | } 404 | }); 405 | 406 | if (sqlColumns.length) { 407 | mutationQuery = sql.query` 408 | update ${sql.identifier( 409 | table.namespace.name, 410 | table.name, 411 | )} set ${sql.join( 412 | sqlColumns.map( 413 | (col, i) => sql.fragment`${col} = ${sqlValues[i]}`, 414 | ), 415 | ', ', 416 | )} 417 | where ${condition} 418 | returning *`; 419 | } else { 420 | mutationQuery = sql.query` 421 | select * from ${sql.identifier(table.namespace.name, table.name)} 422 | where ${condition}`; 423 | } 424 | } 425 | 426 | const { text, values } = sql.compile(mutationQuery); 427 | const { rows } = await pgClient.query(text, values); 428 | const row = rows[0]; 429 | 430 | await Promise.all( 431 | Object.keys(inputData).map(async (key) => { 432 | const nestedField = pgNestedPluginReverseInputTypes[table.id].find( 433 | (obj) => obj.name === key, 434 | ); 435 | if (!nestedField || !inputData[key]) { 436 | return; 437 | } 438 | 439 | const { 440 | constraint, 441 | foreignTable, 442 | keys, // nested table's keys 443 | foreignKeys, // main mutation table's keys 444 | isUnique, 445 | } = nestedField; 446 | const modifiedRows = []; 447 | 448 | const fieldValue = inputData[key]; 449 | const { primaryKeyConstraint } = foreignTable; 450 | const primaryKeys = primaryKeyConstraint 451 | ? primaryKeyConstraint.keyAttributes 452 | : null; 453 | 454 | if (isUnique && Object.keys(fieldValue).length > 1) { 455 | throw new Error( 456 | 'Unique relations may only create or connect a single row.', 457 | ); 458 | } 459 | 460 | // perform nested connects 461 | await Promise.all( 462 | pgNestedTableConnectorFields[foreignTable.id] 463 | .filter((f) => fieldValue[f.fieldName]) 464 | .map(async (connectorField) => { 465 | const connections = Array.isArray( 466 | fieldValue[connectorField.fieldName], 467 | ) 468 | ? fieldValue[connectorField.fieldName] 469 | : [fieldValue[connectorField.fieldName]]; 470 | 471 | await Promise.all( 472 | connections.map(async (k) => { 473 | const connectedRow = await pgNestedTableConnect({ 474 | nestedField, 475 | connectorField, 476 | input: k, 477 | pgClient, 478 | parentRow: row, 479 | }); 480 | 481 | if (primaryKeys) { 482 | if (!connectedRow) { 483 | throw new Error( 484 | 'Unable to update/select parent row.', 485 | ); 486 | } 487 | const rowKeyValues = {}; 488 | primaryKeys.forEach((col) => { 489 | rowKeyValues[col.name] = connectedRow[col.name]; 490 | }); 491 | modifiedRows.push(rowKeyValues); 492 | } 493 | }), 494 | ); 495 | }), 496 | ); 497 | 498 | // perform nested deletes 499 | await Promise.all( 500 | pgNestedTableDeleterFields[foreignTable.id] 501 | .filter((f) => fieldValue[f.fieldName]) 502 | .map(async (deleterField) => { 503 | const connections = Array.isArray( 504 | fieldValue[deleterField.fieldName], 505 | ) 506 | ? fieldValue[deleterField.fieldName] 507 | : [fieldValue[deleterField.fieldName]]; 508 | 509 | await Promise.all( 510 | connections.map(async (k) => { 511 | await pgNestedTableDelete({ 512 | nestedField, 513 | deleterField, 514 | input: k, 515 | pgClient, 516 | parentRow: row, 517 | }); 518 | }), 519 | ); 520 | }), 521 | ); 522 | 523 | // perform nested updates 524 | await Promise.all( 525 | pgNestedTableUpdaterFields[table.id][constraint.id] 526 | .filter((f) => fieldValue[f.fieldName]) 527 | .map(async (connectorField) => { 528 | const updaterField = Array.isArray( 529 | fieldValue[connectorField.fieldName], 530 | ) 531 | ? fieldValue[connectorField.fieldName] 532 | : [fieldValue[connectorField.fieldName]]; 533 | 534 | await Promise.all( 535 | updaterField.map(async (node) => { 536 | const where = sql.fragment` 537 | (${sql.join( 538 | keys.map( 539 | (k, i) => 540 | sql.fragment`${sql.identifier(k.name)} = ${sql.value( 541 | row[foreignKeys[i].name], 542 | )}`, 543 | ), 544 | ') and (', 545 | )}) 546 | `; 547 | const updatedRow = await pgNestedTableUpdate({ 548 | nestedField, 549 | connectorField, 550 | input: node, 551 | pgClient, 552 | context, 553 | where, 554 | }); 555 | 556 | if (!updatedRow) { 557 | throw new Error('unmatched update'); 558 | } 559 | 560 | if (primaryKeys) { 561 | const rowKeyValues = {}; 562 | primaryKeys.forEach((k) => { 563 | rowKeyValues[k.name] = updatedRow[k.name]; 564 | }); 565 | modifiedRows.push(rowKeyValues); 566 | } 567 | }), 568 | ); 569 | }), 570 | ); 571 | 572 | if (fieldValue.deleteOthers) { 573 | // istanbul ignore next 574 | if (!primaryKeys) { 575 | throw new Error( 576 | '`deleteOthers` is not supported on foreign relations with no primary key.', 577 | ); 578 | } 579 | const keyCondition = sql.fragment`(${sql.join( 580 | keys.map( 581 | (k, idx) => sql.fragment` 582 | ${sql.identifier(k.name)} = ${sql.value( 583 | row[foreignKeys[idx].name], 584 | )} 585 | `, 586 | ), 587 | ') and (', 588 | )})`; 589 | let rowCondition; 590 | if (modifiedRows.length === 0) { 591 | rowCondition = sql.fragment``; 592 | } else { 593 | rowCondition = sql.fragment` and ( 594 | ${sql.join( 595 | modifiedRows.map( 596 | (r) => 597 | sql.fragment`${sql.join( 598 | Object.keys(r).map( 599 | (k) => sql.fragment` 600 | ${sql.identifier(k)} <> ${sql.value(r[k])} 601 | `, 602 | ), 603 | ' and ', 604 | )}`, 605 | ), 606 | ') and (', 607 | )})`; 608 | } 609 | 610 | const deleteQuery = sql.query` 611 | delete from ${sql.identifier( 612 | foreignTable.namespace.name, 613 | foreignTable.name, 614 | )} 615 | where (${keyCondition})${rowCondition}`; 616 | const { 617 | text: deleteQueryText, 618 | values: deleteQueryValues, 619 | } = sql.compile(deleteQuery); 620 | await pgClient.query(deleteQueryText, deleteQueryValues); 621 | } 622 | 623 | if (fieldValue.create) { 624 | await Promise.all( 625 | fieldValue.create.map(async (rowData) => { 626 | const resolver = pgNestedCreateResolvers[foreignTable.id]; 627 | const tableVar = inflection.tableFieldName(foreignTable); 628 | 629 | const keyData = {}; 630 | keys.forEach((k, idx) => { 631 | const columnName = inflection.column(k); 632 | keyData[columnName] = row[foreignKeys[idx].name]; 633 | }); 634 | 635 | const { data: reverseRow } = await resolver( 636 | data, 637 | { 638 | input: { 639 | [tableVar]: Object.assign({}, rowData, keyData), 640 | }, 641 | }, 642 | { pgClient }, 643 | resolveInfo, 644 | ); 645 | 646 | const rowKeyValues = {}; 647 | if (primaryKeys) { 648 | primaryKeys.forEach((k) => { 649 | rowKeyValues[k.name] = reverseRow[`__pk__${k.name}`]; 650 | }); 651 | } 652 | modifiedRows.push(rowKeyValues); 653 | }), 654 | ); 655 | } 656 | 657 | if (fieldValue.updateById || fieldValue.updateByNodeId) { 658 | await Promise.all( 659 | ['updateById', 'updateByNodeId'] 660 | .filter((f) => fieldValue[f]) 661 | .map(async (f) => { 662 | await Promise.all( 663 | (Array.isArray(fieldValue[f]) 664 | ? fieldValue[f] 665 | : [fieldValue[f]] 666 | ).map(async (rowData) => { 667 | const resolver = 668 | pgNestedUpdateResolvers[foreignTable.id]; 669 | 670 | const { data: reverseRow } = await resolver( 671 | data, 672 | { 673 | input: Object.assign({}, rowData), 674 | }, 675 | { pgClient }, 676 | resolveInfo, 677 | ); 678 | 679 | const rowKeyValues = {}; 680 | if (primaryKeys && reverseRow) { 681 | primaryKeys.forEach((k) => { 682 | rowKeyValues[k.name] = 683 | reverseRow[`__pk__${k.name}`]; 684 | }); 685 | } 686 | modifiedRows.push(rowKeyValues); 687 | }), 688 | ); 689 | }), 690 | ); 691 | } 692 | }), 693 | ); 694 | 695 | let mutationData = null; 696 | 697 | const primaryKeyConstraint = table.constraints.find( 698 | (con) => con.type === 'p', 699 | ); 700 | if (primaryKeyConstraint && row) { 701 | const primaryKeyFields = primaryKeyConstraint.keyAttributes; 702 | 703 | const where = []; 704 | primaryKeyFields.forEach((f) => { 705 | where.push(sql.fragment` 706 | ${sql.identifier(f.name)} = ${sql.value(row[f.name])} 707 | `); 708 | }); 709 | 710 | const finalRows = await viaTemporaryTable( 711 | pgClient, 712 | sql.identifier(table.namespace.name, table.name), 713 | sql.query` 714 | select * from ${sql.identifier(table.namespace.name, table.name)} 715 | where ${sql.join(where, ' AND ')} 716 | `, 717 | insertedRowAlias, 718 | query, 719 | ); 720 | mutationData = finalRows[0]; 721 | } 722 | 723 | return { 724 | clientMutationId: input.clientMutationId, 725 | data: mutationData, 726 | }; 727 | } catch (e) { 728 | debug(e); 729 | await pgClient.query('ROLLBACK TO SAVEPOINT graphql_nested_mutation'); 730 | throw e; 731 | } finally { 732 | await pgClient.query('RELEASE SAVEPOINT graphql_nested_mutation'); 733 | } 734 | }; 735 | 736 | if (isPgCreateMutationField) { 737 | pgNestedCreateResolvers[table.id] = mutationResolver; 738 | } 739 | 740 | if (isPgUpdateMutationField) { 741 | pgNestedUpdateResolvers[table.id] = mutationResolver; 742 | } 743 | 744 | return Object.assign({}, field, { resolve: mutationResolver }); 745 | }); 746 | }; 747 | -------------------------------------------------------------------------------- /src/PostgraphileNestedTypesPlugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function PostGraphileNestedTypesPlugin( 2 | builder, 3 | { 4 | nestedMutationsSimpleFieldNames = false, 5 | nestedMutationsDeleteOthers = true, 6 | nestedMutationsOldUniqueFields = false, 7 | } = {}, 8 | ) { 9 | builder.hook('inflection', (inflection, build) => 10 | build.extend(inflection, { 11 | nestedConnectorType(options) { 12 | const { 13 | constraint: { 14 | name, 15 | tags: { name: tagName }, 16 | }, 17 | isForward, 18 | } = options; 19 | return inflection.upperCamelCase( 20 | `${tagName || name}_${isForward ? '' : 'Inverse'}_input`, 21 | ); 22 | }, 23 | nestedCreateInputType(options) { 24 | const { 25 | constraint: { 26 | name, 27 | tags: { name: tagName }, 28 | }, 29 | foreignTable, 30 | } = options; 31 | return inflection.upperCamelCase( 32 | `${tagName || name}_${foreignTable.name}_create_input`, 33 | ); 34 | }, 35 | }), 36 | ); 37 | 38 | builder.hook('build', (build) => { 39 | const { extend, pgOmit: omit, inflection } = build; 40 | 41 | return extend(build, { 42 | pgNestedPluginForwardInputTypes: {}, 43 | pgNestedPluginReverseInputTypes: {}, 44 | pgNestedCreateResolvers: {}, 45 | pgNestedUpdateResolvers: {}, 46 | pgNestedFieldName(options) { 47 | const { 48 | constraint: { 49 | keyAttributes: keys, 50 | foreignKeyAttributes: foreignKeys, 51 | tags: { 52 | fieldName, 53 | foreignFieldName, 54 | forwardMutationName, 55 | reverseMutationName, 56 | }, 57 | }, 58 | table, 59 | isForward, 60 | foreignTable, 61 | } = options; 62 | const tableFieldName = inflection.tableFieldName(foreignTable); 63 | const keyNames = keys.map((k) => inflection.column(k)); 64 | const foreignKeyNames = foreignKeys.map((k) => inflection.column(k)); 65 | 66 | const constraints = foreignTable.constraints 67 | .filter((con) => con.type === 'f') 68 | .filter((con) => con.foreignClass.id === table.id) 69 | .filter((con) => !omit(con, 'read')); 70 | 71 | const multipleFKs = constraints.length > 1; 72 | 73 | const isUnique = !!foreignTable.constraints.find( 74 | (c) => 75 | (c.type === 'p' || c.type === 'u') && 76 | c.keyAttributeNums.length === keys.length && 77 | c.keyAttributeNums.every((n, i) => keys[i].num === n), 78 | ); 79 | 80 | const computedReverseMutationName = inflection.camelCase( 81 | `${ 82 | isUnique 83 | ? nestedMutationsOldUniqueFields 84 | ? inflection.pluralize(tableFieldName) 85 | : tableFieldName 86 | : inflection.pluralize(tableFieldName) 87 | }`, 88 | ); 89 | 90 | if (isForward) { 91 | if (forwardMutationName) { 92 | return forwardMutationName; 93 | } 94 | if (fieldName) { 95 | return fieldName; 96 | } 97 | if (nestedMutationsSimpleFieldNames && !multipleFKs) { 98 | return inflection.camelCase(`${tableFieldName}`); 99 | } 100 | return inflection.camelCase( 101 | `${tableFieldName}_to_${keyNames.join('_and_')}`, 102 | ); 103 | } 104 | 105 | // reverse mutation 106 | if (reverseMutationName) { 107 | return reverseMutationName; 108 | } 109 | if (foreignFieldName) { 110 | return foreignFieldName; 111 | } 112 | if (!multipleFKs) { 113 | return nestedMutationsSimpleFieldNames 114 | ? computedReverseMutationName 115 | : inflection.camelCase( 116 | `${computedReverseMutationName}_using_${foreignKeyNames.join( 117 | '_and_', 118 | )}`, 119 | ); 120 | } 121 | // tables have mutliple relations between them 122 | return inflection.camelCase( 123 | `${computedReverseMutationName}_to_${keyNames.join( 124 | '_and_', 125 | )}_using_${foreignKeyNames.join('_and_')}`, 126 | ); 127 | }, 128 | }); 129 | }); 130 | 131 | builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => { 132 | const { 133 | inflection, 134 | newWithHooks, 135 | pgOmit: omit, 136 | pgGetGqlInputTypeByTypeIdAndModifier: getGqlInputTypeByTypeIdAndModifier, 137 | pgIntrospectionResultsByKind: introspectionResultsByKind, 138 | pgNestedPluginForwardInputTypes, 139 | pgNestedPluginReverseInputTypes, 140 | pgNestedTableConnectorFields, 141 | pgNestedTableDeleterFields, 142 | pgNestedTableUpdaterFields, 143 | pgNestedFieldName, 144 | graphql: { 145 | GraphQLInputObjectType, 146 | GraphQLList, 147 | GraphQLNonNull, 148 | GraphQLBoolean, 149 | }, 150 | } = build; 151 | 152 | const { 153 | scope: { isInputType, isPgRowType, pgIntrospection: table }, 154 | GraphQLInputObjectType: gqlType, 155 | } = context; 156 | 157 | if (!isInputType || !isPgRowType) { 158 | return fields; 159 | } 160 | 161 | const foreignKeyConstraints = introspectionResultsByKind.constraint 162 | .filter((con) => con.type === 'f') 163 | .filter( 164 | (con) => con.classId === table.id || con.foreignClassId === table.id, 165 | ) 166 | .filter((con) => !omit(con, 'read')); 167 | 168 | if (!foreignKeyConstraints.length) { 169 | // table has no foreign relations 170 | return fields; 171 | } 172 | 173 | const tableTypeName = gqlType.name; 174 | 175 | pgNestedPluginForwardInputTypes[table.id] = []; 176 | pgNestedPluginReverseInputTypes[table.id] = []; 177 | 178 | foreignKeyConstraints.forEach((constraint) => { 179 | const isForward = constraint.classId === table.id; 180 | 181 | const foreignTable = isForward 182 | ? introspectionResultsByKind.classById[constraint.foreignClassId] 183 | : introspectionResultsByKind.classById[constraint.classId]; 184 | 185 | // istanbul ignore next 186 | if (!foreignTable) { 187 | throw new Error( 188 | `Could not find the foreign table (constraint: ${constraint.name})`, 189 | ); 190 | } 191 | 192 | const foreignTableName = inflection.tableFieldName(foreignTable); 193 | 194 | const foreignUniqueConstraints = foreignTable.constraints 195 | .filter((con) => con.type === 'u' || con.type === 'p') 196 | .filter((con) => !con.keyAttributes.some((key) => omit(key))); 197 | 198 | const connectable = !!foreignUniqueConstraints.length; 199 | const creatable = 200 | !omit(foreignTable, 'create') && 201 | !omit(constraint, 'create') && 202 | !constraint.keyAttributes.some((key) => omit(key, 'create')); 203 | const updateable = 204 | !omit(foreignTable, 'update') && !omit(constraint, 'update'); 205 | const deleteable = 206 | nestedMutationsDeleteOthers && 207 | foreignTable.primaryKeyConstraint && 208 | !omit(foreignTable, 'delete') && 209 | !omit(constraint, 'delete'); 210 | 211 | if ( 212 | (!connectable && !creatable && !deleteable && !updateable) || 213 | omit(foreignTable, 'read') 214 | // || primaryKey.keyAttributes.some(key => omit(key, 'read')) 215 | // || foreignPrimaryKey.keyAttributes.some(key => omit(key, 'read')) 216 | ) { 217 | return; 218 | } 219 | 220 | const keys = constraint.keyAttributes; 221 | const isUnique = !!foreignTable.constraints.find( 222 | (c) => 223 | (c.type === 'p' || c.type === 'u') && 224 | c.keyAttributeNums.length === keys.length && 225 | c.keyAttributeNums.every((n, i) => keys[i].num === n), 226 | ); 227 | 228 | const fieldName = pgNestedFieldName({ 229 | constraint, 230 | table, 231 | foreignTable, 232 | isForward, 233 | }); 234 | 235 | const createInputTypeName = inflection.nestedCreateInputType({ 236 | constraint, 237 | table, 238 | foreignTable, 239 | isForward, 240 | }); 241 | 242 | const connectorTypeName = inflection.nestedConnectorType({ 243 | constraint, 244 | table, 245 | foreignTable, 246 | isForward, 247 | }); 248 | 249 | const connectorInputField = newWithHooks( 250 | GraphQLInputObjectType, 251 | { 252 | name: connectorTypeName, 253 | description: `Input for the nested mutation of \`${foreignTableName}\` in the \`${tableTypeName}\` mutation.`, 254 | fields: () => { 255 | const gqlForeignTableType = getGqlInputTypeByTypeIdAndModifier( 256 | foreignTable.type.id, 257 | null, 258 | ); 259 | const operations = {}; 260 | 261 | if (!isForward && deleteable) { 262 | operations.deleteOthers = { 263 | description: `Flag indicating whether all other \`${foreignTableName}\` records that match this relationship should be removed.`, 264 | type: GraphQLBoolean, 265 | }; 266 | } 267 | pgNestedTableConnectorFields[foreignTable.id].forEach( 268 | ({ field, fieldName: connectorFieldName }) => { 269 | operations[connectorFieldName] = { 270 | description: `The primary key(s) for \`${foreignTableName}\` for the far side of the relationship.`, 271 | type: isForward 272 | ? field 273 | : isUnique 274 | ? field 275 | : new GraphQLList(new GraphQLNonNull(field)), 276 | }; 277 | }, 278 | ); 279 | if (deleteable) { 280 | pgNestedTableDeleterFields[foreignTable.id].forEach( 281 | ({ field, fieldName: deleterFieldName }) => { 282 | operations[deleterFieldName] = { 283 | description: `The primary key(s) for \`${foreignTableName}\` for the far side of the relationship.`, 284 | type: isForward 285 | ? field 286 | : isUnique 287 | ? field 288 | : new GraphQLList(new GraphQLNonNull(field)), 289 | }; 290 | }, 291 | ); 292 | } 293 | if (pgNestedTableUpdaterFields[table.id][constraint.id]) { 294 | pgNestedTableUpdaterFields[table.id][constraint.id].forEach( 295 | ({ field, fieldName: updaterFieldName }) => { 296 | operations[updaterFieldName] = { 297 | description: `The primary key(s) and patch data for \`${foreignTableName}\` for the far side of the relationship.`, 298 | type: isForward 299 | ? field 300 | : isUnique 301 | ? field 302 | : new GraphQLList(new GraphQLNonNull(field)), 303 | }; 304 | }, 305 | ); 306 | } 307 | if (creatable && gqlForeignTableType) { 308 | const createInputType = newWithHooks( 309 | GraphQLInputObjectType, 310 | { 311 | name: createInputTypeName, 312 | description: `The \`${foreignTableName}\` to be created by this mutation.`, 313 | fields: () => { 314 | const inputFields = gqlForeignTableType._fields; 315 | return Object.keys(inputFields) 316 | .map((k) => Object.assign({}, { [k]: inputFields[k] })) 317 | .reduce((res, o) => Object.assign(res, o), {}); 318 | }, 319 | }, 320 | { 321 | isNestedMutationInputType: true, 322 | isNestedMutationCreateInputType: true, 323 | isNestedInverseMutation: !isForward, 324 | pgInflection: table, 325 | pgNestedForeignInflection: foreignTable, 326 | }, 327 | ); 328 | 329 | operations.create = { 330 | description: `A \`${gqlForeignTableType.name}\` object that will be created and connected to this object.`, 331 | type: isForward 332 | ? createInputType 333 | : new GraphQLList(new GraphQLNonNull(createInputType)), 334 | }; 335 | } 336 | return operations; 337 | }, 338 | }, 339 | { 340 | isNestedMutationConnectorType: true, 341 | isNestedInverseMutation: !isForward, 342 | pgInflection: table, 343 | pgNestedForeignInflection: foreignTable, 344 | }, 345 | ); 346 | 347 | if (isForward) { 348 | pgNestedPluginForwardInputTypes[table.id].push({ 349 | name: fieldName, 350 | constraint, 351 | table, 352 | foreignTable, 353 | keys: constraint.keyAttributes, 354 | foreignKeys: constraint.foreignKeyAttributes, 355 | connectorInputField, 356 | isUnique, 357 | }); 358 | } else { 359 | pgNestedPluginReverseInputTypes[table.id].push({ 360 | name: fieldName, 361 | constraint, 362 | table, 363 | foreignTable, 364 | keys: constraint.keyAttributes, 365 | foreignKeys: constraint.foreignKeyAttributes, 366 | connectorInputField, 367 | isUnique, 368 | }); 369 | } 370 | }); 371 | 372 | return fields; 373 | }); 374 | }; 375 | -------------------------------------------------------------------------------- /src/PostgraphileNestedUpdatersPlugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function PostGraphileNestedUpdatersPlugin(builder) { 2 | builder.hook('inflection', (inflection, build) => 3 | build.extend(inflection, { 4 | nestedUpdateByNodeIdField() { 5 | return this.camelCase(`update_by_${build.nodeIdFieldName}`); 6 | }, 7 | nestedUpdateByKeyField(options) { 8 | const { constraint } = options; 9 | return this.camelCase( 10 | `update_by_${constraint.keyAttributes 11 | .map((k) => this.column(k)) 12 | .join('_and_')}`, 13 | ); 14 | }, 15 | nestedUpdateByNodeIdInputType(options) { 16 | const { table, constraint } = options; 17 | 18 | const tableFieldName = this.tableFieldName(table); 19 | const parentTableFieldName = this.tableFieldName(constraint.class); 20 | const constraintName = constraint.tags.name || constraint.name; 21 | 22 | return this.upperCamelCase( 23 | `${tableFieldName}_on_${parentTableFieldName}_for_${constraintName}_node_id_update`, 24 | ); 25 | }, 26 | nestedUpdatePatchType(options) { 27 | const { table, constraint } = options; 28 | 29 | const tableFieldName = this.tableFieldName(table); 30 | const parentTableFieldName = this.tableFieldName(constraint.class); 31 | const constraintName = constraint.tags.name || constraint.name; 32 | 33 | return this.camelCase( 34 | `update_${tableFieldName}_on_${parentTableFieldName}_for_${constraintName}_patch`, 35 | ); 36 | }, 37 | nestedUpdateByKeyInputType(options) { 38 | const { table, constraint, keyConstraint } = options; 39 | 40 | const tableFieldName = this.tableFieldName(table); 41 | const parentTableFieldName = this.tableFieldName(constraint.class); 42 | const constraintName = constraint.tags.name || constraint.name; 43 | const keyConstraintName = keyConstraint.tags.name || keyConstraint.name; 44 | 45 | return this.upperCamelCase( 46 | `${tableFieldName}_on_${parentTableFieldName}_for_${constraintName}_using_${keyConstraintName}_update`, 47 | ); 48 | }, 49 | }), 50 | ); 51 | 52 | builder.hook('build', (build) => { 53 | const { 54 | extend, 55 | inflection, 56 | gql2pg, 57 | pgSql: sql, 58 | pgColumnFilter, 59 | pgOmit: omit, 60 | pgGetGqlTypeByTypeIdAndModifier, 61 | nodeIdFieldName, 62 | } = build; 63 | 64 | return extend(build, { 65 | pgNestedTableUpdaterFields: {}, 66 | pgNestedTableUpdate: async ({ 67 | nestedField, 68 | connectorField, 69 | input, 70 | pgClient, 71 | where, 72 | context, 73 | }) => { 74 | const { foreignTable } = nestedField; 75 | const { isNodeIdUpdater, constraint } = connectorField; 76 | 77 | let keyWhere = ''; 78 | 79 | if (isNodeIdUpdater) { 80 | const nodeId = input[nodeIdFieldName]; 81 | const primaryKeys = foreignTable.primaryKeyConstraint.keyAttributes; 82 | const { Type, identifiers } = build.getTypeAndIdentifiersFromNodeId( 83 | nodeId, 84 | ); 85 | const ForeignTableType = pgGetGqlTypeByTypeIdAndModifier( 86 | foreignTable.type.id, 87 | null, 88 | ); 89 | if (Type !== ForeignTableType) { 90 | throw new Error('Mismatched type'); 91 | } 92 | if (identifiers.length !== primaryKeys.length) { 93 | throw new Error('Invalid ID'); 94 | } 95 | keyWhere = sql.fragment`(${sql.join( 96 | primaryKeys.map( 97 | (key, idx) => 98 | sql.fragment`${sql.identifier(key.name)} = ${gql2pg( 99 | identifiers[idx], 100 | key.type, 101 | key.typeModifier, 102 | )}`, 103 | ), 104 | ') and (', 105 | )})`; 106 | } else { 107 | const foreignPrimaryKeys = constraint.keyAttributes; 108 | keyWhere = sql.fragment`(${sql.join( 109 | foreignPrimaryKeys.map( 110 | (k) => sql.fragment` 111 | ${sql.identifier(k.name)} = ${gql2pg( 112 | input[inflection.column(k)], 113 | k.type, 114 | k.typeModifier, 115 | )} 116 | `, 117 | ), 118 | ') and (', 119 | )})`; 120 | } 121 | 122 | const patchField = 123 | input[inflection.patchField(inflection.tableFieldName(foreignTable))]; 124 | const sqlColumns = []; 125 | const sqlValues = []; 126 | foreignTable.attributes.forEach((attr) => { 127 | if (!pgColumnFilter(attr, build, context)) return; 128 | if (omit(attr, 'update')) return; 129 | 130 | const colFieldName = inflection.column(attr); 131 | if (colFieldName in patchField) { 132 | const val = patchField[colFieldName]; 133 | sqlColumns.push(sql.identifier(attr.name)); 134 | sqlValues.push(gql2pg(val, attr.type, attr.typeModifier)); 135 | } 136 | }); 137 | 138 | if (sqlColumns.length === 0) { 139 | const selectQuery = sql.query` 140 | select * 141 | from ${sql.identifier( 142 | foreignTable.namespace.name, 143 | foreignTable.name, 144 | )} 145 | where ${ 146 | where ? sql.fragment`(${keyWhere}) and (${where})` : keyWhere 147 | } 148 | `; 149 | const { text, values } = sql.compile(selectQuery); 150 | const { rows } = await pgClient.query(text, values); 151 | return rows[0]; 152 | } 153 | 154 | const updateQuery = sql.query` 155 | update ${sql.identifier( 156 | foreignTable.namespace.name, 157 | foreignTable.name, 158 | )} 159 | set ${sql.join( 160 | sqlColumns.map((col, i) => sql.fragment`${col} = ${sqlValues[i]}`), 161 | ', ', 162 | )} 163 | where ${where ? sql.fragment`(${keyWhere}) and (${where})` : keyWhere} 164 | returning *`; 165 | 166 | const { text, values } = sql.compile(updateQuery); 167 | const { rows } = await pgClient.query(text, values); 168 | return rows[0]; 169 | }, 170 | }); 171 | }); 172 | 173 | builder.hook('GraphQLObjectType:fields', (fields, build, context) => { 174 | const { 175 | inflection, 176 | newWithHooks, 177 | describePgEntity, 178 | nodeIdFieldName, 179 | getTypeByName, 180 | pgIntrospectionResultsByKind: introspectionResultsByKind, 181 | pgGetGqlInputTypeByTypeIdAndModifier, 182 | pgGetGqlTypeByTypeIdAndModifier, 183 | pgOmit: omit, 184 | pgNestedTableUpdaterFields, 185 | graphql: { GraphQLNonNull, GraphQLInputObjectType, GraphQLID }, 186 | } = build; 187 | const { 188 | scope: { isRootMutation }, 189 | } = context; 190 | 191 | if (!isRootMutation) { 192 | return fields; 193 | } 194 | 195 | introspectionResultsByKind.class 196 | .filter((cls) => cls.namespace && cls.isSelectable) 197 | .forEach((table) => { 198 | pgNestedTableUpdaterFields[table.id] = 199 | pgNestedTableUpdaterFields[table.id] || {}; 200 | introspectionResultsByKind.constraint 201 | .filter((con) => con.type === 'f') 202 | .filter( 203 | (con) => 204 | con.classId === table.id || con.foreignClassId === table.id, 205 | ) 206 | .filter((con) => !omit(con, 'read')) 207 | .filter((con) => !con.keyAttributes.some((key) => omit(key, 'read'))) 208 | .forEach((constraint) => { 209 | const foreignTable = 210 | constraint.classId === table.id 211 | ? constraint.foreignClass 212 | : constraint.class; 213 | const ForeignTableType = pgGetGqlTypeByTypeIdAndModifier( 214 | foreignTable.type.id, 215 | null, 216 | ); 217 | const foreignTableFieldName = inflection.tableFieldName( 218 | foreignTable, 219 | ); 220 | const patchFieldName = inflection.patchField(foreignTableFieldName); 221 | const ForeignTablePatch = getTypeByName( 222 | inflection.patchType(ForeignTableType.name), 223 | ); 224 | 225 | if (!ForeignTablePatch) { 226 | return; 227 | } 228 | 229 | const patchType = newWithHooks( 230 | GraphQLInputObjectType, 231 | { 232 | name: inflection.nestedUpdatePatchType({ 233 | table: foreignTable, 234 | constraint, 235 | }), 236 | description: `An object where the defined keys will be set on the \`${foreignTableFieldName}\` being updated.`, 237 | fields: () => { 238 | const omittedFields = constraint.keyAttributes.map((k) => 239 | inflection.column(k), 240 | ); 241 | return Object.keys(ForeignTablePatch._fields) 242 | .filter((key) => !omittedFields.includes(key)) 243 | .map((k) => 244 | Object.assign({}, { [k]: ForeignTablePatch._fields[k] }), 245 | ) 246 | .reduce((res, o) => Object.assign(res, o), {}); 247 | }, 248 | }, 249 | { 250 | isNestedMutationPatchType: true, 251 | pgInflection: foreignTable, 252 | pgFieldInflection: constraint, 253 | }, 254 | ); 255 | 256 | const foreignFields = foreignTable.constraints 257 | .filter((con) => con.type === 'u' || con.type === 'p') 258 | .filter((con) => !omit(con)) 259 | .filter( 260 | (con) => !con.keyAttributes.some((key) => omit(key, 'read')), 261 | ) 262 | .map((keyConstraint) => { 263 | const keys = keyConstraint.keyAttributes; 264 | 265 | // istanbul ignore next 266 | if (!keys.every((_) => _)) { 267 | throw new Error( 268 | `Consistency error: could not find an attribute in the constraint when building nested connection type for ${describePgEntity( 269 | foreignTable, 270 | )}!`, 271 | ); 272 | } 273 | 274 | return { 275 | constraint: keyConstraint, 276 | keys: keyConstraint.keyAttributes, 277 | isNodeIdUpdater: false, 278 | fieldName: inflection.nestedUpdateByKeyField({ 279 | table: foreignTable, 280 | constraint: keyConstraint, 281 | }), 282 | field: newWithHooks( 283 | GraphQLInputObjectType, 284 | { 285 | name: inflection.nestedUpdateByKeyInputType({ 286 | table: foreignTable, 287 | constraint, 288 | keyConstraint, 289 | }), 290 | description: `The fields on \`${foreignTableFieldName}\` to look up the row to update.`, 291 | fields: () => 292 | Object.assign( 293 | {}, 294 | { 295 | [patchFieldName]: { 296 | description: `An object where the defined keys will be set on the \`${foreignTableFieldName}\` being updated.`, 297 | type: new GraphQLNonNull(patchType), 298 | }, 299 | }, 300 | keys 301 | .map((k) => 302 | Object.assign( 303 | {}, 304 | { 305 | [inflection.column(k)]: { 306 | description: k.description, 307 | type: new GraphQLNonNull( 308 | pgGetGqlInputTypeByTypeIdAndModifier( 309 | k.typeId, 310 | k.typeModifier, 311 | ), 312 | ), 313 | }, 314 | }, 315 | ), 316 | ) 317 | .reduce((res, o) => Object.assign(res, o), {}), 318 | ), 319 | }, 320 | { 321 | isNestedMutationInputType: true, 322 | isNestedMutationUpdateInputType: true, 323 | pgInflection: foreignTable, 324 | pgFieldInflection: constraint, 325 | }, 326 | ), 327 | }; 328 | }); 329 | 330 | const { primaryKeyConstraint: foreignPrimaryKey } = foreignTable; 331 | if (nodeIdFieldName && foreignPrimaryKey) { 332 | foreignFields.push({ 333 | constraint: null, 334 | keys: null, 335 | isNodeIdUpdater: true, 336 | fieldName: inflection.nestedUpdateByNodeIdField(), 337 | field: newWithHooks( 338 | GraphQLInputObjectType, 339 | { 340 | name: inflection.nestedUpdateByNodeIdInputType({ 341 | table, 342 | constraint, 343 | }), 344 | description: 345 | 'The globally unique `ID` look up for the row to update.', 346 | fields: { 347 | [nodeIdFieldName]: { 348 | description: `The globally unique \`ID\` which identifies a single \`${foreignTableFieldName}\` to be connected.`, 349 | type: new GraphQLNonNull(GraphQLID), 350 | }, 351 | [patchFieldName]: { 352 | description: `An object where the defined keys will be set on the \`${foreignTableFieldName}\` being updated.`, 353 | type: new GraphQLNonNull(ForeignTablePatch), 354 | }, 355 | }, 356 | }, 357 | { 358 | isNestedMutationInputType: true, 359 | isNestedMutationUpdateInputType: true, 360 | isNestedMutationUpdateByNodeIdType: true, 361 | pgInflection: foreignTable, 362 | }, 363 | ), 364 | }); 365 | } 366 | 367 | pgNestedTableUpdaterFields[table.id][constraint.id] = foreignFields; 368 | }); 369 | }); 370 | 371 | return fields; 372 | }); 373 | }; 374 | --------------------------------------------------------------------------------