diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 9b0e4f777..3d2524102 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -791,6 +791,13 @@ export enum FeedsErrorCode { Unauthorized = 'UNAUTHORIZED' } +export type FeedsInput = { + after?: InputMaybe; + first?: InputMaybe; + query: Scalars['String']; + sort?: InputMaybe; +}; + export type FeedsResult = FeedsError | FeedsSuccess; export type FeedsSuccess = { @@ -1801,10 +1808,7 @@ export type QueryArticleSavingRequestArgs = { export type QueryFeedsArgs = { - after?: InputMaybe; - first?: InputMaybe; - query: Scalars['String']; - sort?: InputMaybe; + input: FeedsInput; }; @@ -3515,6 +3519,7 @@ export type ResolversTypes = { FeedEdge: ResolverTypeWrapper; FeedsError: ResolverTypeWrapper; FeedsErrorCode: FeedsErrorCode; + FeedsInput: FeedsInput; FeedsResult: ResolversTypes['FeedsError'] | ResolversTypes['FeedsSuccess']; FeedsSuccess: ResolverTypeWrapper; Filter: ResolverTypeWrapper; @@ -3999,6 +4004,7 @@ export type ResolversParentTypes = { FeedArticlesSuccess: FeedArticlesSuccess; FeedEdge: FeedEdge; FeedsError: FeedsError; + FeedsInput: FeedsInput; FeedsResult: ResolversParentTypes['FeedsError'] | ResolversParentTypes['FeedsSuccess']; FeedsSuccess: FeedsSuccess; Filter: Filter; @@ -5357,7 +5363,7 @@ export type QueryResolvers>; articleSavingRequest?: Resolver>; deviceTokens?: Resolver; - feeds?: Resolver>; + feeds?: Resolver>; filters?: Resolver; getUserPersonalization?: Resolver; groups?: Resolver; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 8455bd80c..719c59e02 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -700,6 +700,13 @@ enum FeedsErrorCode { UNAUTHORIZED } +input FeedsInput { + after: String + first: Int + query: String! + sort: SortParams +} + union FeedsResult = FeedsError | FeedsSuccess type FeedsSuccess { @@ -1327,7 +1334,7 @@ type Query { article(format: String, slug: String!, username: String!): ArticleResult! articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult! deviceTokens: DeviceTokensResult! - feeds(after: String, first: Int, query: String!, sort: SortParams): FeedsResult! + feeds(input: FeedsInput!): FeedsResult! filters: FiltersResult! getUserPersonalization: GetUserPersonalizationResult! groups: GroupsResult! diff --git a/packages/api/src/repository/feed.ts b/packages/api/src/repository/feed.ts new file mode 100644 index 000000000..99d242de8 --- /dev/null +++ b/packages/api/src/repository/feed.ts @@ -0,0 +1,29 @@ +import { ILike } from 'typeorm' +import { appDataSource } from '../data_source' +import { Feed } from '../entity/feed' + +export const feedRepository = appDataSource.getRepository(Feed).extend({ + async searchFeeds( + query: string, + take = 10, + skip = 0, + orderBy = 'createdAt', + order = 'DESC' + ) { + query = `%${query}%` + const where = [{ title: ILike(query) }, { url: ILike(query) }] + const feeds = await this.find({ + where, + order: { [orderBy]: order }, + take, + skip, + }) + + const count = await this.countBy(where) + + return { + feeds, + count, + } + }, +}) diff --git a/packages/api/src/resolvers/feeds/index.ts b/packages/api/src/resolvers/feeds/index.ts new file mode 100644 index 000000000..f3ef53b51 --- /dev/null +++ b/packages/api/src/resolvers/feeds/index.ts @@ -0,0 +1,61 @@ +import { + FeedEdge, + FeedsError, + FeedsErrorCode, + FeedsSuccess, + QueryFeedsArgs, +} from '../../generated/graphql' +import { feedRepository } from '../../repository/feed' +import { authorized } from '../../utils/helpers' + +export const feedsResolve = authorized< + FeedsSuccess, + FeedsError, + QueryFeedsArgs +>(async (_, { input }, { log }) => { + try { + const startCursor = input.after || '' + const start = + startCursor && !isNaN(Number(startCursor)) ? Number(startCursor) : 0 + const first = input.first || 10 + + const { feeds, count } = await feedRepository.searchFeeds( + input.query, + first + 1, // fetch one extra to check if there is a next page + start, + input.sort?.by, + input.sort?.order || undefined + ) + + const hasNextPage = feeds.length > first + const endCursor = String(start + feeds.length - (hasNextPage ? 1 : 0)) + + if (hasNextPage) { + // remove an extra if exists + feeds.pop() + } + + const edges: FeedEdge[] = feeds.map((feed) => ({ + node: feed, + cursor: endCursor, + })) + + return { + __typename: 'FeedsSuccess', + edges, + pageInfo: { + hasPreviousPage: start > 0, + hasNextPage, + startCursor, + endCursor, + totalCount: count, + }, + } + } catch (error) { + log.error('Error fetching feeds', error) + + return { + errorCodes: [FeedsErrorCode.BadRequest], + } + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 24ef1ae9a..bd1b2df8f 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2605,6 +2605,13 @@ const schema = gql` email: String! } + input FeedsInput { + after: String + first: Int @sanitize(minLength: 1, maxLength: 100) + query: String! @sanitize(maxLength: 255) + sort: SortParams + } + union FeedsResult = FeedsSuccess | FeedsError type FeedsSuccess { @@ -2799,12 +2806,7 @@ const schema = gql` filters: FiltersResult! groups: GroupsResult! recentEmails: RecentEmailsResult! - feeds( - after: String - first: Int - query: String! - sort: SortParams - ): FeedsResult! + feeds(input: FeedsInput!): FeedsResult! } ` diff --git a/packages/api/src/services/feeds.ts b/packages/api/src/services/feeds.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/db/migrations/0144.do.feed.sql b/packages/db/migrations/0145.do.feed.sql similarity index 100% rename from packages/db/migrations/0144.do.feed.sql rename to packages/db/migrations/0145.do.feed.sql diff --git a/packages/db/migrations/0144.undo.feed.sql b/packages/db/migrations/0145.undo.feed.sql similarity index 100% rename from packages/db/migrations/0144.undo.feed.sql rename to packages/db/migrations/0145.undo.feed.sql