Add list integrations API and integration tests

This commit is contained in:
Hongbo Wu
2022-08-08 18:12:44 +08:00
parent 0cf3f58258
commit 4aa62d6031
8 changed files with 246 additions and 15 deletions

View File

@ -479,6 +479,24 @@ export type DeleteHighlightSuccess = {
highlight: Highlight;
};
export type DeleteIntegrationError = {
__typename?: 'DeleteIntegrationError';
errorCodes: Array<DeleteIntegrationErrorCode>;
};
export enum DeleteIntegrationErrorCode {
BadRequest = 'BAD_REQUEST',
NotFound = 'NOT_FOUND',
Unauthorized = 'UNAUTHORIZED'
}
export type DeleteIntegrationResult = DeleteIntegrationError | DeleteIntegrationSuccess;
export type DeleteIntegrationSuccess = {
__typename?: 'DeleteIntegrationSuccess';
integration: Integration;
};
export type DeleteLabelError = {
__typename?: 'DeleteLabelError';
errorCodes: Array<DeleteLabelErrorCode>;
@ -759,6 +777,23 @@ export enum IntegrationType {
Readwise = 'READWISE'
}
export type IntegrationsError = {
__typename?: 'IntegrationsError';
errorCodes: Array<IntegrationsErrorCode>;
};
export enum IntegrationsErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}
export type IntegrationsResult = IntegrationsError | IntegrationsSuccess;
export type IntegrationsSuccess = {
__typename?: 'IntegrationsSuccess';
integrations: Array<Integration>;
};
export type Label = {
__typename?: 'Label';
color: Scalars['String'];
@ -918,6 +953,7 @@ export type Mutation = {
deleteAccount: DeleteAccountResult;
deleteHighlight: DeleteHighlightResult;
deleteHighlightReply: DeleteHighlightReplyResult;
deleteIntegration: DeleteIntegrationResult;
deleteLabel: DeleteLabelResult;
deleteNewsletterEmail: DeleteNewsletterEmailResult;
deleteReaction: DeleteReactionResult;
@ -1016,6 +1052,11 @@ export type MutationDeleteHighlightReplyArgs = {
};
export type MutationDeleteIntegrationArgs = {
id: Scalars['ID'];
};
export type MutationDeleteLabelArgs = {
id: Scalars['ID'];
};
@ -1306,6 +1347,7 @@ export type Query = {
getFollowing: GetFollowingResult;
getUserPersonalization: GetUserPersonalizationResult;
hello?: Maybe<Scalars['String']>;
integrations: IntegrationsResult;
labels: LabelsResult;
me?: Maybe<User>;
newsletterEmails: NewsletterEmailsResult;
@ -2584,6 +2626,10 @@ export type ResolversTypes = {
DeleteHighlightReplySuccess: ResolverTypeWrapper<DeleteHighlightReplySuccess>;
DeleteHighlightResult: ResolversTypes['DeleteHighlightError'] | ResolversTypes['DeleteHighlightSuccess'];
DeleteHighlightSuccess: ResolverTypeWrapper<DeleteHighlightSuccess>;
DeleteIntegrationError: ResolverTypeWrapper<DeleteIntegrationError>;
DeleteIntegrationErrorCode: DeleteIntegrationErrorCode;
DeleteIntegrationResult: ResolversTypes['DeleteIntegrationError'] | ResolversTypes['DeleteIntegrationSuccess'];
DeleteIntegrationSuccess: ResolverTypeWrapper<DeleteIntegrationSuccess>;
DeleteLabelError: ResolverTypeWrapper<DeleteLabelError>;
DeleteLabelErrorCode: DeleteLabelErrorCode;
DeleteLabelResult: ResolversTypes['DeleteLabelError'] | ResolversTypes['DeleteLabelSuccess'];
@ -2641,6 +2687,10 @@ export type ResolversTypes = {
Int: ResolverTypeWrapper<Scalars['Int']>;
Integration: ResolverTypeWrapper<Integration>;
IntegrationType: IntegrationType;
IntegrationsError: ResolverTypeWrapper<IntegrationsError>;
IntegrationsErrorCode: IntegrationsErrorCode;
IntegrationsResult: ResolversTypes['IntegrationsError'] | ResolversTypes['IntegrationsSuccess'];
IntegrationsSuccess: ResolverTypeWrapper<IntegrationsSuccess>;
Label: ResolverTypeWrapper<Label>;
LabelsError: ResolverTypeWrapper<LabelsError>;
LabelsErrorCode: LabelsErrorCode;
@ -2938,6 +2988,9 @@ export type ResolversParentTypes = {
DeleteHighlightReplySuccess: DeleteHighlightReplySuccess;
DeleteHighlightResult: ResolversParentTypes['DeleteHighlightError'] | ResolversParentTypes['DeleteHighlightSuccess'];
DeleteHighlightSuccess: DeleteHighlightSuccess;
DeleteIntegrationError: DeleteIntegrationError;
DeleteIntegrationResult: ResolversParentTypes['DeleteIntegrationError'] | ResolversParentTypes['DeleteIntegrationSuccess'];
DeleteIntegrationSuccess: DeleteIntegrationSuccess;
DeleteLabelError: DeleteLabelError;
DeleteLabelResult: ResolversParentTypes['DeleteLabelError'] | ResolversParentTypes['DeleteLabelSuccess'];
DeleteLabelSuccess: DeleteLabelSuccess;
@ -2984,6 +3037,9 @@ export type ResolversParentTypes = {
ID: Scalars['ID'];
Int: Scalars['Int'];
Integration: Integration;
IntegrationsError: IntegrationsError;
IntegrationsResult: ResolversParentTypes['IntegrationsError'] | ResolversParentTypes['IntegrationsSuccess'];
IntegrationsSuccess: IntegrationsSuccess;
Label: Label;
LabelsError: LabelsError;
LabelsResult: ResolversParentTypes['LabelsError'] | ResolversParentTypes['LabelsSuccess'];
@ -3488,6 +3544,20 @@ export type DeleteHighlightSuccessResolvers<ContextType = ResolverContext, Paren
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DeleteIntegrationErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteIntegrationError'] = ResolversParentTypes['DeleteIntegrationError']> = {
errorCodes?: Resolver<Array<ResolversTypes['DeleteIntegrationErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DeleteIntegrationResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteIntegrationResult'] = ResolversParentTypes['DeleteIntegrationResult']> = {
__resolveType: TypeResolveFn<'DeleteIntegrationError' | 'DeleteIntegrationSuccess', ParentType, ContextType>;
};
export type DeleteIntegrationSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteIntegrationSuccess'] = ResolversParentTypes['DeleteIntegrationSuccess']> = {
integration?: Resolver<ResolversTypes['Integration'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DeleteLabelErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteLabelError'] = ResolversParentTypes['DeleteLabelError']> = {
errorCodes?: Resolver<Array<ResolversTypes['DeleteLabelErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -3713,6 +3783,20 @@ export type IntegrationResolvers<ContextType = ResolverContext, ParentType exten
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type IntegrationsErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['IntegrationsError'] = ResolversParentTypes['IntegrationsError']> = {
errorCodes?: Resolver<Array<ResolversTypes['IntegrationsErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type IntegrationsResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['IntegrationsResult'] = ResolversParentTypes['IntegrationsResult']> = {
__resolveType: TypeResolveFn<'IntegrationsError' | 'IntegrationsSuccess', ParentType, ContextType>;
};
export type IntegrationsSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['IntegrationsSuccess'] = ResolversParentTypes['IntegrationsSuccess']> = {
integrations?: Resolver<Array<ResolversTypes['Integration']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type LabelResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Label'] = ResolversParentTypes['Label']> = {
color?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
createdAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
@ -3831,6 +3915,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
deleteAccount?: Resolver<ResolversTypes['DeleteAccountResult'], ParentType, ContextType, RequireFields<MutationDeleteAccountArgs, 'userID'>>;
deleteHighlight?: Resolver<ResolversTypes['DeleteHighlightResult'], ParentType, ContextType, RequireFields<MutationDeleteHighlightArgs, 'highlightId'>>;
deleteHighlightReply?: Resolver<ResolversTypes['DeleteHighlightReplyResult'], ParentType, ContextType, RequireFields<MutationDeleteHighlightReplyArgs, 'highlightReplyId'>>;
deleteIntegration?: Resolver<ResolversTypes['DeleteIntegrationResult'], ParentType, ContextType, RequireFields<MutationDeleteIntegrationArgs, 'id'>>;
deleteLabel?: Resolver<ResolversTypes['DeleteLabelResult'], ParentType, ContextType, RequireFields<MutationDeleteLabelArgs, 'id'>>;
deleteNewsletterEmail?: Resolver<ResolversTypes['DeleteNewsletterEmailResult'], ParentType, ContextType, RequireFields<MutationDeleteNewsletterEmailArgs, 'newsletterEmailId'>>;
deleteReaction?: Resolver<ResolversTypes['DeleteReactionResult'], ParentType, ContextType, RequireFields<MutationDeleteReactionArgs, 'id'>>;
@ -3940,6 +4025,7 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
getFollowing?: Resolver<ResolversTypes['GetFollowingResult'], ParentType, ContextType, Partial<QueryGetFollowingArgs>>;
getUserPersonalization?: Resolver<ResolversTypes['GetUserPersonalizationResult'], ParentType, ContextType>;
hello?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
integrations?: Resolver<ResolversTypes['IntegrationsResult'], ParentType, ContextType>;
labels?: Resolver<ResolversTypes['LabelsResult'], ParentType, ContextType>;
me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
newsletterEmails?: Resolver<ResolversTypes['NewsletterEmailsResult'], ParentType, ContextType>;
@ -4662,6 +4748,9 @@ export type Resolvers<ContextType = ResolverContext> = {
DeleteHighlightReplySuccess?: DeleteHighlightReplySuccessResolvers<ContextType>;
DeleteHighlightResult?: DeleteHighlightResultResolvers<ContextType>;
DeleteHighlightSuccess?: DeleteHighlightSuccessResolvers<ContextType>;
DeleteIntegrationError?: DeleteIntegrationErrorResolvers<ContextType>;
DeleteIntegrationResult?: DeleteIntegrationResultResolvers<ContextType>;
DeleteIntegrationSuccess?: DeleteIntegrationSuccessResolvers<ContextType>;
DeleteLabelError?: DeleteLabelErrorResolvers<ContextType>;
DeleteLabelResult?: DeleteLabelResultResolvers<ContextType>;
DeleteLabelSuccess?: DeleteLabelSuccessResolvers<ContextType>;
@ -4702,6 +4791,9 @@ export type Resolvers<ContextType = ResolverContext> = {
HighlightReply?: HighlightReplyResolvers<ContextType>;
HighlightStats?: HighlightStatsResolvers<ContextType>;
Integration?: IntegrationResolvers<ContextType>;
IntegrationsError?: IntegrationsErrorResolvers<ContextType>;
IntegrationsResult?: IntegrationsResultResolvers<ContextType>;
IntegrationsSuccess?: IntegrationsSuccessResolvers<ContextType>;
Label?: LabelResolvers<ContextType>;
LabelsError?: LabelsErrorResolvers<ContextType>;
LabelsResult?: LabelsResultResolvers<ContextType>;

View File

@ -420,6 +420,22 @@ type DeleteHighlightSuccess {
highlight: Highlight!
}
type DeleteIntegrationError {
errorCodes: [DeleteIntegrationErrorCode!]!
}
enum DeleteIntegrationErrorCode {
BAD_REQUEST
NOT_FOUND
UNAUTHORIZED
}
union DeleteIntegrationResult = DeleteIntegrationError | DeleteIntegrationSuccess
type DeleteIntegrationSuccess {
integration: Integration!
}
type DeleteLabelError {
errorCodes: [DeleteLabelErrorCode!]!
}
@ -671,6 +687,21 @@ enum IntegrationType {
READWISE
}
type IntegrationsError {
errorCodes: [IntegrationsErrorCode!]!
}
enum IntegrationsErrorCode {
BAD_REQUEST
UNAUTHORIZED
}
union IntegrationsResult = IntegrationsError | IntegrationsSuccess
type IntegrationsSuccess {
integrations: [Integration!]!
}
type Label {
color: String!
createdAt: Date
@ -816,6 +847,7 @@ type Mutation {
deleteAccount(userID: ID!): DeleteAccountResult!
deleteHighlight(highlightId: ID!): DeleteHighlightResult!
deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult!
deleteIntegration(id: ID!): DeleteIntegrationResult!
deleteLabel(id: ID!): DeleteLabelResult!
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
deleteReaction(id: ID!): DeleteReactionResult!
@ -947,6 +979,7 @@ type Query {
getFollowing(userId: ID): GetFollowingResult!
getUserPersonalization: GetUserPersonalizationResult!
hello: String
integrations: IntegrationsResult!
labels: LabelsResult!
me: User
newsletterEmails: NewsletterEmailsResult!

View File

@ -49,6 +49,7 @@ import {
getUserResolver,
googleLoginResolver,
googleSignupResolver,
integrationsResolver,
labelsResolver,
logOutResolver,
mergeHighlightResolver,
@ -66,6 +67,7 @@ import {
setBookmarkArticleResolver,
setDeviceTokenResolver,
setFollowResolver,
setIntegrationResolver,
setLabelsForHighlightResolver,
setLabelsResolver,
setLinkArchivedResolver,
@ -97,7 +99,6 @@ import {
generateUploadFilePathName,
} from '../utils/uploads'
import { getPageByParam } from '../elastic/pages'
import { setIntegrationResolver } from './integrations'
/* eslint-disable @typescript-eslint/naming-convention */
type ResultResolveType = {
@ -192,6 +193,7 @@ export const functionResolvers = {
apiKeys: apiKeysResolver,
typeaheadSearch: typeaheadSearchResolver,
updatesSince: updatesSinceResolver,
integrations: integrationsResolver,
},
User: {
async sharedArticles(
@ -595,4 +597,5 @@ export const functionResolvers = {
...resultResolveTypeResolver('UpdatesSince'),
...resultResolveTypeResolver('MoveLabel'),
...resultResolveTypeResolver('SetIntegration'),
...resultResolveTypeResolver('Integrations'),
}

View File

@ -20,3 +20,4 @@ export * from './update'
export * from './popular_reads'
export * from './webhooks'
export * from './api_key'
export * from './integrations'

View File

@ -1,5 +1,8 @@
import { authorized } from '../../utils/helpers'
import {
IntegrationsError,
IntegrationsErrorCode,
IntegrationsSuccess,
MutationSetIntegrationArgs,
SetIntegrationError,
SetIntegrationErrorCode,
@ -119,3 +122,30 @@ export const setIntegrationResolver = authorized<
}
}
})
export const integrationsResolver = authorized<
IntegrationsSuccess,
IntegrationsError
>(async (_, __, { claims: { uid }, log }) => {
try {
const user = await getRepository(User).findOneBy({ id: uid })
if (!user) {
return {
errorCodes: [IntegrationsErrorCode.Unauthorized],
}
}
const integrations = await getRepository(Integration).findBy({
user: { id: uid },
})
return {
integrations,
}
} catch (error) {
log.error(error)
return {
errorCodes: [IntegrationsErrorCode.BadRequest],
}
}
})

View File

@ -1850,6 +1850,39 @@ const schema = gql`
enabled: Boolean!
}
union IntegrationsResult = IntegrationsSuccess | IntegrationsError
type IntegrationsSuccess {
integrations: [Integration!]!
}
type IntegrationsError {
errorCodes: [IntegrationsErrorCode!]!
}
enum IntegrationsErrorCode {
UNAUTHORIZED
BAD_REQUEST
}
union DeleteIntegrationResult =
DeleteIntegrationSuccess
| DeleteIntegrationError
type DeleteIntegrationSuccess {
integration: Integration!
}
type DeleteIntegrationError {
errorCodes: [DeleteIntegrationErrorCode!]!
}
enum DeleteIntegrationErrorCode {
UNAUTHORIZED
BAD_REQUEST
NOT_FOUND
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -1920,6 +1953,7 @@ const schema = gql`
setLabelsForHighlight(input: SetLabelsForHighlightInput!): SetLabelsResult!
moveLabel(input: MoveLabelInput!): MoveLabelResult!
setIntegration(input: SetIntegrationInput!): SetIntegrationResult!
deleteIntegration(id: ID!): DeleteIntegrationResult!
}
# FIXME: remove sort from feedArticles after all cached tabs are closed
@ -1965,6 +1999,7 @@ const schema = gql`
apiKeys: ApiKeysResult!
typeaheadSearch(query: String!, first: Int): TypeaheadSearchResult!
updatesSince(after: String, first: Int, since: Date!): UpdatesSinceResult!
integrations: IntegrationsResult!
}
`

View File

@ -64,10 +64,11 @@ describe('Integrations resolvers', () => {
let token: string
let integrationType: IntegrationType
let enabled: boolean
let scope: nock.Scope
// mock Readwise Auth API
before(() => {
nock(READWISE_API_URL, {
scope = nock(READWISE_API_URL, {
reqheaders: { Authorization: `Token ${validToken}` },
})
.get('/auth')
@ -76,7 +77,7 @@ describe('Integrations resolvers', () => {
})
after(() => {
nock.cleanAll()
scope.persist(false)
})
context('when id is not in the request', () => {
@ -300,4 +301,48 @@ describe('Integrations resolvers', () => {
})
})
})
describe('integrations API', () => {
const query = `
query {
integrations {
... on IntegrationsSuccess {
integrations {
id
type
enabled
}
}
}
}
`
let existingIntegration: Integration
before(async () => {
existingIntegration = await getRepository(Integration).save({
user: loginUser,
type: DataIntegrationType.Readwise,
token: 'fakeToken',
})
})
after(async () => {
await getRepository(Integration).delete(existingIntegration.id)
})
it('returns all integrations', async () => {
const res = await graphqlRequest(query, authToken)
expect(res.body.data.integrations.integrations).to.have.length(1)
expect(res.body.data.integrations.integrations[0].id).to.equal(
existingIntegration.id
)
expect(res.body.data.integrations.integrations[0].type).to.equal(
existingIntegration.type
)
expect(res.body.data.integrations.integrations[0].enabled).to.equal(
existingIntegration.enabled
)
})
})
})

View File

@ -58,9 +58,7 @@ describe('Labels API', () => {
after(async () => {
// clean up
for (const label of labels) {
await getRepository(Label).delete(label.id)
}
await getRepository(Label).delete(labels.map((l) => l.id))
})
beforeEach(() => {
@ -345,9 +343,7 @@ describe('Labels API', () => {
after(async () => {
// clean up
for (const label of labels) {
await getRepository(Label).delete(label.id)
}
await getRepository(Label).delete(labels.map((l) => l.id))
await deletePage(page.id, ctx)
})
@ -547,9 +543,7 @@ describe('Labels API', () => {
after(async () => {
// clean up
for (const label of labels) {
await getRepository(Label).delete(label.id)
}
await getRepository(Label).delete(labels.map((l) => l.id))
await deletePage(page.id, ctx)
})
@ -677,9 +671,7 @@ describe('Labels API', () => {
after(async () => {
// clean up
for (const label of labels) {
await getRepository(Label).delete(label.id)
}
await getRepository(Label).delete(labels.map((l) => l.id))
})
context('when label exists', () => {