diff --git a/src/commands/graphql.ts b/src/commands/graphql.ts index d760e42..0b718a4 100644 --- a/src/commands/graphql.ts +++ b/src/commands/graphql.ts @@ -10,7 +10,7 @@ interface GraphQLProps { name?: string } -export const graphql = async ({ name }: GraphQLProps) => { +export const graphql = async ({ name, flags }: GraphQLProps) => { if (!name) { console.log(`No name provided`) return @@ -55,10 +55,29 @@ export const graphql = async ({ name }: GraphQLProps) => { await createFolder(`${name}/lib`) - await create({ - templateName: 'graphql/server.ts', - output: `${name}/lib/server.ts`, - }) + if (flags.examples) { + await create({ + templateName: 'graphql/serverExample.ts', + output: `${name}/lib/server.ts`, + }) + + await createFolder(`${name}/lib/resolvers`) + await create({ + templateName: 'graphql/resolversExample.ts', + output: `${name}/lib/resolvers/queue.ts`, + }) + + await createFolder(`${name}/lib/__generated__`) + await create({ + templateName: 'graphql/graphql.d.ts', + output: `${name}/lib/__generated__/graphql.d.ts`, + }) + } else { + await create({ + templateName: 'graphql/server.ts', + output: `${name}/lib/server.ts`, + }) + } spinner.stop() diff --git a/src/index.ts b/src/index.ts index 0c016ed..1531e88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ export interface CLIFlags { javascript: boolean ide?: string language?: string + examples?: boolean } export interface CLIProps { @@ -68,6 +69,7 @@ const cli = meow( --javascript JavaScript app (react) --ide IDE for snippets (snippets) --language Language for snippets (snippets) + --examples GraphQL examples (examples) `, { flags: { @@ -81,6 +83,9 @@ const cli = meow( language: { type: 'string', }, + examples: { + type: 'boolean', + }, }, } ) diff --git a/src/templates/graphql/graphql.d.ts.ejs b/src/templates/graphql/graphql.d.ts.ejs new file mode 100644 index 0000000..8a5ec52 --- /dev/null +++ b/src/templates/graphql/graphql.d.ts.ejs @@ -0,0 +1,206 @@ + import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; +export type Maybe = T | null; +export type RequireFields = { [X in Exclude]?: T[X] } & { [P in K]-?: NonNullable }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string, + String: string, + Boolean: boolean, + Int: number, + Float: number, + /** The `Upload` scalar type represents a file upload. */ + Upload: any, +}; + + +export enum CacheControlScope { + Public = 'PUBLIC', + Private = 'PRIVATE' +} + +export type Mutation = { + __typename?: 'Mutation', + _empty?: Maybe, + addTrack: Array, +}; + + +export type MutationAddTrackArgs = { + input: TrackInput +}; + +export type Query = { + __typename?: 'Query', + _empty?: Maybe, + currentQueue: Array, +}; + +export type Subscription = { + __typename?: 'Subscription', + _empty?: Maybe, + trackAdded: Track, +}; + +export type Track = { + __typename?: 'Track', + title: Scalars['String'], + artist: Scalars['String'], + album: Scalars['String'], +}; + +export type TrackInput = { + title: Scalars['String'], + artist: Scalars['String'], + album: Scalars['String'], +}; + + + +export type ResolverTypeWrapper = Promise | T; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise | TResult; + + +export type StitchingResolver = { + fragment: string; + resolve: ResolverFn; +}; + +export type Resolver = + | ResolverFn + | StitchingResolver; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterator | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Query: ResolverTypeWrapper<{}>, + String: ResolverTypeWrapper, + Track: ResolverTypeWrapper, + Mutation: ResolverTypeWrapper<{}>, + TrackInput: TrackInput, + Subscription: ResolverTypeWrapper<{}>, + Boolean: ResolverTypeWrapper, + CacheControlScope: CacheControlScope, + Upload: ResolverTypeWrapper, + Int: ResolverTypeWrapper, +}; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Query: {}, + String: Scalars['String'], + Track: Track, + Mutation: {}, + TrackInput: TrackInput, + Subscription: {}, + Boolean: Scalars['Boolean'], + CacheControlScope: CacheControlScope, + Upload: Scalars['Upload'], + Int: Scalars['Int'], +}; + +export type CacheControlDirectiveResolver>, + scope?: Maybe> }> = DirectiveResolverFn; + +export type MutationResolvers = { + _empty?: Resolver, ParentType, ContextType>, + addTrack?: Resolver, ParentType, ContextType, RequireFields>, +}; + +export type QueryResolvers = { + _empty?: Resolver, ParentType, ContextType>, + currentQueue?: Resolver, ParentType, ContextType>, +}; + +export type SubscriptionResolvers = { + _empty?: SubscriptionResolver, "_empty", ParentType, ContextType>, + trackAdded?: SubscriptionResolver, +}; + +export type TrackResolvers = { + title?: Resolver, + artist?: Resolver, + album?: Resolver, +}; + +export interface UploadScalarConfig extends GraphQLScalarTypeConfig { + name: 'Upload' +} + +export type Resolvers = { + Mutation?: MutationResolvers, + Query?: QueryResolvers, + Subscription?: SubscriptionResolvers, + Track?: TrackResolvers, + Upload?: GraphQLScalarType, +}; + + +/** + * @deprecated + * Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config. +*/ +export type IResolvers = Resolvers; +export type DirectiveResolvers = { + cacheControl?: CacheControlDirectiveResolver, +}; + + +/** +* @deprecated +* Use "DirectiveResolvers" root object instead. If you wish to get "IDirectiveResolvers", add "typesPrefix: I" to your config. +*/ +export type IDirectiveResolvers = DirectiveResolvers; + diff --git a/src/templates/graphql/package.json.ejs b/src/templates/graphql/package.json.ejs index 27fa60b..2389223 100644 --- a/src/templates/graphql/package.json.ejs +++ b/src/templates/graphql/package.json.ejs @@ -14,9 +14,14 @@ "license": "MIT", "dependencies": { "apollo-server-express": "2.9.4", - "express": "4.17.1" + "express": "4.17.1", + "lodash.merge": "4.6.2" }, "devDependencies": { + "@graphql-codegen/cli": "1.7.0", + "@graphql-codegen/typescript": "1.7.0", + "@graphql-codegen/typescript-resolvers": "1.7.0", + "@types/lodash.merge": "4.6.6", "ts-node-dev": "1.0.0-pre.43", "typescript": "3.6.3" } diff --git a/src/templates/graphql/resolversExample.ts.ejs b/src/templates/graphql/resolversExample.ts.ejs new file mode 100644 index 0000000..fe4d890 --- /dev/null +++ b/src/templates/graphql/resolversExample.ts.ejs @@ -0,0 +1,63 @@ +import { gql } from 'apollo-server-express' +import { queue, pubsub } from '../server' +import { + QueryResolvers, + MutationResolvers, + SubscriptionResolvers, +} from '../__generated__/graphql' + +const TRACK_ADDED = 'TRACK_ADDED' + +export const typeDefs = gql` + type Track { + title: String! + artist: String! + album: String! + } + + input TrackInput { + title: String! + artist: String! + album: String! + } + + extend type Query { + currentQueue: [Track!]! + } + + extend type Mutation { + addTrack(input: TrackInput!): [Track!]! + } + + extend type Subscription { + trackAdded: Track! + } +` + +interface Resolvers { + Query: QueryResolvers + Mutation: MutationResolvers + Subscription: SubscriptionResolvers +} + +export const resolvers: Resolvers = { + Query: { + currentQueue: () => queue, + }, + + Mutation: { + addTrack: (_, { input }) => { + pubsub.publish(TRACK_ADDED, { trackAdded: input }) + queue.push(input) + + return queue + }, + }, + + Subscription: { + trackAdded: { + subscribe: () => pubsub.asyncIterator([TRACK_ADDED]), + }, + }, +} + diff --git a/src/templates/graphql/serverExample.ts.ejs b/src/templates/graphql/serverExample.ts.ejs new file mode 100644 index 0000000..089bbf2 --- /dev/null +++ b/src/templates/graphql/serverExample.ts.ejs @@ -0,0 +1,98 @@ +import express from 'express' +import { ApolloServer, gql, PubSub } from 'apollo-server-express' +import { + typeDefs as queueDefs, + resolvers as queueResolvers, +} from './resolvers/queue' +import merge from 'lodash.merge' +import { Track } from './__generated__/graphql' +import http from 'http' + +export const pubsub = new PubSub() + +export const queue: Track[] = [ + { + title: 'Hospital for Souls', + artist: 'Bring Me The Horizon', + album: 'Sempiternal', + }, +] + +const typeDefs = gql` + type Query { + _empty: String + } + + type Mutation { + _empty: String + } + + type Subscription { + _empty: String + } +` + +const server = new ApolloServer({ + typeDefs: [typeDefs, queueDefs], + resolvers: merge(queueResolvers), + playground: { + tabs: [ + { + endpoint: 'http://localhost:4000/graphql', + name: 'Queries', + query: `query currentQueue { + currentQueue { + ...TrackInfo + } +} + +# Might need to prettify for Playground to see this mutation +mutation addTrack($track: TrackInput!) { + addTrack(input: $track) { + ...TrackInfo + } +} + +fragment TrackInfo on Track { + title + artist + album +} + `, + variables: `{ + "track": { + "title": "Antivist", + "album": "Sempiternal", + "artist": "Bring Me The Horizon" + } +}`, + }, + { + endpoint: 'http://localhost:4000/graphql', + name: 'Subscription', + query: `# Run this subscription query then run the mutation in the first tab +# to see the results to the right +subscription trackAdded { + trackAdded { + title + artist + album + } +}`, + }, + ], + }, +}) + +const app = express() + +server.applyMiddleware({ app }) + +const httpServer = http.createServer(app) +server.installSubscriptionHandlers(httpServer) + +httpServer.listen({ port: 4000 }, () => + console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`) +) + + diff --git a/test/commands/graphql.spec.ts b/test/commands/graphql.spec.ts index f31f66e..3b8d811 100644 --- a/test/commands/graphql.spec.ts +++ b/test/commands/graphql.spec.ts @@ -98,3 +98,32 @@ test('should add server', async () => { output: 'test/lib/server.ts', }) }) + +test('should add server with examples', async () => { + await graphql({ name: 'test', flags: { examples: true } }) + + expect(create).toHaveBeenCalledWith({ + templateName: 'graphql/serverExample.ts', + output: 'test/lib/server.ts', + }) +}) + +test('should add resolvers with examples', async () => { + await graphql({ name: 'test', flags: { examples: true } }) + + expect(createFolder).toHaveBeenCalledWith('test/lib/resolvers') + expect(create).toHaveBeenCalledWith({ + templateName: 'graphql/resolversExample.ts', + output: 'test/lib/resolvers/queue.ts', + }) +}) + +test('should add generated types', async () => { + await graphql({ name: 'test', flags: { examples: true } }) + + expect(createFolder).toHaveBeenCalledWith('test/lib/__generated__') + expect(create).toHaveBeenCalledWith({ + templateName: 'graphql/graphql.d.ts', + output: 'test/lib/__generated__/graphql.d.ts', + }) +})