├── .nvmrc ├── CLAUDE.md ├── .infra ├── .nvmrc ├── .npmrc ├── .gitignore ├── Pulumi.yaml ├── tsconfig.json └── package.json ├── geoip └── .gitignore ├── .npmrc ├── clickhouse └── migrations │ ├── .gitkeep │ ├── 20251104112214_post_analytics_events_mv.down.sql │ ├── 20251104173102_post_analytics_events_mv_revert.down.sql │ ├── 1757338344011_post_analytics_events_ads_mv.down.sql │ ├── 1756316678212_post_analytics.down.sql │ ├── 1757334086390_post_analytics_ads.down.sql │ └── 1757334086390_post_analytics_ads.up.sql ├── .github └── CODEOWNERS ├── src ├── integrations │ ├── index.ts │ ├── anthropic │ │ ├── index.ts │ │ └── types.ts │ ├── automation │ │ ├── index.ts │ │ ├── types.ts │ │ └── automations.ts │ ├── lofn │ │ ├── index.ts │ │ └── types.ts │ ├── mimir │ │ ├── index.ts │ │ └── types.ts │ ├── skadi │ │ ├── api │ │ │ ├── v2 │ │ │ │ └── index.ts │ │ │ └── common.ts │ │ ├── index.ts │ │ └── types.ts │ ├── freyja │ │ ├── index.ts │ │ └── types.ts │ ├── snotra │ │ ├── index.ts │ │ └── types.ts │ ├── meilisearch.ts │ └── feed │ │ └── index.ts ├── telemetry │ └── index.ts ├── entity │ ├── opportunities │ │ ├── user │ │ │ ├── index.ts │ │ │ └── OpportunityUserRecruiter.ts │ │ └── types.ts │ ├── campaign │ │ ├── index.ts │ │ ├── CampaignSource.ts │ │ └── CampaignPost.ts │ ├── questions │ │ ├── types.ts │ │ └── QuestionCandidatePreference.ts │ ├── Checkpoint.ts │ ├── posts │ │ ├── WelcomePost.ts │ │ ├── index.ts │ │ ├── SharePost.ts │ │ ├── PollPost.ts │ │ ├── BriefPost.ts │ │ └── PostQuestion.ts │ ├── user │ │ ├── experiences │ │ │ ├── types.ts │ │ │ ├── UserExperienceVolunteering.ts │ │ │ ├── UserExperienceProject.ts │ │ │ ├── UserExperienceEducation.ts │ │ │ ├── UserExperienceOpenSource.ts │ │ │ ├── UserExperienceCertification.ts │ │ │ └── UserExperienceWork.ts │ │ ├── DeletedUser.ts │ │ ├── index.ts │ │ ├── UserState.ts │ │ └── UserCandidateKeyword.ts │ ├── TagSegment.ts │ ├── contentPreference │ │ ├── ContentPreferenceWord.ts │ │ ├── types.ts │ │ ├── ContentPreferenceKeyword.ts │ │ └── ContentPreferenceUser.ts │ ├── ChMigration.ts │ ├── notifications │ │ ├── index.ts │ │ ├── NotificationPreferencePost.ts │ │ ├── NotificationPreferenceSource.ts │ │ ├── NotificationPreferenceComment.ts │ │ ├── NotificationPreferenceUser.ts │ │ └── NotificationAttachmentV2.ts │ ├── PostTag.ts │ ├── Banner.ts │ ├── Category.ts │ ├── SourceFeed.ts │ ├── ExperimentVariant.ts │ ├── PopularSource.ts │ ├── TrendingSource.ts │ ├── PopularTag.ts │ ├── TrendingTag.ts │ ├── DisallowHandle.ts │ ├── ActiveView.ts │ ├── FeedSource.ts │ ├── ContentImage.ts │ ├── PopularVideoSource.ts │ ├── AdvancedSettings.ts │ ├── CommentReport.ts │ └── FeedTag.ts ├── workers │ ├── generators │ │ ├── index.ts │ │ └── generateNewImagesHandler.ts │ ├── notifications │ │ ├── worker.ts │ │ ├── articleNewCommentPostCommented.ts │ │ ├── articleNewCommentCommentCommented.ts │ │ ├── communityPicksGranted.ts │ │ └── communityPicksFailed.ts │ ├── postEditedFreeformImages.ts │ ├── userReadmeImages.ts │ ├── userDeletedCio.ts │ ├── digestDeadLetterLog.ts │ ├── transactionBalanceLog.ts │ ├── bannerDeleted.ts │ ├── commentEditedImages.ts │ └── postFreeformImages.ts ├── common │ ├── storekit.ts │ ├── paddle │ │ └── recruiter │ │ │ └── types.ts │ ├── healthCheck.ts │ ├── schema │ │ ├── reminders.ts │ │ ├── campaigns.ts │ │ ├── topics.ts │ │ └── polls.ts │ ├── base64.ts │ ├── experiment.ts │ ├── fibonacci.ts │ ├── scraper.ts │ ├── queryReadReplica.ts │ ├── plus │ │ ├── index.ts │ │ └── subscription.ts │ ├── twitter.ts │ ├── search.ts │ ├── flags.ts │ ├── clickhouse.ts │ ├── opportunity │ │ └── question.ts │ ├── queryDataSource.ts │ ├── constants.ts │ └── index.ts ├── routes │ ├── integrations │ │ └── index.ts │ └── notifications.ts ├── compatibility │ └── index.ts ├── db.ts ├── temporal │ ├── local.ts │ ├── worker.ts │ └── client.ts ├── cron │ ├── cron.ts │ ├── updateSourceTagView.ts │ ├── checkAnalyticsReport.ts │ ├── updateTagRecommendations.ts │ ├── cleanZombieImages.ts │ └── cleanZombieUserCompany.ts ├── migration │ ├── 1669630762042-DeleteLegacyNotification.ts │ ├── 1696520516809-IndexEmail.ts │ ├── 1685957377259-PostTitleHtml.ts │ ├── 1702899818874-DeleteNotificationV1.ts │ ├── 1687171948216-PostReportTags.ts │ ├── 1708364779258-PostGin.ts │ ├── 1722585215836-UserWeekStart.ts │ ├── 1724168427191-UserLanguage.ts │ ├── 1720502712657-BookmarkRemindAt.ts │ ├── 1752062615323-ContentJSON.ts │ ├── 1635231888769-PostSummary.ts │ ├── 1636615784674-AddTimeZoneToUser.ts │ ├── 1708961227968-PostLanguage.ts │ ├── 1713775430475-MarketingCta.ts │ ├── 1713784472035-UserExperienceLevel.ts │ ├── 1723135865854-SubmissionFlags.ts │ ├── 1628603355496-PostReportReplica.ts │ ├── 1635777599202-ReportReasonComment.ts │ ├── 1685509975573-PostOrder.ts │ ├── 1714140296134-SourceFlags.ts │ ├── 1736519518183-PostTranslations.ts │ ├── 1646231933553-AddOptOutCompanion.ts │ ├── 1650459501417-AddCompanionExpanded.ts │ ├── 1695795022369-FeatureValue.ts │ ├── 1714045647906-LastBootPopup.ts │ ├── 1730379505031-AlertsTopReader.ts │ ├── 1602486972915-IndexViews.ts │ ├── 1697642145710-TagSearchIndex.ts │ ├── 1700836062543-Notification.ts │ ├── 1730214092490-SettingsFlags.ts │ ├── 1730860362949-UserStatsIndex.ts │ ├── 1669735640941-AvatarName.ts │ ├── 1692536195223-AlertsBanner.ts │ ├── 1642656948858-SettingsCustomLinks.ts │ ├── 1677030838294-AlertViewedTour.ts │ ├── 1713194178000-PostContentMeta.ts │ ├── 1636725824921-AlertsRankColumn.ts │ ├── 1675687066898-SourceCreatedAt.ts │ ├── 1724925076759-UserCompanyFlags.ts │ ├── 1636431439048-ViewHiddenColumn.ts │ ├── 1649851992869-AddCompanionHelper.ts │ ├── 1671719516235-NullableSourceImage.ts │ ├── 1675936666556-AlertsChangelog.ts │ ├── 1682690930599-ContentCuration.ts │ ├── 1709002546321-UserAcquisitionChannel.ts │ ├── 1655452252620-ChangeSubmissionStatus.ts │ ├── 1675235844097-SourceReplica.ts │ ├── 1681817520844-MemberInviteRank.ts │ ├── 1714395908762-PostContentQuality.ts │ ├── 1721377128858-FeedSettingsBlocked.ts │ ├── 1724657443097-CompanyImage.ts │ ├── 1739337003828-UserDropProfileConfirmed.ts │ ├── 1744015053751-UserNotificationReplica.ts │ ├── 1746779086984-DropRoleContentPreference.ts │ ├── 1600267654798-Reputation.ts │ ├── 1637823009822-HTMLComments.ts │ ├── 1680519844392-MemberPostingRank.ts │ ├── 1702655844110-AdvancedSettings.ts │ ├── 1727627009736-UserCompanyUserIndex.ts │ ├── 1627479592436-DevCardEligible.ts │ ├── 1700661785471-PostCollectionSources.ts │ ├── 1721316842386-BookmarkReplica.ts │ ├── 1754662946146-CampaignIdentity.ts │ ├── 1755856463464-CampaignCreativeId.ts │ ├── 1684485068564-DropActivePost.ts │ ├── 1694443626041-ViewIndex.ts │ ├── 1718023112446-SidebarExpanded.ts │ ├── 1728910864611-UserStreakActionIndex.ts │ ├── 1603894888844-SourceRankBoost.ts │ ├── 1636372778451-AlertsFullReplication.ts │ ├── 1642055432413-SettingsEnabledSorting.ts │ ├── 1643720074970-OptOutWeeklyGoal.ts │ ├── 1671874590903-SourceMemberCDC.ts │ ├── 1683868176744-PostMentionTracking.ts │ ├── 1698317123443-DigestVariation.ts │ ├── 1700559262252-AlertsReplicaRemoval.ts │ ├── 1719406451367-ReplicaUserStreak.ts │ ├── 1693903931385-UserPostVoteIndex.ts │ ├── 1717144015912-OptOutReadingStreak.ts │ ├── 1736761342821-UserReportReplica.ts │ ├── 1754302649060-UserNotificationFlags.ts │ ├── 1764256128667-OpportunityAnonUserClaim.ts │ ├── 1609057573320-EcoDefault.ts │ ├── 1696422431723-SourceIndex.ts │ ├── 1704816100142-RemoveUpvoteDownvoteHiddenPostTables.ts │ ├── 1727025212763-SourceReportReplica.ts │ ├── 1744110320719-CoresRoleDefaultNone.ts │ ├── 1595072041410-OpenNewTabSetting.ts │ ├── 1598351006459-SourceFeedLastFetched.ts │ ├── 1606393430015-RoomyDefault.ts │ ├── 1611589310852-TagsStrCheckpoint.ts │ ├── 1687422430370-DisallowHandle.ts │ ├── 1688567430880-CommentReportReplica.ts │ ├── 1725988464760-SourceMembersIndex.ts │ ├── 1731398655167-UserSubscriptionFlags.ts │ ├── 1681297594048-OwnerToAdmin.ts │ ├── 1701100478531-PostRelationReplica.ts │ ├── 1708024370161-readingStreaksAlert.ts │ ├── 1740757299661-SettingsCommentsAlgo.ts │ ├── 1756452623176-AlertsBriefBannerLastSeen.ts │ ├── 1756730280766-RemoveCampaignCreativeId.ts │ ├── 1760100161241-PostAnalyticsGoToLink.ts │ ├── 1638431396006-OpenSidebarSettings.ts │ ├── 1644408031624-addViewHiddenIndex.ts │ ├── 1715592299433-SourceFlagFeaturedIndex.ts │ ├── 1723751266939-AlertShowRecoverStreak.ts │ ├── 1733918463705-BookmarkListLowerIndex.ts │ ├── 1718116498943-OnboardingChecklistSetting.ts │ ├── 1764600895705-OrganizationNameUnique.ts │ ├── 1651214210491-AutoDismissSettings.ts │ ├── 1741868793950-UserTransactionProcessor.ts │ ├── 1757935568161-QuestionOrder.ts │ ├── 1727958325934-SourceModerationRequired.ts │ ├── 1731424255967-ContentPreferenceReplicaFull.ts │ ├── 1761756586183-MarketingCtaTargets.ts │ ├── 1763996658211-UserExperienceFlagsImport.ts │ ├── 1595345916300-User.ts │ ├── 1620311370218-CommentLastUpdate.ts │ ├── 1699372102745-UserPersonalizedDigestLastSendDate.ts │ ├── 1671161744023-EmailNotificationPreference.ts │ ├── 1731913456006-UserSubscriptionIndex.ts │ ├── 1715682646033-SourceAdvancedSettingsRemoval.ts │ ├── 1716379554138-SquadPublicRequestReplica.ts │ ├── 1761817857874-OrganizationGenerationId.ts │ ├── 1739429058124-UserEmailConfirmed.ts │ ├── 1756822434859-OpportunityEnums.ts │ ├── 1759768307910-UserCandidatePreferenceCvParsedMarkdown.ts │ ├── 1762500537748-OpportunityFeedbackQuestions.ts │ ├── 1609237342955-KeywordOccurrencesDefaultUpdate.ts │ ├── 1730390077794-ContentPreferenceSquad.ts │ ├── 1730978012371-SourcePostModerationReplica.ts │ ├── 1747309505414-UserTransactionValueDesc.ts │ ├── 1764076115716-UserExperienceCustomLocation.ts │ ├── 1765378734870-OpportunitySubscriptionFlags.ts │ ├── 1743776947033-UserTransactionValueFee.ts │ ├── 1639645108022-SidebarExpandedSettings.ts │ ├── 1728894860612-UserNotificationIndex.ts │ ├── 1600350754278-RetroReputation.ts │ ├── 1689858892658-PublicSourceData.ts │ ├── 1716976350324-AlertFeedSettingsFeedback.ts │ ├── 1741863600700-UserUniqueAppAccountToken.ts │ ├── 1683721525235-DummyUser.ts │ ├── 1689854830946-WelcomeShowOnFeed.ts │ ├── 1754321021915-SourceMemberHasUnreadPostsIndexFix.ts │ ├── 1747300279523-StaleUserTransactionIndex.ts │ ├── 1660205249491-AddReferralIncreaseUsername.ts │ ├── 1587564396149-Notification.ts │ ├── 1730377577679-ContentPreferenceUserStatusTypeIndex.ts │ ├── 1645627361402-ActiveView.ts │ ├── 1739339702842-PostDropRationPlaceholder.ts │ ├── 1690963996461-PostYggdrasilId.ts │ ├── 1726691862710-User.ts │ ├── 1743605489726-ExperimentVariant.ts │ ├── 1756375302904-ChMigration.ts │ ├── 1631197656554-DeleteViewColumns.ts │ ├── 1671781195018-RequiredHandle.ts │ ├── 1623847855158-PostToc.ts │ ├── 1700735236452-YouTubePost.ts │ ├── 1743501278867-UserTransactionProviderId.ts │ ├── 1758797445760-SourcePostModerationFlagsDedup.ts │ ├── 1641904779220-AlertMyFeedDefaultValue.ts │ ├── 1686736666472-PostDownvotes.ts │ ├── 1744027556819-UserAwardEmail.ts │ └── 1757682053334-UserCandidatePreferenceStatusDefault.ts ├── http.ts ├── notifications │ └── icons.ts ├── cron.ts ├── logger.ts └── ids.ts ├── .gitattributes ├── .eslintignore ├── nodemon.json ├── .prettierrc ├── __tests__ ├── fixture │ ├── screen.pdf │ ├── testCA.der │ ├── happy_card.png │ ├── index.ts │ ├── company.ts │ ├── profile │ │ ├── certification.ts │ │ └── project.ts │ └── notifications.ts ├── .eslintrc.json ├── cron │ └── updateHighlightedViews.ts ├── common │ ├── fibonacci.ts │ └── search.ts ├── __snapshots__ │ └── tags.ts.snap └── teardown.ts ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── .git-crypt ├── keys │ └── default │ │ └── 0 │ │ ├── 27DE4D956FCC3AEFFDE6A223829FB1011004D789.gpg │ │ └── 74235A652201EA7C16D5D8D72472D3473F0C19F2.gpg └── .gitattributes ├── .dockerignore ├── .gitignore ├── seeds ├── OpportunityUserRecruiter.json └── OpportunityKeyword.json ├── .graphqlconfig ├── bin ├── demoTopic.ts └── runWorkflow.ts ├── .editorconfig ├── tsconfig.json └── pg-init-scripts └── create-databases.sh /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.16 2 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | AGENTS.md -------------------------------------------------------------------------------- /.infra/.nvmrc: -------------------------------------------------------------------------------- 1 | 22.16 2 | -------------------------------------------------------------------------------- /geoip/.gitignore: -------------------------------------------------------------------------------- 1 | *.mmdb 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /clickhouse/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.infra/.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dailydotdev/web-team @capJavert 2 | -------------------------------------------------------------------------------- /.infra/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /node_modules/ 3 | .env 4 | -------------------------------------------------------------------------------- /src/integrations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './magni'; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | helm/values/** filter=git-crypt diff=git-crypt -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | src/migration/* 3 | infra/node_modules 4 | -------------------------------------------------------------------------------- /clickhouse/migrations/20251104112214_post_analytics_events_mv.down.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts,json", 3 | "exec": "ts-node" 4 | } 5 | -------------------------------------------------------------------------------- /clickhouse/migrations/20251104173102_post_analytics_events_mv_revert.down.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /src/integrations/anthropic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './client'; 3 | -------------------------------------------------------------------------------- /src/integrations/automation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './automations'; 3 | -------------------------------------------------------------------------------- /src/integrations/lofn/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { LofnClient } from './clients'; 3 | -------------------------------------------------------------------------------- /__tests__/fixture/screen.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dailydotdev/daily-api/HEAD/__tests__/fixture/screen.pdf -------------------------------------------------------------------------------- /__tests__/fixture/testCA.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dailydotdev/daily-api/HEAD/__tests__/fixture/testCA.der -------------------------------------------------------------------------------- /__tests__/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "plugins": ["jest-extended"] 6 | } 7 | -------------------------------------------------------------------------------- /src/integrations/mimir/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { MimirClient, mimirClient } from './clients'; 3 | -------------------------------------------------------------------------------- /src/integrations/skadi/api/v2/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { SkadiApiClientV2 } from './clients'; 3 | -------------------------------------------------------------------------------- /__tests__/fixture/happy_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dailydotdev/daily-api/HEAD/__tests__/fixture/happy_card.png -------------------------------------------------------------------------------- /src/integrations/freyja/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { FreyjaClient, freyjaClient } from './clients'; 3 | -------------------------------------------------------------------------------- /src/integrations/snotra/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { SnotraClient, snotraClient } from './clients'; 3 | -------------------------------------------------------------------------------- /src/telemetry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './opentelemetry'; 2 | export * from './metrics'; 3 | export * from './common'; 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["firsttris.vscode-jest-runner", "qufiwefefwoyn.inline-sql-syntax"] 3 | } 4 | -------------------------------------------------------------------------------- /src/entity/opportunities/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './OpportunityUser'; 2 | export * from './OpportunityUserRecruiter'; 3 | -------------------------------------------------------------------------------- /src/workers/generators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generateNewImagesHandler'; 2 | export * from './generateEditImagesHandler'; 3 | -------------------------------------------------------------------------------- /src/entity/campaign/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Campaign'; 2 | export * from './CampaignPost'; 3 | export * from './CampaignSource'; 4 | -------------------------------------------------------------------------------- /__tests__/fixture/index.ts: -------------------------------------------------------------------------------- 1 | export * from './campaign'; 2 | export * from './marketingCta'; 3 | export * from './source'; 4 | export * from './user'; 5 | -------------------------------------------------------------------------------- /.infra/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: api 2 | runtime: 3 | name: nodejs 4 | options: 5 | packagemanager: pnpm 6 | description: Infrastructure for daily-api 7 | -------------------------------------------------------------------------------- /src/common/storekit.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export const generateAppAccountToken = (): string => { 4 | return uuidv4(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/integrations/meilisearch.ts: -------------------------------------------------------------------------------- 1 | export type MeiliPagination = { 2 | limit: number; 3 | offset: number; 4 | total: number; 5 | current: number; 6 | }; 7 | -------------------------------------------------------------------------------- /src/entity/questions/types.ts: -------------------------------------------------------------------------------- 1 | export enum QuestionType { 2 | Screening = 'screening', 3 | CandidatePreference = 'candidate_preference', 4 | Feedback = 'feedback', 5 | } 6 | -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/27DE4D956FCC3AEFFDE6A223829FB1011004D789.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dailydotdev/daily-api/HEAD/.git-crypt/keys/default/0/27DE4D956FCC3AEFFDE6A223829FB1011004D789.gpg -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/74235A652201EA7C16D5D8D72472D3473F0C19F2.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dailydotdev/daily-api/HEAD/.git-crypt/keys/default/0/74235A652201EA7C16D5D8D72472D3473F0C19F2.gpg -------------------------------------------------------------------------------- /src/common/paddle/recruiter/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const recruiterPaddleCustomDataSchema = z.object({ 4 | user_id: z.string(), 5 | opportunity_id: z.uuid(), 6 | }); 7 | -------------------------------------------------------------------------------- /src/integrations/skadi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { SkadiClient, skadiPersonalizedDigestClient } from './clients'; 3 | export * from './api/v2'; 4 | export * from './api/common'; 5 | -------------------------------------------------------------------------------- /.git-crypt/.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not edit this file. To specify the files to encrypt, create your own 2 | # .gitattributes file in the directory where your files are. 3 | * !filter !diff 4 | *.gpg binary 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .garden/ 3 | .infra/ 4 | .circleci/ 5 | node_modules/ 6 | .env.development 7 | .env.production 8 | local.sh 9 | prod.sh 10 | schema.graphql 11 | debezium/conf/key.json 12 | debezium/data 13 | -------------------------------------------------------------------------------- /clickhouse/migrations/1757338344011_post_analytics_events_ads_mv.down.sql: -------------------------------------------------------------------------------- 1 | -- down 2 | 3 | DROP TABLE IF EXISTS api.post_analytics_history_events_ads_daily_mv; 4 | 5 | DROP TABLE IF EXISTS api.post_analytics_events_ads_mv; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .garden/ 3 | build/ 4 | node_modules/ 5 | .env.development 6 | .env.production 7 | local.sh 8 | prod.sh 9 | schema.graphql 10 | debezium/conf/key.json 11 | debezium/data 12 | coverage 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /src/entity/Checkpoint.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Checkpoint { 5 | @PrimaryColumn({ type: 'text' }) 6 | key: string; 7 | 8 | @Column() 9 | timestamp: Date; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/healthCheck.ts: -------------------------------------------------------------------------------- 1 | import fastJson from 'fast-json-stringify'; 2 | 3 | export const stringifyHealthCheck = fastJson({ 4 | type: 'object', 5 | properties: { 6 | status: { 7 | type: 'string', 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/entity/posts/WelcomePost.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity } from 'typeorm'; 2 | import { PostType } from './Post'; 3 | import { FreeformPost } from './FreeformPost'; 4 | 5 | @ChildEntity(PostType.Welcome) 6 | export class WelcomePost extends FreeformPost {} 7 | -------------------------------------------------------------------------------- /src/routes/integrations/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify'; 2 | import slack from './slack'; 3 | 4 | export default async function (fastify: FastifyInstance): Promise { 5 | fastify.register(slack, { prefix: '/slack' }); 6 | } 7 | -------------------------------------------------------------------------------- /src/entity/user/experiences/types.ts: -------------------------------------------------------------------------------- 1 | export enum UserExperienceType { 2 | Work = 'work', 3 | Education = 'education', 4 | Project = 'project', 5 | Certification = 'certification', 6 | Volunteering = 'volunteering', 7 | OpenSource = 'opensource', 8 | } 9 | -------------------------------------------------------------------------------- /seeds/OpportunityUserRecruiter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 4 | "userId": "recruiter0" 5 | }, 6 | { 7 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 8 | "userId": "recruiter1" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /src/common/schema/reminders.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const entityReminderSchema = z.object({ 4 | entityId: z.string(), 5 | entityTableName: z.string(), 6 | scheduledAtMs: z.number().int().nonnegative(), 7 | delayMs: z.number().int().nonnegative(), 8 | }); 9 | -------------------------------------------------------------------------------- /src/compatibility/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify'; 2 | 3 | import publications from './publications'; 4 | 5 | export default async function (fastify: FastifyInstance): Promise { 6 | fastify.register(publications, { prefix: '/publications' }); 7 | } 8 | -------------------------------------------------------------------------------- /src/entity/user/DeletedUser.ts: -------------------------------------------------------------------------------- 1 | import { CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class DeletedUser { 5 | @PrimaryColumn({ length: 36 }) 6 | id: string; 7 | 8 | @CreateDateColumn() 9 | userDeletedAt: Date; 10 | } 11 | -------------------------------------------------------------------------------- /src/entity/TagSegment.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class TagSegment { 5 | @PrimaryColumn({ type: 'text' }) 6 | tag: string; 7 | 8 | @Column({ type: 'text' }) 9 | @Index() 10 | segment: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { AppDataSource } from './data-source'; 2 | 3 | const createOrGetConnection = async () => { 4 | if (!AppDataSource.isInitialized) { 5 | await AppDataSource.initialize(); 6 | } 7 | return AppDataSource; 8 | }; 9 | 10 | export default createOrGetConnection; 11 | -------------------------------------------------------------------------------- /src/integrations/automation/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An interface for an automation service (Retool, Zapier, etc) 3 | */ 4 | export interface IAutomationService { 5 | run(args: Args): Promise; 6 | } 7 | 8 | export enum Automation { 9 | Roaster = 'roaster', 10 | } 11 | -------------------------------------------------------------------------------- /src/temporal/local.ts: -------------------------------------------------------------------------------- 1 | import { run } from './notifications'; 2 | 3 | run() 4 | .then(() => { 5 | console.log('registered worker'); 6 | }) 7 | .catch((err) => { 8 | console.log('error registering worker'); 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /src/entity/questions/QuestionCandidatePreference.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity } from 'typeorm'; 2 | import { Question } from './Question'; 3 | import { QuestionType } from './types'; 4 | 5 | @ChildEntity(QuestionType.CandidatePreference) 6 | export class QuestionCandidatePreference extends Question {} 7 | -------------------------------------------------------------------------------- /src/common/base64.ts: -------------------------------------------------------------------------------- 1 | export type Base64String = string; 2 | 3 | export function base64(i: string): Base64String { 4 | return Buffer.from(i, 'utf8').toString('base64'); 5 | } 6 | 7 | export function unbase64(i: Base64String): string { 8 | return Buffer.from(i, 'base64').toString('utf8'); 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jestrunner.codeLensSelector": "**/*{.test,.spec,__tests__/**/*}.{js,jsx,ts,tsx}", 3 | "typescript.updateImportsOnFileMove.enabled": "always", 4 | "typescript.preferences.preferTypeOnlyAutoImports": true, 5 | "typescript.preferences.includePackageJsonAutoImports": "on", 6 | } 7 | -------------------------------------------------------------------------------- /src/common/experiment.ts: -------------------------------------------------------------------------------- 1 | import { ExperimentVariant, type ConnectionManager } from '../entity'; 2 | 3 | export const getExperimentVariant = ( 4 | con: ConnectionManager, 5 | feature: string, 6 | variant: string, 7 | ) => 8 | con.getRepository(ExperimentVariant).findOne({ where: { feature, variant } }); 9 | -------------------------------------------------------------------------------- /src/entity/contentPreference/ContentPreferenceWord.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity } from 'typeorm'; 2 | import { ContentPreference } from './ContentPreference'; 3 | import { ContentPreferenceType } from './types'; 4 | 5 | @ChildEntity(ContentPreferenceType.Word) 6 | export class ContentPreferenceWord extends ContentPreference {} 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Node: Nodemon", 7 | "runtimeVersion": "16", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": ["run", "dev"], 10 | "outputCapture": "std", 11 | }], 12 | } 13 | -------------------------------------------------------------------------------- /src/entity/opportunities/user/OpportunityUserRecruiter.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity } from 'typeorm'; 2 | import { OpportunityUser } from './OpportunityUser'; 3 | import { OpportunityUserType } from '../types'; 4 | 5 | @ChildEntity(OpportunityUserType.Recruiter) 6 | export class OpportunityUserRecruiter extends OpportunityUser {} 7 | -------------------------------------------------------------------------------- /src/entity/user/experiences/UserExperienceVolunteering.ts: -------------------------------------------------------------------------------- 1 | import { UserExperience } from './UserExperience'; 2 | import { ChildEntity } from 'typeorm'; 3 | import { UserExperienceType } from './types'; 4 | 5 | @ChildEntity(UserExperienceType.Volunteering) 6 | export class UserExperienceVolunteering extends UserExperience {} 7 | -------------------------------------------------------------------------------- /src/integrations/mimir/types.ts: -------------------------------------------------------------------------------- 1 | // Keep the type flexible to allow for future changes 2 | import { SearchRequest, SearchResponse } from '@dailydotdev/schema'; 3 | 4 | export interface IMimirClient { 5 | search({ 6 | query, 7 | version, 8 | offset, 9 | limit, 10 | }: SearchRequest): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/integrations/feed/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { 3 | FeedPreferencesConfigGenerator, 4 | SimpleFeedConfigGenerator, 5 | } from './configs'; 6 | export { FeedClient } from './clients'; 7 | export { 8 | FeedGenerator, 9 | feedGenerators, 10 | versionToFeedGenerator, 11 | feedClient, 12 | } from './generators'; 13 | -------------------------------------------------------------------------------- /src/common/schema/campaigns.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const CAMPAIGN_VALIDATION_SCHEMA = z.object({ 4 | budget: z 5 | .int() 6 | .min(1000) 7 | .max(100000) 8 | .refine((value) => value % 1000 === 0, { 9 | error: 'Budget must be divisible by 1000', 10 | }), 11 | duration: z.int().min(1).max(30), 12 | }); 13 | -------------------------------------------------------------------------------- /src/cron/cron.ts: -------------------------------------------------------------------------------- 1 | import { FastifyLoggerInstance } from 'fastify'; 2 | import { PubSub } from '@google-cloud/pubsub'; 3 | import { DataSource } from 'typeorm'; 4 | 5 | export interface Cron { 6 | name: string; 7 | handler: ( 8 | con: DataSource, 9 | logger: FastifyLoggerInstance, 10 | pubsub: PubSub, 11 | ) => Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/entity/contentPreference/types.ts: -------------------------------------------------------------------------------- 1 | export enum ContentPreferenceType { 2 | User = 'user', 3 | Keyword = 'keyword', 4 | Source = 'source', 5 | Word = 'word', 6 | Organization = 'organization', 7 | } 8 | 9 | export enum ContentPreferenceStatus { 10 | Follow = 'follow', 11 | Subscribed = 'subscribed', 12 | Blocked = 'blocked', 13 | } 14 | -------------------------------------------------------------------------------- /src/common/fibonacci.ts: -------------------------------------------------------------------------------- 1 | export const isPerfectSquare = (n: number) => { 2 | return Number.isInteger(Math.sqrt(n)); 3 | }; 4 | 5 | // Number is Fibonacci if (5*n^2 + 4) or (5*n^2 - 4) is a perfect square 6 | export const isFibonacci = (num: number) => { 7 | const x = 5 * Math.pow(num, 2); 8 | return isPerfectSquare(x + 4) || isPerfectSquare(x - 4); 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/scraper.ts: -------------------------------------------------------------------------------- 1 | import { GarmrService } from '../integrations/garmr'; 2 | 3 | export const garmScraperService = new GarmrService({ 4 | service: 'daily-scraper', 5 | breakerOpts: { 6 | halfOpenAfter: 10 * 1000, 7 | threshold: 0.2, 8 | duration: 20 * 1000, 9 | minimumRps: 1, 10 | }, 11 | retryOpts: { 12 | maxAttempts: 3, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/workers/notifications/worker.ts: -------------------------------------------------------------------------------- 1 | import { NotificationBaseContext } from '../../notifications'; 2 | import { NotificationType } from '../../notifications/common'; 3 | 4 | interface NotificationWorkerResult { 5 | type: NotificationType; 6 | ctx: NotificationBaseContext; 7 | } 8 | 9 | export type NotificationHandlerReturn = NotificationWorkerResult[] | undefined; 10 | -------------------------------------------------------------------------------- /src/entity/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './User'; 2 | export * from './UserPost'; 3 | export * from './UserState'; 4 | export * from './UserAction'; 5 | export * from './UserStreak'; 6 | export * from './UserStreakAction'; 7 | export * from './UserPersonalizedDigest'; 8 | export * from './UserMarketingCta'; 9 | export * from './UserStats'; 10 | export * from './UserTopReader'; 11 | -------------------------------------------------------------------------------- /src/integrations/snotra/types.ts: -------------------------------------------------------------------------------- 1 | // Keep the type flexible to allow for future changes 2 | export interface ProfileRequest { 3 | user_id: string; 4 | } 5 | 6 | export interface ProfileResponse { 7 | profile_text: string; 8 | update_at: string; 9 | } 10 | 11 | export interface ISnotraClient { 12 | getProfile(request: ProfileRequest): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/fixture/company.ts: -------------------------------------------------------------------------------- 1 | import type { DeepPartial } from 'typeorm'; 2 | import type { Company } from '../../src/entity/Company'; 3 | 4 | export const companyFixture: DeepPartial[] = [ 5 | { 6 | id: 'dailydev', 7 | name: 'daily.dev', 8 | image: 'cloudinary.com/dailydev/121232121/image', 9 | domains: ['daily.dev', 'dailydev.com'], 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /clickhouse/migrations/1756316678212_post_analytics.down.sql: -------------------------------------------------------------------------------- 1 | -- down 2 | 3 | drop table if exists api.post_analytics_history_events_daily_mv; 4 | 5 | drop table if exists api.post_analytics_history; 6 | 7 | drop table if exists api.post_analytics_events_referrer_mv; 8 | 9 | drop table if exists api.post_analytics_events_mv; 10 | 11 | drop table if exists api.post_analytics; 12 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Untitled GraphQL Schema", 3 | "schemaPath": "schema.graphql", 4 | "extensions": { 5 | "endpoints": { 6 | "Default GraphQL Endpoint": { 7 | "url": "http://localhost:5000/graphql", 8 | "headers": { 9 | "user-agent": "JS GraphQL" 10 | }, 11 | "introspect": false 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entity/ChMigration.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity({ name: 'migrations_ch' }) 4 | export class ChMigration { 5 | @PrimaryColumn({ type: 'bigint' }) 6 | id: string; 7 | 8 | @Column() 9 | name: string; 10 | 11 | @UpdateDateColumn() 12 | timestamp: Date; 13 | 14 | @Column() 15 | dirty: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /src/common/schema/topics.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const postMetricsUpdatedTopic = z.object({ 4 | postId: z.string(), 5 | payload: z.object({ 6 | upvotes: z.coerce.number().int().optional(), 7 | downvotes: z.coerce.number().int().optional(), 8 | comments: z.coerce.number().int().optional(), 9 | awards: z.coerce.number().int().optional(), 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /__tests__/cron/updateHighlightedViews.ts: -------------------------------------------------------------------------------- 1 | import { crons } from '../../src/cron/index'; 2 | import cron from '../../src/cron/updateHighlightedViews'; 3 | 4 | describe('updateHighlightedViews cron', () => { 5 | it('should be registered', () => { 6 | const registeredWorker = crons.find((item) => item.name === cron.name); 7 | 8 | expect(registeredWorker).toBeDefined(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/entity/posts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Post'; 2 | export * from './ArticlePost'; 3 | export * from './SharePost'; 4 | export * from './FreeformPost'; 5 | export * from './WelcomePost'; 6 | export * from './PostMention'; 7 | export * from './PostQuestion'; 8 | export * from './utils'; 9 | export * from './PostRelation'; 10 | export * from './CollectionPost'; 11 | export * from './YouTubePost'; 12 | -------------------------------------------------------------------------------- /src/entity/user/experiences/UserExperienceProject.ts: -------------------------------------------------------------------------------- 1 | import { UserExperience } from './UserExperience'; 2 | import { ChildEntity, Column } from 'typeorm'; 3 | import { UserExperienceType } from './types'; 4 | 5 | @ChildEntity(UserExperienceType.Project) 6 | export class UserExperienceProject extends UserExperience { 7 | @Column({ type: 'text', nullable: true }) 8 | url: string | null; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/queryReadReplica.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, QueryRunner } from 'typeorm'; 2 | import { queryDataSource } from './queryDataSource'; 3 | 4 | export const queryReadReplica = async ( 5 | con: DataSource, 6 | callback: ({ queryRunner }: { queryRunner: QueryRunner }) => Promise, 7 | ): Promise => { 8 | return queryDataSource(con, callback, { 9 | mode: 'slave', 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/workers/postEditedFreeformImages.ts: -------------------------------------------------------------------------------- 1 | import { ContentImageUsedByType } from '../entity'; 2 | import { Worker } from './worker'; 3 | import { generateEditImagesHandler } from './generators'; 4 | 5 | const worker: Worker = { 6 | subscription: 'api.post-edited-freeform-images', 7 | handler: generateEditImagesHandler('post', ContentImageUsedByType.Post), 8 | }; 9 | 10 | export default worker; 11 | -------------------------------------------------------------------------------- /src/entity/user/experiences/UserExperienceEducation.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column } from 'typeorm'; 2 | import { UserExperience } from './UserExperience'; 3 | import { UserExperienceType } from './types'; 4 | 5 | @ChildEntity(UserExperienceType.Education) 6 | export class UserExperienceEducation extends UserExperience { 7 | @Column({ type: 'text', nullable: true }) 8 | grade: string | null; 9 | } 10 | -------------------------------------------------------------------------------- /src/entity/user/experiences/UserExperienceOpenSource.ts: -------------------------------------------------------------------------------- 1 | import { UserExperience } from './UserExperience'; 2 | import { ChildEntity, Column } from 'typeorm'; 3 | import { UserExperienceType } from './types'; 4 | 5 | @ChildEntity(UserExperienceType.OpenSource) 6 | export class UserExperienceOpenSource extends UserExperience { 7 | @Column({ type: 'text', nullable: true }) 8 | url: string | null; 9 | } 10 | -------------------------------------------------------------------------------- /clickhouse/migrations/1757334086390_post_analytics_ads.down.sql: -------------------------------------------------------------------------------- 1 | -- down 2 | 3 | ALTER TABLE api.post_analytics_history 4 | DROP COLUMN IF EXISTS impressions_ads; 5 | 6 | ALTER TABLE api.post_analytics 7 | DROP COLUMN IF EXISTS reach_all; 8 | 9 | ALTER TABLE api.post_analytics 10 | DROP COLUMN IF EXISTS reach_ads; 11 | 12 | ALTER TABLE api.post_analytics 13 | DROP COLUMN IF EXISTS impressions_ads; 14 | -------------------------------------------------------------------------------- /src/entity/opportunities/types.ts: -------------------------------------------------------------------------------- 1 | export enum OpportunityUserType { 2 | Recruiter = 'recruiter', 3 | } 4 | 5 | export enum OpportunityMatchStatus { 6 | Pending = 'pending', 7 | CandidateAccepted = 'candidate_accepted', 8 | CandidateRejected = 'candidate_rejected', 9 | CandidateTimeOut = 'candidate_time_out', 10 | RecruiterAccepted = 'recruiter_accepted', 11 | RecruiterRejected = 'recruiter_rejected', 12 | } 13 | -------------------------------------------------------------------------------- /src/workers/notifications/articleNewCommentPostCommented.ts: -------------------------------------------------------------------------------- 1 | import { TypedNotificationWorker } from '../worker'; 2 | import { articleNewCommentHandler } from './utils'; 3 | 4 | export const articleNewCommentPostCommented: TypedNotificationWorker<'post-commented'> = 5 | { 6 | subscription: 'api.article-new-comment-notification.post-commented', 7 | handler: ({ commentId }, con) => articleNewCommentHandler(con, commentId), 8 | }; 9 | -------------------------------------------------------------------------------- /src/routes/notifications.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify'; 2 | import { injectGraphql } from '../compatibility/utils'; 3 | 4 | export default async function (fastify: FastifyInstance): Promise { 5 | fastify.get('/', async (req, res) => { 6 | const query = `{ 7 | unreadNotificationsCount 8 | }`; 9 | 10 | return injectGraphql(fastify, { query }, (obj) => obj['data'], req, res); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/common/plus/index.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import { ContentPreferenceOrganization } from '../../entity/contentPreference/ContentPreferenceOrganization'; 3 | 4 | export * from './subscription'; 5 | 6 | export const isUserPartOfOrganization = async ( 7 | con: DataSource, 8 | userId: string, 9 | ): Promise => 10 | con.getRepository(ContentPreferenceOrganization).existsBy({ 11 | userId, 12 | }); 13 | -------------------------------------------------------------------------------- /src/workers/userReadmeImages.ts: -------------------------------------------------------------------------------- 1 | import { ContentImageUsedByType } from '../entity'; 2 | import { Worker } from './worker'; 3 | import { generateEditImagesHandler } from './generators'; 4 | 5 | const worker: Worker = { 6 | subscription: 'api.user-readme-images', 7 | handler: generateEditImagesHandler( 8 | 'user', 9 | ContentImageUsedByType.User, 10 | {}, 11 | 'readmeHtml', 12 | ), 13 | }; 14 | 15 | export default worker; 16 | -------------------------------------------------------------------------------- /src/entity/notifications/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NotificationV2'; 2 | export * from './NotificationAvatarV2'; 3 | export * from './NotificationAttachmentV2'; 4 | export * from './NotificationPreference'; 5 | export * from './NotificationPreferencePost'; 6 | export * from './NotificationPreferenceSource'; 7 | export * from './NotificationPreferenceComment'; 8 | export * from './NotificationPreferenceUser'; 9 | export * from './UserNotification'; 10 | -------------------------------------------------------------------------------- /src/common/twitter.ts: -------------------------------------------------------------------------------- 1 | import { Post } from '../entity'; 2 | 3 | export const truncateToTweet = (text?: string): string => { 4 | if (!text) return ''; 5 | 6 | return text.length <= 130 ? text : `${text.substring(0, 127)}...`; 7 | }; 8 | 9 | export const truncatePostToTweet = ( 10 | post: Pick | undefined, 11 | ): string => { 12 | if (!post || !post.title?.length) return ''; 13 | 14 | return truncateToTweet(post.title); 15 | }; 16 | -------------------------------------------------------------------------------- /__tests__/fixture/profile/certification.ts: -------------------------------------------------------------------------------- 1 | export const userExperienceCertificationFixture = [ 2 | { 3 | type: 'certification', 4 | company: 'Udemy+', 5 | title: 'Master in Node.js', 6 | started_at: '2024-01-01', 7 | ended_at: '2024-12-31', 8 | }, 9 | { 10 | type: 'certification', 11 | company: 'Some Academy', 12 | title: 'Advanced Unknown Tech', 13 | started_at: '2022-05-01', 14 | ended_at: null, 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/entity/campaign/CampaignSource.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, ManyToOne } from 'typeorm'; 2 | import type { Source } from '../'; 3 | import { Campaign, CampaignType } from './Campaign'; 4 | 5 | @ChildEntity(CampaignType.Squad) 6 | export class CampaignSource extends Campaign { 7 | @Column({ type: 'text', default: null }) 8 | sourceId: string; 9 | 10 | @ManyToOne('Source', { lazy: true, onDelete: 'CASCADE' }) 11 | source: Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/entity/PostTag.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import type { Post } from './posts/Post'; 3 | 4 | @Entity() 5 | export class PostTag { 6 | @PrimaryColumn({ type: 'text' }) 7 | @Index() 8 | postId: string; 9 | 10 | @PrimaryColumn({ type: 'text' }) 11 | tag: string; 12 | 13 | @ManyToOne('Post', (post: Post) => post.tags, { 14 | lazy: true, 15 | onDelete: 'CASCADE', 16 | }) 17 | post: Promise; 18 | } 19 | -------------------------------------------------------------------------------- /src/entity/posts/SharePost.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, Index, ManyToOne } from 'typeorm'; 2 | import { Post, PostType } from './Post'; 3 | 4 | @ChildEntity(PostType.Share) 5 | export class SharePost extends Post { 6 | @Column({ type: 'text' }) 7 | @Index('IDX_sharedPostId') 8 | sharedPostId: string; 9 | 10 | @ManyToOne(() => Post, { lazy: true, onDelete: 'SET NULL' }) 11 | sharedPost: Promise; 12 | } 13 | 14 | export const MAX_COMMENTARY_LENGTH = 5000; 15 | -------------------------------------------------------------------------------- /src/temporal/worker.ts: -------------------------------------------------------------------------------- 1 | import { NativeConnection } from '@temporalio/worker'; 2 | import { getTemporalServerOptions } from './config'; 3 | 4 | let connection: NativeConnection; 5 | 6 | export const getTemporalWorkerConnection = async () => { 7 | if (connection) { 8 | return connection; 9 | } 10 | 11 | const { tls, address } = getTemporalServerOptions(); 12 | 13 | connection = await NativeConnection.connect({ address, tls }); 14 | 15 | return connection; 16 | }; 17 | -------------------------------------------------------------------------------- /src/workers/notifications/articleNewCommentCommentCommented.ts: -------------------------------------------------------------------------------- 1 | import { TypedNotificationWorker } from '../worker'; 2 | import { articleNewCommentHandler } from './utils'; 3 | 4 | export const articleNewCommentCommentCommented: TypedNotificationWorker<'comment-commented'> = 5 | { 6 | subscription: 'api.article-new-comment-notification.comment-commented', 7 | handler: async ({ childCommentId }, con) => 8 | articleNewCommentHandler(con, childCommentId), 9 | }; 10 | -------------------------------------------------------------------------------- /src/migration/1669630762042-DeleteLegacyNotification.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class DeleteLegacyNotification1669630762042 4 | implements MigrationInterface 5 | { 6 | name = 'DeleteLegacyNotification1669630762042'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query(`DROP TABLE "notification"`); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise {} 13 | } 14 | -------------------------------------------------------------------------------- /src/http.ts: -------------------------------------------------------------------------------- 1 | import { AgentOptions } from 'http'; 2 | import http from 'node:http'; 3 | import https from 'node:https'; 4 | import { RequestInit } from 'node-fetch'; 5 | 6 | const agentOpts: AgentOptions = { keepAlive: true, timeout: 1000 * 5 }; 7 | const httpAgent = new http.Agent(agentOpts); 8 | const httpsAgent = new https.Agent(agentOpts); 9 | export const fetchOptions: RequestInit = { 10 | agent: (_parsedURL) => 11 | _parsedURL.protocol === 'http:' ? httpAgent : httpsAgent, 12 | }; 13 | -------------------------------------------------------------------------------- /src/workers/userDeletedCio.ts: -------------------------------------------------------------------------------- 1 | import { TypedWorker } from './worker'; 2 | import { cio } from '../cio'; 3 | 4 | const worker: TypedWorker<'user-deleted'> = { 5 | subscription: 'api.user-deleted-cio', 6 | handler: async (message, _, log) => { 7 | if (!process.env.CIO_SITE_ID) { 8 | return; 9 | } 10 | 11 | await cio.destroy(message.data.id); 12 | log.info({ userId: message.data.id }, 'deleted user from customerio'); 13 | }, 14 | }; 15 | 16 | export default worker; 17 | -------------------------------------------------------------------------------- /src/entity/Banner.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Banner { 5 | @PrimaryColumn({ default: () => 'now()' }) 6 | timestamp: Date; 7 | 8 | @Column({ type: 'text' }) 9 | title: string; 10 | 11 | @Column({ type: 'text' }) 12 | subtitle: string; 13 | 14 | @Column({ type: 'text' }) 15 | cta: string; 16 | 17 | @Column({ type: 'text' }) 18 | url: string; 19 | 20 | @Column({ type: 'text' }) 21 | theme: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/entity/campaign/CampaignPost.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, ManyToOne } from 'typeorm'; 2 | import type { Post, FreeformPost, SharePost } from '../posts'; 3 | import { Campaign, CampaignType } from './Campaign'; 4 | 5 | @ChildEntity(CampaignType.Post) 6 | export class CampaignPost extends Campaign { 7 | @Column({ type: 'text', default: null }) 8 | postId: string; 9 | 10 | @ManyToOne('Post', { lazy: true, onDelete: 'CASCADE' }) 11 | post: Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/integrations/automation/automations.ts: -------------------------------------------------------------------------------- 1 | import { Automation, IAutomationService } from './types'; 2 | import { RetoolAutomationService } from './retool'; 3 | import { fetchOptions as globalFetchOptions } from '../../http'; 4 | 5 | export const automations: Record< 6 | Automation, 7 | IAutomationService, unknown> 8 | > = { 9 | roaster: new RetoolAutomationService(process.env.ROASTER_URL, { 10 | ...globalFetchOptions, 11 | timeout: 1000 * 60, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/integrations/skadi/api/common.ts: -------------------------------------------------------------------------------- 1 | export interface CancelCampaignArgs { 2 | campaignId: string; 3 | userId: string; 4 | } 5 | 6 | export interface EstimatedReach { 7 | impressions: number; 8 | clicks: number; 9 | users: number; 10 | min_impressions: number; 11 | max_impressions: number; 12 | } 13 | 14 | export interface EstimatedReachResponse 15 | extends Pick { 16 | minImpressions: number; 17 | maxImpressions: number; 18 | } 19 | -------------------------------------------------------------------------------- /src/entity/user/experiences/UserExperienceCertification.ts: -------------------------------------------------------------------------------- 1 | import { UserExperience } from './UserExperience'; 2 | import { ChildEntity, Column } from 'typeorm'; 3 | import { UserExperienceType } from './types'; 4 | 5 | @ChildEntity(UserExperienceType.Certification) 6 | export class UserExperienceCertification extends UserExperience { 7 | @Column({ type: 'text', nullable: true }) 8 | externalReferenceId: string | null; 9 | 10 | @Column({ type: 'text', nullable: true }) 11 | url: string | null; 12 | } 13 | -------------------------------------------------------------------------------- /bin/demoTopic.ts: -------------------------------------------------------------------------------- 1 | import { pubsub } from '../src/common/pubsub'; 2 | import { WarmIntro } from '@dailydotdev/schema'; 3 | 4 | (async () => { 5 | console.log('Starting script...'); 6 | await pubsub.topic('gondul.v1.warm-intro-generated').publishMessage({ 7 | data: WarmIntro.fromJson({ 8 | opportunityId: '404e1d3b-a639-4a24-b492-21ad60330d92', 9 | userId: 'testuser1', 10 | description: 'Some random warm intro message for you', 11 | }).toBinary(), 12 | }); 13 | process.exit(0); 14 | })(); 15 | -------------------------------------------------------------------------------- /src/workers/generators/generateNewImagesHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentImageUsedByType, 3 | updateUsedImagesInContent, 4 | } from '../../entity'; 5 | import { DataSource } from 'typeorm'; 6 | 7 | export const generateNewImagesHandler = async ( 8 | data: { id: string; contentHtml: string }, 9 | type: ContentImageUsedByType, 10 | con: DataSource, 11 | ) => { 12 | if (!data || !data?.id || !data?.contentHtml) return; 13 | 14 | await updateUsedImagesInContent(con, type, data.id, data.contentHtml); 15 | }; 16 | -------------------------------------------------------------------------------- /.infra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "bin", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "pretty": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "files": [ 16 | "index.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/workers/digestDeadLetterLog.ts: -------------------------------------------------------------------------------- 1 | import { messageToJson, Worker } from './worker'; 2 | 3 | type Data = unknown; 4 | 5 | const worker: Worker = { 6 | subscription: 'api.personalized-digest-email-dead-letter-log', 7 | handler: async (message, con, logger) => { 8 | const data = messageToJson(message); 9 | 10 | logger.info( 11 | { 12 | data, 13 | messageId: message.messageId, 14 | }, 15 | 'dead message, hel awaits', 16 | ); 17 | }, 18 | }; 19 | 20 | export default worker; 21 | -------------------------------------------------------------------------------- /__tests__/fixture/notifications.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial } from 'typeorm'; 2 | import { NotificationType } from '../../src/notifications/common'; 3 | import { NotificationV2 } from '../../src/entity'; 4 | 5 | const now = new Date(2021, 4, 2); 6 | 7 | export const notificationV2Fixture: DeepPartial = { 8 | createdAt: now, 9 | icon: 'icon', 10 | title: 'notification #1', 11 | description: 'description', 12 | targetUrl: 'https://daily.dev', 13 | type: NotificationType.CommentMention, 14 | public: true, 15 | }; 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | max_line_length = 80 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /src/entity/Category.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Category { 5 | @PrimaryColumn({ type: 'text' }) 6 | id: string; 7 | 8 | @Column({ type: 'text' }) 9 | emoji: string; 10 | 11 | @Column({ type: 'text' }) 12 | title: string; 13 | 14 | @Column({ default: () => 'now()' }) 15 | createdAt: Date; 16 | 17 | @UpdateDateColumn() 18 | updatedAt: Date; 19 | 20 | @Column({ type: 'text', array: true, default: [] }) 21 | tags: string[]; 22 | } 23 | -------------------------------------------------------------------------------- /src/migration/1696520516809-IndexEmail.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class IndexEmail1696520516809 implements MigrationInterface { 4 | name = 'IndexEmail1696520516809' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE INDEX "IDX_user_email" ON "user" ("email") `); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP INDEX "public"."IDX_user_email"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1685957377259-PostTitleHtml.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostTitleHtml1685957377259 implements MigrationInterface { 4 | name = 'PostTitleHtml1685957377259'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "titleHtml" text`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "titleHtml"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/notifications/icons.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationIcon { 2 | DailyDev = 'DailyDev', 3 | CommunityPicks = 'CommunityPicks', 4 | Comment = 'Comment', 5 | Upvote = 'Upvote', 6 | Bell = 'Bell', 7 | View = 'View', 8 | Block = 'Block', 9 | User = 'User', 10 | Star = 'Star', 11 | DevCard = 'DevCard', 12 | BookmarkReminder = 'BookmarkReminder', 13 | Timer = 'Timer', 14 | Streak = 'Streak', 15 | TopReaderBadge = 'TopReaderBadge', 16 | Core = 'Core', 17 | Opportunity = 'Opportunity', 18 | Analytics = 'Analytics', 19 | } 20 | -------------------------------------------------------------------------------- /.infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "scripts": { 4 | "prepare": "corepack enable || true" 5 | }, 6 | "devDependencies": { 7 | "@types/node": "22.15.x" 8 | }, 9 | "dependencies": { 10 | "@dailydotdev/pulumi-common": "^2.17.1", 11 | "@pulumi/gcp": "^9.6.0", 12 | "@pulumi/kubernetes": "^4.24.1", 13 | "@pulumi/pulumi": "^3.209.0" 14 | }, 15 | "packageManager": "pnpm@9.14.4+sha256.26a726b633b629a3fabda006f696ae4260954a3632c8054112d7ae89779e5f9a", 16 | "volta": { 17 | "node": "22.16.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/plus/subscription.ts: -------------------------------------------------------------------------------- 1 | export enum SubscriptionProvider { 2 | Paddle = 'paddle', 3 | AppleStoreKit = 'storekit', 4 | } 5 | 6 | export enum PurchaseType { 7 | Cores = 'cores', 8 | Plus = 'plus', 9 | Organization = 'organization', 10 | Recruiter = 'recruiter', 11 | } 12 | 13 | export enum PlusPlanType { 14 | Organization = 'organization', 15 | Personal = 'personal', 16 | } 17 | 18 | export enum SubscriptionStatus { 19 | Active = 'active', 20 | Expired = 'expired', 21 | Cancelled = 'cancelled', 22 | None = 'none', 23 | } 24 | -------------------------------------------------------------------------------- /src/migration/1702899818874-DeleteNotificationV1.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class DeleteNotificationV11702899818874 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query(`DROP TABLE "notification_attachment"`); 6 | await queryRunner.query(`DROP TABLE "notification_avatar"`); 7 | await queryRunner.query(`DROP TABLE "notification"`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise {} 11 | } 12 | -------------------------------------------------------------------------------- /src/migration/1687171948216-PostReportTags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostReportTags1687171948216 implements MigrationInterface { 4 | name = 'PostReportTags1687171948216'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post_report" ADD "tags" text array`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "post_report" DROP COLUMN "tags"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1708364779258-PostGin.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostGin1708364779258 implements MigrationInterface { 4 | name = 'PostGin1708364779258'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_post_tsv" ON "post" USING GIN(tsv)`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP INDEX "public"."IDX_post_tsv"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1722585215836-UserWeekStart.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserWeekStart1722585215836 implements MigrationInterface { 4 | name = 'UserWeekStart1722585215836' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user" ADD "weekStart" integer DEFAULT '1'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "weekStart"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1724168427191-UserLanguage.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserLanguage1724168427191 implements MigrationInterface { 4 | name = 'UserLanguage1724168427191'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "user" ADD "language" text`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "language"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entity/posts/PollPost.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, OneToMany } from 'typeorm'; 2 | import type { PollOption } from '../polls/PollOption'; 3 | import { Post, PostType } from './Post'; 4 | 5 | @ChildEntity(PostType.Poll) 6 | export class PollPost extends Post { 7 | @OneToMany('PollOption', (option: PollOption) => option.post, { 8 | lazy: true, 9 | }) 10 | pollOptions: Promise; 11 | 12 | @Column({ type: 'timestamp' }) 13 | endsAt?: Date | null; 14 | 15 | @Column({ type: 'integer', default: 0 }) 16 | numPollVotes: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1720502712657-BookmarkRemindAt.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class BookmarkRemindAt1720502712657 implements MigrationInterface { 4 | name = 'BookmarkRemindAt1720502712657'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "bookmark" ADD "remindAt" TIMESTAMP`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "bookmark" DROP COLUMN "remindAt"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1752062615323-ContentJSON.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ContentJSON1752062615323 implements MigrationInterface { 4 | name = 'ContentJSON1752062615323'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "post" ADD "contentJSON" jsonb`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "contentJSON"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bin/runWorkflow.ts: -------------------------------------------------------------------------------- 1 | import { runReminderWorkflow } from '../src/temporal/notifications/utils'; 2 | 3 | const afterFiveSeconds = () => Date.now() + 5000; 4 | const userId = 'B4AdaAXLKy1SdZxDhZwL1'; 5 | const postId = 's-UJPyk4i'; 6 | const params = { 7 | userId, 8 | postId, 9 | remindAt: afterFiveSeconds(), 10 | }; 11 | 12 | runReminderWorkflow(params) 13 | .then(() => { 14 | console.log('Workflow started'); 15 | }) 16 | .catch((err) => { 17 | console.log('Workflow failed:', err); 18 | }) 19 | .finally(() => { 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /src/common/schema/polls.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const pollCreationSchema = z.object({ 4 | title: z.string().max(250), 5 | sourceId: z.string(), 6 | duration: z 7 | .union([ 8 | z.literal(1), 9 | z.literal(3), 10 | z.literal(7), 11 | z.literal(14), 12 | z.literal(30), 13 | ]) 14 | .optional(), 15 | options: z 16 | .array( 17 | z.object({ 18 | text: z.string().min(1).max(35), 19 | order: z.number().min(0).max(4), 20 | }), 21 | ) 22 | .min(2) 23 | .max(4), 24 | }); 25 | -------------------------------------------------------------------------------- /src/migration/1635231888769-PostSummary.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class PostSummary1635231888769 implements MigrationInterface { 4 | name = 'PostSummary1635231888769' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."post" ADD "summary" text`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."post" DROP COLUMN "summary"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1636615784674-AddTimeZoneToUser.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AddTimeZoneToUser1636615784674 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "public"."user" ADD "timezone" text NULL`, 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `ALTER TABLE "public"."user" DROP COLUMN "timezone"`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1708961227968-PostLanguage.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostLanguage1708961227968 implements MigrationInterface { 4 | name = 'PostLanguage1708961227968' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "language" text DEFAULT 'en'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "language"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/entity/user/UserState.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import type { User } from './User'; 3 | 4 | export enum UserStateKey { 5 | CommunityLinkAccess = 'community_link_access', 6 | } 7 | 8 | @Entity() 9 | export class UserState { 10 | @PrimaryColumn({ length: 36 }) 11 | userId: string; 12 | 13 | @PrimaryColumn() 14 | key: string; 15 | 16 | @Column({ default: false }) 17 | value: boolean; 18 | 19 | @ManyToOne('User', { 20 | lazy: true, 21 | onDelete: 'CASCADE', 22 | }) 23 | user: Promise; 24 | } 25 | -------------------------------------------------------------------------------- /src/migration/1713775430475-MarketingCta.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class MarketingCta1713775430475 implements MigrationInterface { 4 | name = 'MarketingCta1713775430475' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "marketing_cta" ADD "status" text NOT NULL DEFAULT 'active'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "marketing_cta" DROP COLUMN "status"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1713784472035-UserExperienceLevel.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserExperienceLevel1713784472035 implements MigrationInterface { 4 | name = 'UserExperienceLevel1713784472035'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user" ADD "experienceLevel" text`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "experienceLevel"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1723135865854-SubmissionFlags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class SubmissionFlags1723135865854 implements MigrationInterface { 4 | name = 'SubmissionFlags1723135865854' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "submission" ADD "flags" jsonb NOT NULL DEFAULT '{}'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "submission" DROP COLUMN "flags"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/workers/transactionBalanceLog.ts: -------------------------------------------------------------------------------- 1 | import { TransferResponse } from '@dailydotdev/schema'; 2 | import type { TypedWorker } from './worker'; 3 | 4 | export const transactionBalanceLogWorker: TypedWorker<'njord.v1.balance-log'> = 5 | { 6 | subscription: 'api.transaction-balance-log', 7 | handler: async (message, con, logger): Promise => { 8 | const { data } = message; 9 | 10 | logger.info({ data }, 'transaction log'); 11 | }, 12 | parseMessage: (message) => { 13 | return TransferResponse.fromBinary(message.data); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /clickhouse/migrations/1757334086390_post_analytics_ads.up.sql: -------------------------------------------------------------------------------- 1 | -- up 2 | 3 | ALTER TABLE api.post_analytics 4 | ADD COLUMN IF NOT EXISTS impressions_ads SimpleAggregateFunction(sum, UInt64) DEFAULT 0; 5 | 6 | ALTER TABLE api.post_analytics 7 | ADD COLUMN IF NOT EXISTS reach_ads AggregateFunction(uniq, String); 8 | 9 | ALTER TABLE api.post_analytics 10 | ADD COLUMN IF NOT EXISTS reach_all AggregateFunction(uniq, String); 11 | 12 | ALTER TABLE api.post_analytics_history 13 | ADD COLUMN IF NOT EXISTS impressions_ads SimpleAggregateFunction(sum, UInt64) DEFAULT 0; 14 | -------------------------------------------------------------------------------- /src/entity/SourceFeed.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import type { Source } from './Source'; 3 | 4 | @Entity() 5 | @Index(['sourceId', 'feed'], { unique: true }) 6 | export class SourceFeed { 7 | @Column() 8 | sourceId: string; 9 | 10 | @ManyToOne('Source', (source: Source) => source.feeds, { 11 | lazy: true, 12 | onDelete: 'CASCADE', 13 | }) 14 | source: Promise; 15 | 16 | @PrimaryColumn({ type: 'text' }) 17 | feed: string; 18 | 19 | @Column({ nullable: true }) 20 | lastFetched: Date; 21 | } 22 | -------------------------------------------------------------------------------- /src/migration/1628603355496-PostReportReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostReportReplica1628603355496 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "public"."post_report" REPLICA IDENTITY FULL`, 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `ALTER TABLE "public"."post_report" REPLICA IDENTITY DEFAULT`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1635777599202-ReportReasonComment.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ReportReasonComment1635777599202 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "public"."post_report" ADD "comment" text`, 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `ALTER TABLE "public"."post_report" DROP COLUMN "comment"`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1685509975573-PostOrder.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostOrder1685509975573 implements MigrationInterface { 4 | name = 'PostOrder1685509975573'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "post" ADD "showOnFeed" boolean NOT NULL DEFAULT true`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "showOnFeed"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1714140296134-SourceFlags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceFlags1714140296134 implements MigrationInterface { 4 | name = 'SourceFlags1714140296134'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "source" ADD "flags" jsonb NOT NULL DEFAULT '{}'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "source" DROP COLUMN "flags"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1736519518183-PostTranslations.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostTranslations1736519518183 implements MigrationInterface { 4 | name = 'PostTranslations1736519518183' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "translation" jsonb NOT NULL DEFAULT '{}'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "translation"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/integrations/freyja/types.ts: -------------------------------------------------------------------------------- 1 | // Keep the type flexible to allow for future changes 2 | export type FunnelState = { 3 | session: { 4 | id: string; 5 | currentStep: string; 6 | userId: string; 7 | } & Record; 8 | funnel: { 9 | id: string; 10 | version: number; 11 | } & Record; 12 | }; 13 | 14 | export interface IFreyjaClient { 15 | createSession( 16 | userId: string, 17 | funnelId: string, 18 | version?: number, 19 | ): Promise; 20 | getSession(sessionId: string): Promise; 21 | } 22 | -------------------------------------------------------------------------------- /src/migration/1646231933553-AddOptOutCompanion.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AddOptOutCompanion1646231933553 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query(`ALTER TABLE "settings" 6 | ADD "optOutCompanion" boolean NOT NULL DEFAULT false`); 7 | } 8 | 9 | public async down(queryRunner: QueryRunner): Promise { 10 | await queryRunner.query( 11 | `ALTER TABLE "settings" DROP COLUMN "optOutCompanion"`, 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1650459501417-AddCompanionExpanded.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AddCompanionExpanded1650459501417 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query(`ALTER TABLE "settings" 6 | ADD "companionExpanded" boolean NULL DEFAULT NULL`); 7 | } 8 | 9 | public async down(queryRunner: QueryRunner): Promise { 10 | await queryRunner.query( 11 | `ALTER TABLE "settings" DROP COLUMN "companionExpanded"`, 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1695795022369-FeatureValue.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class FeatureValue1695795022369 implements MigrationInterface { 4 | name = 'FeatureValue1695795022369'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "feature" ADD "value" smallint NOT NULL DEFAULT '1'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "feature" DROP COLUMN "value"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1714045647906-LastBootPopup.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class LastBootPopup1714045647906 implements MigrationInterface { 4 | name = 'LastBootPopup1714045647906'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "alerts" ADD "lastBootPopup" TIMESTAMP`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "alerts" DROP COLUMN "lastBootPopup"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1730379505031-AlertsTopReader.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class AlertsTopReader1730379505031 implements MigrationInterface { 4 | name = 'AlertsTopReader1730379505031' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "alerts" ADD "showTopReader" boolean NOT NULL DEFAULT false`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "alerts" DROP COLUMN "showTopReader"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/entity/user/UserCandidateKeyword.ts: -------------------------------------------------------------------------------- 1 | import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import type { User } from './User'; 3 | 4 | @Entity() 5 | export class UserCandidateKeyword { 6 | @PrimaryColumn({ type: 'text' }) 7 | userId: string; 8 | 9 | @PrimaryColumn({ type: 'text' }) 10 | keyword: string; 11 | 12 | @ManyToOne('User', { 13 | lazy: true, 14 | onDelete: 'CASCADE', 15 | }) 16 | @JoinColumn({ 17 | name: 'userId', 18 | foreignKeyConstraintName: 'FK_user_candidate_keywork_user_id', 19 | }) 20 | user: Promise; 21 | } 22 | -------------------------------------------------------------------------------- /src/migration/1602486972915-IndexViews.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class IndexViews1602486972915 implements MigrationInterface { 4 | name = 'IndexViews1602486972915' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE INDEX "IDX_post_views" ON "public"."post" ("views") `, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP INDEX "public"."IDX_post_views"`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1697642145710-TagSearchIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class TagSearchIndex1697642145710 implements MigrationInterface { 4 | name = 'TagSearchIndex1697642145710' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE INDEX "IDX_status_value" ON "keyword" ("status", "value") `); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP INDEX "public"."IDX_status_value"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1700836062543-Notification.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class Notification1700836062543 implements MigrationInterface { 4 | name = 'Notification1700836062543' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "notification" ADD "numTotalAvatars" integer`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "numTotalAvatars"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1730214092490-SettingsFlags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SettingsFlags1730214092490 implements MigrationInterface { 4 | name = 'SettingsFlags1730214092490'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "settings" ADD "flags" jsonb NOT NULL DEFAULT '{}'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "settings" DROP COLUMN "flags"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1730860362949-UserStatsIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserStatsIndex1730860362949 implements MigrationInterface { 4 | name = 'UserStatsIndex1730860362949' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_user_stats_id" ON "public"."user_stats" ("id")`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP INDEX IF EXISTS "public"."IDX_user_stats_id"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/common/search.ts: -------------------------------------------------------------------------------- 1 | import { getLimit } from './pagination'; 2 | 3 | export type SearchSuggestionArgs = { 4 | query: string; 5 | version: number; 6 | limit?: number; 7 | includeContentPreference?: boolean; 8 | feedId?: string; 9 | }; 10 | 11 | export const defaultSearchLimit = 3; 12 | export const maxSearchLimit = 100; 13 | 14 | export const getSearchLimit = ({ 15 | limit, 16 | }: Pick) => { 17 | return getLimit({ 18 | limit: limit ?? defaultSearchLimit, 19 | defaultLimit: defaultSearchLimit, 20 | max: maxSearchLimit, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/entity/ExperimentVariant.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn } from 'typeorm'; 2 | 3 | export enum ExperimentVariantType { 4 | ProductPricing = 'productPricing', 5 | } 6 | 7 | @Entity() 8 | export class ExperimentVariant { 9 | @PrimaryColumn({ type: 'text' }) 10 | feature: string; 11 | 12 | @PrimaryColumn({ type: 'text' }) 13 | variant: string; 14 | 15 | @Column({ default: () => 'now()' }) 16 | createdAt: Date; 17 | 18 | @Column({ type: 'text', default: null }) 19 | value: string; 20 | 21 | @Column({ type: 'text' }) 22 | type: ExperimentVariantType; 23 | } 24 | -------------------------------------------------------------------------------- /src/integrations/skadi/types.ts: -------------------------------------------------------------------------------- 1 | // Keep the type flexible to allow for future changes 2 | export type SkadiAd = { 3 | title: string; 4 | link: string; 5 | image: string; 6 | company_name: string; 7 | company_logo: string; 8 | call_to_action: string; 9 | }; 10 | export type SkadiResponse = Partial<{ 11 | type: string; 12 | value: { 13 | digest: SkadiAd; 14 | }; 15 | pixels: string[]; 16 | }>; 17 | 18 | export interface ISkadiClient { 19 | getAd( 20 | placement: string, 21 | metadata: { 22 | USERID: string; 23 | }, 24 | ): Promise; 25 | } 26 | -------------------------------------------------------------------------------- /src/migration/1669735640941-AvatarName.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AvatarName1669735640941 implements MigrationInterface { 4 | name = 'AvatarName1669735640941'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "notification_avatar" 8 | ADD "name" text NOT NULL`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query( 13 | `ALTER TABLE "notification_avatar" DROP COLUMN "name"`, 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/migration/1692536195223-AlertsBanner.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertsBanner1692536195223 implements MigrationInterface { 4 | name = 'AlertsBanner1692536195223'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "alerts" ADD "lastBanner" TIMESTAMP NOT NULL DEFAULT now()`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "alerts" DROP COLUMN "lastBanner"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/common/fibonacci.ts: -------------------------------------------------------------------------------- 1 | import { isFibonacci } from '../../src/common/fibonacci'; 2 | 3 | describe('isFibonacci tests', () => { 4 | it('should return true for fibonacci numbers', () => { 5 | const fibs = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]; 6 | fibs.forEach((n) => { 7 | expect(isFibonacci(n)).toBeTruthy(); 8 | }); 9 | }); 10 | 11 | it('should return false for non-fibonacci numbers', () => { 12 | const nonFibs = [4, 6, 7, 9, 10, 11, 12, 14, 15, 16, 99]; 13 | nonFibs.forEach((n) => { 14 | expect(isFibonacci(n)).toBeFalsy(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/entity/notifications/NotificationPreferencePost.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, ManyToOne } from 'typeorm'; 2 | import type { Post } from '../posts'; 3 | import { NotificationPreferenceType } from '../../notifications/common'; 4 | import { NotificationPreference } from './NotificationPreference'; 5 | 6 | @ChildEntity(NotificationPreferenceType.Post) 7 | export class NotificationPreferencePost extends NotificationPreference { 8 | @Column({ type: 'text', default: null }) 9 | postId: string; 10 | 11 | @ManyToOne('Post', { lazy: true, onDelete: 'CASCADE' }) 12 | post: Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1642656948858-SettingsCustomLinks.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class SettingsCustomLinks1642656948858 implements MigrationInterface { 4 | name = 'SettingsCustomLinks1642656948858' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "settings" ADD "customLinks" text array`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "settings" DROP COLUMN "customLinks"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1677030838294-AlertViewedTour.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertViewedTour1677030838294 implements MigrationInterface { 4 | name = 'AlertViewedTour1677030838294'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "alerts" ADD "squadTour" boolean NOT NULL DEFAULT true`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "alerts" DROP COLUMN "squadTour"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1713194178000-PostContentMeta.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostContentMeta1713194178000 implements MigrationInterface { 4 | name = 'PostContentMeta1713194178000' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "contentMeta" jsonb NOT NULL DEFAULT '{}'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "contentMeta"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/workers/bannerDeleted.ts: -------------------------------------------------------------------------------- 1 | import { Worker } from './worker'; 2 | import { setRedisObject } from '../redis'; 3 | import { REDIS_BANNER_KEY } from '../config'; 4 | 5 | const worker: Worker = { 6 | subscription: 'api.banner-deleted', 7 | handler: async (message, con, logger): Promise => { 8 | try { 9 | await setRedisObject(REDIS_BANNER_KEY, 'false'); 10 | } catch (err) { 11 | logger.error( 12 | { messageId: message.messageId, err }, 13 | 'failed to remove redis cache for banner', 14 | ); 15 | } 16 | }, 17 | }; 18 | 19 | export default worker; 20 | -------------------------------------------------------------------------------- /src/migration/1636725824921-AlertsRankColumn.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class AlertsRankColumn1636725824921 implements MigrationInterface { 4 | name = 'AlertsRankColumn1636725824921' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."alerts" ADD "rankLastSeen" TIMESTAMP`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."alerts" DROP COLUMN "rankLastSeen"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1675687066898-SourceCreatedAt.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceCreatedAt1675687066898 implements MigrationInterface { 4 | name = 'SourceCreatedAt1675687066898'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "source" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "source" DROP COLUMN "createdAt"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1724925076759-UserCompanyFlags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserCompanyFlags1724925076759 implements MigrationInterface { 4 | name = 'UserCompanyFlags1724925076759'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "user_company" ADD "flags" jsonb NOT NULL DEFAULT '{}'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "user_company" DROP COLUMN "flags"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/fixture/profile/project.ts: -------------------------------------------------------------------------------- 1 | export const userExperienceProjectFixture = [ 2 | { 3 | type: 'project', 4 | title: 'Site for checking prices', 5 | description: 6 | 'A web application that allows users to compare prices from different retailers.', 7 | started_at: '2024-01-01', 8 | ended_at: '2025-09-30', 9 | skills: ['Node.js', 'GraphQL'], 10 | }, 11 | { 12 | type: 'project', 13 | title: 'Mystery App', 14 | description: 'An app with minimal info.', 15 | started_at: '2023-06-01', 16 | ended_at: null, 17 | skills: ['ObscureTech'], 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /src/entity/posts/BriefPost.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column } from 'typeorm'; 2 | import { Post, PostType } from './Post'; 3 | 4 | @ChildEntity(PostType.Brief) 5 | export class BriefPost extends Post { 6 | @Column({ type: 'text', nullable: true }) 7 | content: string; 8 | 9 | @Column({ type: 'text', nullable: true }) 10 | contentHtml: string; 11 | 12 | @Column({ nullable: true }) 13 | readTime?: number; 14 | 15 | @Column({ type: 'text', array: true, default: [] }) 16 | collectionSources: string[]; 17 | 18 | @Column({ type: 'jsonb', nullable: true }) 19 | contentJSON: object | null; 20 | } 21 | -------------------------------------------------------------------------------- /src/entity/user/experiences/UserExperienceWork.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column } from 'typeorm'; 2 | import { UserExperience } from './UserExperience'; 3 | import { UserExperienceType } from './types'; 4 | import { EmploymentType } from '@dailydotdev/schema'; 5 | 6 | @ChildEntity(UserExperienceType.Work) 7 | export class UserExperienceWork extends UserExperience { 8 | @Column({ 9 | type: 'integer', 10 | comment: 'EmploymentType from protobuf schema', 11 | default: null, 12 | }) 13 | employmentType: EmploymentType | null; 14 | 15 | @Column({ default: false }) 16 | verified: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1636431439048-ViewHiddenColumn.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class ViewHiddenColumn1636431439048 implements MigrationInterface { 4 | name = 'ViewHiddenColumn1636431439048' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."view" ADD "hidden" boolean NOT NULL DEFAULT false`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."view" DROP COLUMN "hidden"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1649851992869-AddCompanionHelper.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AddCompanionHelper1649851992869 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "public"."alerts" ADD "companionHelper" boolean NOT NULL DEFAULT true`, 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `ALTER TABLE "public"."alerts" DROP COLUMN "companionHelper"`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1671719516235-NullableSourceImage.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class NullableSourceImage1671719516235 implements MigrationInterface { 4 | name = 'NullableSourceImage1671719516235'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "source" 8 | ALTER COLUMN "image" DROP NOT NULL`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "source" 13 | ALTER COLUMN "image" SET NOT NULL`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1675936666556-AlertsChangelog.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertsChangelog1675936666556 implements MigrationInterface { 4 | name = 'AlertsChangelog1675936666556'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "alerts" ADD "lastChangelog" TIMESTAMP NOT NULL DEFAULT now()`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "alerts" DROP COLUMN "lastChangelog"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1682690930599-ContentCuration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ContentCuration1682690930599 implements MigrationInterface { 4 | name = 'ContentCuration1682690930599'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "post" ADD "contentCuration" text array NOT NULL DEFAULT '{}'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "contentCuration"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1709002546321-UserAcquisitionChannel.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserAcquisitionChannel1709002546321 implements MigrationInterface { 4 | name = 'UserAcquisitionChannel1709002546321'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user" ADD "acquisitionChannel" text`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `ALTER TABLE "user" DROP COLUMN "acquisitionChannel"`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entity/notifications/NotificationPreferenceSource.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, ManyToOne } from 'typeorm'; 2 | import type { Source } from '../Source'; 3 | import { NotificationPreferenceType } from '../../notifications/common'; 4 | import { NotificationPreference } from './NotificationPreference'; 5 | 6 | @ChildEntity(NotificationPreferenceType.Source) 7 | export class NotificationPreferenceSource extends NotificationPreference { 8 | @Column({ type: 'text', default: null }) 9 | sourceId: string; 10 | 11 | @ManyToOne('Source', { lazy: true, onDelete: 'CASCADE' }) 12 | source: Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1655452252620-ChangeSubmissionStatus.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ChangeSubmissionStatus1655452252620 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "submission" ALTER COLUMN "status" SET DEFAULT 'STARTED'`, 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `ALTER TABLE "submission" ALTER COLUMN "status" SET DEFAULT 'NOT_STARTED'`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1675235844097-SourceReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceReplica1675235844097 implements MigrationInterface { 4 | name = 'SourceReplica1675235844097'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."source" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."source" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1681817520844-MemberInviteRank.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class MemberInviteRank1681817520844 implements MigrationInterface { 4 | name = 'MemberInviteRank1681817520844' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "source" ADD "memberInviteRank" integer DEFAULT 0 NOT NULL`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "source" DROP COLUMN "memberInviteRank"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1714395908762-PostContentQuality.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostContentQuality1714395908762 implements MigrationInterface { 4 | name = 'PostContentQuality1714395908762' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "contentQuality" jsonb NOT NULL DEFAULT '{}'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "contentQuality"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1721377128858-FeedSettingsBlocked.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class FeedSettingsBlocked1721377128858 implements MigrationInterface { 4 | name = 'FeedSettingsBlocked1721377128858' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "feed_source" ADD "blocked" boolean NOT NULL DEFAULT true`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "feed_source" DROP COLUMN "blocked"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1724657443097-CompanyImage.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class CompanyImage1724657443097 implements MigrationInterface { 4 | name = 'CompanyImage1724657443097'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "company" ALTER COLUMN "image" SET NOT NULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "company" ALTER COLUMN "image" DROP NOT NULL`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1739337003828-UserDropProfileConfirmed.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserDropProfileConfirmed1739337003828 implements MigrationInterface { 4 | name = 'UserDropProfileConfirmed1739337003828' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "profileConfirmed"`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "user" ADD "profileConfirmed" boolean NOT NULL DEFAULT false`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1744015053751-UserNotificationReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserNotificationReplica1744015053751 4 | implements MigrationInterface 5 | { 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."user_transaction" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."user_transaction" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1746779086984-DropRoleContentPreference.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class DropRoleContentPreference1746779086984 implements MigrationInterface { 4 | name = 'DropRoleContentPreference1746779086984' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "content_preference" DROP COLUMN "role"`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "content_preference" ADD "role" text DEFAULT 'member'`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/temporal/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@temporalio/client'; 2 | import { Connection as TemporalConnection } from '@temporalio/client/lib/connection'; 3 | import { getTemporalServerOptions } from './config'; 4 | 5 | let client: Client; 6 | 7 | export const getTemporalClient = async (): Promise => { 8 | if (client) { 9 | return client; 10 | } 11 | 12 | const { namespace, tls, address } = getTemporalServerOptions(); 13 | const connection = await TemporalConnection.connect({ tls, address }); 14 | 15 | client = new Client({ connection, namespace }); 16 | 17 | return client; 18 | }; 19 | -------------------------------------------------------------------------------- /src/migration/1600267654798-Reputation.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class Reputation1600267654798 implements MigrationInterface { 4 | name = 'Reputation1600267654798' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."user" ADD "reputation" integer NOT NULL DEFAULT 0`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."user" DROP COLUMN "reputation"`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1637823009822-HTMLComments.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class HTMLComments1637823009822 implements MigrationInterface { 4 | name = 'HTMLComments1637823009822'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."comment" ADD "contentHtml" text NULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."comment" DROP COLUMN "contentHtml"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1680519844392-MemberPostingRank.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class MemberPostingRank1680519844392 implements MigrationInterface { 4 | name = 'MemberPostingRank1680519844392' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "source" ADD "memberPostingRank" integer DEFAULT 0 NOT NULL`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "source" DROP COLUMN "memberPostingRank"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1702655844110-AdvancedSettings.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class AdvancedSettings1702655844110 implements MigrationInterface { 4 | name = 'AdvancedSettings1702655844110' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "advanced_settings" ADD "options" jsonb NOT NULL DEFAULT '{}'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "advanced_settings" DROP COLUMN "options"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1727627009736-UserCompanyUserIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserCompanyUserIndex1727627009736 implements MigrationInterface { 4 | name = 'UserCompanyUserIndex1727627009736'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_user_company_user_id" ON "user_company" ("userId") `, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP INDEX "public"."IDX_user_company_user_id"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entity/PopularSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; 2 | import { PopularPost } from './PopularPost'; 3 | 4 | @ViewEntity({ 5 | materialized: true, 6 | expression: (dataSource: DataSource) => 7 | dataSource 8 | .createQueryBuilder() 9 | .select('"sourceId"') 10 | .addSelect('avg(r) r') 11 | .from(PopularPost, 'base') 12 | .groupBy('"sourceId"') 13 | .having('count(*) > 5') 14 | .orderBy('r', 'DESC'), 15 | }) 16 | export class PopularSource { 17 | @ViewColumn() 18 | sourceId: string; 19 | 20 | @ViewColumn() 21 | r: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/entity/notifications/NotificationPreferenceComment.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, ManyToOne } from 'typeorm'; 2 | import { NotificationPreference } from './NotificationPreference'; 3 | import { NotificationPreferenceType } from '../../notifications/common'; 4 | import type { Comment } from '../Comment'; 5 | 6 | @ChildEntity(NotificationPreferenceType.Comment) 7 | export class NotificationPreferenceComment extends NotificationPreference { 8 | @Column({ type: 'text', default: null }) 9 | commentId: string; 10 | 11 | @ManyToOne('Comment', { lazy: true, onDelete: 'CASCADE' }) 12 | comment: Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1627479592436-DevCardEligible.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class DevCardEligible1627479592436 implements MigrationInterface { 4 | name = 'DevCardEligible1627479592436' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."user" ADD "devcardEligible" boolean NOT NULL DEFAULT false`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."user" DROP COLUMN "devcardEligible"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1700661785471-PostCollectionSources.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostCollectionSources1700661785471 implements MigrationInterface { 4 | name = 'PostCollectionSources1700661785471' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "collectionSources" text array DEFAULT '{}'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "collectionSources"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1721316842386-BookmarkReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class BookmarkReplica1721316842386 implements MigrationInterface { 4 | name = 'BookmarkReplica1721316842386'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."bookmark" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."bookmark" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1754662946146-CampaignIdentity.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class CampaignIdentity1754662946146 implements MigrationInterface { 4 | name = 'CampaignIdentity1754662946146'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."campaign" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."campaign" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1755856463464-CampaignCreativeId.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class CampaignCreativeId1755856463464 implements MigrationInterface { 4 | name = 'CampaignCreativeId1755856463464'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "campaign" ADD "creativeId" uuid NOT NULL DEFAULT uuid_generate_v4()`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "campaign" DROP COLUMN "creativeId"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/tags.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`query searchTags should search for tags and order by value 1`] = ` 4 | Object { 5 | "searchTags": Object { 6 | "hits": Array [ 7 | Object { 8 | "name": "development", 9 | }, 10 | Object { 11 | "name": "webdev", 12 | }, 13 | ], 14 | "query": "dev", 15 | }, 16 | } 17 | `; 18 | 19 | exports[`query searchTags should take into account keyword synonyms 1`] = ` 20 | Object { 21 | "searchTags": Object { 22 | "hits": Array [], 23 | "query": "web-dev", 24 | }, 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/common/flags.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from '../entity'; 2 | 3 | export const transformSettingFlags = ({ flags }: Pick) => { 4 | return { 5 | sidebarSquadExpanded: flags?.sidebarSquadExpanded ?? true, 6 | sidebarCustomFeedsExpanded: flags?.sidebarCustomFeedsExpanded ?? true, 7 | sidebarOtherExpanded: flags?.sidebarOtherExpanded ?? true, 8 | sidebarResourcesExpanded: flags?.sidebarResourcesExpanded ?? true, 9 | sidebarBookmarksExpanded: flags?.sidebarBookmarksExpanded ?? true, 10 | clickbaitShieldEnabled: flags?.clickbaitShieldEnabled ?? true, 11 | ...flags, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/entity/TrendingSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; 2 | import { TrendingPost } from './TrendingPost'; 3 | 4 | @ViewEntity({ 5 | materialized: true, 6 | expression: (dataSource: DataSource) => 7 | dataSource 8 | .createQueryBuilder() 9 | .select('"sourceId"') 10 | .addSelect('avg(r) r') 11 | .from(TrendingPost, 'base') 12 | .groupBy('"sourceId"') 13 | .having('count(*) > 1') 14 | .orderBy('r', 'DESC'), 15 | }) 16 | export class TrendingSource { 17 | @ViewColumn() 18 | sourceId: string; 19 | 20 | @ViewColumn() 21 | r: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/entity/contentPreference/ContentPreferenceKeyword.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, JoinColumn, ManyToOne } from 'typeorm'; 2 | import { ContentPreference } from './ContentPreference'; 3 | import { ContentPreferenceType } from './types'; 4 | import type { Keyword } from '../Keyword'; 5 | 6 | @ChildEntity(ContentPreferenceType.Keyword) 7 | export class ContentPreferenceKeyword extends ContentPreference { 8 | @Column({ type: 'text', default: null }) 9 | keywordId: string; 10 | 11 | @ManyToOne('Keyword', { lazy: true, onDelete: 'CASCADE' }) 12 | @JoinColumn({ name: 'keywordId' }) 13 | keyword: Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1684485068564-DropActivePost.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class DropActivePost1684485068564 implements MigrationInterface { 4 | name = 'DropActivePost1684485068564'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`DROP VIEW "active_post"`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query( 12 | `CREATE VIEW "active_post" AS SELECT p.* FROM "public"."post" "p" WHERE "p"."deleted" = false AND "p"."visible" = true`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1694443626041-ViewIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ViewIndex1694443626041 implements MigrationInterface { 4 | name = 'ViewIndex1694443626041'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_3aaca4c7b6bd877a50443bed34" ON "view" ("userId", "timestamp") `, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `DROP INDEX "public"."IDX_3aaca4c7b6bd877a50443bed34"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1718023112446-SidebarExpanded.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class SidebarExpanded1718023112446 implements MigrationInterface { 4 | name = 'SidebarExpanded1718023112446' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "settings" ALTER COLUMN "sidebarExpanded" SET DEFAULT false`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "settings" ALTER COLUMN "sidebarExpanded" SET DEFAULT true`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1728910864611-UserStreakActionIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserStreakActionIndex1728910864611 implements MigrationInterface { 4 | name = 'UserStreakActionIndex1728910864611'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_usa_userid_type" ON "user_streak_action" ("userId", "type") `, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP INDEX "public"."IDX_usa_userid_type"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/teardown.ts: -------------------------------------------------------------------------------- 1 | import { closeClickHouseClient } from '../src/common/clickhouse'; 2 | import '../src/config'; 3 | import createOrGetConnection from '../src/db'; 4 | import { ioRedisPool, redisPubSub, singleRedisClient } from '../src/redis'; 5 | 6 | async function teardown() { 7 | const con = await createOrGetConnection(); 8 | await con.destroy(); 9 | singleRedisClient.disconnect(); 10 | redisPubSub.getPublisher().disconnect(); 11 | redisPubSub.getSubscriber().disconnect(); 12 | await redisPubSub.close(); 13 | await ioRedisPool.end(); 14 | await closeClickHouseClient(); 15 | } 16 | 17 | module.exports = teardown; 18 | -------------------------------------------------------------------------------- /src/migration/1603894888844-SourceRankBoost.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class SourceRankBoost1603894888844 implements MigrationInterface { 4 | name = 'SourceRankBoost1603894888844' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."source" ADD "rankBoost" integer NOT NULL DEFAULT 0`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."source" DROP COLUMN "rankBoost"`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1636372778451-AlertsFullReplication.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertsFullReplication1636372778451 implements MigrationInterface { 4 | name = 'AlertsFullReplication1636372778451'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."alerts" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."alerts" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1642055432413-SettingsEnabledSorting.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class SettingsEnabledSorting1642055432413 implements MigrationInterface { 4 | name = 'SettingsEnabledSorting1642055432413' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "settings" ADD "sortingEnabled" boolean NOT NULL DEFAULT false`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "settings" DROP COLUMN "sortingEnabled"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1643720074970-OptOutWeeklyGoal.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OptOutWeeklyGoal1643720074970 implements MigrationInterface { 4 | name = 'OptOutWeeklyGoal1643720074970'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "settings" 8 | ADD "optOutWeeklyGoal" boolean NOT NULL DEFAULT false`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query( 13 | `ALTER TABLE "settings" DROP COLUMN "optOutWeeklyGoal"`, 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/migration/1671874590903-SourceMemberCDC.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceMemberCDC1671874590903 implements MigrationInterface { 4 | name = 'SourceMemberCDC1671874590903'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."source_member" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."source_member" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1683868176744-PostMentionTracking.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | export class PostMentionTracking1683868176744 implements MigrationInterface { 3 | name = 'PostMentionTracking1683868176744'; 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query( 7 | `ALTER TABLE "public"."post_mention" REPLICA IDENTITY FULL`, 8 | ); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query( 13 | `ALTER TABLE "public"."post_mention" REPLICA IDENTITY DEFAULT`, 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/migration/1698317123443-DigestVariation.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class DigestVariation1698317123443 implements MigrationInterface { 4 | name = 'DigestVariation1698317123443' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user_personalized_digest" ADD "variation" integer NOT NULL DEFAULT '1'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "user_personalized_digest" DROP COLUMN "variation"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1700559262252-AlertsReplicaRemoval.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertsReplicaRemoval1700559262252 implements MigrationInterface { 4 | name = 'AlertsReplicaRemoval1700559262252'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."alerts" REPLICA IDENTITY DEFAULT`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."alerts" REPLICA IDENTITY FULL`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1719406451367-ReplicaUserStreak.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ReplicaUserStreak1719406451367 implements MigrationInterface { 4 | name = 'ReplicaUserStreak1719406451367'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."user_streak" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."user_streak" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/workers/notifications/communityPicksGranted.ts: -------------------------------------------------------------------------------- 1 | import { TypedNotificationWorker } from '../worker'; 2 | import { NotificationBaseContext } from '../../notifications'; 3 | import { NotificationType } from '../../notifications/common'; 4 | 5 | export const communityPicksGranted: TypedNotificationWorker<'community-link-access'> = 6 | { 7 | subscription: 'api.community-picks-granted-notification', 8 | handler: async ({ userId }) => { 9 | const ctx: NotificationBaseContext = { 10 | userIds: [userId], 11 | }; 12 | return [{ type: NotificationType.CommunityPicksGranted, ctx }]; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/common/clickhouse.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@clickhouse/client'; 2 | 3 | let client: ReturnType | null = null; 4 | 5 | export const getClickHouseClient = () => { 6 | if (!client) { 7 | client = createClient({ 8 | url: process.env.CLICKHOUSE_URL, 9 | username: process.env.CLICKHOUSE_USER, 10 | password: process.env.CLICKHOUSE_PASSWORD, 11 | request_timeout: 90_000, 12 | }); 13 | } 14 | 15 | return client; 16 | }; 17 | 18 | export const closeClickHouseClient = async () => { 19 | if (client) { 20 | await client.close(); 21 | 22 | client = null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/cron/updateSourceTagView.ts: -------------------------------------------------------------------------------- 1 | import { Cron } from './cron'; 2 | import { SourceTagView } from '../entity/SourceTagView'; 3 | 4 | const cron: Cron = { 5 | name: 'update-source-tag-view', 6 | handler: async (con, logger) => { 7 | const materializedViewName = 8 | con.getRepository(SourceTagView).metadata.tableName; 9 | 10 | try { 11 | await con.query(`REFRESH MATERIALIZED VIEW ${materializedViewName}`); 12 | 13 | logger.info({}, 'source tag view updated'); 14 | } catch (err) { 15 | logger.error({ err }, 'failed to update source tag view'); 16 | } 17 | }, 18 | }; 19 | 20 | export default cron; 21 | -------------------------------------------------------------------------------- /src/entity/PopularTag.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; 2 | import { PopularPost } from './PopularPost'; 3 | 4 | @ViewEntity({ 5 | materialized: true, 6 | expression: (dataSource: DataSource) => 7 | dataSource 8 | .createQueryBuilder() 9 | .select('unnest(string_to_array("tagsStr", \',\')) tag') 10 | .addSelect('avg(r) r') 11 | .from(PopularPost, 'base') 12 | .groupBy('tag') 13 | .having('count(*) > 10') 14 | .orderBy('r', 'DESC'), 15 | }) 16 | export class PopularTag { 17 | @ViewColumn() 18 | tag: string; 19 | 20 | @ViewColumn() 21 | r: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/entity/posts/PostQuestion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | Index, 5 | ManyToOne, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | import type { Post } from './Post'; 9 | 10 | @Entity() 11 | export class PostQuestion { 12 | @PrimaryGeneratedColumn('uuid') 13 | id: string; 14 | 15 | @Index() 16 | @Column({ type: 'text' }) 17 | postId: string; 18 | 19 | @Column({ type: 'text' }) 20 | question: string; 21 | 22 | @Column({ default: () => 'now()' }) 23 | createdAt: Date; 24 | 25 | @ManyToOne('Post', { 26 | lazy: true, 27 | onDelete: 'CASCADE', 28 | }) 29 | post: Promise; 30 | } 31 | -------------------------------------------------------------------------------- /src/migration/1693903931385-UserPostVoteIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserPostVoteIndex1693903931385 implements MigrationInterface { 4 | name = 'UserPostVoteIndex1693903931385' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE INDEX "IDX_1d63afaba1fa8e566ec9b62519" ON "user_post" ("userId", "vote", "votedAt") `); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP INDEX "public"."IDX_1d63afaba1fa8e566ec9b62519"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1717144015912-OptOutReadingStreak.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class OptOutReadingStreak1717144015912 implements MigrationInterface { 4 | name = 'OptOutReadingStreak1717144015912' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "settings" ADD "optOutReadingStreak" boolean NOT NULL DEFAULT false`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "settings" DROP COLUMN "optOutReadingStreak"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1736761342821-UserReportReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserReportReplica1736761342821 implements MigrationInterface { 4 | name = 'UserReportReplica1736761342821' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."user_report" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."user_report" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/migration/1754302649060-UserNotificationFlags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserNotificationFlags1754302649060 implements MigrationInterface { 4 | name = 'UserNotificationFlags1754302649060' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "user" ADD "notificationFlags" jsonb NOT NULL DEFAULT '{}'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "notificationFlags"`); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/migration/1764256128667-OpportunityAnonUserClaim.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OpportunityAnonUserClaim1764256128667 4 | implements MigrationInterface 5 | { 6 | name = 'OpportunityAnonUserClaim1764256128667'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "opportunity" ADD "flags" jsonb NOT NULL DEFAULT '{}'`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query(`ALTER TABLE "opportunity" DROP COLUMN "flags"`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/workers/commentEditedImages.ts: -------------------------------------------------------------------------------- 1 | import { ContentImageUsedByType } from '../entity'; 2 | import { Worker } from './worker'; 3 | import { generateEditImagesHandler } from './generators'; 4 | 5 | export const commentEditedWorker: Worker = { 6 | subscription: 'api.comment-edited-images', 7 | handler: generateEditImagesHandler('comment', ContentImageUsedByType.Comment), 8 | }; 9 | 10 | export const commentDeletedWorker: Worker = { 11 | subscription: 'api.comment-deleted-images', 12 | handler: generateEditImagesHandler( 13 | 'comment', 14 | ContentImageUsedByType.Comment, 15 | { shouldClearOnly: true }, 16 | ), 17 | }; 18 | -------------------------------------------------------------------------------- /src/entity/TrendingTag.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; 2 | import { TrendingPost } from './TrendingPost'; 3 | 4 | @ViewEntity({ 5 | materialized: true, 6 | expression: (dataSource: DataSource) => 7 | dataSource 8 | .createQueryBuilder() 9 | .select('unnest(string_to_array("tagsStr", \',\')) tag') 10 | .addSelect('avg(r) r') 11 | .from(TrendingPost, 'base') 12 | .groupBy('tag') 13 | .having('count(*) > 1') 14 | .orderBy('r', 'DESC'), 15 | }) 16 | export class TrendingTag { 17 | @ViewColumn() 18 | tag: string; 19 | 20 | @ViewColumn() 21 | r: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/migration/1609057573320-EcoDefault.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class EcoDefault1609057573320 implements MigrationInterface { 4 | name = 'EcoDefault1609057573320' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."settings" ALTER COLUMN "spaciness" SET DEFAULT 'eco'`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."settings" ALTER COLUMN "spaciness" SET DEFAULT 'roomy'`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1696422431723-SourceIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceIndex1696422431723 implements MigrationInterface { 4 | name = 'SourceIndex1696422431723'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_source_active_private_image" ON "source" ("active", "private", "image") `, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `DROP INDEX "public"."IDX_source_active_private_image"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1704816100142-RemoveUpvoteDownvoteHiddenPostTables.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class RemoveUpvoteDownvoteHiddenPostTables1704816100142 implements MigrationInterface { 4 | name = 'RemoveUpvoteDownvoteHiddenPostTables1704816100142' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`DROP TABLE "upvote"`); 8 | await queryRunner.query(`DROP TABLE "downvote"`); 9 | await queryRunner.query(`DROP TABLE "hidden_post"`); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise {} 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1727025212763-SourceReportReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceReportReplica1727025212763 implements MigrationInterface { 4 | name = 'SourceReportReplica1727025212763'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."source_report" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."source_report" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1744110320719-CoresRoleDefaultNone.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class CoresRoleDefaultNone1744110320719 implements MigrationInterface { 4 | name = 'CoresRoleDefaultNone1744110320719'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "user" ALTER COLUMN "coresRole" SET DEFAULT '0'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "user" ALTER COLUMN "coresRole" SET DEFAULT '3'`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entity/DisallowHandle.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, Entity, EntityManager, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class DisallowHandle { 5 | @PrimaryColumn({ unique: true }) 6 | value: string; 7 | } 8 | 9 | export const checkDisallowHandle = async ( 10 | entityManager: EntityManager | DataSource, 11 | value: string, 12 | ): Promise => { 13 | const handle = await entityManager 14 | .getRepository(DisallowHandle) 15 | .createQueryBuilder() 16 | .select('value') 17 | .where('value = :value', { 18 | value: value?.toLowerCase(), 19 | }) 20 | .getRawOne(); 21 | return !!handle; 22 | }; 23 | -------------------------------------------------------------------------------- /src/migration/1595072041410-OpenNewTabSetting.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class OpenNewTabSetting1595072041410 implements MigrationInterface { 4 | name = 'OpenNewTabSetting1595072041410' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."settings" ADD "openNewTab" boolean NOT NULL DEFAULT true`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."settings" DROP COLUMN "openNewTab"`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1598351006459-SourceFeedLastFetched.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class SourceFeedLastFetched1598351006459 implements MigrationInterface { 4 | name = 'SourceFeedLastFetched1598351006459' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."source_feed" ADD "lastFetched" TIMESTAMP`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."source_feed" DROP COLUMN "lastFetched"`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1606393430015-RoomyDefault.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class RoomyDefault1606393430015 implements MigrationInterface { 4 | name = 'RoomyDefault1606393430015' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."settings" ALTER COLUMN "spaciness" SET DEFAULT 'roomy'`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."settings" ALTER COLUMN "spaciness" SET DEFAULT 'eco'`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1611589310852-TagsStrCheckpoint.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class TagsStrCheckpoint1611589310852 implements MigrationInterface { 4 | name = 'TagsStrCheckpoint1611589310852' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`INSERT INTO "public"."checkpoint" ("key", "timestamp") VALUES ('last_tags_str_update', now())`) 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DELETE FROM "public"."checkpoint" WHERE "key" = 'last_tags_str_update'`) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1687422430370-DisallowHandle.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class DisallowHandle1687422430370 implements MigrationInterface { 4 | name = 'DisallowHandle1687422430370'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE TABLE "disallow_handle" ("value" character varying NOT NULL, CONSTRAINT "PK_8c84e48365905231826700dd8b4" PRIMARY KEY ("value"))`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP TABLE "disallow_handle"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1688567430880-CommentReportReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class CommentReportReplica1688567430880 implements MigrationInterface { 4 | name = 'CommentReportReplica1688567430880'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."comment_report" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."comment_report" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1725988464760-SourceMembersIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceMembersIndex1725988464760 implements MigrationInterface { 4 | name = 'SourceMembersIndex1725988464760'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_source_flags_total_members" ON source USING BTREE (((flags->>'totalMembers')::integer))`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP INDEX "IDX_source_flags_total_members"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1731398655167-UserSubscriptionFlags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserSubscriptionFlags1731398655167 implements MigrationInterface { 4 | name = 'UserSubscriptionFlags1731398655167'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "user" ADD "subscriptionFlags" jsonb NOT NULL DEFAULT '{}'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "user" DROP COLUMN "subscriptionFlags"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/opportunity/question.ts: -------------------------------------------------------------------------------- 1 | import type { EntityManager } from 'typeorm'; 2 | import { QuestionFeedback } from '../../entity/questions/QuestionFeedback'; 3 | 4 | export const addOpportunityDefaultQuestionFeedback = async ({ 5 | entityManager, 6 | opportunityId, 7 | }: { 8 | entityManager: EntityManager; 9 | opportunityId: string; 10 | }): Promise => { 11 | await entityManager.getRepository(QuestionFeedback).insert({ 12 | opportunityId, 13 | title: 'Why did you reject this opportunity?', 14 | placeholder: `E.g., Not interested in the tech stack, location doesn't work for me, compensation too low...`, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/queryDataSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, QueryRunner, ReplicationMode } from 'typeorm'; 2 | 3 | export const queryDataSource = async ( 4 | con: DataSource, 5 | callback: ({ queryRunner }: { queryRunner: QueryRunner }) => Promise, 6 | options?: Partial<{ 7 | mode: ReplicationMode; 8 | }>, 9 | ): Promise => { 10 | const queryRunner = con.createQueryRunner(options?.mode || 'master'); 11 | 12 | let result: T; 13 | 14 | try { 15 | result = await callback({ queryRunner }); 16 | } catch (error) { 17 | throw error; 18 | } finally { 19 | await queryRunner.release(); 20 | } 21 | 22 | return result; 23 | }; 24 | -------------------------------------------------------------------------------- /src/migration/1681297594048-OwnerToAdmin.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OwnerToAdmin1681297594048 implements MigrationInterface { 4 | name = 'OwnerToAdmin1681297594048'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `UPDATE "public"."source_member" SET "role" = 'admin' WHERE "role" = 'owner'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `UPDATE "public"."source_member" SET "role" = 'owner' WHERE "role" = 'admin'`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1701100478531-PostRelationReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostRelationReplica1701100478531 implements MigrationInterface { 4 | name = 'PostRelationReplica1701100478531' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "post_relation" REPLICA IDENTITY FULL`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "post_relation" REPLICA IDENTITY DEFAULT`, 15 | ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/migration/1708024370161-readingStreaksAlert.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ReadingStreaksAlert1708024370161 implements MigrationInterface { 4 | name = 'ReadingStreaksAlert1708024370161'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "alerts" ADD "showStreakMilestone" boolean NOT NULL DEFAULT false`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "alerts" DROP COLUMN "showStreakMilestone"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1740757299661-SettingsCommentsAlgo.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SettingsCommentsAlgo1740757299661 implements MigrationInterface { 4 | name = 'SettingsCommentsAlgo1740757299661'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "settings" ADD "sortCommentsBy" text NOT NULL DEFAULT 'oldest'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "settings" DROP COLUMN "sortCommentsBy"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1756452623176-AlertsBriefBannerLastSeen.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertsBriefBannerLastSeen1756452623176 implements MigrationInterface { 4 | name = 'AlertsBriefBannerLastSeen1756452623176'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "alerts" ADD "briefBannerLastSeen" TIMESTAMP`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "alerts" DROP COLUMN "briefBannerLastSeen"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1756730280766-RemoveCampaignCreativeId.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class RemoveCampaignCreativeId1756730280766 4 | implements MigrationInterface 5 | { 6 | name = 'RemoveCampaignCreativeId1756730280766'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query(`ALTER TABLE "campaign" DROP COLUMN "creativeId"`); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "campaign" ADD "creativeId" uuid NOT NULL DEFAULT uuid_generate_v4()`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1760100161241-PostAnalyticsGoToLink.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostAnalyticsGoToLink1760100161241 implements MigrationInterface { 4 | name = 'PostAnalyticsGoToLink1760100161241'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "post_analytics" ADD "goToLink" integer NOT NULL DEFAULT '0'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "post_analytics" DROP COLUMN "goToLink"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1638431396006-OpenSidebarSettings.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OpenSidebarSettings1638431396006 implements MigrationInterface { 4 | name = 'OpenSidebarSettings1638431396006'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."settings" ADD "openSidebar" boolean NOT NULL DEFAULT true`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "public"."settings" DROP COLUMN "openSidebar"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1644408031624-addViewHiddenIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class addViewHiddenIndex1644408031624 implements MigrationInterface { 4 | name = 'addViewHiddenIndex1644408031624'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_a91d81ad0de50fab4688a665c1" ON "view" ("postId", "userId", "hidden") `, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `DROP INDEX "public"."IDX_a91d81ad0de50fab4688a665c1"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1715592299433-SourceFlagFeaturedIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceFlagFeaturedIndex1715592299433 4 | implements MigrationInterface 5 | { 6 | name = 'SourceFlagFeaturedIndex1715592299433'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `CREATE INDEX "IDX_source_flags_featured" ON source USING HASH (((flags->'featured')::boolean))`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query(`DROP INDEX "IDX_source_flags_featured"`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1723751266939-AlertShowRecoverStreak.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertShowRecoverStreak1723751266939 implements MigrationInterface { 4 | name = 'AlertShowRecoverStreak1723751266939'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "alerts" ADD "showRecoverStreak" boolean NOT NULL DEFAULT false`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "alerts" DROP COLUMN "showRecoverStreak"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1733918463705-BookmarkListLowerIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class BookmarkListLowerIndex1733918463705 implements MigrationInterface { 4 | name = 'BookmarkListLowerIndex1733918463705' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "bookmark_list_idx_lowername_asc" ON "bookmark_list" (LOWER(name) ASC)`, 9 | ); 10 | } 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`DROP INDEX IF EXISTS "bookmark_list_idx_lowername_asc"`); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "sourceMap": true, 9 | "outDir": "build", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "strictPropertyInitialization": false, 13 | "skipLibCheck": true 14 | }, 15 | "exclude": [ 16 | "__tests__" 17 | ], 18 | "ts-node": { 19 | "swc": true 20 | }, 21 | "files": ["node_modules/jest-extended/types/index.d.ts"], 22 | "include": [ 23 | "bin/**/*.ts", 24 | "src/**/*.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/cron/checkAnalyticsReport.ts: -------------------------------------------------------------------------------- 1 | import { updateFlagsStatement } from '../common'; 2 | import { Post } from '../entity'; 3 | import { Cron } from './cron'; 4 | 5 | const cron: Cron = { 6 | name: 'check-analytics-report', 7 | handler: async (con) => { 8 | await con 9 | .createQueryBuilder() 10 | .update(Post) 11 | .set({ 12 | sentAnalyticsReport: true, 13 | flags: updateFlagsStatement({ sentAnalyticsReport: true }), 14 | }) 15 | .where(`"createdAt" <= now() - interval '20 hour'`) 16 | .andWhere('"sentAnalyticsReport" = false') 17 | .execute(); 18 | }, 19 | }; 20 | 21 | export default cron; 22 | -------------------------------------------------------------------------------- /src/migration/1718116498943-OnboardingChecklistSetting.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class OnboardingChecklistSetting1718116498943 implements MigrationInterface { 4 | name = 'OnboardingChecklistSetting1718116498943' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "settings" ADD "onboardingChecklistView" text NOT NULL DEFAULT 'hidden'`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "settings" DROP COLUMN "onboardingChecklistView"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1764600895705-OrganizationNameUnique.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OrganizationNameUnique1764600895705 implements MigrationInterface { 4 | name = 'OrganizationNameUnique1764600895705'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE UNIQUE INDEX "IDX_organization_name_unique" ON "organization" ("name") `, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `DROP INDEX "public"."IDX_organization_name_unique"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/cron/updateTagRecommendations.ts: -------------------------------------------------------------------------------- 1 | import { Cron } from './cron'; 2 | import { TagRecommendation } from '../entity/TagRecommendation'; 3 | 4 | const cron: Cron = { 5 | name: 'update-tag-recommendations', 6 | handler: async (con, logger) => { 7 | const materializedViewName = 8 | con.getRepository(TagRecommendation).metadata.tableName; 9 | 10 | try { 11 | await con.query(`REFRESH MATERIALIZED VIEW ${materializedViewName}`); 12 | 13 | logger.info({}, 'tag recommendations updated'); 14 | } catch (err) { 15 | logger.error({ err }, 'failed to update tag recommendations'); 16 | } 17 | }, 18 | }; 19 | 20 | export default cron; 21 | -------------------------------------------------------------------------------- /src/migration/1651214210491-AutoDismissSettings.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AutoDismissSettings1651214210491 implements MigrationInterface { 4 | name = 'AutoDismissSettings1651214210491'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "settings" ADD "autoDismissNotifications" boolean NOT NULL DEFAULT true`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `ALTER TABLE "settings" DROP COLUMN "autoDismissNotifications"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1741868793950-UserTransactionProcessor.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserTransactionProcessor1741868793950 4 | implements MigrationInterface 5 | { 6 | name = 'UserTransactionPaddleFlags1741868793950'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "user_transaction" ADD "processor" text NOT NULL`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "user_transaction" DROP COLUMN "processor"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1757935568161-QuestionOrder.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class QuestionOrder1757935568161 implements MigrationInterface { 4 | name = 'QuestionOrder1757935568161' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(/* sql */` 8 | ALTER TABLE "question" 9 | ADD "questionOrder" smallint NOT NULL DEFAULT '0' 10 | `); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query(/* sql */` 15 | ALTER TABLE "question" 16 | DROP COLUMN "questionOrder" 17 | `); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity/ActiveView.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; 2 | import { Post } from './posts'; 3 | import { View } from './View'; 4 | 5 | @ViewEntity({ 6 | expression: (connection: DataSource) => 7 | connection 8 | .createQueryBuilder() 9 | .select('view.*') 10 | .from(View, 'view') 11 | .leftJoin(Post, 'post', 'post.id = view.postId') 12 | .where('post.deleted = false'), 13 | }) 14 | export class ActiveView { 15 | @ViewColumn() 16 | userId: string; 17 | 18 | @ViewColumn() 19 | postId: string; 20 | 21 | @ViewColumn() 22 | timestamp: Date; 23 | 24 | @ViewColumn() 25 | hidden: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /src/entity/notifications/NotificationPreferenceUser.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, JoinColumn, ManyToOne } from 'typeorm'; 2 | import type { User } from '../user/User'; 3 | import { NotificationPreference } from './NotificationPreference'; 4 | import { NotificationPreferenceType } from '../../notifications/common'; 5 | 6 | @ChildEntity(NotificationPreferenceType.User) 7 | export class NotificationPreferenceUser extends NotificationPreference { 8 | @Column({ type: 'text', default: null }) 9 | referenceUserId: string; 10 | 11 | @ManyToOne('User', { lazy: true, onDelete: 'CASCADE' }) 12 | @JoinColumn({ name: 'referenceUserId' }) 13 | user: Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1727958325934-SourceModerationRequired.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceModerationRequired1727958325934 4 | implements MigrationInterface 5 | { 6 | name = 'SourceModerationRequired1727958325934'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "source" ADD "moderationRequired" boolean DEFAULT false`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "source" DROP COLUMN "moderationRequired"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1731424255967-ContentPreferenceReplicaFull.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ContentPreferenceReplicaFull1731424255967 4 | implements MigrationInterface 5 | { 6 | name = 'ContentPreferenceReplicaFull1731424255967'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "content_preference" REPLICA IDENTITY FULL`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "content_preference" REPLICA IDENTITY DEFAULT`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1761756586183-MarketingCtaTargets.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class MarketingCtaTargets1761756586183 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(/* sql */` 7 | ALTER TABLE "marketing_cta" 8 | ADD "targets" jsonb NOT NULL DEFAULT '{"webapp":true,"extension":true,"ios":true}' 9 | `); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(/* sql */` 14 | ALTER TABLE "marketing_cta" 15 | DROP COLUMN "targets" 16 | `); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/migration/1763996658211-UserExperienceFlagsImport.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserExperienceFlagsImport1763996658211 4 | implements MigrationInterface 5 | { 6 | name = 'UserExperienceFlagsImport1763996658211'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "user_experience" ADD "flags" jsonb NOT NULL DEFAULT '{}'`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "user_experience" DROP COLUMN "flags"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1595345916300-User.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class User1595345916300 implements MigrationInterface { 4 | name = 'User1595345916300' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE TABLE "public"."user" ("id" character varying(36) NOT NULL, "name" text NOT NULL, "image" text NOT NULL, CONSTRAINT "PK_03b91d2b8321aa7ba32257dc321" PRIMARY KEY ("id"))`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP TABLE "public"."user"`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1620311370218-CommentLastUpdate.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class CommentLastUpdate1620311370218 implements MigrationInterface { 4 | name = 'CommentLastUpdate1620311370218'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "public"."comment" ADD "lastUpdatedAt" TIMESTAMP`, 9 | undefined, 10 | ); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query( 15 | `ALTER TABLE "public"."comment" DROP COLUMN "lastUpdatedAt"`, 16 | undefined, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1699372102745-UserPersonalizedDigestLastSendDate.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserPersonalizedDigestLastSendDate1699372102745 implements MigrationInterface { 4 | name = 'UserPersonalizedDigestLastSendDate1699372102745' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user_personalized_digest" ADD "lastSendDate" TIMESTAMP`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "user_personalized_digest" DROP COLUMN "lastSendDate"`); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/workers/notifications/communityPicksFailed.ts: -------------------------------------------------------------------------------- 1 | import { TypedNotificationWorker } from '../worker'; 2 | import { NotificationSubmissionContext } from '../../notifications'; 3 | import { NotificationType } from '../../notifications/common'; 4 | 5 | export const communityPicksFailed: TypedNotificationWorker<'community-link-rejected'> = 6 | { 7 | subscription: 'api.community-picks-failed-notification', 8 | handler: async (submission) => { 9 | const ctx: NotificationSubmissionContext = { 10 | userIds: [submission.userId], 11 | submission, 12 | }; 13 | return [{ type: NotificationType.CommunityPicksFailed, ctx }]; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/migration/1671161744023-EmailNotificationPreference.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class EmailNotificationPreference1671161744023 4 | implements MigrationInterface 5 | { 6 | name = 'EmailNotificationPreference1671161744023'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "user" ADD "notificationEmail" boolean NOT NULL DEFAULT true`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "user" DROP COLUMN "notificationEmail"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1731913456006-UserSubscriptionIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserSubscriptionIndex1731913456006 implements MigrationInterface { 4 | name = 'UserSubscriptionIndex1731913456006'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_user_subflags_subscriptionid" ON "public"."user" USING gin("subscriptionFlags")`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `DROP INDEX "public"."IDX_user_subflags_subscriptionid"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/cron/cleanZombieImages.ts: -------------------------------------------------------------------------------- 1 | import { Cron } from './cron'; 2 | import { IsNull, LessThan } from 'typeorm'; 3 | import { subDays } from 'date-fns'; 4 | import { ContentImage } from '../entity'; 5 | 6 | const cron: Cron = { 7 | name: 'clean-zombie-images', 8 | handler: async (con, logger) => { 9 | logger.info('cleaning zombie images...'); 10 | const timeThreshold = subDays(new Date(), 30); 11 | const { affected } = await con.getRepository(ContentImage).delete({ 12 | createdAt: LessThan(timeThreshold), 13 | usedByType: IsNull(), 14 | }); 15 | logger.info({ count: affected }, 'zombies images cleaned! 🧟'); 16 | }, 17 | }; 18 | 19 | export default cron; 20 | -------------------------------------------------------------------------------- /src/migration/1715682646033-SourceAdvancedSettingsRemoval.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourceAdvancedSettingsRemoval1715682646033 4 | implements MigrationInterface 5 | { 6 | name = 'SourceAdvancedSettingsRemoval1715682646033'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "source" DROP COLUMN "advancedSettings"`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "source" ADD "advancedSettings" integer array DEFAULT '{}'`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1716379554138-SquadPublicRequestReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SquadPublicRequestReplica1716379554138 4 | implements MigrationInterface 5 | { 6 | name = 'SquadPublicRequestReplica1716379554138'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "public"."squad_public_request" REPLICA IDENTITY FULL`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "public"."squad_public_request" REPLICA IDENTITY DEFAULT`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1761817857874-OrganizationGenerationId.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OrganizationGenerationId1761817857874 4 | implements MigrationInterface 5 | { 6 | name = 'OrganizationGenerationId.ts1761817857874'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "organization" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4()`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "organization" ALTER COLUMN "id" SET DEFAULT NULL`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1739429058124-UserEmailConfirmed.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserEmailConfirmed1739429058124 implements MigrationInterface { 4 | name = 'UserEmailConfirmed1739429058124' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user" ADD "emailConfirmed" boolean NOT NULL DEFAULT true`); 8 | await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "emailConfirmed" SET DEFAULT false`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "emailConfirmed"`); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1756822434859-OpportunityEnums.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class OpportunityEnums1756822434859 implements MigrationInterface { 4 | name = 'OpportunityEnums1756822434859' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(/* sql */` 8 | ALTER TABLE "user_candidate_preference" ALTER COLUMN "status" SET DEFAULT '1' 9 | `); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(/* sql */` 14 | ALTER TABLE "user_candidate_preference" ALTER COLUMN "status" SET DEFAULT 'disabled' 15 | `); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1759768307910-UserCandidatePreferenceCvParsedMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserCandidatePreferenceCvParsedMarkdown1759768307910 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(/* sql */` 7 | ALTER TABLE "user_candidate_preference" 8 | ADD "cvParsedMarkdown" text 9 | `); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(/* sql */` 14 | ALTER TABLE "user_candidate_preference" 15 | DROP COLUMN "cvParsedMarkdown" 16 | `); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/migration/1762500537748-OpportunityFeedbackQuestions.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OpportunityFeedbackQuestions1762500537748 4 | implements MigrationInterface 5 | { 6 | name = 'OpportunityFeedbackQuestions1762500537748'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "opportunity_match" ADD "feedback" jsonb NOT NULL DEFAULT '[]'`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "opportunity_match" DROP COLUMN "feedback"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1609237342955-KeywordOccurrencesDefaultUpdate.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class KeywordOccurrencesDefaultUpdate1609237342955 implements MigrationInterface { 4 | name = 'KeywordOccurrencesDefaultUpdate1609237342955' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."keyword" ALTER COLUMN "occurrences" SET DEFAULT 1`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "public"."keyword" ALTER COLUMN "occurrences" SET DEFAULT 0`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1730390077794-ContentPreferenceSquad.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ContentPreferenceSquad1730390077794 implements MigrationInterface { 4 | name = 'ContentPreferenceSquad1730390077794'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_content_preference_flags_referralToken" ON "content_preference" ((flags->>'referralToken'))`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `DROP INDEX "IDX_content_preference_flags_referralToken"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1730978012371-SourcePostModerationReplica.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourcePostModerationReplica1730978012371 4 | implements MigrationInterface 5 | { 6 | name = 'SourcePostModerationReplica1730978012371'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "public"."source_post_moderation" REPLICA IDENTITY FULL`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "public"."source_post_moderation" REPLICA IDENTITY DEFAULT`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1747309505414-UserTransactionValueDesc.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserTransactionValueDesc1747309505414 4 | implements MigrationInterface 5 | { 6 | name = 'UserTransactionValueDesc1747309505414'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `CREATE INDEX IF NOT EXISTS "idx_user_transaction_value_desc" ON user_transaction ("value" DESC)`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `DROP INDEX IF EXISTS "idx_user_transaction_value_desc"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1764076115716-UserExperienceCustomLocation.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserExperienceCustomLocation1764076115716 4 | implements MigrationInterface 5 | { 6 | name = 'UserExperienceCustomLocation1764076115716'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "user_experience" ADD "customLocation" jsonb NOT NULL DEFAULT '{}'`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "user_experience" DROP COLUMN "customLocation"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1765378734870-OpportunitySubscriptionFlags.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class OpportunitySubscriptionFlags1765378734870 4 | implements MigrationInterface 5 | { 6 | name = 'OpportunitySubscriptionFlags1765378734870'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "opportunity" ADD "subscriptionFlags" jsonb NOT NULL DEFAULT '{}'`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "opportunity" DROP COLUMN "subscriptionFlags"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/integrations/lofn/types.ts: -------------------------------------------------------------------------------- 1 | import { FeedConfig, FeedVersion } from '../feed'; 2 | 3 | export type UserState = 'personalised' | 'non_personalised'; 4 | 5 | export type GenericMetadata = { 6 | [key: string]: unknown; 7 | }; 8 | 9 | export type LofnFeedConfigResponse = { 10 | user_id: string; 11 | config: Omit; 12 | tyr_metadata?: GenericMetadata; 13 | extra?: GenericMetadata; 14 | }; 15 | 16 | export type LofnFeedConfigPayload = { 17 | user_id: string; 18 | feed_version: FeedVersion; 19 | cursor: string; 20 | }; 21 | 22 | export interface ILofnClient { 23 | fetchConfig(payload: LofnFeedConfigPayload): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /src/migration/1743776947033-UserTransactionValueFee.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserTransactionValueFee1743776947033 4 | implements MigrationInterface 5 | { 6 | name = 'UserTransactionValueFee1743776947033'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "user_transaction" ADD COLUMN IF NOT EXISTS "valueIncFees" integer NOT NULL`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "user_transaction" DROP COLUMN IF EXISTS "valueIncFees"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cron/cleanZombieUserCompany.ts: -------------------------------------------------------------------------------- 1 | import { Cron } from './cron'; 2 | import { LessThan } from 'typeorm'; 3 | import { subHours } from 'date-fns'; 4 | import { UserCompany } from '../entity/UserCompany'; 5 | 6 | export const cleanZombieUserCompany: Cron = { 7 | name: 'clean-zombie-user-companies', 8 | handler: async (con, logger) => { 9 | logger.info('cleaning zombie user companies...'); 10 | const timeThreshold = subHours(new Date(), 1); 11 | const { affected } = await con.getRepository(UserCompany).delete({ 12 | verified: false, 13 | updatedAt: LessThan(timeThreshold), 14 | }); 15 | logger.info({ count: affected }, 'zombies user companies cleaned! 🧟'); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/entity/FeedSource.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import type { Feed } from './Feed'; 3 | import type { Source } from './Source'; 4 | 5 | @Entity() 6 | export class FeedSource { 7 | @PrimaryColumn({ type: 'text' }) 8 | @Index() 9 | feedId: string; 10 | 11 | @PrimaryColumn({ type: 'text' }) 12 | @Index() 13 | sourceId: string; 14 | 15 | @Column({ default: true }) 16 | blocked: boolean; 17 | 18 | @ManyToOne('Feed', { 19 | lazy: true, 20 | onDelete: 'CASCADE', 21 | }) 22 | feed: Promise; 23 | 24 | @ManyToOne('Source', { 25 | lazy: true, 26 | onDelete: 'CASCADE', 27 | }) 28 | source: Promise; 29 | } 30 | -------------------------------------------------------------------------------- /src/migration/1639645108022-SidebarExpandedSettings.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SidebarExpandedSettings1639645108022 4 | implements MigrationInterface 5 | { 6 | name = 'SidebarExpandedSettings1639645108022'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "public"."settings" RENAME COLUMN "openSidebar" TO "sidebarExpanded"`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "public"."settings" RENAME COLUMN "sidebarExpanded" TO "openSidebar"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1728894860612-UserNotificationIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserNotificationIndex1728894860612 implements MigrationInterface { 4 | name = 'UserNotificationIndex1728894860612'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE INDEX "IDX_user_notification_userid_public_readat" ON "user_notification" ("userId", "public", "readAt") `, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `DROP INDEX "public"."IDX_user_notification_userid_public_readat"`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pg-init-scripts/create-databases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | function create_user_and_database() { 7 | local database=$1 8 | echo " Creating user and database '$database'" 9 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 10 | CREATE USER $database; 11 | CREATE DATABASE $database; 12 | GRANT ALL PRIVILEGES ON DATABASE $database TO $database; 13 | EOSQL 14 | } 15 | 16 | if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then 17 | echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" 18 | for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do 19 | create_user_and_database $db 20 | done 21 | echo "Multiple databases created" 22 | fi 23 | -------------------------------------------------------------------------------- /src/migration/1600350754278-RetroReputation.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class RetroReputation1600350754278 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(`update "user" u set reputation = res.reputation from (select (sum(c.upvotes) + sum(c.featured::int) * 2) reputation, c."userId" from "comment" c where c.upvotes > 0 group by c."userId") res where u.id = res."userId";`, undefined); 7 | } 8 | 9 | public async down(queryRunner: QueryRunner): Promise { 10 | await queryRunner.query(`UPDATE "public"."user" SET "reputation" = 0`, undefined); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1689858892658-PublicSourceData.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PublicSourceData1689858892658 implements MigrationInterface { 4 | name = 'PublicSourceData1689858892658'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "source" ADD "headerImage" text`); 8 | await queryRunner.query(`ALTER TABLE "source" ADD "color" text`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "source" DROP COLUMN "color"`); 13 | await queryRunner.query(`ALTER TABLE "source" DROP COLUMN "headerImage"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1716976350324-AlertFeedSettingsFeedback.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertFeedSettingsFeedback1716976350324 4 | implements MigrationInterface 5 | { 6 | name = 'AlertFeedSettingsFeedback1716976350324'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "alerts" ADD "lastFeedSettingsFeedback" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "alerts" DROP COLUMN "lastFeedSettingsFeedback"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1741863600700-UserUniqueAppAccountToken.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserUniqueAppAccountToken1741863600700 implements MigrationInterface { 4 | name = 'UserUniqueAppAccountToken1741863600700' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(/* sql */`CREATE UNIQUE INDEX IF NOT EXISTS IDX_user_app_account_token_unique 8 | ON "user" (("subscriptionFlags"->>'appAccountToken')); 9 | `); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(/* sql */`DROP INDEX IF EXISTS IDX_user_app_account_token_unique;`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /seeds/OpportunityKeyword.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 4 | "keyword": "GraphQL" 5 | }, 6 | { 7 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 8 | "keyword": "Next.js" 9 | }, 10 | { 11 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 12 | "keyword": "Node.js" 13 | }, 14 | { 15 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 16 | "keyword": "React" 17 | }, 18 | { 19 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 20 | "keyword": "Tailwind CSS" 21 | }, 22 | { 23 | "opportunityId": "89f3daff-d6bb-4652-8f9c-b9f7254c9af1", 24 | "keyword": "TypeScript" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const ONE_MINUTE_IN_SECONDS = 60; 2 | export const ONE_HOUR_IN_SECONDS = 60 * 60; 3 | export const ONE_DAY_IN_SECONDS = ONE_HOUR_IN_SECONDS * 24; 4 | export const ONE_WEEK_IN_SECONDS = ONE_DAY_IN_SECONDS * 7; 5 | export const ONE_MONTH_IN_SECONDS = ONE_DAY_IN_SECONDS * 30; 6 | export const ONE_YEAR_IN_SECONDS = ONE_DAY_IN_SECONDS * 365; 7 | 8 | export const THREE_MONTHS_IN_SECONDS = ONE_MONTH_IN_SECONDS * 3; 9 | 10 | export const MAX_FOLLOWERS_LIMIT = 5_000; 11 | 12 | export const SUCCESSFUL_CIO_SYNC_DATE = 'successful_cio_sync_date'; 13 | 14 | export const customFeedsPlusDate = new Date('2024-12-11'); 15 | 16 | export const coresBalanceExpirationSeconds = 60 * ONE_MINUTE_IN_SECONDS; 17 | -------------------------------------------------------------------------------- /src/entity/ContentImage.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; 2 | 3 | export enum ContentImageUsedByType { 4 | Post = 'post', 5 | Comment = 'comment', 6 | User = 'user', 7 | } 8 | 9 | @Entity() 10 | @Index('IDX_content_image_used_by', ['usedByType', 'usedById']) 11 | export class ContentImage { 12 | @PrimaryColumn({ type: 'text' }) 13 | url: string; 14 | 15 | @Column({ type: 'text' }) 16 | serviceId: string; 17 | 18 | @Column({ default: () => 'now()' }) 19 | createdAt: Date; 20 | 21 | @Column({ type: 'text', nullable: true }) 22 | usedByType: ContentImageUsedByType | null; 23 | 24 | @Column({ type: 'text', nullable: true }) 25 | usedById: string | null; 26 | } 27 | -------------------------------------------------------------------------------- /src/migration/1683721525235-DummyUser.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class DummyUser1683721525235 implements MigrationInterface { 4 | name = 'DummyUser1683721525235'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `INSERT INTO "public"."user" ("id", "name", "image", "username") VALUES ('404', 'Inactive user', 'https://daily-now-res.cloudinary.com/image/upload/f_auto,q_auto/placeholders/placeholder3', 'inactive_user')`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DELETE FROM "public"."user" where id = "404"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1689854830946-WelcomeShowOnFeed.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class WelcomeShowOnFeed1689854830946 implements MigrationInterface { 4 | name = 'WelcomeShowOnFeed1689854830946'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `UPDATE post SET flags = flags || '{"showOnFeed": false}', "showOnFeed" = false WHERE type = 'welcome'`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | `UPDATE post SET flags = flags || '{"showOnFeed": true}', "showOnFeed" = true WHERE type = 'welcome'`, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1754321021915-SourceMemberHasUnreadPostsIndexFix.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class SourceMemberHasUnreadPostsIndexFix1754321021915 implements MigrationInterface { 4 | name = 'SourceMemberHasUnreadPostsIndexFix1754321021915' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(/* sql */`CREATE INDEX IF NOT EXISTS "IDX_source_member_flags_hasUnreadPosts" ON "source_member" (("flags"->>'hasUnreadPosts'));`) 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(/* sql */`DROP INDEX IF EXISTS "IDX_source_member_flags_hasUnreadPosts";`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/integrations/anthropic/types.ts: -------------------------------------------------------------------------------- 1 | import { IGarmrService } from '../garmr'; 2 | 3 | export interface AnthropicMessage { 4 | role: 'user' | 'assistant'; 5 | content: string; 6 | } 7 | 8 | export interface AnthropicRequest { 9 | model: string; 10 | max_tokens: number; 11 | system: string; 12 | messages: AnthropicMessage[]; 13 | [key: string]: unknown; 14 | } 15 | 16 | export interface AnthropicContentBlock { 17 | input: Record; 18 | } 19 | 20 | export interface AnthropicResponse { 21 | content: AnthropicContentBlock[]; 22 | } 23 | 24 | export interface IAnthropicClient { 25 | garmr: IGarmrService; 26 | createMessage(request: AnthropicRequest): Promise; 27 | } 28 | -------------------------------------------------------------------------------- /src/workers/postFreeformImages.ts: -------------------------------------------------------------------------------- 1 | import { ContentImageUsedByType, FreeformPost } from '../entity'; 2 | import { messageToJson, Worker } from './worker'; 3 | import { generateNewImagesHandler } from './generators'; 4 | import { ChangeObject } from '../types'; 5 | 6 | const worker: Worker = { 7 | subscription: 'api.post-freeform-images', 8 | handler: async (message, con): Promise => { 9 | const data: { 10 | post: ChangeObject; 11 | } = messageToJson(message); 12 | await generateNewImagesHandler( 13 | { id: data.post?.id, contentHtml: data.post?.contentHtml }, 14 | ContentImageUsedByType.Post, 15 | con, 16 | ); 17 | }, 18 | }; 19 | 20 | export default worker; 21 | -------------------------------------------------------------------------------- /src/entity/notifications/NotificationAttachmentV2.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | export enum NotificationAttachmentType { 4 | Post = 'post', 5 | Video = 'video', 6 | } 7 | 8 | @Entity() 9 | @Index('IDX_notification_attch_v2_type_reference_id', ['type', 'referenceId'], { 10 | unique: true, 11 | }) 12 | export class NotificationAttachmentV2 { 13 | @PrimaryGeneratedColumn('uuid') 14 | id: string; 15 | 16 | @Column({ type: 'text' }) 17 | type: NotificationAttachmentType; 18 | 19 | @Column({ type: 'text' }) 20 | image: string; 21 | 22 | @Column({ type: 'text' }) 23 | title: string; 24 | 25 | @Column({ type: 'text' }) 26 | referenceId: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/migration/1747300279523-StaleUserTransactionIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class StaleUserTransactionIndex1747300279523 4 | implements MigrationInterface 5 | { 6 | name = 'StaleUserTransactionIndex1747300279523'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `CREATE INDEX IF NOT EXISTS "idx_user_transaction_status_updated_at" ON user_transaction ("status", "updatedAt")`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `DROP INDEX IF EXISTS "idx_user_transaction_status_updated_at"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cron.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { PubSub } from '@google-cloud/pubsub'; 3 | 4 | import './config'; 5 | 6 | import { crons } from './cron/index'; 7 | import createOrGetConnection from './db'; 8 | import { logger } from './logger'; 9 | 10 | export default async function app(cronName: string): Promise { 11 | const connection = await createOrGetConnection(); 12 | const pubsub = new PubSub(); 13 | 14 | const selectedCron = crons.find((cron) => cron.name === cronName); 15 | if (selectedCron) { 16 | logger.info({ cron: cronName }, 'running cron'); 17 | await selectedCron.handler(connection, logger, pubsub); 18 | } else { 19 | logger.warn({ cron: cronName }, 'no such cron'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/migration/1660205249491-AddReferralIncreaseUsername.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AddReferralIncreaseUsername1660205249491 4 | implements MigrationInterface 5 | { 6 | name = 'AddReferralIncreaseUsername1660205249491'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query(`ALTER TABLE "user" 10 | ADD "referral" text`); 11 | await queryRunner.query( 12 | `ALTER TABLE "user" ALTER COLUMN "username" type character varying(39)`, 13 | ); 14 | } 15 | 16 | public async down(queryRunner: QueryRunner): Promise { 17 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "referral"`); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity/PopularVideoSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; 2 | import { PopularVideoPost } from './PopularVideoPost'; 3 | 4 | @ViewEntity({ 5 | materialized: true, 6 | expression: (dataSource: DataSource) => 7 | dataSource 8 | .createQueryBuilder() 9 | .select('"sourceId"') 10 | .addSelect('avg(r) r') 11 | .addSelect('count(*) posts') 12 | .from(PopularVideoPost, 'base') 13 | .groupBy('"sourceId"') 14 | .having('count(*) > 5') 15 | .orderBy('r', 'DESC'), 16 | }) 17 | export class PopularVideoSource { 18 | @ViewColumn() 19 | sourceId: string; 20 | 21 | @ViewColumn() 22 | r: number; 23 | 24 | @ViewColumn() 25 | posts: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | 3 | import { createGcpLoggingPinoConfig } from '@google-cloud/pino-logging-gcp-config'; 4 | import pino, { type LoggerOptions } from 'pino'; 5 | 6 | const pinoLoggerOptions: LoggerOptions = { 7 | level: env.LOG_LEVEL || 'info', 8 | }; 9 | 10 | export const loggerConfig: LoggerOptions = 11 | env.NODE_ENV === 'production' 12 | ? createGcpLoggingPinoConfig( 13 | { 14 | serviceContext: { 15 | service: env.SERVICE_NAME || 'service', 16 | version: env.SERVICE_VERSION || 'latest', 17 | }, 18 | }, 19 | pinoLoggerOptions, 20 | ) 21 | : pinoLoggerOptions; 22 | 23 | export const logger = pino(loggerConfig); 24 | -------------------------------------------------------------------------------- /src/migration/1587564396149-Notification.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class Notification1587564396149 implements MigrationInterface { 4 | name = 'Notification1587564396149' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`CREATE TABLE "public"."notification" ("timestamp" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, "html" text NOT NULL, CONSTRAINT "PK_aaea7d16887fb591ff6228131aa" PRIMARY KEY ("timestamp"))`, undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP TABLE "public"."notification"`, undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/1730377577679-ContentPreferenceUserStatusTypeIndex.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ContentPreferenceUserStatusTypeIndex1730377577679 4 | implements MigrationInterface 5 | { 6 | name = 'ContentPreferenceUserStatusTypeIndex1730377577679'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `CREATE INDEX "IDX_669e1fba44617e2a2f3939deec" ON "content_preference" ("userId", "status", "type") `, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `DROP INDEX "public"."IDX_669e1fba44617e2a2f3939deec"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity/AdvancedSettings.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { PostType } from './posts'; 3 | import { Source } from './Source'; 4 | 5 | @Entity() 6 | export class AdvancedSettings { 7 | @PrimaryGeneratedColumn('increment') 8 | id: number; 9 | 10 | @Column({ type: 'text' }) 11 | title: string; 12 | 13 | @Column({ type: 'text' }) 14 | description: string; 15 | 16 | @Column({ type: 'text', default: 'advanced' }) 17 | group: string; 18 | 19 | @Column({ type: 'bool', default: true }) 20 | defaultEnabledState: boolean; 21 | 22 | @Column({ type: 'jsonb', default: {} }) 23 | options: { 24 | source?: Pick; 25 | type?: PostType | string; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/migration/1645627361402-ActiveView.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ActiveView1645627361402 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE VIEW "public"."active_view" AS SELECT "view"."postId" AS "postId", "view"."userId" AS "userId", "view"."timestamp" AS "timestamp", "view"."hidden" AS "hidden" FROM "public"."view" LEFT JOIN "public"."post" "post" ON "post"."id" = "view"."postId" WHERE "post"."deleted" = false`, 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP VIEW "public"."active_view"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/migration/1739339702842-PostDropRationPlaceholder.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostDropRationPlaceholder1739339702842 implements MigrationInterface { 4 | name = 'PostDropRationPlaceholder1739339702842' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "ratio"`); 8 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "placeholder"`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "post" ADD "placeholder" text`); 13 | await queryRunner.query(`ALTER TABLE "post" ADD "ratio" double precision`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1690963996461-PostYggdrasilId.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostYggdrasilId1690963996461 implements MigrationInterface { 4 | name = 'PostYggdrasilId1690963996461'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "yggdrasilId" uuid`); 8 | await queryRunner.query( 9 | `CREATE INDEX "IDX_yggdrasil_id" ON "post" ("yggdrasilId") `, 10 | ); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query(`DROP INDEX "public"."IDX_yggdrasil_id"`); 15 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "yggdrasilId"`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migration/1726691862710-User.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class User1726691862710 implements MigrationInterface { 4 | name = 'User1726691862710' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user" ADD "followingEmail" boolean NOT NULL DEFAULT true`); 8 | await queryRunner.query(`ALTER TABLE "user" ADD "followNotifications" boolean NOT NULL DEFAULT true`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "followNotifications"`); 13 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "followingEmail"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1743605489726-ExperimentVariant.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ExperimentVariant1743605489726 implements MigrationInterface { 4 | name = 'ExperimentVariant1743605489726'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE TABLE "experiment_variant" ("feature" text NOT NULL, "variant" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "value" text, CONSTRAINT "PK_7ad1025f0a1a8674a557253db8e" PRIMARY KEY ("feature", "variant"))`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP TABLE "experiment_variant"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1756375302904-ChMigration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ChMigration1756375302904 implements MigrationInterface { 4 | name = 'ChMigration1756375302904'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE TABLE IF NOT EXISTS "migrations_ch" ("id" bigint NOT NULL, "name" character varying NOT NULL, "timestamp" TIMESTAMP NOT NULL DEFAULT now(), "dirty" boolean NOT NULL, CONSTRAINT "PK_b0fa9708b4cd4f2ade3bf4d3e56" PRIMARY KEY ("id"))`, 9 | ); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`DROP TABLE IF EXISTS "migrations_ch"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base64'; 2 | export * from './cloudinary'; 3 | export * from './crypto'; 4 | export * from './date'; 5 | export * from './feedGenerator'; 6 | export * from './healthCheck'; 7 | export * from './pagination'; 8 | export * from './pubsub'; 9 | export * from './reputation'; 10 | export * from './slack'; 11 | export * from './twitter'; 12 | export * from './users'; 13 | export * from './mailing'; 14 | export * from './post'; 15 | export * from './links'; 16 | export * from './utils'; 17 | export * from './typedPubsub'; 18 | export * from './personalizedDigest'; 19 | export * from './vote'; 20 | export * from './feed'; 21 | export * from './constants'; 22 | export * from './userIntegration'; 23 | export * from './protobuf'; 24 | -------------------------------------------------------------------------------- /src/migration/1631197656554-DeleteViewColumns.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class DeleteViewColumns1631197656554 implements MigrationInterface { 4 | name = 'DeleteViewColumns1631197656554' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "public"."view" DROP COLUMN "agent"`); 8 | await queryRunner.query(`ALTER TABLE "public"."view" DROP COLUMN "ip"`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "public"."view" ADD "ip" text`); 13 | await queryRunner.query(`ALTER TABLE "public"."view" ADD "agent" text`); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/migration/1671781195018-RequiredHandle.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class RequiredHandle1671781195018 implements MigrationInterface { 4 | name = 'RequiredHandle1671781195018'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`UPDATE "source" 8 | SET handle = id 9 | WHERE handle is null`); 10 | await queryRunner.query(`ALTER TABLE "source" 11 | ALTER COLUMN "handle" SET NOT NULL`); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query(`ALTER TABLE "source" 16 | ALTER COLUMN "handle" DROP NOT NULL`); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ids.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | import { customAlphabet } from 'nanoid/async'; 3 | import { FastifyRequest } from 'fastify'; 4 | import { counters } from './telemetry'; 5 | 6 | const alphabet = 7 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 8 | export const generateLongId = customAlphabet(alphabet, 21); 9 | export const generateShortId = customAlphabet(alphabet, 9); 10 | export const generateVerifyCode = customAlphabet('1234567890', 6); 11 | export const generateUUID = () => randomUUID(); 12 | 13 | export const generateTrackingId = ( 14 | req: FastifyRequest, 15 | origin: string, 16 | ): Promise => { 17 | counters?.api?.generateTrackingId?.add(1, { origin }); 18 | return generateLongId(); 19 | }; 20 | -------------------------------------------------------------------------------- /src/migration/1623847855158-PostToc.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class PostToc1623847855158 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "public"."post" 7 | ADD "description" text`, 8 | ); 9 | await queryRunner.query(`ALTER TABLE "public"."post" 10 | ADD "toc" jsonb`); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query(`ALTER TABLE "public"."post" 15 | DROP COLUMN "toc"`); 16 | await queryRunner.query( 17 | `ALTER TABLE "public"."post" 18 | DROP COLUMN "description"`, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/migration/1700735236452-YouTubePost.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class YouTubePost1700735236452 implements MigrationInterface { 4 | name = 'YouTubePost1700735236452' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "videoId" text`); 8 | await queryRunner.query(`ALTER TABLE "advanced_settings" ADD "group" text NOT NULL DEFAULT 'advanced'`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "videoId"`); 13 | await queryRunner.query(`ALTER TABLE "advanced_settings" DROP COLUMN "group"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1743501278867-UserTransactionProviderId.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserTransactionProviderId1743501278867 4 | implements MigrationInterface 5 | { 6 | name = 'UserTransactionProviderId1743501278867'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query(/* sql */ `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_transaction_flags_providerId" 10 | ON "user_transaction" (("flags"->>'providerId')); 11 | `); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | /* sql */ `DROP INDEX IF EXISTS "IDX_user_transaction_flags_providerId";`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migration/1758797445760-SourcePostModerationFlagsDedup.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class SourcePostModerationFlagsDedup1758797445760 4 | implements MigrationInterface 5 | { 6 | name = 'SourcePostModerationFlagsDedup1758797445760'; 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `CREATE INDEX IF NOT EXISTS "IDX_source_post_moderation_flags_dedupKey" ON source_post_moderation USING HASH ((flags->>'dedupKey'))`, 11 | ); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `DROP INDEX IF EXISTS "IDX_source_post_moderation_flags_dedupKey"`, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity/CommentReport.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import type { User } from './user'; 3 | import { ReportReason } from './common'; 4 | 5 | @Entity() 6 | export class CommentReport { 7 | @PrimaryColumn({ type: 'text' }) 8 | commentId: string; 9 | 10 | @PrimaryColumn({ length: 36 }) 11 | @Index('IDX_comment_report_user_id') 12 | userId: string; 13 | 14 | @Column({ default: () => 'now()' }) 15 | createdAt: Date; 16 | 17 | @Column({ length: 36, type: 'varchar' }) 18 | reason: ReportReason; 19 | 20 | @Column({ type: 'text', nullable: true }) 21 | note: string; 22 | 23 | @ManyToOne('User', { 24 | lazy: true, 25 | onDelete: 'CASCADE', 26 | }) 27 | user: Promise; 28 | } 29 | -------------------------------------------------------------------------------- /src/migration/1641904779220-AlertMyFeedDefaultValue.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AlertMyFeedDefaultValue1641904779220 4 | implements MigrationInterface 5 | { 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "alerts" ALTER COLUMN "myFeed" DROP NOT NULL`); 8 | await queryRunner.query(`ALTER TABLE "alerts" ALTER COLUMN "myFeed" DROP DEFAULT`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "alerts" ALTER COLUMN "myFeed" SET DEFAULT 'created'`); 13 | await queryRunner.query(`ALTER TABLE "alerts" ALTER COLUMN "myFeed" SET NOT NULL`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1686736666472-PostDownvotes.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class PostDownvotes1686736666472 implements MigrationInterface { 4 | name = 'PostDownvotes1686736666472' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "post" ADD "downvotes" integer NOT NULL DEFAULT '0'`); 8 | await queryRunner.query(`CREATE INDEX "IDX_post_downvotes" ON "post" ("downvotes") `); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`DROP INDEX "public"."IDX_post_downvotes"`); 13 | await queryRunner.query(`ALTER TABLE "post" DROP COLUMN "downvotes"`); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/migration/1744027556819-UserAwardEmail.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserAwardEmail1744027556819 implements MigrationInterface { 4 | name = 'UserAwardEmail1744027556819' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "user" ADD "awardEmail" boolean NOT NULL DEFAULT true`); 8 | await queryRunner.query(`ALTER TABLE "user" ADD "awardNotifications" boolean NOT NULL DEFAULT true`); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "awardNotifications"`); 13 | await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "awardEmail"`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/migration/1757682053334-UserCandidatePreferenceStatusDefault.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class UserCandidatePreferenceStatusDefault1757682053334 implements MigrationInterface { 4 | name = 'UserCandidatePreferenceStatusDefault1757682053334' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(/* sql */` 8 | ALTER TABLE "user_candidate_preference" 9 | ALTER COLUMN "status" SET DEFAULT '3' 10 | `); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query(/* sql */` 15 | ALTER TABLE "user_candidate_preference" 16 | ALTER COLUMN "status" SET DEFAULT '1' 17 | `); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/common/search.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultSearchLimit, 3 | getSearchLimit, 4 | maxSearchLimit, 5 | } from '../../src/common/search'; 6 | 7 | describe('getSearchLimit', () => { 8 | it('should return default limit', () => { 9 | expect(getSearchLimit({ limit: undefined })).toBe(defaultSearchLimit); 10 | }); 11 | 12 | it('should return custom search limit', () => { 13 | expect(getSearchLimit({ limit: 20 })).toBe(20); 14 | }); 15 | 16 | it('should return max search limit', () => { 17 | expect(getSearchLimit({ limit: 200 })).toBe(maxSearchLimit); 18 | }); 19 | 20 | it('should return min search limit', () => { 21 | expect(getSearchLimit({ limit: 0 })).toBe(1); 22 | expect(getSearchLimit({ limit: -5 })).toBe(1); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/entity/FeedTag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | Index, 6 | ManyToOne, 7 | PrimaryColumn, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | import type { Feed } from './Feed'; 11 | 12 | @Entity() 13 | @Index('IDX_feed_id_blocked', ['feedId', 'blocked']) 14 | export class FeedTag { 15 | @PrimaryColumn({ type: 'text' }) 16 | feedId: string; 17 | 18 | @PrimaryColumn({ type: 'text' }) 19 | tag: string; 20 | 21 | @Column({ default: false }) 22 | blocked: boolean; 23 | 24 | @CreateDateColumn() 25 | createdAt: Date; 26 | 27 | @Index() 28 | @UpdateDateColumn() 29 | updatedAt: Date; 30 | 31 | @ManyToOne('Feed', { 32 | lazy: true, 33 | onDelete: 'CASCADE', 34 | }) 35 | feed: Promise; 36 | } 37 | -------------------------------------------------------------------------------- /src/entity/contentPreference/ContentPreferenceUser.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntity, Column, Index, JoinColumn, ManyToOne } from 'typeorm'; 2 | import { ContentPreference } from './ContentPreference'; 3 | import { ContentPreferenceType } from './types'; 4 | import type { User } from '../user/User'; 5 | 6 | @ChildEntity(ContentPreferenceType.User) 7 | export class ContentPreferenceUser extends ContentPreference { 8 | @Column({ type: 'text', default: null }) 9 | @Index('IDX_content_preference_reference_user_id') 10 | referenceUserId: string; 11 | 12 | @Column({ type: 'text', default: null }) 13 | feedId: string; 14 | 15 | @ManyToOne('User', { lazy: true, onDelete: 'CASCADE' }) 16 | @JoinColumn({ name: 'referenceUserId' }) 17 | referenceUser: Promise; 18 | } 19 | --------------------------------------------------------------------------------