diff --git a/README.md b/README.md index 3a3061b..8517c0d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ import { SubscriberModule } from 'twitch-graphql' import { UserModule } from 'twitch-graphql' import { StreamModule } from 'twitch-graphql' import { GameModule } from 'twitch-graphql' +import { UserSubscriberLinkModule } from 'twitch-graphql' +import { StreamUserLinkModule } from 'twitch-graphql' +import { GameStreamLinkModule } from 'twitch-graphql' import { createApplication } from 'graphql-modules' const port = 5555 @@ -52,6 +55,9 @@ const app = createApplication({ UserModule, StreamModule, GameModule, + UserSubscriberLinkModule, + StreamUserLinkModule, + GameStreamLinkModule, ], }) const execute = app.createExecution() @@ -108,7 +114,7 @@ Contributors should be using [prettier](https://prettier.io/) to automatically f To contribute please follow these steps: -1. Clone the repository: ` git clone https://github.com/ColeWalker/twitch-graphql-server` +1. Clone the repository: `git clone https://github.com/ColeWalker/twitch-graphql-server` 2. Create a new branch named after the issue that you're going to fix. Prefix branch name with a Conventional Commits type. Example: `git checkout -b feature/add-port-config` 3. Write code and write tests for your code. We use jest and ts-jest. If your code is not properly covered by tests, it will be rejected. Since this is a wrapper for an external API, our tests cannot be thorough, however, you should provide as much coverage as possible. Follow any .test.ts file for examples. @@ -204,16 +210,22 @@ type FollowConnection { cursor: String } -extend type Subscriber { - user: User! -} - extend type Query { getUserById(userId: String!): User getUserByDisplayName(displayName: String!): User } ``` +### UserSubscriberLink + +This module extends Subscriber to add the user field. Only use if both modules are being used in your application. + +```graphql +extend type Subscriber { + user: User! +} +``` + ### Stream ```graphql @@ -226,13 +238,20 @@ type Stream { thumbnailUrl: String! userDisplayName: String! userId: String! - - user: User } +``` + +### StreamUserLink +This module extends Stream to add the user field, and User to add the stream field. Only use if both modules are being used in your application. + +```graphql extend type User { stream: Stream } +extend type Stream { + user: User +} ``` ### Game @@ -244,6 +263,16 @@ type Game { name: String! } +extend type Query { + getGameByName(gameName: String!): Game +} +``` + +### GameStreamLink + +This module extends Stream to add the game field. Only use if both modules are being used in your application. + +```graphql extend type Stream { game: Game } diff --git a/src/index.ts b/src/index.ts index ef931b9..5d3b295 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,3 +23,18 @@ export { QueryResolvers, QuerySchema, } from './schema/query-type-schema' +export { + UserSubscriberLinkModule, + UserSubscriberLinkResolvers, + UserSubscriberLinkSchema, +} from './schema/user-subscriber-link-type-schema' +export { + GameStreamLinkModule, + GameStreamLinkResolvers, + GameStreamLinkSchema, +} from './schema/game-stream-link-type-schema' +export { + StreamUserLinkModule, + StreamUserLinkResolvers, + StreamUserLinkSchema, +} from './schema/stream-user-link-type-schema' diff --git a/src/package.test.ts b/src/package.test.ts index f796cff..dfcff89 100644 --- a/src/package.test.ts +++ b/src/package.test.ts @@ -16,6 +16,15 @@ import { QueryModule, QueryResolvers, QuerySchema, + UserSubscriberLinkModule, + UserSubscriberLinkResolvers, + UserSubscriberLinkSchema, + GameStreamLinkModule, + GameStreamLinkResolvers, + GameStreamLinkSchema, + StreamUserLinkModule, + StreamUserLinkResolvers, + StreamUserLinkSchema, } from './index' describe('npm package', () => { @@ -35,6 +44,15 @@ describe('npm package', () => { expect(QueryModule).toBeTruthy() expect(QueryResolvers).toBeTruthy() expect(QuerySchema).toBeTruthy() + expect(UserSubscriberLinkModule).toBeTruthy() + expect(UserSubscriberLinkResolvers).toBeTruthy() + expect(UserSubscriberLinkSchema).toBeTruthy() + expect(GameStreamLinkModule).toBeTruthy() + expect(GameStreamLinkResolvers).toBeTruthy() + expect(GameStreamLinkSchema).toBeTruthy() + expect(StreamUserLinkModule).toBeTruthy() + expect(StreamUserLinkResolvers).toBeTruthy() + expect(StreamUserLinkSchema).toBeTruthy() }) it('modules should work together', async () => { @@ -44,7 +62,10 @@ describe('npm package', () => { SubscriberModule, UserModule, StreamModule, + UserSubscriberLinkModule, + GameStreamLinkModule, GameModule, + StreamUserLinkModule, ], }) const schema = app.createSchemaForApollo() diff --git a/src/schema/game-stream-link-type-schema.test.ts b/src/schema/game-stream-link-type-schema.test.ts new file mode 100644 index 0000000..5d3c1c1 --- /dev/null +++ b/src/schema/game-stream-link-type-schema.test.ts @@ -0,0 +1,59 @@ +import { createApplication } from 'graphql-modules' +import { SubscriberModule } from './subscriber-type-schema' +import { UserModule } from './user-type-schema' +import { StreamModule } from './stream-type-schema' +import { GameModule } from './game-type-schema' +import { parse, execute } from 'graphql' +import { QueryModule } from './query-type-schema' +import { UserSubscriberLinkModule } from './user-subscriber-link-type-schema' +import { GameStreamLinkModule } from './game-stream-link-type-schema' + +describe('GameStreamLinkModule', () => { + it('game should have all fields', async () => { + const app = createApplication({ + modules: [ + QueryModule, + SubscriberModule, + UserModule, + UserSubscriberLinkModule, + StreamModule, + GameStreamLinkModule, + GameModule, + ], + }) + const schema = app.createSchemaForApollo() + + const document = parse(` + { + latestSub { + user{ + displayName + stream { + game { + id + boxArtUrl + name + } + } + } + } + } + `) + const contextValue = { request: {}, response: {} } + const result = await execute({ + schema, + contextValue, + document, + }) + + expect(result?.errors?.length).toBeFalsy() + + const game = result?.data?.latestSub?.user?.stream?.game + + if (game) { + expect(game).toHaveProperty('boxArtUrl') + expect(game).toHaveProperty('name') + expect(game).toHaveProperty('id') + } + }) +}) diff --git a/src/schema/game-stream-link-type-schema.ts b/src/schema/game-stream-link-type-schema.ts new file mode 100644 index 0000000..ae0bfe4 --- /dev/null +++ b/src/schema/game-stream-link-type-schema.ts @@ -0,0 +1,27 @@ +import { gql, createModule } from 'graphql-modules' +import { HelixStream } from 'twitch' +import { TwitchClients } from '../injections/Twitch-Clients' +import { TwitchId } from '../injections/Twitch-Id' +import { UserId } from '../injections/User-Id' + +export const GameStreamLinkResolvers = { + Stream: { + async game(stream: HelixStream) { + return await stream.getGame() + }, + }, +} + +export const GameStreamLinkSchema = gql` + extend type Stream { + game: Game + } +` + +export const GameStreamLinkModule = createModule({ + id: `game-stream-link-module`, + dirname: __dirname, + providers: [TwitchClients, TwitchId, UserId], + typeDefs: GameStreamLinkSchema, + resolvers: GameStreamLinkResolvers, +}) diff --git a/src/schema/game-type-schema.test.ts b/src/schema/game-type-schema.test.ts index 7069870..5873633 100644 --- a/src/schema/game-type-schema.test.ts +++ b/src/schema/game-type-schema.test.ts @@ -5,6 +5,8 @@ import { StreamModule } from './stream-type-schema' import { GameModule } from './game-type-schema' import { parse, execute } from 'graphql' import { QueryModule } from './query-type-schema' +import { UserSubscriberLinkModule } from './user-subscriber-link-type-schema' +import { GameStreamLinkModule } from './game-stream-link-type-schema' describe('GameModule', () => { it('game should have all fields', async () => { const app = createApplication({ @@ -12,6 +14,8 @@ describe('GameModule', () => { QueryModule, SubscriberModule, UserModule, + UserSubscriberLinkModule, + GameStreamLinkModule, StreamModule, GameModule, ], @@ -22,6 +26,7 @@ describe('GameModule', () => { { latestSub { user{ + displayName stream { game { id @@ -56,6 +61,7 @@ describe('GameModule', () => { SubscriberModule, UserModule, StreamModule, + UserSubscriberLinkModule, GameModule, ], }) diff --git a/src/schema/game-type-schema.ts b/src/schema/game-type-schema.ts index 34ab6b7..32d9e16 100644 --- a/src/schema/game-type-schema.ts +++ b/src/schema/game-type-schema.ts @@ -1,15 +1,10 @@ import { createModule, gql } from 'graphql-modules' -import { HelixStream, HelixGame } from 'twitch/lib' +import { HelixGame } from 'twitch/lib' import { TwitchClients } from '../injections/Twitch-Clients' import { TwitchId } from '../injections/Twitch-Id' import { UserId } from '../injections/User-Id' export const GameResolvers = { - Stream: { - async game(stream: HelixStream) { - return await stream.getGame() - }, - }, Query: { async getGameByName( _parent: {}, @@ -42,10 +37,6 @@ export const GameSchema = gql` name: String! } - extend type Stream { - game: Game - } - extend type Query { getGameByName(gameName: String!): Game } diff --git a/src/schema/stream-type-schema.test.ts b/src/schema/stream-type-schema.test.ts index 104d779..a4a9844 100644 --- a/src/schema/stream-type-schema.test.ts +++ b/src/schema/stream-type-schema.test.ts @@ -4,10 +4,19 @@ import { UserModule } from './user-type-schema' import { StreamModule } from './stream-type-schema' import { parse, execute } from 'graphql' import { QueryModule } from './query-type-schema' +import { UserSubscriberLinkModule } from './user-subscriber-link-type-schema' +import { StreamUserLinkModule } from './stream-user-link-type-schema' describe('StreamModule', () => { it('stream should have all fields', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule, StreamModule], + modules: [ + QueryModule, + SubscriberModule, + UserModule, + UserSubscriberLinkModule, + StreamModule, + StreamUserLinkModule, + ], }) const schema = app.createSchemaForApollo() @@ -15,6 +24,7 @@ describe('StreamModule', () => { { latestSub { user{ + displayName stream { language gameId diff --git a/src/schema/stream-type-schema.ts b/src/schema/stream-type-schema.ts index 43ad0f5..a3e8773 100644 --- a/src/schema/stream-type-schema.ts +++ b/src/schema/stream-type-schema.ts @@ -1,12 +1,7 @@ import { createModule, gql } from 'graphql-modules' -import { HelixUser, HelixStream } from 'twitch/lib' +import { HelixStream } from 'twitch/lib' export const StreamResolvers = { - User: { - async stream(user: HelixUser) { - return await user.getStream() - }, - }, Stream: { language(stream: HelixStream) { return stream.language @@ -32,9 +27,6 @@ export const StreamResolvers = { userId(stream: HelixStream) { return stream.userId }, - async user(stream: HelixStream) { - return await stream.getUser() - }, }, } @@ -48,12 +40,6 @@ export const StreamSchema = gql` thumbnailUrl: String! userDisplayName: String! userId: String! - - user: User - } - - extend type User { - stream: Stream } ` diff --git a/src/schema/stream-user-link-type-schema.test.ts b/src/schema/stream-user-link-type-schema.test.ts new file mode 100644 index 0000000..1bf1241 --- /dev/null +++ b/src/schema/stream-user-link-type-schema.test.ts @@ -0,0 +1,62 @@ +import { createApplication } from 'graphql-modules' +import { SubscriberModule } from './subscriber-type-schema' +import { UserModule } from './user-type-schema' +import { StreamModule } from './stream-type-schema' +import { parse, execute } from 'graphql' +import { QueryModule } from './query-type-schema' +import { UserSubscriberLinkModule } from './user-subscriber-link-type-schema' +import { StreamUserLinkModule } from './stream-user-link-type-schema' +describe('StreamUserLinkModule', () => { + it('stream should have all fields', async () => { + const app = createApplication({ + modules: [ + QueryModule, + SubscriberModule, + UserModule, + UserSubscriberLinkModule, + StreamModule, + StreamUserLinkModule, + ], + }) + const schema = app.createSchemaForApollo() + + const document = parse(` + { + latestSub { + user{ + displayName + stream { + language + gameId + id + title + viewers + thumbnailUrl + userDisplayName + userId + } + } + } + } + `) + const contextValue = { request: {}, response: {} } + const result = await execute({ + schema, + contextValue, + document, + }) + + expect(result?.errors?.length).toBeFalsy() + const stream = result?.data?.latestSub?.user?.stream + if (stream) { + expect(stream).toHaveProperty('language') + expect(stream).toHaveProperty('gameId') + expect(stream).toHaveProperty('id') + expect(stream).toHaveProperty('title') + expect(stream).toHaveProperty('viewers') + expect(stream).toHaveProperty('thumbnailUrl') + expect(stream).toHaveProperty('userDisplayName') + expect(stream).toHaveProperty('userId') + } + }) +}) diff --git a/src/schema/stream-user-link-type-schema.ts b/src/schema/stream-user-link-type-schema.ts new file mode 100644 index 0000000..3aa1e27 --- /dev/null +++ b/src/schema/stream-user-link-type-schema.ts @@ -0,0 +1,31 @@ +import { createModule, gql } from 'graphql-modules' +import { HelixStream, HelixUser } from 'twitch' + +export const StreamUserLinkResolvers = { + Stream: { + async user(stream: HelixStream) { + return await stream.getUser() + }, + }, + User: { + async stream(user: HelixUser) { + return await user.getStream() + }, + }, +} + +export const StreamUserLinkSchema = gql` + extend type User { + stream: Stream + } + extend type Stream { + user: User + } +` + +export const StreamUserLinkModule = createModule({ + id: `stream-user-link-module`, + dirname: __dirname, + typeDefs: StreamUserLinkSchema, + resolvers: StreamUserLinkResolvers, +}) diff --git a/src/schema/subscriber-type-schema.test.ts b/src/schema/subscriber-type-schema.test.ts index ff709dd..4fb0f1e 100644 --- a/src/schema/subscriber-type-schema.test.ts +++ b/src/schema/subscriber-type-schema.test.ts @@ -3,6 +3,7 @@ import { execute, parse } from 'graphql' import { createApplication } from 'graphql-modules' import { UserModule } from './user-type-schema' import { QueryModule } from './query-type-schema' +import { UserSubscriberLinkModule } from './user-subscriber-link-type-schema' describe('SubscriberModule', () => { it('latestSub', async () => { @@ -139,7 +140,12 @@ describe('SubscriberModule', () => { it('sub user', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() diff --git a/src/schema/user-subscriber-link-type-schema.test.ts b/src/schema/user-subscriber-link-type-schema.test.ts new file mode 100644 index 0000000..075a015 --- /dev/null +++ b/src/schema/user-subscriber-link-type-schema.test.ts @@ -0,0 +1,48 @@ +import { createApplication } from 'graphql-modules' +import { SubscriberModule } from './subscriber-type-schema' +import { UserModule } from './user-type-schema' +import { parse, execute } from 'graphql' +import { QueryModule } from './query-type-schema' +import { UserSubscriberLinkModule } from './user-subscriber-link-type-schema' +describe('UserSubscriberLinkModule', () => { + it('user should have all fields', async () => { + const app = createApplication({ + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], + }) + const schema = app.createSchemaForApollo() + + const document = parse(` + { + latestSub { + user{ + displayName + description + id + profilePictureURL + views + } + } + } + `) + const contextValue = { request: {}, response: {} } + const result = await execute({ + schema, + contextValue, + document, + }) + + expect(result?.errors?.length).toBeFalsy() + const user = result?.data?.latestSub?.user + expect(user).toBeTruthy() + expect(user).toHaveProperty('displayName') + expect(user).toHaveProperty('id') + expect(user).toHaveProperty('profilePictureURL') + expect(user).toHaveProperty('views') + expect(user).toHaveProperty('description') + }) +}) diff --git a/src/schema/user-subscriber-link-type-schema.ts b/src/schema/user-subscriber-link-type-schema.ts new file mode 100644 index 0000000..7c1ebb1 --- /dev/null +++ b/src/schema/user-subscriber-link-type-schema.ts @@ -0,0 +1,25 @@ +import { gql, createModule } from 'graphql-modules' +import { HelixSubscription } from 'twitch/lib' +import { TwitchClients } from '../injections/Twitch-Clients' +import { TwitchId } from '../injections/Twitch-Id' +import { UserId } from '../injections/User-Id' + +export const UserSubscriberLinkResolvers = { + Subscriber: { + async user(sub: HelixSubscription) { + return await sub.getUser() + }, + }, +} +export const UserSubscriberLinkSchema = gql` + extend type Subscriber { + user: User! + } +` +export const UserSubscriberLinkModule = createModule({ + id: `user-subscriber-link-module`, + dirname: __dirname, + typeDefs: UserSubscriberLinkSchema, + providers: [TwitchClients, TwitchId, UserId], + resolvers: UserSubscriberLinkResolvers, +}) diff --git a/src/schema/user-type-schema.test.ts b/src/schema/user-type-schema.test.ts index a57b675..5d39ce2 100644 --- a/src/schema/user-type-schema.test.ts +++ b/src/schema/user-type-schema.test.ts @@ -3,11 +3,17 @@ import { SubscriberModule } from './subscriber-type-schema' import { UserModule } from './user-type-schema' import { parse, execute } from 'graphql' import { QueryModule } from './query-type-schema' +import { UserSubscriberLinkModule } from './user-subscriber-link-type-schema' describe('UserModule', () => { it('user should have all fields', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() @@ -42,7 +48,12 @@ describe('UserModule', () => { }) it('getUserById should work', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() @@ -79,7 +90,12 @@ describe('UserModule', () => { }) it('getUserByDisplayName should work', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() @@ -116,7 +132,12 @@ describe('UserModule', () => { }) it('getFollowToId should work', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() @@ -166,7 +187,12 @@ describe('UserModule', () => { }) it('getFollowToDisplayName should work', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() @@ -214,7 +240,12 @@ describe('UserModule', () => { }) it('followsId should work', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() @@ -242,7 +273,12 @@ describe('UserModule', () => { }) it('followsDisplayName should work', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() @@ -270,7 +306,12 @@ describe('UserModule', () => { }) it('follows should work', async () => { const app = createApplication({ - modules: [QueryModule, SubscriberModule, UserModule], + modules: [ + QueryModule, + SubscriberModule, + UserSubscriberLinkModule, + UserModule, + ], }) const schema = app.createSchemaForApollo() diff --git a/src/schema/user-type-schema.ts b/src/schema/user-type-schema.ts index 61e638d..fc99eaf 100644 --- a/src/schema/user-type-schema.ts +++ b/src/schema/user-type-schema.ts @@ -1,15 +1,10 @@ import { createModule, gql } from 'graphql-modules' -import { HelixFollow, HelixSubscription, HelixUser } from 'twitch' +import { HelixFollow, HelixUser } from 'twitch' import { TwitchClients } from '../injections/Twitch-Clients' import { TwitchId } from '../injections/Twitch-Id' import { UserId } from '../injections/User-Id' export const UserResolvers = { - Subscriber: { - async user(sub: HelixSubscription) { - return await sub.getUser() - }, - }, Query: { async getUserByDisplayName( _parent: {}, @@ -154,10 +149,6 @@ export const UserSchema = gql` cursor: String } - extend type Subscriber { - user: User! - } - extend type Query { getUserById(userId: String!): User getUserByDisplayName(displayName: String!): User diff --git a/src/server.ts b/src/server.ts index b3b44a5..84192de 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,6 +7,9 @@ import { UserModule } from './schema/user-type-schema' import { StreamModule } from './schema/stream-type-schema' import { GameModule } from './schema/game-type-schema' import { createApplication } from 'graphql-modules' +import { UserSubscriberLinkModule } from './schema/user-subscriber-link-type-schema' +import { GameStreamLinkModule } from './schema/game-stream-link-type-schema' +import { StreamUserLinkModule } from './schema/stream-user-link-type-schema' require('dotenv').config() let port = 5555 @@ -44,7 +47,10 @@ const app = createApplication({ SubscriberModule, UserModule, StreamModule, + UserSubscriberLinkModule, + GameStreamLinkModule, GameModule, + StreamUserLinkModule, ], }) const execute = app.createExecution()