-
-
Notifications
You must be signed in to change notification settings - Fork 818
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(stitch): add helpers for Relay * Docs * Add changeset * Improve inheritance * Improve tests
- Loading branch information
Showing
6 changed files
with
311 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@graphql-tools/stitch': minor | ||
--- | ||
|
||
@ardatanfeat(stitch): add helpers for Relay |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { MergedTypeConfig, SubschemaConfig } from '@graphql-tools/delegate'; | ||
import { makeExecutableSchema } from '@graphql-tools/schema'; | ||
import { GraphQLResolveInfo, isInterfaceType, Kind } from 'graphql'; | ||
|
||
const defaultRelayMergeConfig: MergedTypeConfig = { | ||
selectionSet: `{ id }`, | ||
fieldName: 'node', | ||
args: ({ id }: any) => ({ id }), | ||
}; | ||
|
||
export function handleRelaySubschemas(subschemas: SubschemaConfig[], getTypeNameFromId?: (id: string) => string) { | ||
const typeNames: string[] = []; | ||
|
||
for (const subschema of subschemas) { | ||
const nodeType = subschema.schema.getType('Node'); | ||
if (nodeType) { | ||
if (!isInterfaceType(nodeType)) { | ||
throw new Error(`Node type should be an interface!`); | ||
} | ||
const implementations = subschema.schema.getPossibleTypes(nodeType); | ||
for (const implementedType of implementations) { | ||
typeNames.push(implementedType.name); | ||
|
||
subschema.merge = subschema.merge || {}; | ||
subschema.merge[implementedType.name] = defaultRelayMergeConfig; | ||
} | ||
} | ||
} | ||
|
||
const relaySubschemaConfig: SubschemaConfig = { | ||
schema: makeExecutableSchema({ | ||
typeDefs: /* GraphQL */ ` | ||
type Query { | ||
node(id: ID!): Node | ||
} | ||
interface Node { | ||
id: ID! | ||
} | ||
${typeNames | ||
.map( | ||
typeName => ` | ||
type ${typeName} implements Node { | ||
id: ID! | ||
} | ||
` | ||
) | ||
.join('\n')} | ||
`, | ||
resolvers: { | ||
Query: { | ||
node: (_, { id }) => ({ id }), | ||
}, | ||
Node: { | ||
__resolveType: ({ id }: { id: string }, _: any, info: GraphQLResolveInfo) => { | ||
if (!getTypeNameFromId) { | ||
const possibleTypeNames = new Set<string>(); | ||
for (const fieldNode of info.fieldNodes) { | ||
if (fieldNode.selectionSet?.selections) { | ||
for (const selection of fieldNode.selectionSet?.selections) { | ||
switch (selection.kind) { | ||
case Kind.FRAGMENT_SPREAD: { | ||
const fragment = info.fragments[selection.name.value]; | ||
possibleTypeNames.add(fragment.typeCondition.name.value); | ||
break; | ||
} | ||
case Kind.INLINE_FRAGMENT: { | ||
const possibleTypeName = selection.typeCondition?.name.value; | ||
if (possibleTypeName) { | ||
possibleTypeNames.add(possibleTypeName); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
if (possibleTypeNames.size !== 1) { | ||
console.warn( | ||
`You need to define getTypeNameFromId as a parameter to handleRelaySubschemas or add a fragment for "node" operation with specific single type condition!` | ||
); | ||
} | ||
return [...possibleTypeNames][0] || typeNames[0]; | ||
} | ||
return getTypeNameFromId(id); | ||
}, | ||
}, | ||
}, | ||
}), | ||
}; | ||
|
||
subschemas.push(relaySubschemaConfig); | ||
return subschemas; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
import { makeExecutableSchema } from "@graphql-tools/schema"; | ||
import { execute, parse } from "graphql"; | ||
import { handleRelaySubschemas, stitchSchemas } from "../src"; | ||
|
||
const users = [ | ||
{ | ||
id: 'User_0', | ||
name: 'John Doe' | ||
}, | ||
{ | ||
id: 'User_1', | ||
name: 'Jane Doe' | ||
} | ||
]; | ||
|
||
const posts = [ | ||
{ id: 'Post_0', content: 'Lorem Ipsum', userId: 'User_1' }, { id: 'Post_1', content: 'Dolor Sit Amet', userId: 'User_0' } | ||
]; | ||
|
||
describe('Relay', () => { | ||
it('should', async () => { | ||
const userSchema = makeExecutableSchema({ | ||
typeDefs: /* GraphQL */` | ||
type Query { | ||
node(id: ID!): Node | ||
} | ||
interface Node { | ||
id: ID! | ||
} | ||
type User implements Node { | ||
id: ID! | ||
name: String! | ||
} | ||
`, | ||
resolvers: { | ||
Node: { | ||
__resolveType: ({ id }: { id: string }) => id.split('_')[0], | ||
}, | ||
Query: { | ||
node: (_, { id }) => { | ||
if (id.startsWith('User_')) { | ||
return users.find(user => user.id === id); | ||
} | ||
return { | ||
id, | ||
}; | ||
} | ||
} | ||
} | ||
}); | ||
const userResult = await execute({ | ||
schema: userSchema, | ||
document: parse(/* GraphQL */` | ||
fragment User on User { | ||
id | ||
name | ||
} | ||
query UserSchemaQuery { | ||
user0: node(id: "User_0") { | ||
...User | ||
} | ||
user1: node(id: "User_1") { | ||
...User | ||
} | ||
} | ||
`) | ||
}); | ||
expect(userResult.data?.["user0"]?.name).toBe(users[0].name); | ||
expect(userResult.data?.["user1"]?.name).toBe(users[1].name); | ||
const postSchema = makeExecutableSchema({ | ||
typeDefs: /* GraphQL */` | ||
type Query { | ||
node(id: ID!): Node | ||
} | ||
interface Node { | ||
id: ID! | ||
} | ||
type User implements Node { | ||
id: ID! | ||
posts: [Post] | ||
} | ||
type Post implements Node { | ||
id: ID! | ||
content: String! | ||
} | ||
`, | ||
resolvers: { | ||
Node: { | ||
__resolveType: ({ id }: { id: string }) => id.split('_')[0], | ||
}, | ||
Query: { | ||
node: (_, { id }) => { | ||
if (id.startsWith('Post_')) { | ||
return posts.find(post => post.id === id); | ||
} | ||
return { | ||
id, | ||
}; | ||
} | ||
}, | ||
User: { | ||
posts: ({ id }) => posts.filter(({ userId }) => id === userId), | ||
} | ||
} | ||
}); | ||
const postResult = await execute({ | ||
schema: postSchema, | ||
document: parse(/* GraphQL */` | ||
fragment Post on Post { | ||
id | ||
content | ||
} | ||
fragment User on User { | ||
id | ||
posts { | ||
id | ||
content | ||
} | ||
} | ||
query PostSchemaQuery { | ||
post0: node(id: "Post_0") { | ||
...Post | ||
} | ||
post1: node(id: "Post_1") { | ||
...Post | ||
} | ||
user0: node(id: "User_0") { | ||
...User | ||
} | ||
user1: node(id: "User_1") { | ||
...User | ||
} | ||
} | ||
`) | ||
}); | ||
expect(postResult.data?.["post0"]?.content).toBe(posts[0].content); | ||
expect(postResult.data?.["post1"]?.content).toBe(posts[1].content); | ||
expect(postResult.data?.["user0"]?.id).toBe(users[0].id); | ||
expect(postResult.data?.["user0"]?.posts[0].content).toBe(posts[1].content); | ||
expect(postResult.data?.["user1"]?.id).toBe(users[1].id); | ||
expect(postResult.data?.["user1"]?.posts[0].content).toBe(posts[0].content); | ||
|
||
const stitchedSchema = stitchSchemas({ | ||
subschemas: handleRelaySubschemas([ | ||
{ schema: postSchema }, | ||
{ schema: userSchema }, | ||
], id => id.split('_')[0]) | ||
}); | ||
|
||
const stitchedResult = await execute({ | ||
schema: stitchedSchema, | ||
document: parse(/* GraphQL */` | ||
fragment Post on Post { | ||
id | ||
content | ||
} | ||
fragment User on User { | ||
id | ||
name | ||
posts { | ||
id | ||
content | ||
} | ||
} | ||
query PostSchemaQuery { | ||
post0: node(id: "Post_0") { | ||
...Post | ||
} | ||
post1: node(id: "Post_1") { | ||
...Post | ||
} | ||
user0: node(id: "User_0") { | ||
...User | ||
} | ||
user1: node(id: "User_1") { | ||
...User | ||
} | ||
} | ||
`) | ||
}); | ||
|
||
expect(stitchedResult.data?.["post0"]?.content).toBe(posts[0].content); | ||
expect(stitchedResult.data?.["post1"]?.content).toBe(posts[1].content); | ||
expect(stitchedResult.data?.["user0"]?.name).toBe(users[0].name); | ||
expect(stitchedResult.data?.["user0"]?.posts[0].content).toBe(posts[1].content); | ||
expect(stitchedResult.data?.["user1"]?.name).toBe(users[1].name); | ||
expect(stitchedResult.data?.["user1"]?.posts[0].content).toBe(posts[0].content); | ||
}) | ||
}) |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters