Merge pull request #3698 from omnivore-app/fix/notion

Fix: notion
This commit is contained in:
Hongbo Wu
2024-03-21 14:08:16 +08:00
committed by GitHub
17 changed files with 1712 additions and 258 deletions

View File

@ -892,6 +892,23 @@ export type EmptyTrashSuccess = {
success?: Maybe<Scalars['Boolean']>;
};
export type ExportToIntegrationError = {
__typename?: 'ExportToIntegrationError';
errorCodes: Array<ExportToIntegrationErrorCode>;
};
export enum ExportToIntegrationErrorCode {
FailedToCreateTask = 'FAILED_TO_CREATE_TASK',
Unauthorized = 'UNAUTHORIZED'
}
export type ExportToIntegrationResult = ExportToIntegrationError | ExportToIntegrationSuccess;
export type ExportToIntegrationSuccess = {
__typename?: 'ExportToIntegrationSuccess';
task: Task;
};
export type Feature = {
__typename?: 'Feature';
createdAt: Scalars['Date'];
@ -1274,6 +1291,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'
@ -1566,6 +1599,7 @@ export type Mutation = {
deleteWebhook: DeleteWebhookResult;
editDiscoverFeed: EditDiscoverFeedResult;
emptyTrash: EmptyTrashResult;
exportToIntegration: ExportToIntegrationResult;
fetchContent: FetchContentResult;
generateApiKey: GenerateApiKeyResult;
googleLogin: LoginResult;
@ -1721,6 +1755,11 @@ export type MutationEditDiscoverFeedArgs = {
};
export type MutationExportToIntegrationArgs = {
integrationId: Scalars['ID'];
};
export type MutationFetchContentArgs = {
id: Scalars['ID'];
};
@ -2096,6 +2135,7 @@ export type Query = {
getUserPersonalization: GetUserPersonalizationResult;
groups: GroupsResult;
hello?: Maybe<Scalars['String']>;
integration: IntegrationResult;
integrations: IntegrationsResult;
labels: LabelsResult;
me?: Maybe<User>;
@ -2143,6 +2183,11 @@ export type QueryGetDiscoverFeedArticlesArgs = {
};
export type QueryIntegrationArgs = {
name: Scalars['String'];
};
export type QueryRulesArgs = {
enabled?: InputMaybe<Scalars['Boolean']>;
};
@ -3162,6 +3207,26 @@ export type SyncUpdatedItemEdge = {
updateReason: UpdateReason;
};
export type Task = {
__typename?: 'Task';
cancellable?: Maybe<Scalars['Boolean']>;
createdAt: Scalars['Date'];
failedReason?: Maybe<Scalars['String']>;
id: Scalars['ID'];
name: Scalars['String'];
progress?: Maybe<Scalars['Float']>;
runningTime?: Maybe<Scalars['Int']>;
state: TaskState;
};
export enum TaskState {
Cancelled = 'CANCELLED',
Failed = 'FAILED',
Pending = 'PENDING',
Running = 'RUNNING',
Succeeded = 'SUCCEEDED'
}
export type TypeaheadSearchError = {
__typename?: 'TypeaheadSearchError';
errorCodes: Array<TypeaheadSearchErrorCode>;
@ -3993,6 +4058,10 @@ export type ResolversTypes = {
EmptyTrashErrorCode: EmptyTrashErrorCode;
EmptyTrashResult: ResolversTypes['EmptyTrashError'] | ResolversTypes['EmptyTrashSuccess'];
EmptyTrashSuccess: ResolverTypeWrapper<EmptyTrashSuccess>;
ExportToIntegrationError: ResolverTypeWrapper<ExportToIntegrationError>;
ExportToIntegrationErrorCode: ExportToIntegrationErrorCode;
ExportToIntegrationResult: ResolversTypes['ExportToIntegrationError'] | ResolversTypes['ExportToIntegrationSuccess'];
ExportToIntegrationSuccess: ResolverTypeWrapper<ExportToIntegrationSuccess>;
Feature: ResolverTypeWrapper<Feature>;
Feed: ResolverTypeWrapper<Feed>;
FeedArticle: ResolverTypeWrapper<FeedArticle>;
@ -4064,6 +4133,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;
@ -4298,6 +4371,8 @@ export type ResolversTypes = {
SubscriptionsResult: ResolversTypes['SubscriptionsError'] | ResolversTypes['SubscriptionsSuccess'];
SubscriptionsSuccess: ResolverTypeWrapper<SubscriptionsSuccess>;
SyncUpdatedItemEdge: ResolverTypeWrapper<SyncUpdatedItemEdge>;
Task: ResolverTypeWrapper<Task>;
TaskState: TaskState;
TypeaheadSearchError: ResolverTypeWrapper<TypeaheadSearchError>;
TypeaheadSearchErrorCode: TypeaheadSearchErrorCode;
TypeaheadSearchItem: ResolverTypeWrapper<TypeaheadSearchItem>;
@ -4539,6 +4614,9 @@ export type ResolversParentTypes = {
EmptyTrashError: EmptyTrashError;
EmptyTrashResult: ResolversParentTypes['EmptyTrashError'] | ResolversParentTypes['EmptyTrashSuccess'];
EmptyTrashSuccess: EmptyTrashSuccess;
ExportToIntegrationError: ExportToIntegrationError;
ExportToIntegrationResult: ResolversParentTypes['ExportToIntegrationError'] | ResolversParentTypes['ExportToIntegrationSuccess'];
ExportToIntegrationSuccess: ExportToIntegrationSuccess;
Feature: Feature;
Feed: Feed;
FeedArticle: FeedArticle;
@ -4595,6 +4673,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;
@ -4776,6 +4857,7 @@ export type ResolversParentTypes = {
SubscriptionsResult: ResolversParentTypes['SubscriptionsError'] | ResolversParentTypes['SubscriptionsSuccess'];
SubscriptionsSuccess: SubscriptionsSuccess;
SyncUpdatedItemEdge: SyncUpdatedItemEdge;
Task: Task;
TypeaheadSearchError: TypeaheadSearchError;
TypeaheadSearchItem: TypeaheadSearchItem;
TypeaheadSearchResult: ResolversParentTypes['TypeaheadSearchError'] | ResolversParentTypes['TypeaheadSearchSuccess'];
@ -5474,6 +5556,20 @@ export type EmptyTrashSuccessResolvers<ContextType = ResolverContext, ParentType
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ExportToIntegrationErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ExportToIntegrationError'] = ResolversParentTypes['ExportToIntegrationError']> = {
errorCodes?: Resolver<Array<ResolversTypes['ExportToIntegrationErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ExportToIntegrationResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ExportToIntegrationResult'] = ResolversParentTypes['ExportToIntegrationResult']> = {
__resolveType: TypeResolveFn<'ExportToIntegrationError' | 'ExportToIntegrationSuccess', ParentType, ContextType>;
};
export type ExportToIntegrationSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ExportToIntegrationSuccess'] = ResolversParentTypes['ExportToIntegrationSuccess']> = {
task?: Resolver<ResolversTypes['Task'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type FeatureResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Feature'] = ResolversParentTypes['Feature']> = {
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
expiresAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
@ -5778,6 +5874,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>;
@ -5995,6 +6105,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
deleteWebhook?: Resolver<ResolversTypes['DeleteWebhookResult'], ParentType, ContextType, RequireFields<MutationDeleteWebhookArgs, 'id'>>;
editDiscoverFeed?: Resolver<ResolversTypes['EditDiscoverFeedResult'], ParentType, ContextType, RequireFields<MutationEditDiscoverFeedArgs, 'input'>>;
emptyTrash?: Resolver<ResolversTypes['EmptyTrashResult'], ParentType, ContextType>;
exportToIntegration?: Resolver<ResolversTypes['ExportToIntegrationResult'], ParentType, ContextType, RequireFields<MutationExportToIntegrationArgs, 'integrationId'>>;
fetchContent?: Resolver<ResolversTypes['FetchContentResult'], ParentType, ContextType, RequireFields<MutationFetchContentArgs, 'id'>>;
generateApiKey?: Resolver<ResolversTypes['GenerateApiKeyResult'], ParentType, ContextType, RequireFields<MutationGenerateApiKeyArgs, 'input'>>;
googleLogin?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationGoogleLoginArgs, 'input'>>;
@ -6133,6 +6244,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, 'name'>>;
integrations?: Resolver<ResolversTypes['IntegrationsResult'], ParentType, ContextType>;
labels?: Resolver<ResolversTypes['LabelsResult'], ParentType, ContextType>;
me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
@ -6746,6 +6858,18 @@ export type SyncUpdatedItemEdgeResolvers<ContextType = ResolverContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type TaskResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Task'] = ResolversParentTypes['Task']> = {
cancellable?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
failedReason?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
progress?: Resolver<Maybe<ResolversTypes['Float']>, ParentType, ContextType>;
runningTime?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
state?: Resolver<ResolversTypes['TaskState'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type TypeaheadSearchErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['TypeaheadSearchError'] = ResolversParentTypes['TypeaheadSearchError']> = {
errorCodes?: Resolver<Array<ResolversTypes['TypeaheadSearchErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -7230,6 +7354,9 @@ export type Resolvers<ContextType = ResolverContext> = {
EmptyTrashError?: EmptyTrashErrorResolvers<ContextType>;
EmptyTrashResult?: EmptyTrashResultResolvers<ContextType>;
EmptyTrashSuccess?: EmptyTrashSuccessResolvers<ContextType>;
ExportToIntegrationError?: ExportToIntegrationErrorResolvers<ContextType>;
ExportToIntegrationResult?: ExportToIntegrationResultResolvers<ContextType>;
ExportToIntegrationSuccess?: ExportToIntegrationSuccessResolvers<ContextType>;
Feature?: FeatureResolvers<ContextType>;
Feed?: FeedResolvers<ContextType>;
FeedArticle?: FeedArticleResolvers<ContextType>;
@ -7279,6 +7406,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>;
@ -7428,6 +7558,7 @@ export type Resolvers<ContextType = ResolverContext> = {
SubscriptionsResult?: SubscriptionsResultResolvers<ContextType>;
SubscriptionsSuccess?: SubscriptionsSuccessResolvers<ContextType>;
SyncUpdatedItemEdge?: SyncUpdatedItemEdgeResolvers<ContextType>;
Task?: TaskResolvers<ContextType>;
TypeaheadSearchError?: TypeaheadSearchErrorResolvers<ContextType>;
TypeaheadSearchItem?: TypeaheadSearchItemResolvers<ContextType>;
TypeaheadSearchResult?: TypeaheadSearchResultResolvers<ContextType>;

View File

@ -794,6 +794,21 @@ type EmptyTrashSuccess {
success: Boolean
}
type ExportToIntegrationError {
errorCodes: [ExportToIntegrationErrorCode!]!
}
enum ExportToIntegrationErrorCode {
FAILED_TO_CREATE_TASK
UNAUTHORIZED
}
union ExportToIntegrationResult = ExportToIntegrationError | ExportToIntegrationSuccess
type ExportToIntegrationSuccess {
task: Task!
}
type Feature {
createdAt: Date!
expiresAt: Date
@ -1140,6 +1155,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
@ -1408,6 +1437,7 @@ type Mutation {
deleteWebhook(id: ID!): DeleteWebhookResult!
editDiscoverFeed(input: EditDiscoverFeedInput!): EditDiscoverFeedResult!
emptyTrash: EmptyTrashResult!
exportToIntegration(integrationId: ID!): ExportToIntegrationResult!
fetchContent(id: ID!): FetchContentResult!
generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -1591,6 +1621,7 @@ type Query {
getUserPersonalization: GetUserPersonalizationResult!
groups: GroupsResult!
hello: String
integration(name: String!): IntegrationResult!
integrations: IntegrationsResult!
labels: LabelsResult!
me: User
@ -2503,6 +2534,25 @@ type SyncUpdatedItemEdge {
updateReason: UpdateReason!
}
type Task {
cancellable: Boolean
createdAt: Date!
failedReason: String
id: ID!
name: String!
progress: Float
runningTime: Int
state: TaskState!
}
enum TaskState {
CANCELLED
FAILED
PENDING
RUNNING
SUCCEEDED
}
type TypeaheadSearchError {
errorCodes: [TypeaheadSearchErrorCode!]!
}

View File

@ -1,5 +1,5 @@
import { IntegrationType } from '../../entity/integration'
import { findIntegration } from '../../services/integrations'
import { findIntegration, updateIntegration } from '../../services/integrations'
import { findRecentLibraryItems } from '../../services/library_item'
import { findActiveUser } from '../../services/user'
import { enqueueExportItem } from '../../utils/createTask'
@ -39,8 +39,8 @@ export const exportAllItems = async (jobData: ExportAllItemsJobData) => {
return
}
const maxItems = 1000
const limit = 100
const maxItems = 100
const limit = 10
let offset = 0
// get max 1000 most recent items from the database
while (offset < maxItems) {
@ -72,4 +72,12 @@ export const exportAllItems = async (jobData: ExportAllItemsJobData) => {
integrationId,
})
}
logger.info('exported all items', {
userId,
integrationId,
})
// clear task name in integration
await updateIntegration(integration.id, { taskName: null }, userId)
}

View File

@ -5,6 +5,7 @@
import {
ConnectionOptions,
Job,
JobState,
JobType,
Queue,
QueueEvents,
@ -13,6 +14,7 @@ import {
import express, { Express } from 'express'
import { appDataSource } from './data_source'
import { env } from './env'
import { TaskState } from './generated/graphql'
import { aiSummarize, AI_SUMMARIZE_JOB_NAME } from './jobs/ai-summarize'
import { bulkAction, BULK_ACTION_JOB_NAME } from './jobs/bulk_action'
import { callWebhook, CALL_WEBHOOK_JOB_NAME } from './jobs/call_webhook'
@ -25,6 +27,12 @@ import {
exportItem,
EXPORT_ITEM_JOB_NAME,
} from './jobs/integration/export_item'
import {
processYouTubeTranscript,
processYouTubeVideo,
PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME,
PROCESS_YOUTUBE_VIDEO_JOB_NAME,
} from './jobs/process-youtube-video'
import { refreshAllFeeds } from './jobs/rss/refreshAllFeeds'
import { refreshFeed } from './jobs/rss/refreshFeed'
import { savePageJob } from './jobs/save_page'
@ -44,12 +52,6 @@ import { redisDataSource } from './redis_data_source'
import { CACHED_READING_POSITION_PREFIX } from './services/cached_reading_position'
import { getJobPriority } from './utils/createTask'
import { logger } from './utils/logger'
import {
PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME,
PROCESS_YOUTUBE_VIDEO_JOB_NAME,
processYouTubeTranscript,
processYouTubeVideo,
} from './jobs/process-youtube-video'
export const QUEUE_NAME = 'omnivore-backend-queue'
export const JOB_VERSION = 'v001'
@ -82,6 +84,33 @@ export const getBackendQueue = async (): Promise<Queue | undefined> => {
return backendQueue
}
export const getJob = async (jobId: string) => {
const queue = await getBackendQueue()
if (!queue) {
return
}
return queue.getJob(jobId)
}
export const jobStateToTaskState = (
jobState: JobState | 'unknown'
): TaskState => {
switch (jobState) {
case 'completed':
return TaskState.Succeeded
case 'failed':
return TaskState.Failed
case 'active':
return TaskState.Running
case 'delayed':
return TaskState.Pending
case 'waiting':
return TaskState.Pending
default:
return TaskState.Pending
}
}
export const createWorker = (connection: ConnectionOptions) =>
new Worker(
QUEUE_NAME,

View File

@ -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'
@ -39,6 +41,15 @@ import {
generateUploadFilePathName,
} from '../utils/uploads'
import { emptyTrashResolver, fetchContentResolver } from './article'
import {
addDiscoverFeedResolver,
deleteDiscoverArticleResolver,
deleteDiscoverFeedsResolver,
editDiscoverFeedsResolver,
getDiscoverFeedArticlesResolver,
getDiscoverFeedsResolver,
saveDiscoverArticleResolver,
} from './discover_feeds'
import { optInFeatureResolver } from './features'
import { uploadImportFileResolver } from './importers/uploadImportFileResolver'
import {
@ -63,6 +74,7 @@ import {
deleteRuleResolver,
deleteWebhookResolver,
deviceTokensResolver,
exportToIntegrationResolver,
feedsResolver,
filtersResolver,
generateApiKeyResolver,
@ -79,6 +91,7 @@ import {
googleSignupResolver,
groupsResolver,
importFromIntegrationResolver,
integrationResolver,
integrationsResolver,
joinGroupResolver,
labelsResolver,
@ -141,17 +154,6 @@ import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails'
import { recentSearchesResolver } from './recent_searches'
import { WithDataSourcesContext } from './types'
import { updateEmailResolver } from './user'
import {
addDiscoverFeedResolver,
getDiscoverFeedsResolver,
getDiscoverFeedArticlesResolver,
saveDiscoverArticleResolver,
deleteDiscoverArticleResolver,
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 = {
@ -313,6 +315,7 @@ export const functionResolvers = {
editDiscoverFeed: editDiscoverFeedsResolver,
emptyTrash: emptyTrashResolver,
fetchContent: fetchContentResolver,
exportToIntegration: exportToIntegrationResolver,
},
Query: {
me: getMeUserResolver,
@ -348,6 +351,7 @@ export const functionResolvers = {
recentEmails: recentEmailsResolver,
feeds: feedsResolver,
scanFeeds: scanFeedsResolver,
integration: integrationResolver,
},
User: {
async intercomHash(
@ -662,4 +666,6 @@ export const functionResolvers = {
...resultResolveTypeResolver('UpdateNewsletterEmail'),
...resultResolveTypeResolver('EmptyTrash'),
...resultResolveTypeResolver('FetchContent'),
...resultResolveTypeResolver('Integration'),
...resultResolveTypeResolver('ExportToIntegration'),
}

View File

@ -9,22 +9,31 @@ import {
DeleteIntegrationError,
DeleteIntegrationErrorCode,
DeleteIntegrationSuccess,
ExportToIntegrationError,
ExportToIntegrationErrorCode,
ExportToIntegrationSuccess,
ImportFromIntegrationError,
ImportFromIntegrationErrorCode,
ImportFromIntegrationSuccess,
IntegrationError,
IntegrationErrorCode,
IntegrationsError,
IntegrationsErrorCode,
IntegrationsSuccess,
IntegrationSuccess,
MutationDeleteIntegrationArgs,
MutationExportToIntegrationArgs,
MutationImportFromIntegrationArgs,
MutationSetIntegrationArgs,
QueryIntegrationArgs,
SetIntegrationError,
SetIntegrationErrorCode,
SetIntegrationSuccess,
TaskState,
} from '../../generated/graphql'
import { createIntegrationToken } from '../../routers/auth/jwt_helpers'
import {
findIntegration,
findIntegrationByName,
findIntegrations,
getIntegrationClient,
removeIntegration,
@ -34,6 +43,7 @@ import {
import { analytics } from '../../utils/analytics'
import {
deleteTask,
enqueueExportToIntegration,
enqueueImportFromIntegration,
} from '../../utils/createTask'
import { authorized } from '../../utils/gql-utils'
@ -42,85 +52,94 @@ 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,
},
})
if (integration.name.toLowerCase() === 'readwise') {
// create a task to export all the items for readwise temporarily
await enqueueExportToIntegration(integration.id, uid)
}
return {
integration,
}
} catch (error) {
log.error(error)
analytics.capture({
distinctId: uid,
event: 'integration_set',
properties: {
id: integrationToSave.id,
env: env.server.apiEnv,
},
})
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 (_, { name }, { uid, log }) => {
const integration = await findIntegrationByName(name, uid)
if (!integration) {
log.error('integration not found', name)
return {
integrations,
errorCodes: [IntegrationErrorCode.NotFound],
}
} catch (error) {
log.error(error)
}
return {
errorCodes: [IntegrationsErrorCode.BadRequest],
}
return {
integration,
}
})
@ -131,42 +150,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 +186,93 @@ 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,
}
})
export const exportToIntegrationResolver = authorized<
ExportToIntegrationSuccess,
ExportToIntegrationError,
MutationExportToIntegrationArgs
>(async (_, { integrationId }, { uid, log }) => {
const integration = await findIntegration({ id: integrationId }, uid)
if (!integration) {
log.error('integration not found', integrationId)
return {
errorCodes: [ExportToIntegrationErrorCode.Unauthorized],
}
}
// create a job to export all the items
const job = await enqueueExportToIntegration(integration.id, uid)
if (!job || !job.id) {
log.error('failed to create task', integrationId)
return {
errorCodes: [ExportToIntegrationErrorCode.FailedToCreateTask],
}
}
// update task name in integration
await updateIntegration(integration.id, { taskName: job.id }, uid)
analytics.capture({
distinctId: uid,
event: 'integration_export',
properties: {
integrationId,
},
})
return {
task: {
id: job.id,
name: job.name,
state: TaskState.Pending,
createdAt: new Date(job.timestamp),
progress: 0,
runningTime: 0,
cancellable: true,
},
}
})

View File

@ -1,65 +0,0 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import express from 'express'
import { Integration, IntegrationType } from '../../entity/integration'
import { readPushSubscription } from '../../pubsub'
import { getRepository } from '../../repository'
import { enqueueExportAllItems } from '../../utils/createTask'
import { logger } from '../../utils/logger'
import { createIntegrationToken } from '../auth/jwt_helpers'
export function integrationsServiceRouter() {
const router = express.Router()
router.post('/export', async (req, res) => {
logger.info('start to sync with integration')
try {
const { message: msgStr, expired } = readPushSubscription(req)
if (!msgStr) {
return res.status(200).send('Bad Request')
}
if (expired) {
logger.info('discarding expired message')
return res.status(200).send('Expired')
}
// find all active integrations
const integrations = await getRepository(Integration).find({
where: {
enabled: true,
type: IntegrationType.Export,
},
relations: ['user'],
})
// create a task to sync with each integration
await Promise.all(
integrations.map(async (integration) => {
const authToken = await createIntegrationToken({
uid: integration.user.id,
token: integration.token,
})
if (!authToken) {
logger.error('failed to create auth token', {
integrationId: integration.id,
})
return
}
return enqueueExportAllItems(integration.id, integration.user.id)
})
)
} catch (err) {
logger.error('sync with integrations failed', err)
return res.status(500).send(err)
}
res.status(200).send('OK')
})
return router
}

View File

@ -0,0 +1,49 @@
import cors from 'cors'
import express from 'express'
import { Task, TaskState } from '../generated/graphql'
import { getJob, jobStateToTaskState } from '../queue-processor'
import { getClaimsByToken, getTokenByRequest } from '../utils/auth'
import { corsConfig } from '../utils/corsConfig'
import { logger } from '../utils/logger'
export function taskRouter() {
const router = express.Router()
router.get('/:id', cors<express.Request>(corsConfig), async (req, res) => {
const token = getTokenByRequest(req)
const claims = await getClaimsByToken(token)
if (!claims) {
return res.status(401).send('UNAUTHORIZED')
}
try {
const job = await getJob(req.params.id)
if (!job || !job.id) {
res.status(404).send('Not Found')
return
}
const jobState = await job.getState()
const state = jobStateToTaskState(jobState)
const finishedAt = job.finishedOn ? job.finishedOn : Date.now()
const runningTime = job.processedOn ? finishedAt - job.processedOn : 0
const result: Task = {
id: job.id,
state,
createdAt: new Date(job.timestamp),
name: job.name,
runningTime,
progress: job.progress as number,
failedReason: state === TaskState.Failed ? job.failedReason : undefined,
}
res.send(result)
} catch (e) {
logger.error('failed to get task', e)
res.status(500)
}
})
return router
}

View File

@ -3010,6 +3010,56 @@ const schema = gql`
name: String!
}
union IntegrationResult = IntegrationSuccess | IntegrationError
type IntegrationSuccess {
integration: Integration!
}
type IntegrationError {
errorCodes: [IntegrationErrorCode!]!
}
enum IntegrationErrorCode {
NOT_FOUND
}
union ExportToIntegrationResult =
ExportToIntegrationSuccess
| ExportToIntegrationError
type ExportToIntegrationSuccess {
task: Task!
}
type Task {
id: ID!
name: String!
state: TaskState!
createdAt: Date!
runningTime: Int # in milliseconds
cancellable: Boolean
progress: Float
failedReason: String
}
enum TaskState {
PENDING
RUNNING
SUCCEEDED
FAILED
CANCELLED
}
type ExportToIntegrationError {
errorCodes: [ExportToIntegrationErrorCode!]!
}
enum ExportToIntegrationErrorCode {
UNAUTHORIZED
FAILED_TO_CREATE_TASK
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -3118,6 +3168,7 @@ const schema = gql`
arguments: JSON # additional arguments for the action
): BulkActionResult!
importFromIntegration(integrationId: ID!): ImportFromIntegrationResult!
exportToIntegration(integrationId: ID!): ExportToIntegrationResult!
setFavoriteArticle(id: ID!): SetFavoriteArticleResult!
updateSubscription(
input: UpdateSubscriptionInput!
@ -3192,6 +3243,7 @@ const schema = gql`
sort: SortParams
folder: String
): UpdatesSinceResult!
integration(name: String!): IntegrationResult!
integrations: IntegrationsResult!
recentSearches: RecentSearchesResult!
rules(enabled: Boolean): RulesResult!

View File

@ -18,6 +18,7 @@ import { makeApolloServer } from './apollo'
import { appDataSource } from './data_source'
import { env } from './env'
import { redisDataSource } from './redis_data_source'
import { aiSummariesRouter } from './routers/ai_summary_router'
import { articleRouter } from './routers/article_router'
import { authRouter } from './routers/auth/auth_router'
import { mobileAuthRouter } from './routers/auth/mobile/mobile_auth_router'
@ -29,7 +30,6 @@ import { contentServiceRouter } from './routers/svc/content'
import { emailsServiceRouter } from './routers/svc/emails'
import { emailAttachmentRouter } from './routers/svc/email_attachment'
import { followingServiceRouter } from './routers/svc/following'
import { integrationsServiceRouter } from './routers/svc/integrations'
import { linkServiceRouter } from './routers/svc/links'
import { newsletterServiceRouter } from './routers/svc/newsletters'
// import { remindersServiceRouter } from './routers/svc/reminders'
@ -37,6 +37,7 @@ import { rssFeedRouter } from './routers/svc/rss_feed'
import { uploadServiceRouter } from './routers/svc/upload'
import { userServiceRouter } from './routers/svc/user'
import { webhooksServiceRouter } from './routers/svc/webhooks'
import { taskRouter } from './routers/task_router'
import { textToSpeechRouter } from './routers/text_to_speech'
import { userRouter } from './routers/user_router'
import { sentryConfig } from './sentry'
@ -48,7 +49,6 @@ import {
} from './utils/auth'
import { corsConfig } from './utils/corsConfig'
import { buildLogger, buildLoggerTransport, logger } from './utils/logger'
import { aiSummariesRouter } from './routers/ai_summary_router'
const PORT = process.env.PORT || 4000
@ -126,13 +126,13 @@ export const createApp = (): {
app.use('/api/text-to-speech', textToSpeechRouter())
app.use('/api/notification', notificationRouter())
app.use('/api/integration', integrationRouter())
app.use('/api/tasks', taskRouter())
app.use('/svc/pubsub/content', contentServiceRouter())
app.use('/svc/pubsub/links', linkServiceRouter())
app.use('/svc/pubsub/newsletters', newsletterServiceRouter())
app.use('/svc/pubsub/emails', emailsServiceRouter())
app.use('/svc/pubsub/upload', uploadServiceRouter())
app.use('/svc/pubsub/webhooks', webhooksServiceRouter())
app.use('/svc/pubsub/integrations', integrationsServiceRouter())
app.use('/svc/pubsub/rss-feed', rssFeedRouter())
app.use('/svc/pubsub/user', userServiceRouter())
// app.use('/svc/reminders', remindersServiceRouter())

View File

@ -60,6 +60,22 @@ export const findIntegration = async (
)
}
export const findIntegrationByName = async (name: string, userId: string) => {
return authTrx(
async (t) =>
t
.getRepository(Integration)
.createQueryBuilder()
.where({
user: { id: userId },
})
.andWhere('LOWER(name) = LOWER(:name)', { name }) // case insensitive
.getOne(),
undefined,
userId
)
}
export const findIntegrations = async (
userId: string,
where?: FindOptionsWhere<Integration> | FindOptionsWhere<Integration>[]

View File

@ -603,7 +603,7 @@ export const enqueueImportFromIntegration = async (
return createdTasks[0].name
}
export const enqueueExportAllItems = async (
export const enqueueExportToIntegration = async (
integrationId: string,
userId: string
) => {

View File

@ -403,4 +403,55 @@ describe('Integrations resolvers', () => {
})
})
})
describe('integration API', () => {
const query = `
query Integration ($name: String!) {
integration(name: $name) {
... 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, {
name: existingIntegration.name,
})
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
)
})
})
})

View File

@ -0,0 +1,53 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
export enum TaskState {
Cancelled = 'CANCELLED',
Failed = 'FAILED',
Pending = 'PENDING',
Running = 'RUNNING',
Succeeded = 'SUCCEEDED'
}
export interface Task {
id: string
state: TaskState
createdAt: Date
name: string
runningTime: number
progress: number
failedReason?: string
}
interface ExportToIntegrationDataResponseData {
exportToIntegration: {
task: Task
errorCodes?: string[]
}
}
export async function exportToIntegrationMutation(integrationId: string) {
const mutation = gql`
mutation ExportToIntegration($integrationId: ID!) {
exportToIntegration(integrationId: $integrationId) {
... on ExportToIntegrationError {
errorCodes
}
... on ExportToIntegrationSuccess {
task {
id
}
}
}
}
`
const data = await gqlFetcher(mutation, { integrationId })
const output = data as ExportToIntegrationDataResponseData
const error = output.exportToIntegration.errorCodes?.find(() => true)
if (error) {
throw error
}
return output.exportToIntegration.task
}

View File

@ -0,0 +1,67 @@
import { gql } from 'graphql-request'
import useSWR from 'swr'
import { makeGqlFetcher } from '../networkHelpers'
import { Integration } from './useGetIntegrationsQuery'
interface IntegrationQueryResponse {
isValidating: boolean
integration: Integration
revalidate: () => void
}
interface IntegrationQueryResponseData {
integration: {
integration: Integration
errorCodes?: string[]
}
}
export function useGetIntegrationQuery(name: string): IntegrationQueryResponse {
const query = gql`
query GetIntegration($name: String!) {
integration(name: $name) {
... on IntegrationSuccess {
integration {
id
name
type
token
enabled
createdAt
updatedAt
taskName
settings
}
}
... on IntegrationError {
errorCodes
}
}
}
`
const { data, mutate, isValidating } = useSWR(query, makeGqlFetcher({ name }))
if (!data) {
return {
isValidating,
integration: {} as Integration,
revalidate: () => {
mutate()
},
}
}
const result = data as IntegrationQueryResponseData
const error = result.integration.errorCodes?.find(() => true)
if (error) {
throw error
}
return {
isValidating,
integration: result.integration.integration,
revalidate: () => {
mutate()
},
}
}

View File

@ -6,21 +6,28 @@ import {
Input,
message,
Space,
Spin,
Switch,
} from 'antd'
import 'antd/dist/antd.compact.css'
import { CheckboxValueType } from 'antd/lib/checkbox/Group'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useMemo } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { HStack, VStack } from '../../../components/elements/LayoutPrimitives'
import { PageMetaData } from '../../../components/patterns/PageMetaData'
import { Beta } from '../../../components/templates/Beta'
import { Header } from '../../../components/templates/settings/SettingsTable'
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
import { deleteIntegrationMutation } from '../../../lib/networking/mutations/deleteIntegrationMutation'
import {
exportToIntegrationMutation,
Task,
TaskState,
} from '../../../lib/networking/mutations/exportToIntegrationMutation'
import { setIntegrationMutation } from '../../../lib/networking/mutations/setIntegrationMutation'
import { useGetIntegrationsQuery } from '../../../lib/networking/queries/useGetIntegrationsQuery'
import { apiFetcher } from '../../../lib/networking/networkHelpers'
import { useGetIntegrationQuery } from '../../../lib/networking/queries/useGetIntegrationQuery'
import { applyStoredTheme } from '../../../lib/themeUpdater'
import { showSuccessToast } from '../../../lib/toastHelpers'
@ -35,28 +42,22 @@ export default function Notion(): JSX.Element {
applyStoredTheme()
const router = useRouter()
const { integrations, revalidate } = useGetIntegrationsQuery()
const notion = useMemo(() => {
return integrations.find((i) => i.name == 'NOTION' && i.type == 'EXPORT')
}, [integrations])
const { integration: notion, revalidate } = useGetIntegrationQuery('notion')
const [form] = Form.useForm<FieldType>()
const [messageApi, contextHolder] = message.useMessage()
const [exporting, setExporting] = useState(!!notion.taskName)
useEffect(() => {
form.setFieldsValue({
parentPageId: notion?.settings?.parentPageId,
parentDatabaseId: notion?.settings?.parentDatabaseId,
enabled: notion?.enabled,
properties: notion?.settings?.properties,
parentPageId: notion.settings?.parentPageId,
parentDatabaseId: notion.settings?.parentDatabaseId,
enabled: notion.enabled,
properties: notion.settings?.properties,
})
}, [form, notion])
const deleteNotion = async () => {
if (!notion) {
throw new Error('Notion integration not found')
}
await deleteIntegrationMutation(notion.id)
showSuccessToast('Notion integration disconnected successfully.')
@ -65,10 +66,6 @@ export default function Notion(): JSX.Element {
}
const updateNotion = async (values: FieldType) => {
if (!notion) {
throw new Error('Notion integration not found')
}
await setIntegrationMutation({
id: notion.id,
name: notion.name,
@ -100,18 +97,54 @@ export default function Notion(): JSX.Element {
form.setFieldsValue({ properties: value.map((v) => v.toString()) })
}
const exportToNotion = useCallback(async () => {
if (exporting) {
messageApi.warning('Exporting process is already running.')
return
}
try {
const task = await exportToIntegrationMutation(notion.id)
// long polling to check the status of the task in every 10 seconds
setExporting(true)
const interval = setInterval(async () => {
const updatedTask = (await apiFetcher(`/api/tasks/${task.id}`)) as Task
if (updatedTask.state === TaskState.Succeeded) {
clearInterval(interval)
setExporting(false)
messageApi.success('Exported to Notion successfully.')
return
}
if (updatedTask.state === TaskState.Failed) {
clearInterval(interval)
setExporting(false)
messageApi.error('There was an error exporting to Notion.')
return
}
}, 10000)
messageApi.info('Exporting to Notion...')
} catch (error) {
messageApi.error('There was an error exporting to Notion.')
}
}, [exporting, messageApi, notion.id])
return (
<>
{contextHolder}
<PageMetaData title="Notion" path="/integrations/notion" />
<SettingsLayout>
<VStack
distribution="start"
alignment="start"
css={{
margin: '0 auto',
width: '80%',
height: '500px',
}}
>
<HStack
alignment="start"
distribution="start"
css={{
width: '100%',
pb: '$2',
@ -129,8 +162,8 @@ export default function Notion(): JSX.Element {
<Beta />
</HStack>
{notion && (
<div style={{ width: '100%', marginTop: '40px' }}>
<div style={{ width: '100%', marginTop: '40px' }}>
<Spin spinning={exporting} tip="Exporting" size="large">
<Form
labelCol={{ span: 6 }}
wrapperCol={{ span: 8 }}
@ -142,6 +175,7 @@ export default function Notion(): JSX.Element {
<Form.Item<FieldType>
label="Notion Page Id"
name="parentPageId"
help="The id of the Notion page where the items will be exported to. You can find it in the URL of the page."
rules={[
{
required: true,
@ -164,6 +198,7 @@ export default function Notion(): JSX.Element {
label="Automatic Sync"
name="enabled"
valuePropName="checked"
help="Once connected all new items will be exported to Notion"
>
<Switch />
</Form.Item>
@ -188,8 +223,16 @@ export default function Notion(): JSX.Element {
</Space>
</Form.Item>
</Form>
</div>
)}
<Button
type="primary"
onClick={exportToNotion}
disabled={exporting}
>
{exporting ? 'Exporting' : 'Export last 100 items'}
</Button>
</Spin>
</div>
</VStack>
</SettingsLayout>
</>

938
yarn.lock

File diff suppressed because it is too large Load Diff