Skip to content

Commit

Permalink
Returning basic results
Browse files Browse the repository at this point in the history
The initial docs I'd found on user search were wrong. I now realise querying will have to be a multi-step process. This gives me a great opportunity to use resolvers to solve this
  • Loading branch information
jbachhardie committed Jul 12, 2021
1 parent 171b86f commit 51fea2d
Show file tree
Hide file tree
Showing 20 changed files with 279 additions and 206 deletions.
50 changes: 41 additions & 9 deletions backend/api-graphql/src/create-grapql-server.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,56 @@
import { UserSearchPage } from '@backend/domain'
import { SearchService } from '@backend/services'
import { User, UserResult, UserSearchPage } from '@backend/domain'
import { UserService } from '@backend/services'
import { gql } from 'apollo-server'
import { IContext } from './context'
import { createGraphQLServer } from './create-graphql-server'

const mockSearchUsers = jest.fn()
const mockFetchUser = jest.fn()

class MockSearchService extends SearchService {
searchUsers = mockSearchUsers
class MockSearchService extends UserService {
findUsers = mockSearchUsers
fetchUser = mockFetchUser
}

const context = () => ({
const context = (): IContext => ({
services: {
search: new MockSearchService(),
user: new MockSearchService(),
},
})

it('should respond with test results', async () => {
const server = await createGraphQLServer(context)

mockSearchUsers.mockResolvedValue(new UserSearchPage(0, false, []))
mockSearchUsers.mockResolvedValue(
new UserSearchPage(0, false, [new UserResult('test-user', [])])
)
mockFetchUser.mockResolvedValue(
new User(
'test-user',
'http://nuwis.co/wo',
'http://og.na/gat',
642549735,
983133452,
630987351,
1254879450,
'Leroy Phelps',
null,
null,
'[email protected]',
'Dolgicwa'
)
)

const { data, errors } = await server.executeOperation({
query: gql`
query testSearch {
searchUsers(query: "test-query", page: 3) {
resultsCount
results {
login
user {
id
name
}
}
}
}
Expand All @@ -37,8 +61,16 @@ it('should respond with test results', async () => {
expect(data).toMatchObject({
searchUsers: {
resultsCount: 0,
results: [],
results: [
{
user: {
id: 'test-user',
name: 'Leroy Phelps',
},
},
],
},
})
expect(mockSearchUsers).toHaveBeenCalledWith('test-query', 3)
expect(mockFetchUser).toHaveBeenCalledWith('test-user')
})
79 changes: 27 additions & 52 deletions backend/api-graphql/src/generated-resolver-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,47 +33,32 @@ export type ITextMatch = {

export type IUser = {
readonly __typename?: 'User';
readonly login: Scalars['String'];
readonly id: Scalars['Int'];
readonly nodeId: Scalars['String'];
readonly id: Scalars['ID'];
readonly avatarUrl: Scalars['String'];
readonly gravatarId?: Maybe<Scalars['String']>;
readonly url: Scalars['String'];
readonly htmlUrl: Scalars['String'];
readonly followersUrl: Scalars['String'];
readonly subscriptionsUrl: Scalars['String'];
readonly organizationsUrl: Scalars['String'];
readonly reposUrl: Scalars['String'];
readonly receivedEventsUrl: Scalars['String'];
readonly type: Scalars['String'];
readonly score: Scalars['Int'];
readonly followingUrl: Scalars['String'];
readonly gistsUrl: Scalars['String'];
readonly starredUrl: Scalars['String'];
readonly eventsUrl: Scalars['String'];
readonly publicRepos: Scalars['Int'];
readonly publicGists: Scalars['Int'];
readonly followers: Scalars['Int'];
readonly following: Scalars['Int'];
readonly createdAt: Scalars['String'];
readonly updatedAt: Scalars['String'];
readonly name?: Maybe<Scalars['String']>;
readonly bio?: Maybe<Scalars['String']>;
readonly blog?: Maybe<Scalars['String']>;
readonly company?: Maybe<Scalars['String']>;
readonly email?: Maybe<Scalars['String']>;
readonly location?: Maybe<Scalars['String']>;
readonly siteAdmin: Scalars['Boolean'];
readonly hireable?: Maybe<Scalars['Boolean']>;
};

export type IUserResult = {
readonly __typename?: 'UserResult';
readonly textMatches: ReadonlyArray<ITextMatch>;
readonly blog?: Maybe<Scalars['String']>;
readonly company?: Maybe<Scalars['String']>;
readonly suspendedAt?: Maybe<Scalars['String']>;
readonly userId: Scalars['ID'];
readonly user?: Maybe<IUser>;
};

export type IUserSearchPage = {
readonly __typename?: 'UserSearchPage';
readonly resultsCount: Scalars['Int'];
readonly resultsAreIncomplete: Scalars['Boolean'];
readonly results: ReadonlyArray<IUser>;
readonly results: ReadonlyArray<IUserResult>;
};


Expand Down Expand Up @@ -146,8 +131,10 @@ export type IResolversTypes = {
Int: ResolverTypeWrapper<Scalars['Int']>;
TextMatch: ResolverTypeWrapper<ITextMatch>;
User: ResolverTypeWrapper<IUser>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
ID: ResolverTypeWrapper<Scalars['ID']>;
UserResult: ResolverTypeWrapper<IUserResult>;
UserSearchPage: ResolverTypeWrapper<IUserSearchPage>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
};

/** Mapping between all available schema types and the resolvers parents */
Expand All @@ -157,8 +144,10 @@ export type IResolversParentTypes = {
Int: Scalars['Int'];
TextMatch: ITextMatch;
User: IUser;
Boolean: Scalars['Boolean'];
ID: Scalars['ID'];
UserResult: IUserResult;
UserSearchPage: IUserSearchPage;
Boolean: Scalars['Boolean'];
};

export type IQueryResolvers<ContextType = IContext, ParentType extends IResolversParentTypes['Query'] = IResolversParentTypes['Query']> = {
Expand All @@ -172,54 +161,40 @@ export type ITextMatchResolvers<ContextType = IContext, ParentType extends IReso
};

export type IUserResolvers<ContextType = IContext, ParentType extends IResolversParentTypes['User'] = IResolversParentTypes['User']> = {
login?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<IResolversTypes['Int'], ParentType, ContextType>;
nodeId?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<IResolversTypes['ID'], ParentType, ContextType>;
avatarUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
gravatarId?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
url?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
htmlUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
followersUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
subscriptionsUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
organizationsUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
reposUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
receivedEventsUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
type?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
score?: Resolver<IResolversTypes['Int'], ParentType, ContextType>;
followingUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
gistsUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
starredUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
eventsUrl?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
publicRepos?: Resolver<IResolversTypes['Int'], ParentType, ContextType>;
publicGists?: Resolver<IResolversTypes['Int'], ParentType, ContextType>;
followers?: Resolver<IResolversTypes['Int'], ParentType, ContextType>;
following?: Resolver<IResolversTypes['Int'], ParentType, ContextType>;
createdAt?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<IResolversTypes['String'], ParentType, ContextType>;
name?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
bio?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
blog?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
company?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
email?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
location?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
siteAdmin?: Resolver<IResolversTypes['Boolean'], ParentType, ContextType>;
hireable?: Resolver<Maybe<IResolversTypes['Boolean']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type IUserResultResolvers<ContextType = IContext, ParentType extends IResolversParentTypes['UserResult'] = IResolversParentTypes['UserResult']> = {
textMatches?: Resolver<ReadonlyArray<IResolversTypes['TextMatch']>, ParentType, ContextType>;
blog?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
company?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
suspendedAt?: Resolver<Maybe<IResolversTypes['String']>, ParentType, ContextType>;
userId?: Resolver<IResolversTypes['ID'], ParentType, ContextType>;
user?: Resolver<Maybe<IResolversTypes['User']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type IUserSearchPageResolvers<ContextType = IContext, ParentType extends IResolversParentTypes['UserSearchPage'] = IResolversParentTypes['UserSearchPage']> = {
resultsCount?: Resolver<IResolversTypes['Int'], ParentType, ContextType>;
resultsAreIncomplete?: Resolver<IResolversTypes['Boolean'], ParentType, ContextType>;
results?: Resolver<ReadonlyArray<IResolversTypes['User']>, ParentType, ContextType>;
results?: Resolver<ReadonlyArray<IResolversTypes['UserResult']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type IResolvers<ContextType = IContext> = {
Query?: IQueryResolvers<ContextType>;
TextMatch?: ITextMatchResolvers<ContextType>;
User?: IUserResolvers<ContextType>;
UserResult?: IUserResultResolvers<ContextType>;
UserSearchPage?: IUserSearchPageResolvers<ContextType>;
};

Expand Down
7 changes: 6 additions & 1 deletion backend/api-graphql/src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { IResolvers } from './generated-resolver-types'
export const resolvers: IResolvers<IContext> = {
Query: {
async searchUsers(_parent, args, context) {
return await context.services.search.searchUsers(args.query, args.page)
return await context.services.user.findUsers(args.query, args.page)
},
},
UserResult: {
async user(parent, _args, context) {
return await context.services.user.fetchUser(parent.userId)
},
},
}
36 changes: 10 additions & 26 deletions backend/api-graphql/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,28 @@ export const typeDefs = gql`
type UserSearchPage {
resultsCount: Int!
resultsAreIncomplete: Boolean!
results: [User!]!
results: [UserResult!]!
}
type UserResult {
textMatches: [TextMatch!]!
userId: ID!
user: User
}
type User {
login: String!
id: Int!
nodeId: String!
id: ID!
avatarUrl: String!
gravatarId: String
url: String!
htmlUrl: String!
followersUrl: String!
subscriptionsUrl: String!
organizationsUrl: String!
reposUrl: String!
receivedEventsUrl: String!
type: String!
score: Int!
followingUrl: String!
gistsUrl: String!
starredUrl: String!
eventsUrl: String!
publicRepos: Int!
publicGists: Int!
followers: Int!
following: Int!
createdAt: String!
updatedAt: String!
name: String
bio: String
email: String
location: String
siteAdmin: Boolean!
hireable: Boolean
textMatches: [TextMatch!]!
blog: String
company: String
suspendedAt: String
email: String
location: String
}
type TextMatch {
Expand Down
6 changes: 5 additions & 1 deletion backend/data-sources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
"main": "src/index.ts",
"types": "dist/index.d.ts",
"esbuild": "src/index.ts",
"license": "None"
"license": "None",
"dependencies": {
"axios": "^0.21.1",
"zod": "^3.5.1"
}
}
8 changes: 0 additions & 8 deletions backend/data-sources/src/github-rest-search-service.ts

This file was deleted.

Loading

0 comments on commit 51fea2d

Please sign in to comment.