diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 4e9c0735d..7f25a12d1 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1274,6 +1274,22 @@ export type Integration = { updatedAt?: Maybe; }; +export type IntegrationError = { + __typename?: 'IntegrationError'; + errorCodes: Array; +}; + +export enum IntegrationErrorCode { + NotFound = 'NOT_FOUND' +} + +export type IntegrationResult = IntegrationError | IntegrationSuccess; + +export type IntegrationSuccess = { + __typename?: 'IntegrationSuccess'; + integration: Integration; +}; + export enum IntegrationType { Export = 'EXPORT', Import = 'IMPORT' @@ -2096,6 +2112,7 @@ export type Query = { getUserPersonalization: GetUserPersonalizationResult; groups: GroupsResult; hello?: Maybe; + integration: IntegrationResult; integrations: IntegrationsResult; labels: LabelsResult; me?: Maybe; @@ -2143,6 +2160,11 @@ export type QueryGetDiscoverFeedArticlesArgs = { }; +export type QueryIntegrationArgs = { + id: Scalars['ID']; +}; + + export type QueryRulesArgs = { enabled?: InputMaybe; }; @@ -4063,6 +4085,10 @@ export type ResolversTypes = { ImportItemState: ImportItemState; Int: ResolverTypeWrapper; Integration: ResolverTypeWrapper; + IntegrationError: ResolverTypeWrapper; + IntegrationErrorCode: IntegrationErrorCode; + IntegrationResult: ResolversTypes['IntegrationError'] | ResolversTypes['IntegrationSuccess']; + IntegrationSuccess: ResolverTypeWrapper; IntegrationType: IntegrationType; IntegrationsError: ResolverTypeWrapper; IntegrationsErrorCode: IntegrationsErrorCode; @@ -4594,6 +4620,9 @@ export type ResolversParentTypes = { ImportFromIntegrationSuccess: ImportFromIntegrationSuccess; Int: Scalars['Int']; Integration: Integration; + IntegrationError: IntegrationError; + IntegrationResult: ResolversParentTypes['IntegrationError'] | ResolversParentTypes['IntegrationSuccess']; + IntegrationSuccess: IntegrationSuccess; IntegrationsError: IntegrationsError; IntegrationsResult: ResolversParentTypes['IntegrationsError'] | ResolversParentTypes['IntegrationsSuccess']; IntegrationsSuccess: IntegrationsSuccess; @@ -5777,6 +5806,20 @@ export type IntegrationResolvers; }; +export type IntegrationErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type IntegrationResultResolvers = { + __resolveType: TypeResolveFn<'IntegrationError' | 'IntegrationSuccess', ParentType, ContextType>; +}; + +export type IntegrationSuccessResolvers = { + integration?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type IntegrationsErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -6132,6 +6175,7 @@ export type QueryResolvers; groups?: Resolver; hello?: Resolver, ParentType, ContextType>; + integration?: Resolver>; integrations?: Resolver; labels?: Resolver; me?: Resolver, ParentType, ContextType>; @@ -7277,6 +7321,9 @@ export type Resolvers = { ImportFromIntegrationResult?: ImportFromIntegrationResultResolvers; ImportFromIntegrationSuccess?: ImportFromIntegrationSuccessResolvers; Integration?: IntegrationResolvers; + IntegrationError?: IntegrationErrorResolvers; + IntegrationResult?: IntegrationResultResolvers; + IntegrationSuccess?: IntegrationSuccessResolvers; IntegrationsError?: IntegrationsErrorResolvers; IntegrationsResult?: IntegrationsResultResolvers; IntegrationsSuccess?: IntegrationsSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 60ad805cd..865e0cb6e 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1140,6 +1140,20 @@ type Integration { updatedAt: Date } +type IntegrationError { + errorCodes: [IntegrationErrorCode!]! +} + +enum IntegrationErrorCode { + NOT_FOUND +} + +union IntegrationResult = IntegrationError | IntegrationSuccess + +type IntegrationSuccess { + integration: Integration! +} + enum IntegrationType { EXPORT IMPORT @@ -1591,6 +1605,7 @@ type Query { getUserPersonalization: GetUserPersonalizationResult! groups: GroupsResult! hello: String + integration(id: ID!): IntegrationResult! integrations: IntegrationsResult! labels: LabelsResult! me: User diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 443769a3f..c70f7145d 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -22,6 +22,8 @@ import { SearchItem, User, } from '../generated/graphql' +import { getAISummary } from '../services/ai-summaries' +import { findUserFeatures } from '../services/features' import { findHighlightsByLibraryItemId } from '../services/highlights' import { findLabelsByLibraryItemId } from '../services/labels' import { findRecommendationsByLibraryItemId } from '../services/recommendation' @@ -79,6 +81,7 @@ import { googleSignupResolver, groupsResolver, importFromIntegrationResolver, + integrationResolver, integrationsResolver, joinGroupResolver, labelsResolver, @@ -150,8 +153,6 @@ import { deleteDiscoverFeedsResolver, editDiscoverFeedsResolver, } from './discover_feeds' -import { getAISummary } from '../services/ai-summaries' -import { findUserFeatures, getFeatureName } from '../services/features' /* eslint-disable @typescript-eslint/naming-convention */ type ResultResolveType = { @@ -348,6 +349,7 @@ export const functionResolvers = { recentEmails: recentEmailsResolver, feeds: feedsResolver, scanFeeds: scanFeedsResolver, + integration: integrationResolver, }, User: { async intercomHash( @@ -662,4 +664,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('UpdateNewsletterEmail'), ...resultResolveTypeResolver('EmptyTrash'), ...resultResolveTypeResolver('FetchContent'), + ...resultResolveTypeResolver('Integration'), } diff --git a/packages/api/src/resolvers/integrations/index.ts b/packages/api/src/resolvers/integrations/index.ts index d0bfc45d1..ffc11e4f8 100644 --- a/packages/api/src/resolvers/integrations/index.ts +++ b/packages/api/src/resolvers/integrations/index.ts @@ -12,12 +12,15 @@ import { ImportFromIntegrationError, ImportFromIntegrationErrorCode, ImportFromIntegrationSuccess, + IntegrationError, + IntegrationErrorCode, IntegrationsError, - IntegrationsErrorCode, IntegrationsSuccess, + IntegrationSuccess, MutationDeleteIntegrationArgs, MutationImportFromIntegrationArgs, MutationSetIntegrationArgs, + QueryIntegrationArgs, SetIntegrationError, SetIntegrationErrorCode, SetIntegrationSuccess, @@ -42,85 +45,89 @@ export const setIntegrationResolver = authorized< SetIntegrationSuccess, SetIntegrationError, MutationSetIntegrationArgs ->(async (_, { input }, { uid, log }) => { - try { - const integrationToSave: DeepPartial = { - ...input, - user: { id: uid }, - id: input.id || undefined, - type: input.type || IntegrationType.Export, - syncedAt: input.syncedAt ? new Date(input.syncedAt) : undefined, - importItemState: - input.type === IntegrationType.Import - ? input.importItemState || ImportItemState.Unarchived // default to unarchived - : undefined, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - settings: input.settings, - } - if (input.id) { - // Update - const existingIntegration = await findIntegration({ id: input.id }, uid) - if (!existingIntegration) { - return { - errorCodes: [SetIntegrationErrorCode.NotFound], - } +>(async (_, { input }, { uid }) => { + const integrationToSave: DeepPartial = { + ...input, + user: { id: uid }, + id: input.id || undefined, + type: input.type || IntegrationType.Export, + syncedAt: input.syncedAt ? new Date(input.syncedAt) : undefined, + importItemState: + input.type === IntegrationType.Import + ? input.importItemState || ImportItemState.Unarchived // default to unarchived + : undefined, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + settings: input.settings, + } + if (input.id) { + // Update + const existingIntegration = await findIntegration({ id: input.id }, uid) + if (!existingIntegration) { + return { + errorCodes: [SetIntegrationErrorCode.NotFound], } + } - integrationToSave.id = existingIntegration.id - integrationToSave.taskName = existingIntegration.taskName - } else { - // Create - const integrationService = getIntegrationClient(input.name, input.token) - // authorize and get access token - const token = await integrationService.accessToken() - if (!token) { - return { - errorCodes: [SetIntegrationErrorCode.InvalidToken], - } + integrationToSave.id = existingIntegration.id + integrationToSave.taskName = existingIntegration.taskName + } else { + // Create + const integrationService = getIntegrationClient(input.name, input.token) + // authorize and get access token + const token = await integrationService.accessToken() + if (!token) { + return { + errorCodes: [SetIntegrationErrorCode.InvalidToken], } - integrationToSave.token = token } + integrationToSave.token = token + } - // save integration - const integration = await saveIntegration(integrationToSave, uid) + // save integration + const integration = await saveIntegration(integrationToSave, uid) - analytics.capture({ - distinctId: uid, - event: 'integration_set', - properties: { - id: integrationToSave.id, - env: env.server.apiEnv, - }, - }) + analytics.capture({ + distinctId: uid, + event: 'integration_set', + properties: { + id: integrationToSave.id, + env: env.server.apiEnv, + }, + }) - return { - integration, - } - } catch (error) { - log.error(error) - - return { - errorCodes: [SetIntegrationErrorCode.BadRequest], - } + return { + integration, } }) export const integrationsResolver = authorized< IntegrationsSuccess, IntegrationsError ->(async (_, __, { uid, log }) => { - try { - const integrations = await findIntegrations(uid) +>(async (_, __, { uid }) => { + const integrations = await findIntegrations(uid) + + return { + integrations, + } +}) + +export const integrationResolver = authorized< + IntegrationSuccess, + IntegrationError, + QueryIntegrationArgs +>(async (_, { id }, { uid, log }) => { + const integration = await findIntegration({ id }, uid) + + if (!integration) { + log.error('integration not found', id) return { - integrations, + errorCodes: [IntegrationErrorCode.NotFound], } - } catch (error) { - log.error(error) + } - return { - errorCodes: [IntegrationsErrorCode.BadRequest], - } + return { + integration, } }) @@ -131,42 +138,34 @@ export const deleteIntegrationResolver = authorized< >(async (_, { id }, { claims: { uid }, log }) => { log.info('deleteIntegrationResolver') - try { - const integration = await findIntegration({ id }, uid) - - if (!integration) { - return { - errorCodes: [DeleteIntegrationErrorCode.NotFound], - } - } - - if (integration.taskName) { - // delete the task if task exists - await deleteTask(integration.taskName) - log.info('task deleted', integration.taskName) - } - - const deletedIntegration = await removeIntegration(integration, uid) - deletedIntegration.id = id - - analytics.capture({ - distinctId: uid, - event: 'integration_delete', - properties: { - integrationId: deletedIntegration.id, - env: env.server.apiEnv, - }, - }) + const integration = await findIntegration({ id }, uid) + if (!integration) { return { - integration, + errorCodes: [DeleteIntegrationErrorCode.NotFound], } - } catch (error) { - log.error(error) + } - return { - errorCodes: [DeleteIntegrationErrorCode.BadRequest], - } + if (integration.taskName) { + // delete the task if task exists + await deleteTask(integration.taskName) + log.info('task deleted', integration.taskName) + } + + const deletedIntegration = await removeIntegration(integration, uid) + deletedIntegration.id = id + + analytics.capture({ + distinctId: uid, + event: 'integration_delete', + properties: { + integrationId: deletedIntegration.id, + env: env.server.apiEnv, + }, + }) + + return { + integration, } }) @@ -175,52 +174,44 @@ export const importFromIntegrationResolver = authorized< ImportFromIntegrationError, MutationImportFromIntegrationArgs >(async (_, { integrationId }, { claims: { uid }, log }) => { - try { - const integration = await findIntegration({ id: integrationId }, uid) - - if (!integration) { - return { - errorCodes: [ImportFromIntegrationErrorCode.Unauthorized], - } - } - - const authToken = await createIntegrationToken({ - uid: integration.user.id, - token: integration.token, - }) - if (!authToken) { - return { - errorCodes: [ImportFromIntegrationErrorCode.BadRequest], - } - } - - // create a task to import all the pages - const taskName = await enqueueImportFromIntegration( - integration.id, - integration.name, - integration.syncedAt?.getTime() || 0, - authToken, - integration.importItemState || ImportItemState.Unarchived - ) - // update task name in integration - await updateIntegration(integration.id, { taskName }, uid) - - analytics.capture({ - distinctId: uid, - event: 'integration_import', - properties: { - integrationId, - }, - }) + const integration = await findIntegration({ id: integrationId }, uid) + if (!integration) { return { - success: true, + errorCodes: [ImportFromIntegrationErrorCode.Unauthorized], } - } catch (error) { - log.error(error) + } + const authToken = await createIntegrationToken({ + uid: integration.user.id, + token: integration.token, + }) + if (!authToken) { return { errorCodes: [ImportFromIntegrationErrorCode.BadRequest], } } + + // create a task to import all the pages + const taskName = await enqueueImportFromIntegration( + integration.id, + integration.name, + integration.syncedAt?.getTime() || 0, + authToken, + integration.importItemState || ImportItemState.Unarchived + ) + // update task name in integration + await updateIntegration(integration.id, { taskName }, uid) + + analytics.capture({ + distinctId: uid, + event: 'integration_import', + properties: { + integrationId, + }, + }) + + return { + success: true, + } }) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 6c20fb51f..1effa7a57 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3009,6 +3009,20 @@ const schema = gql` name: String! } + union IntegrationResult = IntegrationSuccess | IntegrationError + + type IntegrationSuccess { + integration: Integration! + } + + type IntegrationError { + errorCodes: [IntegrationErrorCode!]! + } + + enum IntegrationErrorCode { + NOT_FOUND + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -3191,6 +3205,7 @@ const schema = gql` sort: SortParams folder: String ): UpdatesSinceResult! + integration(id: ID!): IntegrationResult! integrations: IntegrationsResult! recentSearches: RecentSearchesResult! rules(enabled: Boolean): RulesResult! diff --git a/packages/api/test/resolvers/integrations.test.ts b/packages/api/test/resolvers/integrations.test.ts index 538841a53..ac85cf48b 100644 --- a/packages/api/test/resolvers/integrations.test.ts +++ b/packages/api/test/resolvers/integrations.test.ts @@ -403,4 +403,55 @@ describe('Integrations resolvers', () => { }) }) }) + + describe('integration API', () => { + const query = ` + query Integration ($id: ID!) { + integration(id: $id) { + ... on IntegrationSuccess { + integration { + id + type + enabled + } + } + ... on IntegrationError { + errorCodes + } + } + } + ` + + let existingIntegration: Integration + + before(async () => { + existingIntegration = await saveIntegration( + { + user: { id: loginUser.id }, + name: 'READWISE', + token: 'fakeToken', + }, + loginUser.id + ) + }) + + after(async () => { + await deleteIntegrations(loginUser.id, [existingIntegration.id]) + }) + + it('returns the integration', async () => { + const res = await graphqlRequest(query, authToken, { + id: existingIntegration.id, + }) + expect(res.body.data.integration.integration.id).to.equal( + existingIntegration.id + ) + expect(res.body.data.integration.integration.type).to.equal( + existingIntegration.type + ) + expect(res.body.data.integration.integration.enabled).to.equal( + existingIntegration.enabled + ) + }) + }) })