Skip to content

Commit 064d795

Browse files
committed
Add basic type wrapping, that support Relay specific logic/fields/interfaces
1 parent 643ec87 commit 064d795

File tree

9 files changed

+250
-32
lines changed

9 files changed

+250
-32
lines changed

.babelrc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"presets": ["react"],
32
"plugins": ["dev-expression", "transform-runtime"],
43

54
"env": {
@@ -11,4 +10,4 @@
1110
"presets": ["es2015-loose-native-modules", "stage-0"]
1211
}
1312
}
14-
}
13+
}

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lib/*
2+
es/*

src/composeWithRelay.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* @flow */
2+
/* eslint-disable no-use-before-define */
3+
4+
import { TypeComposer } from 'graphql-compose';
5+
import NodeInterface from './nodeInterface';
6+
import MutationMiddleware from './mutationMiddleware';
7+
import { toGlobalId } from './globalId';
8+
import { GraphQLID, GraphQLString } from 'graphql';
9+
import { getNodeFieldConfig } from './nodeFieldConfig';
10+
11+
// all wrapped typeComposers with Relay, stored in this variable
12+
// for futher type resolving via NodeInterface.resolveType method
13+
const typeComposerMap = {};
14+
15+
export function composeWithRelay(
16+
typeComposer: TypeComposer
17+
): TypeComposer {
18+
if (typeComposer.getTypeName() === 'RootQuery') {
19+
typeComposer.addField('node', getNodeFieldConfig(typeComposerMap));
20+
return typeComposer;
21+
}
22+
23+
typeComposerMap[typeComposer.getTypeName()] = typeComposer;
24+
25+
typeComposer.addFields({
26+
id: {
27+
type: GraphQLID,
28+
description: 'The globally unique ID among all types',
29+
resolve: (source) => toGlobalId(
30+
typeComposer.getTypeName(),
31+
typeComposer.getRecordId(source)
32+
),
33+
},
34+
_nodeTypeName: {
35+
type: GraphQLString,
36+
description: 'Cuurent object type name, for resolving via node interface.',
37+
resolve: typeComposer.getTypeName(),
38+
},
39+
});
40+
41+
typeComposer.addInterface(NodeInterface);
42+
43+
typeComposer.getResolvers().forEach(resolver => {
44+
if (resolver.kind === 'mutation') {
45+
resolver.addMiddleware(MutationMiddleware);
46+
}
47+
});
48+
49+
return typeComposer;
50+
}

src/definition.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// RE-EXPORT graphql-compose definitions
2+
import type {
3+
ResolveParams as _ResolveParams,
4+
} from 'graphql-compose/lib/definition.js';
5+
6+
export type ResolveParams = _ResolveParams;
7+
8+
9+
// INTERNAL TYPES
10+
export type Base64String = string;
11+
export type ResolvedGlobalId = {
12+
type: string,
13+
id: string,
14+
};

src/globalId.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* @flow */
2+
3+
import {
4+
ResolvedGlobalId,
5+
Base64String,
6+
} from './definition.js';
7+
8+
export function base64(i: string): Base64String {
9+
return ((new Buffer(i, 'ascii')).toString('base64'));
10+
}
11+
12+
export function unbase64(i: Base64String): string {
13+
return ((new Buffer(i, 'base64')).toString('ascii'));
14+
}
15+
16+
/**
17+
* Takes a type name and an ID specific to that type name, and returns a
18+
* "global ID" that is unique among all types.
19+
*/
20+
export function toGlobalId(type: string, id: string): string {
21+
return base64([type, id].join(':'));
22+
}
23+
24+
/**
25+
* Takes the "global ID" created by toGlobalID, and returns the type name and ID
26+
* used to create it.
27+
*/
28+
export function fromGlobalId(globalId: string): ResolvedGlobalId {
29+
const unbasedGlobalId = unbase64(globalId);
30+
const delimiterPos = unbasedGlobalId.indexOf(':');
31+
return {
32+
type: unbasedGlobalId.substring(0, delimiterPos),
33+
id: unbasedGlobalId.substring(delimiterPos + 1),
34+
};
35+
}

src/index.js

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,3 @@
1-
import { GraphQLSchema } from 'graphql';
2-
import compose from './compose';
3-
4-
function initGraphqlTypes() {
5-
// populate root types in Storage.Types
6-
getViewerType();
7-
getRootQueryType();
8-
getRootMutationType();
9-
10-
// now all types declared, we are ready to extend types
11-
resolveUnresolvedRefs();
12-
addAdditionalFields();
13-
}
14-
15-
function getSchema() {
16-
initGraphqlTypes();
17-
18-
const schemaConfig = { query: getRootQueryType() };
19-
20-
if (Storage.AdditionalFields.has('RootMutation')) {
21-
schemaConfig.mutation = getRootMutationType();
22-
}
23-
24-
return new GraphQLSchema(schemaConfig);
25-
}
26-
27-
28-
export {
29-
getSchema,
30-
};
1+
import { composeWithRelay } from './composeWithRelay';
312

3+
export default composeWithRelay;

src/mutationMiddleware.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* @flow */
2+
/* eslint-disable no-param-reassign */
3+
4+
import { ResolverMiddleware, TypeComposer } from 'graphql-compose';
5+
import { GraphQLID, GraphQLString, GraphQLObjectType } from 'graphql';
6+
import { toGlobalId } from './globalId';
7+
8+
export class MutationMiddleware extends ResolverMiddleware {
9+
// constructor(typeComposer, opts = {}) {
10+
// super(typeComposer, opts);
11+
// }
12+
13+
args = (next) => (args) => {
14+
const nextArgs = next(args);
15+
16+
if (!nextArgs.clientMutationId) {
17+
nextArgs.clientMutationId = {
18+
type: GraphQLString,
19+
description: 'The client mutation ID used by clients like Relay to track the mutation. '
20+
+ 'If given, returned in the response payload of the mutation.',
21+
};
22+
}
23+
24+
return nextArgs;
25+
};
26+
27+
28+
resolve = (next) => (resolveParams) => {
29+
let clientMutationId;
30+
31+
if (resolveParams && resolveParams.args && resolveParams.args.input
32+
&& resolveParams.args.input.clientMutationId) {
33+
clientMutationId = resolveParams.args.input.clientMutationId;
34+
delete resolveParams.args.input.clientMutationId;
35+
}
36+
37+
return next(resolveParams)
38+
.then(res => {
39+
res.nodeId = toGlobalId(
40+
this.typeComposer.getTypeName(),
41+
this.typeComposer.getRecordId(res),
42+
);
43+
if (clientMutationId) {
44+
res.clientMutationId = clientMutationId;
45+
}
46+
return res;
47+
});
48+
};
49+
50+
51+
outputType = (next) => (outputType) => {
52+
const nextOutputType = next(outputType);
53+
54+
if (!(nextOutputType instanceof GraphQLObjectType)) {
55+
return nextOutputType;
56+
}
57+
58+
const outputTC = new TypeComposer(nextOutputType);
59+
if (!outputTC.hasField('nodeId')) {
60+
outputTC.addField('nodeId', {
61+
type: GraphQLID,
62+
description: 'The globally unique ID among all types',
63+
});
64+
}
65+
66+
if (!outputTC.hasField('clientMutationId')) {
67+
outputTC.addField('clientMutationId', {
68+
type: GraphQLString,
69+
description: 'The client mutation ID used by clients like Relay to track the mutation. '
70+
+ 'If given, returned in the response payload of the mutation.',
71+
});
72+
}
73+
74+
return outputTC.getType();
75+
};
76+
}

src/nodeFieldConfig.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* @flow */
2+
/* eslint-disable no-param-reassign */
3+
4+
import {
5+
GraphQLID,
6+
GraphQLNonNull,
7+
} from 'graphql';
8+
import { fromGlobalId } from './globalId';
9+
import NodeInterface from './nodeInterface';
10+
11+
// this fieldConfig must be set to RootQuery.node field
12+
export function getNodeFieldConfig(typeComposerMap) {
13+
return {
14+
description: 'Fetches an object that has globally unique ID among all types',
15+
type: NodeInterface,
16+
args: {
17+
id: {
18+
type: new GraphQLNonNull(GraphQLID),
19+
description: 'The globally unique ID among all types',
20+
},
21+
},
22+
resolve: (source, args, context, info) => {
23+
const { type, id } = fromGlobalId(args.id);
24+
25+
const typeComposer = typeComposerMap[type];
26+
if (!typeComposer) {
27+
return null;
28+
}
29+
30+
const resolver = typeComposer.getResolver('findById');
31+
if (resolver && resolver.resolve) {
32+
// suppose that first argument is argument with id field
33+
const idArgName = Object.keys(resolver.args)[0];
34+
35+
return resolver.resolve({
36+
source,
37+
args: { [idArgName]: id },
38+
context,
39+
info,
40+
});
41+
}
42+
43+
return null;
44+
},
45+
};
46+
}

src/nodeInterface.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* @flow */
2+
3+
import {
4+
GraphQLID,
5+
GraphQLNonNull,
6+
GraphQLInterfaceType,
7+
} from 'graphql';
8+
9+
const NodeInterface = new GraphQLInterfaceType({
10+
name: 'Node',
11+
description: 'An object, that can be fetched by the globally unique ID among all types.',
12+
fields: () => ({
13+
id: {
14+
type: new GraphQLNonNull(GraphQLID),
15+
description: 'The globally unique ID among all types.',
16+
},
17+
}),
18+
resolveType: (payload) => (
19+
// `payload._nodeTypeName` was added to type via composeWithRelay
20+
payload._nodeTypeName ? payload._nodeTypeName : null
21+
),
22+
});
23+
24+
export default NodeInterface;

0 commit comments

Comments
 (0)