Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions __tests__/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,75 @@ describe('query feedV2', () => {
},
]);
});

it('should fall back to the local feed resolver for legacy versions', async () => {
loggedUser = '1';
await saveFeedFixtures();

const fallbackQuery = `
query FeedFallback($first: Int, $version: Int) {
feed(first: $first, version: $version, unreadOnly: false) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
title
type
feedMeta
}
}
}
feedV2(first: $first, version: $version, unreadOnly: false) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
__typename
... on FeedPostItem {
feedMeta
post {
id
title
type
}
}
}
}
}
}
`;

const res = await client.query(fallbackQuery, {
variables: {
first: 10,
version: 1,
},
});

expect(res.errors).toBeFalsy();
expect(res.data.feedV2.pageInfo).toEqual(res.data.feed.pageInfo);
expect(res.data.feedV2.edges).toEqual(
res.data.feed.edges.map((edge) => ({
cursor: edge.cursor,
node: {
__typename: 'FeedPostItem',
feedMeta: edge.node.feedMeta,
post: {
id: edge.node.id,
title: edge.node.title,
type: edge.node.type,
},
},
})),
);
});
});

describe('query feedByConfig', () => {
Expand Down
33 changes: 30 additions & 3 deletions src/schema/feedV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import graphorm from '../graphorm';
import type { AuthContext, BaseContext } from '../Context';
import type { GQLPost } from './posts';
import type { FeedGenerator, FeedResponse } from '../integrations/feed';
import type { Connection } from 'graphql-relay';
import {
isFeedResponseHighlightItem,
versionToFeedGenerator,
Expand Down Expand Up @@ -171,7 +172,7 @@ const supportsHighlights = ({
}: Pick<FeedV2Args, 'supportedTypes' | 'highlightsLimit'>): boolean =>
!!supportedTypes?.includes('highlight') && !!highlightsLimit;

const getAllowedPostTypes = (
export const getFeedV2AllowedPostTypes = (
supportedTypes: FeedV2Args['supportedTypes'],
): string[] | undefined =>
supportedTypes?.filter((type) => type !== 'highlight');
Expand All @@ -192,7 +193,7 @@ const getResolveTreeChild = (
);
};

const getFeedV2FieldTree = (
export const getFeedV2FieldTree = (
info: GraphQLResolveInfo,
typeName: 'FeedPostItem' | 'FeedHighlightsItem',
fieldName: 'post' | 'highlights',
Expand Down Expand Up @@ -248,6 +249,32 @@ const toFeedV2Items = ({
return acc;
}, []);

export const toFeedV2PostConnection = (
connection: Connection<GQLPost>,
): Connection<GQLFeedItem> => ({
pageInfo: connection.pageInfo,
edges: connection.edges.map((edge) => ({
cursor: edge.cursor,
node: {
itemType: 'post',
postId: edge.node.id,
post: edge.node,
feedMeta: edge.node.feedMeta ?? null,
},
})),
});

export const emptyFeedV2Connection = ({
after,
}: Pick<FeedV2Args, 'after'>): Connection<GQLFeedItem> =>
graphorm.nodesToConnection<GQLFeedItem>(
[],
0,
() => !!after,
() => false,
() => after || '',
);

export const feedV2QueryResolver: IFieldResolver<
unknown,
AuthContext,
Expand All @@ -257,7 +284,7 @@ export const feedV2QueryResolver: IFieldResolver<
limit: Math.min(args.first || 30, 50),
cursor: args.after || undefined,
};
const allowedPostTypes = getAllowedPostTypes(args.supportedTypes);
const allowedPostTypes = getFeedV2AllowedPostTypes(args.supportedTypes);
const shouldApplyNoAi = args.noAi || (await isSavedNoAiEnabled(ctx));
const response = await getForYouFeedGenerator({
...args,
Expand Down
166 changes: 122 additions & 44 deletions src/schema/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { In, Not, SelectQueryBuilder } from 'typeorm';
import { ensureSourcePermissions, GQLSource } from './sources';
import {
connectionFromNodes,
CursorPage,
feedCursorPageGenerator,
GQLEmptyResponse,
Expand Down Expand Up @@ -87,12 +88,16 @@ import { SourceMemberRoles } from '../roles';
import { ContentPreferenceKeyword } from '../entity/contentPreference/ContentPreferenceKeyword';
import { briefingPostIdsMaxItems } from '../common/brief';
import {
emptyFeedV2Connection,
feedV2QueryResolver,
feedV2Resolvers,
feedV2TypeDefs,
type FeedV2Args,
getFeedV2AllowedPostTypes,
getFeedV2FieldTree,
getForYouFeedGenerator,
isSavedNoAiEnabled,
toFeedV2PostConnection,
} from './feedV2';

interface GQLTagsCategory {
Expand Down Expand Up @@ -1163,6 +1168,10 @@ interface FeedPage extends Page {
score?: number;
}

type FeedVersionArgs = Pick<FeedArgs, 'ranking'> & {
version: number;
};

const feedPageGenerator: PageGenerator<GQLPost, FeedArgs, FeedPage, unknown> = {
connArgsToPage: ({ ranking, first, after }: FeedArgs) => {
const cursor = getCursorFromAfter(after || undefined);
Expand Down Expand Up @@ -1219,6 +1228,36 @@ const applyFeedPaging = (
return newBuilder;
};

const shouldUseFeedGenerator = ({ version }: FeedVersionArgs) => {
return version >= 2;
};

const getConfiguredFeedId = (
ctx: Pick<Context, 'userId'>,
args: Pick<ConfiguredFeedArgs, 'feedId'> | Pick<FeedV2Args, 'feedId'>,
): string | undefined => args.feedId || ctx.userId;

const getConfiguredFeedQueryParams = (
ctx: Context,
args: Pick<ConfiguredFeedArgs, 'feedId'> | Pick<FeedV2Args, 'feedId'>,
) => feedToFilters(ctx.con, getConfiguredFeedId(ctx, args), ctx.userId);

const buildConfiguredFeedQuery = (
ctx: Context,
args: Pick<ConfiguredFeedArgs, 'feedId' | 'unreadOnly'> | FeedV2Args,
builder: SelectQueryBuilder<Post>,
alias: string,
queryParams: AnonymousFeedFilters | undefined,
) =>
configuredFeedBuilder(
ctx,
getConfiguredFeedId(ctx, args),
args.unreadOnly,
builder,
alias,
queryParams,
);

interface UpvotedPage extends Page {
timestamp?: Date;
}
Expand Down Expand Up @@ -1387,27 +1426,86 @@ const feedResolverV1: IFieldResolver<unknown, Context, ConfiguredFeedArgs> =
builder,
alias,
queryParams: AnonymousFeedFilters | undefined,
) => {
const feedId = args.feedId || ctx.userId;

return configuredFeedBuilder(
ctx,
feedId,
args.unreadOnly,
builder,
alias,
queryParams,
);
},
) => buildConfiguredFeedQuery(ctx, args, builder, alias, queryParams),
feedPageGenerator,
applyFeedPaging,
{
fetchQueryParams: async (ctx, args) =>
feedToFilters(ctx.con, args.feedId || ctx.userId, ctx.userId),
fetchQueryParams: (ctx, args) => getConfiguredFeedQueryParams(ctx, args),
allowPrivatePosts: false,
},
);

const feedResolverV2Local: IFieldResolver<
unknown,
AuthContext,
FeedV2Args
> = async (source, args, ctx, info) => {
const postFieldTree = getFeedV2FieldTree(info, 'FeedPostItem', 'post');
const allowedPostTypes = getFeedV2AllowedPostTypes(args.supportedTypes);

if ((args.supportedTypes && !allowedPostTypes?.length) || !postFieldTree) {
return emptyFeedV2Connection(args);
}

const page = feedPageGenerator.connArgsToPage(args);
const queryParams = await getConfiguredFeedQueryParams(ctx, args);
const supportedTypes = allowedPostTypes?.filter((type) => {
return queryParams.excludeTypes
? !queryParams.excludeTypes.includes(type)
: true;
});
const sourceTypes = queryParams.excludeSourceTypes?.length
? baseFeedConfig.source_types?.filter(
(type) => !queryParams.excludeSourceTypes?.includes(type),
) || []
: [];

const nodes = await graphorm.queryResolveTree<GQLPost>(
ctx,
postFieldTree,
(builder) => {
builder.queryBuilder = applyFeedWhere(
ctx,
applyFeedPaging(
ctx,
args,
page,
buildConfiguredFeedQuery(
ctx,
args,
builder.queryBuilder,
builder.alias,
queryParams,
),
builder.alias,
),
builder.alias,
supportedTypes || ['article'],
true,
true,
false,
true,
true,
sourceTypes,
);
return builder;
},
true,
);

return toFeedV2PostConnection(
connectionFromNodes(
args,
nodes,
undefined,
page,
feedPageGenerator,
undefined,
queryParams,
),
);
};

const feedResolverCursor = feedResolver<
unknown,
FeedArgs & { generator: FeedGenerator },
Expand Down Expand Up @@ -1554,23 +1652,15 @@ const postRepostsFeedResolver = feedResolver(
export const resolvers: IResolvers<unknown, BaseContext> = {
Query: {
anonymousFeed: (source, args: AnonymousFeedArgs, ctx: Context, info) => {
if (args.version >= 2 && args.ranking === Ranking.POPULARITY) {
return feedResolverCursor(
source,
{
...(args as FeedArgs),
generator: feedGenerators['popular']!,
},
ctx,
info,
);
}
if (args.version >= 2 && args.ranking === Ranking.TIME) {
if (shouldUseFeedGenerator(args)) {
return feedResolverCursor(
source,
{
...(args as FeedArgs),
generator: feedGenerators['time']!,
generator:
args.ranking === Ranking.TIME
? feedGenerators['time']!
: feedGenerators['popular']!,
},
ctx,
info,
Expand All @@ -1579,22 +1669,8 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
return anonymousFeedResolverV1(source, args, ctx, info);
},
feed: async (source, args: ConfiguredFeedArgs, ctx: AuthContext, info) => {
const shouldApplyNoAi = args.noAi || (await isSavedNoAiEnabled(ctx));
if (args.version >= 2 && args.ranking === Ranking.POPULARITY) {
return feedResolverCursor(
source,
{
...(args as FeedArgs),
generator: getForYouFeedGenerator({
...args,
noAi: shouldApplyNoAi,
}),
},
ctx,
info,
);
}
if (args.version >= 2 && args.ranking === Ranking.TIME) {
if (shouldUseFeedGenerator(args)) {
const shouldApplyNoAi = args.noAi || (await isSavedNoAiEnabled(ctx));
return feedResolverCursor(
source,
{
Expand All @@ -1611,7 +1687,9 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
return feedResolverV1(source, args, ctx, info);
},
feedV2: (source, args: FeedV2Args, ctx: AuthContext, info) =>
feedV2QueryResolver(source, args, ctx, info),
shouldUseFeedGenerator(args)
? feedV2QueryResolver(source, args, ctx, info)
: feedResolverV2Local(source, args, ctx, info),
followingFeed: async (source, args: FeedArgs, ctx: Context, info) => {
return feedResolverCursor(
source,
Expand Down
Loading