@ -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>;
|
||||
|
||||
@ -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!]!
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'),
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
49
packages/api/src/routers/task_router.ts
Normal file
49
packages/api/src/routers/task_router.ts
Normal 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
|
||||
}
|
||||
@ -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!
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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>[]
|
||||
|
||||
@ -603,7 +603,7 @@ export const enqueueImportFromIntegration = async (
|
||||
return createdTasks[0].name
|
||||
}
|
||||
|
||||
export const enqueueExportAllItems = async (
|
||||
export const enqueueExportToIntegration = async (
|
||||
integrationId: string,
|
||||
userId: string
|
||||
) => {
|
||||
|
||||
@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user