Skip to content

Commit b4ddf47

Browse files
authored
fix: add legacy fallback for feed V2 (#3803)
1 parent 2a5f0bf commit b4ddf47

3 files changed

Lines changed: 221 additions & 47 deletions

File tree

__tests__/feeds.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,75 @@ describe('query feedV2', () => {
16051605
},
16061606
]);
16071607
});
1608+
1609+
it('should fall back to the local feed resolver for legacy versions', async () => {
1610+
loggedUser = '1';
1611+
await saveFeedFixtures();
1612+
1613+
const fallbackQuery = `
1614+
query FeedFallback($first: Int, $version: Int) {
1615+
feed(first: $first, version: $version, unreadOnly: false) {
1616+
pageInfo {
1617+
endCursor
1618+
hasNextPage
1619+
}
1620+
edges {
1621+
cursor
1622+
node {
1623+
id
1624+
title
1625+
type
1626+
feedMeta
1627+
}
1628+
}
1629+
}
1630+
feedV2(first: $first, version: $version, unreadOnly: false) {
1631+
pageInfo {
1632+
endCursor
1633+
hasNextPage
1634+
}
1635+
edges {
1636+
cursor
1637+
node {
1638+
__typename
1639+
... on FeedPostItem {
1640+
feedMeta
1641+
post {
1642+
id
1643+
title
1644+
type
1645+
}
1646+
}
1647+
}
1648+
}
1649+
}
1650+
}
1651+
`;
1652+
1653+
const res = await client.query(fallbackQuery, {
1654+
variables: {
1655+
first: 10,
1656+
version: 1,
1657+
},
1658+
});
1659+
1660+
expect(res.errors).toBeFalsy();
1661+
expect(res.data.feedV2.pageInfo).toEqual(res.data.feed.pageInfo);
1662+
expect(res.data.feedV2.edges).toEqual(
1663+
res.data.feed.edges.map((edge) => ({
1664+
cursor: edge.cursor,
1665+
node: {
1666+
__typename: 'FeedPostItem',
1667+
feedMeta: edge.node.feedMeta,
1668+
post: {
1669+
id: edge.node.id,
1670+
title: edge.node.title,
1671+
type: edge.node.type,
1672+
},
1673+
},
1674+
})),
1675+
);
1676+
});
16081677
});
16091678

16101679
describe('query feedByConfig', () => {

src/schema/feedV2.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import graphorm from '../graphorm';
55
import type { AuthContext, BaseContext } from '../Context';
66
import type { GQLPost } from './posts';
77
import type { FeedGenerator, FeedResponse } from '../integrations/feed';
8+
import type { Connection } from 'graphql-relay';
89
import {
910
isFeedResponseHighlightItem,
1011
versionToFeedGenerator,
@@ -171,7 +172,7 @@ const supportsHighlights = ({
171172
}: Pick<FeedV2Args, 'supportedTypes' | 'highlightsLimit'>): boolean =>
172173
!!supportedTypes?.includes('highlight') && !!highlightsLimit;
173174

174-
const getAllowedPostTypes = (
175+
export const getFeedV2AllowedPostTypes = (
175176
supportedTypes: FeedV2Args['supportedTypes'],
176177
): string[] | undefined =>
177178
supportedTypes?.filter((type) => type !== 'highlight');
@@ -192,7 +193,7 @@ const getResolveTreeChild = (
192193
);
193194
};
194195

195-
const getFeedV2FieldTree = (
196+
export const getFeedV2FieldTree = (
196197
info: GraphQLResolveInfo,
197198
typeName: 'FeedPostItem' | 'FeedHighlightsItem',
198199
fieldName: 'post' | 'highlights',
@@ -248,6 +249,32 @@ const toFeedV2Items = ({
248249
return acc;
249250
}, []);
250251

252+
export const toFeedV2PostConnection = (
253+
connection: Connection<GQLPost>,
254+
): Connection<GQLFeedItem> => ({
255+
pageInfo: connection.pageInfo,
256+
edges: connection.edges.map((edge) => ({
257+
cursor: edge.cursor,
258+
node: {
259+
itemType: 'post',
260+
postId: edge.node.id,
261+
post: edge.node,
262+
feedMeta: edge.node.feedMeta ?? null,
263+
},
264+
})),
265+
});
266+
267+
export const emptyFeedV2Connection = ({
268+
after,
269+
}: Pick<FeedV2Args, 'after'>): Connection<GQLFeedItem> =>
270+
graphorm.nodesToConnection<GQLFeedItem>(
271+
[],
272+
0,
273+
() => !!after,
274+
() => false,
275+
() => after || '',
276+
);
277+
251278
export const feedV2QueryResolver: IFieldResolver<
252279
unknown,
253280
AuthContext,
@@ -257,7 +284,7 @@ export const feedV2QueryResolver: IFieldResolver<
257284
limit: Math.min(args.first || 30, 50),
258285
cursor: args.after || undefined,
259286
};
260-
const allowedPostTypes = getAllowedPostTypes(args.supportedTypes);
287+
const allowedPostTypes = getFeedV2AllowedPostTypes(args.supportedTypes);
261288
const shouldApplyNoAi = args.noAi || (await isSavedNoAiEnabled(ctx));
262289
const response = await getForYouFeedGenerator({
263290
...args,

src/schema/feeds.ts

Lines changed: 122 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
import { In, Not, SelectQueryBuilder } from 'typeorm';
4242
import { ensureSourcePermissions, GQLSource } from './sources';
4343
import {
44+
connectionFromNodes,
4445
CursorPage,
4546
feedCursorPageGenerator,
4647
GQLEmptyResponse,
@@ -87,12 +88,16 @@ import { SourceMemberRoles } from '../roles';
8788
import { ContentPreferenceKeyword } from '../entity/contentPreference/ContentPreferenceKeyword';
8889
import { briefingPostIdsMaxItems } from '../common/brief';
8990
import {
91+
emptyFeedV2Connection,
9092
feedV2QueryResolver,
9193
feedV2Resolvers,
9294
feedV2TypeDefs,
9395
type FeedV2Args,
96+
getFeedV2AllowedPostTypes,
97+
getFeedV2FieldTree,
9498
getForYouFeedGenerator,
9599
isSavedNoAiEnabled,
100+
toFeedV2PostConnection,
96101
} from './feedV2';
97102

98103
interface GQLTagsCategory {
@@ -1163,6 +1168,10 @@ interface FeedPage extends Page {
11631168
score?: number;
11641169
}
11651170

1171+
type FeedVersionArgs = Pick<FeedArgs, 'ranking'> & {
1172+
version: number;
1173+
};
1174+
11661175
const feedPageGenerator: PageGenerator<GQLPost, FeedArgs, FeedPage, unknown> = {
11671176
connArgsToPage: ({ ranking, first, after }: FeedArgs) => {
11681177
const cursor = getCursorFromAfter(after || undefined);
@@ -1219,6 +1228,36 @@ const applyFeedPaging = (
12191228
return newBuilder;
12201229
};
12211230

1231+
const shouldUseFeedGenerator = ({ version }: FeedVersionArgs) => {
1232+
return version >= 2;
1233+
};
1234+
1235+
const getConfiguredFeedId = (
1236+
ctx: Pick<Context, 'userId'>,
1237+
args: Pick<ConfiguredFeedArgs, 'feedId'> | Pick<FeedV2Args, 'feedId'>,
1238+
): string | undefined => args.feedId || ctx.userId;
1239+
1240+
const getConfiguredFeedQueryParams = (
1241+
ctx: Context,
1242+
args: Pick<ConfiguredFeedArgs, 'feedId'> | Pick<FeedV2Args, 'feedId'>,
1243+
) => feedToFilters(ctx.con, getConfiguredFeedId(ctx, args), ctx.userId);
1244+
1245+
const buildConfiguredFeedQuery = (
1246+
ctx: Context,
1247+
args: Pick<ConfiguredFeedArgs, 'feedId' | 'unreadOnly'> | FeedV2Args,
1248+
builder: SelectQueryBuilder<Post>,
1249+
alias: string,
1250+
queryParams: AnonymousFeedFilters | undefined,
1251+
) =>
1252+
configuredFeedBuilder(
1253+
ctx,
1254+
getConfiguredFeedId(ctx, args),
1255+
args.unreadOnly,
1256+
builder,
1257+
alias,
1258+
queryParams,
1259+
);
1260+
12221261
interface UpvotedPage extends Page {
12231262
timestamp?: Date;
12241263
}
@@ -1387,27 +1426,86 @@ const feedResolverV1: IFieldResolver<unknown, Context, ConfiguredFeedArgs> =
13871426
builder,
13881427
alias,
13891428
queryParams: AnonymousFeedFilters | undefined,
1390-
) => {
1391-
const feedId = args.feedId || ctx.userId;
1392-
1393-
return configuredFeedBuilder(
1394-
ctx,
1395-
feedId,
1396-
args.unreadOnly,
1397-
builder,
1398-
alias,
1399-
queryParams,
1400-
);
1401-
},
1429+
) => buildConfiguredFeedQuery(ctx, args, builder, alias, queryParams),
14021430
feedPageGenerator,
14031431
applyFeedPaging,
14041432
{
1405-
fetchQueryParams: async (ctx, args) =>
1406-
feedToFilters(ctx.con, args.feedId || ctx.userId, ctx.userId),
1433+
fetchQueryParams: (ctx, args) => getConfiguredFeedQueryParams(ctx, args),
14071434
allowPrivatePosts: false,
14081435
},
14091436
);
14101437

1438+
const feedResolverV2Local: IFieldResolver<
1439+
unknown,
1440+
AuthContext,
1441+
FeedV2Args
1442+
> = async (source, args, ctx, info) => {
1443+
const postFieldTree = getFeedV2FieldTree(info, 'FeedPostItem', 'post');
1444+
const allowedPostTypes = getFeedV2AllowedPostTypes(args.supportedTypes);
1445+
1446+
if ((args.supportedTypes && !allowedPostTypes?.length) || !postFieldTree) {
1447+
return emptyFeedV2Connection(args);
1448+
}
1449+
1450+
const page = feedPageGenerator.connArgsToPage(args);
1451+
const queryParams = await getConfiguredFeedQueryParams(ctx, args);
1452+
const supportedTypes = allowedPostTypes?.filter((type) => {
1453+
return queryParams.excludeTypes
1454+
? !queryParams.excludeTypes.includes(type)
1455+
: true;
1456+
});
1457+
const sourceTypes = queryParams.excludeSourceTypes?.length
1458+
? baseFeedConfig.source_types?.filter(
1459+
(type) => !queryParams.excludeSourceTypes?.includes(type),
1460+
) || []
1461+
: [];
1462+
1463+
const nodes = await graphorm.queryResolveTree<GQLPost>(
1464+
ctx,
1465+
postFieldTree,
1466+
(builder) => {
1467+
builder.queryBuilder = applyFeedWhere(
1468+
ctx,
1469+
applyFeedPaging(
1470+
ctx,
1471+
args,
1472+
page,
1473+
buildConfiguredFeedQuery(
1474+
ctx,
1475+
args,
1476+
builder.queryBuilder,
1477+
builder.alias,
1478+
queryParams,
1479+
),
1480+
builder.alias,
1481+
),
1482+
builder.alias,
1483+
supportedTypes || ['article'],
1484+
true,
1485+
true,
1486+
false,
1487+
true,
1488+
true,
1489+
sourceTypes,
1490+
);
1491+
return builder;
1492+
},
1493+
true,
1494+
);
1495+
1496+
return toFeedV2PostConnection(
1497+
connectionFromNodes(
1498+
args,
1499+
nodes,
1500+
undefined,
1501+
page,
1502+
feedPageGenerator,
1503+
undefined,
1504+
queryParams,
1505+
),
1506+
);
1507+
};
1508+
14111509
const feedResolverCursor = feedResolver<
14121510
unknown,
14131511
FeedArgs & { generator: FeedGenerator },
@@ -1554,23 +1652,15 @@ const postRepostsFeedResolver = feedResolver(
15541652
export const resolvers: IResolvers<unknown, BaseContext> = {
15551653
Query: {
15561654
anonymousFeed: (source, args: AnonymousFeedArgs, ctx: Context, info) => {
1557-
if (args.version >= 2 && args.ranking === Ranking.POPULARITY) {
1558-
return feedResolverCursor(
1559-
source,
1560-
{
1561-
...(args as FeedArgs),
1562-
generator: feedGenerators['popular']!,
1563-
},
1564-
ctx,
1565-
info,
1566-
);
1567-
}
1568-
if (args.version >= 2 && args.ranking === Ranking.TIME) {
1655+
if (shouldUseFeedGenerator(args)) {
15691656
return feedResolverCursor(
15701657
source,
15711658
{
15721659
...(args as FeedArgs),
1573-
generator: feedGenerators['time']!,
1660+
generator:
1661+
args.ranking === Ranking.TIME
1662+
? feedGenerators['time']!
1663+
: feedGenerators['popular']!,
15741664
},
15751665
ctx,
15761666
info,
@@ -1579,22 +1669,8 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
15791669
return anonymousFeedResolverV1(source, args, ctx, info);
15801670
},
15811671
feed: async (source, args: ConfiguredFeedArgs, ctx: AuthContext, info) => {
1582-
const shouldApplyNoAi = args.noAi || (await isSavedNoAiEnabled(ctx));
1583-
if (args.version >= 2 && args.ranking === Ranking.POPULARITY) {
1584-
return feedResolverCursor(
1585-
source,
1586-
{
1587-
...(args as FeedArgs),
1588-
generator: getForYouFeedGenerator({
1589-
...args,
1590-
noAi: shouldApplyNoAi,
1591-
}),
1592-
},
1593-
ctx,
1594-
info,
1595-
);
1596-
}
1597-
if (args.version >= 2 && args.ranking === Ranking.TIME) {
1672+
if (shouldUseFeedGenerator(args)) {
1673+
const shouldApplyNoAi = args.noAi || (await isSavedNoAiEnabled(ctx));
15981674
return feedResolverCursor(
15991675
source,
16001676
{
@@ -1611,7 +1687,9 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
16111687
return feedResolverV1(source, args, ctx, info);
16121688
},
16131689
feedV2: (source, args: FeedV2Args, ctx: AuthContext, info) =>
1614-
feedV2QueryResolver(source, args, ctx, info),
1690+
shouldUseFeedGenerator(args)
1691+
? feedV2QueryResolver(source, args, ctx, info)
1692+
: feedResolverV2Local(source, args, ctx, info),
16151693
followingFeed: async (source, args: FeedArgs, ctx: Context, info) => {
16161694
return feedResolverCursor(
16171695
source,

0 commit comments

Comments
 (0)