125 | );
126 | }
127 | }
128 |
129 | export default createFragmentContainer(Todo, {
130 | todo: graphql`
131 | fragment Todo_todo on Todo {
132 | complete
133 | id
134 | text
135 | }
136 | `,
137 | viewer: graphql`
138 | fragment Todo_viewer on User {
139 | id
140 | totalCount
141 | completedCount
142 | }
143 | `,
144 | });
145 |
--------------------------------------------------------------------------------
/data/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
5 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | "lib": [
7 | "es2015"
8 | ], /* Specify library files to be included in the compilation: */
9 | // "allowJs": true, /* Allow javascript files to be compiled. */
10 | // "checkJs": true, /* Report errors in .js files. */
11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | // "outDir": "./", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "removeComments": true, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 | /* Strict Type-Checking Options */
23 | "strict": true, /* Enable all strict type-checking options. */
24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
25 | // "strictNullChecks": true, /* Enable strict null checks. */
26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
28 | /* Additional Checks */
29 | // "noUnusedLocals": true, /* Report errors on unused locals. */
30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
33 | /* Module Resolution Options */
34 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
35 | "baseUrl": "../types" /* Base directory to resolve non-absolute module names. */
36 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
37 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
38 | // "typeRoots": [], /* List of folders to include type definitions from. */
39 | // "types": [], /* Type declaration files to be included in compilation. */
40 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
41 | /* Source Map Options */
42 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
43 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
44 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
45 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
46 | /* Experimental Options */
47 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
48 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/scripts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
5 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | "lib": [
7 | "es2015"
8 | ], /* Specify library files to be included in the compilation: */
9 | // "allowJs": true, /* Allow javascript files to be compiled. */
10 | // "checkJs": true, /* Report errors in .js files. */
11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | // "outDir": "./", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "removeComments": true, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 | /* Strict Type-Checking Options */
23 | "strict": true, /* Enable all strict type-checking options. */
24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
25 | // "strictNullChecks": true, /* Enable strict null checks. */
26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
28 | /* Additional Checks */
29 | // "noUnusedLocals": true, /* Report errors on unused locals. */
30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
33 | /* Module Resolution Options */
34 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
35 | "baseUrl": "." /* Base directory to resolve non-absolute module names. */
36 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
37 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
38 | // "typeRoots": [], /* List of folders to include type definitions from. */
39 | // "types": [], /* Type declaration files to be included in compilation. */
40 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
41 | /* Source Map Options */
42 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
43 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
44 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
45 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
46 | /* Experimental Options */
47 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
48 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
5 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | "lib": [
7 | "es2015"
8 | ], /* Specify library files to be included in the compilation: */
9 | // "allowJs": true, /* Allow javascript files to be compiled. */
10 | // "checkJs": true, /* Report errors in .js files. */
11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | // "outDir": "./", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "removeComments": true, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 | /* Strict Type-Checking Options */
23 | "strict": true, /* Enable all strict type-checking options. */
24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
25 | // "strictNullChecks": true, /* Enable strict null checks. */
26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
28 | /* Additional Checks */
29 | // "noUnusedLocals": true, /* Report errors on unused locals. */
30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
33 | /* Module Resolution Options */
34 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
35 | "baseUrl": "../types" /* Base directory to resolve non-absolute module names. */
36 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
37 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
38 | // "typeRoots": [], /* List of folders to include type definitions from. */
39 | // "types": [], /* Type declaration files to be included in compilation. */
40 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
41 | /* Source Map Options */
42 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
43 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
44 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
45 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
46 | /* Experimental Options */
47 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
48 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/config/transform.ts:
--------------------------------------------------------------------------------
1 | import { parse as parseGraphQL } from 'graphql';
2 | import * as ts from 'typescript';
3 | import { getFragmentNameParts } from './getFragmentNameParts';
4 |
5 | function tsRequireFile(file: string): ts.Expression {
6 | return ts.createPropertyAccess(
7 | ts.createCall(ts.createIdentifier('require'), undefined, [
8 | ts.createLiteral(file),
9 | ]),
10 | ts.createIdentifier('default'),
11 | );
12 | }
13 |
14 | export function transform(context: ts.TransformationContext): ts.Transformer {
15 | return (sourceFile) => {
16 |
17 | function processNode(node: ts.Node): ts.Node {
18 | if (ts.isTaggedTemplateExpression(node)) {
19 | const tag = node.tag.getText();
20 | if (tag === 'graphql' || tag === 'graphql.experimental') {
21 | if (node.template.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
22 | const text = (node.template as ts.NoSubstitutionTemplateLiteral).text;
23 |
24 | const ast = parseGraphQL(text);
25 | const mainDefinition = ast.definitions[0];
26 | if (mainDefinition.kind === 'FragmentDefinition') {
27 | if (!node.parent) {
28 | throw new Error('Expected node to have a parent');
29 | }
30 | // Only a single fragment allowed here
31 | if (node.parent.kind === ts.SyntaxKind.PropertyAssignment) {
32 | const propertyNameNode = (node.parent as ts.PropertyAssignment).name;
33 | if (ast.definitions.length !== 1) {
34 | throw new Error(
35 | `TypescriptTransformerRelay: Expected exactly one fragment in the ` +
36 | `graphql tag refeenced by the property ${propertyNameNode.getText(sourceFile)}.`,
37 | );
38 | }
39 | return tsRequireFile('generated/' + encodeURIComponent(mainDefinition.name.value) + '.graphql');
40 | }
41 |
42 | const nodeMap: { [name: string]: ts.Expression } = {};
43 | for (const definition of ast.definitions) {
44 | if (definition.kind !== 'FragmentDefinition') {
45 | throw new Error(
46 | `TypescriptTransformerRelay: Expected only fragments within this ` +
47 | `graphql tag.`,
48 | );
49 | }
50 | const [, propertyName] = getFragmentNameParts(definition.name.value);
51 | nodeMap[propertyName] = tsRequireFile(
52 | 'generated/' +
53 | encodeURIComponent(definition.name.value) +
54 | '.graphql',
55 | );
56 | }
57 | return ts.createObjectLiteral(
58 | Object.keys(nodeMap).map(propertyName => {
59 | return ts.createPropertyAssignment(
60 | propertyName,
61 | nodeMap[propertyName],
62 | );
63 | }),
64 | true,
65 | );
66 | }
67 |
68 | if (mainDefinition.kind === 'OperationDefinition') {
69 | if (ast.definitions.length !== 1) {
70 | throw new Error(
71 | 'TypescriptTransformerRelay: Expected exactly one operation ' +
72 | '(query, mutation, or subscription) per graphql tag.',
73 | );
74 | }
75 | if (mainDefinition.name == null) {
76 | throw new Error(
77 | 'TypescriptTransformerRelay: Must name GraphQL Operations',
78 | );
79 | }
80 |
81 | return tsRequireFile('generated/' + encodeURIComponent(mainDefinition.name.value) + '.graphql');
82 | }
83 |
84 | throw new Error(
85 | 'TypescriptTransformerRelay: Expected a fragment, mutation, query, or ' +
86 | 'subscription, got `' +
87 | mainDefinition.kind +
88 | '`.',
89 | );
90 | }
91 | }
92 | } else if (ts.isExpressionWithTypeArguments(node)) {
93 | if (
94 | node.parent == null ||
95 | !ts.isHeritageClause(node.parent) ||
96 | node.parent.token !== ts.SyntaxKind.ExtendsKeyword
97 | ) {
98 | return node;
99 | }
100 |
101 | const expr = node.expression;
102 | if (ts.isPropertyAccessExpression(expr)) {
103 | if (!ts.isIdentifier(expr.expression) || expr.expression.text !== 'Relay') {
104 | return node;
105 | }
106 |
107 | if (/Container$/.test(expr.name.text)) {
108 | return ts.createExpressionWithTypeArguments(
109 | node.typeArguments || [],
110 | ts.createPropertyAccess(ts.createIdentifier('React'), ts.createIdentifier('Component')),
111 | );
112 | }
113 | }
114 | }
115 | return node;
116 | }
117 |
118 | function visitNode(node: ts.Node): ts.Node {
119 | return ts.visitEachChild(processNode(node), childNode => visitNode(childNode), context);
120 | }
121 |
122 | return visitNode(sourceFile) as ts.SourceFile;
123 | };
124 | }
125 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
5 | "module": "es2015", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | "lib": [
7 | "es2015",
8 | "dom"
9 | ], /* Specify library files to be included in the compilation: */
10 | "allowJs": false, /* Allow javascript files to be compiled. */
11 | "checkJs": false, /* Report errors in .js files. */
12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "sourceMap": true, /* Generates corresponding '.map' file. */
15 | // "outFile": "./", /* Concatenate and emit output to single file. */
16 | // "outDir": "./", /* Redirect output structure to the directory. */
17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
18 | // "removeComments": true, /* Do not emit comments to output. */
19 | // "noEmit": true, /* Do not emit outputs. */
20 | "importHelpers": true, /* Import emit helpers from 'tslib'. */
21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
23 | /* Strict Type-Checking Options */
24 | "strict": true, /* Enable all strict type-checking options. */
25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
29 | /* Additional Checks */
30 | // "noUnusedLocals": true, /* Report errors on unused locals. */
31 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
32 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
33 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
34 | /* Module Resolution Options */
35 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
36 | "baseUrl": "./types", /* Base directory to resolve non-absolute module names. */
37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
39 | // "typeRoots": [], /* List of folders to include type definitions from. */
40 | // "types": [
41 | // ] /* Type declaration files to be included in compilation. */
42 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
43 | /* Source Map Options */
44 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
45 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
46 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
47 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
48 | /* Experimental Options */
49 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
50 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
51 | "sourceMap": true,
52 | "plugins": [
53 | {
54 | "name": "ts-graphql-plugin",
55 | "schema": "./data/gqlschema.json",
56 | "tag": "graphql"
57 | },
58 | {
59 | "name": "ts-graphql-plugin",
60 | "schema": "./data/gqlschema.json",
61 | "tag": "graphql.experimental"
62 | }
63 | ],
64 | "paths": {
65 | "generated/*": [
66 | "../generated/*"
67 | ]
68 | }
69 | },
70 | "include": [
71 | "includes/**/*.d.ts",
72 | "src/**/*.ts",
73 | "src/**/*.tsx"
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/types/react-relay/definitions.d.ts:
--------------------------------------------------------------------------------
1 | import * as RelayRuntime from 'relay-runtime/definitions';
2 | import * as React from 'react';
3 | declare namespace ReactRelay {
4 | interface BaseQuery {
5 | query: any;
6 | variables: any;
7 | }
8 |
9 | type ConnectionConfig = {
10 | direction?: 'backward' | 'forward';
11 | getConnectionFromProps?: (props: Props & FragmentTypes) => ConnectionData | null;
12 | getFragmentVariables?: FragmentVariablesGetter;
13 | getVariables: (
14 | props: FragmentTypes & Props,
15 | paginationInfo: { count: number, cursor: string | null },
16 | fragmentVariables: Variables,
17 | ) => PaginationQuery['variables'];
18 | query: RelayRuntime.GraphQLTaggedNode;
19 | };
20 |
21 | type FragmentVariablesGetter = (
22 | prevVars: Variables,
23 | totalCount: number,
24 | ) => Variables;
25 |
26 | type ConnectionData = {
27 | edges?: Array | null;
28 | pageInfo?: PageInfo | null;
29 | };
30 |
31 | type PageInfoForward = {
32 | endCursor: string | null;
33 | hasNextPage: boolean;
34 | hasPreviousPage?: boolean;
35 | startCursor?: string | null;
36 | };
37 | type PageInfoBackward = {
38 | endCursor?: string | null;
39 | hasNextPage?: boolean;
40 | hasPreviousPage: boolean;
41 | startCursor: string | null;
42 | };
43 |
44 | type PageInfo = PageInfoForward | PageInfoBackward;
45 |
46 | // Fragment container
47 | class FragmentContainer extends React.Component, State> {
48 | private ' props': Props;
49 | private ' fragmentTypes': FragmentTypes;
50 | private ' fragmentBrandTypes': FragmentBrandTypes;
51 | }
52 | interface FragmentContainerConstructor {
53 | new(props: FragmentContainerProps): FragmentContainer;
54 | }
55 | type FragmentComponent = React.ComponentType;
56 | interface FragmentContainerRelayProp {
57 | environment: RelayRuntime.Environment;
58 | }
59 |
60 | type FragmentContainerProps = FragmentTypes & Props & { relay: FragmentContainerRelayProp };
61 |
62 | // Refetch container
63 | class RefetchContainer extends React.Component, State> {
64 | private ' props': Props;
65 | private ' fragmentTypes': FragmentTypes;
66 | private ' fragmentBrandTypes': FragmentBrandTypes;
67 | private ' refetchQuery': RefetchQuery;
68 | private ' variables': Variables;
69 | }
70 | interface RefetchContainerConstructor {
71 | new(props: RefetchContainerProps): RefetchContainer;
72 | }
73 | type RefetchComponent = React.ComponentType;
74 |
75 | interface RefetchOptions {
76 | force?: boolean;
77 | }
78 |
79 | interface RefetchContainerRelayProp {
80 | environment: RelayRuntime.Environment;
81 | refetch(
82 | variables: RefetchQuery['variables'] | ((fragmentVariables: Variables) => RefetchQuery['variables']),
83 | renderVariables?: RefetchQuery['variables'] | ((fragmentVariables: Variables) => RefetchQuery['variables']),
84 | callback?: (error: Error | null) => void,
85 | options?: RefetchOptions,
86 | ): RelayRuntime.Disposable;
87 | }
88 |
89 | type RefetchContainerProps = FragmentTypes & Props & { relay: RefetchContainerRelayProp, };
90 |
91 | // Pagination container
92 | class PaginationContainer extends React.Component, State> {
93 | private ' props': Props;
94 | private ' fragmentTypes': FragmentTypes;
95 | private ' fragmentBrandTypes': FragmentBrandTypes;
96 | private ' paginationQuery': PaginationQuery;
97 | private ' variables': Variables;
98 | }
99 | interface PaginationContainerConstructor {
100 | new(props: PaginationContainerProps): PaginationContainer;
101 | }
102 | type PaginationComponent = React.ComponentType;
103 |
104 | interface PaginationOptions {
105 | force?: boolean;
106 | }
107 |
108 | interface PaginationContainerRelayProp {
109 | environment: RelayRuntime.Environment;
110 | loadMore(
111 | pageSize: number,
112 | callback?: (error: Error | null) => void,
113 | options?: PaginationOptions,
114 | ): RelayRuntime.Disposable;
115 | isLoading(): boolean;
116 | hasMore(): boolean;
117 | refetchConnection(
118 | totalCount: number,
119 | callback: (error: Error | null) => void,
120 | refetchVariables?: Variables,
121 | ): RelayRuntime.Disposable;
122 | }
123 |
124 | type PaginationContainerProps = FragmentTypes & Props & { relay: PaginationContainerRelayProp, };
125 | }
126 |
127 | export as namespace ReactRelay;
128 | export = ReactRelay;
129 |
--------------------------------------------------------------------------------
/includes/relay.d.ts:
--------------------------------------------------------------------------------
1 | import * as ReactRelay from 'react-relay/definitions';
2 | import { AddTodoMutation as AddTodoMutationPayload, AddTodoMutationVariables } from 'generated/AddTodoMutation.graphql';
3 | import { ChangeTodoStatusMutation as ChangeTodoStatusMutationPayload, ChangeTodoStatusMutationVariables } from 'generated/ChangeTodoStatusMutation.graphql';
4 | import { MarkAllTodosMutation as MarkAllTodosMutationPayload, MarkAllTodosMutationVariables } from 'generated/MarkAllTodosMutation.graphql';
5 | import { RemoveCompletedTodosMutation as RemoveCompletedTodosMutationPayload, RemoveCompletedTodosMutationVariables } from 'generated/RemoveCompletedTodosMutation.graphql';
6 | import { RemoveTodoMutation as RemoveTodoMutationPayload, RemoveTodoMutationVariables } from 'generated/RemoveTodoMutation.graphql';
7 | import { RenameTodoMutation as RenameTodoMutationPayload, RenameTodoMutationVariables } from 'generated/RenameTodoMutation.graphql';
8 | import { appQuery as appQueryPayload } from 'generated/appQuery.graphql';
9 | import { TodoListFooter_viewer, TodoListFooter_viewer_brand } from 'generated/TodoListFooter_viewer.graphql';
10 | import { TodoList_viewer, TodoList_viewer_brand } from 'generated/TodoList_viewer.graphql';
11 | import { Todo_todo, Todo_todo_brand } from 'generated/Todo_todo.graphql';
12 | import { Todo_viewer, Todo_viewer_brand } from 'generated/Todo_viewer.graphql';
13 | import { TodoApp_viewer, TodoApp_viewer_brand } from 'generated/TodoApp_viewer.graphql';
14 |
15 | declare global {
16 | namespace Relay {
17 | export interface AddTodoMutation {
18 | query: AddTodoMutationPayload;
19 | variables: AddTodoMutationVariables;
20 | }
21 | export interface ChangeTodoStatusMutation {
22 | query: ChangeTodoStatusMutationPayload;
23 | variables: ChangeTodoStatusMutationVariables;
24 | }
25 | export interface MarkAllTodosMutation {
26 | query: MarkAllTodosMutationPayload;
27 | variables: MarkAllTodosMutationVariables;
28 | }
29 | export interface RemoveCompletedTodosMutation {
30 | query: RemoveCompletedTodosMutationPayload;
31 | variables: RemoveCompletedTodosMutationVariables;
32 | }
33 | export interface RemoveTodoMutation {
34 | query: RemoveTodoMutationPayload;
35 | variables: RemoveTodoMutationVariables;
36 | }
37 | export interface RenameTodoMutation {
38 | query: RenameTodoMutationPayload;
39 | variables: RenameTodoMutationVariables;
40 | }
41 | export interface appQuery {
42 | query: appQueryPayload;
43 | variables: {};
44 | }
45 |
46 | export type TodoListFooterFragmentContainerProps = ReactRelay.FragmentContainerProps<{ viewer: TodoListFooter_viewer }
47 | , Props>
48 | export abstract class TodoListFooterFragmentContainer extends ReactRelay.FragmentContainer<{ viewer: TodoListFooter_viewer }, { viewer: TodoListFooter_viewer_brand }, Props, State> { }
49 | export type TodoListFooterRefetchContainerProps = ReactRelay.RefetchContainerProps<{ viewer: TodoListFooter_viewer }
50 | , Props, RefetchQuery>
51 | export abstract class TodoListFooterRefetchContainer extends ReactRelay.RefetchContainer<{ viewer: TodoListFooter_viewer }, { viewer: TodoListFooter_viewer_brand }, Props, State, RefetchQuery> { }
52 | export type TodoListFooterPaginationContainerProps = ReactRelay.PaginationContainerProps<{ viewer: TodoListFooter_viewer }
53 | , Props, PaginationQuery>
54 | export abstract class TodoListFooterPaginationContainer extends ReactRelay.PaginationContainer<{ viewer: TodoListFooter_viewer }, { viewer: TodoListFooter_viewer_brand }, Props, State, PaginationQuery> { }
55 | export type TodoListFragmentContainerProps = ReactRelay.FragmentContainerProps<{ viewer: TodoList_viewer }
56 | , Props>
57 | export abstract class TodoListFragmentContainer extends ReactRelay.FragmentContainer<{ viewer: TodoList_viewer }, { viewer: TodoList_viewer_brand }, Props, State> { }
58 | export type TodoListRefetchContainerProps = ReactRelay.RefetchContainerProps<{ viewer: TodoList_viewer }
59 | , Props, RefetchQuery>
60 | export abstract class TodoListRefetchContainer extends ReactRelay.RefetchContainer<{ viewer: TodoList_viewer }, { viewer: TodoList_viewer_brand }, Props, State, RefetchQuery> { }
61 | export type TodoListPaginationContainerProps = ReactRelay.PaginationContainerProps<{ viewer: TodoList_viewer }
62 | , Props, PaginationQuery>
63 | export abstract class TodoListPaginationContainer extends ReactRelay.PaginationContainer<{ viewer: TodoList_viewer }, { viewer: TodoList_viewer_brand }, Props, State, PaginationQuery> { }
64 | export type TodoFragmentContainerProps = ReactRelay.FragmentContainerProps<{ todo: Todo_todo } & { viewer: Todo_viewer }
65 | , Props>
66 | export abstract class TodoFragmentContainer extends ReactRelay.FragmentContainer<{ todo: Todo_todo } & { viewer: Todo_viewer }, { todo: Todo_todo_brand } & { viewer: Todo_viewer_brand }, Props, State> { }
67 | export type TodoRefetchContainerProps = ReactRelay.RefetchContainerProps<{ todo: Todo_todo } & { viewer: Todo_viewer }
68 | , Props, RefetchQuery>
69 | export abstract class TodoRefetchContainer extends ReactRelay.RefetchContainer<{ todo: Todo_todo } & { viewer: Todo_viewer }, { todo: Todo_todo_brand } & { viewer: Todo_viewer_brand }, Props, State, RefetchQuery> { }
70 | export type TodoPaginationContainerProps = ReactRelay.PaginationContainerProps<{ todo: Todo_todo } & { viewer: Todo_viewer }
71 | , Props, PaginationQuery>
72 | export abstract class TodoPaginationContainer extends ReactRelay.PaginationContainer<{ todo: Todo_todo } & { viewer: Todo_viewer }, { todo: Todo_todo_brand } & { viewer: Todo_viewer_brand }, Props, State, PaginationQuery> { }
73 | export type TodoAppFragmentContainerProps = ReactRelay.FragmentContainerProps<{ viewer: TodoApp_viewer }
74 | , Props>
75 | export abstract class TodoAppFragmentContainer extends ReactRelay.FragmentContainer<{ viewer: TodoApp_viewer }, { viewer: TodoApp_viewer_brand }, Props, State> { }
76 | export type TodoAppRefetchContainerProps = ReactRelay.RefetchContainerProps<{ viewer: TodoApp_viewer }
77 | , Props, RefetchQuery>
78 | export abstract class TodoAppRefetchContainer extends ReactRelay.RefetchContainer<{ viewer: TodoApp_viewer }, { viewer: TodoApp_viewer_brand }, Props, State, RefetchQuery> { }
79 | export type TodoAppPaginationContainerProps = ReactRelay.PaginationContainerProps<{ viewer: TodoApp_viewer }
80 | , Props, PaginationQuery>
81 | export abstract class TodoAppPaginationContainer extends ReactRelay.PaginationContainer<{ viewer: TodoApp_viewer }, { viewer: TodoApp_viewer_brand }, Props, State, PaginationQuery> { }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/data/schema.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import {
14 | GraphQLBoolean,
15 | GraphQLFieldConfigArgumentMap,
16 | GraphQLFieldConfigMap,
17 | GraphQLInt,
18 | GraphQLID,
19 | GraphQLList,
20 | GraphQLNonNull,
21 | GraphQLObjectType,
22 | GraphQLSchema,
23 | GraphQLString,
24 | } from 'graphql';
25 |
26 | import {
27 | connectionArgs,
28 | connectionDefinitions,
29 | connectionFromArray,
30 | cursorForObjectInConnection,
31 | fromGlobalId,
32 | globalIdField,
33 | mutationWithClientMutationId,
34 | nodeDefinitions,
35 | toGlobalId,
36 | GraphQLNodeDefinitions,
37 | } from 'graphql-relay';
38 |
39 | import {
40 | addTodo,
41 | changeTodoStatus,
42 | getTodo,
43 | getTodos,
44 | getUser,
45 | getViewer,
46 | markAllTodos,
47 | removeCompletedTodos,
48 | removeTodo,
49 | renameTodo,
50 | Todo,
51 | User,
52 | } from './database';
53 |
54 | const def: GraphQLNodeDefinitions = nodeDefinitions(
55 | (globalId) => {
56 | const { type, id } = fromGlobalId(globalId);
57 | if (type === 'Todo') {
58 | return getTodo(id);
59 | } else if (type === 'User') {
60 | return getUser(id);
61 | }
62 | return null;
63 | },
64 | (obj: any) => {
65 | if (obj instanceof Todo) {
66 | return GraphQLTodo;
67 | } else if (obj instanceof User) {
68 | return GraphQLUser;
69 | }
70 | return null as any;
71 | },
72 | );
73 |
74 | const { nodeInterface, nodeField } = def;
75 |
76 | const GraphQLTodo = new GraphQLObjectType({
77 | fields: {
78 | complete: {
79 | resolve: (obj) => obj.complete,
80 | type: GraphQLBoolean,
81 | },
82 | id: globalIdField('Todo'),
83 | text: {
84 | resolve: (obj) => obj.text,
85 | type: GraphQLString,
86 | },
87 | },
88 | interfaces: [nodeInterface],
89 | name: 'Todo',
90 | });
91 |
92 | const {
93 | connectionType: TodosConnection,
94 | edgeType: GraphQLTodoEdge,
95 | } = connectionDefinitions({
96 | name: 'Todo',
97 | nodeType: GraphQLTodo,
98 | });
99 |
100 | const GraphQLUser = new GraphQLObjectType({
101 | fields: {
102 | completedCount: {
103 | resolve: () => getTodos('completed').length,
104 | type: new GraphQLNonNull(GraphQLInt),
105 | },
106 | id: globalIdField('User'),
107 | todos: {
108 | args: {
109 | status: {
110 | defaultValue: 'any',
111 | type: GraphQLString,
112 | },
113 | ...(connectionArgs as GraphQLFieldConfigArgumentMap),
114 | } as GraphQLFieldConfigArgumentMap,
115 | resolve: (obj, { status, ...args }) =>
116 | connectionFromArray(getTodos(status), args),
117 | type: new GraphQLNonNull(TodosConnection),
118 | },
119 | totalCount: {
120 | resolve: () => getTodos().length,
121 | type: new GraphQLNonNull(GraphQLInt),
122 | },
123 | } as GraphQLFieldConfigMap,
124 | interfaces: [nodeInterface],
125 | name: 'User',
126 | });
127 |
128 | const Query = new GraphQLObjectType({
129 | fields: {
130 | node: nodeField,
131 | viewer: {
132 | resolve: () => getViewer(),
133 | type: GraphQLUser,
134 | },
135 | },
136 | name: 'Query',
137 | });
138 |
139 | const GraphQLAddTodoMutation = mutationWithClientMutationId({
140 | inputFields: {
141 | text: { type: new GraphQLNonNull(GraphQLString) },
142 | },
143 | mutateAndGetPayload: ({ text }) => {
144 | const localTodoId = addTodo(text, false);
145 | return { localTodoId };
146 | },
147 | name: 'AddTodo',
148 | outputFields: {
149 | todoEdge: {
150 | resolve: ({ localTodoId }) => {
151 | const todo = getTodo(localTodoId);
152 | return {
153 | cursor: cursorForObjectInConnection(getTodos(), todo),
154 | node: todo,
155 | };
156 | },
157 | type: GraphQLTodoEdge,
158 | },
159 | viewer: {
160 | resolve: () => getViewer(),
161 | type: GraphQLUser,
162 | },
163 | },
164 | });
165 |
166 | const GraphQLChangeTodoStatusMutation = mutationWithClientMutationId({
167 | inputFields: {
168 | complete: { type: new GraphQLNonNull(GraphQLBoolean) },
169 | id: { type: new GraphQLNonNull(GraphQLID) },
170 | },
171 | mutateAndGetPayload: ({ id, complete }) => {
172 | const localTodoId = fromGlobalId(id).id;
173 | changeTodoStatus(localTodoId, complete);
174 | return { localTodoId };
175 | },
176 | name: 'ChangeTodoStatus',
177 | outputFields: {
178 | todo: {
179 | resolve: ({ localTodoId }) => getTodo(localTodoId),
180 | type: GraphQLTodo,
181 | },
182 | viewer: {
183 | resolve: () => getViewer(),
184 | type: GraphQLUser,
185 | },
186 | },
187 | });
188 |
189 | const GraphQLMarkAllTodosMutation = mutationWithClientMutationId({
190 | inputFields: {
191 | complete: { type: new GraphQLNonNull(GraphQLBoolean) },
192 | },
193 | mutateAndGetPayload: ({ complete }) => {
194 | const changedTodoLocalIds = markAllTodos(complete);
195 | return { changedTodoLocalIds };
196 | },
197 | name: 'MarkAllTodos',
198 | outputFields: {
199 | changedTodos: {
200 | resolve: ({ changedTodoLocalIds }) => changedTodoLocalIds.map(getTodo),
201 | type: new GraphQLList(GraphQLTodo),
202 | },
203 | viewer: {
204 | resolve: () => getViewer(),
205 | type: GraphQLUser,
206 | },
207 | },
208 | });
209 |
210 | // TODO: Support plural deletes
211 | const GraphQLRemoveCompletedTodosMutation = mutationWithClientMutationId({
212 | inputFields: {},
213 | mutateAndGetPayload: () => {
214 | const deletedTodoLocalIds = removeCompletedTodos();
215 | const deletedTodoIds = deletedTodoLocalIds.map(toGlobalId.bind(null, 'Todo'));
216 | return { deletedTodoIds };
217 | },
218 | name: 'RemoveCompletedTodos',
219 | outputFields: {
220 | deletedTodoIds: {
221 | resolve: ({ deletedTodoIds }) => deletedTodoIds,
222 | type: new GraphQLList(GraphQLString),
223 | },
224 | viewer: {
225 | resolve: () => getViewer(),
226 | type: GraphQLUser,
227 | },
228 | },
229 | });
230 |
231 | const GraphQLRemoveTodoMutation = mutationWithClientMutationId({
232 | inputFields: {
233 | id: { type: new GraphQLNonNull(GraphQLID) },
234 | },
235 | mutateAndGetPayload: ({ id }) => {
236 | const localTodoId = fromGlobalId(id).id;
237 | removeTodo(localTodoId);
238 | return { id };
239 | },
240 | name: 'RemoveTodo',
241 | outputFields: {
242 | deletedTodoId: {
243 | resolve: ({ id }) => id,
244 | type: GraphQLID,
245 | },
246 | viewer: {
247 | resolve: () => getViewer(),
248 | type: GraphQLUser,
249 | },
250 | },
251 | });
252 |
253 | const GraphQLRenameTodoMutation = mutationWithClientMutationId({
254 | inputFields: {
255 | id: { type: new GraphQLNonNull(GraphQLID) },
256 | text: { type: new GraphQLNonNull(GraphQLString) },
257 | },
258 | mutateAndGetPayload: ({ id, text }) => {
259 | const localTodoId = fromGlobalId(id).id;
260 | renameTodo(localTodoId, text);
261 | return { localTodoId };
262 | },
263 | name: 'RenameTodo',
264 | outputFields: {
265 | todo: {
266 | resolve: ({ localTodoId }) => getTodo(localTodoId),
267 | type: GraphQLTodo,
268 | },
269 | },
270 | });
271 |
272 | const Mutation = new GraphQLObjectType({
273 | fields: {
274 | addTodo: GraphQLAddTodoMutation,
275 | changeTodoStatus: GraphQLChangeTodoStatusMutation,
276 | markAllTodos: GraphQLMarkAllTodosMutation,
277 | removeCompletedTodos: GraphQLRemoveCompletedTodosMutation,
278 | removeTodo: GraphQLRemoveTodoMutation,
279 | renameTodo: GraphQLRenameTodoMutation,
280 | },
281 | name: 'Mutation',
282 | });
283 |
284 | export const schema = new GraphQLSchema({
285 | mutation: Mutation,
286 | query: Query,
287 | });
288 |
--------------------------------------------------------------------------------
/public/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 | min-width: 230px;
29 | max-width: 550px;
30 | margin: 0 auto;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | font-weight: 300;
34 | }
35 |
36 | :focus {
37 | outline: 0;
38 | }
39 |
40 | .hidden {
41 | display: none;
42 | }
43 |
44 | .todoapp {
45 | background: #fff;
46 | margin: 130px 0 40px 0;
47 | position: relative;
48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
50 | }
51 |
52 | .todoapp input::-webkit-input-placeholder {
53 | font-style: italic;
54 | font-weight: 300;
55 | color: #e6e6e6;
56 | }
57 |
58 | .todoapp input::-moz-placeholder {
59 | font-style: italic;
60 | font-weight: 300;
61 | color: #e6e6e6;
62 | }
63 |
64 | .todoapp input::input-placeholder {
65 | font-style: italic;
66 | font-weight: 300;
67 | color: #e6e6e6;
68 | }
69 |
70 | .todoapp h1 {
71 | position: absolute;
72 | top: -155px;
73 | width: 100%;
74 | font-size: 100px;
75 | font-weight: 100;
76 | text-align: center;
77 | color: rgba(175, 47, 47, 0.15);
78 | -webkit-text-rendering: optimizeLegibility;
79 | -moz-text-rendering: optimizeLegibility;
80 | text-rendering: optimizeLegibility;
81 | }
82 |
83 | .new-todo,
84 | .edit {
85 | position: relative;
86 | margin: 0;
87 | width: 100%;
88 | font-size: 24px;
89 | font-family: inherit;
90 | font-weight: inherit;
91 | line-height: 1.4em;
92 | border: 0;
93 | color: inherit;
94 | padding: 6px;
95 | border: 1px solid #999;
96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
97 | box-sizing: border-box;
98 | -webkit-font-smoothing: antialiased;
99 | -moz-osx-font-smoothing: grayscale;
100 | }
101 |
102 | .new-todo {
103 | padding: 16px 16px 16px 60px;
104 | border: none;
105 | background: rgba(0, 0, 0, 0.003);
106 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
107 | }
108 |
109 | .main {
110 | position: relative;
111 | z-index: 2;
112 | border-top: 1px solid #e6e6e6;
113 | }
114 |
115 | label[for='toggle-all'] {
116 | display: none;
117 | }
118 |
119 | .toggle-all {
120 | position: absolute;
121 | top: -55px;
122 | left: -12px;
123 | width: 60px;
124 | height: 34px;
125 | text-align: center;
126 | border: none; /* Mobile Safari */
127 | }
128 |
129 | .toggle-all:before {
130 | content: '❯';
131 | font-size: 22px;
132 | color: #e6e6e6;
133 | padding: 10px 27px 10px 27px;
134 | }
135 |
136 | .toggle-all:checked:before {
137 | color: #737373;
138 | }
139 |
140 | .todo-list {
141 | margin: 0;
142 | padding: 0;
143 | list-style: none;
144 | }
145 |
146 | .todo-list li {
147 | position: relative;
148 | font-size: 24px;
149 | border-bottom: 1px solid #ededed;
150 | }
151 |
152 | .todo-list li:last-child {
153 | border-bottom: none;
154 | }
155 |
156 | .todo-list li.editing {
157 | border-bottom: none;
158 | padding: 0;
159 | }
160 |
161 | .todo-list li.editing .edit {
162 | display: block;
163 | width: 506px;
164 | padding: 12px 16px;
165 | margin: 0 0 0 43px;
166 | }
167 |
168 | .todo-list li.editing .view {
169 | display: none;
170 | }
171 |
172 | .todo-list li .toggle {
173 | text-align: center;
174 | width: 40px;
175 | /* auto, since non-WebKit browsers doesn't support input styling */
176 | height: auto;
177 | position: absolute;
178 | top: 0;
179 | bottom: 0;
180 | margin: auto 0;
181 | border: none; /* Mobile Safari */
182 | -webkit-appearance: none;
183 | appearance: none;
184 | }
185 |
186 | .todo-list li .toggle:after {
187 | content: url('data:image/svg+xml;utf8,');
188 | }
189 |
190 | .todo-list li .toggle:checked:after {
191 | content: url('data:image/svg+xml;utf8,');
192 | }
193 |
194 | .todo-list li label {
195 | word-break: break-all;
196 | padding: 15px 60px 15px 15px;
197 | margin-left: 45px;
198 | display: block;
199 | line-height: 1.2;
200 | transition: color 0.4s;
201 | }
202 |
203 | .todo-list li.completed label {
204 | color: #d9d9d9;
205 | text-decoration: line-through;
206 | }
207 |
208 | .todo-list li .destroy {
209 | display: none;
210 | position: absolute;
211 | top: 0;
212 | right: 10px;
213 | bottom: 0;
214 | width: 40px;
215 | height: 40px;
216 | margin: auto 0;
217 | font-size: 30px;
218 | color: #cc9a9a;
219 | margin-bottom: 11px;
220 | transition: color 0.2s ease-out;
221 | }
222 |
223 | .todo-list li .destroy:hover {
224 | color: #af5b5e;
225 | }
226 |
227 | .todo-list li .destroy:after {
228 | content: '×';
229 | }
230 |
231 | .todo-list li:hover .destroy {
232 | display: block;
233 | }
234 |
235 | .todo-list li .edit {
236 | display: none;
237 | }
238 |
239 | .todo-list li.editing:last-child {
240 | margin-bottom: -1px;
241 | }
242 |
243 | .footer {
244 | color: #777;
245 | padding: 10px 15px;
246 | height: 20px;
247 | text-align: center;
248 | border-top: 1px solid #e6e6e6;
249 | }
250 |
251 | .footer:before {
252 | content: '';
253 | position: absolute;
254 | right: 0;
255 | bottom: 0;
256 | left: 0;
257 | height: 50px;
258 | overflow: hidden;
259 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
260 | 0 8px 0 -3px #f6f6f6,
261 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
262 | 0 16px 0 -6px #f6f6f6,
263 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
264 | }
265 |
266 | .todo-count {
267 | float: left;
268 | text-align: left;
269 | }
270 |
271 | .todo-count strong {
272 | font-weight: 300;
273 | }
274 |
275 | .filters {
276 | margin: 0;
277 | padding: 0;
278 | list-style: none;
279 | position: absolute;
280 | right: 0;
281 | left: 0;
282 | }
283 |
284 | .filters li {
285 | display: inline;
286 | }
287 |
288 | .filters li a {
289 | color: inherit;
290 | margin: 3px;
291 | padding: 3px 7px;
292 | text-decoration: none;
293 | border: 1px solid transparent;
294 | border-radius: 3px;
295 | }
296 |
297 | .filters li a:hover {
298 | border-color: rgba(175, 47, 47, 0.1);
299 | }
300 |
301 | .filters li a.selected {
302 | border-color: rgba(175, 47, 47, 0.2);
303 | }
304 |
305 | .clear-completed,
306 | html .clear-completed:active {
307 | float: right;
308 | position: relative;
309 | line-height: 20px;
310 | text-decoration: none;
311 | cursor: pointer;
312 | }
313 |
314 | .clear-completed:hover {
315 | text-decoration: underline;
316 | }
317 |
318 | .info {
319 | margin: 65px auto 0;
320 | color: #bfbfbf;
321 | font-size: 10px;
322 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
323 | text-align: center;
324 | }
325 |
326 | .info p {
327 | line-height: 1;
328 | }
329 |
330 | .info a {
331 | color: inherit;
332 | text-decoration: none;
333 | font-weight: 400;
334 | }
335 |
336 | .info a:hover {
337 | text-decoration: underline;
338 | }
339 |
340 | /*
341 | Hack to remove background from Mobile Safari.
342 | Can't use it globally since it destroys checkboxes in Firefox
343 | */
344 | @media screen and (-webkit-min-device-pixel-ratio:0) {
345 | .toggle-all,
346 | .todo-list li .toggle {
347 | background: none;
348 | }
349 |
350 | .todo-list li .toggle {
351 | height: 40px;
352 | }
353 |
354 | .toggle-all {
355 | -webkit-transform: rotate(90deg);
356 | transform: rotate(90deg);
357 | -webkit-appearance: none;
358 | appearance: none;
359 | }
360 | }
361 |
362 | @media (max-width: 430px) {
363 | .footer {
364 | height: 50px;
365 | }
366 |
367 | .filters {
368 | bottom: 10px;
369 | }
370 | }
371 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Relay Modern TodoMVC with TypeScript #
2 |
3 | **DO NOT USE**
4 | This was an early prototype showing one possible way of making TypeScript with Relay modern working. There's a PR incoming into Relay modern allowing a language plugin to make this possible instead. See [facebook/relay#2293](https://github.com/facebook/relay/pull/2293) and [relay-tools/relay-compiler-language-typescript](https://github.com/relay-tools/relay-compiler-language-typescript).
5 |
6 | This is an example application showing one of many ways to integrate TypeScript with Relay Modern.
7 |
8 | The application code is copy/pasted from [Relay examples](https://github.com/relayjs/relay-examples) - all copyright on the application code goes to the appropriate copyright holders.
9 |
10 | This repository also serves (together with [the fork of the relay code base at (secoya/relay)](https://github.com/secoya/relay) - that contains modifications to the compiler) as an example of which possible extensions could be needed in the Relay compiler.
11 |
12 | ## Credits ##
13 |
14 | Monster credits to [s-panferov](https://github.com/s-panferov) - for work on the pull request to add transforms to the relay compiler and his initial implementation of this. We have forked the implementation only to publish a package to be able to play with this.
15 |
16 | ## How to use/test ##
17 |
18 | Clone the repository then run:
19 |
20 | ```bash
21 | npm install
22 | npm run update-schema
23 | npm run generate-vendor-bundle
24 | npm run build
25 | npm start
26 | ```
27 |
28 | In another terminal window you can then run:
29 |
30 | ```bash
31 | npm run watch
32 | ```
33 |
34 | To run the relay-compiler.
35 |
36 | ## Details ##
37 |
38 | In order to generate TypeScript types for the GraphQL queries, this repository uses a package already in use at Secoya, written by one of our employees [graphql-fragment-type-generator](https://git.input-output.dk/strong-graphql/graphql-fragment-type-generator). A proper TypeScript integration would probably work on the RelayIR to do this. Without having looked too much into this, it would probably be fairly straight forward to do as well. One difference in this regard is that the official flow types in Relay does not type up the difference between the props accessible to a component and the props other components rendering a component must provide.
39 |
40 | Consider the following:
41 |
42 | Todo.js
43 | ```jsx
44 | import * as React from 'react';
45 | import { createFragmentContainer, graphql } from 'react-relay';
46 | class Todo extends React.Component {
47 | render() {
48 | return
{this.props.todo.text}
;
49 | }
50 | }
51 |
52 | export default createFragmentContainer(
53 | Todo,
54 | graphql`fragment Todo_todo on Todo { text }`,
55 | );
56 | ```
57 |
58 | TodoContainer.js
59 | ```jsx
60 | import * as React from 'react';
61 | import { createFragmentContainer, graphql } from 'react-relay';
62 | import Todo from './Todo';
63 | class TodoContainer extends React.Component {
64 | render() {
65 | return
Todo:
;
66 | }
67 | }
68 |
69 | export default createFragmentContainer(
70 | TodoContainer,
71 | graphql`fragment TodoContainer_todo on Todo { ...Todo_todo }`,
72 | );
73 | ```
74 |
75 | In this example it is worth noting a couple of things. The runtime prop types for the two components look like this (given a schema where `Todo` has a field called `text` of type `String!`:
76 |
77 | ```typescript
78 | type TodoProps = {
79 | todo: {
80 | text: string;
81 | };
82 | }
83 |
84 | type TodoContainerProps = {
85 | todo: {}
86 | }
87 | ```
88 |
89 | However, inside `TodoContainer` we pass the `todo` prop to the `Todo` component - and this should work, both at runtime *and* compile time. This example should work in this repository - as well as more complex ones. We do this by "branding" every object type being generated - in order to be able to distinguish between `Todo`, `User` and other types. Through some usages of generics we let these types flow through the system - to ensure that only `Todo` objects are passed to the `Todo` component - but allowing them no matter what properties are available to them.
90 |
91 | ## Overview of the system ##
92 |
93 | There are several moving parts in this specific setup.
94 |
95 | 1. Typescript type generation for every fragment, query, mutation and subscription.
96 | 2. Typescript transform to replace `babel-plugin-relay`.
97 | 3. Typescript type definitions for `react-relay` and `relay-runtime` packages, including types of pseudo classes `FragmentContainer`, `RefetchContainer` and `PaginationContainer` (these are defined in `types/react-relay/definitions.d.ts` and does not actually exist at runtime).
98 | 4. Typescript code generation in a globally accessible namespace (named `Relay`) with pseudo component classes for every container found in the code base (ie. `Relay.TodoFragmentContainer`).
99 | 5. Typescript transform to change every class declaration that extends a pseudo container into extending `React.Component`.
100 | 6. A schema definition file (`graphql-schema`) containing type brands (empty enums) and other schema helper types.
101 |
102 | ### 1. Typescript type generation for every fragment, query, mutation and subscription. ###
103 |
104 | This work very much like the original Relay compiler. Ie. here's the generated file output of this fragment:
105 |
106 | ```graphql
107 | fragment Todo_todo on Todo {
108 | complete
109 | id
110 | text
111 | }
112 | ```
113 |
114 | Todo_todo.graphql.ts
115 | ```typescript
116 | /**
117 | * @flow
118 | */
119 | // tslint:disable
120 | import { Todo } from 'graphql-schema';
121 |
122 | export type Todo_todo = {
123 | '': Todo;
124 |
125 | complete: boolean | null;
126 |
127 | /**
128 | * The ID of an object
129 | */
130 | id: string;
131 |
132 | text: string | null;
133 | };
134 |
135 | export type Todo_todo_brand = {
136 | '': Todo;
137 | };
138 |
139 |
140 |
141 | /* eslint-disable */
142 |
143 | 'use strict';
144 |
145 | /*::
146 | import type {ConcreteFragment} from 'relay-runtime';
147 | export type Todo_todo = {|
148 | +complete: ?boolean;
149 | +id: string;
150 | +text: ?string;
151 | |};
152 | */
153 |
154 |
155 | const fragment /*: ConcreteFragment*/ = {
156 | "argumentDefinitions": [],
157 | "kind": "Fragment",
158 | "metadata": null,
159 | "name": "Todo_todo",
160 | "selections": [
161 | {
162 | "kind": "ScalarField",
163 | "alias": null,
164 | "args": null,
165 | "name": "complete",
166 | "storageKey": null
167 | },
168 | {
169 | "kind": "ScalarField",
170 | "alias": null,
171 | "args": null,
172 | "name": "id",
173 | "storageKey": null
174 | },
175 | {
176 | "kind": "ScalarField",
177 | "alias": null,
178 | "args": null,
179 | "name": "text",
180 | "storageKey": null
181 | }
182 | ],
183 | "type": "Todo"
184 | };
185 | export default fragment;
186 | ```
187 |
188 | We here see both the flow types (that the relay compiler generates) and the new TypeScript types generated by `graphql-fragment-type-generator`. We also see the branding of the types happening. Lastly the actual needed runtime data is generated.
189 |
190 | These types are not really meant for you consume - although you could - some much nicer types are generated for that purpose, read on.
191 |
192 | ### 2. Typescript transform to replace `babel-plugin-relay` ###
193 |
194 | Just like the `babel-plugin-relay` transforms `graphql` template literals to calls to `require` - this transform does exactly the same.
195 |
196 | ### 3. Typescript type definitions for `react-relay` and `relay-runtime` packages ###
197 |
198 | These are mainly the flow types (extracted from the package source code) - with some added generic types to make the final step here easier. Of real interest here is that there's classes defined that only exists at compile time - which is used later on to make the types of our containes flow through the system.
199 |
200 | ### 4. Typescript code generation in a globally accessible namespace (named `Relay`) ###
201 |
202 | This is where the real beauty begins. For every container (fragment, refetch or pagination) in your codebase you will have several types available on the global accessible `Relay` namespace.
203 |
204 | For a simple `Todo` component defining two fragments `Todo_todo` and `Todo_viewer` the following types are generated:
205 |
206 | ```typescript
207 | import { Todo_todo, Todo_todo_brand } from 'generated/Todo_todo.graphql';
208 | import { Todo_viewer, Todo_viewer_brand } from 'generated/Todo_viewer.graphql';
209 |
210 | export type TodoFragmentContainerProps = ReactRelay.FragmentContainerProps<{ todo: Todo_todo } & { viewer: Todo_viewer }
211 | , Props>
212 | export abstract class TodoFragmentContainer extends ReactRelay.FragmentContainer<{ todo: Todo_todo } & { viewer: Todo_viewer }, { todo: Todo_todo_brand } & { viewer: Todo_viewer_brand }, Props, State> { }
213 | export type TodoRefetchContainerProps = ReactRelay.RefetchContainerProps<{ todo: Todo_todo } & { viewer: Todo_viewer }
214 | , Props, RefetchQuery>
215 | export abstract class TodoRefetchContainer extends ReactRelay.RefetchContainer<{ todo: Todo_todo } & { viewer: Todo_viewer }, { todo: Todo_todo_brand } & { viewer: Todo_viewer_brand }, Props, State, RefetchQuery> { }
216 | export type TodoPaginationContainerProps = ReactRelay.PaginationContainerProps<{ todo: Todo_todo } & { viewer: Todo_viewer }
217 | , Props, PaginationQuery>
218 | export abstract class TodoPaginationContainer extends ReactRelay.PaginationContainer<{ todo: Todo_todo } & { viewer: Todo_viewer }, { todo: Todo_todo_brand } & { viewer: Todo_viewer_brand }, Props, State, PaginationQuery> { }
219 | export type TodoAppFragmentContainerProps = ReactRelay.FragmentContainerProps<{ viewer: TodoApp_viewer }
220 | , Props>
221 | ```
222 |
223 | This looks very scary when written out like that, here the same types are, but with only the API we care about written out (given that this is a simple fragment container to be used with `createFragmentContainer`):
224 |
225 | * `TodoFragmentContainerProps`:
226 | * This type is useful if you need to type function parameters to have the same type as `this.props` inside your component.
227 | * `TodoFragmentContainer`:
228 | * To create your TodoContainer extend from this class. You can provide types for your props as well as state as usual. However you should not define props for `todo`, `viewer` or `relay`, these will have the correct types (and be updated if your fragments update!).
229 |
230 | These are the APIs for a simple fragment container, so if we wanted to define our `Todo` component to take one additional property `highlight: boolean` we could do it like this:
231 |
232 | ```typescript
233 | import * as React from 'react';
234 | import ViewerInfo from './ViewerInfo';
235 | interface Props {
236 | highlight: boolean;
237 | }
238 | class Todo extends Relay.TodoFragmentContainer {
239 | public render() {
240 | return
241 |
242 | {this.props.todo.text}
243 |
;
244 | }
245 | }
246 |
247 | export default createFragmentContainer(
248 | Todo,
249 | {
250 | todo: graphql`fragment Todo_todo on Todo { text }`,
251 | viewer: graphql`fragment Todo_viewer on User { ... ViewerInfo_viewer }`,
252 | },
253 | );
254 | ```
255 |
256 | This of course assumes that `ViewerInfo` exists. For refetch and pagination containers similiar types are generated (named as such). Only difference is that as a first parameter they take a `Query` generic type. The proper object for this query is the one named the same as the RefetchQuery or PaginationQuery specified in `createPaginationContainer` or `createRefetchContainer`.
257 |
258 | ### 5. Typescript transform to change every class declaration that extends a pseudo container into extending `React.Component` ###
259 |
260 | This one is quite simple. Before converting the TypeScript code to JavaScript code - for every class that extends `Relay.*Container` replace this with `React.Component` as the pseudo container classes do actually not have a run time representation. You do not need to worry about this - except that you need to know that you can't use the pseudo classes for anything but extending other classes from them.
261 |
262 | ### 6. A schema definition file (`graphql-schema`) containing type brands (empty enums) and other schema helper types ###
263 |
264 | Generated at `types/graphql-schema.d.ts` is a simple file containing empty enums for every object type in our schema. It also has types generated to match the input objects defined in our schema to be able to type up variables needed for fragments and operations.
265 |
266 | ## Challenges in the implementation ##
267 |
268 | There has been a few challenges in the implementation:
269 |
270 | ### Transform module for `relay-compiler` ###
271 |
272 | The relay-compiler assumes that it can read the input files using a standard JavaScript parser. TypeScript cannot be parsed like this and as such we need a simple transformation module. See [Pull request #1710 in facebook/relay](https://github.com/facebook/relay/pull/1710). I have applied the patch in that pull request to the commit released as relay-compiler@1.1.0 and used the linked `relay-compiler-typescript` source code provided by [s-panferov](https://github.com/s-panferov). Thank you!
273 |
274 | ### Custom file extension ###
275 |
276 | This one was pretty simple - teach `relay-compiler` to output files with a custom file extension.
277 |
278 | ### extra content generation module ###
279 |
280 | The Relay compiler already has an option in its API (not in the CLI options) to supply a function to call to generate extra files. This is a fine approach if one wants to traverse the RelayIR and generate files from that - and possibly could be used for what we're doing.
281 |
282 | However as we have code operating on the GraphQL AST and not RelayIR - we have opted to add a simple extra hook that can return extra content to be injected into the generated files. We also abuse this hook to generate the `includes/relay.d.ts` file along with `types/graphql-schema.d.ts`. this probably needs a better work around in the long run.
283 |
284 | ### Ignore directives ###
285 |
286 | `graphql-fragment-type-generator` has a useful feature that allows it to extract field selection types with a given name, using a directive (`@exportType`). There has been made simple modifications to the relay compiler to ignore these. Ideally we'd like a commandline switch to give a list of directive names to ignore.
287 |
288 | ### `outputDir` commandline switch ###
289 |
290 | Not much to say here. The relay compiler code base can change it's output directory. Having everything in a single directory makes many things simpler in this example. We added a simple command line switch to be able to supply this option.
291 |
--------------------------------------------------------------------------------
/types/relay-runtime/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as RelayRuntime from './definitions';
2 | export class Environment implements RelayRuntime.Environment {
3 | public constructor(config: RelayRuntime.EnvironmentConfig);
4 |
5 | getStore(): RelayRuntime.Store;
6 |
7 | applyUpdate(updater: RelayRuntime.StoreUpdater): RelayRuntime.Disposable;
8 |
9 | check(readSelector: RelayRuntime.Selector): boolean;
10 |
11 | commitPayload(
12 | operationSelector: RelayRuntime.OperationSelector,
13 | payload: RelayRuntime.PayloadData,
14 | ): void;
15 |
16 | commitUpdate(updater: RelayRuntime.StoreUpdater): void;
17 |
18 | lookup(readSelector: RelayRuntime.Selector): RelayRuntime.Snapshot;
19 |
20 | subscribe(
21 | snapshot: RelayRuntime.Snapshot,
22 | callback: (snapshot: RelayRuntime.Snapshot) => void,
23 | ): RelayRuntime.Disposable;
24 |
25 | retain(selector: RelayRuntime.Selector): RelayRuntime.Disposable;
26 |
27 | sendQuery(queryConfig: {
28 | cacheConfig?: RelayRuntime.CacheConfig | null;
29 | onCompleted?: (() => void) | null,
30 | onError?: ((error: Error) => void) | null;
31 | onNext?: ((payload: RelayRuntime.RelayResponsePayload) => void) | null,
32 | operation: RelayRuntime.OperationSelector,
33 | }): RelayRuntime.Disposable;
34 |
35 | streamQuery(queryConfig: {
36 | cacheConfig?: RelayRuntime.CacheConfig | null;
37 | onCompleted?: (() => void) | null;
38 | onError?: ((error: Error) => void) | null;
39 | onNext?: ((payload: RelayRuntime.RelayResponsePayload) => void) | null;
40 | operation: RelayRuntime.OperationSelector;
41 | }): RelayRuntime.Disposable;
42 |
43 | sendMutation(mutationConfig: {
44 | onCompleted?: ((errors: Array | null) => void) | null;
45 | onError?: ((error: Error) => void) | null;
46 | operation: RelayRuntime.OperationSelector;
47 | optimisticUpdater?: RelayRuntime.SelectorStoreUpdater | null;
48 | optimisticResponse?: Object;
49 | updater?: RelayRuntime.SelectorStoreUpdater | null;
50 | uploadables?: RelayRuntime.UploadableMap;
51 | }): RelayRuntime.Disposable;
52 |
53 | sendSubscription(subscriptionConfig: {
54 | onCompleted?: ((errors: Array | null) => void) | null;
55 | onNext?: ((payload: RelayRuntime.RelayResponsePayload) => void) | null;
56 | onError?: ((error: Error) => void) | null,
57 | operation: RelayRuntime.OperationSelector,
58 | updater?: RelayRuntime.SelectorStoreUpdater | null,
59 | }): RelayRuntime.Disposable;
60 | }
61 |
62 | export const Network: {
63 | /**
64 | * Creates an implementation of the `Network` interface defined in
65 | * `RelayNetworkTypes` given a single `fetch` function.
66 | */
67 | create(fetch: RelayRuntime.FetchFunction, subscribe?: RelayRuntime.SubscribeFunction): RelayRuntime.Network;
68 | };
69 |
70 | export const ConnectionHandler: {
71 | /**
72 | * Creates an edge for a connection record, given a node and edge type.
73 | */
74 | createEdge(store: RelayRuntime.RecordSourceProxy, record: RelayRuntime.RecordProxy, node: RelayRuntime.RecordProxy, edgeType: string): RelayRuntime.RecordProxy;
75 | /**
76 | * Given a record and the name of the schema field for which a connection was
77 | * fetched, returns the linked connection record.
78 | *
79 | * Example:
80 | *
81 | * Given that data has already been fetched on some user `` on the `friends`
82 | * field:
83 | *
84 | * ```
85 | * fragment FriendsFragment on User {
86 | * friends(first: 10) @connection(key: "FriendsFragment_friends") {
87 | * edges {
88 | * node {
89 | * id
90 | * }
91 | * }
92 | * }
93 | * }
94 | * ```
95 | *
96 | * The `friends` connection record can be accessed with:
97 | *
98 | * ```
99 | * store => {
100 | * const user = store.get('');
101 | * const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
102 | * // Access fields on the connection:
103 | * const edges = friends.getLinkedRecords('edges');
104 | * }
105 | * ```
106 | *
107 | * TODO: t15733312
108 | * Currently we haven't run into this case yet, but we need to add a `getConnections`
109 | * that returns an array of the connections under the same `key` regardless of the variables.
110 | */
111 | getConnection(record: RelayRuntime.RecordProxy, key: string, filters?: RelayRuntime.Variables | null): RelayRuntime.RecordProxy | null;
112 | /**
113 | * A default runtime handler for connection fields that appends newly fetched
114 | * edges onto the end of a connection, regardless of the arguments used to fetch
115 | * those edges.
116 | */
117 | update(store: RelayRuntime.RecordSourceProxy, payload: RelayRuntime.HandleFieldPayload): void;
118 |
119 | /**
120 | * Inserts an edge after the given cursor, or at the end of the list if no
121 | * cursor is provided.
122 | *
123 | * Example:
124 | *
125 | * Given that data has already been fetched on some user `` on the `friends`
126 | * field:
127 | *
128 | * ```
129 | * fragment FriendsFragment on User {
130 | * friends(first: 10) @connection(key: "FriendsFragment_friends") {
131 | * edges {
132 | * node {
133 | * id
134 | * }
135 | * }
136 | * }
137 | * }
138 | * ```
139 | *
140 | * An edge can be appended with:
141 | *
142 | * ```
143 | * store => {
144 | * const user = store.get('');
145 | * const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
146 | * const edge = store.create('', 'FriendsEdge');
147 | * RelayConnectionHandler.insertEdgeAfter(friends, edge);
148 | * }
149 | * ```
150 | */
151 | insertEdgeAfter(record: RelayRuntime.RecordProxy, newEdge: RelayRuntime.RecordProxy, cursor?: string | null): void;
152 |
153 | /**
154 | * Inserts an edge before the given cursor, or at the beginning of the list if
155 | * no cursor is provided.
156 | *
157 | * Example:
158 | *
159 | * Given that data has already been fetched on some user `` on the `friends`
160 | * field:
161 | *
162 | * ```
163 | * fragment FriendsFragment on User {
164 | * friends(first: 10) @connection(key: "FriendsFragment_friends") {
165 | * edges {
166 | * node {
167 | * id
168 | * }
169 | * }
170 | * }
171 | * }
172 | * ```
173 | *
174 | * An edge can be prepended with:
175 | *
176 | * ```
177 | * store => {
178 | * const user = store.get('');
179 | * const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
180 | * const edge = store.create('', 'FriendsEdge');
181 | * RelayConnectionHandler.insertEdgeBefore(friends, edge);
182 | * }
183 | * ```
184 | */
185 | insertEdgeBefore(
186 | record: RelayRuntime.RecordProxy,
187 | newEdge: RelayRuntime.RecordProxy,
188 | cursor?: string | null,
189 | ): void;
190 | /**
191 | * Remove any edges whose `node.id` matches the given id.
192 | */
193 | deleteNode(record: RelayRuntime.RecordProxy, nodeID: RelayRuntime.DataID): void;
194 | };
195 |
196 | /**
197 | * Determine if two selectors are equal (represent the same selection). Note
198 | * that this function returns `false` when the two queries/fragments are
199 | * different objects, even if they select the same fields.
200 | */
201 | export function areEqualSelectors(thisSelector: RelayRuntime.Selector, thatSelector: RelayRuntime.Selector): boolean;
202 |
203 | export function createFragmentSpecResolver(
204 | context: RelayRuntime.RelayContext,
205 | containerName: string,
206 | fragments: RelayRuntime.FragmentMap,
207 | props: RelayRuntime.Props,
208 | callback: () => void,
209 | ): RelayRuntime.FragmentSpecResolver;
210 |
211 | /**
212 | * Creates an instance of the `OperationSelector` type defined in
213 | * `RelayStoreTypes` given an operation and some variables. The input variables
214 | * are filtered to exclude variables that do not match defined arguments on the
215 | * operation, and default values are populated for null values.
216 | */
217 | export function createOperationSelector(
218 | operation: RelayRuntime.ConcreteBatch,
219 | variables: RelayRuntime.Variables,
220 | ): RelayRuntime.OperationSelector;
221 |
222 | /**
223 | * Given a mapping of keys -> results and a mapping of keys -> fragments,
224 | * extracts a mapping of keys -> id(s) of the results.
225 | *
226 | * Similar to `getSelectorsFromObject()`, this function can be useful in
227 | * determining the "identity" of the props passed to a component.
228 | */
229 | export function getDataIDsFromObject(
230 | fragments: { [key: string]: RelayRuntime.ConcreteFragment },
231 | object: { [key: string]: any },
232 | ): { [key: string]: (RelayRuntime.DataID | Array) | null };
233 |
234 | /**
235 | * Given a mapping of keys -> results and a mapping of keys -> fragments,
236 | * extracts the selectors for those fragments from the results.
237 | *
238 | * The canonical use-case for this function is ReactRelayFragmentContainer, which
239 | * uses this function to convert (props, fragments) into selectors so that it
240 | * can read the results to pass to the inner component.
241 | */
242 | export function getSelectorsFromObject(
243 | operationVariables: RelayRuntime.Variables,
244 | fragments: { [key: string]: RelayRuntime.ConcreteFragment },
245 | object: { [key: string]: any },
246 | ): { [key: string]: (RelayRuntime.Selector | Array) | null };
247 |
248 | /**
249 | * Given the result `items` from a parent that fetched `fragment`, creates a
250 | * selector that can be used to read the results of that fragment on those
251 | * items. This is similar to `getSelector` but for "plural" fragments that
252 | * expect an array of results and therefore return an array of selectors.
253 | */
254 | export function getSelectorList(
255 | operationVariables: RelayRuntime.Variables,
256 | fragment: RelayRuntime.ConcreteFragment,
257 | items: Array,
258 | ): Array | null;
259 |
260 | /**
261 | * Given the result `item` from a parent that fetched `fragment`, creates a
262 | * selector that can be used to read the results of that fragment for that item.
263 | *
264 | * Example:
265 | *
266 | * Given two fragments as follows:
267 | *
268 | * ```
269 | * fragment Parent on User {
270 | * id
271 | * ...Child
272 | * }
273 | * fragment Child on User {
274 | * name
275 | * }
276 | * ```
277 | *
278 | * And given some object `parent` that is the results of `Parent` for id "4",
279 | * the results of `Child` can be accessed by first getting a selector and then
280 | * using that selector to `lookup()` the results against the environment:
281 | *
282 | * ```
283 | * const childSelector = getSelector(queryVariables, Child, parent);
284 | * const childData = environment.lookup(childSelector).data;
285 | * ```
286 | */
287 | export function getSelector(
288 | operationVariables: RelayRuntime.Variables,
289 | fragment: RelayRuntime.ConcreteFragment,
290 | item: any,
291 | ): RelayRuntime.Selector | null;
292 |
293 | /**
294 | * Given a mapping of keys -> results and a mapping of keys -> fragments,
295 | * extracts the merged variables that would be in scope for those
296 | * fragments/results.
297 | *
298 | * This can be useful in determing what varaibles were used to fetch the data
299 | * for a Relay container, for example.
300 | */
301 | export function getVariablesFromObject(
302 | operationVariables: RelayRuntime.Variables,
303 | fragments: { [key: string]: RelayRuntime.ConcreteFragment },
304 | object: { [key: string]: any },
305 | ): RelayRuntime.Variables;
306 |
307 | export const ViewerHandler: {
308 | /**
309 | * A runtime handler for the `viewer` field. The actual viewer record will
310 | * *never* be accessed at runtime because all fragments that reference it will
311 | * delegate to the handle field. So in order to prevent GC from having to check
312 | * both the original server field *and* the handle field (which would be almost
313 | * duplicate work), the handler copies server fields and then deletes the server
314 | * record.
315 | *
316 | * NOTE: This means other handles may not be added on viewer, since they may
317 | * execute after this handle when the server record is already deleted.
318 | */
319 | update(store: RelayRuntime.RecordSourceProxy, payload: RelayRuntime.HandleFieldPayload): void;
320 | VIEWER_ID: string;
321 | }
322 |
323 | export function commitLocalUpdate(
324 | environment: RelayRuntime.Environment,
325 | updater: RelayRuntime.StoreUpdater,
326 | ): void;
327 |
328 | export interface MutationConfig {
329 | configs?: Array;
330 | mutation: RelayRuntime.GraphQLTaggedNode;
331 | variables: T['variables'];
332 | uploadables?: RelayRuntime.UploadableMap;
333 | onCompleted?: ((response: T['query'], errors: Array | null) => void) | null;
334 | onError?: ((error: Error) => void) | null;
335 | optimisticUpdater?: RelayRuntime.SelectorStoreUpdater | null;
336 | optimisticResponse?: object;
337 | updater?: RelayRuntime.SelectorStoreUpdater | null;
338 | }
339 |
340 | /**
341 | * Higher-level helper function to execute a mutation against a specific
342 | * environment.
343 | */
344 | export function commitMutation(
345 | environment: Environment,
346 | config: MutationConfig,
347 | ): RelayRuntime.Disposable;
348 |
349 | export function fetchQuery(
350 | environment: RelayRuntime.Environment,
351 | taggedNode: RelayRuntime.GraphQLTaggedNode,
352 | variables: RelayRuntime.Variables,
353 | cacheConfig?: RelayRuntime.CacheConfig | null,
354 | ): Promise;
355 |
356 | export function isRelayModernEnvironment(
357 | environment: RelayRuntime.Environment,
358 | ): boolean;
359 |
360 | export interface GraphQLSubscriptionConfig {
361 | subscription: RelayRuntime.GraphQLTaggedNode;
362 | variables: RelayRuntime.Variables;
363 | onCompleted?: (() => void) | null;
364 | onError?: ((error: Error) => void) | null;
365 | onNext?: ((response: object | null) => void) | null;
366 | updater?: ((store: RelayRuntime.RecordSourceSelectorProxy) => void) | null;
367 | }
368 |
369 | export function requestSubscription(
370 | environment: RelayRuntime.Environment,
371 | config: GraphQLSubscriptionConfig,
372 | ): RelayRuntime.Disposable;
373 |
374 | interface GraphQLFunction {
375 | (
376 | parts: TemplateStringsArray,
377 | ...tpl: never[],
378 | ): RelayRuntime.GraphQLTaggedNode;
379 | experimental(
380 | parts: TemplateStringsArray,
381 | ...tpl: never[],
382 | ): RelayRuntime.GraphQLTaggedNode
383 | }
384 |
385 | export const graphql: GraphQLFunction;
386 |
387 | export as namespace RelayRuntime;
388 | export type RecordSourceSelectorProxy = RelayRuntime.RecordSourceSelectorProxy;
389 | export type RecordProxy = RelayRuntime.RecordProxy;
390 |
391 | export class Store implements RelayRuntime.Store {
392 | constructor(source: RelayRuntime.MutableRecordSource);
393 | getSource(): RelayRuntime.RecordSource;
394 | check(selector: RelayRuntime.CSelector): boolean;
395 | lookup(selector: RelayRuntime.CSelector): RelayRuntime.CSnapshot;
396 | notify(): void;
397 | publish(source: RelayRuntime.RecordSource): void;
398 | resolve(target: RelayRuntime.MutableRecordSource, selector: RelayRuntime.CSelector, callback: RelayRuntime.AsyncLoadCallback): void;
399 | retain(selector: RelayRuntime.CSelector): RelayRuntime.Disposable;
400 | subscribe(snapshot: RelayRuntime.CSnapshot, callback: (snapshot: RelayRuntime.CSnapshot) => void): RelayRuntime.Disposable;
401 | }
402 |
403 | export class RecordSource implements RelayRuntime.MutableRecordSource {
404 | constructor();
405 | clear(): void;
406 | delete(dataID: string): void;
407 | remove(dataID: string): void;
408 | set(dataID: string, record: RelayRuntime.Record): void;
409 | get(dataID: string): RelayRuntime.Record | null;
410 | getRecordIDs(): string[];
411 | getStatus(dataID: string): RelayRuntime.RecordState;
412 | has(dataID: string): boolean;
413 | load(dataID: string, callback: (error: Error | null, record: RelayRuntime.Record | null) => void): void;
414 | size(): number;
415 | }
416 |
--------------------------------------------------------------------------------
/config/typeGenerator.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'async-file';
2 | import {
3 | graphql,
4 | introspectionQuery,
5 | parseType,
6 | visit,
7 | ASTNode,
8 | BREAK,
9 | DirectiveNode,
10 | DocumentNode,
11 | FragmentDefinitionNode,
12 | FragmentSpreadNode,
13 | GraphQLDirective,
14 | GraphQLObjectType,
15 | GraphQLSchema,
16 | ListTypeNode,
17 | NamedTypeNode,
18 | NameNode,
19 | OperationDefinitionNode,
20 | Source,
21 | TypeNode,
22 | ValueNode,
23 | } from 'graphql';
24 | import { getClientSchema } from 'graphql-fragment-type-generator';
25 | import { extractNamedTypes } from 'graphql-fragment-type-generator/lib/ExtractNamedTypes';
26 | import { mapFragmentType } from 'graphql-fragment-type-generator/lib/FragmentMapper';
27 | import { mapType } from 'graphql-fragment-type-generator/lib/MultiFragmentMapper';
28 | import { printType } from 'graphql-fragment-type-generator/lib/Printer';
29 | import {
30 | decorateTypeWithTypeBrands,
31 | decorateWithTypeBrands,
32 | getTypeBrandNames,
33 | } from 'graphql-fragment-type-generator/lib/TypeBrandDecorator';
34 | import { normalizeListType, normalizeType } from 'graphql-fragment-type-generator/lib/TypeNormalizer';
35 | import * as T from 'graphql-fragment-type-generator/lib/Types';
36 | import * as path from 'path';
37 | import * as ts from 'typescript';
38 | import { getFragmentNameParts } from './getFragmentNameParts';
39 | import { generateSchemaFile } from './schemaFileGenerator';
40 |
41 | interface NamedBrandedTypeResult {
42 | brandsToImport: string[];
43 | exportNamesTypeScriptCode: string;
44 | fragmentTypeBrandText: string;
45 | fragmentTypeText: string;
46 | }
47 |
48 | interface BrandedTypeResult {
49 | brandsToImport: string[];
50 | fragmentTypeBrandText: string;
51 | fragmentTypeText: string;
52 | }
53 |
54 | function getTypeBrandedTypeDefinition(
55 | normalizedAst: T.FlattenedObjectType | T.FlattenedListType,
56 | withNames: boolean,
57 | indentSpaces?: number,
58 | ): BrandedTypeResult {
59 | const brandedAst = decorateTypeWithTypeBrands(normalizedAst) as T.FlattenedObjectType | T.FlattenedListType;
60 |
61 | const names = getTypeBrandNames(brandedAst);
62 |
63 | const brandsToImport = names.allRequiredNames;
64 | const fragmentTypeBrandText = getFragmentTypeBrandText(names.fragmentTypeNames, brandedAst.kind === 'List');
65 |
66 | const typeText = printType(false, brandedAst, withNames, indentSpaces);
67 |
68 | return {
69 | brandsToImport: brandsToImport,
70 | fragmentTypeBrandText: fragmentTypeBrandText,
71 | fragmentTypeText: typeText,
72 | };
73 | }
74 |
75 | function getNamedTypeBrandedTypeDefinitions(
76 | normalizedAst: T.FlattenedObjectType | T.FlattenedListType,
77 | indentSpaces?: number,
78 | ): NamedBrandedTypeResult {
79 | const res = getTypeBrandedTypeDefinition(normalizedAst, true, indentSpaces);
80 | const extractedNames = extractNamedTypes(normalizedAst);
81 |
82 | const tsChunks: string[] = [];
83 |
84 | extractedNames.forEach((typeAst, name) => {
85 | const decorated = decorateTypeWithTypeBrands(typeAst);
86 |
87 | const def = printType(false, decorated, true, 0);
88 | tsChunks.push(`export type ${name} = ${def};`);
89 | });
90 |
91 | return {
92 | ...res,
93 | exportNamesTypeScriptCode: tsChunks.join('\n'),
94 | };
95 | }
96 |
97 | function getFragmentTypeBrandText(names: string[], plural: boolean, indentSpaces?: number): string {
98 | if (indentSpaces == null) {
99 | indentSpaces = 0;
100 | }
101 | if (plural) {
102 | return '(' + getFragmentTypeBrandText(names, false, indentSpaces) + ' | null)[]';
103 | }
104 | return `{
105 | ${' '.repeat(indentSpaces + 2)}'': ${names.join(' | ')};
106 | ${' '.repeat(indentSpaces)}}`;
107 | }
108 |
109 | function getNormalizedAst(
110 | schema: GraphQLSchema,
111 | documentNode: DocumentNode,
112 | ): T.FlattenedObjectType | T.FlattenedListType {
113 | const ast = mapFragmentType(schema, documentNode);
114 | if (ast.kind === 'Object') {
115 | return normalizeType(schema, ast);
116 | } else {
117 | return normalizeListType(schema, ast);
118 | }
119 | }
120 | function getNormalizedOperationAst(
121 | schema: GraphQLSchema,
122 | documentNode: DocumentNode,
123 | rootNode: OperationDefinitionNode,
124 | ): T.FlattenedObjectType {
125 |
126 | const ast = mapType(schema, documentNode, rootNode);
127 | return normalizeType(schema, ast);
128 | }
129 |
130 | function findReferencedFragmentNames(
131 | node: FragmentDefinitionNode,
132 | fragmentMap: Map,
133 | ): Set {
134 | const visitedFragments = new Set();
135 | const fragmentsToVisit = [node];
136 | const allFragments = new Set();
137 |
138 | while (fragmentsToVisit.length > 0) {
139 | const nodeToVisit = fragmentsToVisit[fragmentsToVisit.length - 1];
140 | fragmentsToVisit.pop();
141 | visitedFragments.add(nodeToVisit.name.value);
142 | visit(nodeToVisit, {
143 | FragmentSpread: (fragmentSpread: FragmentSpreadNode) => {
144 | allFragments.add(fragmentSpread.name.value);
145 |
146 | if (!visitedFragments.has(fragmentSpread.name.value)) {
147 | const fragment = fragmentMap.get(fragmentSpread.name.value);
148 | if (fragment == null) {
149 | throw new Error('Could not find fragment: ' + fragmentSpread.name.value);
150 | }
151 | fragmentsToVisit.push(fragment);
152 | }
153 | },
154 | });
155 | }
156 |
157 | return allFragments;
158 | }
159 |
160 | interface VariableInfo {
161 | hasDefaultValue: boolean;
162 | typeNode: TypeNode;
163 | }
164 |
165 | function getArgumentType(
166 | inputValue: ValueNode,
167 | ): VariableInfo | null {
168 | if (inputValue.kind !== 'ObjectValue') {
169 | return null;
170 | }
171 |
172 | const typeField = inputValue.fields.find(v => v.name.value === 'type');
173 |
174 | if (typeField == null || typeField.value.kind !== 'StringValue') {
175 | return null;
176 | }
177 |
178 | const typeString = typeField.value.value;
179 | const hasDefaultValue = inputValue.fields.find(v => v.name.value === 'defaultValue') != null;
180 | return {
181 | hasDefaultValue: hasDefaultValue,
182 | // Type definitions are not right for this version of graphql
183 | typeNode: (parseType as any as (source: string) => TypeNode)(typeString),
184 | };
185 | }
186 |
187 | function getVariableDefinitions(
188 | definition: FragmentDefinitionNode,
189 | ): Map | null {
190 | if (definition.directives == null) {
191 | return null;
192 | }
193 |
194 | const argumentsDefinition = definition.directives.find(v => v.name.value === 'argumentDefinitions');
195 |
196 | if (argumentsDefinition == null || argumentsDefinition.arguments == null) {
197 | return null;
198 | }
199 |
200 | const result = new Map();
201 | for (const argument of argumentsDefinition.arguments) {
202 | const argName = argument.name.value;
203 | const argType = getArgumentType(argument.value);
204 | if (argType != null) {
205 | result.set(argName, argType);
206 | }
207 | }
208 | return result;
209 | }
210 |
211 | function getOperationVariables(
212 | operationNode: OperationDefinitionNode,
213 | ): Map | null {
214 | if (operationNode.variableDefinitions == null || operationNode.variableDefinitions.length === 0) {
215 | return null;
216 | }
217 |
218 | const result = new Map();
219 | for (const variableDef of operationNode.variableDefinitions) {
220 | const varName = variableDef.variable.name.value;
221 | const varType = variableDef.type;
222 | const hasDefaultValue = variableDef.defaultValue != null;
223 | result.set(varName, {
224 | hasDefaultValue: hasDefaultValue,
225 | typeNode: varType,
226 | });
227 | }
228 | return result;
229 | }
230 |
231 | function assertNever(val: never, msg: string): never {
232 | throw new Error(msg);
233 | }
234 |
235 | function printGraphQLListType(
236 | listType: ListTypeNode,
237 | typesToImport: Set,
238 | isNonNull: boolean,
239 | ): string {
240 | if (isNonNull) {
241 | return `Array<${printGraphQLType(listType.type, typesToImport, false)}>`;
242 | }
243 | return `(Array<${printGraphQLType(listType.type, typesToImport, false)}> | null)`;
244 | }
245 |
246 | function getScalarType(typeName: string): string | null {
247 | switch (typeName) {
248 | case 'Int':
249 | case 'Float':
250 | case 'Probability':
251 | case 'ProjectTaskProgress':
252 | return 'number';
253 | case 'Boolean':
254 | return 'boolean';
255 | case 'String':
256 | case 'ID':
257 | case 'DateTime':
258 | case 'LocalDate':
259 | case 'LocalDateTime':
260 | return 'string';
261 | default:
262 | return null;
263 | }
264 | }
265 |
266 | function printNamedGraphQLType(
267 | namedType: NamedTypeNode,
268 | typesToImport: Set,
269 | ): string {
270 | const scalarType = getScalarType(namedType.name.value);
271 |
272 | if (scalarType != null) {
273 | return scalarType;
274 | }
275 |
276 | typesToImport.add(namedType.name.value);
277 |
278 | return namedType.name.value;
279 | }
280 |
281 | function printGraphQLType(
282 | typeNode: TypeNode,
283 | typesToImport: Set,
284 | isNonNull: boolean = false,
285 | ): string {
286 | switch (typeNode.kind) {
287 | case 'NamedType': {
288 | const namedType = printNamedGraphQLType(typeNode, typesToImport);
289 | if (isNonNull) {
290 | return namedType;
291 | }
292 | return namedType + ' | null';
293 | }
294 | case 'NonNullType':
295 | return printGraphQLType(typeNode.type, typesToImport, true);
296 | case 'ListType':
297 | return printGraphQLListType(typeNode, typesToImport, isNonNull);
298 | default:
299 | return assertNever(typeNode, 'Unexpected type');
300 | }
301 | }
302 |
303 | function getOperationInputType(
304 | variablesInfo: Map | null,
305 | operationName: string,
306 | typesToImport: Set,
307 | ): string {
308 | if (variablesInfo == null) {
309 | return '';
310 | }
311 |
312 | const typeLines: string[] = [];
313 | variablesInfo.forEach((varInfo, varName) => {
314 | const optionalParamDef = varInfo.hasDefaultValue || varInfo.typeNode.kind !== 'NonNullType' ? '?' : '';
315 | typeLines.push(` ${varName}${optionalParamDef}: ${printGraphQLType(varInfo.typeNode, typesToImport, false)},\n`);
316 | });
317 |
318 | return `export interface ${operationName}Variables {\n${typeLines.join('')}}\n`;
319 | }
320 |
321 | function stripExportTypeDirectives(
322 | graphQLNode: TNode,
323 | ): TNode {
324 | return visit(graphQLNode, {
325 | Directive(directiveNode: DirectiveNode) {
326 | if (directiveNode.name.value === 'exportType') {
327 | return null;
328 | }
329 | },
330 | });
331 | }
332 |
333 | export async function generator(
334 | schema: GraphQLSchema,
335 | baseDefinitions: DocumentNode[],
336 | documents: DocumentNode[],
337 | ): Promise