diff --git a/packages/api/src/apollo.ts b/packages/api/src/apollo.ts index 67b9bf79a..7d4c8ea02 100644 --- a/packages/api/src/apollo.ts +++ b/packages/api/src/apollo.ts @@ -13,10 +13,10 @@ import * as httpContext from 'express-http-context2' import * as jwt from 'jsonwebtoken' import { EntityManager } from 'typeorm' import { promisify } from 'util' +import { appDataSource } from './data_source' import { sanitizeDirectiveTransformer } from './directives' import { env } from './env' import { createPubSubClient } from './pubsub' -import { entityManager } from './repository' import { functionResolvers } from './resolvers/function_resolvers' import { ClaimsToSet, ResolverContext } from './resolvers/types' import ScalarResolvers from './scalars' @@ -79,7 +79,7 @@ const contextFunc: ContextFunction = async ({ cb: (em: EntityManager) => TResult, userRole?: string ): Promise => - entityManager.transaction(async (tx) => { + appDataSource.transaction(async (tx) => { await setClaims(tx, undefined, userRole) return cb(tx) }), diff --git a/packages/api/src/events/user/profile_created.ts b/packages/api/src/events/user/profile_created.ts new file mode 100644 index 000000000..0af9326e0 --- /dev/null +++ b/packages/api/src/events/user/profile_created.ts @@ -0,0 +1,34 @@ +import { + EntitySubscriberInterface, + EventSubscriber, + InsertEvent, +} from 'typeorm' +import { Profile } from '../../entity/profile' +import { createDefaultFiltersForUser } from '../../services/create_user' +import { addPopularReadsForNewUser } from '../../services/popular_reads' + +@EventSubscriber() +export class AddPopularReadsToNewUser + implements EntitySubscriberInterface +{ + listenTo() { + return Profile + } + + async afterInsert(event: InsertEvent): Promise { + await addPopularReadsForNewUser(event.entity.user.id, event.manager) + } +} + +@EventSubscriber() +export class AddDefaultFiltersToNewUser + implements EntitySubscriberInterface +{ + listenTo() { + return Profile + } + + async afterInsert(event: InsertEvent): Promise { + await createDefaultFiltersForUser(event.manager)(event.entity.user.id) + } +} diff --git a/packages/api/src/events/user/user_created.ts b/packages/api/src/events/user/user_created.ts deleted file mode 100644 index 57d73fe1f..000000000 --- a/packages/api/src/events/user/user_created.ts +++ /dev/null @@ -1,64 +0,0 @@ -// import { -// EntitySubscriberInterface, -// EventSubscriber, -// InsertEvent, -// } from 'typeorm' -// import { Profile } from '../../entity/profile' -// import { createPubSubClient } from '../../pubsub' -// import { addPopularReadsForNewUser } from '../../services/popular_reads' -// import { IntercomClient } from '../../utils/intercom' - -// @EventSubscriber() -// export class CreateIntercomAccount -// implements EntitySubscriberInterface -// { -// listenTo() { -// return Profile -// } - -// async afterInsert(event: InsertEvent): Promise { -// const profile = event.entity - -// const customAttributes: { source_user_id: string } = { -// source_user_id: profile.user.sourceUserId, -// } -// await IntercomClient?.contacts.createUser({ -// email: profile.user.email, -// externalId: profile.user.id, -// name: profile.user.name, -// avatar: profile.pictureUrl || undefined, -// customAttributes: customAttributes, -// signedUpAt: Math.floor(Date.now() / 1000), -// }) -// } -// } - -// @EventSubscriber() -// export class PublishNewUserEvent implements EntitySubscriberInterface { -// listenTo() { -// return Profile -// } - -// async afterInsert(event: InsertEvent): Promise { -// const client = createPubSubClient() -// await client.userCreated( -// event.entity.user.id, -// event.entity.user.email, -// event.entity.user.name, -// event.entity.username -// ) -// } -// } - -// @EventSubscriber() -// export class AddPopularReadsToNewUser -// implements EntitySubscriberInterface -// { -// listenTo() { -// return Profile -// } - -// async afterInsert(event: InsertEvent): Promise { -// await addPopularReadsForNewUser(event.entity.user.id, event.manager) -// } -// } diff --git a/packages/api/src/repository/highlight.ts b/packages/api/src/repository/highlight.ts index 543669170..488a80281 100644 --- a/packages/api/src/repository/highlight.ts +++ b/packages/api/src/repository/highlight.ts @@ -1,6 +1,6 @@ import { DeepPartial } from 'typeorm' import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity' -import { entityManager } from '.' +import { appDataSource } from '../data_source' import { Highlight } from '../entity/highlight' import { unescapeHtml } from '../utils/helpers' @@ -16,7 +16,7 @@ const unescapeHighlight = (highlight: DeepPartial) => { return highlight } -export const highlightRepository = entityManager +export const highlightRepository = appDataSource .getRepository(Highlight) .extend({ findById(id: string) { diff --git a/packages/api/src/repository/index.ts b/packages/api/src/repository/index.ts index fd2d9ed5a..7b02d12a2 100644 --- a/packages/api/src/repository/index.ts +++ b/packages/api/src/repository/index.ts @@ -22,7 +22,7 @@ export const setClaims = async ( export const authTrx = async ( fn: (manager: EntityManager) => Promise, - em = entityManager, + em = appDataSource.manager, uid?: string, userRole?: string ): Promise => { @@ -40,7 +40,5 @@ export const authTrx = async ( } export const getRepository = (entity: EntityTarget) => { - return entityManager.getRepository(entity) + return appDataSource.getRepository(entity) } - -export const entityManager = appDataSource.manager diff --git a/packages/api/src/repository/label.ts b/packages/api/src/repository/label.ts index adb90457a..d21708961 100644 --- a/packages/api/src/repository/label.ts +++ b/packages/api/src/repository/label.ts @@ -1,6 +1,6 @@ import { In } from 'typeorm' import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity' -import { entityManager } from '.' +import { appDataSource } from '../data_source' import { Label } from '../entity/label' import { generateRandomColor } from '../utils/helpers' @@ -38,7 +38,7 @@ const convertToLabel = (label: CreateLabelInput, userId: string) => { } } -export const labelRepository = entityManager.getRepository(Label).extend({ +export const labelRepository = appDataSource.getRepository(Label).extend({ findById(id: string) { return this.findOneBy({ id }) }, diff --git a/packages/api/src/repository/library_item.ts b/packages/api/src/repository/library_item.ts index b6a4ca4cf..1a428d65b 100644 --- a/packages/api/src/repository/library_item.ts +++ b/packages/api/src/repository/library_item.ts @@ -1,7 +1,7 @@ -import { entityManager } from '.' +import { appDataSource } from '../data_source' import { LibraryItem } from '../entity/library_item' -export const libraryItemRepository = entityManager +export const libraryItemRepository = appDataSource .getRepository(LibraryItem) .extend({ findById(id: string) { diff --git a/packages/api/src/repository/user.ts b/packages/api/src/repository/user.ts index a900746eb..408543c95 100644 --- a/packages/api/src/repository/user.ts +++ b/packages/api/src/repository/user.ts @@ -1,5 +1,5 @@ import { In } from 'typeorm' -import { entityManager } from '.' +import { appDataSource } from '../data_source' import { User } from './../entity/user' const TOP_USERS = [ @@ -14,7 +14,7 @@ const TOP_USERS = [ ] export const MAX_RECORDS_LIMIT = 1000 -export const userRepository = entityManager.getRepository(User).extend({ +export const userRepository = appDataSource.getRepository(User).extend({ findById(id: string) { return this.findOneBy({ id }) }, diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index c7add3e48..4648658c8 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -1,4 +1,5 @@ import { EntityManager } from 'typeorm' +import { appDataSource } from '../data_source' import { Filter } from '../entity/filter' import { GroupMembership } from '../entity/groups/group_membership' import { Invite } from '../entity/groups/invite' @@ -7,14 +8,13 @@ import { StatusType, User } from '../entity/user' import { env } from '../env' import { SignupErrorCode } from '../generated/graphql' import { createPubSubClient } from '../pubsub' -import { authTrx, entityManager, getRepository } from '../repository' +import { authTrx, getRepository } from '../repository' import { userRepository } from '../repository/user' import { AuthProvider } from '../routers/auth/auth_types' import { analytics } from '../utils/analytics' import { IntercomClient } from '../utils/intercom' import { logger } from '../utils/logger' import { validateUsername } from '../utils/usernamePolicy' -import { addPopularReadsForNewUser } from './popular_reads' import { sendConfirmationEmail } from './send_emails' export const MAX_RECORDS_LIMIT = 1000 @@ -64,7 +64,7 @@ export const createUser = async (input: { return Promise.reject({ errorCode: SignupErrorCode.InvalidUsername }) } - const [user, profile] = await entityManager.transaction<[User, Profile]>( + const [user, profile] = await appDataSource.transaction<[User, Profile]>( async (t) => { let hasInvite = false let invite: Invite | null = null @@ -103,19 +103,10 @@ export const createUser = async (input: { }) } - await createDefaultFiltersForUser(t)(user.id) - await addPopularReadsForNewUser(user.id, t) - return [user, profile] } ) - if (input.pendingConfirmation) { - if (!(await sendConfirmationEmail(user))) { - return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail }) - } - } - const customAttributes: { source_user_id: string } = { source_user_id: user.sourceUserId, } @@ -146,10 +137,16 @@ export const createUser = async (input: { }, }) + if (input.pendingConfirmation) { + if (!(await sendConfirmationEmail(user))) { + return Promise.reject({ errorCode: SignupErrorCode.InvalidEmail }) + } + } + return [user, profile] } -const createDefaultFiltersForUser = +export const createDefaultFiltersForUser = (t: EntityManager) => async (userId: string): Promise => { const defaultFilters = [ diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index 93acfe77d..e0a9b337a 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -1,8 +1,9 @@ import * as jwt from 'jsonwebtoken' import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm' +import { appDataSource } from '../data_source' import { Feature } from '../entity/feature' import { env } from '../env' -import { entityManager, getRepository } from '../repository' +import { getRepository } from '../repository' import { logger } from '../utils/logger' export enum FeatureName { @@ -41,7 +42,7 @@ const optInUltraRealisticVoice = async (uid: string): Promise => { const MAX_USERS = 1500 // opt in to feature for the first 1500 users - const optedInFeatures = (await entityManager.query( + const optedInFeatures = (await appDataSource.query( `insert into omnivore.features (user_id, name, granted_at) select $1, $2, $3 from omnivore.features where name = $2 and granted_at is not null diff --git a/packages/api/src/services/groups.ts b/packages/api/src/services/groups.ts index 7f8c70fe1..0a3de5891 100644 --- a/packages/api/src/services/groups.ts +++ b/packages/api/src/services/groups.ts @@ -1,4 +1,5 @@ import { nanoid } from 'nanoid' +import { appDataSource } from '../data_source' import { Group } from '../entity/groups/group' import { GroupMembership } from '../entity/groups/group_membership' import { Invite } from '../entity/groups/invite' @@ -6,7 +7,7 @@ import { RuleActionType } from '../entity/rule' import { User } from '../entity/user' import { homePageURL } from '../env' import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql' -import { entityManager, getRepository } from '../repository' +import { getRepository } from '../repository' import { userDataToUser } from '../utils/helpers' import { findOrCreateLabels } from './labels' import { createRule } from './rules' @@ -21,7 +22,7 @@ export const createGroup = async (input: { onlyAdminCanPost?: boolean | null onlyAdminCanSeeMembers?: boolean | null }): Promise<[Group, Invite]> => { - const [group, invite] = await entityManager.transaction<[Group, Invite]>( + const [group, invite] = await appDataSource.transaction<[Group, Invite]>( async (t) => { // Max number of groups a user can create const maxGroups = 3 @@ -113,7 +114,7 @@ export const joinGroup = async ( user: User, inviteCode: string ): Promise => { - const invite = await entityManager.transaction(async (t) => { + const invite = await appDataSource.transaction(async (t) => { // Check if the invite exists const invite = await t .getRepository(Invite) @@ -173,7 +174,7 @@ export const leaveGroup = async ( user: User, groupId: string ): Promise => { - return entityManager.transaction(async (t) => { + return appDataSource.transaction(async (t) => { const group = await t .getRepository(Group) .createQueryBuilder('group') diff --git a/packages/api/src/services/popular_reads.ts b/packages/api/src/services/popular_reads.ts index 28030be8e..c98fd9851 100644 --- a/packages/api/src/services/popular_reads.ts +++ b/packages/api/src/services/popular_reads.ts @@ -2,9 +2,10 @@ import * as httpContext from 'express-http-context2' import { readFileSync } from 'fs' import path from 'path' import { DeepPartial, EntityManager } from 'typeorm' +import { appDataSource } from '../data_source' import { LibraryItem } from '../entity/library_item' import { PageType } from '../generated/graphql' -import { authTrx, entityManager } from '../repository' +import { authTrx } from '../repository' import { libraryItemRepository } from '../repository/library_item' import { generateSlug, stringToHash, wordsCount } from '../utils/helpers' import { logger } from '../utils/logger' @@ -107,7 +108,7 @@ const addPopularReads = async ( export const addPopularReadsForNewUser = async ( userId: string, - em = entityManager + em = appDataSource.manager ): Promise => { const defaultReads = ['omnivore_organize', 'power_read_it_later'] diff --git a/packages/api/src/services/reports.ts b/packages/api/src/services/reports.ts index a0d040043..f7829dadf 100644 --- a/packages/api/src/services/reports.ts +++ b/packages/api/src/services/reports.ts @@ -1,8 +1,10 @@ import { AbuseReport } from '../entity/reports/abuse_report' import { ContentDisplayReport } from '../entity/reports/content_display_report' +import { env } from '../env' import { ReportItemInput, ReportType } from '../generated/graphql' import { authTrx, getRepository } from '../repository' import { logger } from '../utils/logger' +import { sendEmail } from '../utils/sendEmail' import { findLibraryItemById } from './library_item' export const saveContentDisplayReport = async ( @@ -18,7 +20,7 @@ export const saveContentDisplayReport = async ( // We capture the article content and original html now, in case it // reparsed or updated later, this gives us a view of exactly // what the user saw. - const result = await getRepository(ContentDisplayReport).save({ + const report = await getRepository(ContentDisplayReport).save({ user: { id: uid }, content: item.readableContent, originalHtml: item.originalContent || undefined, @@ -27,7 +29,23 @@ export const saveContentDisplayReport = async ( libraryItemId: item.id, }) - return !!result + const message = `A new content display report was created by: + ${report.user.id} for URL: ${report.originalUrl} + ${report.reportComment}` + + logger.info(message) + + if (!env.dev.isLocal) { + // If we are in the local environment, just log a message, otherwise email the report + await sendEmail({ + to: env.sender.feedback, + subject: 'New content display report', + text: message, + from: env.sender.message, + }) + } + + return !!report } export const saveAbuseReport = async ( diff --git a/packages/api/src/services/subscriptions.ts b/packages/api/src/services/subscriptions.ts index 34df424a2..8aaea89b8 100644 --- a/packages/api/src/services/subscriptions.ts +++ b/packages/api/src/services/subscriptions.ts @@ -1,8 +1,9 @@ import axios from 'axios' +import { appDataSource } from '../data_source' import { NewsletterEmail } from '../entity/newsletter_email' import { Subscription } from '../entity/subscription' import { SubscriptionStatus, SubscriptionType } from '../generated/graphql' -import { authTrx, entityManager, getRepository } from '../repository' +import { authTrx, getRepository } from '../repository' import { logger } from '../utils/logger' import { sendEmail } from '../utils/sendEmail' @@ -105,7 +106,7 @@ export const saveSubscription = async ({ } const existingSubscription = await getSubscriptionByName(name, userId) - const result = await entityManager.transaction(async (tx) => { + const result = await appDataSource.transaction(async (tx) => { if (existingSubscription) { // update subscription if already exists await tx diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index 988fb13e7..bb3457e00 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -260,15 +260,17 @@ export const enqueueParseRequest = async ({ // If there is no Google Cloud Project Id exposed, it means that we are in local environment if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) { - // Calling the handler function directly. - setTimeout(() => { - axios.post(env.queue.contentFetchUrl, payload).catch((error) => { - logError(error) - logger.error( - `Error occurred while requesting local puppeteer-parse function\nPlease, ensure your function is set up properly and running using "yarn start" from the "/pkg/gcf/puppeteer-parse" folder` - ) - }) - }, 0) + if (env.queue.contentFetchUrl) { + // Calling the handler function directly. + setTimeout(() => { + axios.post(env.queue.contentFetchUrl, payload).catch((error) => { + logError(error) + logger.error( + `Error occurred while requesting local puppeteer-parse function\nPlease, ensure your function is set up properly and running using "yarn start" from the "/pkg/gcf/puppeteer-parse" folder` + ) + }) + }, 0) + } return '' } @@ -414,12 +416,14 @@ export const enqueueTextToSpeech = async ({ const taskHandlerUrl = `${env.queue.textToSpeechTaskHandlerUrl}?token=${token}` // If there is no Google Cloud Project Id exposed, it means that we are in local environment if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) { - // Calling the handler function directly. - setTimeout(() => { - axios.post(taskHandlerUrl, payload).catch((error) => { - logError(error) - }) - }, 0) + if (env.queue.textToSpeechTaskHandlerUrl) { + // Calling the handler function directly. + setTimeout(() => { + axios.post(taskHandlerUrl, payload).catch((error) => { + logError(error) + }) + }, 0) + } return '' } const createdTasks = await createHttpTaskWithToken({ @@ -461,16 +465,18 @@ export const enqueueRecommendation = async ( } // If there is no Google Cloud Project Id exposed, it means that we are in local environment if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) { - // Calling the handler function directly. - setTimeout(() => { - axios - .post(env.queue.recommendationTaskHandlerUrl, payload, { - headers, - }) - .catch((error) => { - logError(error) - }) - }, 0) + if (env.queue.recommendationTaskHandlerUrl) { + // Calling the handler function directly. + setTimeout(() => { + axios + .post(env.queue.recommendationTaskHandlerUrl, payload, { + headers, + }) + .catch((error) => { + logError(error) + }) + }, 0) + } return '' } @@ -505,16 +511,18 @@ export const enqueueImportFromIntegration = async ( } // If there is no Google Cloud Project Id exposed, it means that we are in local environment if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) { - // Calling the handler function directly. - setTimeout(() => { - axios - .post(`${env.queue.integrationTaskHandlerUrl}/import`, payload, { - headers, - }) - .catch((error) => { - logError(error) - }) - }, 0) + if (env.queue.integrationTaskHandlerUrl) { + // Calling the handler function directly. + setTimeout(() => { + axios + .post(`${env.queue.integrationTaskHandlerUrl}/import`, payload, { + headers, + }) + .catch((error) => { + logError(error) + }) + }, 0) + } return nanoid() } @@ -552,16 +560,18 @@ export const enqueueThumbnailTask = async ( // If there is no Google Cloud Project Id exposed, it means that we are in local environment if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) { - // Calling the handler function directly. - setTimeout(() => { - axios - .post(env.queue.thumbnailTaskHandlerUrl, payload, { - headers, - }) - .catch((error) => { - logError(error) - }) - }, 0) + if (env.queue.thumbnailTaskHandlerUrl) { + // Calling the handler function directly. + setTimeout(() => { + axios + .post(env.queue.thumbnailTaskHandlerUrl, payload, { + headers, + }) + .catch((error) => { + logError(error) + }) + }, 0) + } return '' } @@ -599,16 +609,18 @@ export const enqueueRssFeedFetch = async ( // If there is no Google Cloud Project Id exposed, it means that we are in local environment if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) { - // Calling the handler function directly. - setTimeout(() => { - axios - .post(env.queue.rssFeedTaskHandlerUrl, payload, { - headers, - }) - .catch((error) => { - logError(error) - }) - }, 0) + if (env.queue.rssFeedTaskHandlerUrl) { + // Calling the handler function directly. + setTimeout(() => { + axios + .post(env.queue.rssFeedTaskHandlerUrl, payload, { + headers, + }) + .catch((error) => { + logError(error) + }) + }, 0) + } return nanoid() } diff --git a/packages/api/test/db.ts b/packages/api/test/db.ts index f37defe3a..d7ed692d9 100644 --- a/packages/api/test/db.ts +++ b/packages/api/test/db.ts @@ -6,7 +6,7 @@ import { LibraryItem } from '../src/entity/library_item' import { Reminder } from '../src/entity/reminder' import { User } from '../src/entity/user' import { UserDeviceToken } from '../src/entity/user_device_tokens' -import { entityManager, getRepository, setClaims } from '../src/repository' +import { getRepository, setClaims } from '../src/repository' import { userRepository } from '../src/repository/user' import { createUser } from '../src/services/create_user' import { saveLabelsInLibraryItem } from '../src/services/labels' @@ -37,7 +37,7 @@ export const createTestConnection = async (): Promise => { } export const deleteFiltersFromUser = async (userId: string) => { - await entityManager.transaction(async (t) => { + await appDataSource.transaction(async (t) => { await setClaims(t, userId) const filterRepo = t.getRepository(Filter)