Skip to content

Commit b827c60

Browse files
authored
Added separate input types for create and update mutations while maintaining original input type (aws#93)
The utility currently generates the same input types for queries as mutations. This is not ideal as it would be expected that the required fields will differ for queries vs mutations. For example, id should not be mandatory when creating a node as it should be auto generated by the server. However id should be required when updating a node. Generating distinct input types for queries vs create and update mutations will provide better flexibility to the user to modify the generated schema accordingly.
1 parent 749474b commit b827c60

File tree

8 files changed

+547
-50
lines changed

8 files changed

+547
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ This release contains new support for Apollo Server integration.
4848
* Separated graphQL schema from resolver template ([#79](https://github.com/aws/amazon-neptune-for-graphql/pull/79))
4949
* Added unit tests for resolver and moved resolver integration tests to be unit tests ([#83](https://github.com/aws/amazon-neptune-for-graphql/pull/83))
5050
* Set limit on the expensive query which is retrieving distinct to and from labels for edges ([#89](https://github.com/aws/amazon-neptune-for-graphql/pull/89))
51+
* Added distinct input types for create and update mutations ([#93](https://github.com/aws/amazon-neptune-for-graphql/pull/93))

src/graphdb.js

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
8181
checkForDuplicateNames(gdbs);
8282

8383
gdbs.nodeStructures.forEach(node => {
84+
// map with key=node property name and value=formatted string containing property name, type and optional alias
85+
const nodePropertiesMap = new Map(node.properties.map(property => {
86+
if (property.name === 'id') {
87+
return [property.name, `\tid: ID\n`];
88+
}
89+
const sanitizedLabel = cleanseLabel(property.name);
90+
let alias = '';
91+
if (property.name !== sanitizedLabel) {
92+
alias = ` @alias(property: "${property.name}")`;
93+
}
94+
return [property.name, `\t${sanitizedLabel}: ${property.type}${alias}\n`];
95+
}));
96+
97+
const formattedNodeProperties = Array.from(nodePropertiesMap.values()).join('');
98+
8499
// node type
85100
let nodeCase = cleanseLabel(node.label);
86101
if (changeCase) {
@@ -95,20 +110,7 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
95110
}
96111

97112
r += '\t_id: ID! @id\n';
98-
99-
node.properties.forEach(property => {
100-
if (property.name === 'id') {
101-
r+= `\tid: ID\n`;
102-
}
103-
else {
104-
let propertyCase = cleanseLabel(property.name);
105-
let alias = '';
106-
if (property.name !== propertyCase) {
107-
alias = ` @alias(property: "${property.name}")`;
108-
}
109-
r+= `\t${propertyCase}: ${property.type}${alias}\n`;
110-
}
111-
});
113+
r += formattedNodeProperties;
112114

113115
let edgeTypes = [];
114116
gdbs.edgeStructures.forEach(edge => {
@@ -160,12 +162,10 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
160162
});
161163
});
162164

163-
const nodePropertyNames = new Set(node.properties.map((p) => p.name));
164-
165165
// Add edge types
166166
edgeTypes.forEach((edgeType) => {
167167
// resolve any collision with node properties with the same name by adding an underscore prefix
168-
const aliasedEdgeType = nodePropertyNames.has(edgeType)
168+
const aliasedEdgeType = nodePropertiesMap.has(edgeType)
169169
? `_${edgeType}`
170170
: edgeType;
171171

@@ -182,16 +182,22 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
182182
// input for the node type
183183
r += `input ${nodeCase}Input {\n`;
184184
r += '\t_id: ID @id\n';
185-
node.properties.forEach(property => {
186-
let propertyCase = cleanseLabel(property.name);
187-
if (property.name !== propertyCase) {
188-
r+= `\t${propertyCase}: ${property.type} @alias(property: "${property.name}")\n`;
189-
}
190-
else {
191-
r+= `\t${property.name}: ${property.type}\n`;
192-
}
193-
});
185+
r += formattedNodeProperties;
194186
r += '}\n\n';
187+
188+
if (addMutations) {
189+
// Create input for mutations
190+
r += `input ${nodeCase}CreateInput {\n`;
191+
r += '\t_id: ID @id\n';
192+
r += formattedNodeProperties;
193+
r += '}\n\n';
194+
195+
// Update input for mutations
196+
r += `input ${nodeCase}UpdateInput {\n`;
197+
r += '\t_id: ID! @id\n';
198+
r += formattedNodeProperties;
199+
r += '}\n\n';
200+
}
195201
})
196202

197203
const nodeLabels = new Set(gdbs.nodeStructures.map((n) => n.label));
@@ -267,8 +273,8 @@ function graphDBInferenceSchema (graphbSchema, addMutations) {
267273
r += `type Mutation {\n`;
268274
gdbs.nodeStructures.forEach(node => {
269275
let nodeCase = toPascalCase(cleanseLabel(node.label));
270-
r += `\tcreateNode${nodeCase}(input: ${nodeCase}Input!): ${nodeCase}\n`;
271-
r += `\tupdateNode${nodeCase}(input: ${nodeCase}Input!): ${nodeCase}\n`;
276+
r += `\tcreateNode${nodeCase}(input: ${nodeCase}CreateInput!): ${nodeCase}\n`;
277+
r += `\tupdateNode${nodeCase}(input: ${nodeCase}UpdateInput!): ${nodeCase}\n`;
272278
r += `\tdeleteNode${nodeCase}(_id: ID!): Boolean\n`;
273279
});
274280

src/schemaModelValidator.js

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ permissions and limitations under the License.
1111
*/
1212

1313
import { schemaStringify } from './schemaParser.js';
14-
import { print } from 'graphql';
14+
import { GraphQLID, print } from 'graphql';
1515
import {gql} from 'graphql-tag'
1616
import { loggerInfo, yellow } from "./logger.js";
17-
import { GraphQLID } from 'graphql';
1817

1918
let quiet = false;
2019
// TODO change variables to local scope instead of global so this module can be used against multiple schemas
@@ -142,19 +141,55 @@ function addNode(def) {
142141
let name = def.name.value;
143142
const idField = getIdFieldWithDirective(def);
144143

144+
// General Input type
145+
typesToAdd.push(`input ${name}Input {\n${print(getInputFields(def))}\n}`);
146+
145147
// Create Input type
146-
typesToAdd.push(`input ${name}Input {\n${print(getInputFields(def))}\n}`);
148+
const createFields = [];
149+
for (const field of def.fields) {
150+
if (isScalarOrEnum(nullable(field.type))) {
151+
if (field.type?.kind === 'NonNullType' && field.type?.type?.name?.value === GraphQLID.name) {
152+
const idFieldCopy = {
153+
...field,
154+
type: field.type.type
155+
};
156+
createFields.push(idFieldCopy);
157+
} else {
158+
createFields.push(field);
159+
}
160+
}
161+
}
162+
typesToAdd.push(`input ${name}CreateInput {\n${print(createFields)}\n}`);
163+
164+
// Update Input type
165+
const updateFields = [];
166+
for (const field of def.fields) {
167+
if (isScalarOrEnum(nullable(field.type))) {
168+
if (field.type?.kind === 'NonNullType' && field.type?.type?.name?.value !== GraphQLID.name) {
169+
const fieldCopy = {
170+
...field,
171+
type: field.type.type
172+
};
173+
updateFields.push(fieldCopy);
174+
} else {
175+
updateFields.push(field);
176+
}
177+
}
178+
}
179+
typesToAdd.push(`input ${name}UpdateInput {\n${print(updateFields)}\n}`);
147180

148181
// Create query
149182
queriesToAdd.push(`getNode${name}(filter: ${name}Input): ${name}\n`);
150183
queriesToAdd.push(`getNode${name}s(filter: ${name}Input, options: Options): [${name}]\n`);
151184

152185
// Create mutation
153-
mutationsToAdd.push(`createNode${name}(input: ${name}Input!): ${name}\n`);
154-
mutationsToAdd.push(`updateNode${name}(input: ${name}Input!): ${name}\n`);
186+
mutationsToAdd.push(`createNode${name}(input: ${name}CreateInput!): ${name}\n`);
187+
mutationsToAdd.push(`updateNode${name}(input: ${name}UpdateInput!): ${name}\n`);
155188
mutationsToAdd.push(`deleteNode${name}(${print(idFieldToInputValue(idField))}): Boolean\n`);
156189

157190
loggerInfo(`Added input type: ${yellow(name+'Input')}`);
191+
loggerInfo(`Added input type: ${yellow(name+'CreateInput')}`);
192+
loggerInfo(`Added input type: ${yellow(name+'UpdateInput')}`);
158193
loggerInfo(`Added query: ${yellow('getNode' + name)}`);
159194
loggerInfo(`Added query: ${yellow('getNode' + name + 's')}`);
160195
loggerInfo(`Added mutation: ${yellow('createNode' + name)}`);
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
type Continent @alias(property:"continent") {
2+
_id: ID! @id
3+
type: String
4+
code: String
5+
desc: String
6+
airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"contains", direction:OUT)
7+
contains:Contains
8+
}
9+
10+
input ContinentInput {
11+
_id: ID @id
12+
type: String
13+
code: String
14+
desc: String
15+
}
16+
17+
input ContinentCreateInput {
18+
_id: ID @id
19+
type: String
20+
code: String
21+
desc: String
22+
}
23+
24+
input ContinentUpdateInput {
25+
_id: ID! @id
26+
type: String
27+
code: String
28+
desc: String
29+
}
30+
31+
type Country @alias(property:"country") {
32+
_id: ID! @id
33+
type: String
34+
code: String
35+
desc: String
36+
airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"contains", direction:OUT)
37+
contains:Contains
38+
}
39+
40+
input CountryInput {
41+
_id: ID @id
42+
type: String
43+
code: String
44+
desc: String
45+
}
46+
47+
input CountryCreateInput {
48+
_id: ID @id
49+
type: String
50+
code: String
51+
desc: String
52+
}
53+
54+
input CountryUpdateInput {
55+
_id: ID! @id
56+
type: String
57+
code: String
58+
desc: String
59+
}
60+
61+
type Version @alias(property:"version") {
62+
_id: ID! @id
63+
date: String
64+
desc: String
65+
author: String
66+
type: String
67+
code: String
68+
}
69+
70+
input VersionInput {
71+
_id: ID @id
72+
date: String
73+
desc: String
74+
author: String
75+
type: String
76+
code: String
77+
}
78+
79+
input VersionCreateInput {
80+
_id: ID @id
81+
date: String
82+
desc: String
83+
author: String
84+
type: String
85+
code: String
86+
}
87+
88+
input VersionUpdateInput {
89+
_id: ID! @id
90+
date: String
91+
desc: String
92+
author: String
93+
type: String
94+
code: String
95+
}
96+
97+
type Airport @alias(property:"airport") {
98+
_id: ID! @id
99+
type: String
100+
city: String
101+
icao: String
102+
code: String
103+
country: String
104+
lat: Float
105+
longest: Int
106+
runways: Int
107+
desc: String
108+
lon: Float
109+
region: String
110+
elev: Int
111+
continentContainsIn: Continent @relationship(edgeType:"contains", direction:IN)
112+
countryContainsIn: Country @relationship(edgeType:"contains", direction:IN)
113+
airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"route", direction:OUT)
114+
airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"route", direction:IN)
115+
contains:Contains
116+
route:Route
117+
}
118+
119+
input AirportInput {
120+
_id: ID @id
121+
type: String
122+
city: String
123+
icao: String
124+
code: String
125+
country: String
126+
lat: Float
127+
longest: Int
128+
runways: Int
129+
desc: String
130+
lon: Float
131+
region: String
132+
elev: Int
133+
}
134+
135+
input AirportCreateInput {
136+
_id: ID @id
137+
type: String
138+
city: String
139+
icao: String
140+
code: String
141+
country: String
142+
lat: Float
143+
longest: Int
144+
runways: Int
145+
desc: String
146+
lon: Float
147+
region: String
148+
elev: Int
149+
}
150+
151+
input AirportUpdateInput {
152+
_id: ID! @id
153+
type: String
154+
city: String
155+
icao: String
156+
code: String
157+
country: String
158+
lat: Float
159+
longest: Int
160+
runways: Int
161+
desc: String
162+
lon: Float
163+
region: String
164+
elev: Int
165+
}
166+
167+
type Contains @alias(property:"contains") {
168+
_id: ID! @id
169+
}
170+
171+
type Route @alias(property:"route") {
172+
_id: ID! @id
173+
dist: Int
174+
}
175+
176+
input RouteInput {
177+
dist: Int
178+
}
179+
180+
input Options {
181+
limit:Int
182+
}
183+
184+
type Query {
185+
getNodeContinent(filter: ContinentInput): Continent
186+
getNodeContinents(filter: ContinentInput, options: Options): [Continent]
187+
getNodeCountry(filter: CountryInput): Country
188+
getNodeCountrys(filter: CountryInput, options: Options): [Country]
189+
getNodeVersion(filter: VersionInput): Version
190+
getNodeVersions(filter: VersionInput, options: Options): [Version]
191+
getNodeAirport(filter: AirportInput): Airport
192+
getNodeAirports(filter: AirportInput, options: Options): [Airport]
193+
}
194+
195+
type Mutation {
196+
createNodeContinent(input: ContinentCreateInput!): Continent
197+
updateNodeContinent(input: ContinentUpdateInput!): Continent
198+
deleteNodeContinent(_id: ID!): Boolean
199+
createNodeCountry(input: CountryCreateInput!): Country
200+
updateNodeCountry(input: CountryUpdateInput!): Country
201+
deleteNodeCountry(_id: ID!): Boolean
202+
createNodeVersion(input: VersionCreateInput!): Version
203+
updateNodeVersion(input: VersionUpdateInput!): Version
204+
deleteNodeVersion(_id: ID!): Boolean
205+
createNodeAirport(input: AirportCreateInput!): Airport
206+
updateNodeAirport(input: AirportUpdateInput!): Airport
207+
deleteNodeAirport(_id: ID!): Boolean
208+
connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains
209+
deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean
210+
connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains
211+
deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean
212+
connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route
213+
updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route
214+
deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean
215+
}
216+
217+
schema {
218+
query: Query
219+
mutation: Mutation
220+
}

0 commit comments

Comments
 (0)