remove knex

This commit is contained in:
Hongbo Wu 2023-08-22 22:00:48 +08:00
parent 928ed2e6a9
commit 425864ce01
90 changed files with 1431 additions and 1557 deletions

View file

@ -10,9 +10,8 @@ import { ContextFunction } from 'apollo-server-core'
import { ApolloServer } from 'apollo-server-express'
import { ExpressContext } from 'apollo-server-express/dist/ApolloServer'
import * as jwt from 'jsonwebtoken'
import { Knex } from 'knex'
import { EntityManager } from 'typeorm'
import { promisify } from 'util'
import { kx } from './datalayer/knex_config'
import { createPubSubClient } from './datalayer/pubsub'
import { sanitizeDirectiveTransformer } from './directives'
import { env } from './env'
@ -20,7 +19,7 @@ import { functionResolvers } from './resolvers/function_resolvers'
import { ClaimsToSet, ResolverContext } from './resolvers/types'
import ScalarResolvers from './scalars'
import typeDefs from './schema'
import { initModels } from './server'
import { AppDataSource } from './server'
import { tracer } from './tracing'
import { getClaimsByToken, setAuthInCookie } from './utils/auth'
import { SetClaimsRole } from './utils/dictionary'
@ -47,7 +46,7 @@ const contextFunc: ContextFunction<ExpressContext, ResolverContext> = async ({
const claims = await getClaimsByToken(token)
async function setClaims(
tx: Knex.Transaction,
em: EntityManager,
uuid?: string,
userRole?: string
): Promise<void> {
@ -55,18 +54,14 @@ const contextFunc: ContextFunction<ExpressContext, ResolverContext> = async ({
(claims && claims.uid) || uuid || '00000000-0000-0000-0000-000000000000'
const dbRole =
userRole === SetClaimsRole.ADMIN ? 'omnivore_admin' : 'omnivore_user'
return tx.raw('SELECT * from omnivore.set_claims(?, ?)', [uid, dbRole])
return em.query('SELECT * from omnivore.set_claims($1, $2)', [uid, dbRole])
}
const ctx = {
log: logger,
claims,
kx,
pubsub,
// no caching for subscriptions
// TODO: create per request caching for connections
// eslint-disable-next-line @typescript-eslint/no-explicit-any
models: initModels(kx, true),
clearAuth: () => {
res.clearCookie('auth')
res.clearCookie('pendingUserAuth')
@ -78,12 +73,12 @@ const contextFunc: ContextFunction<ExpressContext, ResolverContext> = async ({
) => await setAuthInCookie(claims, res, secret),
setClaims,
authTrx: <TResult>(
cb: (tx: Knex.Transaction) => TResult,
cb: (em: EntityManager) => TResult,
userRole?: string
): Promise<TResult> =>
kx.transaction(async (tx) => {
await setClaims(tx, undefined, userRole)
return cb(tx)
AppDataSource.transaction(async (em) => {
await setClaims(em, undefined, userRole)
return cb(em)
}),
tracingSpan: tracer.startSpan('apollo.request'),
}

View file

@ -1,5 +1,8 @@
import { EntityManager, EntityTarget, Repository } from 'typeorm'
import { AppDataSource } from '../server'
import { Reminder } from './reminder'
import { UploadFile } from './upload_file'
import { User } from './user'
export const setClaims = async (
t: EntityManager,
@ -14,3 +17,7 @@ export const setClaims = async (
export const getRepository = <T>(entity: EntityTarget<T>): Repository<T> => {
return AppDataSource.getRepository(entity)
}
export const userRepository = getRepository(User)
export const uploadFileRepository = getRepository(UploadFile)
export const reminderRepository = getRepository(Reminder)

View file

@ -3,12 +3,9 @@ import {
CreateDateColumn,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm'
import { LibraryItem } from './library_item'
import { User } from './user'
@Entity({ name: 'labels' })
@ -37,8 +34,4 @@ export class Label {
@Column('boolean', { default: false })
internal!: boolean
@ManyToMany(() => LibraryItem, (libraryItem) => libraryItem.labels)
@JoinTable()
libraryItems?: LibraryItem[]
}

View file

@ -64,7 +64,7 @@ export class LibraryItem {
originalUrl!: string
@Column('text', { nullable: true })
downloadUrl?: string
downloadUrl?: string | null
@Column('text')
slug!: string
@ -73,10 +73,10 @@ export class LibraryItem {
title!: string
@Column('text', { nullable: true })
author?: string
author?: string | null
@Column('text', { nullable: true })
description?: string
description?: string | null
@Column('timestamptz')
savedAt?: Date
@ -85,49 +85,49 @@ export class LibraryItem {
createdAt?: Date
@Column('timestamptz', { nullable: true })
publishedAt?: Date
publishedAt?: Date | null
@Column('timestamptz')
archivedAt?: Date
archivedAt?: Date | null
@Column('timestamptz')
deletedAt?: Date
deletedAt?: Date | null
@Column('timestamptz')
readAt?: Date
readAt?: Date | null
@UpdateDateColumn()
updatedAt?: Date
@Column('text', { nullable: true })
itemLanguage?: string
itemLanguage?: string | null
@Column('integer', { nullable: true })
wordCount?: number
wordCount?: number | null
@Column('text', { nullable: true })
siteName?: string
siteName?: string | null
@Column('text', { nullable: true })
siteIcon?: string
siteIcon?: string | null
@Column('json', { nullable: true })
metadata?: Record<string, unknown>
metadata?: Record<string, unknown> | null
@Column('integer', { nullable: true })
readingProgressLastReadAnchor?: number
readingProgressLastReadAnchor?: number | null
@Column('integer', { nullable: true })
readingProgressHighestReadAnchor?: number
readingProgressHighestReadAnchor?: number | null
@Column('real', { nullable: true })
readingProgressTopPercent?: number
readingProgressTopPercent?: number | null
@Column('real', { nullable: true })
readingProgressBottomPercent?: number
readingProgressBottomPercent?: number | null
@Column('text', { nullable: true })
thumbnail?: string
thumbnail?: string | null
@Column('enum', { enum: LibraryItemType, default: LibraryItemType.Unknown })
itemType?: LibraryItemType
@ -140,47 +140,29 @@ export class LibraryItem {
contentReader?: ContentReaderType
@Column('text', { nullable: true })
originalContent?: string
originalContent?: string | null
@Column('text', { nullable: true })
readableContent?: string
@Column('tsvector', { nullable: true })
contentTsv?: string
@Column('tsvector', { nullable: true })
siteTsv?: string
@Column('tsvector', { nullable: true })
titleTsv?: string
@Column('tsvector', { nullable: true })
authorTsv?: string
@Column('tsvector', { nullable: true })
descriptionTsv?: string
@Column('tsvector', { nullable: true })
searchTsv?: string
readableContent?: string | null
@Column('text', { nullable: true })
modelName?: string
modelName?: string | null
// NOT SUPPORTED IN TYPEORM
// @Column('vector', { nullable: true })
// embedding?: number[]
@Column('text', { nullable: true })
textContentHash?: string
textContentHash?: string | null
@Column('text', { nullable: true })
gcsArchiveId?: string
gcsArchiveId?: string | null
@OneToOne(() => Subscription, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'subscription_id' })
subscription?: Subscription
@ManyToMany(() => Label, (label) => label.libraryItems)
@ManyToMany(() => Label)
@JoinTable()
labels?: Label[]

View file

@ -18,11 +18,11 @@ export class Profile {
@Column('text')
username!: string
@Column('text')
bio!: string
@Column('text', { nullable: true })
bio?: string | null
@Column('text')
pictureUrl!: string
@Column('text', { nullable: true })
pictureUrl?: string | null
@OneToOne(() => User, (user) => user.profile)
@JoinColumn({ name: 'user_id' })

View file

@ -7,13 +7,23 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import { RegistrationType, StatusType } from '../datalayer/user/model'
import { Label } from './label'
import { NewsletterEmail } from './newsletter_email'
import { Profile } from './profile'
import { Label } from './label'
import { Subscription } from './subscription'
import { UserPersonalization } from './user_personalization'
export enum RegistrationType {
Google = 'GOOGLE',
Apple = 'APPLE',
Email = 'EMAIL',
}
export enum StatusType {
Active = 'ACTIVE',
Pending = 'PENDING',
}
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
@ -40,7 +50,10 @@ export class User {
@OneToMany(() => NewsletterEmail, (newsletterEmail) => newsletterEmail.user)
newsletterEmails?: NewsletterEmail[]
@OneToOne(() => Profile, (profile) => profile.user, { eager: true })
@OneToOne(() => Profile, (profile) => profile.user, {
eager: true,
cascade: true,
})
profile!: Profile
@Column('varchar', { length: 255, nullable: true })

View file

@ -26,7 +26,7 @@ export class CreateIntercomAccount
email: profile.user.email,
externalId: profile.user.id,
name: profile.user.name,
avatar: profile.pictureUrl,
avatar: profile.pictureUrl || undefined,
customAttributes: customAttributes,
signedUpAt: Math.floor(Date.now() / 1000),
})

View file

@ -1,6 +1,6 @@
import { getRepository } from '../../entity'
import { ApiKey } from '../../entity/api_key'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
ApiKeysError,

View file

@ -20,6 +20,9 @@ import {
PageType,
SearchItem as SearchItemData,
} from '../../elastic/types'
import { getRepository } from '../../entity'
import { UploadFile } from '../../entity/upload_file'
import { User } from '../../entity/user'
import { env } from '../../env'
import {
Article,
@ -42,7 +45,6 @@ import {
MutationSaveArticleReadingProgressArgs,
MutationSetBookmarkArticleArgs,
MutationSetFavoriteArticleArgs,
MutationSetShareArticleArgs,
PageInfo,
QueryArticleArgs,
QueryArticlesArgs,
@ -63,8 +65,6 @@ import {
SetFavoriteArticleError,
SetFavoriteArticleErrorCode,
SetFavoriteArticleSuccess,
SetShareArticleError,
SetShareArticleErrorCode,
SetShareArticleSuccess,
SortParams,
TypeaheadSearchError,
@ -82,6 +82,7 @@ import {
createLabels,
getLabelsByIds,
} from '../../services/labels'
import { setFileUploadComplete } from '../../services/save_file'
import { parsedContentToPage } from '../../services/save_page'
import { traceAs } from '../../tracing'
import { Merge } from '../../util'
@ -167,12 +168,7 @@ export const createArticleResolver = authorized<
},
ctx
) => {
const {
models,
authTrx,
claims: { uid },
log,
} = ctx
const { authTrx, log, uid } = ctx
analytics.track({
userId: uid,
@ -184,7 +180,18 @@ export const createArticleResolver = authorized<
},
})
const user = userDataToUser(await models.user.get(uid))
const userData = await getRepository(User).findOneBy({ id: uid })
if (!userData) {
return pageError(
{
errorCodes: [CreateArticleErrorCode.Unauthorized],
},
ctx,
pageId
)
}
const user = userDataToUser(userData)
try {
if (isSiteBlockedForParse(url)) {
return pageError(
@ -235,14 +242,17 @@ export const createArticleResolver = authorized<
},
}
// save state
let archivedAt =
const archivedAt =
state === ArticleSavingRequestStatus.Archived ? new Date() : null
if (pageId) {
const reminder = await models.reminder.getByRequestId(uid, pageId)
if (reminder && reminder.archiveUntil) {
archivedAt = new Date()
}
}
// if (pageId) {
// const reminder = await getRepository(Reminder).findOneBy({
// articleSavingRequest: pageId,
// user: { id: uid },
// })
// if (reminder && reminder.archiveUntil) {
// archivedAt = new Date()
// }
// }
// add labels to page
const labels = inputLabels
? await createLabels(ctx, inputLabels)
@ -252,9 +262,9 @@ export const createArticleResolver = authorized<
/* We do not trust the values from client, lookup upload file by querying
* with filtering on user ID and URL to verify client's uploadFileId is valid.
*/
const uploadFile = await models.uploadFile.getWhere({
const uploadFile = await getRepository(UploadFile).findOneBy({
id: uploadFileId,
userId: uid,
user: { id: uid },
})
if (!uploadFile) {
return pageError(
@ -330,7 +340,7 @@ export const createArticleResolver = authorized<
if (uploadFileId) {
const uploadFileData = await authTrx(async (tx) => {
return models.uploadFile.setFileUploadComplete(uploadFileId, tx)
return setFileUploadComplete(uploadFileId, tx)
})
if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) {
return pageError(
@ -579,64 +589,65 @@ export type SetShareArticleSuccessPartial = Merge<
}
>
export const setShareArticleResolver = authorized<
SetShareArticleSuccessPartial,
SetShareArticleError,
MutationSetShareArticleArgs
>(
async (
_,
{ input: { articleID, share, sharedComment, sharedWithHighlights } },
{ models, authTrx, claims: { uid }, log }
) => {
const article = await models.article.get(articleID)
if (!article) {
return { errorCodes: [SetShareArticleErrorCode.NotFound] }
}
// TODO: not implemented yet
// export const setShareArticleResolver = authorized<
// SetShareArticleSuccessPartial,
// SetShareArticleError,
// MutationSetShareArticleArgs
// >(
// async (
// _,
// { input: { articleID, share, sharedComment, sharedWithHighlights } },
// { models, authTrx, claims: { uid }, log }
// ) => {
// const article = await models.article.get(articleID)
// if (!article) {
// return { errorCodes: [SetShareArticleErrorCode.NotFound] }
// }
const sharedAt = share ? new Date() : null
// const sharedAt = share ? new Date() : null
log.info(`${share ? 'S' : 'Uns'}haring an article`, {
article: Object.assign({}, article, {
content: undefined,
originalHtml: undefined,
sharedAt,
}),
labels: {
source: 'resolver',
resolver: 'setShareArticleResolver',
articleId: article.id,
userId: uid,
},
})
// log.info(`${share ? 'S' : 'Uns'}haring an article`, {
// article: Object.assign({}, article, {
// content: undefined,
// originalHtml: undefined,
// sharedAt,
// }),
// labels: {
// source: 'resolver',
// resolver: 'setShareArticleResolver',
// articleId: article.id,
// userId: uid,
// },
// })
const result = await authTrx((tx) =>
models.userArticle.updateByArticleId(
uid,
articleID,
{ sharedAt, sharedComment, sharedWithHighlights },
tx
)
)
// const result = await authTrx((tx) =>
// models.userArticle.updateByArticleId(
// uid,
// articleID,
// { sharedAt, sharedComment, sharedWithHighlights },
// tx
// )
// )
if (!result) {
return { errorCodes: [SetShareArticleErrorCode.NotFound] }
}
// if (!result) {
// return { errorCodes: [SetShareArticleErrorCode.NotFound] }
// }
// Make sure article.id instead of userArticle.id has passed. We use it for cache updates
const updatedArticle = {
...result,
...article,
postedByViewer: !!sharedAt,
}
const updatedFeedArticle = sharedAt ? { ...result, sharedAt } : undefined
return {
updatedFeedArticleId: result.id,
updatedFeedArticle,
updatedArticle,
}
}
)
// // Make sure article.id instead of userArticle.id has passed. We use it for cache updates
// const updatedArticle = {
// ...result,
// ...article,
// postedByViewer: !!sharedAt,
// }
// const updatedFeedArticle = sharedAt ? { ...result, sharedAt } : undefined
// return {
// updatedFeedArticleId: result.id,
// updatedFeedArticle,
// updatedArticle,
// }
// }
// )
export type SetBookmarkArticleSuccessPartial = Merge<
SetBookmarkArticleSuccess,

View file

@ -1,7 +1,7 @@
/* eslint-disable prefer-const */
import { getPageByParam } from '../../elastic/pages'
import { getRepository } from '../../entity'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
ArticleSavingRequestError,

View file

@ -1,4 +1,8 @@
import { authorized } from '../../utils/helpers'
import { Between } from 'typeorm'
import { getRepository, setClaims } from '../../entity'
import { Filter } from '../../entity/filter'
import { User } from '../../entity/user'
import { env } from '../../env'
import {
DeleteFilterError,
DeleteFilterErrorCode,
@ -20,14 +24,11 @@ import {
UpdateFilterSuccess,
UpdateFilterErrorCode,
} from '../../generated/graphql'
import { Filter } from '../../entity/filter'
import { getRepository, setClaims } from '../../entity/utils'
import { User } from '../../entity/user'
import { AppDataSource } from '../../server'
import { Between } from 'typeorm'
import { analytics } from '../../utils/analytics'
import { env } from '../../env'
import { isNil, mergeWith } from 'lodash'
import { authorized } from '../../utils/helpers'
export const saveFilterResolver = authorized<
SaveFilterSuccess,

View file

@ -3,23 +3,21 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { getShareInfoForArticle } from '../datalayer/links/share_info'
import { getPageByParam } from '../elastic/pages'
import { getRepository } from '../entity'
import { Subscription } from '../entity/subscription'
import { UploadFile } from '../entity/upload_file'
import { User } from '../entity/user'
import {
Article,
ArticleHighlightsInput,
Highlight,
HighlightType,
LinkShareInfo,
PageType,
Reaction,
SearchItem,
User,
} from '../generated/graphql'
import { userDataToUser, validatedDate, wordsCount } from '../utils/helpers'
import { createImageProxyUrl } from '../utils/imageproxy'
import { logger } from '../utils/logger'
import {
contentReaderForPage,
generateDownloadSignedUrl,
@ -38,14 +36,14 @@ import {
createHighlightResolver,
createLabelResolver,
createNewsletterEmailResolver,
createReminderResolver,
// createReminderResolver,
deleteAccountResolver,
deleteFilterResolver,
deleteHighlightResolver,
deleteIntegrationResolver,
deleteLabelResolver,
deleteNewsletterEmailResolver,
deleteReminderResolver,
// deleteReminderResolver,
deleteRuleResolver,
deleteWebhookResolver,
deviceTokensResolver,
@ -54,11 +52,11 @@ import {
getAllUsersResolver,
getArticleResolver,
getArticlesResolver,
getFollowersResolver,
getFollowingResolver,
// getFollowersResolver,
// getFollowingResolver,
getMeUserResolver,
getSharedArticleResolver,
getUserFeedArticlesResolver,
// getSharedArticleResolver,
// getUserFeedArticlesResolver,
getUserPersonalizationResolver,
getUserResolver,
googleLoginResolver,
@ -76,7 +74,7 @@ import {
newsletterEmailsResolver,
recommendHighlightsResolver,
recommendResolver,
reminderResolver,
// reminderResolver,
reportItemResolver,
revokeApiKeyResolver,
rulesResolver,
@ -90,13 +88,13 @@ import {
setBookmarkArticleResolver,
setDeviceTokenResolver,
setFavoriteArticleResolver,
setFollowResolver,
// setFollowResolver,
setIntegrationResolver,
setLabelsForHighlightResolver,
setLabelsResolver,
setLinkArchivedResolver,
setRuleResolver,
setShareArticleResolver,
// setShareArticleResolver,
setShareHighlightResolver,
setUserPersonalizationResolver,
setWebhookResolver,
@ -107,10 +105,10 @@ import {
updateFilterResolver,
updateHighlightResolver,
updateLabelResolver,
updateLinkShareInfoResolver,
// updateLinkShareInfoResolver,
updatePageResolver,
updateReminderResolver,
updateSharedCommentResolver,
// updateReminderResolver,
// updateSharedCommentResolver,
updatesSinceResolver,
updateSubscriptionResolver,
updateUserProfileResolver,
@ -120,7 +118,6 @@ import {
webhookResolver,
webhooksResolver,
} from './index'
import { createReactionResolver, deleteReactionResolver } from './reaction'
import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails'
import { recentSearchesResolver } from './recent_searches'
import { Claims, WithDataSourcesContext } from './types'
@ -154,30 +151,30 @@ export const functionResolvers = {
updateUserProfile: updateUserProfileResolver,
createArticle: createArticleResolver,
createHighlight: createHighlightResolver,
createReaction: createReactionResolver,
deleteReaction: deleteReactionResolver,
// createReaction: createReactionResolver,
// deleteReaction: deleteReactionResolver,
mergeHighlight: mergeHighlightResolver,
updateHighlight: updateHighlightResolver,
deleteHighlight: deleteHighlightResolver,
uploadFileRequest: uploadFileRequestResolver,
setShareArticle: setShareArticleResolver,
updateSharedComment: updateSharedCommentResolver,
setFollow: setFollowResolver,
// setShareArticle: setShareArticleResolver,
// updateSharedComment: updateSharedCommentResolver,
// setFollow: setFollowResolver,
setBookmarkArticle: setBookmarkArticleResolver,
setUserPersonalization: setUserPersonalizationResolver,
createArticleSavingRequest: createArticleSavingRequestResolver,
setShareHighlight: setShareHighlightResolver,
reportItem: reportItemResolver,
updateLinkShareInfo: updateLinkShareInfoResolver,
// updateLinkShareInfo: updateLinkShareInfoResolver,
setLinkArchived: setLinkArchivedResolver,
createNewsletterEmail: createNewsletterEmailResolver,
deleteNewsletterEmail: deleteNewsletterEmailResolver,
saveUrl: saveUrlResolver,
savePage: savePageResolver,
saveFile: saveFileResolver,
createReminder: createReminderResolver,
updateReminder: updateReminderResolver,
deleteReminder: deleteReminderResolver,
// createReminder: createReminderResolver,
// updateReminder: updateReminderResolver,
// deleteReminder: deleteReminderResolver,
setDeviceToken: setDeviceTokenResolver,
createLabel: createLabelResolver,
updateLabel: updateLabelResolver,
@ -221,15 +218,15 @@ export const functionResolvers = {
users: getAllUsersResolver,
validateUsername: validateUsernameResolver,
article: getArticleResolver,
sharedArticle: getSharedArticleResolver,
// sharedArticle: getSharedArticleResolver,
articles: getArticlesResolver,
feedArticles: getUserFeedArticlesResolver,
getFollowers: getFollowersResolver,
getFollowing: getFollowingResolver,
// feedArticles: getUserFeedArticlesResolver,
// getFollowers: getFollowersResolver,
// getFollowing: getFollowingResolver,
getUserPersonalization: getUserPersonalizationResolver,
articleSavingRequest: articleSavingRequestResolver,
newsletterEmails: newsletterEmailsResolver,
reminder: reminderResolver,
// reminder: reminderResolver,
labels: labelsResolver,
search: searchResolver,
subscriptions: subscriptionsResolver,
@ -247,141 +244,141 @@ export const functionResolvers = {
groups: groupsResolver,
recentEmails: recentEmailsResolver,
},
User: {
async sharedArticles(
user: User,
__: Record<string, unknown>,
ctx: WithDataSourcesContext
) {
return ctx.models.userArticle.getUserSharedArticles(user.id, ctx.kx)
},
async sharedArticlesCount(
user: { id: string; sharedArticlesCount?: number },
__: Record<string, unknown>,
ctx: WithDataSourcesContext
) {
if (user.sharedArticlesCount) return user.sharedArticlesCount
return ctx.models.userArticle.getSharedArticlesCount(user.id, ctx.kx)
},
async sharedHighlightsCount(
user: { id: string; sharedHighlightsCount?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
// #TODO: restructure highlightStats and sharedArticlesCount in order to get it within a single query
if (user.sharedHighlightsCount) return user.sharedHighlightsCount
const { sharedHighlightsCount } =
await ctx.models.user.getSharedHighlightsStats(user.id)
return sharedHighlightsCount
},
async sharedNotesCount(
user: User,
_: unknown,
ctx: WithDataSourcesContext
) {
if (user.sharedNotesCount) return user.sharedNotesCount
const { sharedNotesCount } =
await ctx.models.user.getSharedHighlightsStats(user.id)
return sharedNotesCount
},
},
FeedArticle: {
async article(
feedArticle: { articleId: string; userId: string; article?: Article },
__: unknown,
ctx: WithDataSourcesContext
) {
if (feedArticle.article) return feedArticle.article
// User: {
// async sharedArticles(
// user: User,
// __: Record<string, unknown>,
// ctx: WithDataSourcesContext
// ) {
// return ctx.models.userArticle.getUserSharedArticles(user.id, ctx.kx)
// },
// async sharedArticlesCount(
// user: { id: string; sharedArticlesCount?: number },
// __: Record<string, unknown>,
// ctx: WithDataSourcesContext
// ) {
// if (user.sharedArticlesCount) return user.sharedArticlesCount
// return ctx.models.userArticle.getSharedArticlesCount(user.id, ctx.kx)
// },
// async sharedHighlightsCount(
// user: { id: string; sharedHighlightsCount?: number },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// // #TODO: restructure highlightStats and sharedArticlesCount in order to get it within a single query
// if (user.sharedHighlightsCount) return user.sharedHighlightsCount
// const { sharedHighlightsCount } =
// await ctx.models.user.getSharedHighlightsStats(user.id)
// return sharedHighlightsCount
// },
// async sharedNotesCount(
// user: User,
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (user.sharedNotesCount) return user.sharedNotesCount
// const { sharedNotesCount } =
// await ctx.models.user.getSharedHighlightsStats(user.id)
// return sharedNotesCount
// },
// },
// FeedArticle: {
// async article(
// feedArticle: { articleId: string; userId: string; article?: Article },
// __: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.article) return feedArticle.article
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let a: any
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// let a: any
const savedArticle =
ctx.claims?.uid &&
(await ctx.models.userArticle.getForUser(
ctx.claims?.uid,
feedArticle.articleId,
ctx.kx
))
// const savedArticle =
// ctx.claims?.uid &&
// (await ctx.models.userArticle.getForUser(
// ctx.claims?.uid,
// feedArticle.articleId,
// ctx.kx
// ))
if (savedArticle) {
// If user has saved the article, use his version (slug) then
a = {
...savedArticle,
savedByViewer: true,
postedByViewer: !!savedArticle.sharedAt,
}
} else {
a = await ctx.models.userArticle.getForUser(
feedArticle.userId,
feedArticle.articleId,
ctx.kx
)
}
// if (savedArticle) {
// // If user has saved the article, use his version (slug) then
// a = {
// ...savedArticle,
// savedByViewer: true,
// postedByViewer: !!savedArticle.sharedAt,
// }
// } else {
// a = await ctx.models.userArticle.getForUser(
// feedArticle.userId,
// feedArticle.articleId,
// ctx.kx
// )
// }
if (a && a.image) {
a.image = createImageProxyUrl(a.image, 0, 180)
} else {
logger.info(
'error getting article for feedItem',
feedArticle.userId,
feedArticle.articleId
)
}
// if (a && a.image) {
// a.image = createImageProxyUrl(a.image, 0, 180)
// } else {
// logger.info(
// 'error getting article for feedItem',
// feedArticle.userId,
// feedArticle.articleId
// )
// }
return a
},
async sharedBy(
feedArticle: { userId: string; sharedBy?: User },
__: unknown,
ctx: WithDataSourcesContext
) {
if (feedArticle.sharedBy) return feedArticle.sharedBy
return userDataToUser(await ctx.models.user.get(feedArticle.userId))
},
async highlight(
feedArticle: { highlightId?: string; highlight?: Highlight },
_: unknown,
ctx: WithDataSourcesContext
) {
if (feedArticle.highlight) return feedArticle.highlight
return feedArticle.highlightId
? await ctx.models.highlight.get(feedArticle.highlightId)
: null
},
async reactions(
feedArticle: { id: string; reactions?: Reaction[] },
_: unknown,
ctx: WithDataSourcesContext
) {
const { reactions, id } = feedArticle
if (reactions) return reactions
// return a
// },
// async sharedBy(
// feedArticle: { userId: string; sharedBy?: User },
// __: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.sharedBy) return feedArticle.sharedBy
// return userDataToUser(await ctx.models.user.get(feedArticle.userId))
// },
// async highlight(
// feedArticle: { highlightId?: string; highlight?: Highlight },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.highlight) return feedArticle.highlight
// return feedArticle.highlightId
// ? await ctx.models.highlight.get(feedArticle.highlightId)
// : null
// },
// async reactions(
// feedArticle: { id: string; reactions?: Reaction[] },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// const { reactions, id } = feedArticle
// if (reactions) return reactions
return await ctx.models.reaction.batchGetFromArticle(id)
},
async highlightsCount(
feedArticle: { id: string; highlightsCount?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
if (feedArticle.highlightsCount) return feedArticle.highlightsCount
const { highlightsCount } = await ctx.models.userArticle.getStats(
feedArticle.id
)
return highlightsCount
},
async annotationsCount(
feedArticle: { id: string; annotationsCount?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
if (feedArticle.annotationsCount) return feedArticle.annotationsCount
const { annotationsCount } = await ctx.models.userArticle.getStats(
feedArticle.id
)
return annotationsCount
},
},
// return await ctx.models.reaction.batchGetFromArticle(id)
// },
// async highlightsCount(
// feedArticle: { id: string; highlightsCount?: number },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.highlightsCount) return feedArticle.highlightsCount
// const { highlightsCount } = await ctx.models.userArticle.getStats(
// feedArticle.id
// )
// return highlightsCount
// },
// async annotationsCount(
// feedArticle: { id: string; annotationsCount?: number },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.annotationsCount) return feedArticle.annotationsCount
// const { annotationsCount } = await ctx.models.userArticle.getStats(
// feedArticle.id
// )
// return annotationsCount
// },
// },
Article: {
async url(article: Article, _: unknown, ctx: WithDataSourcesContext) {
if (
@ -390,7 +387,9 @@ export const functionResolvers = {
ctx.claims &&
article.uploadFileId
) {
const upload = await ctx.models.uploadFile.get(article.uploadFileId)
const upload = await getRepository(UploadFile).findOneBy({
id: article.uploadFileId,
})
if (!upload || !upload.fileName) {
return undefined
}
@ -490,20 +489,20 @@ export const functionResolvers = {
) {
return article.highlights || []
},
async shareInfo(
article: { id: string; sharedBy?: User; shareInfo?: LinkShareInfo },
__: unknown,
ctx: WithDataSourcesContext
): Promise<LinkShareInfo | undefined> {
if (article.shareInfo) return article.shareInfo
if (!ctx.claims?.uid) return undefined
return getShareInfoForArticle(
ctx.kx,
ctx.claims?.uid,
article.id,
ctx.models
)
},
// async shareInfo(
// article: { id: string; sharedBy?: User; shareInfo?: LinkShareInfo },
// __: unknown,
// ctx: WithDataSourcesContext
// ): Promise<LinkShareInfo | undefined> {
// if (article.shareInfo) return article.shareInfo
// if (!ctx.claims?.uid) return undefined
// return getShareInfoForArticle(
// ctx.kx,
// ctx.claims?.uid,
// article.id,
// ctx.models
// )
// },
image(article: { image?: string }): string | undefined {
return article.image && createImageProxyUrl(article.image, 320, 320)
},
@ -528,18 +527,23 @@ export const functionResolvers = {
__: unknown,
ctx: WithDataSourcesContext
) {
return userDataToUser(await ctx.models.user.get(highlight.userId))
},
async reactions(
highlight: { id: string; reactions?: Reaction[] },
_: unknown,
ctx: WithDataSourcesContext
) {
const { reactions, id } = highlight
if (reactions) return reactions
const userData = await getRepository(User).findOneBy({
id: highlight.userId,
})
if (!userData) return null
return await ctx.models.reaction.batchGetFromHighlight(id)
return userDataToUser(userData)
},
// async reactions(
// highlight: { id: string; reactions?: Reaction[] },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// const { reactions, id } = highlight
// if (reactions) return reactions
// return await ctx.models.reaction.batchGetFromHighlight(id)
// },
async createdByMe(
highlight: { userId: string; createdByMe?: boolean },
__: unknown,
@ -551,15 +555,15 @@ export const functionResolvers = {
return highlight.type || HighlightType.Highlight
},
},
Reaction: {
async user(
reaction: { userId: string },
__: unknown,
ctx: WithDataSourcesContext
) {
return userDataToUser(await ctx.models.user.get(reaction.userId))
},
},
// Reaction: {
// async user(
// reaction: { userId: string },
// __: unknown,
// ctx: WithDataSourcesContext
// ) {
// return userDataToUser(await ctx.models.user.get(reaction.userId))
// },
// },
SearchItem: {
async url(item: SearchItem, _: unknown, ctx: WithDataSourcesContext) {
if (
@ -567,7 +571,9 @@ export const functionResolvers = {
ctx.claims &&
item.uploadFileId
) {
const upload = await ctx.models.uploadFile.get(item.uploadFileId)
const upload = await getRepository(UploadFile).findOneBy({
id: item.uploadFileId,
})
if (!upload || !upload.fileName) {
return undefined
}

View file

@ -1,7 +1,7 @@
import { DateTime } from 'luxon'
import { v4 as uuidv4 } from 'uuid'
import { getRepository } from '../../entity'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
MutationUploadImportFileArgs,

View file

@ -1,6 +1,6 @@
import { getRepository } from '../../entity'
import { Integration, IntegrationType } from '../../entity/integration'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
DeleteIntegrationError,

View file

@ -8,9 +8,9 @@ import {
updateLabelsInPage,
} from '../../elastic/labels'
import { getPageById } from '../../elastic/pages'
import { getRepository, setClaims } from '../../entity'
import { Label } from '../../entity/label'
import { User } from '../../entity/user'
import { getRepository, setClaims } from '../../entity/utils'
import { env } from '../../env'
import {
CreateLabelError,

View file

@ -1,4 +1,3 @@
import { createOrUpdateLinkShareInfo } from '../../datalayer/links/share_info'
import { updatePage } from '../../elastic/pages'
import { env } from '../../env'
import {
@ -6,52 +5,48 @@ import {
ArchiveLinkErrorCode,
ArchiveLinkSuccess,
MutationSetLinkArchivedArgs,
MutationUpdateLinkShareInfoArgs,
UpdateLinkShareInfoError,
UpdateLinkShareInfoErrorCode,
UpdateLinkShareInfoSuccess,
} from '../../generated/graphql'
import { analytics } from '../../utils/analytics'
import { authorized } from '../../utils/helpers'
export const updateLinkShareInfoResolver = authorized<
UpdateLinkShareInfoSuccess,
UpdateLinkShareInfoError,
MutationUpdateLinkShareInfoArgs
>(async (_obj, args, { models, claims, authTrx, log }) => {
const { title, description } = args.input
// export const updateLinkShareInfoResolver = authorized<
// UpdateLinkShareInfoSuccess,
// UpdateLinkShareInfoError,
// MutationUpdateLinkShareInfoArgs
// >(async (_obj, args, { models, claims, authTrx, log }) => {
// const { title, description } = args.input
log.info('updateLinkShareInfoResolver', args.input.linkId, title, description)
// log.info('updateLinkShareInfoResolver', args.input.linkId, title, description)
// TEMP: because the old API uses articles instead of Links, we are actually
// getting an article ID here and need to map it to a link ID. When the API
// is updated to use Links instead of Articles this will be removed.
const link = await authTrx((tx) =>
models.userArticle.getByArticleId(claims.uid, args.input.linkId, tx)
)
// // TEMP: because the old API uses articles instead of Links, we are actually
// // getting an article ID here and need to map it to a link ID. When the API
// // is updated to use Links instead of Articles this will be removed.
// const link = await authTrx((tx) =>
// models.userArticle.getByArticleId(claims.uid, args.input.linkId, tx)
// )
if (!link?.id) {
return {
__typename: 'UpdateLinkShareInfoError',
errorCodes: [UpdateLinkShareInfoErrorCode.Unauthorized],
}
}
// if (!link?.id) {
// return {
// __typename: 'UpdateLinkShareInfoError',
// errorCodes: [UpdateLinkShareInfoErrorCode.Unauthorized],
// }
// }
const result = await authTrx((tx) =>
createOrUpdateLinkShareInfo(tx, link.id, title, description)
)
if (!result) {
return {
__typename: 'UpdateLinkShareInfoError',
errorCodes: [UpdateLinkShareInfoErrorCode.BadRequest],
}
}
// const result = await authTrx((tx) =>
// createOrUpdateLinkShareInfo(tx, link.id, title, description)
// )
// if (!result) {
// return {
// __typename: 'UpdateLinkShareInfoError',
// errorCodes: [UpdateLinkShareInfoErrorCode.BadRequest],
// }
// }
return {
__typename: 'UpdateLinkShareInfoSuccess',
message: 'Updated Share Information',
}
})
// return {
// __typename: 'UpdateLinkShareInfoSuccess',
// message: 'Updated Share Information',
// }
// })
export const setLinkArchivedResolver = authorized<
ArchiveLinkSuccess,

View file

@ -1,6 +1,6 @@
import { getRepository } from '../../entity'
import { NewsletterEmail } from '../../entity/newsletter_email'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
CreateNewsletterEmailError,

View file

@ -1,15 +1,5 @@
import { Merge } from '../../util'
import { authorized } from '../../utils/helpers'
import {
CreateReactionError,
CreateReactionErrorCode,
CreateReactionSuccess,
DeleteReactionError,
DeleteReactionErrorCode,
MutationCreateReactionArgs,
MutationDeleteReactionArgs,
Reaction,
} from './../../generated/graphql'
import { CreateReactionSuccess, Reaction } from './../../generated/graphql'
export type PartialReaction = Omit<Reaction, 'user'>
@ -18,125 +8,125 @@ export type PartialCreateReactionSuccess = Merge<
{ reaction: PartialReaction }
>
export const createReactionResolver = authorized<
PartialCreateReactionSuccess,
CreateReactionError,
MutationCreateReactionArgs
>(async (_, { input }, { models, claims, log, authTrx }) => {
const { userArticleId, highlightId } = input
// export const createReactionResolver = authorized<
// PartialCreateReactionSuccess,
// CreateReactionError,
// MutationCreateReactionArgs
// >(async (_, { input }, { models, claims, log, authTrx }) => {
// const { userArticleId, highlightId } = input
if ((!userArticleId && !highlightId) || (userArticleId && highlightId)) {
// One reaction target is required
// Higlight replies hasn't supported yet
return {
errorCodes: [CreateReactionErrorCode.BadTarget],
}
}
// if ((!userArticleId && !highlightId) || (userArticleId && highlightId)) {
// // One reaction target is required
// // Higlight replies hasn't supported yet
// return {
// errorCodes: [CreateReactionErrorCode.BadTarget],
// }
// }
if (input.code && input.code.length > 50) {
return {
errorCodes: [CreateReactionErrorCode.BadCode],
}
}
// if (input.code && input.code.length > 50) {
// return {
// errorCodes: [CreateReactionErrorCode.BadCode],
// }
// }
if (userArticleId) {
if (!(await models.userArticle.get(userArticleId))) {
return {
errorCodes: [CreateReactionErrorCode.BadTarget],
}
}
} else if (highlightId) {
if (!(await models.highlight.get(highlightId))) {
return {
errorCodes: [CreateReactionErrorCode.BadTarget],
}
}
}
// if (userArticleId) {
// if (!(await models.userArticle.get(userArticleId))) {
// return {
// errorCodes: [CreateReactionErrorCode.BadTarget],
// }
// }
// } else if (highlightId) {
// if (!(await models.highlight.get(highlightId))) {
// return {
// errorCodes: [CreateReactionErrorCode.BadTarget],
// }
// }
// }
try {
const previousReaction = userArticleId
? await models.reaction.getByUserAndParam(claims.uid, { userArticleId })
: await models.reaction.getByUserAndParam(claims.uid, { highlightId })
// try {
// const previousReaction = userArticleId
// ? await models.reaction.getByUserAndParam(claims.uid, { userArticleId })
// : await models.reaction.getByUserAndParam(claims.uid, { highlightId })
let reaction
if (!previousReaction) {
reaction = await authTrx((tx) =>
models.reaction.create({ ...input, userId: claims.uid }, tx)
)
} else {
reaction = await authTrx((tx) =>
models.reaction.update(
previousReaction.id,
{
code: input.code,
},
tx
)
)
}
// let reaction
// if (!previousReaction) {
// reaction = await authTrx((tx) =>
// models.reaction.create({ ...input, userId: claims.uid }, tx)
// )
// } else {
// reaction = await authTrx((tx) =>
// models.reaction.update(
// previousReaction.id,
// {
// code: input.code,
// },
// tx
// )
// )
// }
if (!reaction) {
return {
errorCodes: [CreateReactionErrorCode.NotFound],
}
}
log.info(`${previousReaction ? 'Updating' : 'Creating'} a new reaction`, {
reaction,
labels: {
source: 'resolver',
resolver: 'createReactionResolver',
uid: claims.uid,
},
})
// if (!reaction) {
// return {
// errorCodes: [CreateReactionErrorCode.NotFound],
// }
// }
// log.info(`${previousReaction ? 'Updating' : 'Creating'} a new reaction`, {
// reaction,
// labels: {
// source: 'resolver',
// resolver: 'createReactionResolver',
// uid: claims.uid,
// },
// })
return {
reaction: reaction as PartialReaction,
}
} catch (err) {
log.info(err)
return {
errorCodes: [CreateReactionErrorCode.NotFound],
}
}
})
// return {
// reaction: reaction as PartialReaction,
// }
// } catch (err) {
// log.info(err)
// return {
// errorCodes: [CreateReactionErrorCode.NotFound],
// }
// }
// })
export const deleteReactionResolver = authorized<
PartialCreateReactionSuccess,
DeleteReactionError,
MutationDeleteReactionArgs
>(async (_, { id }, { authTrx, models, claims, log }) => {
const reaction = await models.reaction.get(id)
// export const deleteReactionResolver = authorized<
// PartialCreateReactionSuccess,
// DeleteReactionError,
// MutationDeleteReactionArgs
// >(async (_, { id }, { authTrx, models, claims, log }) => {
// const reaction = await models.reaction.get(id)
if (!reaction?.id) {
return {
errorCodes: [DeleteReactionErrorCode.NotFound],
}
}
// if (!reaction?.id) {
// return {
// errorCodes: [DeleteReactionErrorCode.NotFound],
// }
// }
if (reaction.userId !== claims.uid) {
return {
errorCodes: [DeleteReactionErrorCode.Forbidden],
}
}
// if (reaction.userId !== claims.uid) {
// return {
// errorCodes: [DeleteReactionErrorCode.Forbidden],
// }
// }
const deleted = await authTrx((tx) => models.reaction.delete(id, tx))
// const deleted = await authTrx((tx) => models.reaction.delete(id, tx))
if ('error' in deleted) {
return {
errorCodes: [DeleteReactionErrorCode.NotFound],
}
}
// if ('error' in deleted) {
// return {
// errorCodes: [DeleteReactionErrorCode.NotFound],
// }
// }
log.info('Deleting a highlight', {
deleted,
labels: {
source: 'resolver',
resolver: 'deleteHighlightResolver',
uid: claims.uid,
},
})
// log.info('Deleting a highlight', {
// deleted,
// labels: {
// source: 'resolver',
// resolver: 'deleteHighlightResolver',
// uid: claims.uid,
// },
// })
return {
reaction: reaction as PartialReaction,
}
})
// return {
// reaction: reaction as PartialReaction,
// }
// })

View file

@ -1,7 +1,7 @@
import { ILike } from 'typeorm'
import { getRepository } from '../../entity'
import { NewsletterEmail } from '../../entity/newsletter_email'
import { ReceivedEmail } from '../../entity/received_email'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
MarkEmailAsItemError,

View file

@ -1,14 +1,14 @@
import { authorized } from '../../utils/helpers'
import { getRepository } from '../../entity'
import { User } from '../../entity/user'
import { env } from '../../env'
import {
RecentSearchesError,
RecentSearchesErrorCode,
RecentSearchesSuccess,
} from '../../generated/graphql'
import { analytics } from '../../utils/analytics'
import { env } from '../../env'
import { getRepository } from '../../entity/utils'
import { User } from '../../entity/user'
import { getRecentSearches } from '../../services/search_history'
import { analytics } from '../../utils/analytics'
import { authorized } from '../../utils/helpers'
export const recentSearchesResolver = authorized<
RecentSearchesSuccess,

View file

@ -1,8 +1,8 @@
import { In } from 'typeorm'
import { getPageByParam } from '../../elastic/pages'
import { getRepository } from '../../entity'
import { Group } from '../../entity/groups/group'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
CreateGroupError,

View file

@ -1,31 +1,4 @@
import { DateTime } from 'luxon'
import { getPageById } from '../../elastic/pages'
import { Page } from '../../elastic/types'
import { env } from '../../env'
import {
CreateReminderError,
CreateReminderErrorCode,
CreateReminderSuccess,
DeleteReminderError,
DeleteReminderErrorCode,
DeleteReminderSuccess,
MutationCreateReminderArgs,
MutationDeleteReminderArgs,
MutationUpdateReminderArgs,
QueryReminderArgs,
ReminderError,
ReminderErrorCode,
ReminderSuccess,
UpdateReminderError,
UpdateReminderErrorCode,
UpdateReminderSuccess,
} from '../../generated/graphql'
import { setLinkArchived } from '../../services/archive_link'
import { analytics } from '../../utils/analytics'
import { deleteTask, enqueueReminder } from '../../utils/createTask'
import { authorized } from '../../utils/helpers'
import { logger } from '../../utils/logger'
import { DataModels } from '../types'
const validScheduleTime = (str: string): Date | undefined => {
const scheduleTime = DateTime.fromISO(str, { setZone: true }).set({
@ -39,320 +12,320 @@ const validScheduleTime = (str: string): Date | undefined => {
return scheduleTime.toJSDate()
}
export const createReminderResolver = authorized<
CreateReminderSuccess,
CreateReminderError,
MutationCreateReminderArgs
>(async (_, { input }, { models, claims: { uid }, log }) => {
log.info('createReminderResolver')
// export const createReminderResolver = authorized<
// CreateReminderSuccess,
// CreateReminderError,
// MutationCreateReminderArgs
// >(async (_, { input }, { models, claims: { uid }, log }) => {
// log.info('createReminderResolver')
const { clientRequestId, linkId, archiveUntil, sendNotification } = input
// const { clientRequestId, linkId, archiveUntil, sendNotification } = input
const scheduledTime = validScheduleTime(input.remindAt)
if (!scheduledTime) {
log.error('Invalid scheduled time', input.remindAt)
return {
errorCodes: [CreateReminderErrorCode.BadRequest],
}
}
// const scheduledTime = validScheduleTime(input.remindAt)
// if (!scheduledTime) {
// log.error('Invalid scheduled time', input.remindAt)
// return {
// errorCodes: [CreateReminderErrorCode.BadRequest],
// }
// }
const pageId = linkId || clientRequestId
if (!pageId) {
log.error('client request id or link id is required')
return {
errorCodes: [CreateReminderErrorCode.BadRequest],
}
}
// const pageId = linkId || clientRequestId
// if (!pageId) {
// log.error('client request id or link id is required')
// return {
// errorCodes: [CreateReminderErrorCode.BadRequest],
// }
// }
analytics.track({
userId: uid,
event: 'reminder_created',
properties: {
clientRequestId,
remindAt: scheduledTime,
archiveUntil,
sendNotification,
linkId,
env: env.server.apiEnv,
},
})
// analytics.track({
// userId: uid,
// event: 'reminder_created',
// properties: {
// clientRequestId,
// remindAt: scheduledTime,
// archiveUntil,
// sendNotification,
// linkId,
// env: env.server.apiEnv,
// },
// })
try {
// saving from web
const page = await getPageById(pageId)
if (!page) {
log.error('page not found', pageId)
// try {
// // saving from web
// const page = await getPageById(pageId)
// if (!page) {
// log.error('page not found', pageId)
return {
errorCodes: [CreateReminderErrorCode.NotFound],
}
}
// return {
// errorCodes: [CreateReminderErrorCode.NotFound],
// }
// }
if (page.userId !== uid) {
log.error('user not authorized', uid)
// if (page.userId !== uid) {
// log.error('user not authorized', uid)
return {
errorCodes: [CreateReminderErrorCode.Unauthorized],
}
}
if (archiveUntil) {
await archivePage(uid, page)
}
// return {
// errorCodes: [CreateReminderErrorCode.Unauthorized],
// }
// }
// if (archiveUntil) {
// await archivePage(uid, page)
// }
const taskName = await groupReminders(scheduledTime, uid, models)
log.info('scheduled task name', taskName)
// const taskName = await groupReminders(scheduledTime, uid, models)
// log.info('scheduled task name', taskName)
// insert reminder to db
const reminder = await models.reminder.create({
userId: uid,
taskName: taskName,
archiveUntil: archiveUntil,
sendNotification: sendNotification,
createdAt: new Date(),
remindAt: scheduledTime,
elasticPageId: pageId,
})
log.info('created reminder', reminder)
// // insert reminder to db
// const reminder = await models.reminder.create({
// userId: uid,
// taskName: taskName,
// archiveUntil: archiveUntil,
// sendNotification: sendNotification,
// createdAt: new Date(),
// remindAt: scheduledTime,
// elasticPageId: pageId,
// })
// log.info('created reminder', reminder)
return {
reminder: {
id: reminder.id,
archiveUntil,
sendNotification,
remindAt: scheduledTime,
},
}
} catch (e) {
log.info('error creating reminder', e)
// return {
// reminder: {
// id: reminder.id,
// archiveUntil,
// sendNotification,
// remindAt: scheduledTime,
// },
// }
// } catch (e) {
// log.info('error creating reminder', e)
return {
errorCodes: [CreateReminderErrorCode.BadRequest],
}
}
})
// return {
// errorCodes: [CreateReminderErrorCode.BadRequest],
// }
// }
// })
// Attempts to find a link and archive it if it exists.
// It is possible that the link has not been created
// yet if it is still in the saving process. In that
// case it will be archived when the link is created.
const archivePage = async (uid: string, page: Page) => {
try {
await setLinkArchived(uid, page.id, true)
} catch (e) {
logger.info('error archiving link', e)
}
}
// // Attempts to find a link and archive it if it exists.
// // It is possible that the link has not been created
// // yet if it is still in the saving process. In that
// // case it will be archived when the link is created.
// const archivePage = async (uid: string, page: Page) => {
// try {
// await setLinkArchived(uid, page.id, true)
// } catch (e) {
// logger.info('error archiving link', e)
// }
// }
export const reminderResolver = authorized<
ReminderSuccess,
ReminderError,
QueryReminderArgs
>(async (_, { linkId: pageId }, { models, claims: { uid }, log }) => {
log.info('reminderResolver')
// export const reminderResolver = authorized<
// ReminderSuccess,
// ReminderError,
// QueryReminderArgs
// >(async (_, { linkId: pageId }, { models, claims: { uid }, log }) => {
// log.info('reminderResolver')
analytics.track({
userId: uid,
event: 'reminder',
properties: {
linkId: pageId,
env: env.server.apiEnv,
},
})
// analytics.track({
// userId: uid,
// event: 'reminder',
// properties: {
// linkId: pageId,
// env: env.server.apiEnv,
// },
// })
try {
// get page from articleId
const page = await getPageById(pageId)
if (!page) {
return {
errorCodes: [ReminderErrorCode.NotFound],
}
}
if (page.userId !== uid) {
return {
errorCodes: [ReminderErrorCode.Unauthorized],
}
}
const reminder = await models.reminder.getCreatedByParameters(uid, {
elasticPageId: page.id,
})
// try {
// // get page from articleId
// const page = await getPageById(pageId)
// if (!page) {
// return {
// errorCodes: [ReminderErrorCode.NotFound],
// }
// }
// if (page.userId !== uid) {
// return {
// errorCodes: [ReminderErrorCode.Unauthorized],
// }
// }
// const reminder = await models.reminder.getCreatedByParameters(uid, {
// elasticPageId: page.id,
// })
if (!reminder) {
log.error('reminder not found: pageId: ', pageId)
// if (!reminder) {
// log.error('reminder not found: pageId: ', pageId)
return {
errorCodes: [ReminderErrorCode.NotFound],
}
}
// return {
// errorCodes: [ReminderErrorCode.NotFound],
// }
// }
return {
reminder: {
id: reminder.id,
archiveUntil: reminder.archiveUntil || false,
sendNotification: reminder.sendNotification || true,
remindAt: reminder.remindAt,
},
}
} catch (e) {
log.error(e)
// return {
// reminder: {
// id: reminder.id,
// archiveUntil: reminder.archiveUntil || false,
// sendNotification: reminder.sendNotification || true,
// remindAt: reminder.remindAt,
// },
// }
// } catch (e) {
// log.error(e)
return {
errorCodes: [ReminderErrorCode.BadRequest],
}
}
})
// return {
// errorCodes: [ReminderErrorCode.BadRequest],
// }
// }
// })
export const updateReminderResolver = authorized<
UpdateReminderSuccess,
UpdateReminderError,
MutationUpdateReminderArgs
>(async (_, { input }, { models, claims: { uid }, log, authTrx }) => {
log.info('updateReminderResolver')
// export const updateReminderResolver = authorized<
// UpdateReminderSuccess,
// UpdateReminderError,
// MutationUpdateReminderArgs
// >(async (_, { input }, { models, claims: { uid }, log, authTrx }) => {
// log.info('updateReminderResolver')
const { id, archiveUntil, sendNotification } = input
// const { id, archiveUntil, sendNotification } = input
const scheduledTime = validScheduleTime(input.remindAt)
if (!scheduledTime) {
log.error('Invalid scheduled time', input.remindAt)
return {
errorCodes: [UpdateReminderErrorCode.BadRequest],
}
}
// const scheduledTime = validScheduleTime(input.remindAt)
// if (!scheduledTime) {
// log.error('Invalid scheduled time', input.remindAt)
// return {
// errorCodes: [UpdateReminderErrorCode.BadRequest],
// }
// }
analytics.track({
userId: uid,
event: 'reminder_updated',
properties: {
id,
remindAt: scheduledTime,
archiveUntil,
sendNotification,
env: env.server.apiEnv,
},
})
// analytics.track({
// userId: uid,
// event: 'reminder_updated',
// properties: {
// id,
// remindAt: scheduledTime,
// archiveUntil,
// sendNotification,
// env: env.server.apiEnv,
// },
// })
try {
const reminder = await models.reminder.getCreated(id)
// try {
// const reminder = await models.reminder.getCreated(id)
if (!reminder) {
log.error('reminder not found:', id)
// if (!reminder) {
// log.error('reminder not found:', id)
return {
errorCodes: [UpdateReminderErrorCode.NotFound],
}
}
// return {
// errorCodes: [UpdateReminderErrorCode.NotFound],
// }
// }
if (reminder.userId !== uid) {
return {
errorCodes: [UpdateReminderErrorCode.Unauthorized],
}
}
// if (reminder.userId !== uid) {
// return {
// errorCodes: [UpdateReminderErrorCode.Unauthorized],
// }
// }
// delete old google cloud task
if (reminder.taskName) {
await deleteTask(reminder.taskName)
}
// // delete old google cloud task
// if (reminder.taskName) {
// await deleteTask(reminder.taskName)
// }
const taskName = await groupReminders(scheduledTime, uid, models)
// const taskName = await groupReminders(scheduledTime, uid, models)
// update db
await authTrx((tx) =>
models.reminder.update(
id,
{
taskName: taskName,
archiveUntil,
sendNotification,
remindAt: scheduledTime,
},
tx
)
)
// // update db
// await authTrx((tx) =>
// models.reminder.update(
// id,
// {
// taskName: taskName,
// archiveUntil,
// sendNotification,
// remindAt: scheduledTime,
// },
// tx
// )
// )
return {
reminder: {
id: reminder.id,
archiveUntil,
sendNotification,
remindAt: scheduledTime,
},
}
} catch (e) {
log.error(e)
// return {
// reminder: {
// id: reminder.id,
// archiveUntil,
// sendNotification,
// remindAt: scheduledTime,
// },
// }
// } catch (e) {
// log.error(e)
return {
errorCodes: [UpdateReminderErrorCode.BadRequest],
}
}
})
// return {
// errorCodes: [UpdateReminderErrorCode.BadRequest],
// }
// }
// })
export const deleteReminderResolver = authorized<
DeleteReminderSuccess,
DeleteReminderError,
MutationDeleteReminderArgs
>(async (_, { id }, { models, claims: { uid }, log, authTrx }) => {
log.info('deleteReminderResolver')
// export const deleteReminderResolver = authorized<
// DeleteReminderSuccess,
// DeleteReminderError,
// MutationDeleteReminderArgs
// >(async (_, { id }, { models, claims: { uid }, log, authTrx }) => {
// log.info('deleteReminderResolver')
analytics.track({
userId: uid,
event: 'reminder_deleted',
properties: {
id: id,
env: env.server.apiEnv,
},
})
// analytics.track({
// userId: uid,
// event: 'reminder_deleted',
// properties: {
// id: id,
// env: env.server.apiEnv,
// },
// })
try {
const reminder = await models.reminder.getCreated(id)
// try {
// const reminder = await models.reminder.getCreated(id)
if (!reminder) {
log.error('reminder not found:', id)
// if (!reminder) {
// log.error('reminder not found:', id)
return {
errorCodes: [DeleteReminderErrorCode.NotFound],
}
}
// return {
// errorCodes: [DeleteReminderErrorCode.NotFound],
// }
// }
if (reminder.userId !== uid) {
return {
errorCodes: [DeleteReminderErrorCode.Unauthorized],
}
}
// if (reminder.userId !== uid) {
// return {
// errorCodes: [DeleteReminderErrorCode.Unauthorized],
// }
// }
// update db
await authTrx((tx) => models.reminder.delete(id, tx))
// // update db
// await authTrx((tx) => models.reminder.delete(id, tx))
return {
reminder: {
id: reminder.id,
archiveUntil: reminder.archiveUntil || false,
sendNotification: reminder.sendNotification || true,
remindAt: reminder.remindAt,
},
}
} catch (e) {
log.error(e)
// return {
// reminder: {
// id: reminder.id,
// archiveUntil: reminder.archiveUntil || false,
// sendNotification: reminder.sendNotification || true,
// remindAt: reminder.remindAt,
// },
// }
// } catch (e) {
// log.error(e)
return {
errorCodes: [DeleteReminderErrorCode.BadRequest],
}
}
})
// return {
// errorCodes: [DeleteReminderErrorCode.BadRequest],
// }
// }
// })
// check if there exists reminders for the same user at the same time
// create a Google cloud task if no existing task and return task name
const groupReminders = async (
scheduledTime: Date,
userId: string,
models: DataModels
): Promise<string | undefined> => {
const exists = await models.reminder.existByUserAndRemindAt(
userId,
scheduledTime
)
// // check if there exists reminders for the same user at the same time
// // create a Google cloud task if no existing task and return task name
// const groupReminders = async (
// scheduledTime: Date,
// userId: string,
// models: DataModels
// ): Promise<string | undefined> => {
// const exists = await models.reminder.existByUserAndRemindAt(
// userId,
// scheduledTime
// )
if (!exists) {
return enqueueReminder(userId, scheduledTime.getTime())
}
// if (!exists) {
// return enqueueReminder(userId, scheduledTime.getTime())
// }
return undefined
}
// return undefined
// }

View file

@ -1,4 +1,6 @@
import { authorized } from '../../utils/helpers'
import { getRepository } from '../../entity'
import { Rule } from '../../entity/rule'
import { User } from '../../entity/user'
import {
DeleteRuleError,
DeleteRuleErrorCode,
@ -13,9 +15,7 @@ import {
SetRuleErrorCode,
SetRuleSuccess,
} from '../../generated/graphql'
import { getRepository } from '../../entity/utils'
import { User } from '../../entity/user'
import { Rule } from '../../entity/rule'
import { authorized } from '../../utils/helpers'
export const setRuleResolver = authorized<
SetRuleSuccess,

View file

@ -1,5 +1,5 @@
import { getRepository } from '../../entity'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
MutationSaveFileArgs,
@ -13,19 +13,15 @@ import { saveFile } from '../../services/save_file'
import { savePage } from '../../services/save_page'
import { saveUrl } from '../../services/save_url'
import { analytics } from '../../utils/analytics'
import { authorized, userDataToUser } from '../../utils/helpers'
import { authorized } from '../../utils/helpers'
export const savePageResolver = authorized<
SaveSuccess,
SaveError,
MutationSavePageArgs
>(async (_, { input }, ctx) => {
const {
models,
claims: { uid },
} = ctx
analytics.track({
userId: uid,
userId: ctx.uid,
event: 'link_saved',
properties: {
url: input.url,
@ -35,16 +31,14 @@ export const savePageResolver = authorized<
},
})
const user = userDataToUser(await models.user.get(uid))
const user = await getRepository(User).findOneBy({
id: ctx.uid,
})
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
return savePage(
{ ...ctx, uid, refresh: true },
{ userId: user.id, username: user.profile.username },
input
)
return savePage(ctx, user, input)
})
export const saveUrlResolver = authorized<
@ -74,7 +68,7 @@ export const saveUrlResolver = authorized<
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
return (await saveUrl({ ...ctx, uid }, user, input)) as SaveSuccess
return saveUrl(ctx, user, input)
})
export const saveFileResolver = authorized<
@ -82,13 +76,8 @@ export const saveFileResolver = authorized<
SaveError,
MutationSaveFileArgs
>(async (_, { input }, ctx) => {
const {
models,
claims: { uid },
} = ctx
analytics.track({
userId: uid,
userId: ctx.uid,
event: 'link_saved',
properties: {
url: input.url,
@ -98,10 +87,12 @@ export const saveFileResolver = authorized<
},
})
const user = userDataToUser(await models.user.get(uid))
const user = await getRepository(User).findOneBy({
id: ctx.uid,
})
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
return (await saveFile({ ...ctx, uid }, user, input)) as SaveSuccess
return saveFile(ctx, user, input)
})

View file

@ -1,8 +1,8 @@
import Parser from 'rss-parser'
import { Brackets } from 'typeorm'
import { getRepository } from '../../entity'
import { Subscription } from '../../entity/subscription'
import { User } from '../../entity/user'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import {
MutationSubscribeArgs,

View file

@ -1,20 +1,10 @@
/* eslint-disable @typescript-eslint/ban-types */
import { Context as ApolloContext } from 'apollo-server-core'
import winston from 'winston'
import { Knex } from 'knex'
import UserModel from '../datalayer/user'
import ArticleModel from '../datalayer/article'
import UserArticleModel from '../datalayer/links'
import UserFriendModel from '../datalayer/user_friends'
import UserPersonalizationModel from '../datalayer/user_personalization'
import ArticleSavingRequestModel from '../datalayer/article_saving_request'
import UploadFileDataModel from '../datalayer/upload_files'
import * as jwt from 'jsonwebtoken'
import { Span } from '@opentelemetry/api'
import HighlightModel from '../datalayer/highlight'
import ReactionModel from '../datalayer/reaction'
import { Context as ApolloContext } from 'apollo-server-core'
import * as jwt from 'jsonwebtoken'
import { EntityManager } from 'typeorm'
import winston from 'winston'
import { PubsubClient } from '../datalayer/pubsub'
import ReminderModel from '../datalayer/reminders'
export interface Claims {
uid: string
@ -30,35 +20,19 @@ export type ClaimsToSet = {
userRole?: string | null
}
export type DataModels = {
user: UserModel
article: ArticleModel
userArticle: UserArticleModel
userFriends: UserFriendModel
userPersonalization: UserPersonalizationModel
articleSavingRequest: ArticleSavingRequestModel
uploadFile: UploadFileDataModel
highlight: HighlightModel
reaction: ReactionModel
reminder: ReminderModel
}
export interface RequestContext {
log: winston.Logger
claims: Claims | undefined
kx: Knex
pubsub: PubsubClient
models: DataModels
setAuth: (claims: ClaimsToSet, secret?: string) => Promise<void>
clearAuth: () => void
setClaims: (tx: Knex.Transaction, uuid?: string | undefined) => Promise<void>
// eslint-disable-next-line @typescript-eslint/ban-types
setClaims: (em: EntityManager, uuid?: string | undefined) => Promise<void>
signToken: (
arg1: string | object | Buffer,
arg2: jwt.Secret
) => Promise<unknown>
authTrx: <TResult>(
cb: (tx: Knex.Transaction) => TResult,
cb: (em: EntityManager) => TResult,
userRole?: string
) => Promise<TResult>
tracingSpan: Span

View file

@ -3,24 +3,25 @@ import normalizeUrl from 'normalize-url'
import path from 'path'
import { createPage, getPageByParam, updatePage } from '../../elastic/pages'
import { PageType } from '../../elastic/types'
import { uploadFileRepository } from '../../entity'
import { UploadFile } from '../../entity/upload_file'
import { env } from '../../env'
import {
ArticleSavingRequestStatus,
MutationUploadFileRequestArgs,
ResolverFn,
UploadFileRequestError,
UploadFileRequestErrorCode,
UploadFileRequestResult,
UploadFileRequestSuccess,
UploadFileStatus,
} from '../../generated/graphql'
import { validateUrl } from '../../services/create_page_save_request'
import { analytics } from '../../utils/analytics'
import { generateSlug } from '../../utils/helpers'
import { authorized, generateSlug } from '../../utils/helpers'
import {
generateUploadFilePathName,
generateUploadSignedUrl,
getFilePublicUrl,
} from '../../utils/uploads'
import { WithDataSourcesContext } from '../types'
const isFileUrl = (url: string): boolean => {
const parsedUrl = new URL(url)
@ -34,23 +35,18 @@ export const pageTypeForContentType = (contentType: string): PageType => {
return PageType.File
}
export const uploadFileRequestResolver: ResolverFn<
UploadFileRequestResult,
unknown,
WithDataSourcesContext,
export const uploadFileRequestResolver = authorized<
UploadFileRequestSuccess,
UploadFileRequestError,
MutationUploadFileRequestArgs
> = async (_obj, { input }, ctx) => {
const { models, authTrx, claims, log } = ctx
>(async (_, { input }, ctx) => {
const { authTrx, uid, log } = ctx
let uploadFileData: { id: string | null } = {
id: null,
}
if (!claims?.uid) {
return { errorCodes: [UploadFileRequestErrorCode.Unauthorized] }
}
analytics.track({
userId: claims.uid,
userId: uid,
event: 'file_upload_request',
properties: {
url: input.url,
@ -89,10 +85,10 @@ export const uploadFileRequestResolver: ResolverFn<
return { errorCodes: [UploadFileRequestErrorCode.BadInput] }
}
uploadFileData = await models.uploadFile.create({
uploadFileData = await uploadFileRepository.save({
url: input.url,
userId: claims.uid,
fileName: fileName,
userId: uid,
fileName,
status: UploadFileStatus.Initialized,
contentType: input.contentType,
})
@ -113,14 +109,10 @@ export const uploadFileRequestResolver: ResolverFn<
// If this is a file URL, we swap in the GCS public URL
if (isFileUrl(input.url)) {
await authTrx(async (tx) => {
await models.uploadFile.update(
uploadFileId,
{
url: publicUrl,
status: UploadFileStatus.Initialized,
},
tx
)
await tx.getRepository(UploadFile).update(uploadFileId, {
url: publicUrl,
status: UploadFileStatus.Initialized,
})
})
}
@ -131,7 +123,7 @@ export const uploadFileRequestResolver: ResolverFn<
// new item.
const page = isFileUrl(input.url)
? await getPageByParam({
userId: claims.uid,
userId: uid,
url: input.url,
})
: undefined
@ -155,7 +147,7 @@ export const uploadFileRequestResolver: ResolverFn<
{
url: isFileUrl(input.url) ? publicUrl : input.url,
id: input.clientRequestId || '',
userId: claims.uid,
userId: uid,
title: title,
hash: uploadFilePathName,
content: '',
@ -185,4 +177,4 @@ export const uploadFileRequestResolver: ResolverFn<
} else {
return { errorCodes: [UploadFileRequestErrorCode.FailedCreate] }
}
}
})

View file

@ -1,8 +1,8 @@
import * as jwt from 'jsonwebtoken'
import { RegistrationType } from '../../datalayer/user/model'
import { deletePagesByParam } from '../../elastic/pages'
import { setClaims, userRepository } from '../../entity'
import { User as UserEntity } from '../../entity/user'
import { getRepository, setClaims } from '../../entity/utils'
import { env } from '../../env'
import {
DeleteAccountError,
@ -39,7 +39,7 @@ import {
UsersSuccess,
} from '../../generated/graphql'
import { AppDataSource } from '../../server'
import { createUser } from '../../services/create_user'
import { createUser, getTopUsers } from '../../services/create_user'
import { sendVerificationEmail } from '../../services/send_emails'
import { authorized, userDataToUser } from '../../utils/helpers'
import { validateUsername } from '../../utils/usernamePolicy'
@ -49,8 +49,10 @@ export const updateUserResolver = authorized<
UpdateUserSuccess,
UpdateUserError,
MutationUpdateUserArgs
>(async (_, { input: { name, bio } }, { models, authTrx, claims }) => {
const user = await models.user.get(claims.uid)
>(async (_, { input: { name, bio } }, { uid }) => {
const user = await userRepository.findOneBy({
id: uid,
})
if (!user) {
return { errorCodes: [UpdateUserErrorCode.UserNotFound] }
}
@ -69,16 +71,14 @@ export const updateUserResolver = authorized<
return { errorCodes }
}
const updatedUser = await authTrx(async (tx) => {
const [updatedRecord, profile] = await Promise.all([
models.user.update(
claims.uid,
{ name, source: user.source, sourceUserId: user.sourceUserId },
tx
),
models.user.updateProfile(claims.uid, { bio }, tx),
])
return { ...updatedRecord, profile }
const updatedUser = await userRepository.save({
id: uid,
name,
source: user.source,
sourceUserId: user.sourceUserId,
profile: {
bio,
},
})
return { user: userDataToUser(updatedUser) }
@ -88,69 +88,63 @@ export const updateUserProfileResolver = authorized<
UpdateUserProfileSuccess,
UpdateUserProfileError,
MutationUpdateUserProfileArgs
>(
async (
_,
{ input: { userId, username, pictureUrl } },
{ models, authTrx, claims }
) => {
const user = await models.user.get(userId)
if (user.id !== claims.uid) {
return {
errorCodes: [UpdateUserProfileErrorCode.Forbidden],
}
}
if (!(username || pictureUrl)) {
return {
errorCodes: [UpdateUserProfileErrorCode.BadData],
}
}
const lowerCasedUsername = username?.toLowerCase()
if (lowerCasedUsername) {
const existingUser = await models.user.getWhere({
username: lowerCasedUsername,
})
if (existingUser?.id) {
return {
errorCodes: [UpdateUserProfileErrorCode.UsernameExists],
}
}
if (!validateUsername(lowerCasedUsername)) {
return {
errorCodes: [UpdateUserProfileErrorCode.BadUsername],
}
}
}
const updatedProfile = await authTrx((tx) =>
models.user.updateProfile(
userId,
{
username: lowerCasedUsername,
picture_url: pictureUrl,
},
tx
)
)
user.profile = {
...updatedProfile,
picture_url: updatedProfile.pictureUrl,
}
return { user: userDataToUser(user) }
>(async (_, { input: { userId, username, pictureUrl } }, { uid }) => {
const user = await userRepository.findOneBy({
id: userId,
})
if (!user) {
return { errorCodes: [UpdateUserProfileErrorCode.Unauthorized] }
}
)
if (user.id !== uid) {
return {
errorCodes: [UpdateUserProfileErrorCode.Forbidden],
}
}
if (!(username || pictureUrl)) {
return {
errorCodes: [UpdateUserProfileErrorCode.BadData],
}
}
const lowerCasedUsername = username?.toLowerCase()
if (lowerCasedUsername) {
const existingUser = await userRepository.findOneBy({
profile: {
username: lowerCasedUsername,
},
})
if (existingUser?.id) {
return {
errorCodes: [UpdateUserProfileErrorCode.UsernameExists],
}
}
if (!validateUsername(lowerCasedUsername)) {
return {
errorCodes: [UpdateUserProfileErrorCode.BadUsername],
}
}
}
const updatedUser = await userRepository.save({
id: uid,
profile: {
username: lowerCasedUsername,
pictureUrl,
},
})
return { user: userDataToUser(updatedUser) }
})
export const googleLoginResolver: ResolverFn<
LoginResult,
unknown,
WithDataSourcesContext,
MutationGoogleLoginArgs
> = async (_obj, { input }, { models, setAuth }) => {
> = async (_obj, { input }, { setAuth }) => {
const { email, secret } = input
try {
@ -159,7 +153,7 @@ export const googleLoginResolver: ResolverFn<
return { errorCodes: [LoginErrorCode.AuthFailed] }
}
const user = await models.user.getWhere({
const user = await userRepository.findOneBy({
email,
})
if (!user?.id) {
@ -176,13 +170,18 @@ export const validateUsernameResolver: ResolverFn<
Record<string, unknown>,
WithDataSourcesContext,
QueryValidateUsernameArgs
> = async (_obj, { username }, { models }) => {
> = async (_obj, { username }) => {
const lowerCasedUsername = username.toLowerCase()
if (!validateUsername(lowerCasedUsername)) {
return false
}
return !(await models.user.exists({ username: lowerCasedUsername }))
const user = await userRepository.findOneBy({
profile: {
username: lowerCasedUsername,
},
})
return !user
}
export const googleSignupResolver: ResolverFn<
@ -245,11 +244,20 @@ export const getMeUserResolver: ResolverFn<
unknown,
WithDataSourcesContext,
unknown
> = async (_obj, __, { models, claims }) => {
> = async (_obj, __, { claims }) => {
try {
return claims?.uid
? userDataToUser(await models.user.get(claims.uid))
: undefined
if (!claims?.uid) {
return undefined
}
const user = await userRepository.findOneBy({
id: claims.uid,
})
if (!user) {
return undefined
}
return userDataToUser(user)
} catch (error) {
return undefined
}
@ -260,18 +268,20 @@ export const getUserResolver: ResolverFn<
unknown,
WithDataSourcesContext,
QueryUserArgs
> = async (_obj, { userId: id, username }, { models, claims }) => {
> = async (_obj, { userId: id, username }, { uid }) => {
if (!(id || username)) {
return { errorCodes: [UserErrorCode.BadRequest] }
}
const userId =
id || (username && (await models.user.getWhere({ username }))?.id)
id ||
(username &&
(await userRepository.findOneBy({ profile: { username } }))?.id)
if (!userId) {
return { errorCodes: [UserErrorCode.UserNotFound] }
}
const userRecord = await models.user.getUserDetails(claims?.uid, userId)
const userRecord = await userRepository.findOneBy({ id: userId })
if (!userRecord) {
return { errorCodes: [UserErrorCode.UserNotFound] }
}
@ -280,9 +290,8 @@ export const getUserResolver: ResolverFn<
}
export const getAllUsersResolver = authorized<UsersSuccess, UsersError>(
async (_obj, _params, { models, claims, authTrx }) => {
const users =
(await authTrx((tx) => models.user.getTopUsers(claims.uid, tx))) || []
async (_obj, _params) => {
const users = await getTopUsers()
const result = { users: users.map((userData) => userDataToUser(userData)) }
return result
}
@ -303,8 +312,10 @@ export const deleteAccountResolver = authorized<
DeleteAccountSuccess,
DeleteAccountError,
MutationDeleteAccountArgs
>(async (_, { userID }, { models, claims, log, pubsub }) => {
const user = await models.user.get(userID)
>(async (_, { userID }, { claims, log, pubsub }) => {
const user = await userRepository.findOneBy({
id: userID,
})
if (!user) {
return {
errorCodes: [DeleteAccountErrorCode.UserNotFound],

View file

@ -1,24 +1,12 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { PartialArticle } from '..'
import { getShareInfoForArticle } from '../../datalayer/links/share_info'
import {
FeedArticle,
MutationUpdateSharedCommentArgs,
PageInfo,
QueryFeedArticlesArgs,
QuerySharedArticleArgs,
ResolverFn,
SharedArticleError,
SharedArticleErrorCode,
SharedArticleSuccess,
UpdateSharedCommentError,
UpdateSharedCommentErrorCode,
UpdateSharedCommentSuccess,
} from '../../generated/graphql'
import { Merge } from '../../util'
import { authorized } from '../../utils/helpers'
import { WithDataSourcesContext } from '../types'
export type PartialFeedArticle = Omit<
FeedArticle,
@ -35,139 +23,139 @@ export type SharedArticleSuccessPartial = Merge<
{ article: PartialArticle }
>
export const getSharedArticleResolver: ResolverFn<
SharedArticleSuccessPartial | SharedArticleError,
Record<string, unknown>,
WithDataSourcesContext,
QuerySharedArticleArgs
> = async (_obj, { username, slug, selectedHighlightId }, { kx, models }) => {
try {
const user = await models.user.getWhere({ username })
if (!user) {
return {
errorCodes: [SharedArticleErrorCode.NotFound],
}
}
// export const getSharedArticleResolver: ResolverFn<
// SharedArticleSuccessPartial | SharedArticleError,
// Record<string, unknown>,
// WithDataSourcesContext,
// QuerySharedArticleArgs
// > = async (_obj, { username, slug, selectedHighlightId }, { kx, models }) => {
// try {
// const user = await models.user.getWhere({ username })
// if (!user) {
// return {
// errorCodes: [SharedArticleErrorCode.NotFound],
// }
// }
const article = await models.userArticle.getBySlug(username, slug)
if (!article || !article.sharedAt) {
return {
errorCodes: [SharedArticleErrorCode.NotFound],
}
}
// const article = await models.userArticle.getBySlug(username, slug)
// if (!article || !article.sharedAt) {
// return {
// errorCodes: [SharedArticleErrorCode.NotFound],
// }
// }
if (selectedHighlightId) {
const highlightResult = await models.highlight.getWhereIn('shortId', [
selectedHighlightId,
])
if (!highlightResult || !highlightResult[0].sharedAt) {
return {
errorCodes: [SharedArticleErrorCode.NotFound],
}
}
}
// if (selectedHighlightId) {
// const highlightResult = await models.highlight.getWhereIn('shortId', [
// selectedHighlightId,
// ])
// if (!highlightResult || !highlightResult[0].sharedAt) {
// return {
// errorCodes: [SharedArticleErrorCode.NotFound],
// }
// }
// }
const shareInfo = await getShareInfoForArticle(
kx,
user.id,
article.id,
models
)
// const shareInfo = await getShareInfoForArticle(
// kx,
// user.id,
// article.id,
// models
// )
return { article: { ...article, userId: user.id, shareInfo: shareInfo } }
} catch (error) {
return { errorCodes: [SharedArticleErrorCode.NotFound] }
}
}
// return { article: { ...article, userId: user.id, shareInfo: shareInfo } }
// } catch (error) {
// return { errorCodes: [SharedArticleErrorCode.NotFound] }
// }
// }
export const getUserFeedArticlesResolver: ResolverFn<
PaginatedFeedArticlesSuccessPartial,
unknown,
WithDataSourcesContext,
QueryFeedArticlesArgs
> = async (
_obj,
{ after: _startCursor, first: _first, sharedByUser },
{ models, claims, authTrx }
) => {
if (!(sharedByUser || claims?.uid)) {
return {
edges: [],
pageInfo: {
startCursor: '',
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
},
}
}
// export const getUserFeedArticlesResolver: ResolverFn<
// PaginatedFeedArticlesSuccessPartial,
// unknown,
// WithDataSourcesContext,
// QueryFeedArticlesArgs
// > = async (
// _obj,
// { after: _startCursor, first: _first, sharedByUser },
// { models, claims, authTrx }
// ) => {
// if (!(sharedByUser || claims?.uid)) {
// return {
// edges: [],
// pageInfo: {
// startCursor: '',
// endCursor: '',
// hasNextPage: false,
// hasPreviousPage: false,
// },
// }
// }
const first = _first || 0
const startCursor = _startCursor || ''
// const first = _first || 0
// const startCursor = _startCursor || ''
const feedArticles =
(await authTrx((tx) =>
models.userArticle.getUserFeedArticlesPaginatedWithHighlights(
{ cursor: startCursor, first: first + 1, sharedByUser }, // fetch one more item to get next cursor
claims?.uid || '',
tx
)
)) || []
// const feedArticles =
// (await authTrx((tx) =>
// models.userArticle.getUserFeedArticlesPaginatedWithHighlights(
// { cursor: startCursor, first: first + 1, sharedByUser }, // fetch one more item to get next cursor
// claims?.uid || '',
// tx
// )
// )) || []
const endCursor = feedArticles[feedArticles.length - 1]?.sharedAt
.getTime()
?.toString()
const hasNextPage = feedArticles.length > first
// const endCursor = feedArticles[feedArticles.length - 1]?.sharedAt
// .getTime()
// ?.toString()
// const hasNextPage = feedArticles.length > first
if (hasNextPage) {
// remove an extra if exists
feedArticles.pop()
}
// if (hasNextPage) {
// // remove an extra if exists
// feedArticles.pop()
// }
const edges = feedArticles.map((fa) => {
return {
node: fa,
cursor: fa.sharedAt.getTime()?.toString(),
}
})
// const edges = feedArticles.map((fa) => {
// return {
// node: fa,
// cursor: fa.sharedAt.getTime()?.toString(),
// }
// })
return {
edges,
pageInfo: {
hasPreviousPage: false,
startCursor: '',
hasNextPage,
endCursor,
},
}
}
// return {
// edges,
// pageInfo: {
// hasPreviousPage: false,
// startCursor: '',
// hasNextPage,
// endCursor,
// },
// }
// }
export const updateSharedCommentResolver = authorized<
UpdateSharedCommentSuccess,
UpdateSharedCommentError,
MutationUpdateSharedCommentArgs
>(
async (
_,
{ input: { articleID, sharedComment } },
{ models, authTrx, claims: { uid } }
) => {
const ua = await authTrx((tx) =>
models.userArticle.getByParameters(uid, { articleId: articleID }, tx)
)
if (!ua) {
return { errorCodes: [UpdateSharedCommentErrorCode.NotFound] }
}
// export const updateSharedCommentResolver = authorized<
// UpdateSharedCommentSuccess,
// UpdateSharedCommentError,
// MutationUpdateSharedCommentArgs
// >(
// async (
// _,
// { input: { articleID, sharedComment } },
// { models, authTrx, claims: { uid } }
// ) => {
// const ua = await authTrx((tx) =>
// models.userArticle.getByParameters(uid, { articleId: articleID }, tx)
// )
// if (!ua) {
// return { errorCodes: [UpdateSharedCommentErrorCode.NotFound] }
// }
await authTrx((tx) =>
models.userArticle.updateByArticleId(
uid,
articleID,
{ sharedComment },
tx
)
)
// await authTrx((tx) =>
// models.userArticle.updateByArticleId(
// uid,
// articleID,
// { sharedComment },
// tx
// )
// )
return { articleID, sharedComment }
}
)
// return { articleID, sharedComment }
// }
// )

View file

@ -1,118 +1,101 @@
import { Knex } from 'knex'
import { UserData } from '../../datalayer/user/model'
import {
GetFollowersResult,
GetFollowingResult,
MutationSetFollowArgs,
QueryGetFollowersArgs,
QueryGetFollowingArgs,
ResolverFn,
SetFollowError,
SetFollowErrorCode,
SetFollowSuccess,
User,
} from '../../generated/graphql'
import { authorized, userDataToUser } from '../../utils/helpers'
import { DataModels, WithDataSourcesContext } from '../types'
// export const setFollowResolver = authorized<
// SetFollowSuccess,
// SetFollowError,
// MutationSetFollowArgs
// >(
// async (
// _,
// { input: { userId: friendUserId, follow } },
// { models, authTrx, claims: { uid } }
// ) => {
// const user = await models.user.getUserDetails(uid, friendUserId)
// if (!user) return { errorCodes: [SetFollowErrorCode.NotFound] }
export const setFollowResolver = authorized<
SetFollowSuccess,
SetFollowError,
MutationSetFollowArgs
>(
async (
_,
{ input: { userId: friendUserId, follow } },
{ models, authTrx, claims: { uid } }
) => {
const user = await models.user.getUserDetails(uid, friendUserId)
if (!user) return { errorCodes: [SetFollowErrorCode.NotFound] }
// const userFriendRecord = await authTrx((tx) =>
// models.userFriends.getByUserFriendId(uid, friendUserId, tx)
// )
const userFriendRecord = await authTrx((tx) =>
models.userFriends.getByUserFriendId(uid, friendUserId, tx)
)
// if (follow) {
// if (!userFriendRecord) {
// await authTrx((tx) =>
// models.userFriends.create({ friendUserId, userId: uid }, tx)
// )
// }
// } else if (userFriendRecord) {
// await authTrx((tx) => models.userFriends.delete(userFriendRecord.id, tx))
// }
if (follow) {
if (!userFriendRecord) {
await authTrx((tx) =>
models.userFriends.create({ friendUserId, userId: uid }, tx)
)
}
} else if (userFriendRecord) {
await authTrx((tx) => models.userFriends.delete(userFriendRecord.id, tx))
}
// const updatedUser = await models.user.getUserDetails(uid, friendUserId)
// if (!updatedUser) return { errorCodes: [SetFollowErrorCode.NotFound] }
const updatedUser = await models.user.getUserDetails(uid, friendUserId)
if (!updatedUser) return { errorCodes: [SetFollowErrorCode.NotFound] }
// return {
// updatedUser: {
// ...userDataToUser(updatedUser),
// isFriend: updatedUser.viewerIsFollowing,
// },
// }
// }
// )
return {
updatedUser: {
...userDataToUser(updatedUser),
isFriend: updatedUser.viewerIsFollowing,
},
}
}
)
// const getUserList = async (
// uid: string,
// users: UserData[],
// models: DataModels,
// authTrx: <TResult>(
// cb: (tx: Knex.Transaction) => TResult,
// userRole?: string
// ) => Promise<TResult>
// ): Promise<User[]> => {
// const usersIds = users.map(({ id }) => id)
// const friends = await authTrx((tx) =>
// models.userFriends.getByFriendIds(uid, usersIds, tx)
// )
const getUserList = async (
uid: string,
users: UserData[],
models: DataModels,
authTrx: <TResult>(
cb: (tx: Knex.Transaction) => TResult,
userRole?: string
) => Promise<TResult>
): Promise<User[]> => {
const usersIds = users.map(({ id }) => id)
const friends = await authTrx((tx) =>
models.userFriends.getByFriendIds(uid, usersIds, tx)
)
// const friendsIds = friends.map(({ friendUserId }) => friendUserId)
// users = users.map((f) => ({
// ...f,
// isFriend: friendsIds.includes(f.id),
// viewerIsFollowing: friendsIds.includes(f.id),
// }))
const friendsIds = friends.map(({ friendUserId }) => friendUserId)
users = users.map((f) => ({
...f,
isFriend: friendsIds.includes(f.id),
viewerIsFollowing: friendsIds.includes(f.id),
}))
// return users.map((u) => userDataToUser(u))
// }
return users.map((u) => userDataToUser(u))
}
// export const getFollowersResolver: ResolverFn<
// GetFollowersResult,
// unknown,
// WithDataSourcesContext,
// QueryGetFollowersArgs
// > = async (_parent, { userId }, { models, claims, authTrx }) => {
// const followers = userId
// ? await authTrx((tx) => models.user.getUserFollowersList(userId, tx))
// : []
// if (!claims?.uid) return { followers: usersWithNoFriends(followers) }
// return {
// followers: await getUserList(claims?.uid, followers, models, authTrx),
// }
// }
export const getFollowersResolver: ResolverFn<
GetFollowersResult,
unknown,
WithDataSourcesContext,
QueryGetFollowersArgs
> = async (_parent, { userId }, { models, claims, authTrx }) => {
const followers = userId
? await authTrx((tx) => models.user.getUserFollowersList(userId, tx))
: []
if (!claims?.uid) return { followers: usersWithNoFriends(followers) }
return {
followers: await getUserList(claims?.uid, followers, models, authTrx),
}
}
// export const getFollowingResolver: ResolverFn<
// GetFollowingResult,
// unknown,
// WithDataSourcesContext,
// QueryGetFollowingArgs
// > = async (_parent, { userId }, { models, claims, authTrx }) => {
// const following = userId
// ? await authTrx((tx) => models.user.getUserFollowingList(userId, tx))
// : []
// if (!claims?.uid) return { following: usersWithNoFriends(following) }
// return {
// following: await getUserList(claims?.uid, following, models, authTrx),
// }
// }
export const getFollowingResolver: ResolverFn<
GetFollowingResult,
unknown,
WithDataSourcesContext,
QueryGetFollowingArgs
> = async (_parent, { userId }, { models, claims, authTrx }) => {
const following = userId
? await authTrx((tx) => models.user.getUserFollowingList(userId, tx))
: []
if (!claims?.uid) return { following: usersWithNoFriends(following) }
return {
following: await getUserList(claims?.uid, following, models, authTrx),
}
}
const usersWithNoFriends = (users: UserData[]): User[] => {
return users.map((f) =>
userDataToUser({
...f,
isFriend: false,
} as UserData)
)
}
// const usersWithNoFriends = (users: UserData[]): User[] => {
// return users.map((f) =>
// userDataToUser({
// ...f,
// isFriend: false,
// } as UserData)
// )
// }

View file

@ -1,3 +1,5 @@
import { getRepository, setClaims } from '../../entity'
import { UserPersonalization } from '../../entity/user_personalization'
import {
GetUserPersonalizationError,
GetUserPersonalizationResult,
@ -7,10 +9,8 @@ import {
SetUserPersonalizationSuccess,
SortOrder,
} from '../../generated/graphql'
import { authorized } from '../../utils/helpers'
import { UserPersonalization } from '../../entity/user_personalization'
import { AppDataSource } from '../../server'
import { getRepository, setClaims } from '../../entity/utils'
import { authorized } from '../../utils/helpers'
export const setUserPersonalizationResolver = authorized<
SetUserPersonalizationSuccess,
@ -58,8 +58,12 @@ export const setUserPersonalizationResolver = authorized<
export const getUserPersonalizationResolver = authorized<
GetUserPersonalizationResult,
GetUserPersonalizationError
>(async (_parent, _args, { models, claims: { uid } }) => {
const userPersonalization = await models.userPersonalization.getByUserId(uid)
>(async (_parent, _args, { uid }) => {
const userPersonalization = await getRepository(
UserPersonalization
).findOneBy({
user: { id: uid },
})
// Cast SortOrder from string to enum
const librarySortOrder = userPersonalization?.librarySortOrder as

View file

@ -1,4 +1,7 @@
import { authorized } from '../../utils/helpers'
import { getRepository } from '../../entity'
import { User } from '../../entity/user'
import { Webhook } from '../../entity/webhook'
import { env } from '../../env'
import {
DeleteWebhookError,
DeleteWebhookErrorCode,
@ -18,11 +21,8 @@ import {
WebhooksSuccess,
WebhookSuccess,
} from '../../generated/graphql'
import { getRepository } from '../../entity/utils'
import { User } from '../../entity/user'
import { Webhook } from '../../entity/webhook'
import { analytics } from '../../utils/analytics'
import { env } from '../../env'
import { authorized } from '../../utils/helpers'
export const webhooksResolver = authorized<WebhooksSuccess, WebhooksError>(
async (_obj, _params, { claims: { uid }, log }) => {

View file

@ -6,8 +6,8 @@ import express from 'express'
import * as jwt from 'jsonwebtoken'
import { createPubSubClient } from '../datalayer/pubsub'
import { getPageById, updatePage } from '../elastic/pages'
import { getRepository } from '../entity'
import { Speech, SpeechState } from '../entity/speech'
import { getRepository } from '../entity/utils'
import { env } from '../env'
import { CreateArticleErrorCode } from '../generated/graphql'
import { Claims } from '../resolvers/types'

View file

@ -21,8 +21,8 @@ import {
StatusType,
UserData,
} from '../../datalayer/user/model'
import { getRepository, setClaims } from '../../entity'
import { User } from '../../entity/user'
import { getRepository, setClaims } from '../../entity/utils'
import { env } from '../../env'
import { LoginErrorCode, SignupErrorCode } from '../../generated/graphql'
import { isErrorWithCode } from '../../resolvers'

View file

@ -5,11 +5,11 @@
import cors from 'cors'
import express from 'express'
import * as jwt from 'jsonwebtoken'
import { kx } from '../datalayer/knex_config'
import { createPubSubClient } from '../datalayer/pubsub'
import { createPage, getPageByParam, updatePage } from '../elastic/pages'
import { addRecommendation } from '../elastic/recommendation'
import { Recommendation } from '../elastic/types'
import { uploadFileRepository } from '../entity'
import { env } from '../env'
import {
ArticleSavingRequestStatus,
@ -17,7 +17,6 @@ import {
UploadFileStatus,
} from '../generated/graphql'
import { Claims } from '../resolvers/types'
import { initModels } from '../server'
import { getTokenByRequest } from '../utils/auth'
import { corsConfig } from '../utils/corsConfig'
import {
@ -76,7 +75,6 @@ export function pageRouter() {
return res.status(400).send({ errorCode: 'BAD_DATA' })
}
const models = initModels(kx, false)
const ctx = {
uid: claims.uid,
pubsub: createPubSubClient(),
@ -84,10 +82,10 @@ export function pageRouter() {
const title = titleForFilePath(url)
const fileName = fileNameForFilePath(url)
const uploadFileData = await models.uploadFile.create({
url: url,
const uploadFileData = await uploadFileRepository.save({
url,
userId: claims.uid,
fileName: fileName,
fileName,
status: UploadFileStatus.Initialized,
contentType: 'application/pdf',
})

View file

@ -2,16 +2,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import express from 'express'
import { setClaims } from '../../datalayer/helpers'
import { kx } from '../../datalayer/knex_config'
import {
createPubSubClient,
readPushSubscription,
} from '../../datalayer/pubsub'
import { getPageByParam, updatePage } from '../../elastic/pages'
import { Page } from '../../elastic/types'
import { setClaims } from '../../entity'
import { ArticleSavingRequestStatus } from '../../generated/graphql'
import { initModels } from '../../server'
import { AppDataSource } from '../../server'
import { setFileUploadComplete } from '../../services/save_file'
import { logger } from '../../utils/logger'
interface UpdateContentMessage {
@ -76,10 +76,9 @@ export function contentServiceRouter() {
pageToUpdate.state = ArticleSavingRequestStatus.Succeeded
try {
const models = initModels(kx, false)
const uploadFileData = await kx.transaction(async (tx) => {
const uploadFileData = await AppDataSource.transaction(async (tx) => {
await setClaims(tx, page.userId)
return models.uploadFile.setFileUploadComplete(fileId, tx)
return setFileUploadComplete(fileId, tx)
})
logger.info('updated uploadFileData', uploadFileData)
} catch (error) {

View file

@ -1,14 +1,14 @@
import express from 'express'
import { setClaims } from '../../datalayer/helpers'
import { kx } from '../../datalayer/knex_config'
import { createPubSubClient } from '../../datalayer/pubsub'
import { createPage } from '../../elastic/pages'
import { ArticleSavingRequestStatus, Page } from '../../elastic/types'
import { setClaims, uploadFileRepository } from '../../entity'
import { env } from '../../env'
import { PageType, UploadFileStatus } from '../../generated/graphql'
import { initModels } from '../../server'
import { AppDataSource } from '../../server'
import { getNewsletterEmail } from '../../services/newsletters'
import { updateReceivedEmail } from '../../services/received_emails'
import { setFileUploadComplete } from '../../services/save_file'
import { analytics } from '../../utils/analytics'
import { getClaimsByToken } from '../../utils/auth'
import { generateSlug } from '../../utils/helpers'
@ -54,8 +54,7 @@ export function emailAttachmentRouter() {
})
try {
const models = initModels(kx, false)
const uploadFileData = await models.uploadFile.create({
const uploadFileData = await uploadFileRepository.save({
url: '',
userId: user.id,
fileName: fileName,
@ -117,10 +116,9 @@ export function emailAttachmentRouter() {
})
try {
const models = initModels(kx, false)
const uploadFile = await models.uploadFile.getWhere({
const uploadFile = await uploadFileRepository.findOneBy({
id: uploadFileId,
userId: user.id,
user: { id: user.id },
})
if (!uploadFile) {
return res.status(400).send('BAD REQUEST')
@ -131,9 +129,9 @@ export function emailAttachmentRouter() {
uploadFile.fileName
)
const uploadFileData = await kx.transaction(async (tx) => {
const uploadFileData = await AppDataSource.transaction(async (tx) => {
await setClaims(tx, user.id)
return models.uploadFile.setFileUploadComplete(uploadFileId, tx)
return setFileUploadComplete(uploadFileId, tx)
})
if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) {
return res.status(400).send('BAD REQUEST')

View file

@ -8,8 +8,8 @@ import { v4 as uuidv4 } from 'uuid'
import { EntityType, readPushSubscription } from '../../datalayer/pubsub'
import { getPageById, searchPages } from '../../elastic/pages'
import { Page } from '../../elastic/types'
import { getRepository } from '../../entity'
import { Integration, IntegrationType } from '../../entity/integration'
import { getRepository } from '../../entity/utils'
import { Claims } from '../../resolvers/types'
import { getIntegrationService } from '../../services/integrations'
import { getClaimsByToken } from '../../utils/auth'

View file

@ -1,20 +1,12 @@
import express from 'express'
import { MulticastMessage } from 'firebase-admin/messaging'
import { setClaims } from '../../datalayer/helpers'
import { kx } from '../../datalayer/knex_config'
import { createPubSubClient } from '../../datalayer/pubsub'
import { updatePage } from '../../elastic/pages'
import { setClaims } from '../../entity'
import { UserDeviceToken } from '../../entity/user_device_tokens'
import { env, homePageURL } from '../../env'
import { homePageURL } from '../../env'
import { ContentReader } from '../../generated/graphql'
import { DataModels } from '../../resolvers/types'
import { initModels } from '../../server'
import { getPagesWithReminder, PageReminder } from '../../services/reminders'
import { getDeviceTokensByUserId } from '../../services/user_device_tokens'
import { analytics } from '../../utils/analytics'
import { logger } from '../../utils/logger'
import { sendEmail } from '../../utils/sendEmail'
import { sendMulticastPushNotifications } from '../../utils/sendNotification'
import { AppDataSource } from '../../server'
import { PageReminder, setRemindersComplete } from '../../services/reminders'
interface PageToNotify {
title: string
@ -24,116 +16,116 @@ interface PageToNotify {
image: string | undefined | null
}
export function remindersServiceRouter() {
const router = express.Router()
// export function remindersServiceRouter() {
// const router = express.Router()
// eslint-disable-next-line @typescript-eslint/no-misused-promises
router.post('/trigger', async (req, res) => {
logger.info('reminders/trigger')
// // eslint-disable-next-line @typescript-eslint/no-misused-promises
// router.post('/trigger', async (req, res) => {
// logger.info('reminders/trigger')
const { userId, scheduleTime } = req.body as {
userId?: string
scheduleTime?: number
}
// const { userId, scheduleTime } = req.body as {
// userId?: string
// scheduleTime?: number
// }
analytics.track({
userId: userId,
event: 'reminder_triggered',
properties: {
env: env.server.apiEnv,
},
})
// analytics.track({
// userId: userId,
// event: 'reminder_triggered',
// properties: {
// env: env.server.apiEnv,
// },
// })
if (!userId || !scheduleTime) {
res.status(400).send('Bad Request')
return
}
// if (!userId || !scheduleTime) {
// res.status(400).send('Bad Request')
// return
// }
try {
const remindAt = new Date(scheduleTime)
// try {
// const remindAt = new Date(scheduleTime)
// get all reminders by userid and scheduled time
const models = initModels(kx, false)
// // get all reminders by userid and scheduled time
// const models = initModels(kx, false)
const user = await models.user.get(userId)
if (!user || !user.email) {
logger.info('user not found', userId)
res.status(400).send('User Not Found')
return
}
// const user = await models.user.get(userId)
// if (!user || !user.email) {
// logger.info('user not found', userId)
// res.status(400).send('User Not Found')
// return
// }
const pageReminders = await getPagesWithReminder(userId, remindAt)
// const pageReminders = await getPagesWithReminder(userId, remindAt)
if (!pageReminders) {
logger.info('pages with reminders not found', userId, scheduleTime)
res.status(200).send('Reminders Not Found')
return
}
// if (!pageReminders) {
// logger.info('pages with reminders not found', userId, scheduleTime)
// res.status(200).send('Reminders Not Found')
// return
// }
logger.info('page with reminders:', pageReminders)
// logger.info('page with reminders:', pageReminders)
const [pagesToNotify, pagesToUnarchive] = getPagesToNotifyAndUnarchive(
pageReminders,
user.profile.username
)
// const [pagesToNotify, pagesToUnarchive] = getPagesToNotifyAndUnarchive(
// pageReminders,
// user.profile.username
// )
// If none of the fetch reminders have sendNotification
// set to true, then we should not send an email or notification
if (pagesToNotify.length > 0) {
// we have configured Sendgrid to send a template
if (!process.env.SENDGRID_REMINDER_TEMPLATE_ID) {
logger.info('Sendgrid reminder email template_id not set')
// // If none of the fetch reminders have sendNotification
// // set to true, then we should not send an email or notification
// if (pagesToNotify.length > 0) {
// // we have configured Sendgrid to send a template
// if (!process.env.SENDGRID_REMINDER_TEMPLATE_ID) {
// logger.info('Sendgrid reminder email template_id not set')
await updateRemindersStatus(
models,
userId,
pagesToUnarchive,
remindAt
)
res.status(200).send('Template Id Not Found')
return
}
// await updateRemindersStatus(
// models,
// userId,
// pagesToUnarchive,
// remindAt
// )
// res.status(200).send('Template Id Not Found')
// return
// }
const dynamicTemplateData = {
subject: `Omnivore Reminder Service`,
title: `Hey ${user.name}, you have ${pagesToNotify.length} article(s) to read on Omnivore`,
articles: pagesToNotify,
}
// const dynamicTemplateData = {
// subject: `Omnivore Reminder Service`,
// title: `Hey ${user.name}, you have ${pagesToNotify.length} article(s) to read on Omnivore`,
// articles: pagesToNotify,
// }
logger.info('dynamic template data:', dynamicTemplateData)
// logger.info('dynamic template data:', dynamicTemplateData)
await sendEmail({
from: env.sender.message,
dynamicTemplateData: dynamicTemplateData,
templateId: env.sendgrid.reminderTemplateId,
to: user.email,
})
// await sendEmail({
// from: env.sender.message,
// dynamicTemplateData: dynamicTemplateData,
// templateId: env.sendgrid.reminderTemplateId,
// to: user.email,
// })
// send push notifications
const deviceTokens = await getDeviceTokensByUserId(userId)
if (deviceTokens && deviceTokens.length > 0) {
const message = messageForPages(pageReminders, deviceTokens)
await sendMulticastPushNotifications(userId, message, 'reminder')
}
// // send push notifications
// const deviceTokens = await getDeviceTokensByUserId(userId)
// if (deviceTokens && deviceTokens.length > 0) {
// const message = messageForPages(pageReminders, deviceTokens)
// await sendMulticastPushNotifications(userId, message, 'reminder')
// }
if (!deviceTokens) {
logger.info('Device tokens not set:', userId)
// if (!deviceTokens) {
// logger.info('Device tokens not set:', userId)
res.status(400).send('Device token Not Found')
return
}
}
// res.status(400).send('Device token Not Found')
// return
// }
// }
await updateRemindersStatus(models, userId, pagesToUnarchive, remindAt)
res.status(200).send('Reminders triggered')
} catch (e) {
logger.info(e)
res.status(500).send(e)
}
})
// await updateRemindersStatus(models, userId, pagesToUnarchive, remindAt)
// res.status(200).send('Reminders triggered')
// } catch (e) {
// logger.info(e)
// res.status(500).send(e)
// }
// })
return router
}
// return router
// }
const getPagesToNotifyAndUnarchive = (
pageReminders: PageReminder[],
@ -221,7 +213,6 @@ const messageForPages = (
}
const updateRemindersStatus = async (
models: DataModels,
userId: string,
pagesToUnarchive: string[],
remindAt: Date
@ -243,8 +234,8 @@ const updateRemindersStatus = async (
}
// db update
await kx.transaction(async (tx) => {
await AppDataSource.transaction(async (tx) => {
await setClaims(tx, userId)
await models.reminder.setRemindersComplete(userId, remindAt, tx)
await setRemindersComplete(tx, userId, remindAt)
})
}

View file

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import express from 'express'
import { readPushSubscription } from '../../datalayer/pubsub'
import { getRepository } from '../../entity'
import { Subscription } from '../../entity/subscription'
import { getRepository } from '../../entity/utils'
import { SubscriptionStatus, SubscriptionType } from '../../generated/graphql'
import { enqueueRssFeedFetch } from '../../utils/createTask'
import { logger } from '../../utils/logger'

View file

@ -4,7 +4,7 @@
import axios, { Method } from 'axios'
import express from 'express'
import { readPushSubscription } from '../../datalayer/pubsub'
import { getRepository } from '../../entity/utils'
import { getRepository } from '../../entity'
import { Webhook } from '../../entity/webhook'
import { logger } from '../../utils/logger'

View file

@ -7,9 +7,9 @@ import express from 'express'
import { readPushSubscription } from '../datalayer/pubsub'
import { getPageById } from '../elastic/pages'
import { ArticleSavingRequestStatus } from '../elastic/types'
import { getRepository, setClaims } from '../entity'
import { Speech, SpeechState } from '../entity/speech'
import { UserPersonalization } from '../entity/user_personalization'
import { getRepository, setClaims } from '../entity/utils'
import { AppDataSource } from '../server'
import { FeatureName, getFeature } from '../services/features'
import { shouldSynthesize } from '../services/speech'

View file

@ -2,8 +2,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import cors from 'cors'
import express from 'express'
import { getRepository } from '../entity'
import { User } from '../entity/user'
import { getRepository } from '../entity/utils'
import { env } from '../env'
import { getClaimsByToken, getTokenByRequest } from '../utils/auth'
import { corsConfig } from '../utils/corsConfig'

View file

@ -2604,24 +2604,24 @@ const schema = gql`
mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
deleteHighlight(highlightId: ID!): DeleteHighlightResult!
createHighlightReply(
input: CreateHighlightReplyInput!
): CreateHighlightReplyResult!
updateHighlightReply(
input: UpdateHighlightReplyInput!
): UpdateHighlightReplyResult!
deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult!
createReaction(input: CreateReactionInput!): CreateReactionResult!
deleteReaction(id: ID!): DeleteReactionResult!
# createHighlightReply(
# input: CreateHighlightReplyInput!
# ): CreateHighlightReplyResult!
# updateHighlightReply(
# input: UpdateHighlightReplyInput!
# ): UpdateHighlightReplyResult!
# deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult!
# createReaction(input: CreateReactionInput!): CreateReactionResult!
# deleteReaction(id: ID!): DeleteReactionResult!
uploadFileRequest(input: UploadFileRequestInput!): UploadFileRequestResult!
saveArticleReadingProgress(
input: SaveArticleReadingProgressInput!
): SaveArticleReadingProgressResult!
setShareArticle(input: SetShareArticleInput!): SetShareArticleResult!
updateSharedComment(
input: UpdateSharedCommentInput!
): UpdateSharedCommentResult!
setFollow(input: SetFollowInput!): SetFollowResult!
# setShareArticle(input: SetShareArticleInput!): SetShareArticleResult!
# updateSharedComment(
# input: UpdateSharedCommentInput!
# ): UpdateSharedCommentResult!
# setFollow(input: SetFollowInput!): SetFollowResult!
setBookmarkArticle(
input: SetBookmarkArticleInput!
): SetBookmarkArticleResult!
@ -2631,11 +2631,11 @@ const schema = gql`
createArticleSavingRequest(
input: CreateArticleSavingRequestInput!
): CreateArticleSavingRequestResult!
setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult!
# setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult!
reportItem(input: ReportItemInput!): ReportItemResult!
updateLinkShareInfo(
input: UpdateLinkShareInfoInput!
): UpdateLinkShareInfoResult!
# updateLinkShareInfo(
# input: UpdateLinkShareInfoInput!
# ): UpdateLinkShareInfoResult!
setLinkArchived(input: ArchiveLinkInput!): ArchiveLinkResult!
createNewsletterEmail: CreateNewsletterEmailResult!
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
@ -2643,9 +2643,9 @@ const schema = gql`
savePage(input: SavePageInput!): SaveResult!
updatePage(input: UpdatePageInput!): UpdatePageResult!
saveFile(input: SaveFileInput!): SaveResult!
createReminder(input: CreateReminderInput!): CreateReminderResult!
updateReminder(input: UpdateReminderInput!): UpdateReminderResult!
deleteReminder(id: ID!): DeleteReminderResult!
# createReminder(input: CreateReminderInput!): CreateReminderResult!
# updateReminder(input: UpdateReminderInput!): UpdateReminderResult!
# deleteReminder(id: ID!): DeleteReminderResult!
setDeviceToken(input: SetDeviceTokenInput!): SetDeviceTokenResult!
createLabel(input: CreateLabelInput!): CreateLabelResult!
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
@ -2710,25 +2710,25 @@ const schema = gql`
includePending: Boolean
): ArticlesResult!
article(username: String!, slug: String!, format: String): ArticleResult!
sharedArticle(
username: String!
slug: String!
selectedHighlightId: String
): SharedArticleResult!
feedArticles(
after: String
first: Int
sort: SortParams
sharedByUser: ID
): FeedArticlesResult!
# sharedArticle(
# username: String!
# slug: String!
# selectedHighlightId: String
# ): SharedArticleResult!
# feedArticles(
# after: String
# first: Int
# sort: SortParams
# sharedByUser: ID
# ): FeedArticlesResult!
users: UsersResult!
validateUsername(username: String!): Boolean!
getFollowers(userId: ID): GetFollowersResult!
getFollowing(userId: ID): GetFollowingResult!
# getFollowers(userId: ID): GetFollowersResult!
# getFollowing(userId: ID): GetFollowingResult!
getUserPersonalization: GetUserPersonalizationResult!
articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult!
newsletterEmails: NewsletterEmailsResult!
reminder(linkId: ID!): ReminderResult!
# reminder(linkId: ID!): ReminderResult!
labels: LabelsResult!
search(
after: String

View file

@ -11,24 +11,12 @@ import express, { Express } from 'express'
import * as httpContext from 'express-http-context2'
import rateLimit from 'express-rate-limit'
import { createServer, Server } from 'http'
import { Knex } from 'knex'
import { DataSource } from 'typeorm'
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'
import { config, loggers } from 'winston'
import { makeApolloServer } from './apollo'
import ArticleModel from './datalayer/article'
import ArticleSavingRequestModel from './datalayer/article_saving_request'
import HighlightModel from './datalayer/highlight'
import UserArticleModel from './datalayer/links'
import ReactionModel from './datalayer/reaction'
import ReminderModel from './datalayer/reminders'
import UploadFileDataModel from './datalayer/upload_files'
import UserModel from './datalayer/user'
import UserFriendModel from './datalayer/user_friends'
import UserPersonalizationModel from './datalayer/user_personalization'
import { initElasticsearch } from './elastic'
import { env } from './env'
import { DataModels } from './resolvers/types'
import { articleRouter } from './routers/article_router'
import { authRouter } from './routers/auth/auth_router'
import { mobileAuthRouter } from './routers/auth/mobile/mobile_auth_router'
@ -42,7 +30,7 @@ import { emailAttachmentRouter } from './routers/svc/email_attachment'
import { integrationsServiceRouter } from './routers/svc/integrations'
import { linkServiceRouter } from './routers/svc/links'
import { newsletterServiceRouter } from './routers/svc/newsletters'
import { remindersServiceRouter } from './routers/svc/reminders'
// import { remindersServiceRouter } from './routers/svc/reminders'
import { rssFeedRouter } from './routers/svc/rss_feed'
import { uploadServiceRouter } from './routers/svc/upload'
import { webhooksServiceRouter } from './routers/svc/webhooks'
@ -59,19 +47,6 @@ import {
const PORT = process.env.PORT || 4000
export const initModels = (kx: Knex, cache = true): DataModels => ({
user: new UserModel(kx, cache),
article: new ArticleModel(kx, cache),
userArticle: new UserArticleModel(kx, cache),
userFriends: new UserFriendModel(kx, cache),
userPersonalization: new UserPersonalizationModel(kx, cache),
articleSavingRequest: new ArticleSavingRequestModel(kx, cache),
uploadFile: new UploadFileDataModel(kx, cache),
highlight: new HighlightModel(kx, cache),
reaction: new ReactionModel(kx, cache),
reminder: new ReminderModel(kx, cache),
})
export const AppDataSource = new DataSource({
type: 'postgres',
host: env.pg.host,
@ -85,6 +60,9 @@ export const AppDataSource = new DataSource({
subscribers: [__dirname + '/events/**/*{.js,.ts}'],
namingStrategy: new SnakeNamingStrategy(),
logger: new CustomTypeOrmLogger(),
cache: true,
connectTimeoutMS: 60000, // 60 seconds
maxQueryExecutionTime: 60000, // 60 seconds
})
export const createApp = (): {
@ -164,7 +142,7 @@ export const createApp = (): {
app.use('/svc/pubsub/webhooks', webhooksServiceRouter())
app.use('/svc/pubsub/integrations', integrationsServiceRouter())
app.use('/svc/pubsub/rss-feed', rssFeedRouter())
app.use('/svc/reminders', remindersServiceRouter())
// app.use('/svc/reminders', remindersServiceRouter())
app.use('/svc/email-attachment', emailAttachmentRouter())
if (env.dev.isLocal) {

View file

@ -1,5 +1,5 @@
import { setClaims } from '../entity'
import { Link } from '../entity/link'
import { setClaims } from '../entity/utils'
import { AppDataSource } from '../server'
export const setLinkArchived = async (

View file

@ -8,8 +8,8 @@ import {
updatePage,
} from '../elastic/pages'
import { ArticleSavingRequestStatus, Label, PageType } from '../elastic/types'
import { getRepository } from '../entity'
import { User } from '../entity/user'
import { getRepository } from '../entity/utils'
import {
ArticleSavingRequest,
CreateArticleSavingRequestErrorCode,

View file

@ -1,10 +1,10 @@
import { EntityManager } from 'typeorm'
import { EntityManager, In } from 'typeorm'
import { StatusType } from '../datalayer/user/model'
import { getRepository, userRepository } from '../entity'
import { GroupMembership } from '../entity/groups/group_membership'
import { Invite } from '../entity/groups/invite'
import { Profile } from '../entity/profile'
import { User } from '../entity/user'
import { getRepository } from '../entity/utils'
import { SignupErrorCode } from '../generated/graphql'
import { AuthProvider } from '../routers/auth/auth_types'
import { AppDataSource } from '../server'
@ -15,6 +15,18 @@ import { Filter } from '../entity/filter'
import { analytics } from '../utils/analytics'
import { env } from '../env'
const TOP_USERS = [
'jacksonh',
'nat',
'luis',
'satindar',
'malandrina',
'patrick',
'alexgutjahr',
'hongbowu',
]
export const MAX_RECORDS_LIMIT = 1000
export const createUser = async (input: {
provider: AuthProvider
sourceUserId?: string
@ -166,9 +178,19 @@ const validateInvite = async (
}
export const getUserByEmail = async (email: string): Promise<User | null> => {
return getRepository(User)
return userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.where('LOWER(email) = LOWER(:email)', { email }) // case insensitive
.getOne()
}
export const getTopUsers = async (): Promise<User[]> => {
return userRepository
.createQueryBuilder()
.where({
profile: { username: In(TOP_USERS) },
})
.limit(MAX_RECORDS_LIMIT)
.getMany()
}

View file

@ -1,7 +1,7 @@
import * as jwt from 'jsonwebtoken'
import { IsNull, Not } from 'typeorm'
import { getRepository } from '../entity'
import { Feature } from '../entity/feature'
import { getRepository } from '../entity/utils'
import { env } from '../env'
import { AppDataSource } from '../server'
import { logger } from '../utils/logger'

View file

@ -1,6 +1,6 @@
import { User } from '../entity/user'
import { getRepository } from '../entity'
import { Follower } from '../entity/follower'
import { getRepository } from '../entity/utils'
import { User } from '../entity/user'
export const getUserFollowers = async (
user: User,

View file

@ -1,10 +1,10 @@
import { nanoid } from 'nanoid'
import { getRepository } from '../entity'
import { Group } from '../entity/groups/group'
import { GroupMembership } from '../entity/groups/group_membership'
import { Invite } from '../entity/groups/invite'
import { RuleActionType } from '../entity/rule'
import { User } from '../entity/user'
import { getRepository } from '../entity/utils'
import { homePageURL } from '../env'
import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql'
import { AppDataSource } from '../server'

View file

@ -1,7 +1,7 @@
import axios from 'axios'
import { HighlightType, Page } from '../../elastic/types'
import { getRepository } from '../../entity'
import { Integration } from '../../entity/integration'
import { getRepository } from '../../entity/utils'
import { env } from '../../env'
import { wait } from '../../utils/helpers'
import { logger } from '../../utils/logger'

View file

@ -2,10 +2,10 @@ import DataLoader from 'dataloader'
import { In } from 'typeorm'
import { addLabelInPage } from '../elastic/labels'
import { PageContext } from '../elastic/types'
import { getRepository } from '../entity'
import { Label } from '../entity/label'
import { Link } from '../entity/link'
import { User } from '../entity/user'
import { getRepository } from '../entity/utils'
import { CreateLabelInput } from '../generated/graphql'
import { generateRandomColor } from '../utils/helpers'
import { logger } from '../utils/logger'
@ -117,18 +117,10 @@ export const createLabels = async (
ctx: PageContext,
labels: CreateLabelInput[]
): Promise<Label[]> => {
const user = await getRepository(User).findOneBy({
id: ctx.uid,
})
if (!user) {
logger.error('user not found')
return []
}
const labelEntities = await getRepository(Label)
.createQueryBuilder()
.where({
user: { id: user.id },
user: { id: ctx.uid },
})
.andWhere('LOWER(name) IN (:...names)', {
names: labels.map((l) => l.name.toLowerCase()),
@ -148,7 +140,7 @@ export const createLabels = async (
description: l.description,
color: l.color || generateRandomColor(),
internal: isLabelInternal(l.name),
user,
user: { id: ctx.uid },
}))
)
return [...labelEntities, ...newLabelEntities]

View file

@ -1,12 +1,12 @@
import { NewsletterEmail } from '../entity/newsletter_email'
import { nanoid } from 'nanoid'
import { getRepository } from '../entity'
import { NewsletterEmail } from '../entity/newsletter_email'
import { User } from '../entity/user'
import { env } from '../env'
import {
CreateNewsletterEmailErrorCode,
SubscriptionStatus,
} from '../generated/graphql'
import { env } from '../env'
import { getRepository } from '../entity/utils'
import addressparser = require('nodemailer/lib/addressparser')
const parsedAddress = (emailAddress: string): string | undefined => {

View file

@ -1,5 +1,5 @@
import { getRepository } from '../entity'
import { ReceivedEmail } from '../entity/received_email'
import { getRepository } from '../entity/utils'
export const saveReceivedEmail = async (
from: string,

View file

@ -1,7 +1,7 @@
import { IsNull, Not } from 'typeorm'
import { EntityManager, IsNull, Not } from 'typeorm'
import { getPageById } from '../elastic/pages'
import { getRepository } from '../entity'
import { Reminder } from '../entity/reminder'
import { getRepository } from '../entity/utils'
import { logger } from '../utils/logger'
export interface PageReminder {
@ -52,3 +52,23 @@ export const getPagesWithReminder = async (
return results
}
export const setRemindersComplete = async (
tx: EntityManager,
userId: string,
remindAt: Date
): Promise<Reminder | null> => {
const updateResult = await tx
.createQueryBuilder()
.where({ userId, remindAt })
.update(Reminder)
.set({ status: 'COMPLETED' })
.returning('*')
.execute()
if (updateResult.generatedMaps.length === 0) {
return null
}
return updateResult.generatedMaps[0] as Reminder
}

View file

@ -1,7 +1,7 @@
import { getPageById } from '../elastic/pages'
import { getRepository } from '../entity'
import { AbuseReport } from '../entity/reports/abuse_report'
import { ContentDisplayReport } from '../entity/reports/content_display_report'
import { getRepository } from '../entity/utils'
import { ReportItemInput, ReportType } from '../generated/graphql'
import { logger } from '../utils/logger'

View file

@ -1,6 +1,6 @@
import { Rule, RuleAction } from '../entity/rule'
import { getRepository } from '../entity/utils'
import { ILike } from 'typeorm'
import { getRepository } from '../entity'
import { Rule, RuleAction } from '../entity/rule'
export const createRule = async (
userId: string,

View file

@ -1,7 +1,7 @@
import { Knex } from 'knex'
import { PubsubClient } from '../datalayer/pubsub'
import { UserData } from '../datalayer/user/model'
import { updatePage } from '../elastic/pages'
import { getRepository } from '../entity'
import { UploadFile } from '../entity/upload_file'
import { homePageURL } from '../env'
import {
ArticleSavingRequestStatus,
@ -9,33 +9,30 @@ import {
SaveFileInput,
SaveResult,
} from '../generated/graphql'
import { DataModels } from '../resolvers/types'
import { WithDataSourcesContext } from '../resolvers/types'
import { AppDataSource } from '../server'
import { logger } from '../utils/logger'
import { getStorageFileDetails } from '../utils/uploads'
import { createLabels } from './labels'
type SaveContext = {
pubsub: PubsubClient
models: DataModels
authTrx: <TResult>(
cb: (tx: Knex.Transaction) => TResult,
userRole?: string
) => Promise<TResult>
uid: string
export const setFileUploadComplete = async (
id: string,
em = AppDataSource.createEntityManager()
): Promise<UploadFile | null> => {
return em.getRepository(UploadFile).save({ id, status: 'COMPLETED' })
}
export const saveFile = async (
ctx: SaveContext,
saver: UserData,
ctx: WithDataSourcesContext,
user: UserData,
input: SaveFileInput
): Promise<SaveResult> => {
logger.info('saving file with input', input)
const pageId = input.clientRequestId
const uploadFile = await ctx.models.uploadFile.getWhere({
const uploadFile = await getRepository(UploadFile).findOneBy({
id: input.uploadFileId,
userId: saver.id,
user: { id: ctx.uid },
})
if (!uploadFile) {
return {
errorCodes: [SaveErrorCode.Unauthorized],
@ -45,7 +42,7 @@ export const saveFile = async (
await getStorageFileDetails(input.uploadFileId, uploadFile.fileName)
const uploadFileData = await ctx.authTrx(async (tx) => {
return ctx.models.uploadFile.setFileUploadComplete(input.uploadFileId, tx)
return setFileUploadComplete(input.uploadFileId, tx)
})
if (!uploadFileData) {
@ -59,7 +56,7 @@ export const saveFile = async (
input.state === ArticleSavingRequestStatus.Archived ? new Date() : null
// add labels to page
const labels = input.labels
? await createLabels({ ...ctx, uid: saver.id }, input.labels)
? await createLabels(ctx, input.labels)
: undefined
if (input.state || input.labels) {
const updated = await updatePage(
@ -80,7 +77,7 @@ export const saveFile = async (
return {
clientRequestId: input.clientRequestId,
url: `${homePageURL()}/${saver.profile.username}/links/${
url: `${homePageURL()}/${user.profile.username}/links/${
input.clientRequestId
}`,
}

View file

@ -1,8 +1,8 @@
import { Readability } from '@omnivore/readability'
import { PubsubClient } from '../datalayer/pubsub'
import { addHighlightToPage } from '../elastic/highlights'
import { createPage, getPageByParam, updatePage } from '../elastic/pages'
import { ArticleSavingRequestStatus, Page, PageType } from '../elastic/types'
import { User } from '../entity/user'
import { homePageURL } from '../env'
import {
HighlightType,
@ -12,7 +12,7 @@ import {
SavePageInput,
SaveResult,
} from '../generated/graphql'
import { DataModels } from '../resolvers/types'
import { WithDataSourcesContext } from '../resolvers/types'
import { enqueueThumbnailTask } from '../utils/createTask'
import {
cleanUrl,
@ -27,18 +27,6 @@ import { parsePreparedContent } from '../utils/parser'
import { createPageSaveRequest } from './create_page_save_request'
import { createLabels } from './labels'
type SaveContext = {
pubsub: PubsubClient
models: DataModels
uid: string
refresh?: boolean
}
type SaverUserData = {
userId: string
username: string
}
// where we can use APIs to fetch their underlying content.
const FORCE_PUPPETEER_URLS = [
TWEET_URL_REGEX,
@ -65,19 +53,9 @@ const shouldParseInBackend = (input: SavePageInput): boolean => {
)
}
export const createSavingRequest = (
ctx: SaveContext,
clientRequestId: string
) => {
return ctx.models.articleSavingRequest.create({
userId: ctx.uid,
id: clientRequestId,
})
}
export const savePage = async (
ctx: SaveContext,
saver: SaverUserData,
ctx: WithDataSourcesContext,
user: User,
input: SavePageInput
): Promise<SaveResult> => {
const parseResult = await parsePreparedContent(
@ -97,7 +75,7 @@ export const savePage = async (
const articleToSave = parsedContentToPage({
url: input.url,
title: input.title,
userId: saver.userId,
userId: user.id,
pageId,
slug,
croppedPathname,
@ -124,7 +102,7 @@ export const savePage = async (
if (shouldParseInBackend(input)) {
try {
await createPageSaveRequest({
userId: saver.userId,
userId: user.id,
url: articleToSave.url,
pubsub: ctx.pubsub,
articleSavingRequestId: input.clientRequestId,
@ -140,7 +118,7 @@ export const savePage = async (
} else {
// check if the page already exists
const existingPage = await getPageByParam({
userId: saver.userId,
userId: user.id,
url: articleToSave.url,
})
if (existingPage) {
@ -151,7 +129,7 @@ export const savePage = async (
) {
return {
clientRequestId: pageId,
url: `${homePageURL()}/${saver.username}/${slug}`,
url: `${homePageURL()}/${user.profile.username}/${slug}`,
}
}
@ -195,7 +173,7 @@ export const savePage = async (
if (!isImported) {
try {
// create a task to update thumbnail and pre-cache all images
const taskId = await enqueueThumbnailTask(saver.userId, slug)
const taskId = await enqueueThumbnailTask(user.id, slug)
logger.info('Created thumbnail task', { taskId })
} catch (e) {
logger.error('Failed to create thumbnail task', e)
@ -222,7 +200,7 @@ export const savePage = async (
return {
clientRequestId: pageId,
url: `${homePageURL()}/${saver.username}/${slug}`,
url: `${homePageURL()}/${user.profile.username}/${slug}`,
}
}

View file

@ -1,7 +1,7 @@
import { PubsubClient } from '../datalayer/pubsub'
import { ArticleSavingRequestStatus } from '../elastic/types'
import { getRepository } from '../entity'
import { User } from '../entity/user'
import { getRepository } from '../entity/utils'
import { homePageURL } from '../env'
import { SaveErrorCode, SaveResult, SaveUrlInput } from '../generated/graphql'
import { logger } from '../utils/logger'
@ -24,7 +24,7 @@ export const saveUrl = async (
input.state === ArticleSavingRequestStatus.Archived ? new Date() : null
// add labels to page
const labels = input.labels
? await createLabels({ ...ctx, uid: ctx.uid }, input.labels)
? await createLabels(ctx, input.labels)
: undefined
const pageSaveRequest = await createPageSaveRequest({

View file

@ -1,5 +1,5 @@
import { getRepository } from '../entity'
import { SearchHistory } from '../entity/search_history'
import { getRepository } from '../entity/utils'
export const getRecentSearches = async (
userId: string

View file

@ -1,7 +1,7 @@
import axios from 'axios'
import { getRepository } from '../entity'
import { NewsletterEmail } from '../entity/newsletter_email'
import { Subscription } from '../entity/subscription'
import { getRepository } from '../entity/utils'
import { SubscriptionStatus, SubscriptionType } from '../generated/graphql'
import { logger } from '../utils/logger'
import { sendEmail } from '../utils/sendEmail'

View file

@ -1,10 +1,10 @@
import { UserDeviceToken } from '../entity/user_device_tokens'
import { getRepository, setClaims } from '../entity'
import { User } from '../entity/user'
import { SetDeviceTokenErrorCode } from '../generated/graphql'
import { getRepository, setClaims } from '../entity/utils'
import { analytics } from '../utils/analytics'
import { UserDeviceToken } from '../entity/user_device_tokens'
import { env } from '../env'
import { SetDeviceTokenErrorCode } from '../generated/graphql'
import { AppDataSource } from '../server'
import { analytics } from '../utils/analytics'
export const getDeviceToken = async (
id: string

View file

@ -4,8 +4,8 @@ import express from 'express'
import * as jwt from 'jsonwebtoken'
import { promisify } from 'util'
import { v4 as uuidv4 } from 'uuid'
import { getRepository } from '../entity'
import { ApiKey } from '../entity/api_key'
import { getRepository } from '../entity/utils'
import { env } from '../env'
import { Claims, ClaimsToSet } from '../resolvers/types'
import { logger } from './logger'

View file

@ -5,10 +5,9 @@ import path from 'path'
import _ from 'underscore'
import slugify from 'voca/slugify'
import wordsCounter from 'word-counting'
import { RegistrationType, UserData } from '../datalayer/user/model'
import { updatePage } from '../elastic/pages'
import { ArticleSavingRequestStatus, Page } from '../elastic/types'
import { User } from '../entity/user'
import { RegistrationType, User } from '../entity/user'
import {
ArticleSavingRequest,
CreateArticleError,
@ -116,7 +115,7 @@ export const findDelimiter = (
// FIXME: Remove this Date stub after nullable types will be fixed
export const userDataToUser = (
user: Merge<
UserData,
User,
{
isFriend?: boolean
followersCount?: number
@ -156,15 +155,11 @@ export const userDataToUser = (
followersCount: user.followersCount || 0,
isFullUser: true,
viewerIsFollowing: user.viewerIsFollowing || user.isFriend || false,
picture: user.profile.picture_url,
picture: user.profile.pictureUrl,
sharedArticles: [],
sharedArticlesCount: user.sharedArticlesCount || 0,
sharedHighlightsCount: user.sharedHighlightsCount || 0,
sharedNotesCount: user.sharedNotesCount || 0,
profile: {
...user.profile,
pictureUrl: user.profile.picture_url,
},
})
export const generateSlug = (title: string): string => {

View file

@ -16,8 +16,8 @@ import { ILike } from 'typeorm'
import { promisify } from 'util'
import { v4 as uuid } from 'uuid'
import { Highlight } from '../elastic/types'
import { getRepository } from '../entity'
import { User } from '../entity/user'
import { getRepository } from '../entity/utils'
import { env } from '../env'
import { PageType, PreparedDocumentInput } from '../generated/graphql'
import { ArticleFormat } from '../resolvers/article'

View file

@ -1,6 +1,7 @@
import Postgrator from 'postgrator'
import { FindOptionsWhere } from 'typeorm'
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'
import { getRepository, setClaims } from '../src/entity'
import { Integration } from '../src/entity/integration'
import { Label } from '../src/entity/label'
import { Link } from '../src/entity/link'
@ -11,7 +12,6 @@ import { Reminder } from '../src/entity/reminder'
import { Subscription } from '../src/entity/subscription'
import { User } from '../src/entity/user'
import { UserDeviceToken } from '../src/entity/user_device_tokens'
import { getRepository, setClaims } from '../src/entity/utils'
import { SubscriptionStatus, SubscriptionType } from '../src/generated/graphql'
import { AppDataSource } from '../src/server'
import { createUser } from '../src/services/create_user'

View file

@ -3,7 +3,7 @@ import { createTestUser, deleteTestUser } from '../db'
import { graphqlRequest, request } from '../util'
import { expect } from 'chai'
import supertest from 'supertest'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { ApiKey } from '../../src/entity/api_key'
const testAPIKey = (apiKey: string): supertest.Test => {

View file

@ -22,9 +22,9 @@ import {
PageContext,
PageType,
} from '../../src/elastic/types'
import { getRepository } from '../../src/entity'
import { UploadFile } from '../../src/entity/upload_file'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import {
BulkActionType,
SyncUpdatedItemEdge,

View file

@ -2,9 +2,9 @@ import { expect } from 'chai'
import * as jwt from 'jsonwebtoken'
import 'mocha'
import sinon, { SinonFakeTimers } from 'sinon'
import { getRepository } from '../../src/entity'
import { Feature } from '../../src/entity/feature'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { env } from '../../src/env'
import { createTestUser, deleteTestUser } from '../db'
import { graphqlRequest, request } from '../util'

View file

@ -4,7 +4,7 @@ import { createTestUser, deleteTestIntegrations, deleteTestUser } from '../db'
import { generateFakeUuid, graphqlRequest, request } from '../util'
import { SetIntegrationErrorCode } from '../../src/generated/graphql'
import chai, { expect } from 'chai'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { Integration } from '../../src/entity/integration'
import nock from 'nock'
import { READWISE_API_URL } from '../../src/services/integrations/readwise'

View file

@ -20,7 +20,7 @@ import {
Page,
PageContext,
} from '../../src/elastic/types'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { deletePage, getPageById } from '../../src/elastic/pages'
import { createPubSubClient } from '../../src/datalayer/pubsub'
import {

View file

@ -14,7 +14,7 @@ import {
SubscriptionStatus,
} from '../../src/generated/graphql'
import 'mocha'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
describe('Newsletters API', () => {
let user: User

View file

@ -3,7 +3,7 @@ import { expect } from 'chai'
import { User } from '../../src/entity/user'
import { createTestUser, deleteTestUser } from '../db'
import { graphqlRequest, request } from '../util'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { ReceivedEmail } from '../../src/entity/received_email'
import { NewsletterEmail } from '../../src/entity/newsletter_email'
import sinon from 'sinon'

View file

@ -5,7 +5,7 @@ import { PageContext } from '../../src/elastic/types'
import { createTestUser, deleteTestUser } from '../db'
import { graphqlRequest, request } from '../util'
import { createPubSubClient } from '../../src/datalayer/pubsub'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { SearchHistory } from '../../src/entity/search_history'
describe('recent_searches resolver', () => {

View file

@ -5,7 +5,7 @@ import { createTestElasticPage, graphqlRequest, request } from '../util'
import { ReportType } from '../../src/generated/graphql'
import { ContentDisplayReport } from '../../src/entity/reports/content_display_report'
import { expect } from 'chai'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
describe('Report API', () => {
let user: User

View file

@ -3,7 +3,7 @@ import { expect } from 'chai'
import { graphqlRequest, request } from '../util'
import { User } from '../../src/entity/user'
import { createTestUser, deleteTestUser } from '../db'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { Rule, RuleAction, RuleActionType } from '../../src/entity/rule'
describe('Rules Resolver', () => {

View file

@ -5,7 +5,7 @@ import sinonChai from 'sinon-chai'
import { NewsletterEmail } from '../../src/entity/newsletter_email'
import { Subscription } from '../../src/entity/subscription'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import {
SubscriptionStatus,
SubscriptionType,

View file

@ -11,7 +11,7 @@ import { UserDeviceToken } from '../../src/entity/user_device_tokens'
import { SetDeviceTokenErrorCode } from '../../src/generated/graphql'
import 'mocha'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
describe('Device tokens API', () => {
let authToken: string

View file

@ -11,7 +11,7 @@ import { Page } from '../../src/entity/page'
import { Link } from '../../src/entity/link'
import { Highlight } from '../../src/entity/highlight'
import 'mocha'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { User } from '../../src/entity/user'
describe('User feed article API', () => {

View file

@ -5,7 +5,7 @@ import 'mocha'
import { User } from '../../src/entity/user'
import { WebhookEvent } from '../../src/generated/graphql'
import { Webhook } from '../../src/entity/webhook'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
describe('Webhooks API', () => {
let user: User

View file

@ -5,8 +5,8 @@ import sinonChai from 'sinon-chai'
import supertest from 'supertest'
import { StatusType } from '../../src/datalayer/user/model'
import { searchPages } from '../../src/elastic/pages'
import { getRepository } from '../../src/entity'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { AuthProvider } from '../../src/routers/auth/auth_types'
import { createPendingUserToken } from '../../src/routers/auth/jwt_helpers'
import {

View file

@ -11,7 +11,7 @@ import { request } from '../util'
import * as parser from '../../src/utils/parser'
import * as sendNotification from '../../src/utils/sendNotification'
import * as sendEmail from '../../src/utils/sendEmail'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { ReceivedEmail } from '../../src/entity/received_email'
import * as jwt from 'jsonwebtoken'

View file

@ -16,9 +16,9 @@ import {
Page,
PageContext,
} from '../../src/elastic/types'
import { getRepository } from '../../src/entity'
import { Integration, IntegrationType } from '../../src/entity/integration'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { env } from '../../src/env'
import { getHighlightUrl } from '../../src/services/highlights'
import { READWISE_API_URL } from '../../src/services/integrations/readwise'

View file

@ -2,7 +2,7 @@ import { createTestUser, deleteTestUser } from '../db'
import { request } from '../util'
import { User } from '../../src/entity/user'
import 'mocha'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { Webhook } from '../../src/entity/webhook'
import { expect } from 'chai'
import nock from 'nock'

View file

@ -2,11 +2,8 @@ import 'mocha'
import chai, { expect } from 'chai'
import { createTestUser, deleteTestUser } from '../db'
import sinonChai from 'sinon-chai'
import sinon from 'sinon'
import * as util from '../../src/utils/sendEmail'
import { MailDataRequired } from '@sendgrid/helpers/classes/mail'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { ContentDisplayReport } from '../../src/entity/reports/content_display_report'
import { saveContentDisplayReport } from '../../src/services/reports'
import { ReportType } from '../../src/generated/graphql'

View file

@ -11,7 +11,6 @@ import {
getUserFollowers,
getUserFollowing,
} from '../../src/services/followers'
import { StatusType } from '../../src/datalayer/user/model'
import sinonChai from 'sinon-chai'
import sinon from 'sinon'
import * as util from '../../src/utils/sendEmail'
@ -19,6 +18,7 @@ import { MailDataRequired } from '@sendgrid/helpers/classes/mail'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { Filter } from "../../src/entity/filter"
import { getRepository } from '../../src/entity'
chai.use(sinonChai)

View file

@ -11,7 +11,7 @@ import { LinkLabel } from '../../src/entity/link_label'
import { Label } from '../../src/entity/label'
import { Link } from '../../src/entity/link'
import { labelsLoader } from '../../src/services/labels'
import { getRepository } from '../../src/entity/utils'
import { getRepository } from '../../src/entity'
import { User } from '../../src/entity/user'
describe('batch get labels from linkIds', () => {

View file

@ -3,11 +3,11 @@ import 'mocha'
import nock from 'nock'
import { createPubSubClient } from '../../src/datalayer/pubsub'
import { getPageByParam } from '../../src/elastic/pages'
import { getRepository } from '../../src/entity'
import { NewsletterEmail } from '../../src/entity/newsletter_email'
import { ReceivedEmail } from '../../src/entity/received_email'
import { Subscription } from '../../src/entity/subscription'
import { User } from '../../src/entity/user'
import { getRepository } from '../../src/entity/utils'
import { createNewsletterEmail } from '../../src/services/newsletters'
import { SaveContext } from '../../src/services/save_email'
import { saveNewsletterEmail } from '../../src/services/save_newsletter_email'