From 8ee56e94d90c1df6862b54073ed0cf3b99330ead Mon Sep 17 00:00:00 2001 From: James Baxley Date: Mon, 17 Jul 2017 21:59:00 -0400 Subject: [PATCH] fix typescript usage (#862) * fix typescript usage * updated changelog * add issue information to changelog * Version bump * update dangerfile --- Changelog.md | 7 +- dangerfile.ts | 13 ++- package.json | 9 +- src/browser.ts | 9 +- src/graphql.tsx | 194 ++++--------------------------------------- src/queryRecycler.ts | 92 ++++++++++++++++++++ src/types.ts | 98 ++++++++++++++++++++++ src/withApollo.tsx | 2 +- test/typescript.ts | 57 +++++++++++++ 9 files changed, 283 insertions(+), 198 deletions(-) create mode 100644 src/queryRecycler.ts create mode 100644 src/types.ts create mode 100644 test/typescript.ts diff --git a/Changelog.md b/Changelog.md index b312116899..19a9722f03 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,11 @@ # Change log ### vNext -- Fix: Fix issue around hoisting non react statics for RN -- Fix: Fix issue where options was called even though skip was present + +### 1.4.4 +- Fix: Fix issue around hoisting non react statics for RN [#859](https://github.com/apollographql/react-apollo/pull/859) +- Fix: Fix issue where options was called even though skip was present [#859](https://github.com/apollographql/react-apollo/pull/859) +- Improvement: Allow for better typescript usage with improved types [#862](https://github.com/apollographql/react-apollo/pull/862) ### 1.4.3 - Feature: You can now supply a client in options object passed to the `graphql` high oder component. [PR #729](https://github.com/apollographql/react-apollo/pull/729) diff --git a/dangerfile.ts b/dangerfile.ts index b87df74a77..2468444fff 100644 --- a/dangerfile.ts +++ b/dangerfile.ts @@ -1,4 +1,4 @@ -import { danger, fail, warn, message } from 'danger'; +// Removed import import { includes } from 'lodash'; import * as fs from 'fs'; @@ -17,9 +17,11 @@ const filesOnly = (file: string) => // Custom subsets of known files const modifiedAppFiles = modified - .filter(p => includes(p, 'src/') || includes(p, 'test/')) + .filter(p => includes(p, 'src/')) .filter(p => filesOnly(p) && typescriptOnly(p)); +const modifiedTestFiles = modified.filter(p => includes(p, 'test/')); + // Takes a list of file paths, and converts it into clickable links const linkableFiles = paths => { const repoURL = danger.github.pr.head.repo.html_url; @@ -70,10 +72,7 @@ if (pr.body.length === 0) { const hasAppChanges = modifiedAppFiles.length > 0; -const testChanges = modifiedAppFiles.filter(filepath => - filepath.includes('test'), -); -const hasTestChanges = testChanges.length > 0; +const hasTestChanges = modifiedTestFiles.length > 0; // Warn when there is a big PR const bigPRThreshold = 500; @@ -89,7 +88,7 @@ if (hasAppChanges && !hasTestChanges) { } // Be careful of leaving testing shortcuts in the codebase -const onlyTestFiles = testChanges.filter(x => { +const onlyTestFiles = modifiedTestFiles.filter(x => { const content = fs.readFileSync(x).toString(); return ( content.includes('it.only') || diff --git a/package.json b/package.json index 48b0995509..c65e801f31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-apollo", - "version": "1.4.3", + "version": "1.4.4", "description": "React data container for Apollo Client", "main": "lib/react-apollo.umd.js", "module": "./lib/index.js", @@ -15,7 +15,7 @@ "test-watch": "jest --watch", "posttest": "npm run lint", "filesize": "npm run compile:browser && bundlesize", - "flow-check": "flow check", + "type-check": "tsc ./test/typescript.ts --noEmit --jsx react --noEmitOnError --lib es6,dom --experimentalDecorators && flow check", "compile": "tsc", "bundle": "rollup -c && rollup -c rollup.browser.config.js && rollup -c rollup.test-utils.config.js && cp ./index.js.flow ./lib", "compile:browser": "rm -rf ./dist && mkdir ./dist && browserify ./lib/react-apollo.browser.umd.js --i graphql-tag --i react --i apollo-client -o=./dist/index.js && npm run minify:browser && npm run compress:browser", @@ -52,7 +52,7 @@ "npm", "react" ], - "author": "James Baxley ", + "author": "James Baxley ", "babel": { "presets": [ "react-native" @@ -72,7 +72,8 @@ ], "modulePathIgnorePatterns": [ "/examples", - "/test/flow.js" + "/test/flow.js", + "/test/typescript.ts" ], "testRegex": "(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$", "collectCoverage": true diff --git a/src/browser.ts b/src/browser.ts index 4aa7302630..39d760c2b2 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,14 +1,15 @@ export { default as ApolloProvider } from './ApolloProvider'; -export { - default as graphql, +export { default as graphql } from './graphql'; +import { MutationOpts, QueryOpts, QueryProps, + NamedProps, MutationFunc, OptionProps, - DefaultChildProps, + ChildProps, OperationOption, -} from './graphql'; +} from './types'; export { withApollo } from './withApollo'; // expose easy way to join queries from redux diff --git a/src/graphql.tsx b/src/graphql.tsx index 8d83701e2c..5f173b9c37 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -1,9 +1,4 @@ -import { - Component, - createElement, - ComponentClass, - StatelessComponent, -} from 'react'; +import { Component, createElement } from 'react'; import * as PropTypes from 'prop-types'; const pick = require('lodash.pick'); @@ -16,98 +11,26 @@ const hoistNonReactStatics = require('hoist-non-react-statics'); import ApolloClient, { ObservableQuery, - MutationQueryReducersMap, Subscription, ApolloStore, ApolloQueryResult, - ApolloError, - FetchPolicy, - FetchMoreOptions, - UpdateQueryOptions, - FetchMoreQueryOptions, - SubscribeToMoreOptions, } from 'apollo-client'; -import { PureQueryOptions } from 'apollo-client/core/types'; -import { MutationUpdaterFn } from 'apollo-client/core/watchQueryOptions'; - -import { ExecutionResult, DocumentNode } from 'graphql'; import { parser, DocumentType } from './parser'; +import { ObservableQueryRecycler } from './queryRecycler'; -export interface MutationOpts { - variables?: Object; - optimisticResponse?: Object; - updateQueries?: MutationQueryReducersMap; - refetchQueries?: string[] | PureQueryOptions[]; - update?: MutationUpdaterFn; - client?: ApolloClient; -} - -export interface QueryOpts { - ssr?: boolean; - variables?: { [key: string]: any }; - fetchPolicy?: FetchPolicy; - pollInterval?: number; - client?: ApolloClient; - // deprecated - skip?: boolean; -} - -export interface QueryProps { - error?: ApolloError; - networkStatus: number; - loading: boolean; - variables: { - [variable: string]: any; - }; - fetchMore: ( - fetchMoreOptions: FetchMoreQueryOptions & FetchMoreOptions, - ) => Promise>; - refetch: (variables?: any) => Promise>; - startPolling: (pollInterval: number) => void; - stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: ( - mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any, - ) => void; -} - -export type MutationFunc = ( - opts: MutationOpts, -) => Promise>; - -export interface OptionProps { - ownProps: TProps; - data?: QueryProps & TResult; - mutate?: MutationFunc; -} - -export type DefaultChildProps = P & { - data?: QueryProps & R; - mutate?: MutationFunc; -}; - -export interface OperationOption { - options?: - | QueryOpts - | MutationOpts - | ((props: TProps) => QueryOpts | MutationOpts); - props?: (props: OptionProps) => any; - skip?: boolean | ((props: any) => boolean); - name?: string; - withRef?: boolean; - shouldResubscribe?: (props: TProps, nextProps: TProps) => boolean; - alias?: string; -} - -export type CompositeComponent

= ComponentClass

| StatelessComponent

; +import { DocumentNode } from 'graphql'; -export interface ComponentDecorator { - (component: CompositeComponent): ComponentClass; -} -export interface InferableComponentDecorator { - >(component: T): T; -} +import { + MutationOpts, + ChildProps, + OperationOption, + ComponentDecorator, + QueryOpts, + QueryProps, + MutationFunc, + OptionProps, +} from './types'; const defaultMapPropsToOptions = props => ({}); const defaultMapResultToProps = props => props; @@ -145,7 +68,7 @@ let nextVersion = 0; export default function graphql< TResult = {}, TProps = {}, - TChildProps = DefaultChildProps + TChildProps = ChildProps >( document: DocumentNode, operationOptions: OperationOption = {}, @@ -671,92 +594,3 @@ export default function graphql< return wrapWithApolloComponent; } - -/** - * An observable query recycler stores some observable queries that are no - * longer in use, but that we may someday use again. - * - * Recycling observable queries avoids a few unexpected functionalities that - * may be hit when using the `react-apollo` API. Namely not updating queries - * when a component unmounts, and calling reducers/`updateQueries` more times - * then is necessary for old observable queries. - * - * We assume that the GraphQL document for every `ObservableQuery` is the same. - * - * For more context on why this was added and links to the issues recycling - * `ObservableQuery`s fixes see issue [#462][1]. - * - * [1]: https://github.com/apollographql/react-apollo/pull/462 - */ -class ObservableQueryRecycler { - /** - * The internal store for our observable queries and temporary subscriptions. - */ - private observableQueries: Array<{ - observableQuery: ObservableQuery; - subscription: Subscription; - }> = []; - - /** - * Recycles an observable query that the recycler is finished with. It is - * stored in this class so that it may be used later on. - * - * A subscription is made to the observable query so that it continues to - * live even though the updates are noops. - * - * By recycling an observable query we keep the results fresh so that when it - * gets reused all of the mutations that have happened since recycle and - * reuse have been applied. - */ - public recycle(observableQuery: ObservableQuery): void { - // Stop the query from polling when we recycle. Polling may resume when we - // reuse it and call `setOptions`. - observableQuery.setOptions({ - fetchPolicy: 'standby', - pollInterval: 0, - fetchResults: false, // ensure we don't create another observer in AC - }); - - this.observableQueries.push({ - observableQuery, - subscription: observableQuery.subscribe({}), - }); - } - - /** - * Reuses an observable query that was recycled earlier on in this class’s - * lifecycle. This observable was kept fresh by our recycler with a - * subscription that will be unsubscribed from before returning the - * observable query. - * - * All mutations that occured between the time of recycling and the time of - * reusing have been applied. - */ - public reuse(options: QueryOpts): ObservableQuery { - if (this.observableQueries.length <= 0) { - return null; - } - const { observableQuery, subscription } = this.observableQueries.pop(); - subscription.unsubscribe(); - - // strip off react-apollo specific options - const { ssr, skip, client, ...modifiableOpts } = options; - - // When we reuse an `ObservableQuery` then the document and component - // GraphQL display name should be the same. Only the options may be - // different. - // - // Therefore we need to set the new options. - // - // If this observable query used to poll then polling will be restarted. - observableQuery.setOptions({ - ...modifiableOpts, - // Explicitly set options changed when recycling to make sure they - // are set to `undefined` if not provided in options. - pollInterval: options.pollInterval, - fetchPolicy: options.fetchPolicy, - }); - - return observableQuery; - } -} diff --git a/src/queryRecycler.ts b/src/queryRecycler.ts new file mode 100644 index 0000000000..fe28a10902 --- /dev/null +++ b/src/queryRecycler.ts @@ -0,0 +1,92 @@ +import { ObservableQuery, Subscription } from 'apollo-client'; + +import { QueryOpts } from './types'; + +/** + * An observable query recycler stores some observable queries that are no + * longer in use, but that we may someday use again. + * + * Recycling observable queries avoids a few unexpected functionalities that + * may be hit when using the `react-apollo` API. Namely not updating queries + * when a component unmounts, and calling reducers/`updateQueries` more times + * then is necessary for old observable queries. + * + * We assume that the GraphQL document for every `ObservableQuery` is the same. + * + * For more context on why this was added and links to the issues recycling + * `ObservableQuery`s fixes see issue [#462][1]. + * + * [1]: https://github.com/apollographql/react-apollo/pull/462 + */ +export class ObservableQueryRecycler { + /** + * The internal store for our observable queries and temporary subscriptions. + */ + private observableQueries: Array<{ + observableQuery: ObservableQuery; + subscription: Subscription; + }> = []; + + /** + * Recycles an observable query that the recycler is finished with. It is + * stored in this class so that it may be used later on. + * + * A subscription is made to the observable query so that it continues to + * live even though the updates are noops. + * + * By recycling an observable query we keep the results fresh so that when it + * gets reused all of the mutations that have happened since recycle and + * reuse have been applied. + */ + public recycle(observableQuery: ObservableQuery): void { + // Stop the query from polling when we recycle. Polling may resume when we + // reuse it and call `setOptions`. + observableQuery.setOptions({ + fetchPolicy: 'standby', + pollInterval: 0, + fetchResults: false, // ensure we don't create another observer in AC + }); + + this.observableQueries.push({ + observableQuery, + subscription: observableQuery.subscribe({}), + }); + } + + /** + * Reuses an observable query that was recycled earlier on in this class’s + * lifecycle. This observable was kept fresh by our recycler with a + * subscription that will be unsubscribed from before returning the + * observable query. + * + * All mutations that occured between the time of recycling and the time of + * reusing have been applied. + */ + public reuse(options: QueryOpts): ObservableQuery { + if (this.observableQueries.length <= 0) { + return null; + } + const { observableQuery, subscription } = this.observableQueries.pop(); + subscription.unsubscribe(); + + // strip off react-apollo specific options + const { ssr, skip, client, ...modifiableOpts } = options; + + // When we reuse an `ObservableQuery` then the document and component + // GraphQL display name should be the same. Only the options may be + // different. + // + // Therefore we need to set the new options. + // + // If this observable query used to poll then polling will be restarted. + observableQuery.setOptions({ + ...modifiableOpts, + // Explicitly set options changed when recycling to make sure they + // are set to `undefined` if not provided in options. + pollInterval: options.pollInterval, + fetchPolicy: options.fetchPolicy, + }); + + return observableQuery; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000000..c8e1c25631 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,98 @@ +import { ComponentClass, StatelessComponent } from 'react'; + +import ApolloClient, { + ObservableQuery, + MutationQueryReducersMap, + Subscription, + ApolloStore, + ApolloQueryResult, + ApolloError, + FetchPolicy, + FetchMoreOptions, + UpdateQueryOptions, + FetchMoreQueryOptions, + SubscribeToMoreOptions, +} from 'apollo-client'; +import { PureQueryOptions } from 'apollo-client/core/types'; +import { MutationUpdaterFn } from 'apollo-client/core/watchQueryOptions'; + +import { ExecutionResult, DocumentNode } from 'graphql'; + +export interface MutationOpts { + variables?: Object; + optimisticResponse?: Object; + updateQueries?: MutationQueryReducersMap; + refetchQueries?: string[] | PureQueryOptions[]; + update?: MutationUpdaterFn; + client?: ApolloClient; +} + +export interface QueryOpts { + ssr?: boolean; + variables?: { [key: string]: any }; + fetchPolicy?: FetchPolicy; + pollInterval?: number; + client?: ApolloClient; + // deprecated + skip?: boolean; +} + +export interface QueryProps { + error?: ApolloError; + networkStatus: number; + loading: boolean; + variables: { + [variable: string]: any; + }; + fetchMore: ( + fetchMoreOptions: FetchMoreQueryOptions & FetchMoreOptions, + ) => Promise>; + refetch: (variables?: any) => Promise>; + startPolling: (pollInterval: number) => void; + stopPolling: () => void; + subscribeToMore: (options: SubscribeToMoreOptions) => () => void; + updateQuery: ( + mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any, + ) => void; +} + +export type MutationFunc = ( + opts: MutationOpts, +) => Promise>; + +export interface OptionProps { + ownProps: TProps; + data?: QueryProps & TResult; + mutate?: MutationFunc; +} + +export type ChildProps = P & { + data?: QueryProps & R; + mutate?: MutationFunc; +}; + +export type NamedProps = P & { + ownProps: R; +}; + +export interface OperationOption { + options?: + | QueryOpts + | MutationOpts + | ((props: TProps) => QueryOpts | MutationOpts); + props?: (props: OptionProps) => any; + skip?: boolean | ((props: any) => boolean); + name?: string; + withRef?: boolean; + shouldResubscribe?: (props: TProps, nextProps: TProps) => boolean; + alias?: string; +} + +export type CompositeComponent

= ComponentClass

| StatelessComponent

; + +export interface ComponentDecorator { + (component: CompositeComponent): ComponentClass; +} +export interface InferableComponentDecorator { + >(component: T): T; +} diff --git a/src/withApollo.tsx b/src/withApollo.tsx index 4e78134d80..694aae6139 100644 --- a/src/withApollo.tsx +++ b/src/withApollo.tsx @@ -31,7 +31,7 @@ import { } from 'graphql'; import { parser, DocumentType } from './parser'; -import { OperationOption } from './graphql'; +import { OperationOption } from './types'; function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; diff --git a/test/typescript.ts b/test/typescript.ts new file mode 100644 index 0000000000..73a6e0aacf --- /dev/null +++ b/test/typescript.ts @@ -0,0 +1,57 @@ +// this file tests compliation of typescript types to ensure compatibility +// we intentionly don't enfore TS compliation on the reset of the tests so we can +// test things like improper arugment calling / etc to cause errors and ensure +// that the are handled +import * as React from 'react'; +import gql from 'graphql-tag'; + +import { graphql } from '../src'; +import { ChildProps, NamedProps, QueryProps } from '../src/types'; + +const historyQuery = gql` + query history($solutionId: String) { + history(solutionId: $solutionId) { + solutionId + delta + } + } +`; + +type Data = { + history: Record[]; +}; + +type Props = { + solutionId: string; +}; + +// standard wrapping +const withHistory = graphql(historyQuery, { + options: ownProps => ({ + variables: { + solutionId: ownProps.solutionId, + }, + }), +}); + +class HistoryView extends React.Component, {}> {} + +const HistoryViewWithData = withHistory(HistoryView); + +// decorator +@graphql(historyQuery) +class DecoratedHistoryView extends React.Component> { + render() { + return null; + } +} + +// with using name +const withHistoryUsingName = graphql(historyQuery, { + name: 'organisationData', + props: ({ + organisationData, + }: NamedProps<{ organisationData: QueryProps & Data }, Props>) => ({ + ...organisationData, + }), +});