├── .watchmanconfig ├── tslint.json ├── .gitignore ├── types ├── webpack-dashboard │ └── plugin.d.ts ├── fork-ts-checker-webpack-plugin │ └── index.d.ts ├── graphql-schema.ts ├── react-relay │ ├── index.d.ts │ └── definitions.d.ts └── relay-runtime │ ├── index.d.ts │ └── definitions.d.ts ├── config ├── extraContent.js ├── getFragmentNameParts.ts ├── vendor-bundles.webpack.config.ts ├── webpack.config.ts ├── devServer.ts ├── schemaFileGenerator.ts ├── tsconfig.json ├── transform.ts └── typeGenerator.ts ├── public ├── index.html ├── base.css └── index.css ├── scripts ├── updateSchema.ts └── tsconfig.json ├── src ├── mutations │ ├── RenameTodoMutation.ts │ ├── ChangeTodoStatusMutation.ts │ ├── RemoveTodoMutation.ts │ ├── MarkAllTodosMutation.ts │ ├── RemoveCompletedTodosMutation.ts │ └── AddTodoMutation.ts ├── app.tsx └── components │ ├── TodoListFooter.tsx │ ├── TodoApp.tsx │ ├── TodoList.tsx │ ├── TodoTextInput.tsx │ └── Todo.tsx ├── package.json ├── data ├── schema.graphql ├── database.ts ├── tsconfig.json └── schema.ts ├── tsconfig.json ├── includes └── relay.d.ts └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-secoya", 3 | "rules": { 4 | "no-relative-imports": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | public/vendor.bundle.js 3 | public/vendor.bundle.js.map 4 | generated/ 5 | node_modules/ 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /types/webpack-dashboard/plugin.d.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from 'webpack'; 2 | declare namespace DashboardPlugin { 3 | 4 | } 5 | 6 | declare class DashboardPlugin extends webpack.Plugin { 7 | } 8 | 9 | export as namespace DashboardPlugin; 10 | export = DashboardPlugin; 11 | -------------------------------------------------------------------------------- /config/extraContent.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | require('ts-node').register({ 3 | project: path.resolve(__dirname, 'tsconfig.json'), 4 | }); 5 | 6 | var generator = require('./typeGenerator').generator; 7 | module.exports = { 8 | default: function (schema, baseDefinitions, definitions) { 9 | return generator(schema, baseDefinitions, definitions); 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /types/fork-ts-checker-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace ForkTSCheckerPlugin { 2 | interface Config { 3 | tsconfig?: string; 4 | tslint?: string; 5 | async?: boolean; 6 | } 7 | } 8 | import * as webpack from 'webpack'; 9 | 10 | declare class ForkTSCheckerPlugin extends webpack.Plugin { 11 | constructor(config: ForkTSCheckerPlugin.Config); 12 | } 13 | 14 | export as namespace ForkTSCheckerPlugin; 15 | export = ForkTSCheckerPlugin; 16 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Relay • TodoMVC • TypeScript 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /scripts/updateSchema.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | import * as fs from 'async-file'; 3 | import { graphql, introspectionQuery, printSchema } from 'graphql'; 4 | import * as path from 'path'; 5 | import { schema } from '../data/schema'; 6 | 7 | async function run() { 8 | 9 | const schemaPath = path.resolve(__dirname, '../data/schema.graphql'); 10 | const jsonSchemaPath = path.resolve(__dirname, '../data/gqlschema.json'); 11 | 12 | const jsonSchema = await graphql(schema, introspectionQuery); 13 | await Promise.all([ 14 | fs.writeFile(schemaPath, printSchema(schema), 'utf8'), 15 | fs.writeFile(jsonSchemaPath, JSON.stringify(jsonSchema.data, null, 4), 'utf8'), 16 | ]); 17 | } 18 | 19 | run().catch((err) => { 20 | // tslint:disable-next-line:no-console 21 | console.error(err); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /config/getFragmentNameParts.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_PROP_NAME = 'data'; 2 | export function getFragmentNameParts(fragmentName: string): [string, string] { 3 | const match = fragmentName.match( 4 | /^([a-zA-Z][a-zA-Z0-9]*)(?:_([a-zA-Z][_a-zA-Z0-9]*))?$/, 5 | ); 6 | if (!match) { 7 | throw new Error( 8 | 'BabelPluginGraphQL: Fragments should be named ' + 9 | '`ModuleName_fragmentName`, got `' + 10 | fragmentName + 11 | '`.', 12 | ); 13 | } 14 | const moduleName = match[1]; 15 | const propName = match[2]; 16 | if (propName === DEFAULT_PROP_NAME) { 17 | throw new Error( 18 | 'TypescriptTransformerRelay: Fragment `' + 19 | fragmentName + 20 | '` should not end in ' + 21 | '`_data` to avoid conflict with a fragment named `' + 22 | moduleName + 23 | '` ' + 24 | 'which also provides resulting data via the React prop `data`. Either ' + 25 | 'rename this fragment to `' + 26 | moduleName + 27 | '` or choose a different ' + 28 | 'prop name.', 29 | ); 30 | } 31 | return [moduleName, propName || DEFAULT_PROP_NAME]; 32 | } 33 | -------------------------------------------------------------------------------- /config/vendor-bundles.webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as webpack from 'webpack'; 3 | 4 | const config: webpack.Configuration = { 5 | devtool: 'source-map', 6 | entry: { 7 | vendor: ['react', 'react-dom', 'react-relay', 'relay-runtime', 'classnames'], 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | use: 'ts-loader', 14 | }, 15 | ], 16 | }, 17 | node: { 18 | Buffer: false, 19 | __dirname: false, 20 | __filename: false, 21 | console: false, 22 | global: true, 23 | process: false, 24 | setImmediate: false, 25 | }, 26 | output: { 27 | filename: '[name].bundle.js', 28 | library: '[name]_lib', 29 | path: path.resolve(__dirname, '..', 'public'), 30 | }, 31 | plugins: [ 32 | new webpack.optimize.AggressiveMergingPlugin(), 33 | new webpack.optimize.OccurrenceOrderPlugin(true), 34 | new webpack.DefinePlugin({ 35 | 'process.env': { 36 | NODE_ENV: JSON.stringify(process.env.NODE_ENV == null ? 'development' : process.env.NODE_ENV), 37 | }, 38 | }), 39 | new webpack.DllPlugin({ 40 | name: '[name]_lib', 41 | path: path.resolve(__dirname, '..', 'dist', '[name]-manifest.json'), 42 | }), 43 | ], 44 | }; 45 | 46 | export default config; 47 | -------------------------------------------------------------------------------- /types/graphql-schema.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | export enum AddTodoPayload {}; 3 | export enum ChangeTodoStatusPayload {}; 4 | export enum MarkAllTodosPayload {}; 5 | export enum PageInfo {}; 6 | export enum RemoveCompletedTodosPayload {}; 7 | export enum RemoveTodoPayload {}; 8 | export enum RenameTodoPayload {}; 9 | export enum Todo {}; 10 | export enum TodoConnection {}; 11 | export enum TodoEdge {}; 12 | export enum User {}; 13 | export enum Query {}; 14 | export enum Mutation {}; 15 | 16 | export interface AddTodoInput { 17 | clientMutationId?: string | null; 18 | text: string; 19 | } 20 | 21 | export interface ChangeTodoStatusInput { 22 | clientMutationId?: string | null; 23 | complete: boolean; 24 | id: string; 25 | } 26 | 27 | export interface MarkAllTodosInput { 28 | clientMutationId?: string | null; 29 | complete: boolean; 30 | } 31 | 32 | export interface RemoveCompletedTodosInput { 33 | clientMutationId?: string | null; 34 | } 35 | 36 | export interface RemoveTodoInput { 37 | clientMutationId?: string | null; 38 | id: string; 39 | } 40 | 41 | export interface RenameTodoInput { 42 | clientMutationId?: string | null; 43 | id: string; 44 | text: string; 45 | } 46 | 47 | 48 | 49 | export type NodeTypeNames = "User" | "Todo"; 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/mutations/RenameTodoMutation.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 | commitMutation, 15 | graphql, 16 | } from 'react-relay'; 17 | import { Environment } from 'relay-runtime'; 18 | 19 | const mutation = graphql` 20 | mutation RenameTodoMutation($input: RenameTodoInput!) { 21 | renameTodo(input:$input) { 22 | todo { 23 | id 24 | text 25 | } 26 | } 27 | } 28 | `; 29 | 30 | function getOptimisticResponse(text: string, todo: TodoInfo) { 31 | return { 32 | renameTodo: { 33 | todo: { 34 | id: todo.id, 35 | text: text, 36 | }, 37 | }, 38 | }; 39 | } 40 | 41 | interface TodoInfo { 42 | id: string; 43 | } 44 | 45 | function commit( 46 | environment: Environment, 47 | text: string, 48 | todo: TodoInfo, 49 | ) { 50 | return commitMutation( 51 | environment, 52 | { 53 | mutation, 54 | optimisticResponse: getOptimisticResponse(text, todo), 55 | variables: { 56 | input: { text, id: todo.id }, 57 | }, 58 | }, 59 | ); 60 | } 61 | 62 | export default { commit }; 63 | -------------------------------------------------------------------------------- /config/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as webpack from 'webpack'; 3 | import { transform } from './transform'; 4 | 5 | const config: webpack.Configuration = { 6 | devServer: { 7 | contentBase: path.resolve(__dirname, '..', 'public'), 8 | hot: true, 9 | publicPath: '/dist', 10 | stats: { 11 | colors: true, 12 | }, 13 | }, 14 | devtool: 'source-map', 15 | entry: { 16 | bundle: [path.resolve(__dirname, '..', 'src', 'app')], 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.tsx?$/, 22 | use: [ 23 | { 24 | loader: 'ts-loader', 25 | options: { 26 | configFileName: path.resolve(__dirname, '..', 'tsconfig.json'), 27 | getCustomTransformers: () => ({ 28 | after: [ 29 | ], 30 | before: [ 31 | transform, 32 | ], 33 | }), 34 | }, 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | output: { 41 | filename: 'bundle.js', 42 | path: path.resolve(__dirname, '..', 'dist'), 43 | publicPath: '/dist/', 44 | }, 45 | performance: { 46 | hints: false, 47 | }, 48 | plugins: [ 49 | new webpack.optimize.OccurrenceOrderPlugin(true), 50 | new webpack.NamedModulesPlugin(), 51 | new webpack.DefinePlugin({ 52 | 'process.env': { 53 | NODE_ENV: JSON.stringify(process.env.NODE_ENV == null ? 'development' : process.env.NODE_ENV), 54 | }, 55 | }), 56 | new webpack.DllReferencePlugin({ 57 | context: '.', 58 | manifest: require('../dist/vendor-manifest.json'), 59 | }), 60 | ], 61 | resolve: { 62 | alias: { 63 | generated: path.resolve(__dirname, '..', 'generated'), 64 | }, 65 | extensions: ['.ts', '.tsx', '.js'], 66 | mainFields: ['module', 'main'], 67 | }, 68 | }; 69 | 70 | export default config; 71 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 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 * as React from 'react'; 14 | import * as ReactDOM from 'react-dom'; 15 | 16 | import { 17 | graphql, 18 | QueryRenderer, 19 | ReadyState, 20 | } from 'react-relay'; 21 | import { 22 | Environment, 23 | Network, 24 | RecordSource, 25 | Store, 26 | } from 'relay-runtime'; 27 | 28 | import TodoApp from './components/TodoApp'; 29 | 30 | const mountNode = document.getElementById('root'); 31 | 32 | const fetchQuery: RelayRuntime.FetchFunction = ( 33 | operation, 34 | variables, 35 | ) => { 36 | return fetch('/graphql', { 37 | body: JSON.stringify({ 38 | query: operation.text, 39 | variables, 40 | }), 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | }, 44 | method: 'POST', 45 | }).then(response => { 46 | return response.json(); 47 | }); 48 | }; 49 | 50 | const modernEnvironment = new Environment({ 51 | network: Network.create(fetchQuery), 52 | store: new Store(new RecordSource()), 53 | }); 54 | 55 | const query = graphql` 56 | query appQuery { 57 | viewer { 58 | ...TodoApp_viewer 59 | } 60 | } 61 | `; 62 | 63 | const renderComponent: (readyState: ReadyState) => React.ReactElement | null = ({ error, props }) => { 64 | if (props) { 65 | return ; 66 | } else { 67 | return
Loading
; 68 | } 69 | }; 70 | 71 | ReactDOM.render( 72 | ( 73 | 79 | ), 80 | mountNode, 81 | ); 82 | -------------------------------------------------------------------------------- /src/mutations/ChangeTodoStatusMutation.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 | commitMutation, 15 | graphql, 16 | } from 'react-relay'; 17 | import { Environment } from 'relay-runtime'; 18 | 19 | const mutation = graphql` 20 | mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) { 21 | changeTodoStatus(input: $input) { 22 | todo { 23 | id 24 | complete 25 | } 26 | viewer { 27 | id 28 | completedCount 29 | } 30 | } 31 | } 32 | `; 33 | 34 | function getOptimisticResponse( 35 | complete: boolean, 36 | todo: TodoInfo, 37 | user: UserInfo, 38 | ) { 39 | const viewerPayload: { 40 | completedCount?: number; 41 | id: string; 42 | } = { id: user.id }; 43 | if (user.completedCount != null) { 44 | viewerPayload.completedCount = complete ? 45 | user.completedCount + 1 : 46 | user.completedCount - 1; 47 | } 48 | return { 49 | changeTodoStatus: { 50 | todo: { 51 | complete: complete, 52 | id: todo.id, 53 | }, 54 | viewer: viewerPayload, 55 | }, 56 | }; 57 | } 58 | 59 | interface UserInfo { 60 | completedCount?: number | null; 61 | id: string; 62 | } 63 | 64 | interface TodoInfo { 65 | id: string; 66 | } 67 | 68 | function commit( 69 | environment: Environment, 70 | complete: boolean, 71 | todo: TodoInfo, 72 | user: UserInfo, 73 | ) { 74 | return commitMutation( 75 | environment, 76 | { 77 | mutation, 78 | optimisticResponse: getOptimisticResponse(complete, todo, user), 79 | variables: { 80 | input: { complete, id: todo.id }, 81 | }, 82 | }, 83 | ); 84 | } 85 | 86 | export default { commit }; 87 | -------------------------------------------------------------------------------- /types/react-relay/index.d.ts: -------------------------------------------------------------------------------- 1 | export { commitLocalUpdate, fetchQuery, commitMutation, requestSubscription, graphql } from 'relay-runtime'; 2 | import * as RelayRuntime from 'relay-runtime/definitions'; 3 | import * as ReactRelay from 'react-relay/definitions'; 4 | import * as React from 'react'; 5 | 6 | 7 | 8 | export interface QueryRendererProps { 9 | cacheConfig?: RelayRuntime.CacheConfig | null; 10 | environment: RelayRuntime.Environment; 11 | query: RelayRuntime.GraphQLTaggedNode | null; 12 | render: (readyState: ReadyState) => React.ReactElement | null; 13 | variables: RelayRuntime.Variables; 14 | } 15 | export interface ReadyState { 16 | error: Error | null; 17 | props: object | null; 18 | retry: (() => void) | null; 19 | } 20 | 21 | export class QueryRenderer extends React.Component { } 22 | 23 | export interface GeneratedNodeMap { 24 | [key: string]: RelayRuntime.GraphQLTaggedNode; 25 | } 26 | 27 | export function createFragmentContainer( 28 | Component: ReactRelay.FragmentContainerConstructor, 29 | fragmentSpec: RelayRuntime.GraphQLTaggedNode | GeneratedNodeMap, 30 | ): ReactRelay.FragmentComponent; 31 | 32 | export function createRefetchContainer( 33 | Component: ReactRelay.RefetchContainerConstructor, 34 | fragmentspec: RelayRuntime.GraphQLTaggedNode | GeneratedNodeMap, 35 | refetchQuery: RelayRuntime.GraphQLTaggedNode, 36 | ): ReactRelay.RefetchComponent; 37 | 38 | export function createPaginationContainer( 39 | Component: ReactRelay.PaginationContainerConstructor, 40 | fragmentSpec: RelayRuntime.GraphQLTaggedNode | GeneratedNodeMap, 41 | connectionConfig: ReactRelay.ConnectionConfig, 42 | ): ReactRelay.PaginationComponent; 43 | -------------------------------------------------------------------------------- /src/components/TodoListFooter.tsx: -------------------------------------------------------------------------------- 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 RemoveCompletedTodosMutation from '../mutations/RemoveCompletedTodosMutation'; 14 | 15 | import * as React from 'react'; 16 | import { 17 | createFragmentContainer, 18 | graphql, 19 | } from 'react-relay'; 20 | 21 | class TodoListFooter extends Relay.TodoListFooterFragmentContainer { 22 | private handleRemoveCompletedTodosClick = () => { 23 | RemoveCompletedTodosMutation.commit( 24 | this.props.relay.environment, 25 | this.props.viewer.completedTodos, 26 | this.props.viewer, 27 | ); 28 | } 29 | 30 | public render() { 31 | const numCompletedTodos = this.props.viewer.completedCount; 32 | const numRemainingTodos = this.props.viewer.totalCount - numCompletedTodos; 33 | 34 | const clearCompleted = numCompletedTodos > 0 && ( 35 | 40 | ); 41 | return ( 42 |
43 | 44 | {numRemainingTodos} item{numRemainingTodos === 1 ? '' : 's'} left 45 | 46 | {clearCompleted} 47 |
48 | ); 49 | } 50 | } 51 | 52 | export default createFragmentContainer( 53 | TodoListFooter, 54 | graphql` 55 | fragment TodoListFooter_viewer on User { 56 | id, 57 | completedCount, 58 | completedTodos: todos( 59 | status: "completed", 60 | first: 2147483647 # max GraphQLInt 61 | ) { 62 | edges { 63 | node { 64 | id 65 | complete 66 | } 67 | } 68 | }, 69 | totalCount, 70 | } 71 | `, 72 | ); 73 | -------------------------------------------------------------------------------- /src/mutations/RemoveTodoMutation.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 | commitMutation, 15 | graphql, 16 | } from 'react-relay'; 17 | import { ConnectionHandler, Environment, RecordSourceSelectorProxy } from 'relay-runtime'; 18 | 19 | const mutation = graphql` 20 | mutation RemoveTodoMutation($input: RemoveTodoInput!) { 21 | removeTodo(input: $input) { 22 | deletedTodoId, 23 | viewer { 24 | completedCount, 25 | totalCount, 26 | }, 27 | } 28 | } 29 | `; 30 | 31 | function sharedUpdater(store: RecordSourceSelectorProxy, user: UserInfo, deletedID: string) { 32 | const userProxy = store.get(user.id); 33 | if (userProxy == null) { 34 | throw new Error('Could not find user with id: ' + user.id); 35 | } 36 | const conn = ConnectionHandler.getConnection( 37 | userProxy, 38 | 'TodoList_todos', 39 | ); 40 | if (conn == null) { 41 | throw new Error('Could not find connection TodoList_todos for user ' + user.id); 42 | } 43 | ConnectionHandler.deleteNode( 44 | conn, 45 | deletedID, 46 | ); 47 | } 48 | 49 | interface UserInfo { 50 | id: string; 51 | } 52 | 53 | interface TodoInfo { 54 | id: string; 55 | } 56 | 57 | function commit( 58 | environment: Environment, 59 | todo: TodoInfo, 60 | user: UserInfo, 61 | ) { 62 | return commitMutation( 63 | environment, 64 | { 65 | mutation, 66 | optimisticUpdater: (store) => { 67 | sharedUpdater(store, user, todo.id); 68 | }, 69 | updater: (store) => { 70 | const payload = store.getRootField('removeTodo'); 71 | if (payload == null) { 72 | throw new Error('Could not find root field: removeTodo'); 73 | } 74 | sharedUpdater(store, user, payload.getValue('deletedTodoId')); 75 | }, 76 | variables: { 77 | input: { id: todo.id }, 78 | }, 79 | }, 80 | ); 81 | } 82 | 83 | export default { commit }; 84 | -------------------------------------------------------------------------------- /src/components/TodoApp.tsx: -------------------------------------------------------------------------------- 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 AddTodoMutation from '../mutations/AddTodoMutation'; 14 | import TodoList from './TodoList'; 15 | import TodoListFooter from './TodoListFooter'; 16 | import TodoTextInput from './TodoTextInput'; 17 | 18 | import * as React from 'react'; 19 | import { 20 | createFragmentContainer, 21 | graphql, 22 | } from 'react-relay'; 23 | 24 | class TodoApp extends Relay.TodoAppFragmentContainer { 25 | private handleTextInputSave = (text: string) => { 26 | AddTodoMutation.commit( 27 | this.props.relay.environment, 28 | text, 29 | this.props.viewer, 30 | ); 31 | } 32 | 33 | public render() { 34 | const hasTodos = this.props.viewer.totalCount > 0; 35 | const footer = hasTodos && ( 36 | 39 | ); 40 | 41 | return ( 42 |
43 |
44 |
45 |

46 | todos 47 |

48 | 55 |
56 | 57 | {footer} 58 |
59 | 72 |
73 | ); 74 | } 75 | } 76 | 77 | export default createFragmentContainer(TodoApp, { 78 | viewer: graphql` 79 | fragment TodoApp_viewer on User { 80 | id 81 | totalCount 82 | ...TodoListFooter_viewer 83 | ...TodoList_viewer 84 | } 85 | `, 86 | }); 87 | -------------------------------------------------------------------------------- /config/devServer.ts: -------------------------------------------------------------------------------- 1 | #!../node_modules/.bin/ts-node 2 | import * as express from 'express'; 3 | import * as graphQLHTTP from 'express-graphql'; 4 | import * as ForkTSCheckerPlugin from 'fork-ts-checker-webpack-plugin'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | import * as webpack from 'webpack'; 8 | import * as DashboardPlugin from 'webpack-dashboard/plugin'; 9 | import * as webpackDevMiddleware from 'webpack-dev-middleware'; 10 | import * as webpackHotMiddleware from 'webpack-hot-middleware'; 11 | import { schema } from '../data/schema'; 12 | import { transform } from './transform'; 13 | import config from './webpack.config'; 14 | 15 | const devConfig = { 16 | ...config, 17 | entry: { 18 | ...config.entry as object, 19 | bundle: [ 20 | 'webpack-hot-middleware/client', 21 | ...(config.entry as { bundle: string[] }).bundle, 22 | ], 23 | }, 24 | module: { 25 | ...config.module, 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: [ 30 | { 31 | loader: 'ts-loader', 32 | options: { 33 | configFileName: path.resolve(__dirname, '..', 'tsconfig.json'), 34 | getCustomTransformers: () => ({ 35 | after: [ 36 | ], 37 | before: [ 38 | transform, 39 | ], 40 | }), 41 | transpileOnly: true, 42 | }, 43 | }, 44 | ], 45 | }, 46 | ], 47 | }, 48 | plugins: [ 49 | new ForkTSCheckerPlugin({ 50 | tsconfig: path.resolve(__dirname, '..', 'tsconfig.json'), 51 | }), 52 | new webpack.HotModuleReplacementPlugin(), 53 | ...config.plugins || [], 54 | new DashboardPlugin(), 55 | ], 56 | }; 57 | 58 | const compiler = webpack(devConfig); 59 | const hotMiddleware = webpackHotMiddleware(compiler, { 60 | path: '/__webpack_hmr', 61 | }); 62 | const devMiddleware = webpackDevMiddleware(compiler, { 63 | noInfo: true, 64 | publicPath: '/dist/', 65 | }); 66 | 67 | const app = express(); 68 | 69 | app.get('/*', (req, res, next) => { 70 | if (req.url.startsWith('/dist')) { 71 | return devMiddleware(req, res, next); 72 | } else if (req.url.startsWith('/__webpack_hmr')) { 73 | return hotMiddleware(req, res, next); 74 | } 75 | next(); 76 | }); 77 | 78 | app.use(express.static(path.resolve(__dirname, '..', 'public'))); 79 | 80 | app.use('/graphql', graphQLHTTP({ schema: schema, pretty: true })); 81 | 82 | app.listen(8080, () => { 83 | // tslint:disable-next-line:no-console 84 | console.log('Server running on http://localhost:8080/'); 85 | // tslint:disable-next-line:no-console 86 | console.log('GraphQL running on http://localhost:8080/graphql'); 87 | }); 88 | -------------------------------------------------------------------------------- /public/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-relay-modern", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "relay-compiler --src ./src --outputDir ./generated --schema ./data/schema.graphql --extraContentGeneratorModule './config/extraContent' --outputExtension ts --extensions tsx ts --transform '@secoya/relay-compiler-typescript'", 8 | "watch": "relay-compiler --src ./src --outputDir ./generated --schema ./data/schema.graphql --extraContentGeneratorModule './config/extraContent' --outputExtension ts --extensions tsx ts --transform '@secoya/relay-compiler-typescript' --watch", 9 | "update-schema": "ts-node -P ./scripts/tsconfig.json ./scripts/updateSchema.ts", 10 | "start": "ts-node -P ./config/tsconfig.json ./config/devServer.ts", 11 | "generate-vendor-bundle": "ts-node -P ./config/tsconfig.json ./node_modules/.bin/webpack --config ./config/vendor-bundles.webpack.config.ts -p", 12 | "static-build": "(cd config; ../node_modules/.bin/webpack)", 13 | "static-production-build": "(cd config; ../node_modules/.bin/webpack -p)" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@playlyfe/gql": "^2.3.1", 19 | "@secoya/relay-compiler": "1.1.0-secoya", 20 | "@secoya/relay-compiler-typescript": "1.0.0", 21 | "@types/classnames": "^2.2.0", 22 | "@types/express": "^4.0.36", 23 | "@types/express-graphql": "0.0.33", 24 | "@types/graphql": "^0.9.4", 25 | "@types/graphql-relay": "^0.4.2", 26 | "@types/node": "^8.0.10", 27 | "@types/react": "^15.0.37", 28 | "@types/react-dom": "^15.5.1", 29 | "@types/webpack": "^3.0.4", 30 | "@types/webpack-dev-middleware": "^1.9.1", 31 | "@types/webpack-hot-middleware": "^2.15.0", 32 | "async-file": "2.0.2", 33 | "classnames": "^2.2.5", 34 | "express": "^4.15.3", 35 | "fork-ts-checker-webpack-plugin": "^0.2.6", 36 | "graphql": "^0.10.3", 37 | "graphql-fragment-type-generator": "0.1.4", 38 | "react": "^15.6.1", 39 | "react-dom": "^15.6.1", 40 | "react-relay": "^1.1.0", 41 | "ts-graphql-plugin": "1.1.0", 42 | "ts-loader": "^2.2.2", 43 | "ts-node": "^3.2.0", 44 | "tslib": "^1.7.1", 45 | "tslint": "^5.5.0", 46 | "tslint-secoya": "^1.0.4", 47 | "typescript": "^2.4.1", 48 | "webpack": "^3.2.0", 49 | "webpack-dashboard": "^0.4.0", 50 | "webpack-dev-middleware": "^1.11.0", 51 | "webpack-hot-middleware": "^2.18.2" 52 | }, 53 | "dependencies": { 54 | "express": "^4.15.3", 55 | "express-graphql": "^0.6.6", 56 | "graphql-relay": "^0.5.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/TodoList.tsx: -------------------------------------------------------------------------------- 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 MarkAllTodosMutation from '../mutations/MarkAllTodosMutation'; 14 | import Todo from './Todo'; 15 | 16 | import * as React from 'react'; 17 | import { 18 | createFragmentContainer, 19 | graphql, 20 | } from 'react-relay'; 21 | 22 | class TodoList extends Relay.TodoListFragmentContainer { 23 | private handleMarkAllChange = (e: React.ChangeEvent) => { 24 | const complete = e.target.checked; 25 | MarkAllTodosMutation.commit( 26 | this.props.relay.environment, 27 | complete, 28 | this.props.viewer.todos, 29 | this.props.viewer, 30 | ); 31 | } 32 | 33 | private renderTodos() { 34 | const todos: JSX.Element[] = []; 35 | if (this.props.viewer.todos.edges != null) { 36 | this.props.viewer.todos.edges.forEach(edge => { 37 | if (edge != null && edge.node != null) { 38 | const todo = ( 39 | 44 | ); 45 | todos.push(todo); 46 | } 47 | }); 48 | } 49 | return todos; 50 | } 51 | 52 | public render() { 53 | const numTodos = this.props.viewer.totalCount; 54 | const numCompletedTodos = this.props.viewer.completedCount; 55 | return ( 56 |
57 | 63 | 66 |
    67 | {this.renderTodos()} 68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | export default createFragmentContainer(TodoList, { 75 | viewer: graphql` 76 | fragment TodoList_viewer on User { 77 | todos( 78 | first: 2147483647 # max GraphQLInt 79 | ) @connection(key: "TodoList_todos") { 80 | edges { 81 | node { 82 | id, 83 | complete, 84 | ...Todo_todo, 85 | }, 86 | }, 87 | }, 88 | id, 89 | totalCount, 90 | completedCount, 91 | ...Todo_viewer, 92 | } 93 | `, 94 | }); 95 | -------------------------------------------------------------------------------- /src/mutations/MarkAllTodosMutation.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 | commitMutation, 15 | graphql, 16 | } from 'react-relay'; 17 | import { Environment } from 'relay-runtime'; 18 | 19 | const mutation = graphql` 20 | mutation MarkAllTodosMutation($input: MarkAllTodosInput!) { 21 | markAllTodos(input: $input) { 22 | changedTodos { 23 | id 24 | complete 25 | } 26 | viewer { 27 | id 28 | completedCount 29 | } 30 | } 31 | } 32 | `; 33 | 34 | function getOptimisticResponse(complete: boolean, todos: TodoConnection, user: UserInfo) { 35 | const payload: { 36 | changedTodos?: { 37 | complete: boolean; 38 | id: string; 39 | }[]; 40 | viewer: { 41 | completedCount?: number; 42 | id: string; 43 | }; 44 | } = { 45 | viewer: { id: user.id }, 46 | }; 47 | if (todos && todos.edges) { 48 | payload.changedTodos = []; 49 | const changedTodos = payload.changedTodos; 50 | todos.edges.forEach(edge => { 51 | if (edge != null && edge.node != null && edge.node.complete !== complete) { 52 | changedTodos.push({ 53 | complete: complete, 54 | id: edge.node.id, 55 | }); 56 | } 57 | }); 58 | } 59 | if (user.totalCount != null) { 60 | payload.viewer.completedCount = complete ? 61 | user.totalCount : 62 | 0; 63 | } 64 | return { 65 | markAllTodos: payload, 66 | }; 67 | } 68 | 69 | interface TodoConnection { 70 | edges: (TodoEdge | null)[] | null; 71 | } 72 | 73 | interface TodoEdge { 74 | node: TodoInfo | null; 75 | } 76 | 77 | interface UserInfo { 78 | id: string; 79 | totalCount?: number; 80 | } 81 | 82 | interface TodoInfo { 83 | complete: boolean | null; 84 | id: string; 85 | } 86 | 87 | function commit( 88 | environment: Environment, 89 | complete: boolean, 90 | todos: TodoConnection, 91 | user: UserInfo, 92 | ) { 93 | return commitMutation( 94 | environment, 95 | { 96 | mutation, 97 | optimisticResponse: getOptimisticResponse(complete, todos, user), 98 | variables: { 99 | input: { complete }, 100 | }, 101 | }, 102 | ); 103 | } 104 | 105 | export default { commit }; 106 | -------------------------------------------------------------------------------- /src/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 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 * as React from 'react'; 14 | import * as ReactDOM from 'react-dom'; 15 | 16 | const ENTER_KEY_CODE = 13; 17 | const ESC_KEY_CODE = 27; 18 | 19 | interface Props { 20 | autoFocus: boolean; 21 | className: string; 22 | commitOnBlur?: boolean; 23 | initialValue: string | null; 24 | placeholder?: string; 25 | onCancel?(): void; 26 | onDelete?(): void; 27 | onSave(newText: string): void; 28 | } 29 | 30 | interface State { 31 | isEditing: boolean; 32 | text: string; 33 | } 34 | 35 | export default class TodoTextInput extends React.Component { 36 | public static defaultProps = { 37 | commitOnBlur: false, 38 | }; 39 | public constructor(props: Props) { 40 | super(props); 41 | this.state = { 42 | isEditing: false, 43 | text: this.props.initialValue || '', 44 | }; 45 | } 46 | public componentDidMount() { 47 | if (this.props.autoFocus) { 48 | (ReactDOM.findDOMNode(this) as HTMLInputElement).focus(); 49 | } 50 | } 51 | private commitChanges = () => { 52 | const newText = this.state.text.trim(); 53 | if (this.props.onDelete && newText === '') { 54 | this.props.onDelete(); 55 | } else if (this.props.onCancel && newText === this.props.initialValue) { 56 | this.props.onCancel(); 57 | } else if (newText !== '') { 58 | this.props.onSave(newText); 59 | this.setState({ text: '' }); 60 | } 61 | } 62 | 63 | private handleBlur = () => { 64 | if (this.props.commitOnBlur) { 65 | this.commitChanges(); 66 | } 67 | } 68 | 69 | private handleChange = (e: React.ChangeEvent) => { 70 | this.setState({ text: e.target.value }); 71 | } 72 | 73 | private handleKeyDown = (e: React.KeyboardEvent) => { 74 | if (this.props.onCancel && e.keyCode === ESC_KEY_CODE) { 75 | this.props.onCancel(); 76 | } else if (e.keyCode === ENTER_KEY_CODE) { 77 | this.commitChanges(); 78 | } 79 | } 80 | 81 | public render() { 82 | return ( 83 | 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /config/schemaFileGenerator.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLEnumType, GraphQLObjectType, GraphQLSchema, GraphQLUnionType } from 'graphql'; 2 | import { getInputObjectTypes } from 'graphql-fragment-type-generator/lib/InputObjectTypePrinter'; 3 | import { getTypeBrands } from 'graphql-fragment-type-generator/lib/TypeBrandCreator'; 4 | 5 | export function generateSchemaFile(schema: GraphQLSchema) { 6 | const brands = getTypeBrands(schema); 7 | const inputObjectTypes = getInputObjectTypes(schema); 8 | const typeMap = schema.getTypeMap(); 9 | const extraBrands = []; 10 | const interfacesMap: { [interfaceName: string]: string[] } = {}; 11 | const objects: string[] = []; 12 | const unions: string[] = []; 13 | const enums: string[] = []; 14 | 15 | function addToInterface(interfaceName: string, objectName: string) { 16 | if (interfacesMap[interfaceName] == null) { 17 | interfacesMap[interfaceName] = []; 18 | } 19 | interfacesMap[interfaceName].push(objectName); 20 | } 21 | 22 | const queryType = schema.getQueryType(); 23 | typeMap[queryType.name] = queryType; 24 | extraBrands.push('export enum ' + queryType.name + ' {};'); 25 | 26 | const mutationType = schema.getMutationType(); 27 | if (mutationType != null) { 28 | typeMap[mutationType.name] = mutationType; 29 | extraBrands.push('export enum ' + mutationType.name + ' {};'); 30 | } 31 | 32 | const subscriptionType = schema.getSubscriptionType(); 33 | if (subscriptionType != null) { 34 | typeMap[subscriptionType.name] = subscriptionType; 35 | extraBrands.push('export enum ' + subscriptionType.name + ' {};'); 36 | } 37 | 38 | // tslint:disable-next-line:forin 39 | for (const i in typeMap) { 40 | const typeInfo = typeMap[i]; 41 | if (typeInfo.name.length >= 2 && typeInfo.name[0] === '_' && typeInfo.name[1] === '_') { 42 | continue; 43 | } 44 | // Can't use instanceof, we don't have the ability to require graphql 45 | if (typeInfo instanceof GraphQLObjectType) { 46 | const typeInterfaces = typeInfo.getInterfaces(); 47 | for (const interfaceType of typeInterfaces) { 48 | addToInterface(interfaceType.name, typeInfo.name); 49 | } 50 | } 51 | } 52 | 53 | function generateInterfacePredicates(interfaceName: string, objectTypeNames: string[]) { 54 | const names = objectTypeNames.map((n) => JSON.stringify(n)); 55 | return 'export type ' + interfaceName + 'TypeNames = ' + names.join(' | ') + ';\n'; 56 | } 57 | 58 | const interfaces = []; 59 | 60 | // tslint:disable-next-line:forin 61 | for (const interfaceName in interfacesMap) { 62 | interfaces.push(generateInterfacePredicates(interfaceName, interfacesMap[interfaceName])); 63 | } 64 | 65 | return ( 66 | '// tslint:disable\n' + 67 | brands + '\n' + 68 | extraBrands.join('\n') + '\n\n' + 69 | inputObjectTypes + '\n\n' + 70 | unions.sort().join('\n') + '\n\n' + 71 | interfaces.sort().join('\n') + '\n\n' + 72 | objects.sort().join('\n') + '\n\n' + 73 | enums.sort().join('\n') + 74 | '\n' 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/mutations/RemoveCompletedTodosMutation.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 | commitMutation, 15 | graphql, 16 | } from 'react-relay'; 17 | import { ConnectionHandler, Environment, RecordSourceSelectorProxy } from 'relay-runtime'; 18 | 19 | const mutation = graphql` 20 | mutation RemoveCompletedTodosMutation($input: RemoveCompletedTodosInput!) { 21 | removeCompletedTodos(input: $input) { 22 | deletedTodoIds, 23 | viewer { 24 | completedCount, 25 | totalCount, 26 | }, 27 | } 28 | } 29 | `; 30 | 31 | function sharedUpdater(store: RecordSourceSelectorProxy, user: UserInfo, deletedIDs: string[]) { 32 | const userProxy = store.get(user.id); 33 | if (userProxy == null) { 34 | throw new Error('Could not get user proxy for id: ' + user.id); 35 | } 36 | const conn = ConnectionHandler.getConnection( 37 | userProxy, 38 | 'TodoList_todos', 39 | ); 40 | if (conn == null) { 41 | throw new Error('Could not get connection TodoList_todos for user: ' + user.id); 42 | } 43 | deletedIDs.forEach((deletedID) => 44 | ConnectionHandler.deleteNode(conn, deletedID), 45 | ); 46 | } 47 | 48 | interface TodoConnection { 49 | edges: (TodoEdge | null)[] | null; 50 | } 51 | 52 | interface TodoEdge { 53 | node: TodoInfo | null; 54 | } 55 | 56 | interface UserInfo { 57 | id: string; 58 | totalCount?: number; 59 | } 60 | 61 | interface TodoInfo { 62 | complete: boolean | null; 63 | id: string; 64 | } 65 | 66 | function commit( 67 | environment: Environment, 68 | todos: TodoConnection, 69 | user: UserInfo, 70 | ) { 71 | return commitMutation( 72 | environment, 73 | { 74 | mutation, 75 | optimisticUpdater: (store) => { 76 | if (todos && todos.edges) { 77 | const deletedIDs: string[] = []; 78 | todos.edges.forEach(edge => { 79 | if (edge != null && edge.node != null && edge.node.complete) { 80 | deletedIDs.push(edge.node.id); 81 | } 82 | }); 83 | sharedUpdater(store, user, deletedIDs); 84 | } 85 | }, 86 | updater: (store) => { 87 | const payload = store.getRootField('removeCompletedTodos'); 88 | if (payload == null) { 89 | throw new Error('Could not find root field in payload: removeCompletedTodos'); 90 | } 91 | sharedUpdater(store, user, payload.getValue('deletedTodoIds')); 92 | }, 93 | variables: { 94 | input: {}, 95 | }, 96 | }, 97 | ); 98 | } 99 | 100 | export default { commit }; 101 | -------------------------------------------------------------------------------- /data/schema.graphql: -------------------------------------------------------------------------------- 1 | input AddTodoInput { 2 | text: String! 3 | clientMutationId: String 4 | } 5 | 6 | type AddTodoPayload { 7 | todoEdge: TodoEdge 8 | viewer: User 9 | clientMutationId: String 10 | } 11 | 12 | input ChangeTodoStatusInput { 13 | complete: Boolean! 14 | id: ID! 15 | clientMutationId: String 16 | } 17 | 18 | type ChangeTodoStatusPayload { 19 | todo: Todo 20 | viewer: User 21 | clientMutationId: String 22 | } 23 | 24 | input MarkAllTodosInput { 25 | complete: Boolean! 26 | clientMutationId: String 27 | } 28 | 29 | type MarkAllTodosPayload { 30 | changedTodos: [Todo] 31 | viewer: User 32 | clientMutationId: String 33 | } 34 | 35 | type Mutation { 36 | addTodo(input: AddTodoInput!): AddTodoPayload 37 | changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload 38 | markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload 39 | removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload 40 | removeTodo(input: RemoveTodoInput!): RemoveTodoPayload 41 | renameTodo(input: RenameTodoInput!): RenameTodoPayload 42 | } 43 | 44 | # An object with an ID 45 | interface Node { 46 | # The id of the object. 47 | id: ID! 48 | } 49 | 50 | # Information about pagination in a connection. 51 | type PageInfo { 52 | # When paginating forwards, are there more items? 53 | hasNextPage: Boolean! 54 | 55 | # When paginating backwards, are there more items? 56 | hasPreviousPage: Boolean! 57 | 58 | # When paginating backwards, the cursor to continue. 59 | startCursor: String 60 | 61 | # When paginating forwards, the cursor to continue. 62 | endCursor: String 63 | } 64 | 65 | type Query { 66 | # Fetches an object given its ID 67 | node( 68 | # The ID of an object 69 | id: ID! 70 | ): Node 71 | viewer: User 72 | } 73 | 74 | input RemoveCompletedTodosInput { 75 | clientMutationId: String 76 | } 77 | 78 | type RemoveCompletedTodosPayload { 79 | deletedTodoIds: [String] 80 | viewer: User 81 | clientMutationId: String 82 | } 83 | 84 | input RemoveTodoInput { 85 | id: ID! 86 | clientMutationId: String 87 | } 88 | 89 | type RemoveTodoPayload { 90 | deletedTodoId: ID 91 | viewer: User 92 | clientMutationId: String 93 | } 94 | 95 | input RenameTodoInput { 96 | id: ID! 97 | text: String! 98 | clientMutationId: String 99 | } 100 | 101 | type RenameTodoPayload { 102 | todo: Todo 103 | clientMutationId: String 104 | } 105 | 106 | type Todo implements Node { 107 | complete: Boolean 108 | 109 | # The ID of an object 110 | id: ID! 111 | text: String 112 | } 113 | 114 | # A connection to a list of items. 115 | type TodoConnection { 116 | # Information to aid in pagination. 117 | pageInfo: PageInfo! 118 | 119 | # A list of edges. 120 | edges: [TodoEdge] 121 | } 122 | 123 | # An edge in a connection. 124 | type TodoEdge { 125 | # The item at the end of the edge 126 | node: Todo 127 | 128 | # A cursor for use in pagination 129 | cursor: String! 130 | } 131 | 132 | type User implements Node { 133 | completedCount: Int! 134 | 135 | # The ID of an object 136 | id: ID! 137 | todos(status: String = "any", after: String, first: Int, before: String, last: Int): TodoConnection! 138 | totalCount: Int! 139 | } 140 | -------------------------------------------------------------------------------- /data/database.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 | export class Todo { 14 | public complete: boolean; 15 | public id: string; 16 | public text: string; 17 | } 18 | export class User { 19 | public id: string; 20 | } 21 | 22 | // Mock authenticated ID 23 | const VIEWER_ID = 'me'; 24 | 25 | // Mock user data 26 | const viewer = new User(); 27 | viewer.id = VIEWER_ID; 28 | const usersById = { 29 | [VIEWER_ID]: viewer, 30 | }; 31 | 32 | // Mock todo data 33 | const todosById: { [key: string]: Todo } = {}; 34 | const todoIdsByUser: { [key: string]: string[] } = { 35 | [VIEWER_ID]: [], 36 | }; 37 | let nextTodoId = 0; 38 | addTodo('Taste JavaScript', true); 39 | addTodo('Buy a unicorn', false); 40 | 41 | export function addTodo(text: string, complete: boolean): string { 42 | const todo = new Todo(); 43 | todo.complete = !!complete; 44 | todo.id = `${nextTodoId++}`; 45 | todo.text = text; 46 | todosById[todo.id] = todo; 47 | todoIdsByUser[VIEWER_ID].push(todo.id); 48 | return todo.id; 49 | } 50 | 51 | export function changeTodoStatus(id: string, complete: boolean): void { 52 | const todo = getTodo(id); 53 | todo.complete = complete; 54 | } 55 | 56 | export function getTodo(id: string): Todo { 57 | return todosById[id]; 58 | } 59 | 60 | export function getTodos(status: 'any' | 'completed' | 'not completed' = 'any'): Todo[] { 61 | const todos = todoIdsByUser[VIEWER_ID].map(id => todosById[id]); 62 | if (status === 'any') { 63 | return todos; 64 | } 65 | return todos.filter(todo => todo.complete === (status === 'completed')); 66 | } 67 | 68 | export function getUser(id: string): User { 69 | return usersById[id] || null; 70 | } 71 | 72 | export function getViewer(): User { 73 | return getUser(VIEWER_ID) as User; 74 | } 75 | 76 | export function markAllTodos(complete: boolean): string[] { 77 | const changedTodos: Todo[] = []; 78 | getTodos().forEach(todo => { 79 | if (todo.complete !== complete) { 80 | todo.complete = complete; 81 | changedTodos.push(todo); 82 | } 83 | }); 84 | return changedTodos.map(todo => todo.id); 85 | } 86 | 87 | export function removeTodo(id: string): void { 88 | const todoIndex = todoIdsByUser[VIEWER_ID].indexOf(id); 89 | if (todoIndex !== -1) { 90 | todoIdsByUser[VIEWER_ID].splice(todoIndex, 1); 91 | } 92 | delete todosById[id]; 93 | } 94 | 95 | export function removeCompletedTodos(): string[] { 96 | const todosToRemove = getTodos().filter(todo => todo.complete); 97 | todosToRemove.forEach(todo => removeTodo(todo.id)); 98 | return todosToRemove.map(todo => todo.id); 99 | } 100 | 101 | export function renameTodo(id: string, text: string): void { 102 | const todo = getTodo(id); 103 | todo.text = text; 104 | } 105 | -------------------------------------------------------------------------------- /src/mutations/AddTodoMutation.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 | import { 13 | commitMutation, 14 | graphql, 15 | } from 'react-relay'; 16 | import { ConnectionHandler, Environment, RecordProxy, RecordSourceSelectorProxy } from 'relay-runtime'; 17 | 18 | const mutation = graphql` 19 | mutation AddTodoMutation($input: AddTodoInput!) { 20 | addTodo(input:$input) { 21 | todoEdge { 22 | __typename 23 | cursor 24 | node { 25 | complete 26 | id 27 | text 28 | } 29 | } 30 | viewer { 31 | id 32 | totalCount 33 | } 34 | } 35 | } 36 | `; 37 | 38 | function sharedUpdater(store: RecordSourceSelectorProxy, user: UserInfo, newEdge: RecordProxy) { 39 | const userProxy = store.get(user.id); 40 | if (userProxy == null) { 41 | throw new Error('Could not get proxy for user with id: ' + user.id); 42 | } 43 | const conn = ConnectionHandler.getConnection( 44 | userProxy, 45 | 'TodoList_todos', 46 | ); 47 | if (conn == null) { 48 | throw new Error('Could not get connection for user proxy with id: ' + user.id); 49 | } 50 | ConnectionHandler.insertEdgeAfter(conn, newEdge); 51 | } 52 | 53 | let tempID = 0; 54 | 55 | interface UserInfo { 56 | id: string; 57 | } 58 | 59 | function commit( 60 | environment: Environment, 61 | text: string, 62 | user: UserInfo, 63 | ) { 64 | return commitMutation( 65 | environment, 66 | { 67 | mutation, 68 | optimisticUpdater: (store) => { 69 | const id = 'client:newTodo:' + tempID++; 70 | const node = store.create(id, 'Todo'); 71 | node.setValue(text, 'text'); 72 | node.setValue(id, 'id'); 73 | const newEdge = store.create( 74 | 'client:newEdge:' + tempID++, 75 | 'TodoEdge', 76 | ); 77 | newEdge.setLinkedRecord(node, 'node'); 78 | sharedUpdater(store, user, newEdge); 79 | const userProxy = store.get(user.id); 80 | if (userProxy == null) { 81 | throw new Error('Could not find user with id: ' + user.id); 82 | } 83 | userProxy.setValue( 84 | userProxy.getValue('totalCount') + 1, 85 | 'totalCount', 86 | ); 87 | }, 88 | updater: (store) => { 89 | const payload = store.getRootField('addTodo'); 90 | if (payload == null) { 91 | throw new Error('Could not find root field `addTodo` in response'); 92 | } 93 | const newEdge = payload.getLinkedRecord('todoEdge'); 94 | if (newEdge == null) { 95 | throw new Error('Could not find response property addTodo.todoEdge'); 96 | } 97 | sharedUpdater(store, user, newEdge); 98 | }, 99 | variables: { 100 | input: { 101 | clientMutationId: (tempID++).toString(), 102 | text, 103 | }, 104 | }, 105 | }, 106 | ); 107 | } 108 | 109 | export default { commit }; 110 | -------------------------------------------------------------------------------- /src/components/Todo.tsx: -------------------------------------------------------------------------------- 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 ChangeTodoStatusMutation from '../mutations/ChangeTodoStatusMutation'; 14 | import RemoveTodoMutation from '../mutations/RemoveTodoMutation'; 15 | import RenameTodoMutation from '../mutations/RenameTodoMutation'; 16 | import TodoTextInput from './TodoTextInput'; 17 | 18 | import * as classnames from 'classnames'; 19 | import * as React from 'react'; 20 | import { 21 | createFragmentContainer, 22 | graphql, 23 | } from 'react-relay'; 24 | 25 | interface Props { } 26 | 27 | interface State { 28 | isEditing: boolean; 29 | } 30 | 31 | class Todo extends Relay.TodoFragmentContainer { 32 | public constructor(props: Relay.TodoFragmentContainerProps) { 33 | super(props); 34 | this.state = { 35 | isEditing: false, 36 | }; 37 | } 38 | private handleCompleteChange = (e: React.ChangeEvent) => { 39 | const complete = e.target.checked; 40 | ChangeTodoStatusMutation.commit( 41 | this.props.relay.environment, 42 | complete, 43 | this.props.todo, 44 | this.props.viewer, 45 | ); 46 | } 47 | 48 | private handleDestroyClick = () => { 49 | this.removeTodo(); 50 | } 51 | 52 | private handleLabelDoubleClick = () => { 53 | this.setEditMode(true); 54 | } 55 | 56 | private handleTextInputCancel = () => { 57 | this.setEditMode(false); 58 | } 59 | 60 | private handleTextInputDelete = () => { 61 | this.setEditMode(false); 62 | this.removeTodo(); 63 | } 64 | 65 | private handleTextInputSave = (text: string) => { 66 | this.setEditMode(false); 67 | RenameTodoMutation.commit( 68 | this.props.relay.environment, 69 | text, 70 | this.props.todo, 71 | ); 72 | } 73 | 74 | private removeTodo() { 75 | RemoveTodoMutation.commit( 76 | this.props.relay.environment, 77 | this.props.todo, 78 | this.props.viewer, 79 | ); 80 | } 81 | 82 | private renderTextInput() { 83 | return ( 84 | 93 | ); 94 | } 95 | 96 | private setEditMode = (shouldEdit: boolean) => { 97 | this.setState({ isEditing: shouldEdit }); 98 | } 99 | 100 | public render() { 101 | const className = classnames({ 102 | completed: this.props.todo.complete, 103 | editing: this.state.isEditing, 104 | }); 105 | return ( 106 |
  • 108 |
    109 | 115 | 118 |
    123 | {this.state.isEditing && this.renderTextInput()} 124 |
  • 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> { 338 | const map = new Map(); 339 | 340 | const fragmentDefinitions = new Map(); 341 | const operationDefinitions = []; 342 | 343 | const fragmentVariables = new Map>(); 344 | const containerFragments = new Map>(); 345 | 346 | const addContainerFragment = (containerName: string, fragmentName: string) => { 347 | let fragments = containerFragments.get(containerName); 348 | 349 | if (fragments == null) { 350 | fragments = new Set(); 351 | containerFragments.set(containerName, fragments); 352 | } 353 | fragments.add(fragmentName); 354 | }; 355 | 356 | const getFragmentVariables = ( 357 | variableInfo: Map | null, 358 | fragmentName: string, 359 | typesToImport: Set, 360 | ): string => { 361 | if (variableInfo == null) { 362 | return ''; 363 | } 364 | 365 | const typeLines: string[] = []; 366 | variableInfo.forEach((info, name) => { 367 | typeLines.push(` ${name}: ${printGraphQLType(info.typeNode, typesToImport, info.hasDefaultValue)},\n`); 368 | }); 369 | 370 | return `export type ${fragmentName}_variables = {\n${typeLines.join('')}};\n`; 371 | }; 372 | 373 | for (const document of documents) { 374 | for (const definition of document.definitions) { 375 | if (definition.kind === 'FragmentDefinition') { 376 | const name = definition.name.value; 377 | const ast = getNormalizedAst(schema, { 378 | definitions: [definition], 379 | kind: 'Document', 380 | }); 381 | 382 | const typeDefinitions = getNamedTypeBrandedTypeDefinitions(ast); 383 | 384 | const typesToImport = new Set(typeDefinitions.brandsToImport); 385 | const variableDefinitions = getVariableDefinitions(definition); 386 | const fragmentVariablesCode = getFragmentVariables(variableDefinitions, name, typesToImport); 387 | const parts: string[] = [ 388 | '// tslint:disable\nimport { ' + Array.from(typesToImport.values()).join(', ') + ' } from \'graphql-schema\';\n\n', 389 | 'export type ' + name + ' = ' + typeDefinitions.fragmentTypeText + ';\n\n', 390 | 'export type ' + name + '_brand = ' + typeDefinitions.fragmentTypeBrandText + ';\n' + 391 | fragmentVariablesCode + '\n', 392 | typeDefinitions.exportNamesTypeScriptCode, 393 | ]; 394 | 395 | map.set(name, parts.join('')); 396 | fragmentDefinitions.set(name, stripExportTypeDirectives(definition)); 397 | const containerName = getFragmentNameParts(definition.name.value)[0]; 398 | if (variableDefinitions != null) { 399 | fragmentVariables.set(name, variableDefinitions); 400 | } 401 | addContainerFragment(containerName, name); 402 | } else if (definition.kind === 'OperationDefinition') { 403 | operationDefinitions.push(definition); 404 | } 405 | } 406 | } 407 | const allFragmentDefinitionsNeededForFragment = new Map>(); 408 | 409 | fragmentDefinitions.forEach((fragmentDefinition, name) => { 410 | const referencedFragmentNames = findReferencedFragmentNames(fragmentDefinition, fragmentDefinitions); 411 | 412 | const neededDfinitions = new Set([fragmentDefinition]); 413 | referencedFragmentNames.forEach((fragmentName) => { 414 | neededDfinitions.add(fragmentDefinitions.get(fragmentName) as FragmentDefinitionNode); 415 | }); 416 | allFragmentDefinitionsNeededForFragment.set(fragmentDefinition.name.value, neededDfinitions); 417 | }); 418 | 419 | for (const operationDefinition of operationDefinitions) { 420 | const operationName = operationDefinition.name; 421 | if (operationName == null) { 422 | throw new Error('Unnamed operations are not supported.'); 423 | } 424 | 425 | const neededFragments = new Set(); 426 | const visitedNames = new Set(); 427 | visit(operationDefinition, { 428 | FragmentSpread: (fragmentSpread: FragmentSpreadNode) => { 429 | if (visitedNames.has(fragmentSpread.name.value)) { 430 | return; 431 | } 432 | visitedNames.add(fragmentSpread.name.value); 433 | const fragments = allFragmentDefinitionsNeededForFragment.get(fragmentSpread.name.value); 434 | if (fragments == null) { 435 | throw new Error('Could not find fragment.'); 436 | } 437 | fragments.forEach(fd => neededFragments.add(fd)); 438 | }, 439 | }); 440 | 441 | const document: DocumentNode = { 442 | definitions: [operationDefinition, ...Array.from(neededFragments.values())], 443 | kind: 'Document', 444 | }; 445 | 446 | const operationAst = getNormalizedOperationAst(schema, document, operationDefinition); 447 | const typeDefinitions = getNamedTypeBrandedTypeDefinitions(operationAst); 448 | 449 | const inputVariables = getOperationVariables(operationDefinition); 450 | const name = operationName.value; 451 | 452 | const typesToImport = new Set(typeDefinitions.brandsToImport); 453 | const operationInputVariablesType = getOperationInputType(inputVariables, operationName.value, typesToImport); 454 | const parts: string[] = [ 455 | '// tslint:disable\nimport { ' + Array.from(typesToImport.values()).join(', ') + ' } from \'graphql-schema\';\n\n', 456 | 'export type ' + name + ' = ' + typeDefinitions.fragmentTypeText + ';\n', 457 | operationInputVariablesType + '\n', 458 | typeDefinitions.exportNamesTypeScriptCode, 459 | ]; 460 | 461 | map.set(name, parts.join('')); 462 | } 463 | 464 | await writeRelayGeneratedFile( 465 | schema, 466 | operationDefinitions, 467 | Array.from(fragmentDefinitions.keys()), 468 | (fragmentName: string): boolean => fragmentVariables.has(fragmentName), 469 | ); 470 | return map; 471 | } 472 | 473 | function sortBy( 474 | arr: TEl[], 475 | cmp: (a: TEl) => TCmp, 476 | ): TEl[] { 477 | return arr.sort((a, b) => { 478 | const aCmp = cmp(a); 479 | const bCmp = cmp(b); 480 | 481 | if (aCmp < bCmp) { 482 | return -1; 483 | } else if (bCmp < aCmp) { 484 | return 1; 485 | } 486 | return 0; 487 | }); 488 | } 489 | 490 | function groupBy( 491 | arr: TEl[], 492 | groupSelector: (a: TEl) => TGroupSelector, 493 | elSort?: (el: TEl) => string | number | boolean, 494 | ): { 495 | elements: TEl[]; 496 | key: TGroupSelector; 497 | }[] { 498 | const res = new Map(); 499 | for (const el of arr) { 500 | const group = groupSelector(el); 501 | let groupRes = res.get(group); 502 | if (groupRes == null) { 503 | groupRes = []; 504 | res.set(group, groupRes); 505 | } 506 | groupRes.push(el); 507 | } 508 | return Array.from(res.entries()).map(([group, els]) => ({ 509 | elements: elSort != null ? sortBy(els, elSort) : els, 510 | key: group, 511 | })); 512 | } 513 | 514 | const destFile = path.resolve(__dirname, '..', 'includes', 'relay.d.ts'); 515 | const schemaFile = path.resolve(__dirname, '..', 'types', 'graphql-schema.ts'); 516 | async function writeRelayGeneratedFile( 517 | schema: GraphQLSchema, 518 | operationDefinitions: OperationDefinitionNode[], 519 | fragmentDefinitions: string[], 520 | fragmentHasVariables: (fragmentName: string) => boolean, 521 | ): Promise { 522 | const imports: string[] = []; 523 | const operationDecls: string[] = []; 524 | const containerDecls: string[] = []; 525 | 526 | sortBy(operationDefinitions, (n) => (n.name as NameNode).value).forEach(operationNode => { 527 | const name = (operationNode.name as NameNode).value; 528 | 529 | const hasVariables = operationNode.variableDefinitions != null && operationNode.variableDefinitions.length > 0; 530 | 531 | if (hasVariables) { 532 | imports.push(`import { ${name} as ${name}Payload, ${name}Variables } from 'generated/${name}.graphql';`); 533 | operationDecls.push( 534 | ` export interface ${name} {\n query: ${name}Payload;\n variables: ${name}Variables;\n }`, 535 | ); 536 | } else { 537 | imports.push(`import { ${name} as ${name}Payload } from 'generated/${name}.graphql';`); 538 | operationDecls.push( 539 | ` export interface ${name} {\n query: ${name}Payload;\n variables: {};\n }`, 540 | ); 541 | } 542 | }); 543 | 544 | const containers = groupBy(fragmentDefinitions, v => getFragmentNameParts(v)[0], v => v); 545 | 546 | containers.forEach(containerInfo => { 547 | const containerName = containerInfo.key; 548 | const properties: string[] = []; 549 | const propertiesBrand: string[] = []; 550 | const variableTypes: string[] = []; 551 | containerInfo.elements.forEach((fragmentName) => { 552 | const hasVariables = fragmentHasVariables(fragmentName); 553 | const propertyName = getFragmentNameParts(fragmentName)[1]; 554 | properties.push(`{ ${propertyName}: ${fragmentName} }`); 555 | propertiesBrand.push(`{ ${propertyName}: ${fragmentName}_brand }`); 556 | if (hasVariables) { 557 | variableTypes.push(fragmentName + '_variables'); 558 | imports.push( 559 | `import { ${fragmentName}, ${fragmentName}_brand, ${fragmentName}_variables } ` + 560 | `from 'generated/${fragmentName}.graphql';`, 561 | ); 562 | } else { 563 | imports.push( 564 | `import { ${fragmentName}, ${fragmentName}_brand } from 'generated/${fragmentName}.graphql';`, 565 | ); 566 | } 567 | }); 568 | 569 | let variablesGenericArg = ''; 570 | if (variableTypes.length > 0) { 571 | containerDecls.push( 572 | ` export type ${containerName}Variables = ${variableTypes.join(' & ')};`, 573 | ); 574 | variablesGenericArg = `, ${containerName}Variables`; 575 | } 576 | 577 | // Fragment container 578 | containerDecls.push( 579 | ` export type ${containerName}FragmentContainerProps = ReactRelay.FragmentContainerProps<` + 580 | properties.join(' & '), 581 | `, Props>`, 582 | ); 583 | containerDecls.push( 584 | ` export abstract class ${containerName}FragmentContainer ` + 585 | `extends ReactRelay.FragmentContainer<${properties.join(' & ')}, ${propertiesBrand.join(' & ')}, Props, State> { }`, 586 | ); 587 | 588 | // Refetch container 589 | containerDecls.push( 590 | ` export type ${containerName}RefetchContainerProps` + 591 | `` + 592 | ` = ReactRelay.RefetchContainerProps<` + 593 | properties.join(' & '), 594 | `, Props, RefetchQuery${variablesGenericArg}>`, 595 | ); 596 | containerDecls.push( 597 | ` export abstract class ${containerName}RefetchContainer` + 598 | ` extends ReactRelay.RefetchContainer` + 599 | `<${properties.join(' & ')}, ${propertiesBrand.join(' & ')}, Props, State, RefetchQuery${variablesGenericArg}> { }`, 600 | ); 601 | 602 | // Pagination container 603 | containerDecls.push( 604 | ` export type ${containerName}PaginationContainerProps` + 605 | `` + 606 | ` = ReactRelay.PaginationContainerProps<` + 607 | properties.join(' & '), 608 | `, Props, PaginationQuery${variablesGenericArg}>`, 609 | ); 610 | 611 | containerDecls.push( 612 | ` export abstract class ${containerName}PaginationContainer` + 613 | ` extends ReactRelay.PaginationContainer` + 614 | `<${properties.join(' & ')}, ${propertiesBrand.join(' & ')}, ` + 615 | `Props, State, PaginationQuery${variablesGenericArg}> { }`, 616 | ); 617 | }); 618 | 619 | const content = [ 620 | `import * as ReactRelay from 'react-relay/definitions';`, 621 | `${imports.join('\n')}\n\ndeclare global {\n namespace Relay {`, 622 | ...operationDecls, 623 | '', 624 | ...containerDecls, 625 | ' }', 626 | '}', 627 | '', 628 | ]; 629 | 630 | await writeFile(destFile, content.join('\n')); 631 | await writeFile(schemaFile, generateSchemaFile(schema)); 632 | } 633 | 634 | async function writeFile( 635 | filePath: string, 636 | contents: string, 637 | ): Promise { 638 | if (!await fs.exists(filePath)) { 639 | return fs.writeFile(filePath, contents, 'utf8'); 640 | } 641 | 642 | const existingContents: string = await fs.readFile(filePath, 'utf8'); 643 | 644 | if (existingContents !== contents) { 645 | return fs.writeFile(filePath, contents); 646 | } 647 | } 648 | -------------------------------------------------------------------------------- /types/relay-runtime/definitions.d.ts: -------------------------------------------------------------------------------- 1 | // RelayConcreteNode.js 2 | declare namespace RelayRuntime { 3 | type ConcreteArgument = ConcreteLiteral | ConcreteVariable; 4 | type ConcreteArgumentDefinition = 5 | | ConcreteLocalArgument 6 | | ConcreteRootArgument; 7 | /** 8 | * Represents a single ConcreteRoot along with metadata for processing it at 9 | * runtime. The persisted `id` (or `text`) can be used to fetch the query, 10 | * the `fragment` can be used to read the root data (masking data from child 11 | * fragments), and the `query` can be used to normalize server responses. 12 | * 13 | * NOTE: The use of "batch" in the name is intentional, as this wrapper around 14 | * the ConcreteRoot will provide a place to store multiple concrete nodes that 15 | * are part of the same batch, e.g. in the case of deferred nodes or 16 | * for streaming connections that are represented as distinct concrete roots but 17 | * are still conceptually tied to one source query. 18 | */ 19 | interface ConcreteBatch { 20 | kind: 'Batch'; 21 | fragment: ConcreteFragment; 22 | id: string | null; 23 | metadata: {[key: string]: any}; 24 | name: string; 25 | query: ConcreteRoot; 26 | text: string | null; 27 | } 28 | 29 | interface ConcreteCondition { 30 | kind: 'Condition'; 31 | passingValue: boolean; 32 | condition: string; 33 | selections: Array; 34 | } 35 | type ConcreteField = ConcreteScalarField | ConcreteLinkedField; 36 | interface ConcreteFragment { 37 | argumentDefinitions: Array; 38 | kind: 'Fragment'; 39 | metadata: {[key: string]: any} | null; 40 | name: string; 41 | selections: Array; 42 | type: string; 43 | } 44 | interface ConcreteFragmentSpread { 45 | args: Array | null; 46 | kind: 'FragmentSpread'; 47 | name: string; 48 | } 49 | type ConcreteHandle = ConcreteScalarHandle | ConcreteLinkedHandle; 50 | interface ConcreteRootArgument { 51 | kind: 'RootArgument'; 52 | name: string; 53 | type: string | null; 54 | } 55 | interface ConcreteInlineFragment { 56 | kind: 'InlineFragment'; 57 | selections: Array; 58 | type: string; 59 | } 60 | interface ConcreteLinkedField { 61 | alias: string | null; 62 | args: Array | null; 63 | concreteType: string | null; 64 | kind: 'LinkedField'; 65 | name: string; 66 | plural: boolean; 67 | selections: Array; 68 | storageKey: string | null; 69 | } 70 | interface ConcreteLinkedHandle { 71 | alias: string | null; 72 | args: Array | null; 73 | kind: 'LinkedHandle'; 74 | name: string; 75 | handle: string; 76 | key: string; 77 | filters: Array | null; 78 | } 79 | interface ConcreteLiteral { 80 | kind: 'Literal'; 81 | name: string; 82 | type: string | any; 83 | value: any; 84 | } 85 | interface ConcreteLocalArgument { 86 | defaultValue: any; 87 | kind: 'LocalArgument'; 88 | name: string; 89 | type: string; 90 | } 91 | type ConcreteNode = 92 | | ConcreteCondition 93 | | ConcreteLinkedField 94 | | ConcreteFragment 95 | | ConcreteInlineFragment 96 | | ConcreteRoot; 97 | interface ConcreteRoot { 98 | argumentDefinitions: Array; 99 | kind: 'Root'; 100 | name: string; 101 | operation: 'mutation' | 'query' | 'subscription'; 102 | selections: Array; 103 | } 104 | interface ConcreteScalarField { 105 | alias: string | null; 106 | args: Array | null; 107 | kind: 'ScalarField'; 108 | name: string; 109 | storageKey: string | null; 110 | } 111 | interface ConcreteScalarHandle { 112 | alias: string | null; 113 | args: Array | null; 114 | kind: 'ScalarHandle'; 115 | name: string; 116 | handle: string; 117 | key: string; 118 | filters: Array | null; 119 | } 120 | type ConcreteSelection = 121 | | ConcreteCondition 122 | | ConcreteField 123 | | ConcreteFragmentSpread 124 | | ConcreteHandle 125 | | ConcreteInlineFragment; 126 | 127 | interface ConcreteVariable { 128 | kind: 'Variable'; 129 | name: string; 130 | type: string | null; 131 | variableName: string; 132 | } 133 | type ConcreteSelectableNode = ConcreteFragment | ConcreteRoot; 134 | type GeneratedNode = ConcreteBatch | ConcreteFragment; 135 | } 136 | 137 | // RelayInternalTypes.js 138 | declare namespace RelayRuntime { 139 | type DataID = string; 140 | } 141 | 142 | // RelayTypes.js 143 | declare namespace RelayRuntime { 144 | type Variables = {[name: string]: any}; 145 | } 146 | 147 | // RelayCombinedEnvironmentTypes.js 148 | declare namespace RelayRuntime { 149 | /** 150 | * Settings for how a query response may be cached. 151 | * 152 | * - `force`: causes a query to be issued unconditionally, irrespective of the 153 | * state of any configured response cache. 154 | * - `poll`: causes a query to live update by polling at the specified interval 155 | in milliseconds. (This value will be passed to setTimeout.) 156 | */ 157 | type CacheConfig = { 158 | force?: boolean | null; 159 | poll?: number | null; 160 | }; 161 | 162 | /** 163 | * Represents any resource that must be explicitly disposed of. The most common 164 | * use-case is as a return value for subscriptions, where calling `dispose()` 165 | * would cancel the subscription. 166 | */ 167 | type Disposable = { 168 | dispose(): void; 169 | }; 170 | 171 | /** 172 | * Arbitrary data e.g. received by a container as props. 173 | */ 174 | type Props = {[key: string]: any}; 175 | 176 | /* 177 | * An individual cached graph object. 178 | */ 179 | type Record = {[key: string]: any}; 180 | 181 | /** 182 | * A collection of records keyed by id. 183 | */ 184 | type RecordMap = {[dataID: string]: Record | null}; 185 | 186 | /** 187 | * A selector defines the starting point for a traversal into the graph for the 188 | * purposes of targeting a subgraph. 189 | */ 190 | type CSelector = { 191 | dataID: DataID; 192 | node: TNode; 193 | variables: Variables; 194 | }; 195 | 196 | /** 197 | * A representation of a selector and its results at a particular point in time. 198 | */ 199 | type CSnapshot = CSelector & { 200 | data: SelectorData | null; 201 | seenRecords: RecordMap; 202 | }; 203 | 204 | /** 205 | * The results of a selector given a store/RecordSource. 206 | */ 207 | type SelectorData = {[key: string]: any}; 208 | 209 | /** 210 | * The results of reading the results of a FragmentMap given some input 211 | * `Props`. 212 | */ 213 | type FragmentSpecResults = {[key: string]: any}; 214 | 215 | /** 216 | * A utility for resolving and subscribing to the results of a fragment spec 217 | * (key -> fragment mapping) given some "props" that determine the root ID 218 | * and variables to use when reading each fragment. When props are changed via 219 | * `setProps()`, the resolver will update its results and subscriptions 220 | * accordingly. Internally, the resolver: 221 | * - Converts the fragment map & props map into a map of `Selector`s. 222 | * - Removes any resolvers for any props that became null. 223 | * - Creates resolvers for any props that became non-null. 224 | * - Updates resolvers with the latest props. 225 | */ 226 | interface FragmentSpecResolver { 227 | /** 228 | * Stop watching for changes to the results of the fragments. 229 | */ 230 | dispose(): void; 231 | 232 | /** 233 | * Get the current results. 234 | */ 235 | resolve(): FragmentSpecResults; 236 | 237 | /** 238 | * Update the resolver with new inputs. Call `resolve()` to get the updated 239 | * results. 240 | */ 241 | setProps(props: Props): void; 242 | 243 | /** 244 | * Override the variables used to read the results of the fragments. Call 245 | * `resolve()` to get the updated results. 246 | */ 247 | setVariables(variables: Variables): void; 248 | } 249 | 250 | type CFragmentMap = {[key: string]: TFragment}; 251 | 252 | /** 253 | * An operation selector describes a specific instance of a GraphQL operation 254 | * with variables applied. 255 | * 256 | * - `root`: a selector intended for processing server results or retaining 257 | * response data in the store. 258 | * - `fragment`: a selector intended for use in reading or subscribing to 259 | * the results of the the operation. 260 | */ 261 | type COperationSelector = { 262 | fragment: CSelector; 263 | node: TOperation; 264 | root: CSelector; 265 | variables: Variables; 266 | }; 267 | 268 | /** 269 | * The public API of Relay core. Represents an encapsulated environment with its 270 | * own in-memory cache. 271 | */ 272 | interface CEnvironment< 273 | TEnvironment, 274 | TFragment, 275 | TGraphQLTaggedNode, 276 | TNode, 277 | TOperation, 278 | TPayload, 279 | > { 280 | /** 281 | * Read the results of a selector from in-memory records in the store. 282 | */ 283 | lookup(selector: CSelector): CSnapshot; 284 | 285 | /** 286 | * Subscribe to changes to the results of a selector. The callback is called 287 | * when data has been committed to the store that would cause the results of 288 | * the snapshot's selector to change. 289 | */ 290 | subscribe( 291 | snapshot: CSnapshot, 292 | callback: (snapshot: CSnapshot) => void, 293 | ): Disposable; 294 | 295 | /** 296 | * Ensure that all the records necessary to fulfill the given selector are 297 | * retained in-memory. The records will not be eligible for garbage collection 298 | * until the returned reference is disposed. 299 | * 300 | * Note: This is a no-op in the classic core. 301 | */ 302 | retain(selector: CSelector): Disposable; 303 | 304 | /** 305 | * Send a query to the server with request/response semantics: the query will 306 | * either complete successfully (calling `onNext` and `onCompleted`) or fail 307 | * (calling `onError`). 308 | * 309 | * Note: Most applications should use `streamQuery` in order to 310 | * optionally receive updated information over time, should that feature be 311 | * supported by the network/server. A good rule of thumb is to use this method 312 | * if you would otherwise immediately dispose the `streamQuery()` 313 | * after receving the first `onNext` result. 314 | */ 315 | sendQuery(config: { 316 | cacheConfig?: CacheConfig | null; 317 | onCompleted?: (() => void) | null; 318 | onError?: ((error: Error) => void) | null; 319 | onNext?: ((payload: TPayload) => void) | null; 320 | operation: COperationSelector; 321 | }): Disposable, 322 | 323 | /** 324 | * Send a query to the server with request/subscription semantics: one or more 325 | * responses may be returned (via `onNext`) over time followed by either 326 | * the request completing (`onCompleted`) or an error (`onError`). 327 | * 328 | * Networks/servers that support subscriptions may choose to hold the 329 | * subscription open indefinitely such that `onCompleted` is not called. 330 | */ 331 | streamQuery(config: { 332 | cacheConfig?: CacheConfig | null; 333 | onCompleted?: (() => void) | null; 334 | onError?: ((error: Error) => void) | null; 335 | onNext?: ((payload: TPayload) => void) | null; 336 | operation: COperationSelector; 337 | }): Disposable, 338 | } 339 | 340 | interface CUnstableEnvironmentCore< 341 | TEnvironment, 342 | TFragment, 343 | TGraphQLTaggedNode, 344 | TNode, 345 | TOperation, 346 | > { 347 | /** 348 | * Create an instance of a FragmentSpecResolver. 349 | * 350 | * TODO: The FragmentSpecResolver *can* be implemented via the other methods 351 | * defined here, so this could be moved out of core. It's convenient to have 352 | * separate implementations until the experimental core is in OSS. 353 | */ 354 | createFragmentSpecResolver: ( 355 | context: CRelayContext, 356 | containerName: string, 357 | fragments: CFragmentMap, 358 | props: Props, 359 | callback: () => void, 360 | ) => FragmentSpecResolver; 361 | 362 | /** 363 | * Creates an instance of an OperationSelector given an operation definition 364 | * (see `getOperation`) and the variables to apply. The input variables are 365 | * filtered to exclude variables that do not matche defined arguments on the 366 | * operation, and default values are populated for null values. 367 | */ 368 | createOperationSelector: ( 369 | operation: TOperation, 370 | variables: Variables, 371 | ) => COperationSelector; 372 | 373 | /** 374 | * Given a graphql`...` tagged template, extract a fragment definition usable 375 | * by this version of Relay core. Throws if the value is not a fragment. 376 | */ 377 | getFragment: (node: TGraphQLTaggedNode) => TFragment; 378 | 379 | /** 380 | * Given a graphql`...` tagged template, extract an operation definition 381 | * usable by this version of Relay core. Throws if the value is not an 382 | * operation. 383 | */ 384 | getOperation: (node: TGraphQLTaggedNode) => TOperation; 385 | 386 | /** 387 | * Determine if two selectors are equal (represent the same selection). Note 388 | * that this function returns `false` when the two queries/fragments are 389 | * different objects, even if they select the same fields. 390 | */ 391 | areEqualSelectors: (a: CSelector, b: CSelector) => boolean; 392 | 393 | /** 394 | * Given the result `item` from a parent that fetched `fragment`, creates a 395 | * selector that can be used to read the results of that fragment for that item. 396 | * 397 | * Example: 398 | * 399 | * Given two fragments as follows: 400 | * 401 | * ``` 402 | * fragment Parent on User { 403 | * id 404 | * ...Child 405 | * } 406 | * fragment Child on User { 407 | * name 408 | * } 409 | * ``` 410 | * 411 | * And given some object `parent` that is the results of `Parent` for id "4", 412 | * the results of `Child` can be accessed by first getting a selector and then 413 | * using that selector to `lookup()` the results against the environment: 414 | * 415 | * ``` 416 | * const childSelector = getSelector(queryVariables, Child, parent); 417 | * const childData = environment.lookup(childSelector).data; 418 | * ``` 419 | */ 420 | getSelector: ( 421 | operationVariables: Variables, 422 | fragment: TFragment, 423 | prop: any, 424 | ) => CSelector | null; 425 | 426 | /** 427 | * Given the result `items` from a parent that fetched `fragment`, creates a 428 | * selector that can be used to read the results of that fragment on those 429 | * items. This is similar to `getSelector` but for "plural" fragments that 430 | * expect an array of results and therefore return an array of selectors. 431 | */ 432 | getSelectorList: ( 433 | operationVariables: Variables, 434 | fragment: TFragment, 435 | props: Array, 436 | ) => Array> | null; 437 | 438 | /** 439 | * Given a mapping of keys -> results and a mapping of keys -> fragments, 440 | * extracts the selectors for those fragments from the results. 441 | * 442 | * The canonical use-case for this function are Relay Containers, which 443 | * use this function to convert (props, fragments) into selectors so that they 444 | * can read the results to pass to the inner component. 445 | */ 446 | getSelectorsFromObject: ( 447 | operationVariables: Variables, 448 | fragments: CFragmentMap, 449 | props: Props, 450 | ) => {[key: string]: (CSelector | Array>) | null}; 451 | 452 | /** 453 | * Given a mapping of keys -> results and a mapping of keys -> fragments, 454 | * extracts a mapping of keys -> id(s) of the results. 455 | * 456 | * Similar to `getSelectorsFromObject()`, this function can be useful in 457 | * determining the "identity" of the props passed to a component. 458 | */ 459 | getDataIDsFromObject: ( 460 | fragments: CFragmentMap, 461 | props: Props, 462 | ) => {[key: string]: (DataID | Array) | null}; 463 | 464 | /** 465 | * Given a mapping of keys -> results and a mapping of keys -> fragments, 466 | * extracts the merged variables that would be in scope for those 467 | * fragments/results. 468 | * 469 | * This can be useful in determing what varaibles were used to fetch the data 470 | * for a Relay container, for example. 471 | */ 472 | getVariablesFromObject: ( 473 | operationVariables: Variables, 474 | fragments: CFragmentMap, 475 | props: Props, 476 | ) => Variables; 477 | } 478 | 479 | /** 480 | * The type of the `relay` property set on React context by the React/Relay 481 | * integration layer (e.g. QueryRenderer, FragmentContainer, etc). 482 | */ 483 | type CRelayContext = { 484 | environment: TEnvironment, 485 | variables: Variables, 486 | }; 487 | } 488 | 489 | // RelayModernGraphQLTag.js 490 | declare namespace RelayRuntime { 491 | type GraphQLTaggedNode = 492 | | (() => ConcreteFragment | ConcreteBatch) 493 | | { 494 | modern: () => ConcreteFragment | ConcreteBatch; 495 | classic: any; 496 | }; 497 | } 498 | 499 | // RelayRecordState.js 500 | declare namespace RelayRuntime { 501 | type RecordState = 'EXISTENT' | 'NONEXISTENT' | 'UNKNOWN'; 502 | } 503 | 504 | // RelayNetworkTypes.js 505 | declare namespace RelayRuntime { 506 | /** 507 | * A cache for saving respones to queries (by id) and variables. 508 | */ 509 | interface ResponseCache { 510 | get(id: string, variables: Variables): QueryPayload | null; 511 | set(id: string, variables: Variables, payload: QueryPayload): void; 512 | } 513 | 514 | /** 515 | * An interface for fetching the data for one or more (possibly interdependent) 516 | * queries. 517 | */ 518 | interface Network { 519 | fetch: FetchFunction; 520 | request: RequestResponseFunction; 521 | requestStream: RequestStreamFunction; 522 | } 523 | 524 | type PayloadData = {[key: string]: any}; 525 | 526 | type PayloadError = { 527 | message: string; 528 | locations?: Array<{ 529 | line: number; 530 | column: number; 531 | }>; 532 | }; 533 | 534 | /** 535 | * The shape of a GraphQL response as dictated by the 536 | * [spec](http://facebook.github.io/graphql/#sec-Response) 537 | */ 538 | interface QueryPayload { 539 | data?: PayloadData | null; 540 | errors?: Array; 541 | } 542 | 543 | /** 544 | * The shape of data that is returned by the Relay network layer for a given 545 | * query. 546 | */ 547 | interface RelayResponsePayload { 548 | fieldPayloads?: Array | null; 549 | source: MutableRecordSource; 550 | errors: Array | null; 551 | } 552 | 553 | type PromiseOrValue = Promise | T | Error; 554 | 555 | /** 556 | * A function that executes a GraphQL operation with request/response semantics, 557 | * with exactly one raw server response returned 558 | */ 559 | type FetchFunction = ( 560 | operation: ConcreteBatch, 561 | variables: Variables, 562 | cacheConfig: CacheConfig | null, 563 | uploadables?: UploadableMap, 564 | ) => PromiseOrValue; 565 | 566 | /** 567 | * A function that executes a GraphQL operation with request/subscription 568 | * semantics, returning one or more raw server responses over time. 569 | */ 570 | type SubscribeFunction = ( 571 | operation: ConcreteBatch, 572 | variables: Variables, 573 | cacheConfig: CacheConfig | null, 574 | observer: Observer, 575 | ) => Disposable; 576 | 577 | /** 578 | * A function that executes a GraphQL operation with request/subscription 579 | * semantics, returning one or more responses over time that include the 580 | * initial result and optional updates e.g. as the results of the operation 581 | * change. 582 | */ 583 | type RequestStreamFunction = ( 584 | operation: ConcreteBatch, 585 | variables: Variables, 586 | cacheConfig: CacheConfig | null, 587 | observer: Observer, 588 | ) => Disposable; 589 | 590 | /** 591 | * A function that executes a GraphQL operation with request/response semantics, 592 | * with exactly one response returned. 593 | */ 594 | type RequestResponseFunction = ( 595 | operation: ConcreteBatch, 596 | variables: Variables, 597 | cacheConfig?: CacheConfig | null, 598 | uploadables?: UploadableMap, 599 | ) => PromiseOrValue; 600 | 601 | type Uploadable = File | Blob; 602 | // $FlowFixMe this is compatible with classic api see D4658012 603 | type UploadableMap = {[key: string]: Uploadable}; 604 | } 605 | 606 | // RelayStoreTypes.js 607 | declare namespace RelayRuntime { 608 | type FragmentMap = CFragmentMap; 609 | type OperationSelector = COperationSelector; 610 | type RelayContext = CRelayContext; 611 | type Selector = CSelector; 612 | type Snapshot = CSnapshot; 613 | type UnstableEnvironmentCore = CUnstableEnvironmentCore< 614 | Environment, 615 | ConcreteFragment, 616 | GraphQLTaggedNode, 617 | ConcreteSelectableNode, 618 | ConcreteBatch 619 | >; 620 | 621 | /** 622 | * A read-only interface for accessing cached graph data. 623 | */ 624 | interface RecordSource { 625 | get(dataID: DataID): Record | null; 626 | getRecordIDs(): Array; 627 | getStatus(dataID: DataID): RecordState; 628 | has(dataID: DataID): boolean; 629 | load( 630 | dataID: DataID, 631 | callback: (error: Error | null, record: Record | null) => void, 632 | ): void; 633 | size(): number; 634 | } 635 | 636 | /** 637 | * A read/write interface for accessing and updating graph data. 638 | */ 639 | interface MutableRecordSource extends RecordSource { 640 | clear(): void, 641 | delete(dataID: DataID): void, 642 | remove(dataID: DataID): void, 643 | set(dataID: DataID, record: Record): void, 644 | } 645 | 646 | /** 647 | * An interface for keeping multiple views of data consistent across an 648 | * application. 649 | */ 650 | interface Store { 651 | /** 652 | * Get a read-only view of the store's internal RecordSource. 653 | */ 654 | getSource(): RecordSource, 655 | 656 | /** 657 | * Determine if the selector can be resolved with data in the store (i.e. no 658 | * fields are missing). 659 | */ 660 | check(selector: Selector): boolean, 661 | 662 | /** 663 | * Read the results of a selector from in-memory records in the store. 664 | */ 665 | lookup(selector: Selector): Snapshot, 666 | 667 | /** 668 | * Notify subscribers (see `subscribe`) of any data that was published 669 | * (`publish()`) since the last time `notify` was called. 670 | */ 671 | notify(): void, 672 | 673 | /** 674 | * Publish new information (e.g. from the network) to the store, updating its 675 | * internal record source. Subscribers are not immediately notified - this 676 | * occurs when `notify()` is called. 677 | */ 678 | publish(source: RecordSource): void, 679 | 680 | /** 681 | * Attempts to load all the records necessary to fulfill the selector into the 682 | * target record source. 683 | */ 684 | resolve( 685 | target: MutableRecordSource, 686 | selector: Selector, 687 | callback: AsyncLoadCallback, 688 | ): void, 689 | 690 | /** 691 | * Ensure that all the records necessary to fulfill the given selector are 692 | * retained in-memory. The records will not be eligible for garbage collection 693 | * until the returned reference is disposed. 694 | */ 695 | retain(selector: Selector): Disposable, 696 | 697 | /** 698 | * Subscribe to changes to the results of a selector. The callback is called 699 | * when `notify()` is called *and* records have been published that affect the 700 | * selector results relative to the last `notify()`. 701 | */ 702 | subscribe( 703 | snapshot: Snapshot, 704 | callback: (snapshot: Snapshot) => void, 705 | ): Disposable, 706 | } 707 | 708 | /** 709 | * An interface for imperatively getting/setting properties of a `Record`. This interface 710 | * is designed to allow the appearance of direct Record manipulation while 711 | * allowing different implementations that may e.g. create a changeset of 712 | * the modifications. 713 | */ 714 | interface RecordProxy { 715 | copyFieldsFrom(source: RecordProxy): void, 716 | getDataID(): DataID, 717 | getLinkedRecord(name: string, args?: Variables | null): RecordProxy | null; 718 | getLinkedRecords(name: string, args?: Variables | null): Array | null; 719 | getOrCreateLinkedRecord( 720 | name: string, 721 | typeName: string, 722 | args?: Variables | null, 723 | ): RecordProxy; 724 | getType(): string; 725 | getValue(name: string, args?: Variables | null): any; 726 | setLinkedRecord( 727 | record: RecordProxy, 728 | name: string, 729 | args?: Variables | null, 730 | ): RecordProxy; 731 | setLinkedRecords( 732 | records: Array, 733 | name: string, 734 | args?: Variables | null, 735 | ): RecordProxy, 736 | setValue(value: any, name: string, args?: Variables | null): RecordProxy; 737 | } 738 | 739 | /** 740 | * An interface for imperatively getting/setting properties of a `RecordSource`. This interface 741 | * is designed to allow the appearance of direct RecordSource manipulation while 742 | * allowing different implementations that may e.g. create a changeset of 743 | * the modifications. 744 | */ 745 | interface RecordSourceProxy { 746 | create(dataID: DataID, typeName: string): RecordProxy; 747 | delete(dataID: DataID): void; 748 | get(dataID: DataID): RecordProxy | null; 749 | getRoot(): RecordProxy; 750 | } 751 | 752 | /** 753 | * Extends the RecordSourceProxy interface with methods for accessing the root 754 | * fields of a Selector. 755 | */ 756 | interface RecordSourceSelectorProxy { 757 | create(dataID: DataID, typeName: string): RecordProxy; 758 | delete(dataID: DataID): void; 759 | get(dataID: DataID): RecordProxy | null; 760 | getRoot(): RecordProxy; 761 | getRootField(fieldName: string): RecordProxy | null; 762 | getPluralRootField(fieldName: string): Array | null, 763 | } 764 | 765 | /** 766 | * The public API of Relay core. Represents an encapsulated environment with its 767 | * own in-memory cache. 768 | */ 769 | interface Environment 770 | extends CEnvironment< 771 | Environment, 772 | ConcreteFragment, 773 | GraphQLTaggedNode, 774 | ConcreteSelectableNode, 775 | ConcreteBatch, 776 | RelayResponsePayload 777 | > { 778 | /** 779 | * Apply an optimistic update to the environment. The mutation can be reverted 780 | * by calling `dispose()` on the returned value. 781 | */ 782 | applyUpdate(updater: StoreUpdater): Disposable; 783 | 784 | /** 785 | * Determine if the selector can be resolved with data in the store (i.e. no 786 | * fields are missing). 787 | * 788 | * Note that this operation effectively "executes" the selector against the 789 | * cache and therefore takes time proportional to the size/complexity of the 790 | * selector. 791 | */ 792 | check(selector: Selector): boolean; 793 | 794 | /** 795 | * Commit an updater to the environment. This mutation cannot be reverted and 796 | * should therefore not be used for optimistic updates. This is mainly 797 | * intended for updating fields from client schema extensions. 798 | */ 799 | commitUpdate(updater: StoreUpdater): void; 800 | 801 | /** 802 | * Commit a payload to the environment using the given operation selector. 803 | */ 804 | commitPayload( 805 | operationSelector: OperationSelector, 806 | payload: PayloadData, 807 | ): void; 808 | 809 | /** 810 | * Get the environment's internal Store. 811 | */ 812 | getStore(): Store, 813 | 814 | /** 815 | * Send a mutation to the server. If provided, the optimistic updater is 816 | * executed immediately and reverted atomically when the server payload is 817 | * committed. 818 | */ 819 | sendMutation(config: { 820 | onCompleted?: ((errors: Array | null) => void) | null; 821 | onError?: ((error: Error) => void) | null; 822 | operation: OperationSelector; 823 | optimisticResponse?: Object; 824 | optimisticUpdater?: SelectorStoreUpdater | null; 825 | updater?: SelectorStoreUpdater | null; 826 | uploadables?: UploadableMap; 827 | }): Disposable; 828 | 829 | /** 830 | * Send a (GraphQL) subscription to the server. Whenever there is a push from 831 | * the server, commit the update to the environment. 832 | */ 833 | sendSubscription(config: { 834 | onCompleted?: ((errors: Array | null) => void) | null, 835 | onNext?: ((payload: RelayResponsePayload) => void) | null, 836 | onError?: ((error: Error) => void) | null, 837 | operation: OperationSelector, 838 | updater?: SelectorStoreUpdater | null, 839 | }): Disposable, 840 | } 841 | 842 | type Observer = { 843 | onCompleted?: (() => void) | null; 844 | onError?: ((error: Error) => void) | null; 845 | onNext?: ((data: T) => void) | null; 846 | }; 847 | 848 | /** 849 | * The results of reading data for a fragment. This is similar to a `Selector`, 850 | * but references the (fragment) node by name rather than by value. 851 | */ 852 | type FragmentPointer = { 853 | __id: DataID; 854 | __fragments: {[fragmentName: string]: Variables}; 855 | }; 856 | 857 | /** 858 | * A callback for resolving a Selector from a source. 859 | */ 860 | type AsyncLoadCallback = (loadingState: LoadingState) => void; 861 | interface LoadingState { 862 | status: 'aborted' | 'complete' | 'error' | 'missing'; 863 | error?: Error; 864 | } 865 | 866 | /** 867 | * A map of records affected by an update operation. 868 | */ 869 | type UpdatedRecords = {[dataID: string]: boolean}; 870 | 871 | /** 872 | * A function that updates a store (via a proxy) given the results of a "handle" 873 | * field payload. 874 | */ 875 | type Handler = { 876 | update: (store: RecordSourceProxy, fieldPayload: HandleFieldPayload) => void; 877 | }; 878 | 879 | /** 880 | * A payload that is used to initialize or update a "handle" field with 881 | * information from the server. 882 | */ 883 | interface HandleFieldPayload { 884 | // The arguments that were fetched. 885 | args: Variables; 886 | // The __id of the record containing the source/handle field. 887 | dataID: DataID; 888 | // The (storage) key at which the original server data was written. 889 | fieldKey: string; 890 | // The name of the handle. 891 | handle: string; 892 | // The (storage) key at which the handle's data should be written by the 893 | // handler. 894 | handleKey: string; 895 | } 896 | 897 | /** 898 | * A function that receives a proxy over the store and may trigger side-effects 899 | * (indirectly) by calling `set*` methods on the store or its record proxies. 900 | */ 901 | type StoreUpdater = (store: RecordSourceProxy) => void; 902 | 903 | /** 904 | * Similar to StoreUpdater, but accepts a proxy tied to a specific selector in 905 | * order to easily access the root fields of a query/mutation as well as a 906 | * second argument of the response object of the mutation. 907 | */ 908 | type SelectorStoreUpdater = ( 909 | store: RecordSourceSelectorProxy, 910 | // Actually RelayCombinedEnvironmentTypes#SelectorData, but mixed is 911 | // inconvenient to access deeply in product code. 912 | data: SelectorData, 913 | ) => void; 914 | 915 | } 916 | 917 | // RelayDefaultHandlerProvider.js 918 | declare namespace RelayRuntime { 919 | type HandlerProvider = (name: string) => Handler | null; 920 | } 921 | 922 | // RelayModernEnvironment.js 923 | declare namespace RelayRuntime { 924 | type EnvironmentConfig = { 925 | handlerProvider?: HandlerProvider, 926 | network: Network, 927 | store: Store, 928 | }; 929 | } 930 | 931 | // RelayTypes.js 932 | declare namespace RelayRuntime { 933 | type RangeOperations = 'prepend' | 'append' | 'ignore' | 'remove' | 'refetch'; 934 | type RangeBehaviorsObject = { 935 | [key: string]: RangeOperations; 936 | }; 937 | type RangeBehaviors = RangeBehaviorsFunction | RangeBehaviorsObject; 938 | type RangeBehaviorsFunction = (connectionArgs: { 939 | [argName: string]: CallValue, 940 | }) => RangeOperations; 941 | 942 | type CallValue = 943 | | boolean 944 | | number 945 | | string 946 | | object 947 | | any[] 948 | | null; 949 | // Containers 950 | type RelayMutationConfig = 951 | | { 952 | type: 'FIELDS_CHANGE', 953 | fieldIDs: {[fieldName: string]: DataID | Array}, 954 | } 955 | | { 956 | type: 'RANGE_ADD', 957 | parentName?: string, 958 | parentID?: string, 959 | connectionInfo?: Array<{ 960 | key: string, 961 | filters?: Variables, 962 | rangeBehavior: string, 963 | }>, 964 | connectionName?: string, 965 | edgeName: string, 966 | rangeBehaviors?: RangeBehaviors, 967 | } 968 | | { 969 | type: 'NODE_DELETE', 970 | parentName?: string, 971 | parentID?: string, 972 | connectionName?: string, 973 | deletedIDFieldName: string, 974 | } 975 | | { 976 | type: 'RANGE_DELETE', 977 | parentName?: string, 978 | parentID?: string, 979 | connectionKeys?: Array<{ 980 | key: string, 981 | filters?: Variables, 982 | }>, 983 | connectionName?: string, 984 | deletedIDFieldName: string | Array, 985 | pathToConnection: Array, 986 | }; 987 | } 988 | 989 | export as namespace RelayRuntime; 990 | export = RelayRuntime; 991 | --------------------------------------------------------------------------------