diff --git a/.infra/application.properties b/.infra/application.properties index d527a73dd5..ff16ec7ccd 100644 --- a/.infra/application.properties +++ b/.infra/application.properties @@ -8,7 +8,7 @@ debezium.source.database.user=%database_user% debezium.source.database.password=%database_pass% debezium.source.database.dbname=%database_dbname% debezium.source.database.server.name=api -debezium.source.table.include.list=public.comment,public.user_comment,public.comment_mention,public.source_request,public.post,public.user,public.post_report,public.source_feed,public.settings,public.reputation_event,public.submission,public.user_state,public.notification_v2,public.source_member,public.feature,public.source,public.post_mention,public.content_image,public.comment_report,public.user_post,public.banner,public.post_relation,public.marketing_cta,public.squad_public_request,public.user_streak,public.bookmark,public.bookmark_list,public.user_company,public.source_report,public.user_top_reader,public.source_post_moderation,public.user_report,public.user_transaction,public.content_preference,public.campaign,public.opportunity_match,public.opportunity,public.organization,public.user_candidate_preference,public.user_experience,public.feedback,public.hot_take,public.user_stack,public.quest,public.quest_reward,public.quest_rotation,public.user_quest,public.user_quest_profile +debezium.source.table.include.list=public.comment,public.user_comment,public.comment_mention,public.source_request,public.post,public.user,public.post_report,public.source_feed,public.settings,public.reputation_event,public.submission,public.user_state,public.notification_v2,public.source_member,public.feature,public.source,public.post_mention,public.content_image,public.comment_report,public.user_post,public.banner,public.post_relation,public.marketing_cta,public.squad_public_request,public.user_streak,public.bookmark,public.bookmark_list,public.user_company,public.source_report,public.user_top_reader,public.source_post_moderation,public.user_report,public.user_transaction,public.content_preference,public.campaign,public.opportunity_match,public.opportunity,public.organization,public.user_candidate_preference,public.user_experience,public.feedback,public.hot_take,public.user_stack,public.quest,public.quest_reward,public.quest_rotation,public.user_quest,public.user_quest_profile,public.post_highlight debezium.source.column.exclude.list=public.post.tsv,public.post.placeholder,public.source.flags,public.user_top_reader.image debezium.source.skip.messages.without.change=true debezium.source.plugin.name=pgoutput diff --git a/__tests__/workers/cdc/primary.ts b/__tests__/workers/cdc/primary.ts index 2a546be123..24f9367ded 100644 --- a/__tests__/workers/cdc/primary.ts +++ b/__tests__/workers/cdc/primary.ts @@ -3,6 +3,7 @@ import { CandidateStatus, OpportunityState, OpportunityType, + PostHighlightedMessage, } from '@dailydotdev/schema'; import { Achievement, @@ -36,6 +37,8 @@ import { Post, PostKeyword, PostMention, + PostHighlight, + PostHighlightSignificance, PostRelation, PostRelationType, PostReport, @@ -3209,6 +3212,72 @@ describe('marketing cta', () => { }); }); +describe('post highlight', () => { + type ObjectType = PostHighlight; + + const base: ChangeObject = { + id: 'ph_1', + channel: 'javascript', + postId: 'p1', + highlightedAt: 1_770_000_000_000_000, + headline: 'JavaScript in 2026', + significance: PostHighlightSignificance.Major, + reason: 'Fast ecosystem update', + retiredAt: null, + createdAt: 1_770_000_000_000_000, + updatedAt: 1_770_000_000_000_000, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should publish highlighted event on create', async () => { + await expectSuccessfulBackground( + worker, + mockChangeMessage({ + after: base, + before: null, + op: 'c', + table: 'post_highlight', + }), + ); + + expectTypedEvent( + 'api.v1.post-highlighted', + new PostHighlightedMessage({ + highlightId: base.id, + channel: base.channel, + postId: base.postId, + headline: base.headline, + significance: base.significance, + reason: base.reason ?? undefined, + highlightedAt: base.highlightedAt, + }), + ); + }); + + it('should not publish highlighted event on update', async () => { + const after: ChangeObject = { + ...base, + headline: 'Updated headline', + updatedAt: 1_770_000_100_000_000, + }; + + await expectSuccessfulBackground( + worker, + mockChangeMessage({ + after, + before: base, + op: 'u', + table: 'post_highlight', + }), + ); + + expect(triggerTypedEvent).not.toHaveBeenCalled(); + }); +}); + describe('squad public request', () => { type ObjectType = SquadPublicRequest; const base: ChangeObject = { diff --git a/package.json b/package.json index f5a8562587..c4550c9ea7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@connectrpc/connect-fastify": "^1.6.1", "@connectrpc/connect-node": "^1.6.1", "@dailydotdev/graphql-redis-subscriptions": "^2.4.3", - "@dailydotdev/schema": "0.3.3", + "@dailydotdev/schema": "0.3.6", "@dailydotdev/ts-ioredis-pool": "^1.0.2", "@fastify/cookie": "^11.0.2", "@fastify/cors": "^11.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e47ac08fd7..a9a571ebb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,8 @@ importers: specifier: ^2.4.3 version: 2.4.3(graphql-subscriptions@3.0.0(graphql@16.12.0)) '@dailydotdev/schema': - specifier: 0.3.3 - version: 0.3.3(@bufbuild/protobuf@1.10.0) + specifier: 0.3.6 + version: 0.3.6(@bufbuild/protobuf@1.10.0) '@dailydotdev/ts-ioredis-pool': specifier: ^1.0.2 version: 1.0.2 @@ -879,8 +879,8 @@ packages: peerDependencies: graphql-subscriptions: ^1.0.0 || ^2.0.0 - '@dailydotdev/schema@0.3.3': - resolution: {integrity: sha512-iq2SSJBHedIJlOutHnsMZLbsrBOP4lTErUhpWsrDFWOsj35ZnLOi7rOjaG3UNegU7hiy75wiDG69DeibWH36PQ==} + '@dailydotdev/schema@0.3.6': + resolution: {integrity: sha512-EnWU44LBerccr8LkJmSV0pO6FO0oUuUd/ORpXZPo4fr8qBW/F2mrMI1oKqx3b95AGPOOkVPYzpc259civOHcqw==} peerDependencies: '@bufbuild/protobuf': 1.x @@ -2550,8 +2550,8 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - citty@0.2.1: - resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} @@ -2765,6 +2765,9 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -4184,6 +4187,9 @@ packages: long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5336,8 +5342,8 @@ packages: resolution: {integrity: sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==} engines: {node: '>=12'} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} tldts-core@7.0.19: @@ -6225,7 +6231,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@dailydotdev/schema@0.3.3(@bufbuild/protobuf@1.10.0)': + '@dailydotdev/schema@0.3.6(@bufbuild/protobuf@1.10.0)': dependencies: '@bufbuild/protobuf': 1.10.0 @@ -8223,7 +8229,7 @@ snapshots: dependencies: chokidar: 4.0.3 confbox: 0.2.4 - defu: 6.1.4 + defu: 6.1.7 dotenv: 16.6.1 exsolve: 1.0.8 giget: 2.0.0 @@ -8321,7 +8327,7 @@ snapshots: dependencies: consola: 3.4.2 - citty@0.2.1: {} + citty@0.2.2: {} cjs-module-lexer@1.2.3: {} @@ -8503,6 +8509,8 @@ snapshots: defu@6.1.4: {} + defu@6.1.7: {} + delayed-stream@1.0.0: {} denque@2.1.0: {} @@ -9112,7 +9120,7 @@ snapshots: dependencies: citty: 0.1.6 consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.7 node-fetch-native: 1.6.7 nypm: 0.6.5 pathe: 2.0.3 @@ -10134,6 +10142,8 @@ snapshots: long@5.2.3: {} + long@5.3.2: {} + lru-cache@10.4.3: {} lru-cache@11.2.4: {} @@ -10320,7 +10330,7 @@ snapshots: denque: 2.1.0 generate-function: 2.3.1 iconv-lite: 0.7.2 - long: 5.2.3 + long: 5.3.2 lru.min: 1.1.4 named-placeholders: 1.1.6 seq-queue: 0.0.5 @@ -10411,9 +10421,9 @@ snapshots: nypm@0.6.5: dependencies: - citty: 0.2.1 + citty: 0.2.2 pathe: 2.0.3 - tinyexec: 1.0.2 + tinyexec: 1.1.1 object-hash@3.0.0: {} @@ -10806,7 +10816,7 @@ snapshots: rc9@2.1.2: dependencies: - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 react-dom@19.2.4(react@19.2.4): @@ -11242,7 +11252,7 @@ snapshots: tiny-lru@11.4.5: {} - tinyexec@1.0.2: {} + tinyexec@1.1.1: {} tldts-core@7.0.19: {} diff --git a/src/common/typedPubsub.ts b/src/common/typedPubsub.ts index 0b4a50df42..7bd072b458 100644 --- a/src/common/typedPubsub.ts +++ b/src/common/typedPubsub.ts @@ -36,6 +36,7 @@ import { MatchedCandidate, type OpportunityMessage, type OpportunityPreviewResult, + PostHighlightedMessage, RecruiterAcceptedCandidateMatchMessage, type TransferResponse, type UserBriefingRequest, @@ -189,6 +190,7 @@ export type PubSubSchema = { }; 'skadi.v2.campaign-updated': CampaignUpdateEventArgs; 'api.v1.post-metrics-updated': z.infer; + 'api.v1.post-highlighted': PostHighlightedMessage; 'api.v1.reputation-event': { op: ChangeMessage['payload']['op']; payload: ChangeObject; diff --git a/src/workers/cdc/primary.ts b/src/workers/cdc/primary.ts index 0fcf404815..4bd00cd035 100644 --- a/src/workers/cdc/primary.ts +++ b/src/workers/cdc/primary.ts @@ -1,4 +1,8 @@ -import { OpportunityState, OpportunityType } from '@dailydotdev/schema'; +import { + OpportunityState, + OpportunityType, + PostHighlightedMessage, +} from '@dailydotdev/schema'; import { Alerts, Banner, @@ -44,6 +48,7 @@ import { UserStreak, UserTopReader, Feedback, + PostHighlight, } from '../../entity'; import { BookmarkList } from '../../entity/BookmarkList'; import { HotTake } from '../../entity/user/HotTake'; @@ -2444,6 +2449,33 @@ const onFeedbackChange = async ( } }; +const onPostHighlightChange = async ( + con: DataSource, + logger: FastifyBaseLogger, + data: ChangeMessage, +) => { + if (data.payload.op !== 'c' || !data.payload.after) { + return; + } + + const { id, channel, postId, headline, significance, reason, highlightedAt } = + data.payload.after; + + await triggerTypedEvent( + logger, + 'api.v1.post-highlighted', + new PostHighlightedMessage({ + highlightId: id, + channel, + postId, + headline, + significance, + reason: reason ?? undefined, + highlightedAt, + }), + ); +}; + const onHotTakeChange = async ( con: DataSource, logger: FastifyBaseLogger, @@ -2713,6 +2745,9 @@ const worker: Worker = { case getTableName(con, Feedback): await onFeedbackChange(con, logger, data); break; + case getTableName(con, PostHighlight): + await onPostHighlightChange(con, logger, data); + break; case getTableName(con, HotTake): await onHotTakeChange(con, logger, data); break;