Skip to content

Commit 749474b

Browse files
authored
Allow custom scalars in schema (aws#97)
If a given input schema contained a custom scalar, the utility would generate an invalid schema and any field which has a custom scalar type will not be able to be used as input for a query or mutation. Changes include: -fixed `schemaModelValidator.js` to no longer interpret custom scalars as edges and also allow fields of custom scalar types to be included as part of input types -fixed `schemaModelValidator.js` to add the `Options` parameter to generated queries that retrieve an array of objects instead of the queries that retrieve single objects -added documentation for how to use custom scalars with Apollo Server and scalar restrictions with App Sync
1 parent 362ad8e commit 749474b

File tree

7 files changed

+140
-33
lines changed

7 files changed

+140
-33
lines changed

CHANGELOG.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,31 @@ This release contains new support for Apollo Server integration.
2020

2121
### Bug Fixes
2222

23-
* Don't cast integers to floats in Neptune schema (#62)
24-
* Fix query from AppSync with an empty filter object (#61)
25-
* Retain numeric parameter value type when creating open cypher query (#63)
26-
* Fixed bug with ID argument type conversion and added Apollo arguments to help menu (#74)
27-
* Upgraded axios and babel versions to fix security warnings (#90)
28-
* Fixed failing integration test by excluding `node_modules` from Apollo zip (#94)
29-
* Fixed enum types in schema to be included in input types (#95)
30-
* Fixed bug where id fields without @id directives are not accounted for (#96)
23+
* Don't cast integers to floats in Neptune schema ([#62](https://github.com/aws/amazon-neptune-for-graphql/pull/62))
24+
* Fix query from AppSync with an empty filter object ([#61](https://github.com/aws/amazon-neptune-for-graphql/pull/61))
25+
* Retain numeric parameter value type when creating open cypher query ([#63](https://github.com/aws/amazon-neptune-for-graphql/pull/63))
26+
* Fixed bug with ID argument type conversion and added Apollo arguments to help menu ([#74](https://github.com/aws/amazon-neptune-for-graphql/pull/74))
27+
* Upgraded axios and babel versions to fix security warnings ([#90](https://github.com/aws/amazon-neptune-for-graphql/pull/90))
28+
* Fixed failing integration test by excluding `node_modules` from Apollo zip ([#94](https://github.com/aws/amazon-neptune-for-graphql/pull/94))
29+
* Fixed enum types in schema to be included in input types ([#95](https://github.com/aws/amazon-neptune-for-graphql/pull/95))
30+
* Fixed bug where id fields without @id directives are not accounted for ([#96](https://github.com/aws/amazon-neptune-for-graphql/pull/96))
31+
* Fixed custom scalar types in schema to be included in input types ([#97](https://github.com/aws/amazon-neptune-for-graphql/pull/97))
32+
* Fixed queries generated from an input schema which retrieve an array to have an option parameter with limit ([#97](https://github.com/aws/amazon-neptune-for-graphql/pull/97))
33+
3134

3235
### Features
3336

34-
* Support output of zip package of Apollo Server artifacts (#70, #72, #73, #75, #76)
37+
* Support output of zip package of Apollo Server artifacts (([#70](https://github.com/aws/amazon-neptune-for-graphql/pull/70)), ([#72](https://github.com/aws/amazon-neptune-for-graphql/pull/72)), ([#73](https://github.com/aws/amazon-neptune-for-graphql/pull/73)), ([#75](https://github.com/aws/amazon-neptune-for-graphql/pull/75)), ([#76](https://github.com/aws/amazon-neptune-for-graphql/pull/76)))
3538

3639
### Improvements
3740

38-
* Increased graphdb.js test coverage using sample data (#53)
39-
* Saved the neptune schema to file early so that it can be used for troubleshooting (#56)
40-
* Alias edges with same label as a node (#57)
41-
* Cap concurrent requests to get Neptune schema (#58)
42-
* Honour @id directive on type fields (#60)
43-
* Changed lambda template to use ECMAScripts modules (#68)
44-
* Add template file missing from packaging (#71)
45-
* Separated graphQL schema from resolver template (#79)
46-
* Added unit tests for resolver and moved resolver integration tests to be unit tests (#83)
47-
* Set limit on the expensive query which is retrieving distinct to and from labels for edges (#89)
41+
* Increased graphdb.js test coverage using sample data ([#53](https://github.com/aws/amazon-neptune-for-graphql/pull/53))
42+
* Saved the neptune schema to file early so that it can be used for troubleshooting ([#56](https://github.com/aws/amazon-neptune-for-graphql/pull/56))
43+
* Alias edges with same label as a node ([#57](https://github.com/aws/amazon-neptune-for-graphql/pull/57))
44+
* Cap concurrent requests to get Neptune schema ([#58](https://github.com/aws/amazon-neptune-for-graphql/pull/58))
45+
* Honour @id directive on type fields ([#60](https://github.com/aws/amazon-neptune-for-graphql/pull/60))
46+
* Changed lambda template to use ECMAScripts modules ([#68](https://github.com/aws/amazon-neptune-for-graphql/pull/68))
47+
* Add template file missing from packaging ([#71](https://github.com/aws/amazon-neptune-for-graphql/pull/71))
48+
* Separated graphQL schema from resolver template ([#79](https://github.com/aws/amazon-neptune-for-graphql/pull/79))
49+
* 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))
50+
* 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))

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,17 @@ The example commands above will generate the file `apollo-server-<identifier>-<t
302302
> [!NOTE]
303303
> Node's default AWS credentials provider is used for authentication with Neptune. See [AWS SDK credential providers](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromnodeproviderchain) for more information.
304304
305+
### Using Custom Scalars with Apollo Server
306+
When using custom scalars in your schema (specified via `--input-schema-file`), you should add corresponding scalar resolvers to the index.mjs file in the generated Apollo Server artifact. Although queries will execute without these resolvers, the absence of the custom scalar resolvers will bypass important data validation for fields using custom scalar types. See [Apollo Custom Scalars](https://www.apollographql.com/docs/apollo-server/schema/custom-scalars) for more details on how to attach custom scalar resolvers with Apollo Server.
307+
305308
# Known limitations
306309
- @graphQuery using Gremlin works only if the query returns a scalar value, one elementMap(), or list as elementMap().fold(), this feature is under development.
307310
- Neptune RDF database and SPARQL language is not supported.
308311
- Querying Neptune via SDK is not yet supported for Apollo Server, only HTTPS is supported.
309312
- Mutations are not yet supported for Apollo Server
310-
<br>
313+
- Schemas specified by `--input-schema-file` with `--create-update-aws-pipeline` may not contain custom scalars. See [AWS App Sync Scalar types in GraphQL](https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html) for more information.
314+
- Schemas specified by `--input-schema-file` with `--create-update-apollo-server` or `--create-update-apollo-server-subgraph` which contain custom scalars require manual steps to add custom scalar resolvers for additional query validation.
315+
<br>
311316

312317
# Roadmap
313318
- Gremlin resolver.

doc/todoExample.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ input Options {
7373
}
7474

7575
type Query {
76-
getNodeTodo(filter: TodoInput, options: Options): Todo
77-
getNodeTodos(filter: TodoInput): [Todo]
78-
getNodeComment(filter: CommentInput, options: Options): Comment
79-
getNodeComments(filter: CommentInput): [Comment]
76+
getNodeTodo(filter: TodoInput): Todo
77+
getNodeTodos(filter: TodoInput, options: Options): [Todo]
78+
getNodeComment(filter: CommentInput): Comment
79+
getNodeComments(filter: CommentInput, options: Options): [Comment]
8080
}
8181

8282
type Mutation {

src/schemaModelValidator.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const typesToAdd = [];
2222
const queriesToAdd = [];
2323
const mutationsToAdd = [];
2424
const enumTypes = [];
25+
const customScalarTypes = [];
2526

2627
function lowercaseFirstCharacter(inputString) {
2728
if (inputString.length === 0) {
@@ -145,8 +146,8 @@ function addNode(def) {
145146
typesToAdd.push(`input ${name}Input {\n${print(getInputFields(def))}\n}`);
146147

147148
// Create query
148-
queriesToAdd.push(`getNode${name}(filter: ${name}Input, options: Options): ${name}\n`);
149-
queriesToAdd.push(`getNode${name}s(filter: ${name}Input): [${name}]\n`);
149+
queriesToAdd.push(`getNode${name}(filter: ${name}Input): ${name}\n`);
150+
queriesToAdd.push(`getNode${name}s(filter: ${name}Input, options: Options): [${name}]\n`);
150151

151152
// Create mutation
152153
mutationsToAdd.push(`createNode${name}(input: ${name}Input!): ${name}\n`);
@@ -256,7 +257,7 @@ function nullable(type) {
256257

257258

258259
function isScalarOrEnum(type) {
259-
const scalarOrEnumTypes = ['String', 'Int', 'Float', 'Boolean', 'ID', ...enumTypes];
260+
const scalarOrEnumTypes = ['String', 'Int', 'Float', 'Boolean', 'ID', ...enumTypes, ...customScalarTypes];
260261
return type.kind === 'NamedType' && scalarOrEnumTypes.includes(type.name.value);
261262
}
262263

@@ -266,11 +267,14 @@ function inferGraphDatabaseDirectives(schemaModel) {
266267
var currentType = '';
267268
let referencedType = '';
268269
let edgeName = '';
269-
270270
schemaModel.definitions
271271
.filter(definition => definition.kind === 'EnumTypeDefinition')
272272
.forEach(definition => enumTypes.push(definition.name.value));
273273

274+
schemaModel.definitions
275+
.filter(definition => definition.kind === 'ScalarTypeDefinition')
276+
.forEach(definition => customScalarTypes.push(definition.name.value));
277+
274278
schemaModel.definitions.forEach(def => {
275279
if (def.kind == 'ObjectTypeDefinition') {
276280
if (!(def.name.value == 'Query' || def.name.value == 'Mutation')) {
@@ -294,6 +298,7 @@ function inferGraphDatabaseDirectives(schemaModel) {
294298
// add relationships
295299
def.fields.forEach(field => {
296300
if (field.type.type !== undefined) {
301+
// FIXME handle NonNullType wrapper
297302
if (field.type.type.kind === 'NamedType' && field.type.type.name.value !== 'ID')
298303
{
299304
try {
@@ -316,7 +321,8 @@ function inferGraphDatabaseDirectives(schemaModel) {
316321
field.type.name.value !== 'Int' &&
317322
field.type.name.value !== 'Float' &&
318323
field.type.name.value !== 'Boolean' &&
319-
!enumTypes.includes(field.type.name.value)) {
324+
!enumTypes.includes(field.type.name.value) &&
325+
!customScalarTypes.includes(field.type.name.value)) {
320326

321327
referencedType = field.type.name.value;
322328
edgeName = referencedType + 'Edge';

src/test/schemaModelValidator.test.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { readFileSync } from 'node:fs';
22
import { loggerInit } from '../logger.js';
33
import { validatedSchemaModel } from '../schemaModelValidator.js';
4-
import { schemaParser } from '../schemaParser.js';
4+
import { schemaParser, schemaStringify } from '../schemaParser.js';
55

66
describe('validatedSchemaModel', () => {
77
let model;
88

99
beforeAll(() => {
1010
loggerInit('./output', false, 'silent');
1111

12-
const schema = readFileSync('./src/test/directive-id.graphql');
12+
const schema = readFileSync('./src/test/user-group.graphql');
1313
model = validatedSchemaModel(schemaParser(schema));
1414
});
1515

@@ -28,9 +28,9 @@ describe('validatedSchemaModel', () => {
2828
const groupType = objTypeDefs.find(def => def.name.value === 'Group');
2929
const moderatorType = objTypeDefs.find(def => def.name.value === 'Moderator');
3030

31-
expect(userType.fields).toHaveLength(4);
31+
expect(userType.fields).toHaveLength(5);
3232
expect(groupType.fields).toHaveLength(2);
33-
expect(moderatorType.fields).toHaveLength(2);
33+
expect(moderatorType.fields).toHaveLength(4);
3434

3535
const userIdFields = getIdFields(userType);
3636
const groupIdFields = getIdFields(groupType);
@@ -74,6 +74,12 @@ describe('validatedSchemaModel', () => {
7474
const userRoleField = userInput.fields.find(field => field.name.value === 'role');
7575
expect(userRoleField.type.name.value).toEqual('Role');
7676
});
77+
78+
test('should output expected validated schema', () => {
79+
const actual = schemaStringify(model, true);
80+
const expected = readFileSync('./src/test/user-group-validated.graphql', 'utf8')
81+
expect(actual).toBe(expected);
82+
});
7783

7884
function getIdFields(objTypeDef) {
7985
return objTypeDef.fields.filter(
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
type User {
2+
userId: ID! @id
3+
firstName: String
4+
lastName: String
5+
role: Role
6+
email: EmailAddress
7+
}
8+
9+
type Group {
10+
_id: ID! @id
11+
name: String
12+
}
13+
14+
type Moderator {
15+
moderatorId: ID! @id
16+
name: String
17+
moderates: Group @relationship(type: "GroupEdge", direction: OUT)
18+
groupEdge: GroupEdge
19+
}
20+
21+
enum Role {
22+
ADMIN
23+
USER
24+
GUEST
25+
}
26+
27+
"https://the-guild.dev/graphql/scalars/docs/scalars/email-address"
28+
scalar EmailAddress
29+
30+
input UserInput {
31+
userId: ID! @id
32+
firstName: String
33+
lastName: String
34+
role: Role
35+
email: EmailAddress
36+
}
37+
38+
input GroupInput {
39+
_id: ID! @id
40+
name: String
41+
}
42+
43+
input ModeratorInput {
44+
moderatorId: ID! @id
45+
name: String
46+
}
47+
48+
type GroupEdge {
49+
_id: ID! @id
50+
}
51+
52+
input Options {
53+
limit: Int
54+
}
55+
56+
type Query {
57+
getNodeUser(filter: UserInput): User
58+
getNodeUsers(filter: UserInput, options: Options): [User]
59+
getNodeGroup(filter: GroupInput): Group
60+
getNodeGroups(filter: GroupInput, options: Options): [Group]
61+
getNodeModerator(filter: ModeratorInput): Moderator
62+
getNodeModerators(filter: ModeratorInput, options: Options): [Moderator]
63+
}
64+
65+
type Mutation {
66+
createNodeUser(input: UserInput!): User
67+
updateNodeUser(input: UserInput!): User
68+
deleteNodeUser(userId: ID!): Boolean
69+
createNodeGroup(input: GroupInput!): Group
70+
updateNodeGroup(input: GroupInput!): Group
71+
deleteNodeGroup(_id: ID!): Boolean
72+
createNodeModerator(input: ModeratorInput!): Moderator
73+
updateNodeModerator(input: ModeratorInput!): Moderator
74+
deleteNodeModerator(moderatorId: ID!): Boolean
75+
connectNodeModeratorToNodeGroupEdgeGroupEdge(from_id: ID!, to_id: ID!): GroupEdge
76+
deleteEdgeGroupEdgeFromModeratorToGroup(from_id: ID!, to_id: ID!): Boolean
77+
}
78+
79+
schema {
80+
query: Query
81+
mutation: Mutation
82+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ type User {
33
firstName: String
44
lastName: String
55
role: Role
6+
email: EmailAddress
67
}
78

89
type Group {
@@ -12,10 +13,14 @@ type Group {
1213
type Moderator {
1314
moderatorId: ID!
1415
name: String
16+
moderates: Group
1517
}
1618

1719
enum Role {
1820
ADMIN
1921
USER
2022
GUEST
2123
}
24+
25+
"https://the-guild.dev/graphql/scalars/docs/scalars/email-address"
26+
scalar EmailAddress

0 commit comments

Comments
 (0)