mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #4031 from omnivore-app/feature/highlights-api
feat: highlights api
This commit is contained in:
commit
8f5bbdaeb0
27 changed files with 758 additions and 613 deletions
|
|
@ -33,7 +33,10 @@ import ScalarResolvers from './scalars'
|
|||
import typeDefs from './schema'
|
||||
import { batchGetHighlightsFromLibraryItemIds } from './services/highlights'
|
||||
import { batchGetPublicItems } from './services/home'
|
||||
import { batchGetLabelsFromLibraryItemIds } from './services/labels'
|
||||
import {
|
||||
batchGetLabelsFromHighlightIds,
|
||||
batchGetLabelsFromLibraryItemIds,
|
||||
} from './services/labels'
|
||||
import { batchGetLibraryItems } from './services/library_item'
|
||||
import { batchGetRecommendationsFromLibraryItemIds } from './services/recommendation'
|
||||
import {
|
||||
|
|
@ -42,6 +45,7 @@ import {
|
|||
} from './services/service_usage'
|
||||
import { batchGetSubscriptionsByNames } from './services/subscriptions'
|
||||
import { batchGetUploadFilesByIds } from './services/upload_file'
|
||||
import { findUsersByIds } from './services/user'
|
||||
import { tracer } from './tracing'
|
||||
import { getClaimsByToken, setAuthInCookie } from './utils/auth'
|
||||
import { SetClaimsRole } from './utils/dictionary'
|
||||
|
|
@ -124,6 +128,10 @@ const contextFunc: ContextFunction<ExpressContext, ResolverContext> = async ({
|
|||
|
||||
return batchGetSubscriptionsByNames(claims.uid, names as string[])
|
||||
}),
|
||||
users: new DataLoader(async (ids: readonly string[]) =>
|
||||
findUsersByIds(ids as string[])
|
||||
),
|
||||
highlightLabels: new DataLoader(batchGetLabelsFromHighlightIds),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ export class Highlight {
|
|||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User
|
||||
|
||||
@Column('uuid')
|
||||
userId!: string
|
||||
|
||||
@ManyToOne(() => LibraryItem, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'library_item_id' })
|
||||
libraryItem!: LibraryItem
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ export class LibraryItem {
|
|||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User
|
||||
|
||||
@Column('uuid')
|
||||
userId!: string
|
||||
|
||||
@Column('enum', {
|
||||
enum: LibraryItemState,
|
||||
default: LibraryItemState.Succeeded,
|
||||
|
|
|
|||
|
|
@ -1261,6 +1261,7 @@ export type Highlight = {
|
|||
html?: Maybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
labels?: Maybe<Array<Label>>;
|
||||
libraryItem: Article;
|
||||
patch?: Maybe<Scalars['String']>;
|
||||
prefix?: Maybe<Scalars['String']>;
|
||||
quote?: Maybe<Scalars['String']>;
|
||||
|
|
@ -1275,6 +1276,12 @@ export type Highlight = {
|
|||
user: User;
|
||||
};
|
||||
|
||||
export type HighlightEdge = {
|
||||
__typename?: 'HighlightEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: Highlight;
|
||||
};
|
||||
|
||||
export type HighlightReply = {
|
||||
__typename?: 'HighlightReply';
|
||||
createdAt: Scalars['Date'];
|
||||
|
|
@ -1296,6 +1303,23 @@ export enum HighlightType {
|
|||
Redaction = 'REDACTION'
|
||||
}
|
||||
|
||||
export type HighlightsError = {
|
||||
__typename?: 'HighlightsError';
|
||||
errorCodes: Array<HighlightsErrorCode>;
|
||||
};
|
||||
|
||||
export enum HighlightsErrorCode {
|
||||
BadRequest = 'BAD_REQUEST'
|
||||
}
|
||||
|
||||
export type HighlightsResult = HighlightsError | HighlightsSuccess;
|
||||
|
||||
export type HighlightsSuccess = {
|
||||
__typename?: 'HighlightsSuccess';
|
||||
edges: Array<HighlightEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type HomeEdge = {
|
||||
__typename?: 'HomeEdge';
|
||||
cursor: Scalars['String'];
|
||||
|
|
@ -2261,6 +2285,7 @@ export type Query = {
|
|||
groups: GroupsResult;
|
||||
hello?: Maybe<Scalars['String']>;
|
||||
hiddenHomeSection: HiddenHomeSectionResult;
|
||||
highlights: HighlightsResult;
|
||||
home: HomeResult;
|
||||
integration: IntegrationResult;
|
||||
integrations: IntegrationsResult;
|
||||
|
|
@ -2311,6 +2336,13 @@ export type QueryGetDiscoverFeedArticlesArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryHighlightsArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
query?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryHomeArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
|
|
@ -4331,9 +4363,14 @@ export type ResolversTypes = {
|
|||
HiddenHomeSectionResult: ResolversTypes['HiddenHomeSectionError'] | ResolversTypes['HiddenHomeSectionSuccess'];
|
||||
HiddenHomeSectionSuccess: ResolverTypeWrapper<HiddenHomeSectionSuccess>;
|
||||
Highlight: ResolverTypeWrapper<Highlight>;
|
||||
HighlightEdge: ResolverTypeWrapper<HighlightEdge>;
|
||||
HighlightReply: ResolverTypeWrapper<HighlightReply>;
|
||||
HighlightStats: ResolverTypeWrapper<HighlightStats>;
|
||||
HighlightType: HighlightType;
|
||||
HighlightsError: ResolverTypeWrapper<HighlightsError>;
|
||||
HighlightsErrorCode: HighlightsErrorCode;
|
||||
HighlightsResult: ResolversTypes['HighlightsError'] | ResolversTypes['HighlightsSuccess'];
|
||||
HighlightsSuccess: ResolverTypeWrapper<HighlightsSuccess>;
|
||||
HomeEdge: ResolverTypeWrapper<HomeEdge>;
|
||||
HomeError: ResolverTypeWrapper<HomeError>;
|
||||
HomeErrorCode: HomeErrorCode;
|
||||
|
|
@ -4900,8 +4937,12 @@ export type ResolversParentTypes = {
|
|||
HiddenHomeSectionResult: ResolversParentTypes['HiddenHomeSectionError'] | ResolversParentTypes['HiddenHomeSectionSuccess'];
|
||||
HiddenHomeSectionSuccess: HiddenHomeSectionSuccess;
|
||||
Highlight: Highlight;
|
||||
HighlightEdge: HighlightEdge;
|
||||
HighlightReply: HighlightReply;
|
||||
HighlightStats: HighlightStats;
|
||||
HighlightsError: HighlightsError;
|
||||
HighlightsResult: ResolversParentTypes['HighlightsError'] | ResolversParentTypes['HighlightsSuccess'];
|
||||
HighlightsSuccess: HighlightsSuccess;
|
||||
HomeEdge: HomeEdge;
|
||||
HomeError: HomeError;
|
||||
HomeItem: HomeItem;
|
||||
|
|
@ -6088,6 +6129,7 @@ export type HighlightResolvers<ContextType = ResolverContext, ParentType extends
|
|||
html?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
labels?: Resolver<Maybe<Array<ResolversTypes['Label']>>, ParentType, ContextType>;
|
||||
libraryItem?: Resolver<ResolversTypes['Article'], ParentType, ContextType>;
|
||||
patch?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
prefix?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
quote?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
|
|
@ -6103,6 +6145,12 @@ export type HighlightResolvers<ContextType = ResolverContext, ParentType extends
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type HighlightEdgeResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HighlightEdge'] = ResolversParentTypes['HighlightEdge']> = {
|
||||
cursor?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
node?: Resolver<ResolversTypes['Highlight'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type HighlightReplyResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HighlightReply'] = ResolversParentTypes['HighlightReply']> = {
|
||||
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
highlight?: Resolver<ResolversTypes['Highlight'], ParentType, ContextType>;
|
||||
|
|
@ -6118,6 +6166,21 @@ export type HighlightStatsResolvers<ContextType = ResolverContext, ParentType ex
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type HighlightsErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HighlightsError'] = ResolversParentTypes['HighlightsError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['HighlightsErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type HighlightsResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HighlightsResult'] = ResolversParentTypes['HighlightsResult']> = {
|
||||
__resolveType: TypeResolveFn<'HighlightsError' | 'HighlightsSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type HighlightsSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HighlightsSuccess'] = ResolversParentTypes['HighlightsSuccess']> = {
|
||||
edges?: Resolver<Array<ResolversTypes['HighlightEdge']>, ParentType, ContextType>;
|
||||
pageInfo?: Resolver<ResolversTypes['PageInfo'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type HomeEdgeResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HomeEdge'] = ResolversParentTypes['HomeEdge']> = {
|
||||
cursor?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
node?: Resolver<ResolversTypes['HomeSection'], ParentType, ContextType>;
|
||||
|
|
@ -6581,6 +6644,7 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
|
|||
groups?: Resolver<ResolversTypes['GroupsResult'], ParentType, ContextType>;
|
||||
hello?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
hiddenHomeSection?: Resolver<ResolversTypes['HiddenHomeSectionResult'], ParentType, ContextType>;
|
||||
highlights?: Resolver<ResolversTypes['HighlightsResult'], ParentType, ContextType, Partial<QueryHighlightsArgs>>;
|
||||
home?: Resolver<ResolversTypes['HomeResult'], ParentType, ContextType, Partial<QueryHomeArgs>>;
|
||||
integration?: Resolver<ResolversTypes['IntegrationResult'], ParentType, ContextType, RequireFields<QueryIntegrationArgs, 'name'>>;
|
||||
integrations?: Resolver<ResolversTypes['IntegrationsResult'], ParentType, ContextType>;
|
||||
|
|
@ -7797,8 +7861,12 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
HiddenHomeSectionResult?: HiddenHomeSectionResultResolvers<ContextType>;
|
||||
HiddenHomeSectionSuccess?: HiddenHomeSectionSuccessResolvers<ContextType>;
|
||||
Highlight?: HighlightResolvers<ContextType>;
|
||||
HighlightEdge?: HighlightEdgeResolvers<ContextType>;
|
||||
HighlightReply?: HighlightReplyResolvers<ContextType>;
|
||||
HighlightStats?: HighlightStatsResolvers<ContextType>;
|
||||
HighlightsError?: HighlightsErrorResolvers<ContextType>;
|
||||
HighlightsResult?: HighlightsResultResolvers<ContextType>;
|
||||
HighlightsSuccess?: HighlightsSuccessResolvers<ContextType>;
|
||||
HomeEdge?: HomeEdgeResolvers<ContextType>;
|
||||
HomeError?: HomeErrorResolvers<ContextType>;
|
||||
HomeItem?: HomeItemResolvers<ContextType>;
|
||||
|
|
|
|||
|
|
@ -1133,6 +1133,7 @@ type Highlight {
|
|||
html: String
|
||||
id: ID!
|
||||
labels: [Label!]
|
||||
libraryItem: Article!
|
||||
patch: String
|
||||
prefix: String
|
||||
quote: String
|
||||
|
|
@ -1147,6 +1148,11 @@ type Highlight {
|
|||
user: User!
|
||||
}
|
||||
|
||||
type HighlightEdge {
|
||||
cursor: String!
|
||||
node: Highlight!
|
||||
}
|
||||
|
||||
type HighlightReply {
|
||||
createdAt: Date!
|
||||
highlight: Highlight!
|
||||
|
|
@ -1166,6 +1172,21 @@ enum HighlightType {
|
|||
REDACTION
|
||||
}
|
||||
|
||||
type HighlightsError {
|
||||
errorCodes: [HighlightsErrorCode!]!
|
||||
}
|
||||
|
||||
enum HighlightsErrorCode {
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
union HighlightsResult = HighlightsError | HighlightsSuccess
|
||||
|
||||
type HighlightsSuccess {
|
||||
edges: [HighlightEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type HomeEdge {
|
||||
cursor: String!
|
||||
node: HomeSection!
|
||||
|
|
@ -1738,6 +1759,7 @@ type Query {
|
|||
groups: GroupsResult!
|
||||
hello: String
|
||||
hiddenHomeSection: HiddenHomeSectionResult!
|
||||
highlights(after: String, first: Int, query: String): HighlightsResult!
|
||||
home(after: String, first: Int): HomeResult!
|
||||
integration(name: String!): IntegrationResult!
|
||||
integrations: IntegrationsResult!
|
||||
|
|
|
|||
|
|
@ -12,6 +12,26 @@ import { appDataSource } from '../data_source'
|
|||
import { Claims } from '../resolvers/types'
|
||||
import { SetClaimsRole } from '../utils/dictionary'
|
||||
|
||||
export enum SortOrder {
|
||||
ASCENDING = 'ASC',
|
||||
DESCENDING = 'DESC',
|
||||
}
|
||||
|
||||
export interface Sort {
|
||||
by: string
|
||||
order?: SortOrder
|
||||
nulls?: 'NULLS FIRST' | 'NULLS LAST'
|
||||
}
|
||||
|
||||
export interface Select {
|
||||
column: string
|
||||
alias?: string
|
||||
}
|
||||
|
||||
export const paramtersToObject = (parameters: ObjectLiteral[]) => {
|
||||
return parameters.reduce((a, b) => ({ ...a, ...b }), {})
|
||||
}
|
||||
|
||||
export const getColumns = <T extends ObjectLiteral>(
|
||||
repository: Repository<T>
|
||||
): (keyof T)[] => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { Readability } from '@omnivore/readability'
|
||||
import graphqlFields from 'graphql-fields'
|
||||
import { LibraryItem, LibraryItemState } from '../../entity/library_item'
|
||||
import {
|
||||
ContentReaderType,
|
||||
LibraryItem,
|
||||
LibraryItemState,
|
||||
} from '../../entity/library_item'
|
||||
import { User } from '../../entity/user'
|
||||
import { env } from '../../env'
|
||||
import {
|
||||
ArticleError,
|
||||
|
|
@ -43,6 +48,7 @@ import {
|
|||
SaveArticleReadingProgressSuccess,
|
||||
SearchError,
|
||||
SearchErrorCode,
|
||||
SearchItemEdge,
|
||||
SearchSuccess,
|
||||
SetBookmarkArticleError,
|
||||
SetBookmarkArticleErrorCode,
|
||||
|
|
@ -87,6 +93,7 @@ import {
|
|||
setFileUploadComplete,
|
||||
} from '../../services/upload_file'
|
||||
import { traceAs } from '../../tracing'
|
||||
import { Merge } from '../../util'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { isSiteBlockedForParse } from '../../utils/blocked'
|
||||
import { enqueueBulkAction } from '../../utils/createTask'
|
||||
|
|
@ -96,10 +103,7 @@ import {
|
|||
errorHandler,
|
||||
generateSlug,
|
||||
isParsingTimeout,
|
||||
libraryItemToArticle,
|
||||
libraryItemToSearchItem,
|
||||
titleForFilePath,
|
||||
userDataToUser,
|
||||
} from '../../utils/helpers'
|
||||
import {
|
||||
getDistillerResult,
|
||||
|
|
@ -126,7 +130,10 @@ const FORCE_PUPPETEER_URLS = [
|
|||
const UNPARSEABLE_CONTENT = '<p>We were unable to parse this page.</p>'
|
||||
|
||||
export const createArticleResolver = authorized<
|
||||
CreateArticleSuccess,
|
||||
Merge<
|
||||
CreateArticleSuccess,
|
||||
{ user: User; createdArticle: Partial<LibraryItem> }
|
||||
>,
|
||||
CreateArticleError,
|
||||
MutationCreateArticleArgs
|
||||
>(
|
||||
|
|
@ -160,8 +167,8 @@ export const createArticleResolver = authorized<
|
|||
},
|
||||
})
|
||||
|
||||
const userData = await userRepository.findById(uid)
|
||||
if (!userData) {
|
||||
const user = await userRepository.findById(uid)
|
||||
if (!user) {
|
||||
return errorHandler(
|
||||
{
|
||||
errorCodes: [CreateArticleErrorCode.Unauthorized],
|
||||
|
|
@ -171,7 +178,6 @@ export const createArticleResolver = authorized<
|
|||
pubsub
|
||||
)
|
||||
}
|
||||
const user = userDataToUser(userData)
|
||||
|
||||
try {
|
||||
if (isSiteBlockedForParse(url)) {
|
||||
|
|
@ -203,25 +209,22 @@ export const createArticleResolver = authorized<
|
|||
let domContent = null
|
||||
let itemType = PageType.Unknown
|
||||
|
||||
const DUMMY_RESPONSE: CreateArticleSuccess = {
|
||||
const DUMMY_RESPONSE = {
|
||||
user,
|
||||
created: false,
|
||||
createdArticle: {
|
||||
id: '',
|
||||
slug: '',
|
||||
createdAt: new Date(),
|
||||
originalHtml: domContent,
|
||||
content: '',
|
||||
originalContent: domContent,
|
||||
readableContent: '',
|
||||
description: '',
|
||||
title: '',
|
||||
pageType: itemType,
|
||||
contentReader: ContentReader.Web,
|
||||
itemType,
|
||||
contentReader: ContentReaderType.WEB,
|
||||
author: '',
|
||||
url,
|
||||
hash: '',
|
||||
isArchived: false,
|
||||
readingProgressAnchorIndex: 0,
|
||||
readingProgressPercent: 0,
|
||||
originalUrl: url,
|
||||
textContentHash: '',
|
||||
highlights: [],
|
||||
savedAt: savedAt || new Date(),
|
||||
updatedAt: new Date(),
|
||||
|
|
@ -257,7 +260,7 @@ export const createArticleResolver = authorized<
|
|||
FORCE_PUPPETEER_URLS.some((regex) => regex.test(url))
|
||||
) {
|
||||
await createPageSaveRequest({
|
||||
user: userData,
|
||||
user: user,
|
||||
url,
|
||||
state: state || undefined,
|
||||
labels: inputLabels || undefined,
|
||||
|
|
@ -282,7 +285,7 @@ export const createArticleResolver = authorized<
|
|||
// We have a URL but no document, so we try to send this to puppeteer
|
||||
// and return a dummy response.
|
||||
await createPageSaveRequest({
|
||||
user: userData,
|
||||
user,
|
||||
url,
|
||||
state: state || undefined,
|
||||
labels: inputLabels || undefined,
|
||||
|
|
@ -353,7 +356,7 @@ export const createArticleResolver = authorized<
|
|||
return {
|
||||
user,
|
||||
created: true,
|
||||
createdArticle: libraryItemToArticle(libraryItemToReturn),
|
||||
createdArticle: libraryItemToReturn,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error creating article', error)
|
||||
|
|
@ -370,7 +373,7 @@ export const createArticleResolver = authorized<
|
|||
)
|
||||
|
||||
export const getArticleResolver = authorized<
|
||||
ArticleSuccess,
|
||||
Merge<ArticleSuccess, { article: LibraryItem }>,
|
||||
ArticleError,
|
||||
QueryArticleArgs
|
||||
>(async (_obj, { slug, format }, { authTrx, uid, log }, info) => {
|
||||
|
|
@ -439,7 +442,7 @@ export const getArticleResolver = authorized<
|
|||
}
|
||||
|
||||
return {
|
||||
article: libraryItemToArticle(libraryItem),
|
||||
article: libraryItem,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
|
|
@ -447,88 +450,8 @@ export const getArticleResolver = authorized<
|
|||
}
|
||||
})
|
||||
|
||||
// type PaginatedPartialArticles = {
|
||||
// edges: { cursor: string; node: PartialArticle }[]
|
||||
// pageInfo: PageInfo
|
||||
// }
|
||||
|
||||
// export type SetShareArticleSuccessPartial = Merge<
|
||||
// SetShareArticleSuccess,
|
||||
// {
|
||||
// updatedFeedArticle?: Omit<
|
||||
// FeedArticle,
|
||||
// | 'sharedBy'
|
||||
// | 'article'
|
||||
// | 'highlightsCount'
|
||||
// | 'annotationsCount'
|
||||
// | 'reactions'
|
||||
// >
|
||||
// updatedFeedArticleId?: string
|
||||
// updatedArticle: PartialArticle
|
||||
// }
|
||||
// >
|
||||
|
||||
// export const setShareArticleResolver = authorized<
|
||||
// SetShareArticleSuccessPartial,
|
||||
// SetShareArticleError,
|
||||
// MutationSetShareArticleArgs
|
||||
// >(
|
||||
// async (
|
||||
// _,
|
||||
// { input: { articleID, share, sharedComment, sharedWithHighlights } },
|
||||
// { models, authTrx, claims: { uid }, log }
|
||||
// ) => {
|
||||
// const article = await models.article.get(articleID)
|
||||
// if (!article) {
|
||||
// return { errorCodes: [SetShareArticleErrorCode.NotFound] }
|
||||
// }
|
||||
|
||||
// const sharedAt = share ? new Date() : null
|
||||
|
||||
// log.info(`${share ? 'S' : 'Uns'}haring an article`, {
|
||||
// article: Object.assign({}, article, {
|
||||
// content: undefined,
|
||||
// originalHtml: undefined,
|
||||
// sharedAt,
|
||||
// }),
|
||||
// labels: {
|
||||
// source: 'resolver',
|
||||
// resolver: 'setShareArticleResolver',
|
||||
// articleId: article.id,
|
||||
// distinctId: uid,
|
||||
// },
|
||||
// })
|
||||
|
||||
// const result = await authTrx((tx) =>
|
||||
// models.userArticle.updateByArticleId(
|
||||
// uid,
|
||||
// articleID,
|
||||
// { sharedAt, sharedComment, sharedWithHighlights },
|
||||
// tx
|
||||
// )
|
||||
// )
|
||||
|
||||
// if (!result) {
|
||||
// return { errorCodes: [SetShareArticleErrorCode.NotFound] }
|
||||
// }
|
||||
|
||||
// // Make sure article.id instead of userArticle.id has passed. We use it for cache updates
|
||||
// const updatedArticle = {
|
||||
// ...result,
|
||||
// ...article,
|
||||
// postedByViewer: !!sharedAt,
|
||||
// }
|
||||
// const updatedFeedArticle = sharedAt ? { ...result, sharedAt } : undefined
|
||||
// return {
|
||||
// updatedFeedArticleId: result.id,
|
||||
// updatedFeedArticle,
|
||||
// updatedArticle,
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
|
||||
export const setBookmarkArticleResolver = authorized<
|
||||
SetBookmarkArticleSuccess,
|
||||
Merge<SetBookmarkArticleSuccess, { bookmarkedArticle: LibraryItem }>,
|
||||
SetBookmarkArticleError,
|
||||
MutationSetBookmarkArticleArgs
|
||||
>(async (_, { input: { articleID } }, { uid, log, pubsub }) => {
|
||||
|
|
@ -556,12 +479,12 @@ export const setBookmarkArticleResolver = authorized<
|
|||
})
|
||||
// Make sure article.id instead of userArticle.id has passed. We use it for cache updates
|
||||
return {
|
||||
bookmarkedArticle: libraryItemToArticle(deletedLibraryItem),
|
||||
bookmarkedArticle: deletedLibraryItem,
|
||||
}
|
||||
})
|
||||
|
||||
export const saveArticleReadingProgressResolver = authorized<
|
||||
SaveArticleReadingProgressSuccess,
|
||||
Merge<SaveArticleReadingProgressSuccess, { updatedArticle: LibraryItem }>,
|
||||
SaveArticleReadingProgressError,
|
||||
MutationSaveArticleReadingProgressArgs
|
||||
>(
|
||||
|
|
@ -661,13 +584,15 @@ export const saveArticleReadingProgressResolver = authorized<
|
|||
}
|
||||
|
||||
return {
|
||||
updatedArticle: libraryItemToArticle(updatedItem),
|
||||
updatedArticle: updatedItem,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type PartialLibraryItem = Merge<LibraryItem, { format?: string }>
|
||||
type PartialSearchItemEdge = Merge<SearchItemEdge, { node: PartialLibraryItem }>
|
||||
export const searchResolver = authorized<
|
||||
SearchSuccess,
|
||||
Merge<SearchSuccess, { edges: Array<PartialSearchItemEdge> }>,
|
||||
SearchError,
|
||||
QuerySearchArgs
|
||||
>(async (_obj, params, { uid }) => {
|
||||
|
|
@ -704,7 +629,10 @@ export const searchResolver = authorized<
|
|||
|
||||
return {
|
||||
edges: libraryItems.map((item) => ({
|
||||
node: libraryItemToSearchItem(item, params.format as ArticleFormat),
|
||||
node: {
|
||||
...item,
|
||||
format: params.format || undefined,
|
||||
},
|
||||
cursor: endCursor,
|
||||
})),
|
||||
pageInfo: {
|
||||
|
|
@ -738,7 +666,7 @@ export const typeaheadSearchResolver = authorized<
|
|||
})
|
||||
|
||||
export const updatesSinceResolver = authorized<
|
||||
UpdatesSinceSuccess,
|
||||
Merge<UpdatesSinceSuccess, { edges: Array<PartialSearchItemEdge> }>,
|
||||
UpdatesSinceError,
|
||||
QueryUpdatesSinceArgs
|
||||
>(async (_obj, { since, first, after, sort: sortParams, folder }, { uid }) => {
|
||||
|
|
@ -781,7 +709,7 @@ export const updatesSinceResolver = authorized<
|
|||
const edges = libraryItems.map((item) => {
|
||||
const updateReason = getUpdateReason(item, startDate)
|
||||
return {
|
||||
node: libraryItemToSearchItem(item),
|
||||
node: item,
|
||||
cursor: endCursor,
|
||||
itemID: item.id,
|
||||
updateReason,
|
||||
|
|
|
|||
|
|
@ -17,17 +17,17 @@ import {
|
|||
findLibraryItemById,
|
||||
findLibraryItemByUrl,
|
||||
} from '../../services/library_item'
|
||||
import { Merge } from '../../util'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import {
|
||||
cleanUrl,
|
||||
isParsingTimeout,
|
||||
libraryItemToArticleSavingRequest,
|
||||
} from '../../utils/helpers'
|
||||
import { cleanUrl, isParsingTimeout } from '../../utils/helpers'
|
||||
import { isErrorWithCode } from '../user'
|
||||
|
||||
export const createArticleSavingRequestResolver = authorized<
|
||||
CreateArticleSavingRequestSuccess,
|
||||
Merge<
|
||||
CreateArticleSavingRequestSuccess,
|
||||
{ articleSavingRequest: LibraryItem }
|
||||
>,
|
||||
CreateArticleSavingRequestError,
|
||||
MutationCreateArticleSavingRequestArgs
|
||||
>(async (_, { input: { url } }, { uid, pubsub, log }) => {
|
||||
|
|
@ -67,7 +67,7 @@ export const createArticleSavingRequestResolver = authorized<
|
|||
})
|
||||
|
||||
export const articleSavingRequestResolver = authorized<
|
||||
ArticleSavingRequestSuccess,
|
||||
Merge<ArticleSavingRequestSuccess, { articleSavingRequest: LibraryItem }>,
|
||||
ArticleSavingRequestError,
|
||||
QueryArticleSavingRequestArgs
|
||||
>(async (_, { id, url }, { uid, log }) => {
|
||||
|
|
@ -109,10 +109,7 @@ export const articleSavingRequestResolver = authorized<
|
|||
libraryItem.state = LibraryItemState.Succeeded
|
||||
}
|
||||
return {
|
||||
articleSavingRequest: libraryItemToArticleSavingRequest(
|
||||
user,
|
||||
libraryItem
|
||||
),
|
||||
articleSavingRequest: libraryItem,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('articleSavingRequestResolver error', error)
|
||||
|
|
|
|||
|
|
@ -5,40 +5,31 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { createHmac } from 'crypto'
|
||||
import { isError } from 'lodash'
|
||||
import { Highlight as HighlightEntity } from '../entity/highlight'
|
||||
import { Highlight } from '../entity/highlight'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
import {
|
||||
EXISTING_NEWSLETTER_FOLDER,
|
||||
NewsletterEmail,
|
||||
} from '../entity/newsletter_email'
|
||||
import { PublicItem } from '../entity/public_item'
|
||||
import { Recommendation } from '../entity/recommendation'
|
||||
import {
|
||||
DEFAULT_SUBSCRIPTION_FOLDER,
|
||||
Subscription,
|
||||
} from '../entity/subscription'
|
||||
import { User as UserEntity } from '../entity/user'
|
||||
import { env } from '../env'
|
||||
import {
|
||||
Article,
|
||||
Highlight,
|
||||
HomeItem,
|
||||
HomeItemSource,
|
||||
HomeItemSourceType,
|
||||
Label,
|
||||
PageType,
|
||||
Recommendation,
|
||||
SearchItem,
|
||||
User,
|
||||
} from '../generated/graphql'
|
||||
import { getAISummary } from '../services/ai-summaries'
|
||||
import { findUserFeatures } from '../services/features'
|
||||
import { Merge } from '../util'
|
||||
import {
|
||||
highlightDataToHighlight,
|
||||
isBase64Image,
|
||||
recommandationDataToRecommendation,
|
||||
validatedDate,
|
||||
wordsCount,
|
||||
} from '../utils/helpers'
|
||||
import { isBase64Image, validatedDate, wordsCount } from '../utils/helpers'
|
||||
import { createImageProxyUrl } from '../utils/imageproxy'
|
||||
import { contentConverter } from '../utils/parser'
|
||||
import {
|
||||
|
|
@ -49,6 +40,7 @@ import {
|
|||
ArticleFormat,
|
||||
emptyTrashResolver,
|
||||
fetchContentResolver,
|
||||
PartialLibraryItem,
|
||||
} from './article'
|
||||
import {
|
||||
addDiscoverFeedResolver,
|
||||
|
|
@ -60,6 +52,7 @@ import {
|
|||
saveDiscoverArticleResolver,
|
||||
} from './discover_feeds'
|
||||
import { optInFeatureResolver } from './features'
|
||||
import { highlightsResolver } from './highlight'
|
||||
import {
|
||||
hiddenHomeSectionResolver,
|
||||
homeResolver,
|
||||
|
|
@ -77,14 +70,12 @@ import {
|
|||
createHighlightResolver,
|
||||
createLabelResolver,
|
||||
createNewsletterEmailResolver,
|
||||
// createReminderResolver,
|
||||
deleteAccountResolver,
|
||||
deleteFilterResolver,
|
||||
deleteHighlightResolver,
|
||||
deleteIntegrationResolver,
|
||||
deleteLabelResolver,
|
||||
deleteNewsletterEmailResolver,
|
||||
// deleteReminderResolver,
|
||||
deleteRuleResolver,
|
||||
deleteWebhookResolver,
|
||||
deviceTokensResolver,
|
||||
|
|
@ -94,11 +85,7 @@ import {
|
|||
generateApiKeyResolver,
|
||||
getAllUsersResolver,
|
||||
getArticleResolver,
|
||||
// getFollowersResolver,
|
||||
// getFollowingResolver,
|
||||
getMeUserResolver,
|
||||
// getSharedArticleResolver,
|
||||
// getUserFeedArticlesResolver,
|
||||
getUserPersonalizationResolver,
|
||||
getUserResolver,
|
||||
googleLoginResolver,
|
||||
|
|
@ -118,7 +105,6 @@ import {
|
|||
newsletterEmailsResolver,
|
||||
recommendHighlightsResolver,
|
||||
recommendResolver,
|
||||
// reminderResolver,
|
||||
reportItemResolver,
|
||||
revokeApiKeyResolver,
|
||||
rulesResolver,
|
||||
|
|
@ -133,14 +119,11 @@ import {
|
|||
setBookmarkArticleResolver,
|
||||
setDeviceTokenResolver,
|
||||
setFavoriteArticleResolver,
|
||||
// setFollowResolver,
|
||||
setIntegrationResolver,
|
||||
setLabelsForHighlightResolver,
|
||||
setLabelsResolver,
|
||||
setLinkArchivedResolver,
|
||||
setRuleResolver,
|
||||
// setShareArticleResolver,
|
||||
// setShareHighlightResolver,
|
||||
setUserPersonalizationResolver,
|
||||
setWebhookResolver,
|
||||
subscribeResolver,
|
||||
|
|
@ -151,10 +134,7 @@ import {
|
|||
updateHighlightResolver,
|
||||
updateLabelResolver,
|
||||
updateNewsletterEmailResolver,
|
||||
// updateLinkShareInfoResolver,
|
||||
updatePageResolver,
|
||||
// updateReminderResolver,
|
||||
// updateSharedCommentResolver,
|
||||
updatesSinceResolver,
|
||||
updateSubscriptionResolver,
|
||||
updateUserProfileResolver,
|
||||
|
|
@ -192,7 +172,7 @@ const resultResolveTypeResolver = (
|
|||
|
||||
const readingProgressHandlers = {
|
||||
async readingProgressPercent(
|
||||
article: { id: string; readingProgressPercent?: number },
|
||||
article: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
|
|
@ -204,15 +184,15 @@ const readingProgressHandlers = {
|
|||
)
|
||||
if (readingProgress) {
|
||||
return Math.max(
|
||||
article.readingProgressPercent ?? 0,
|
||||
article.readingProgressBottomPercent ?? 0,
|
||||
readingProgress.readingProgressPercent
|
||||
)
|
||||
}
|
||||
}
|
||||
return article.readingProgressPercent
|
||||
return article.readingProgressBottomPercent
|
||||
},
|
||||
async readingProgressAnchorIndex(
|
||||
article: { id: string; readingProgressAnchorIndex?: number },
|
||||
article: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
|
|
@ -224,15 +204,15 @@ const readingProgressHandlers = {
|
|||
)
|
||||
if (readingProgress && readingProgress.readingProgressAnchorIndex) {
|
||||
return Math.max(
|
||||
article.readingProgressAnchorIndex ?? 0,
|
||||
article.readingProgressHighestReadAnchor ?? 0,
|
||||
readingProgress.readingProgressAnchorIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
return article.readingProgressAnchorIndex
|
||||
return article.readingProgressHighestReadAnchor
|
||||
},
|
||||
async readingProgressTopPercent(
|
||||
article: { id: string; readingProgressTopPercent?: number },
|
||||
article: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
|
|
@ -265,30 +245,20 @@ export const functionResolvers = {
|
|||
updateUserProfile: updateUserProfileResolver,
|
||||
createArticle: createArticleResolver,
|
||||
createHighlight: createHighlightResolver,
|
||||
// createReaction: createReactionResolver,
|
||||
// deleteReaction: deleteReactionResolver,
|
||||
mergeHighlight: mergeHighlightResolver,
|
||||
updateHighlight: updateHighlightResolver,
|
||||
deleteHighlight: deleteHighlightResolver,
|
||||
uploadFileRequest: uploadFileRequestResolver,
|
||||
// setShareArticle: setShareArticleResolver,
|
||||
// updateSharedComment: updateSharedCommentResolver,
|
||||
// setFollow: setFollowResolver,
|
||||
setBookmarkArticle: setBookmarkArticleResolver,
|
||||
setUserPersonalization: setUserPersonalizationResolver,
|
||||
createArticleSavingRequest: createArticleSavingRequestResolver,
|
||||
// setShareHighlight: setShareHighlightResolver,
|
||||
reportItem: reportItemResolver,
|
||||
// updateLinkShareInfo: updateLinkShareInfoResolver,
|
||||
setLinkArchived: setLinkArchivedResolver,
|
||||
createNewsletterEmail: createNewsletterEmailResolver,
|
||||
deleteNewsletterEmail: deleteNewsletterEmailResolver,
|
||||
saveUrl: saveUrlResolver,
|
||||
savePage: savePageResolver,
|
||||
saveFile: saveFileResolver,
|
||||
// createReminder: createReminderResolver,
|
||||
// updateReminder: updateReminderResolver,
|
||||
// deleteReminder: deleteReminderResolver,
|
||||
setDeviceToken: setDeviceTokenResolver,
|
||||
createLabel: createLabelResolver,
|
||||
updateLabel: updateLabelResolver,
|
||||
|
|
@ -346,14 +316,9 @@ export const functionResolvers = {
|
|||
users: getAllUsersResolver,
|
||||
validateUsername: validateUsernameResolver,
|
||||
article: getArticleResolver,
|
||||
// sharedArticle: getSharedArticleResolver,
|
||||
// feedArticles: getUserFeedArticlesResolver,
|
||||
// getFollowers: getFollowersResolver,
|
||||
// getFollowing: getFollowingResolver,
|
||||
getUserPersonalization: getUserPersonalizationResolver,
|
||||
articleSavingRequest: articleSavingRequestResolver,
|
||||
newsletterEmails: newsletterEmailsResolver,
|
||||
// reminder: reminderResolver,
|
||||
labels: labelsResolver,
|
||||
search: searchResolver,
|
||||
subscriptions: subscriptionsResolver,
|
||||
|
|
@ -376,13 +341,10 @@ export const functionResolvers = {
|
|||
home: homeResolver,
|
||||
subscription: subscriptionResolver,
|
||||
hiddenHomeSection: hiddenHomeSectionResolver,
|
||||
highlights: highlightsResolver,
|
||||
},
|
||||
User: {
|
||||
async intercomHash(
|
||||
user: User,
|
||||
__: Record<string, unknown>,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
async intercomHash(user: User) {
|
||||
if (env.intercom.secretKey) {
|
||||
const userIdentifier = user.id.toString()
|
||||
|
||||
|
|
@ -414,12 +376,22 @@ export const functionResolvers = {
|
|||
|
||||
return findUserFeatures(ctx.claims.uid)
|
||||
},
|
||||
picture: (user: UserEntity) => user.profile.pictureUrl,
|
||||
// not implemented yet
|
||||
friendsCount: () => 0,
|
||||
followersCount: () => 0,
|
||||
isFullUser: () => true,
|
||||
viewerIsFollowing: () => false,
|
||||
sharedArticles: () => [],
|
||||
sharedArticlesCount: () => 0,
|
||||
sharedHighlightsCount: () => 0,
|
||||
sharedNotesCount: () => 0,
|
||||
},
|
||||
Article: {
|
||||
async url(article: Article, _: unknown, ctx: WithDataSourcesContext) {
|
||||
async url(article: LibraryItem, _: unknown, ctx: WithDataSourcesContext) {
|
||||
if (
|
||||
(article.pageType == PageType.File ||
|
||||
article.pageType == PageType.Book) &&
|
||||
(article.itemType == PageType.File ||
|
||||
article.itemType == PageType.Book) &&
|
||||
ctx.claims &&
|
||||
article.uploadFileId
|
||||
) {
|
||||
|
|
@ -432,29 +404,33 @@ export const functionResolvers = {
|
|||
const filePath = generateUploadFilePathName(upload.id, upload.fileName)
|
||||
return generateDownloadSignedUrl(filePath)
|
||||
}
|
||||
return article.url
|
||||
return article.originalUrl
|
||||
},
|
||||
originalArticleUrl(article: { url: string }) {
|
||||
return article.url
|
||||
originalArticleUrl(article: LibraryItem) {
|
||||
return article.originalUrl
|
||||
},
|
||||
hasContent(article: {
|
||||
content: string | null
|
||||
originalHtml: string | null
|
||||
}) {
|
||||
return !!article.originalHtml && !!article.content
|
||||
hasContent(article: LibraryItem) {
|
||||
return !!article.originalContent && !!article.readableContent
|
||||
},
|
||||
publishedAt(article: { publishedAt: Date }) {
|
||||
return validatedDate(article.publishedAt)
|
||||
publishedAt(article: LibraryItem) {
|
||||
return validatedDate(article.publishedAt || undefined)
|
||||
},
|
||||
image(article: { image?: string }): string | undefined {
|
||||
return article.image && createImageProxyUrl(article.image, 320, 320)
|
||||
image(article: LibraryItem): string | undefined {
|
||||
if (article.thumbnail) {
|
||||
return createImageProxyUrl(article.thumbnail, 320, 320)
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
wordsCount(article: { wordCount?: number; content?: string }) {
|
||||
wordsCount(article: LibraryItem): number | undefined {
|
||||
if (article.wordCount) return article.wordCount
|
||||
return article.content ? wordsCount(article.content) : undefined
|
||||
|
||||
return article.readableContent
|
||||
? wordsCount(article.readableContent)
|
||||
: undefined
|
||||
},
|
||||
async labels(
|
||||
article: { id: string; labels?: Label[] },
|
||||
article: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
|
|
@ -462,40 +438,57 @@ export const functionResolvers = {
|
|||
|
||||
return ctx.dataLoaders.labels.load(article.id)
|
||||
},
|
||||
async highlights(
|
||||
article: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
if (article.highlights) return article.highlights
|
||||
|
||||
return ctx.dataLoaders.highlights.load(article.id)
|
||||
},
|
||||
content: (item: LibraryItem) => item.readableContent,
|
||||
hash: (item: LibraryItem) => item.textContentHash || '',
|
||||
isArchived: (item: LibraryItem) => !!item.archivedAt,
|
||||
uploadFileId: (item: LibraryItem) => item.uploadFile?.id,
|
||||
pageType: (item: LibraryItem) => item.itemType,
|
||||
...readingProgressHandlers,
|
||||
},
|
||||
Highlight: {
|
||||
// async reactions(
|
||||
// highlight: { id: string; reactions?: Reaction[] },
|
||||
// _: unknown,
|
||||
// ctx: WithDataSourcesContext
|
||||
// ) {
|
||||
// const { reactions, id } = highlight
|
||||
// if (reactions) return reactions
|
||||
|
||||
// return await ctx.models.reaction.batchGetFromHighlight(id)
|
||||
// },
|
||||
reactions: () => [],
|
||||
replies: () => [],
|
||||
type: (highlight: Highlight) => highlight.highlightType,
|
||||
async user(highlight: Highlight, __: unknown, ctx: WithDataSourcesContext) {
|
||||
return ctx.dataLoaders.users.load(highlight.userId)
|
||||
},
|
||||
createdByMe(
|
||||
highlight: { user: { id: string } },
|
||||
highlight: Highlight,
|
||||
__: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
return highlight.user.id === ctx.uid
|
||||
return highlight.userId === ctx.uid
|
||||
},
|
||||
libraryItem(highlight: Highlight, _: unknown, ctx: WithDataSourcesContext) {
|
||||
if (highlight.libraryItem) {
|
||||
return highlight.libraryItem
|
||||
}
|
||||
|
||||
return ctx.dataLoaders.libraryItems.load(highlight.libraryItemId)
|
||||
},
|
||||
labels: async (
|
||||
highlight: Highlight,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) => {
|
||||
return (
|
||||
highlight.labels || ctx.dataLoaders.highlightLabels.load(highlight.id)
|
||||
)
|
||||
},
|
||||
},
|
||||
// Reaction: {
|
||||
// async user(
|
||||
// reaction: { userId: string },
|
||||
// __: unknown,
|
||||
// ctx: WithDataSourcesContext
|
||||
// ) {
|
||||
// return userDataToUser(await ctx.models.user.get(reaction.userId))
|
||||
// },
|
||||
// },
|
||||
SearchItem: {
|
||||
async url(item: SearchItem, _: unknown, ctx: WithDataSourcesContext) {
|
||||
async url(item: LibraryItem, _: unknown, ctx: WithDataSourcesContext) {
|
||||
if (
|
||||
(item.pageType == PageType.File || item.pageType == PageType.Book) &&
|
||||
(item.itemType == PageType.File || item.itemType == PageType.Book) &&
|
||||
ctx.claims &&
|
||||
item.uploadFileId
|
||||
) {
|
||||
|
|
@ -506,51 +499,44 @@ export const functionResolvers = {
|
|||
const filePath = generateUploadFilePathName(upload.id, upload.fileName)
|
||||
return generateDownloadSignedUrl(filePath)
|
||||
}
|
||||
return item.url
|
||||
return item.originalUrl
|
||||
},
|
||||
image(item: SearchItem) {
|
||||
return item.image && createImageProxyUrl(item.image, 320, 320)
|
||||
image(item: LibraryItem) {
|
||||
return item.thumbnail && createImageProxyUrl(item.thumbnail, 320, 320)
|
||||
},
|
||||
originalArticleUrl(item: { url: string }) {
|
||||
return item.url
|
||||
originalArticleUrl(item: LibraryItem) {
|
||||
return item.originalUrl
|
||||
},
|
||||
wordsCount(item: { wordCount?: number; content?: string }) {
|
||||
wordsCount(item: LibraryItem) {
|
||||
if (item.wordCount) return item.wordCount
|
||||
return item.content ? wordsCount(item.content) : undefined
|
||||
return item.readableContent ? wordsCount(item.readableContent) : undefined
|
||||
},
|
||||
siteIcon(item: { siteIcon?: string }) {
|
||||
siteIcon(item: LibraryItem) {
|
||||
if (item.siteIcon && !isBase64Image(item.siteIcon)) {
|
||||
return createImageProxyUrl(item.siteIcon, 128, 128)
|
||||
}
|
||||
|
||||
return item.siteIcon
|
||||
},
|
||||
async labels(
|
||||
item: { id: string; labels?: Label[] },
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
async labels(item: LibraryItem, _: unknown, ctx: WithDataSourcesContext) {
|
||||
if (item.labels) return item.labels
|
||||
|
||||
const labels = await ctx.dataLoaders.labels.load(item.id)
|
||||
return labels
|
||||
return ctx.dataLoaders.labels.load(item.id)
|
||||
},
|
||||
async recommendations(
|
||||
item: {
|
||||
id: string
|
||||
recommendations?: Recommendation[]
|
||||
},
|
||||
item: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
if (item.recommendations) return item.recommendations
|
||||
|
||||
const recommendations = await ctx.dataLoaders.recommendations.load(
|
||||
item.id
|
||||
)
|
||||
return recommendations.map(recommandationDataToRecommendation)
|
||||
return ctx.dataLoaders.recommendations.load(item.id)
|
||||
},
|
||||
async aiSummary(item: SearchItem, _: unknown, ctx: WithDataSourcesContext) {
|
||||
async aiSummary(
|
||||
item: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
return (
|
||||
await getAISummary({
|
||||
userId: ctx.uid,
|
||||
|
|
@ -560,32 +546,26 @@ export const functionResolvers = {
|
|||
)?.summary
|
||||
},
|
||||
async highlights(
|
||||
item: {
|
||||
id: string
|
||||
highlights?: Highlight[]
|
||||
},
|
||||
item: LibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
if (item.highlights) return item.highlights
|
||||
|
||||
const highlights = await ctx.dataLoaders.highlights.load(item.id)
|
||||
return highlights.map(highlightDataToHighlight)
|
||||
return ctx.dataLoaders.highlights.load(item.id)
|
||||
},
|
||||
...readingProgressHandlers,
|
||||
async content(
|
||||
item: {
|
||||
id: string
|
||||
content?: string
|
||||
highlightAnnotations?: string[]
|
||||
format?: ArticleFormat
|
||||
},
|
||||
item: PartialLibraryItem,
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
// convert html to the requested format if requested
|
||||
if (item.format && item.format !== ArticleFormat.Html && item.content) {
|
||||
let highlights: HighlightEntity[] = []
|
||||
if (
|
||||
item.format &&
|
||||
item.format !== ArticleFormat.Html &&
|
||||
item.readableContent
|
||||
) {
|
||||
let highlights: Highlight[] = []
|
||||
// load highlights if needed
|
||||
if (
|
||||
item.format === ArticleFormat.HighlightedMarkdown &&
|
||||
|
|
@ -600,15 +580,18 @@ export const functionResolvers = {
|
|||
// convert html to the requested format
|
||||
const converter = contentConverter(item.format)
|
||||
if (converter) {
|
||||
return converter(item.content, highlights)
|
||||
return converter(item.readableContent, highlights)
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.log.error('Error converting content', error)
|
||||
}
|
||||
}
|
||||
|
||||
return item.content
|
||||
return item.readableContent
|
||||
},
|
||||
isArchived: (item: LibraryItem) => !!item.archivedAt,
|
||||
pageType: (item: LibraryItem) => item.itemType,
|
||||
...readingProgressHandlers,
|
||||
},
|
||||
Subscription: {
|
||||
newsletterEmail(subscription: Subscription) {
|
||||
|
|
@ -783,37 +766,43 @@ export const functionResolvers = {
|
|||
}
|
||||
},
|
||||
},
|
||||
ArticleSavingRequest: {
|
||||
status: (item: LibraryItem) => item.state,
|
||||
url: (item: LibraryItem) => item.originalUrl,
|
||||
},
|
||||
Recommendation: {
|
||||
user: (recommendation: Recommendation) => {
|
||||
return {
|
||||
userId: recommendation.recommender.id,
|
||||
username: recommendation.recommender.profile.username,
|
||||
profileImageURL: recommendation.recommender.profile.pictureUrl,
|
||||
name: recommendation.recommender.name,
|
||||
}
|
||||
},
|
||||
name: (recommendation: Recommendation) => recommendation.group.name,
|
||||
recommendedAt: (recommendation: Recommendation) => recommendation.createdAt,
|
||||
},
|
||||
...resultResolveTypeResolver('Login'),
|
||||
...resultResolveTypeResolver('LogOut'),
|
||||
...resultResolveTypeResolver('GoogleSignup'),
|
||||
...resultResolveTypeResolver('UpdateUser'),
|
||||
...resultResolveTypeResolver('UpdateUserProfile'),
|
||||
...resultResolveTypeResolver('Article'),
|
||||
// ...resultResolveTypeResolver('SharedArticle'),
|
||||
...resultResolveTypeResolver('Articles'),
|
||||
...resultResolveTypeResolver('User'),
|
||||
...resultResolveTypeResolver('Users'),
|
||||
...resultResolveTypeResolver('SaveArticleReadingProgress'),
|
||||
// ...resultResolveTypeResolver('FeedArticles'),
|
||||
...resultResolveTypeResolver('CreateArticle'),
|
||||
...resultResolveTypeResolver('CreateHighlight'),
|
||||
// ...resultResolveTypeResolver('CreateReaction'),
|
||||
// ...resultResolveTypeResolver('DeleteReaction'),
|
||||
...resultResolveTypeResolver('MergeHighlight'),
|
||||
...resultResolveTypeResolver('UpdateHighlight'),
|
||||
...resultResolveTypeResolver('DeleteHighlight'),
|
||||
...resultResolveTypeResolver('UploadFileRequest'),
|
||||
// ...resultResolveTypeResolver('SetShareArticle'),
|
||||
// ...resultResolveTypeResolver('UpdateSharedComment'),
|
||||
...resultResolveTypeResolver('SetBookmarkArticle'),
|
||||
// ...resultResolveTypeResolver('SetFollow'),
|
||||
// ...resultResolveTypeResolver('GetFollowers'),
|
||||
// ...resultResolveTypeResolver('GetFollowing'),
|
||||
...resultResolveTypeResolver('GetUserPersonalization'),
|
||||
...resultResolveTypeResolver('SetUserPersonalization'),
|
||||
...resultResolveTypeResolver('ArticleSavingRequest'),
|
||||
...resultResolveTypeResolver('CreateArticleSavingRequest'),
|
||||
// ...resultResolveTypeResolver('SetShareHighlight'),
|
||||
...resultResolveTypeResolver('ArchiveLink'),
|
||||
...resultResolveTypeResolver('CreateNewsletterEmail'),
|
||||
...resultResolveTypeResolver('NewsletterEmails'),
|
||||
|
|
@ -886,4 +875,5 @@ export const functionResolvers = {
|
|||
...resultResolveTypeResolver('Subscription'),
|
||||
...resultResolveTypeResolver('RefreshHome'),
|
||||
...resultResolveTypeResolver('HiddenHomeSection'),
|
||||
...resultResolveTypeResolver('Highlights'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { DeepPartial } from 'typeorm'
|
||||
import {
|
||||
Highlight as HighlightData,
|
||||
Highlight as HighlightEntity,
|
||||
HighlightType,
|
||||
RepresentationType,
|
||||
} from '../../entity/highlight'
|
||||
|
|
@ -16,6 +16,10 @@ import {
|
|||
DeleteHighlightError,
|
||||
DeleteHighlightErrorCode,
|
||||
DeleteHighlightSuccess,
|
||||
HighlightEdge,
|
||||
HighlightsError,
|
||||
HighlightsErrorCode,
|
||||
HighlightsSuccess,
|
||||
MergeHighlightError,
|
||||
MergeHighlightErrorCode,
|
||||
MergeHighlightSuccess,
|
||||
|
|
@ -23,6 +27,7 @@ import {
|
|||
MutationDeleteHighlightArgs,
|
||||
MutationMergeHighlightArgs,
|
||||
MutationUpdateHighlightArgs,
|
||||
QueryHighlightsArgs,
|
||||
UpdateHighlightError,
|
||||
UpdateHighlightErrorCode,
|
||||
UpdateHighlightSuccess,
|
||||
|
|
@ -32,14 +37,15 @@ import {
|
|||
createHighlight,
|
||||
deleteHighlightById,
|
||||
mergeHighlights,
|
||||
searchHighlights,
|
||||
updateHighlight,
|
||||
} from '../../services/highlights'
|
||||
import { Merge } from '../../util'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import { highlightDataToHighlight } from '../../utils/helpers'
|
||||
|
||||
export const createHighlightResolver = authorized<
|
||||
CreateHighlightSuccess,
|
||||
Merge<CreateHighlightSuccess, { highlight: HighlightEntity }>,
|
||||
CreateHighlightError,
|
||||
MutationCreateHighlightArgs
|
||||
>(async (_, { input }, { log, pubsub, uid }) => {
|
||||
|
|
@ -68,7 +74,7 @@ export const createHighlightResolver = authorized<
|
|||
},
|
||||
})
|
||||
|
||||
return { highlight: highlightDataToHighlight(newHighlight) }
|
||||
return { highlight: newHighlight }
|
||||
} catch (err) {
|
||||
log.error('Error creating highlight', err)
|
||||
return {
|
||||
|
|
@ -78,7 +84,7 @@ export const createHighlightResolver = authorized<
|
|||
})
|
||||
|
||||
export const mergeHighlightResolver = authorized<
|
||||
MergeHighlightSuccess,
|
||||
Merge<MergeHighlightSuccess, { highlight: HighlightEntity }>,
|
||||
MergeHighlightError,
|
||||
MutationMergeHighlightArgs
|
||||
>(async (_, { input }, { authTrx, log, pubsub, uid }) => {
|
||||
|
|
@ -123,7 +129,7 @@ export const mergeHighlightResolver = authorized<
|
|||
const color =
|
||||
newHighlightInput.color || mergedColors[mergedColors.length - 1]
|
||||
|
||||
const highlight: DeepPartial<HighlightData> = {
|
||||
const highlight: DeepPartial<HighlightEntity> = {
|
||||
...newHighlightInput,
|
||||
annotation:
|
||||
mergedAnnotations.length > 0 ? mergedAnnotations.join('\n') : null,
|
||||
|
|
@ -154,7 +160,7 @@ export const mergeHighlightResolver = authorized<
|
|||
})
|
||||
|
||||
return {
|
||||
highlight: highlightDataToHighlight(newHighlight),
|
||||
highlight: newHighlight,
|
||||
overlapHighlightIdList: input.overlapHighlightIdList,
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -166,7 +172,7 @@ export const mergeHighlightResolver = authorized<
|
|||
})
|
||||
|
||||
export const updateHighlightResolver = authorized<
|
||||
UpdateHighlightSuccess,
|
||||
Merge<UpdateHighlightSuccess, { highlight: HighlightEntity }>,
|
||||
UpdateHighlightError,
|
||||
MutationUpdateHighlightArgs
|
||||
>(async (_, { input }, { pubsub, uid, log }) => {
|
||||
|
|
@ -183,7 +189,7 @@ export const updateHighlightResolver = authorized<
|
|||
pubsub
|
||||
)
|
||||
|
||||
return { highlight: highlightDataToHighlight(updatedHighlight) }
|
||||
return { highlight: updatedHighlight }
|
||||
} catch (error) {
|
||||
log.error('updateHighlightResolver error', error)
|
||||
return {
|
||||
|
|
@ -193,7 +199,7 @@ export const updateHighlightResolver = authorized<
|
|||
})
|
||||
|
||||
export const deleteHighlightResolver = authorized<
|
||||
DeleteHighlightSuccess,
|
||||
Merge<DeleteHighlightSuccess, { highlight: HighlightEntity }>,
|
||||
DeleteHighlightError,
|
||||
MutationDeleteHighlightArgs
|
||||
>(async (_, { highlightId }, { log }) => {
|
||||
|
|
@ -206,7 +212,7 @@ export const deleteHighlightResolver = authorized<
|
|||
}
|
||||
}
|
||||
|
||||
return { highlight: highlightDataToHighlight(deletedHighlight) }
|
||||
return { highlight: deletedHighlight }
|
||||
} catch (error) {
|
||||
log.error('deleteHighlightResolver error', error)
|
||||
return {
|
||||
|
|
@ -215,53 +221,63 @@ export const deleteHighlightResolver = authorized<
|
|||
}
|
||||
})
|
||||
|
||||
// export const setShareHighlightResolver = authorized<
|
||||
// SetShareHighlightSuccess,
|
||||
// SetShareHighlightError,
|
||||
// MutationSetShareHighlightArgs
|
||||
// >(async (_, { input: { id, share } }, { pubsub, claims, log }) => {
|
||||
// const highlight = await getHighlightById(id)
|
||||
type PartialHighlightEdge = Merge<
|
||||
HighlightEdge,
|
||||
{
|
||||
node: HighlightEntity
|
||||
}
|
||||
>
|
||||
type PartialHighlightsSuccess = Merge<
|
||||
HighlightsSuccess,
|
||||
{
|
||||
edges: PartialHighlightEdge[]
|
||||
}
|
||||
>
|
||||
export const highlightsResolver = authorized<
|
||||
PartialHighlightsSuccess,
|
||||
HighlightsError,
|
||||
QueryHighlightsArgs
|
||||
>(async (_, { after, first, query }, { uid, log }) => {
|
||||
const limit = first || 10
|
||||
const offset = parseInt(after || '0')
|
||||
if (
|
||||
isNaN(offset) ||
|
||||
offset < 0 ||
|
||||
limit > 50 ||
|
||||
(query?.length && query.length > 1000)
|
||||
) {
|
||||
log.error('Invalid args', { after, first, query })
|
||||
|
||||
// if (!highlight?.id) {
|
||||
// return {
|
||||
// errorCodes: [SetShareHighlightErrorCode.NotFound],
|
||||
// }
|
||||
// }
|
||||
return {
|
||||
errorCodes: [HighlightsErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
// if (highlight.userId !== claims.uid) {
|
||||
// return {
|
||||
// errorCodes: [SetShareHighlightErrorCode.Forbidden],
|
||||
// }
|
||||
// }
|
||||
const highlights = await searchHighlights(
|
||||
uid,
|
||||
query || undefined,
|
||||
limit + 1,
|
||||
offset
|
||||
)
|
||||
|
||||
// const sharedAt = share ? new Date() : null
|
||||
const hasNextPage = highlights.length > limit
|
||||
if (hasNextPage) {
|
||||
highlights.pop()
|
||||
}
|
||||
const endCursor = String(offset + highlights.length)
|
||||
|
||||
// log.info(`${share ? 'S' : 'Uns'}haring a highlight`, {
|
||||
// highlight,
|
||||
// labels: {
|
||||
// source: 'resolver',
|
||||
// resolver: 'setShareHighlightResolver',
|
||||
// userId: highlight.userId,
|
||||
// },
|
||||
// })
|
||||
const edges = highlights.map((highlight) => ({
|
||||
cursor: endCursor,
|
||||
node: highlight,
|
||||
}))
|
||||
|
||||
// const updatedHighlight: HighlightData = {
|
||||
// ...highlight,
|
||||
// sharedAt,
|
||||
// updatedAt: new Date(),
|
||||
// }
|
||||
|
||||
// const updated = await updateHighlight(updatedHighlight, {
|
||||
// pubsub,
|
||||
// uid: claims.uid,
|
||||
// refresh: true,
|
||||
// })
|
||||
|
||||
// if (!updated) {
|
||||
// return {
|
||||
// errorCodes: [SetShareHighlightErrorCode.NotFound],
|
||||
// }
|
||||
// }
|
||||
|
||||
// return { highlight: highlightDataToHighlight(updatedHighlight) }
|
||||
// })
|
||||
return {
|
||||
edges,
|
||||
pageInfo: {
|
||||
startCursor: String(offset),
|
||||
endCursor,
|
||||
hasPreviousPage: offset > 0,
|
||||
hasNextPage,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { In } from 'typeorm'
|
||||
import { Group } from '../../entity/groups/group'
|
||||
import { User } from '../../entity/user'
|
||||
import { env } from '../../env'
|
||||
import {
|
||||
CreateGroupError,
|
||||
|
|
@ -19,6 +20,7 @@ import {
|
|||
MutationLeaveGroupArgs,
|
||||
MutationRecommendArgs,
|
||||
MutationRecommendHighlightsArgs,
|
||||
RecommendationGroup,
|
||||
RecommendError,
|
||||
RecommendErrorCode,
|
||||
RecommendHighlightsError,
|
||||
|
|
@ -38,26 +40,30 @@ import {
|
|||
leaveGroup,
|
||||
} from '../../services/groups'
|
||||
import { findLibraryItemById } from '../../services/library_item'
|
||||
import { Merge } from '../../util'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { enqueueRecommendation } from '../../utils/createTask'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import { userDataToUser } from '../../utils/helpers'
|
||||
|
||||
export type PartialRecommendationGroup = Merge<
|
||||
RecommendationGroup,
|
||||
{ admins: Array<User>; members: Array<User> }
|
||||
>
|
||||
export const createGroupResolver = authorized<
|
||||
CreateGroupSuccess,
|
||||
Merge<CreateGroupSuccess, { group: PartialRecommendationGroup }>,
|
||||
CreateGroupError,
|
||||
MutationCreateGroupArgs
|
||||
>(async (_, { input }, { uid, log }) => {
|
||||
try {
|
||||
const userData = await userRepository.findById(uid)
|
||||
if (!userData) {
|
||||
const user = await userRepository.findById(uid)
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [CreateGroupErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const [group, invite] = await createGroup({
|
||||
admin: userData,
|
||||
admin: user,
|
||||
name: input.name,
|
||||
maxMembers: input.maxMembers,
|
||||
expiresInDays: input.expiresInDays,
|
||||
|
|
@ -80,7 +86,6 @@ export const createGroupResolver = authorized<
|
|||
await createLabelAndRuleForGroup(uid, group.name)
|
||||
|
||||
const inviteUrl = getInviteUrl(invite)
|
||||
const user = userDataToUser(userData)
|
||||
|
||||
return {
|
||||
group: {
|
||||
|
|
@ -103,37 +108,38 @@ export const createGroupResolver = authorized<
|
|||
}
|
||||
})
|
||||
|
||||
export const groupsResolver = authorized<GroupsSuccess, GroupsError>(
|
||||
async (_, __, { uid, log }) => {
|
||||
try {
|
||||
const user = await userRepository.findById(uid)
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [GroupsErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const groups = await getRecommendationGroups(user)
|
||||
|
||||
export const groupsResolver = authorized<
|
||||
Merge<GroupsSuccess, { groups: Array<PartialRecommendationGroup> }>,
|
||||
GroupsError
|
||||
>(async (_, __, { uid, log }) => {
|
||||
try {
|
||||
const user = await userRepository.findById(uid)
|
||||
if (!user) {
|
||||
return {
|
||||
groups,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error getting groups', {
|
||||
error,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'groupsResolver',
|
||||
uid,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
errorCodes: [GroupsErrorCode.BadRequest],
|
||||
errorCodes: [GroupsErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const groups = await getRecommendationGroups(user)
|
||||
|
||||
return {
|
||||
groups,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error getting groups', {
|
||||
error,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'groupsResolver',
|
||||
uid,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
errorCodes: [GroupsErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export const recommendResolver = authorized<
|
||||
RecommendSuccess,
|
||||
|
|
@ -206,7 +212,7 @@ export const recommendResolver = authorized<
|
|||
})
|
||||
|
||||
export const joinGroupResolver = authorized<
|
||||
JoinGroupSuccess,
|
||||
Merge<JoinGroupSuccess, { group: PartialRecommendationGroup }>,
|
||||
JoinGroupError,
|
||||
MutationJoinGroupArgs
|
||||
>(async (_, { inviteCode }, { uid, log }) => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { PublicItem } from '../entity/public_item'
|
|||
import { Recommendation } from '../entity/recommendation'
|
||||
import { Subscription } from '../entity/subscription'
|
||||
import { UploadFile } from '../entity/upload_file'
|
||||
import { User } from '../entity/user'
|
||||
import { HomeItem } from '../generated/graphql'
|
||||
import { PubsubClient } from '../pubsub'
|
||||
|
||||
|
|
@ -58,6 +59,8 @@ export interface RequestContext {
|
|||
libraryItems: DataLoader<string, LibraryItem | undefined>
|
||||
publicItems: DataLoader<string, PublicItem | undefined>
|
||||
subscriptions: DataLoader<string, Subscription | undefined>
|
||||
users: DataLoader<string, User | undefined>
|
||||
highlightLabels: DataLoader<string, Label[]>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { LibraryItemState } from '../../entity/library_item'
|
||||
import { LibraryItem, LibraryItemState } from '../../entity/library_item'
|
||||
import {
|
||||
MutationUpdatePageArgs,
|
||||
UpdatePageError,
|
||||
UpdatePageSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { updateLibraryItem } from '../../services/library_item'
|
||||
import { libraryItemToArticle } from '../../utils/helpers'
|
||||
import { Merge } from '../../util'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
|
||||
export const updatePageResolver = authorized<
|
||||
UpdatePageSuccess,
|
||||
Merge<UpdatePageSuccess, { updatedPage: LibraryItem }>,
|
||||
UpdatePageError,
|
||||
MutationUpdatePageArgs
|
||||
>(async (_, { input }, { uid }) => {
|
||||
|
|
@ -29,6 +29,6 @@ export const updatePageResolver = authorized<
|
|||
uid
|
||||
)
|
||||
return {
|
||||
updatedPage: libraryItemToArticle(updatedPage),
|
||||
updatedPage: updatedPage,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import {
|
|||
UpdateUserProfileErrorCode,
|
||||
UpdateUserProfileSuccess,
|
||||
UpdateUserSuccess,
|
||||
User,
|
||||
UserErrorCode,
|
||||
UserResult,
|
||||
UsersError,
|
||||
|
|
@ -43,13 +42,13 @@ import { userRepository } from '../../repository/user'
|
|||
import { createUser } from '../../services/create_user'
|
||||
import { sendAccountChangeEmail } from '../../services/send_emails'
|
||||
import { softDeleteUser } from '../../services/user'
|
||||
import { userDataToUser } from '../../utils/helpers'
|
||||
import { Merge } from '../../util'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import { validateUsername } from '../../utils/usernamePolicy'
|
||||
import { WithDataSourcesContext } from '../types'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
|
||||
export const updateUserResolver = authorized<
|
||||
UpdateUserSuccess,
|
||||
Merge<UpdateUserSuccess, { user: UserEntity }>,
|
||||
UpdateUserError,
|
||||
MutationUpdateUserArgs
|
||||
>(async (_, { input: { name, bio } }, { uid, authTrx }) => {
|
||||
|
|
@ -83,11 +82,11 @@ export const updateUserResolver = authorized<
|
|||
})
|
||||
)
|
||||
|
||||
return { user: userDataToUser(updatedUser) }
|
||||
return { user: updatedUser }
|
||||
})
|
||||
|
||||
export const updateUserProfileResolver = authorized<
|
||||
UpdateUserProfileSuccess,
|
||||
Merge<UpdateUserProfileSuccess, { user: UserEntity }>,
|
||||
UpdateUserProfileError,
|
||||
MutationUpdateUserProfileArgs
|
||||
>(async (_, { input: { userId, username, pictureUrl } }, { uid, authTrx }) => {
|
||||
|
|
@ -140,11 +139,11 @@ export const updateUserProfileResolver = authorized<
|
|||
})
|
||||
)
|
||||
|
||||
return { user: userDataToUser(updatedUser) }
|
||||
return { user: updatedUser }
|
||||
})
|
||||
|
||||
export const googleLoginResolver: ResolverFn<
|
||||
LoginResult,
|
||||
Merge<LoginResult, { me?: UserEntity }>,
|
||||
unknown,
|
||||
WithDataSourcesContext,
|
||||
MutationGoogleLoginArgs
|
||||
|
|
@ -167,7 +166,7 @@ export const googleLoginResolver: ResolverFn<
|
|||
|
||||
// set auth cookie in response header
|
||||
await setAuth({ uid: user.id })
|
||||
return { me: userDataToUser(user) }
|
||||
return { me: user }
|
||||
}
|
||||
|
||||
export const validateUsernameResolver: ResolverFn<
|
||||
|
|
@ -190,7 +189,7 @@ export const validateUsernameResolver: ResolverFn<
|
|||
}
|
||||
|
||||
export const googleSignupResolver: ResolverFn<
|
||||
GoogleSignupResult,
|
||||
Merge<GoogleSignupResult, { me?: UserEntity }>,
|
||||
Record<string, unknown>,
|
||||
WithDataSourcesContext,
|
||||
MutationGoogleSignupArgs
|
||||
|
|
@ -205,7 +204,7 @@ export const googleSignupResolver: ResolverFn<
|
|||
}
|
||||
|
||||
try {
|
||||
const [user, profile] = await createUser({
|
||||
const [user] = await createUser({
|
||||
email,
|
||||
sourceUserId,
|
||||
provider: 'GOOGLE',
|
||||
|
|
@ -218,7 +217,7 @@ export const googleSignupResolver: ResolverFn<
|
|||
|
||||
await setAuth({ uid: user.id })
|
||||
return {
|
||||
me: userDataToUser({ ...user, profile: { ...profile, private: false } }),
|
||||
me: user,
|
||||
}
|
||||
} catch (err) {
|
||||
log.info('error signing up with google', err)
|
||||
|
|
@ -245,7 +244,7 @@ export const logOutResolver: ResolverFn<
|
|||
}
|
||||
|
||||
export const getMeUserResolver: ResolverFn<
|
||||
User | undefined,
|
||||
UserEntity | undefined,
|
||||
unknown,
|
||||
WithDataSourcesContext,
|
||||
unknown
|
||||
|
|
@ -260,14 +259,14 @@ export const getMeUserResolver: ResolverFn<
|
|||
return undefined
|
||||
}
|
||||
|
||||
return userDataToUser(user)
|
||||
return user
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserResolver: ResolverFn<
|
||||
UserResult,
|
||||
Merge<UserResult, { user?: UserEntity }>,
|
||||
unknown,
|
||||
WithDataSourcesContext,
|
||||
QueryUserArgs
|
||||
|
|
@ -294,16 +293,17 @@ export const getUserResolver: ResolverFn<
|
|||
return { errorCodes: [UserErrorCode.UserNotFound] }
|
||||
}
|
||||
|
||||
return { user: userDataToUser(userRecord) }
|
||||
return { user: userRecord }
|
||||
}
|
||||
|
||||
export const getAllUsersResolver = authorized<UsersSuccess, UsersError>(
|
||||
async (_obj, _params) => {
|
||||
const users = await userRepository.findTopUsers()
|
||||
const result = { users: users.map((userData) => userDataToUser(userData)) }
|
||||
return result
|
||||
}
|
||||
)
|
||||
export const getAllUsersResolver = authorized<
|
||||
Merge<UsersSuccess, { users: Array<UserEntity> }>,
|
||||
UsersError
|
||||
>(async (_obj, _params) => {
|
||||
const users = await userRepository.findTopUsers()
|
||||
const result = { users }
|
||||
return result
|
||||
})
|
||||
|
||||
type ErrorWithCode = {
|
||||
errorCode: string
|
||||
|
|
|
|||
|
|
@ -49,22 +49,23 @@ export function articleRouter() {
|
|||
return res.status(400).send('Bad Request')
|
||||
}
|
||||
|
||||
const result = await createPageSaveRequest({ user, url })
|
||||
|
||||
if (isSiteBlockedForParse(url)) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({ errorCode: CreateArticleErrorCode.NotAllowedToParse })
|
||||
}
|
||||
|
||||
if (result.errorCode) {
|
||||
return res.status(400).send({ errorCode: result.errorCode })
|
||||
}
|
||||
try {
|
||||
const result = await createPageSaveRequest({ user, url })
|
||||
|
||||
return res.send({
|
||||
articleSavingRequestId: result.id,
|
||||
url: result.url,
|
||||
})
|
||||
return res.send({
|
||||
articleSavingRequestId: result.id,
|
||||
url: result.originalUrl,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error saving article:', error)
|
||||
return res.status(500).send({ errorCode: 'INTERNAL_ERROR' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get(
|
||||
|
|
|
|||
|
|
@ -751,6 +751,7 @@ const schema = gql`
|
|||
html: String
|
||||
color: String
|
||||
representation: RepresentationType!
|
||||
libraryItem: Article!
|
||||
}
|
||||
|
||||
input CreateHighlightInput {
|
||||
|
|
@ -3226,6 +3227,26 @@ const schema = gql`
|
|||
PENDING
|
||||
}
|
||||
|
||||
union HighlightsResult = HighlightsSuccess | HighlightsError
|
||||
|
||||
type HighlightsSuccess {
|
||||
edges: [HighlightEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type HighlightEdge {
|
||||
cursor: String!
|
||||
node: Highlight!
|
||||
}
|
||||
|
||||
type HighlightsError {
|
||||
errorCodes: [HighlightsErrorCode!]!
|
||||
}
|
||||
|
||||
enum HighlightsErrorCode {
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
|
|
@ -3425,6 +3446,7 @@ const schema = gql`
|
|||
home(first: Int, after: String): HomeResult!
|
||||
subscription(id: ID!): SubscriptionResult!
|
||||
hiddenHomeSection: HiddenHomeSectionResult!
|
||||
highlights(after: String, first: Int, query: String): HighlightsResult!
|
||||
}
|
||||
|
||||
schema {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import * as privateIpLib from 'private-ip'
|
||||
import { LibraryItemState } from '../entity/library_item'
|
||||
import { LibraryItem, LibraryItemState } from '../entity/library_item'
|
||||
import { User } from '../entity/user'
|
||||
import {
|
||||
ArticleSavingRequest,
|
||||
ArticleSavingRequestStatus,
|
||||
CreateArticleSavingRequestErrorCode,
|
||||
CreateLabelInput,
|
||||
PageType,
|
||||
} from '../generated/graphql'
|
||||
import { createPubSubClient, PubsubClient } from '../pubsub'
|
||||
import { Merge } from '../util'
|
||||
import { enqueueParseRequest } from '../utils/createTask'
|
||||
import {
|
||||
cleanUrl,
|
||||
generateSlug,
|
||||
libraryItemToArticleSavingRequest,
|
||||
} from '../utils/helpers'
|
||||
import { cleanUrl, generateSlug } from '../utils/helpers'
|
||||
import { logger } from '../utils/logger'
|
||||
import { countBySavedAt, createOrUpdateLibraryItem } from './library_item'
|
||||
|
||||
|
|
@ -88,11 +84,12 @@ export const createPageSaveRequest = async ({
|
|||
publishedAt,
|
||||
folder,
|
||||
subscription,
|
||||
}: PageSaveRequest): Promise<ArticleSavingRequest> => {
|
||||
}: PageSaveRequest): Promise<LibraryItem> => {
|
||||
try {
|
||||
validateUrl(url)
|
||||
} catch (error) {
|
||||
logger.info('invalid url', { url, error })
|
||||
logger.error('invalid url', { url, error })
|
||||
|
||||
return Promise.reject({
|
||||
errorCode: CreateArticleSavingRequestErrorCode.BadData,
|
||||
})
|
||||
|
|
@ -140,5 +137,5 @@ export const createPageSaveRequest = async ({
|
|||
rssFeedUrl: subscription,
|
||||
})
|
||||
|
||||
return libraryItemToArticleSavingRequest(user, libraryItem)
|
||||
return libraryItem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ import { Invite } from '../entity/groups/invite'
|
|||
import { RuleActionType } from '../entity/rule'
|
||||
import { User } from '../entity/user'
|
||||
import { homePageURL } from '../env'
|
||||
import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql'
|
||||
import { getRepository } from '../repository'
|
||||
import { userDataToUser } from '../utils/helpers'
|
||||
import { PartialRecommendationGroup } from '../resolvers'
|
||||
import { findOrCreateLabels } from './labels'
|
||||
import { createRule } from './rules'
|
||||
|
||||
|
|
@ -70,22 +69,21 @@ export const createGroup = async (input: {
|
|||
|
||||
export const getRecommendationGroups = async (
|
||||
user: User
|
||||
): Promise<RecommendationGroup[]> => {
|
||||
): Promise<Array<PartialRecommendationGroup>> => {
|
||||
const groupMembers = await getRepository(GroupMembership).find({
|
||||
where: { user: { id: user.id } },
|
||||
relations: ['invite', 'group.members.user.profile'],
|
||||
})
|
||||
|
||||
return groupMembers.map((gm) => {
|
||||
const admins: GraphqlUser[] = []
|
||||
const members: GraphqlUser[] = []
|
||||
const admins: Array<User> = []
|
||||
const members: Array<User> = []
|
||||
// Return all members
|
||||
gm.group.members.forEach((m) => {
|
||||
const user = userDataToUser(m.user)
|
||||
if (m.isAdmin) {
|
||||
admins.push(user)
|
||||
admins.push(m.user)
|
||||
}
|
||||
members.push(user)
|
||||
members.push(m.user)
|
||||
})
|
||||
|
||||
const canSeeMembers = gm.group.onlyAdminCanSeeMembers ? gm.isAdmin : true
|
||||
|
|
@ -113,7 +111,7 @@ export const getInviteUrl = (invite: Invite) => {
|
|||
export const joinGroup = async (
|
||||
user: User,
|
||||
inviteCode: string
|
||||
): Promise<RecommendationGroup> => {
|
||||
): Promise<PartialRecommendationGroup> => {
|
||||
const invite = await appDataSource.transaction<Invite>(async (t) => {
|
||||
// Check if the invite exists
|
||||
const invite = await t
|
||||
|
|
@ -147,15 +145,14 @@ export const joinGroup = async (
|
|||
where: { id: invite.group.id },
|
||||
relations: ['members', 'members.user.profile'],
|
||||
})
|
||||
const admins: GraphqlUser[] = []
|
||||
const members: GraphqlUser[] = []
|
||||
const admins: Array<User> = []
|
||||
const members: Array<User> = []
|
||||
// Return all members
|
||||
group.members.forEach((m) => {
|
||||
const user = userDataToUser(m.user)
|
||||
if (m.isAdmin) {
|
||||
admins.push(user)
|
||||
admins.push(m.user)
|
||||
}
|
||||
members.push(user)
|
||||
members.push(m.user)
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -205,19 +205,26 @@ export const updateHighlight = async (
|
|||
return updatedHighlight
|
||||
}
|
||||
|
||||
export const deleteHighlightById = async (highlightId: string) => {
|
||||
const deletedHighlight = await authTrx(async (tx) => {
|
||||
const highlightRepo = tx.withRepository(highlightRepository)
|
||||
const highlight = await highlightRepo.findOneOrFail({
|
||||
where: { id: highlightId },
|
||||
relations: {
|
||||
user: true,
|
||||
},
|
||||
})
|
||||
export const deleteHighlightById = async (
|
||||
highlightId: string,
|
||||
userId?: string
|
||||
) => {
|
||||
const deletedHighlight = await authTrx(
|
||||
async (tx) => {
|
||||
const highlightRepo = tx.withRepository(highlightRepository)
|
||||
const highlight = await highlightRepo.findOneOrFail({
|
||||
where: { id: highlightId },
|
||||
relations: {
|
||||
user: true,
|
||||
},
|
||||
})
|
||||
|
||||
await highlightRepo.delete(highlightId)
|
||||
return highlight
|
||||
})
|
||||
await highlightRepo.delete(highlightId)
|
||||
return highlight
|
||||
},
|
||||
undefined,
|
||||
userId
|
||||
)
|
||||
|
||||
await enqueueUpdateHighlight({
|
||||
libraryItemId: deletedHighlight.libraryItemId,
|
||||
|
|
@ -227,6 +234,17 @@ export const deleteHighlightById = async (highlightId: string) => {
|
|||
return deletedHighlight
|
||||
}
|
||||
|
||||
export const deleteHighlightsByIds = async (
|
||||
userId: string,
|
||||
highlightIds: string[]
|
||||
) => {
|
||||
await authTrx(
|
||||
async (tx) => tx.getRepository(Highlight).delete(highlightIds),
|
||||
undefined,
|
||||
userId
|
||||
)
|
||||
}
|
||||
|
||||
export const findHighlightById = async (
|
||||
highlightId: string,
|
||||
userId: string
|
||||
|
|
@ -260,3 +278,46 @@ export const findHighlightsByLibraryItemId = async (
|
|||
userId
|
||||
)
|
||||
}
|
||||
|
||||
export const searchHighlights = async (
|
||||
userId: string,
|
||||
query?: string,
|
||||
limit?: number,
|
||||
offset?: number
|
||||
): Promise<Array<Highlight>> => {
|
||||
return authTrx(
|
||||
async (tx) => {
|
||||
const queryBuilder = tx
|
||||
.getRepository(Highlight)
|
||||
.createQueryBuilder('highlight')
|
||||
.andWhere('highlight.userId = :userId', { userId })
|
||||
.orderBy('highlight.updatedAt', 'DESC')
|
||||
.take(limit)
|
||||
.skip(offset)
|
||||
|
||||
if (query) {
|
||||
// parse query and search by it
|
||||
const labelRegex = /label:"([^"]+)"/g
|
||||
const labels = Array.from(query.matchAll(labelRegex)).map(
|
||||
(match) => match[1]
|
||||
)
|
||||
|
||||
labels.forEach((label, index) => {
|
||||
const alias = `label_${index}`
|
||||
queryBuilder.innerJoin(
|
||||
'highlight.labels',
|
||||
alias,
|
||||
`LOWER(${alias}.name) = LOWER(:${alias})`,
|
||||
{
|
||||
[alias]: label,
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return queryBuilder.getMany()
|
||||
},
|
||||
undefined,
|
||||
userId
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,23 @@ export const batchGetLabelsFromLibraryItemIds = async (
|
|||
)
|
||||
}
|
||||
|
||||
export const batchGetLabelsFromHighlightIds = async (
|
||||
highlightIds: readonly string[]
|
||||
): Promise<Label[][]> => {
|
||||
const labels = await authTrx(async (tx) =>
|
||||
tx.getRepository(EntityLabel).find({
|
||||
where: { highlightId: In(highlightIds as string[]) },
|
||||
relations: ['label'],
|
||||
})
|
||||
)
|
||||
|
||||
return highlightIds.map((highlightId) =>
|
||||
labels
|
||||
.filter((label) => label.highlightId === highlightId)
|
||||
.map((label) => label.label)
|
||||
)
|
||||
}
|
||||
|
||||
export const findOrCreateLabels = async (
|
||||
labels: CreateLabelInput[],
|
||||
userId: string
|
||||
|
|
|
|||
|
|
@ -18,7 +18,15 @@ import { env } from '../env'
|
|||
import { BulkActionType, InputMaybe, SortParams } from '../generated/graphql'
|
||||
import { createPubSubClient, EntityEvent, EntityType } from '../pubsub'
|
||||
import { redisDataSource } from '../redis_data_source'
|
||||
import { authTrx, getColumns, queryBuilderToRawSql } from '../repository'
|
||||
import {
|
||||
authTrx,
|
||||
getColumns,
|
||||
paramtersToObject,
|
||||
queryBuilderToRawSql,
|
||||
Select,
|
||||
Sort,
|
||||
SortOrder,
|
||||
} from '../repository'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { Merge, PickTuple } from '../util'
|
||||
import { enqueueBulkUploadContentJob } from '../utils/createTask'
|
||||
|
|
@ -122,22 +130,6 @@ export enum SortBy {
|
|||
WORDS_COUNT = 'wordscount',
|
||||
}
|
||||
|
||||
export enum SortOrder {
|
||||
ASCENDING = 'ASC',
|
||||
DESCENDING = 'DESC',
|
||||
}
|
||||
|
||||
export interface Sort {
|
||||
by: string
|
||||
order?: SortOrder
|
||||
nulls?: 'NULLS FIRST' | 'NULLS LAST'
|
||||
}
|
||||
|
||||
interface Select {
|
||||
column: string
|
||||
alias?: string
|
||||
}
|
||||
|
||||
const readingProgressDataSource = new ReadingProgressDataSource()
|
||||
|
||||
export const batchGetLibraryItems = async (ids: readonly string[]) => {
|
||||
|
|
@ -197,10 +189,6 @@ const handleNoCase = (value: string) => {
|
|||
throw new Error(`Unexpected keyword: ${value}`)
|
||||
}
|
||||
|
||||
const paramtersToObject = (parameters: ObjectLiteral[]) => {
|
||||
return parameters.reduce((a, b) => ({ ...a, ...b }), {})
|
||||
}
|
||||
|
||||
export const sortParamsToSort = (
|
||||
sortParams: InputMaybe<SortParams> | undefined
|
||||
) => {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export const findActiveUser = async (id: string): Promise<User | null> => {
|
|||
return userRepository.findOneBy({ id, status: StatusType.Active })
|
||||
}
|
||||
|
||||
export const findUsersById = async (ids: string[]): Promise<User[]> => {
|
||||
export const findUsersByIds = async (ids: string[]): Promise<User[]> => {
|
||||
return userRepository.findBy({ id: In(ids) })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,30 +7,11 @@ import path from 'path'
|
|||
import _ from 'underscore'
|
||||
import slugify from 'voca/slugify'
|
||||
import wordsCounter from 'word-counting'
|
||||
import { Highlight as HighlightData } from '../entity/highlight'
|
||||
import { LibraryItem, LibraryItemState } from '../entity/library_item'
|
||||
import { Recommendation as RecommendationData } from '../entity/recommendation'
|
||||
import { RegistrationType, User } from '../entity/user'
|
||||
import {
|
||||
Article,
|
||||
ArticleSavingRequest,
|
||||
ArticleSavingRequestStatus,
|
||||
ContentReader,
|
||||
CreateArticleError,
|
||||
CreateArticleSuccess,
|
||||
DirectionalityType,
|
||||
FeedArticle,
|
||||
Highlight,
|
||||
PageType,
|
||||
Profile,
|
||||
Recommendation,
|
||||
SearchItem,
|
||||
} from '../generated/graphql'
|
||||
import { CreateArticleError } from '../generated/graphql'
|
||||
import { createPubSubClient } from '../pubsub'
|
||||
import { ArticleFormat } from '../resolvers'
|
||||
import { validateUrl } from '../services/create_page_save_request'
|
||||
import { updateLibraryItem } from '../services/library_item'
|
||||
import { Merge } from '../util'
|
||||
import { logger } from './logger'
|
||||
|
||||
interface InputObject {
|
||||
|
|
@ -101,55 +82,6 @@ export const findDelimiter = (
|
|||
return delimiter || defaultDelimiter
|
||||
}
|
||||
|
||||
// FIXME: Remove this Date stub after nullable types will be fixed
|
||||
export const userDataToUser = (
|
||||
user: Merge<
|
||||
User,
|
||||
{
|
||||
isFriend?: boolean
|
||||
followersCount?: number
|
||||
friendsCount?: number
|
||||
sharedArticlesCount?: number
|
||||
sharedHighlightsCount?: number
|
||||
sharedNotesCount?: number
|
||||
viewerIsFollowing?: boolean
|
||||
}
|
||||
>
|
||||
): {
|
||||
id: string
|
||||
name: string
|
||||
source: RegistrationType
|
||||
email?: string | null
|
||||
phone?: string | null
|
||||
picture?: string | null
|
||||
googleId?: string | null
|
||||
createdAt: Date
|
||||
isFriend?: boolean | null
|
||||
isFullUser: boolean
|
||||
viewerIsFollowing?: boolean | null
|
||||
sourceUserId: string
|
||||
friendsCount?: number
|
||||
followersCount?: number
|
||||
sharedArticles: FeedArticle[]
|
||||
sharedArticlesCount?: number
|
||||
sharedHighlightsCount?: number
|
||||
sharedNotesCount?: number
|
||||
profile: Profile
|
||||
} => ({
|
||||
...user,
|
||||
source: user.source as RegistrationType,
|
||||
createdAt: user.createdAt,
|
||||
friendsCount: user.friendsCount || 0,
|
||||
followersCount: user.followersCount || 0,
|
||||
isFullUser: true,
|
||||
viewerIsFollowing: user.viewerIsFollowing || user.isFriend || false,
|
||||
picture: user.profile.pictureUrl,
|
||||
sharedArticles: [],
|
||||
sharedArticlesCount: user.sharedArticlesCount || 0,
|
||||
sharedHighlightsCount: user.sharedHighlightsCount || 0,
|
||||
sharedNotesCount: user.sharedNotesCount || 0,
|
||||
})
|
||||
|
||||
export const generateSlug = (title: string): string => {
|
||||
return slugify(title).substring(0, 64) + '-' + Date.now().toString(16)
|
||||
}
|
||||
|
|
@ -161,7 +93,7 @@ export const errorHandler = async (
|
|||
userId: string,
|
||||
pageId?: string | null,
|
||||
pubsub = createPubSubClient()
|
||||
): Promise<CreateArticleError | CreateArticleSuccess> => {
|
||||
): Promise<CreateArticleError> => {
|
||||
if (!pageId) return result
|
||||
|
||||
await updateLibraryItem(
|
||||
|
|
@ -176,86 +108,6 @@ export const errorHandler = async (
|
|||
return result
|
||||
}
|
||||
|
||||
export const highlightDataToHighlight = (
|
||||
highlight: HighlightData
|
||||
): Highlight => ({
|
||||
...highlight,
|
||||
createdByMe: false,
|
||||
reactions: [],
|
||||
replies: [],
|
||||
type: highlight.highlightType,
|
||||
user: userDataToUser(highlight.user),
|
||||
})
|
||||
|
||||
export const recommandationDataToRecommendation = (
|
||||
recommendation: RecommendationData
|
||||
): Recommendation => ({
|
||||
...recommendation,
|
||||
user: {
|
||||
userId: recommendation.recommender.id,
|
||||
username: recommendation.recommender.profile.username,
|
||||
profileImageURL: recommendation.recommender.profile.pictureUrl,
|
||||
name: recommendation.recommender.name,
|
||||
},
|
||||
name: recommendation.group.name,
|
||||
recommendedAt: recommendation.createdAt,
|
||||
})
|
||||
|
||||
export const libraryItemToArticleSavingRequest = (
|
||||
user: User,
|
||||
item: LibraryItem
|
||||
): ArticleSavingRequest => ({
|
||||
...item,
|
||||
user: userDataToUser(user),
|
||||
status: item.state as unknown as ArticleSavingRequestStatus,
|
||||
url: item.originalUrl,
|
||||
userId: user.id,
|
||||
})
|
||||
|
||||
export const libraryItemToArticle = (item: LibraryItem): Article => ({
|
||||
...item,
|
||||
url: item.originalUrl,
|
||||
state: item.state as unknown as ArticleSavingRequestStatus,
|
||||
content: item.readableContent,
|
||||
hash: item.textContentHash || '',
|
||||
isArchived: !!item.archivedAt,
|
||||
recommendations: item.recommendations?.map(
|
||||
recommandationDataToRecommendation
|
||||
),
|
||||
image: item.thumbnail,
|
||||
contentReader: item.contentReader as unknown as ContentReader,
|
||||
readingProgressAnchorIndex: item.readingProgressHighestReadAnchor,
|
||||
readingProgressPercent: item.readingProgressBottomPercent,
|
||||
highlights: item.highlights?.map(highlightDataToHighlight) || [],
|
||||
uploadFileId: item.uploadFile?.id,
|
||||
pageType: item.itemType as unknown as PageType,
|
||||
wordsCount: item.wordCount,
|
||||
directionality: item.directionality as unknown as DirectionalityType,
|
||||
})
|
||||
|
||||
export const libraryItemToSearchItem = (
|
||||
item: LibraryItem,
|
||||
format?: ArticleFormat
|
||||
): SearchItem => ({
|
||||
...item,
|
||||
url: item.originalUrl,
|
||||
state: item.state as unknown as ArticleSavingRequestStatus,
|
||||
content: item.readableContent,
|
||||
isArchived: !!item.archivedAt,
|
||||
pageType: item.itemType as unknown as PageType,
|
||||
readingProgressPercent: item.readingProgressBottomPercent,
|
||||
contentReader: item.contentReader as unknown as ContentReader,
|
||||
readingProgressAnchorIndex: item.readingProgressHighestReadAnchor,
|
||||
recommendations: item.recommendations?.map(
|
||||
recommandationDataToRecommendation
|
||||
),
|
||||
image: item.thumbnail,
|
||||
highlights: item.highlights?.map(highlightDataToHighlight),
|
||||
wordsCount: item.wordCount,
|
||||
directionality: item.directionality as unknown as DirectionalityType,
|
||||
format,
|
||||
})
|
||||
|
||||
export const isParsingTimeout = (libraryItem: LibraryItem): boolean => {
|
||||
return (
|
||||
// item processed more than 30 seconds ago
|
||||
|
|
|
|||
|
|
@ -3,13 +3,20 @@ import * as chai from 'chai'
|
|||
import { expect } from 'chai'
|
||||
import chaiString from 'chai-string'
|
||||
import 'mocha'
|
||||
import { Highlight } from '../../src/entity/highlight'
|
||||
import { User } from '../../src/entity/user'
|
||||
import { HighlightEdge } from '../../src/generated/graphql'
|
||||
import {
|
||||
createHighlight,
|
||||
deleteHighlightById,
|
||||
deleteHighlightsByIds,
|
||||
findHighlightById,
|
||||
} from '../../src/services/highlights'
|
||||
import { createLabel, saveLabelsInHighlight } from '../../src/services/labels'
|
||||
import {
|
||||
createLabel,
|
||||
deleteLabels,
|
||||
saveLabelsInHighlight,
|
||||
} from '../../src/services/labels'
|
||||
import { deleteUser } from '../../src/services/user'
|
||||
import { createTestLibraryItem, createTestUser } from '../db'
|
||||
import {
|
||||
|
|
@ -165,8 +172,14 @@ describe('Highlights API', () => {
|
|||
})
|
||||
|
||||
context('createHighlightMutation', () => {
|
||||
let highlightId: string
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteHighlightById(highlightId, user.id)
|
||||
})
|
||||
|
||||
it('does not fail', async () => {
|
||||
const highlightId = generateFakeUuid()
|
||||
highlightId = generateFakeUuid()
|
||||
const shortHighlightId = '_short_id'
|
||||
const highlightPositionPercent = 35.0
|
||||
const highlightPositionAnchorIndex = 15
|
||||
|
|
@ -194,31 +207,29 @@ describe('Highlights API', () => {
|
|||
|
||||
context('when highlight position is null', () => {
|
||||
it('sets highlight position = 0', async () => {
|
||||
const newHighlightId = generateFakeUuid()
|
||||
highlightId = generateFakeUuid()
|
||||
const newShortHighlightId = '_short_id_5'
|
||||
const query = createHighlightQuery(
|
||||
itemId,
|
||||
newHighlightId,
|
||||
highlightId,
|
||||
newShortHighlightId
|
||||
)
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
expect(
|
||||
res.body.data.createHighlight.highlight.highlightPositionPercent
|
||||
).to.eq(0)
|
||||
|
||||
await deleteHighlightById(newHighlightId)
|
||||
})
|
||||
})
|
||||
|
||||
context('when the annotation has HTML reserved characters', () => {
|
||||
it('unescapes the annotation and creates', async () => {
|
||||
const newHighlightId = generateFakeUuid()
|
||||
highlightId = generateFakeUuid()
|
||||
const newShortHighlightId = '_short_id_4'
|
||||
const highlightPositionPercent = 50.0
|
||||
const highlightPositionAnchorIndex = 25
|
||||
const query = createHighlightQuery(
|
||||
itemId,
|
||||
newHighlightId,
|
||||
highlightId,
|
||||
newShortHighlightId,
|
||||
highlightPositionPercent,
|
||||
highlightPositionAnchorIndex,
|
||||
|
|
@ -244,7 +255,7 @@ describe('Highlights API', () => {
|
|||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteHighlightById(highlightId)
|
||||
await deleteHighlightById(highlightId, user.id)
|
||||
})
|
||||
|
||||
it('should not fail', async () => {
|
||||
|
|
@ -318,6 +329,10 @@ describe('Highlights API', () => {
|
|||
highlightId = highlight.id
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deleteHighlightById(highlightId, user.id)
|
||||
})
|
||||
|
||||
it('updates the quote when the quote is in HTML format when the annotation has HTML reserved characters', async () => {
|
||||
const quote = '> This is a test'
|
||||
const query = updateHighlightQuery({ highlightId, quote })
|
||||
|
|
@ -344,4 +359,121 @@ describe('Highlights API', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Get highlights API', () => {
|
||||
const query = `
|
||||
query Highlights ($first: Int, $after: String, $query: String) {
|
||||
highlights (first: $first, after: $after, query: $query) {
|
||||
... on HighlightsSuccess {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
user {
|
||||
id
|
||||
name
|
||||
}
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
libraryItem {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
... on HighlightsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
let existingHighlights: Highlight[]
|
||||
|
||||
before(async () => {
|
||||
// create test library item
|
||||
const item = await createTestLibraryItem(user.id)
|
||||
|
||||
// create test highlights
|
||||
const highlight1 = await createHighlight(
|
||||
{
|
||||
libraryItem: { id: item.id },
|
||||
shortId: generateFakeShortId(),
|
||||
user: { id: user.id },
|
||||
},
|
||||
itemId,
|
||||
user.id
|
||||
)
|
||||
const highlight2 = await createHighlight(
|
||||
{
|
||||
libraryItem: { id: item.id },
|
||||
shortId: generateFakeShortId(),
|
||||
user: { id: user.id },
|
||||
},
|
||||
itemId,
|
||||
user.id
|
||||
)
|
||||
existingHighlights = [highlight1, highlight2]
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deleteHighlightsByIds(
|
||||
user.id,
|
||||
existingHighlights.map((h) => h.id)
|
||||
)
|
||||
})
|
||||
|
||||
it('returns highlights in descending order', async () => {
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
const highlights = res.body.data.highlights.edges as Array<HighlightEdge>
|
||||
expect(highlights).to.have.lengthOf(existingHighlights.length)
|
||||
expect(highlights[0].node.id).to.eq(existingHighlights[1].id)
|
||||
expect(highlights[1].node.id).to.eq(existingHighlights[0].id)
|
||||
expect(highlights[0].node.user.id).to.eq(user.id)
|
||||
expect(highlights[1].node.libraryItem.id).to.eq(
|
||||
existingHighlights[0].libraryItemId
|
||||
)
|
||||
})
|
||||
|
||||
it('returns highlights with pagination', async () => {
|
||||
const res = await graphqlRequest(query, authToken, {
|
||||
first: 1,
|
||||
}).expect(200)
|
||||
|
||||
const highlights = res.body.data.highlights.edges as Array<HighlightEdge>
|
||||
expect(highlights).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('returns highlights with labels', async () => {
|
||||
// create labels
|
||||
const labelName = 'test_label'
|
||||
const label = await createLabel(labelName, '#ff0000', user.id)
|
||||
const labelName1 = 'test_label_1'
|
||||
const label1 = await createLabel(labelName1, '#ff0001', user.id)
|
||||
|
||||
// save labels in highlights
|
||||
await saveLabelsInHighlight(
|
||||
[label, label1],
|
||||
existingHighlights[0].id,
|
||||
user.id
|
||||
)
|
||||
|
||||
const res = await graphqlRequest(query, authToken, {
|
||||
query: `label:"${labelName}" label:"${labelName1}"`,
|
||||
}).expect(200)
|
||||
const highlights = res.body.data.highlights.edges as Array<HighlightEdge>
|
||||
expect(highlights).to.have.lengthOf(1)
|
||||
expect(highlights[0].node.labels?.[0].name).to.eq(labelName)
|
||||
expect(highlights[0].node.labels?.[1].name).to.eq(labelName1)
|
||||
|
||||
await deleteLabels([label.id, label1.id], user.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { StatusType } from '../../src/entity/user'
|
|||
import {
|
||||
createUsers,
|
||||
deleteUsers,
|
||||
findUsersById,
|
||||
findUsersByIds,
|
||||
} from '../../src/services/user'
|
||||
import { request } from '../util'
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ describe('User Service Router', () => {
|
|||
.send(data)
|
||||
.expect(200)
|
||||
|
||||
const deletedUsers = await findUsersById(toDeleteUserIds)
|
||||
const deletedUsers = await findUsersByIds(toDeleteUserIds)
|
||||
expect(deletedUsers.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
5
packages/db/migrations/0178.do.add_index_on_highlight_user_id.sql
Executable file
5
packages/db/migrations/0178.do.add_index_on_highlight_user_id.sql
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
-- Type: DO
|
||||
-- Name: add_index_on_highlight_user_id
|
||||
-- Description: Add index on user_id column to the highlight table
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS highlight_user_id_idx ON omnivore.highlight (user_id);
|
||||
9
packages/db/migrations/0178.undo.add_index_on_highlight_user_id.sql
Executable file
9
packages/db/migrations/0178.undo.add_index_on_highlight_user_id.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: UNDO
|
||||
-- Name: add_index_on_highlight_user_id
|
||||
-- Description: Add index on user_id column to the highlight table
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS highlight_user_id_idx;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in a new issue