Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(generic-sdk): respect live queries & use AsyncIterable if possible #8204

Merged
merged 2 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/twelve-insects-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-codegen/typescript-generic-sdk': major
---

- Respect GraphQL Live Queries like Subscriptions and use the stream return types (`AsyncIterable` or `Observable`).
- Previously if there was no `usingObservableFrom` set in the configuration, the plugin was using `Promise` as subscriptions' return type, and this is wrong. Now it uses `AsyncIterable` in this case.
19 changes: 15 additions & 4 deletions packages/plugins/typescript/generic-sdk/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ export interface GenericSdkPluginConfig extends ClientSideBasePluginConfig {
rawRequest: boolean;
}

function isStreamOperation(operationAST: OperationDefinitionNode) {
if (operationAST.operation === 'subscription') {
return true;
}
if (
operationAST.operation === 'query' &&
operationAST.directives?.some(directiveNode => directiveNode.name.value === 'live')
charlypoly marked this conversation as resolved.
Show resolved Hide resolved
) {
return true;
}
return false;
}

export class GenericSdkVisitor extends ClientSideBaseVisitor<RawGenericSdkPluginConfig, GenericSdkPluginConfig> {
private _operationsToInclude: {
node: OperationDefinitionNode;
Expand Down Expand Up @@ -75,7 +88,7 @@ export class GenericSdkVisitor extends ClientSideBaseVisitor<RawGenericSdkPlugin
!o.node.variableDefinitions ||
o.node.variableDefinitions.length === 0 ||
o.node.variableDefinitions.every(v => v.type.kind !== Kind.NON_NULL_TYPE || v.defaultValue);
const returnType = usingObservable && o.operationType === 'Subscription' ? 'Observable' : 'Promise';
const returnType = isStreamOperation(o.node) ? (usingObservable ? 'Observable' : 'AsyncIterable') : 'Promise';
const resultData = this.config.rawRequest
? `ExecutionResult<${o.operationResultType}, E>`
: o.operationResultType;
Expand All @@ -91,9 +104,7 @@ export class GenericSdkVisitor extends ClientSideBaseVisitor<RawGenericSdkPlugin

const documentNodeType = this.config.documentMode === DocumentMode.string ? 'string' : 'DocumentNode';
const resultData = this.config.rawRequest ? 'ExecutionResult<R, E>' : 'R';
const returnType = usingObservable
? `Promise<${resultData}> & Observable<${resultData}>`
: `Promise<${resultData}>`;
const returnType = `Promise<${resultData}> | ${usingObservable ? 'Observable' : 'AsyncIterable'}<${resultData}>`;
charlypoly marked this conversation as resolved.
Show resolved Hide resolved

return `export type Requester<C = {}, E = unknown> = <R, V>(doc: ${documentNodeType}, vars?: V, options?: C) => ${returnType}
export function getSdk<C, E>(requester: Requester<C, E>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export const Feed4Document = gql\`
}
}
\`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<R>
export type Requester<C = {}, E = unknown> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C, E>(requester: Requester<C, E>) {
return {
feed(variables?: FeedQueryVariables, options?: C): Promise<FeedQuery> {
Expand Down Expand Up @@ -273,6 +273,245 @@ async function test() {
}"
`;

exports[`generic-sdk sdk Should generate a correct wrap method when usingObservableFrom is not set 1`] = `
"export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
import { DocumentNode } from 'graphql';
import gql from 'graphql-tag';
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};

export type Query = {
__typename?: 'Query';
/** A feed of repository submissions */
feed?: Maybe<Array<Maybe<Entry>>>;
/** A single entry */
entry?: Maybe<Entry>;
/** Return the currently logged in user, or null if nobody is logged in */
currentUser?: Maybe<User>;
};


export type QueryFeedArgs = {
type: FeedType;
offset?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
};


export type QueryEntryArgs = {
repoFullName: Scalars['String'];
};

/** A list of options for the sort order of the feed */
export enum FeedType {
/** Sort by a combination of freshness and score, using Reddit's algorithm */
Hot = 'HOT',
/** Newest entries first */
New = 'NEW',
/** Highest score entries first */
Top = 'TOP'
}

/** Information about a GitHub repository submitted to GitHunt */
export type Entry = {
__typename?: 'Entry';
/** Information about the repository from GitHub */
repository: Repository;
/** The GitHub user who submitted this entry */
postedBy: User;
/** A timestamp of when the entry was submitted */
createdAt: Scalars['Float'];
/** The score of this repository, upvotes - downvotes */
score: Scalars['Int'];
/** The hot score of this repository */
hotScore: Scalars['Float'];
/** Comments posted about this repository */
comments: Array<Maybe<Comment>>;
/** The number of comments posted about this repository */
commentCount: Scalars['Int'];
/** The SQL ID of this entry */
id: Scalars['Int'];
/** XXX to be changed */
vote: Vote;
};


/** Information about a GitHub repository submitted to GitHunt */
export type EntryCommentsArgs = {
limit?: InputMaybe<Scalars['Int']>;
offset?: InputMaybe<Scalars['Int']>;
};

/**
* A repository object from the GitHub API. This uses the exact field names returned by the
* GitHub API for simplicity, even though the convention for GraphQL is usually to camel case.
*/
export type Repository = {
__typename?: 'Repository';
/** Just the name of the repository, e.g. GitHunt-API */
name: Scalars['String'];
/** The full name of the repository with the username, e.g. apollostack/GitHunt-API */
full_name: Scalars['String'];
/** The description of the repository */
description?: Maybe<Scalars['String']>;
/** The link to the repository on GitHub */
html_url: Scalars['String'];
/** The number of people who have starred this repository on GitHub */
stargazers_count: Scalars['Int'];
/** The number of open issues on this repository on GitHub */
open_issues_count?: Maybe<Scalars['Int']>;
/** The owner of this repository on GitHub, e.g. apollostack */
owner?: Maybe<User>;
};

/** A user object from the GitHub API. This uses the exact field names returned from the GitHub API. */
export type User = {
__typename?: 'User';
/** The name of the user, e.g. apollostack */
login: Scalars['String'];
/** The URL to a directly embeddable image for this user's avatar */
avatar_url: Scalars['String'];
/** The URL of this user's GitHub page */
html_url: Scalars['String'];
};

/** A comment about an entry, submitted by a user */
export type Comment = {
__typename?: 'Comment';
/** The SQL ID of this entry */
id: Scalars['Int'];
/** The GitHub user who posted the comment */
postedBy: User;
/** A timestamp of when the comment was posted */
createdAt: Scalars['Float'];
/** The text of the comment */
content: Scalars['String'];
/** The repository which this comment is about */
repoName: Scalars['String'];
};

/** XXX to be removed */
export type Vote = {
__typename?: 'Vote';
vote_value: Scalars['Int'];
};

export type Mutation = {
__typename?: 'Mutation';
/** Submit a new repository, returns the new submission */
submitRepository?: Maybe<Entry>;
/** Vote on a repository submission, returns the submission that was voted on */
vote?: Maybe<Entry>;
/** Comment on a repository, returns the new comment */
submitComment?: Maybe<Comment>;
};


export type MutationSubmitRepositoryArgs = {
repoFullName: Scalars['String'];
};


export type MutationVoteArgs = {
repoFullName: Scalars['String'];
type: VoteType;
};


export type MutationSubmitCommentArgs = {
repoFullName: Scalars['String'];
commentContent: Scalars['String'];
};

/** The type of vote to record, when submitting a vote */
export enum VoteType {
Up = 'UP',
Down = 'DOWN',
Cancel = 'CANCEL'
}

export type Subscription = {
__typename?: 'Subscription';
/** Subscription fires on every comment added */
commentAdded?: Maybe<Comment>;
};


export type SubscriptionCommentAddedArgs = {
repoFullName: Scalars['String'];
};
export type FeedQueryVariables = Exact<{ [key: string]: never; }>;


export type FeedQuery = { __typename?: 'Query', feed?: Array<{ __typename?: 'Entry', id: number } | null> | null };

export type CommentAddedSubscriptionVariables = Exact<{ [key: string]: never; }>;


export type CommentAddedSubscription = { __typename?: 'Subscription', commentAdded?: { __typename?: 'Comment', id: number } | null };

export type FeedLiveQueryVariables = Exact<{ [key: string]: never; }>;


export type FeedLiveQuery = { __typename?: 'Query', feed?: Array<{ __typename?: 'Entry', id: number } | null> | null };

export const FeedDocument = gql\`
query feed {
feed {
id
}
}
\`;
export const CommentAddedDocument = gql\`
subscription commentAdded {
commentAdded {
id
}
}
\`;
export const FeedLiveDocument = gql\`
query feedLive @live {
feed {
id
}
}
\`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C, E>(requester: Requester<C, E>) {
return {
feed(variables?: FeedQueryVariables, options?: C): Promise<FeedQuery> {
return requester<FeedQuery, FeedQueryVariables>(FeedDocument, variables, options);
},
commentAdded(variables?: CommentAddedSubscriptionVariables, options?: C): AsyncIterable<CommentAddedSubscription> {
return requester<CommentAddedSubscription, CommentAddedSubscriptionVariables>(CommentAddedDocument, variables, options);
},
feedLive(variables?: FeedLiveQueryVariables, options?: C): AsyncIterable<FeedLiveQuery> {
return requester<FeedLiveQuery, FeedLiveQueryVariables>(FeedLiveDocument, variables, options);
}
};
}
export type Sdk = ReturnType<typeof getSdk>;
async function test() {
const sdk = getSdk((() => {}) as any);
const test = sdk.commentAdded();
for await (const item of test) {
console.log(item.data);
console.log(item.errors);
}
}
"
`;

exports[`generic-sdk sdk Should generate a correct wrap method when usingObservableFrom is set 1`] = `
"export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
Expand Down Expand Up @@ -461,6 +700,11 @@ export type CommentAddedSubscriptionVariables = Exact<{ [key: string]: never; }>

export type CommentAddedSubscription = { __typename?: 'Subscription', commentAdded?: { __typename?: 'Comment', id: number } | null };

export type FeedLiveQueryVariables = Exact<{ [key: string]: never; }>;


export type FeedLiveQuery = { __typename?: 'Query', feed?: Array<{ __typename?: 'Entry', id: number } | null> | null };

export const FeedDocument = gql\`
query feed {
feed {
Expand All @@ -475,14 +719,24 @@ export const CommentAddedDocument = gql\`
}
}
\`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<R> & Observable<R>
export const FeedLiveDocument = gql\`
query feedLive @live {
feed {
id
}
}
\`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<R> | Observable<R>
export function getSdk<C, E>(requester: Requester<C, E>) {
return {
feed(variables?: FeedQueryVariables, options?: C): Promise<FeedQuery> {
return requester<FeedQuery, FeedQueryVariables>(FeedDocument, variables, options);
},
commentAdded(variables?: CommentAddedSubscriptionVariables, options?: C): Observable<CommentAddedSubscription> {
return requester<CommentAddedSubscription, CommentAddedSubscriptionVariables>(CommentAddedDocument, variables, options);
},
feedLive(variables?: FeedLiveQueryVariables, options?: C): Observable<FeedLiveQuery> {
return requester<FeedLiveQuery, FeedLiveQueryVariables>(FeedLiveDocument, variables, options);
}
};
}
Expand Down Expand Up @@ -724,7 +978,7 @@ export const Feed4Document = \`
}
}
\`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R>
export type Requester<C = {}, E = unknown> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C, E>(requester: Requester<C, E>) {
return {
feed(variables?: FeedQueryVariables, options?: C): Promise<FeedQuery> {
Expand Down Expand Up @@ -997,7 +1251,7 @@ export const Feed4Document = gql\`
}
}
\`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<ExecutionResult<R, E>>
export type Requester<C = {}, E = unknown> = <R, V>(doc: DocumentNode, vars?: V, options?: C) => Promise<ExecutionResult<R, E>> | AsyncIterable<ExecutionResult<R, E>>
export function getSdk<C, E>(requester: Requester<C, E>) {
return {
feed(variables?: FeedQueryVariables, options?: C): Promise<ExecutionResult<FeedQuery, E>> {
Expand Down
Loading