Skip to content

Commit 3cc9e07

Browse files
authored
feat: add highlight-backed channel configurations query (#3791)
## Summary - add a `channelConfigurations` highlights query that returns channel, displayName, and digest metadata - expose digest frequency plus resolved digest source through GraphORM relations - add `displayName` to `ChannelHighlightDefinition` with a backfill migration and cover the query with integration tests ## Testing - NODE_ENV=test npx jest __tests__/highlights.ts --testEnvironment=node --runInBand - pnpm run build - pnpm run lint
1 parent 5e3e8bf commit 3cc9e07

5 files changed

Lines changed: 217 additions & 2 deletions

File tree

__tests__/highlights.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
saveFixtures,
99
} from './helpers';
1010
import createOrGetConnection from '../src/db';
11-
import { ArticlePost, Source } from '../src/entity';
11+
import { ArticlePost } from '../src/entity/posts/ArticlePost';
12+
import { ChannelDigest } from '../src/entity/ChannelDigest';
13+
import { ChannelHighlightDefinition } from '../src/entity/ChannelHighlightDefinition';
14+
import { Source, SourceType } from '../src/entity/Source';
1215
import {
1316
PostHighlight,
1417
PostHighlightSignificance,
@@ -86,9 +89,13 @@ const createTestPosts = async () => {
8689

8790
beforeEach(async () => {
8891
jest.resetAllMocks();
92+
await con.getRepository(ChannelDigest).clear();
93+
await con.getRepository(ChannelHighlightDefinition).clear();
8994
await con.getRepository(PostHighlight).clear();
9095
await con.getRepository(ArticlePost).delete(['h1', 'h2', 'h3', 'h4']);
91-
await con.getRepository(Source).delete(['a', 'b', 'c']);
96+
await con
97+
.getRepository(Source)
98+
.delete(['a', 'b', 'c', 'backend_digest', 'career_digest']);
9299
});
93100

94101
const QUERY = `
@@ -130,6 +137,89 @@ const MAJOR_HEADLINES_QUERY = `
130137
}
131138
`;
132139

140+
const CHANNEL_CONFIGURATIONS_QUERY = `
141+
query ChannelConfigurations {
142+
channelConfigurations {
143+
channel
144+
displayName
145+
digest {
146+
frequency
147+
source {
148+
id
149+
name
150+
handle
151+
}
152+
}
153+
}
154+
}
155+
`;
156+
157+
describe('query channelConfigurations', () => {
158+
it('should return non-disabled highlight channels with digest metadata', async () => {
159+
await con.getRepository(Source).save({
160+
id: 'backend_digest',
161+
name: 'Backend Digest',
162+
image: 'https://example.com/backend.png',
163+
handle: 'backend_digest',
164+
type: SourceType.Machine,
165+
active: true,
166+
private: false,
167+
});
168+
169+
await con.getRepository(ChannelHighlightDefinition).save([
170+
{
171+
channel: 'career',
172+
displayName: 'Career Growth',
173+
mode: 'shadow',
174+
},
175+
{
176+
channel: 'backend',
177+
displayName: 'Backend Engineering',
178+
mode: 'publish',
179+
},
180+
{
181+
channel: 'disabled',
182+
displayName: 'Disabled',
183+
mode: 'disabled',
184+
},
185+
]);
186+
187+
await con.getRepository(ChannelDigest).save({
188+
key: 'backend-digest',
189+
channel: 'backend',
190+
sourceId: 'backend_digest',
191+
targetAudience: 'backend developers',
192+
frequency: 'daily',
193+
includeSentiment: false,
194+
sentimentGroupIds: [],
195+
enabled: true,
196+
});
197+
198+
const res = await client.query(CHANNEL_CONFIGURATIONS_QUERY);
199+
200+
expect(res.errors).toBeFalsy();
201+
expect(res.data.channelConfigurations).toEqual([
202+
{
203+
channel: 'backend',
204+
displayName: 'Backend Engineering',
205+
digest: {
206+
frequency: 'daily',
207+
source: {
208+
id: 'backend_digest',
209+
name: 'Backend Digest',
210+
handle: 'backend_digest',
211+
},
212+
},
213+
},
214+
{
215+
channel: 'career',
216+
displayName: 'Career Growth',
217+
digest: null,
218+
},
219+
]);
220+
});
221+
});
222+
133223
describe('query postHighlights', () => {
134224
it('should return empty array when no highlights exist', async () => {
135225
const res = await client.query(QUERY, {

src/entity/ChannelHighlightDefinition.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export class ChannelHighlightDefinition {
1212
@PrimaryColumn({ type: 'text' })
1313
channel: string;
1414

15+
@Column({ type: 'text', default: '' })
16+
displayName: string;
17+
1518
@Column({ type: 'text', default: 'disabled' })
1619
mode: ChannelHighlightMode;
1720

src/graphorm/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ const existsByUserAndHotTake =
139139
const nullIfNotLoggedIn = <T>(value: T, ctx: Context): T | null =>
140140
ctx.userId ? value : null;
141141

142+
const defaultChannelDisplayName = ({
143+
channel,
144+
displayName,
145+
}: {
146+
channel?: string;
147+
displayName?: string | null;
148+
}): string => displayName?.trim() || channel || '';
149+
142150
export const nullIfNotTeamMember = <
143151
T,
144152
Ctx extends Pick<Context, 'isTeamMember' | 'roles'>,
@@ -1381,6 +1389,45 @@ const obj = new GraphORM({
13811389
},
13821390
},
13831391
},
1392+
ChannelConfiguration: {
1393+
from: 'ChannelHighlightDefinition',
1394+
requiredColumns: ['channel'],
1395+
fields: {
1396+
displayName: {
1397+
transform: (value: string, _, parent) =>
1398+
defaultChannelDisplayName({
1399+
channel: (parent as { channel?: string }).channel,
1400+
displayName: value,
1401+
}),
1402+
},
1403+
digest: {
1404+
relation: {
1405+
isMany: false,
1406+
customRelation: (_, parentAlias, childAlias, qb): QueryBuilder =>
1407+
qb
1408+
.where(`"${childAlias}"."channel" = "${parentAlias}"."channel"`)
1409+
.andWhere(`"${childAlias}"."enabled" = true`)
1410+
.orderBy(`"${childAlias}"."key"`, 'ASC')
1411+
.limit(1),
1412+
},
1413+
},
1414+
},
1415+
},
1416+
ChannelDigestConfiguration: {
1417+
from: 'ChannelDigest',
1418+
fields: {
1419+
source: {
1420+
relation: {
1421+
isMany: false,
1422+
customRelation: (_, parentAlias, childAlias, qb): QueryBuilder =>
1423+
qb
1424+
.where(`"${childAlias}"."id" = "${parentAlias}"."sourceId"`)
1425+
.andWhere(`"${childAlias}"."active" = true`)
1426+
.limit(1),
1427+
},
1428+
},
1429+
},
1430+
},
13841431
UserComment: {
13851432
requiredColumns: ['votedAt', 'awardTransactionId'],
13861433
fields: {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class ChannelHighlightDisplayName1776106185909
4+
implements MigrationInterface
5+
{
6+
name = 'ChannelHighlightDisplayName1776106185909';
7+
8+
public async up(queryRunner: QueryRunner): Promise<void> {
9+
await queryRunner.query(/* sql */ `
10+
ALTER TABLE "channel_highlight_definition"
11+
ADD COLUMN "displayName" text NOT NULL DEFAULT ''
12+
`);
13+
14+
await queryRunner.query(/* sql */ `
15+
UPDATE "channel_highlight_definition"
16+
SET "displayName" = INITCAP(
17+
REPLACE(
18+
REPLACE("channel", '-', ' '),
19+
'_',
20+
' '
21+
)
22+
)
23+
WHERE "displayName" = ''
24+
`);
25+
}
26+
27+
public async down(queryRunner: QueryRunner): Promise<void> {
28+
await queryRunner.query(/* sql */ `
29+
ALTER TABLE "channel_highlight_definition"
30+
DROP COLUMN "displayName"
31+
`);
32+
}
33+
}

src/schema/highlights.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,31 @@ import {
1212
PostHighlight,
1313
PostHighlightSignificance,
1414
} from '../entity/PostHighlight';
15+
import type { GQLSource } from './sources';
16+
17+
type GQLChannelDigestConfiguration = {
18+
frequency: string;
19+
source?: GQLSource | null;
20+
};
21+
22+
type GQLChannelConfiguration = {
23+
channel: string;
24+
displayName: string;
25+
digest?: GQLChannelDigestConfiguration | null;
26+
};
1527

1628
export const typeDefs = /* GraphQL */ `
29+
type ChannelDigestConfiguration {
30+
frequency: String!
31+
source: Source
32+
}
33+
34+
type ChannelConfiguration {
35+
channel: String!
36+
displayName: String!
37+
digest: ChannelDigestConfiguration
38+
}
39+
1740
type PostHighlight {
1841
id: ID!
1942
post: Post!
@@ -35,6 +58,11 @@ export const typeDefs = /* GraphQL */ `
3558
}
3659
3760
extend type Query {
61+
"""
62+
Get highlight-backed channel configuration with digest metadata
63+
"""
64+
channelConfigurations: [ChannelConfiguration!]!
65+
3866
"""
3967
Get highlights for a channel, ordered by recency
4068
"""
@@ -87,6 +115,20 @@ const getDedupedMajorHeadlinesQuery = (
87115

88116
export const resolvers: IResolvers<unknown, BaseContext> = {
89117
Query: {
118+
channelConfigurations: async (_, __, ctx: Context, info) =>
119+
graphorm.query<GQLChannelConfiguration>(
120+
ctx,
121+
info,
122+
(builder) => {
123+
builder.queryBuilder
124+
.where(`"${builder.alias}"."mode" != :disabledMode`, {
125+
disabledMode: 'disabled',
126+
})
127+
.orderBy(`"${builder.alias}"."channel"`, 'ASC');
128+
return builder;
129+
},
130+
true,
131+
),
90132
postHighlights: async (_, args: { channel: string }, ctx: Context, info) =>
91133
graphorm.query(
92134
ctx,

0 commit comments

Comments
 (0)