add integration api
This commit is contained in:
@ -1274,6 +1274,22 @@ export type Integration = {
|
||||
updatedAt?: Maybe<Scalars['Date']>;
|
||||
};
|
||||
|
||||
export type IntegrationError = {
|
||||
__typename?: 'IntegrationError';
|
||||
errorCodes: Array<IntegrationErrorCode>;
|
||||
};
|
||||
|
||||
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<Scalars['String']>;
|
||||
integration: IntegrationResult;
|
||||
integrations: IntegrationsResult;
|
||||
labels: LabelsResult;
|
||||
me?: Maybe<User>;
|
||||
@ -2143,6 +2160,11 @@ export type QueryGetDiscoverFeedArticlesArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryIntegrationArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryRulesArgs = {
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
@ -4063,6 +4085,10 @@ export type ResolversTypes = {
|
||||
ImportItemState: ImportItemState;
|
||||
Int: ResolverTypeWrapper<Scalars['Int']>;
|
||||
Integration: ResolverTypeWrapper<Integration>;
|
||||
IntegrationError: ResolverTypeWrapper<IntegrationError>;
|
||||
IntegrationErrorCode: IntegrationErrorCode;
|
||||
IntegrationResult: ResolversTypes['IntegrationError'] | ResolversTypes['IntegrationSuccess'];
|
||||
IntegrationSuccess: ResolverTypeWrapper<IntegrationSuccess>;
|
||||
IntegrationType: IntegrationType;
|
||||
IntegrationsError: ResolverTypeWrapper<IntegrationsError>;
|
||||
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<ContextType = ResolverContext, ParentType exten
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type IntegrationErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['IntegrationError'] = ResolversParentTypes['IntegrationError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['IntegrationErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type IntegrationResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['IntegrationResult'] = ResolversParentTypes['IntegrationResult']> = {
|
||||
__resolveType: TypeResolveFn<'IntegrationError' | 'IntegrationSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type IntegrationSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['IntegrationSuccess'] = ResolversParentTypes['IntegrationSuccess']> = {
|
||||
integration?: Resolver<ResolversTypes['Integration'], ParentType, ContextType>;
|
||||
__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>;
|
||||
@ -6132,6 +6175,7 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
|
||||
getUserPersonalization?: Resolver<ResolversTypes['GetUserPersonalizationResult'], ParentType, ContextType>;
|
||||
groups?: Resolver<ResolversTypes['GroupsResult'], ParentType, ContextType>;
|
||||
hello?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
integration?: Resolver<ResolversTypes['IntegrationResult'], ParentType, ContextType, RequireFields<QueryIntegrationArgs, 'id'>>;
|
||||
integrations?: Resolver<ResolversTypes['IntegrationsResult'], ParentType, ContextType>;
|
||||
labels?: Resolver<ResolversTypes['LabelsResult'], ParentType, ContextType>;
|
||||
me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
|
||||
@ -7277,6 +7321,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
||||
ImportFromIntegrationResult?: ImportFromIntegrationResultResolvers<ContextType>;
|
||||
ImportFromIntegrationSuccess?: ImportFromIntegrationSuccessResolvers<ContextType>;
|
||||
Integration?: IntegrationResolvers<ContextType>;
|
||||
IntegrationError?: IntegrationErrorResolvers<ContextType>;
|
||||
IntegrationResult?: IntegrationResultResolvers<ContextType>;
|
||||
IntegrationSuccess?: IntegrationSuccessResolvers<ContextType>;
|
||||
IntegrationsError?: IntegrationsErrorResolvers<ContextType>;
|
||||
IntegrationsResult?: IntegrationsResultResolvers<ContextType>;
|
||||
IntegrationsSuccess?: IntegrationsSuccessResolvers<ContextType>;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
}
|
||||
|
||||
@ -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<Integration> = {
|
||||
...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<Integration> = {
|
||||
...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,
|
||||
}
|
||||
})
|
||||
|
||||
@ -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!
|
||||
|
||||
@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user