add Newsletter label to the page created by newsletters email

This commit is contained in:
Hongbo Wu
2022-03-16 18:39:07 +08:00
parent 27157006c1
commit f412758040
20 changed files with 159 additions and 158 deletions

View File

@ -37,8 +37,11 @@ export const createPubSubClient = (): PubsubClient => {
Buffer.from(JSON.stringify({ userId, email, name, username }))
)
},
pageSaved: (page: Partial<Page>): Promise<void> => {
return publish('pageSaved', Buffer.from(JSON.stringify(page)))
pageSaved: (page: Partial<Page>, userId: string): Promise<void> => {
return publish(
'pageSaved',
Buffer.from(JSON.stringify({ ...page, userId }))
)
},
pageCreated: (page: Page): Promise<void> => {
return publish('pageCreated', Buffer.from(JSON.stringify(page)))
@ -70,7 +73,7 @@ export interface PubsubClient {
username: string
) => Promise<void>
pageCreated: (page: Page) => Promise<void>
pageSaved: (page: Partial<Page>) => Promise<void>
pageSaved: (page: Partial<Page>, userId: string) => Promise<void>
pageDeleted: (id: string, userId: string) => Promise<void>
reportSubmitted(
submitterId: string | undefined,

View File

@ -191,7 +191,7 @@ export const updatePage = async (
if (body.result !== 'updated') return false
await ctx.pubsub.pageSaved(page)
await ctx.pubsub.pageSaved(page, ctx.uid)
return true
} catch (e) {
@ -202,7 +202,6 @@ export const updatePage = async (
export const deletePage = async (
id: string,
userId: string,
ctx: PageContext
): Promise<boolean> => {
try {
@ -214,7 +213,7 @@ export const deletePage = async (
if (body.deleted === 0) return false
await ctx.pubsub.pageDeleted(id, userId)
await ctx.pubsub.pageDeleted(id, ctx.uid)
return true
} catch (e) {

View File

@ -151,4 +151,5 @@ export type ParamSet = PickTuple<Page, typeof keys>
export interface PageContext {
pubsub: PubsubClient
refresh?: boolean
uid: string
}

View File

@ -330,13 +330,13 @@ export const createArticleResolver = authorized<
existingPage.url = uploadFileUrlOverride || articleToSave.url
existingPage.hash = articleToSave.hash
await updatePage(existingPage.id, existingPage, ctx)
await updatePage(existingPage.id, existingPage, { ...ctx, uid })
log.info('page updated in elastic', existingPage.id)
articleToSave = existingPage
} else {
// create new page in elastic
const pageId = await createPage(articleToSave, ctx)
const pageId = await createPage(articleToSave, { ...ctx, uid })
if (!pageId) {
return articleSavingRequestError(
@ -626,7 +626,7 @@ export const setBookmarkArticleResolver = authorized<
return { errorCodes: [SetBookmarkArticleErrorCode.NotFound] }
}
await deletePage(userArticleRemoved.id, uid, { pubsub })
await deletePage(userArticleRemoved.id, { pubsub, uid })
const highlightsUnshared = await authTrx(async (tx) => {
return models.highlight.unshareAllHighlights(articleID, uid, tx)
@ -660,7 +660,7 @@ export const setBookmarkArticleResolver = authorized<
userId: uid,
slug: generateSlug(article.title),
}
await updatePage(articleID, userArticle, { pubsub })
await updatePage(articleID, userArticle, { pubsub, uid })
log.info('Article bookmarked', {
article: Object.assign({}, article, {
@ -737,7 +737,7 @@ export const saveArticleReadingProgressResolver = authorized<
: userArticleRecord.readingProgressAnchorIndex,
})
shouldUpdate && (await updatePage(id, updatedArticle, { pubsub }))
shouldUpdate && (await updatePage(id, updatedArticle, { pubsub, uid }))
return {
updatedArticle: {

View File

@ -79,6 +79,7 @@ export const createLabelResolver = authorized<
// Check if label already exists ignoring case of name
const existingLabel = await getRepository(Label).findOne({
where: {
user,
name: ILike(name),
},
})
@ -161,7 +162,10 @@ export const deleteLabelResolver = authorized<
}
// delete label in elastic pages
await deleteLabelInPages(uid, label.name, { pubsub: createPubSubClient() })
await deleteLabelInPages(uid, label.name, {
pubsub: createPubSubClient(),
uid,
})
analytics.track({
userId: uid,
@ -225,7 +229,7 @@ export const setLabelsResolver = authorized<
{
labels,
},
{ pubsub }
{ pubsub, uid }
)
analytics.track({

View File

@ -81,7 +81,7 @@ export const setLinkArchivedResolver = authorized<
{
archivedAt: args.input.archived ? new Date() : null,
},
{ pubsub }
{ pubsub, uid: claims.uid }
)
} catch (e) {
return {

View File

@ -29,7 +29,7 @@ export const savePageResolver = authorized<
}
return savePage(
ctx,
{ ...ctx, uid },
{ userId: user.id, username: user.profile.username },
input
)
@ -70,5 +70,5 @@ export const saveFileResolver = authorized<
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
return (await saveFile(ctx, user, input)) as SaveSuccess
return (await saveFile({ ...ctx, uid }, user, input)) as SaveSuccess
})

View File

@ -67,6 +67,7 @@ export function contentServiceRouter() {
const result = await updatePage(page.id, pageToUpdate, {
pubsub: createPubSubClient(),
uid: page.userId,
})
console.log(
'Updating article text',

View File

@ -159,6 +159,7 @@ export function pdfAttachmentsRouter() {
const pageId = await createPage(articleToSave, {
pubsub: createPubSubClient(),
uid: user.id,
})
res.send({ id: pageId })

View File

@ -1,7 +1,10 @@
import DataLoader from 'dataloader'
import { Label } from '../entity/label'
import { getRepository, In } from 'typeorm'
import { getRepository, ILike, In } from 'typeorm'
import { Link } from '../entity/link'
import { updatePage } from '../elastic'
import { PageContext } from '../elastic/types'
import { User } from '../entity/user'
const batchGetLabelsFromLinkIds = async (
linkIds: readonly string[]
@ -17,3 +20,41 @@ const batchGetLabelsFromLinkIds = async (
}
export const labelsLoader = new DataLoader(batchGetLabelsFromLinkIds)
export const addLabelToPage = async (
ctx: PageContext,
pageId: string,
label: {
name: string
color: string
description?: string
}
): Promise<boolean> => {
const user = await getRepository(User).findOne(ctx.uid)
let labelEntity = await getRepository(Label).findOne({
where: {
user: user,
name: ILike(label.name),
},
})
if (!labelEntity) {
console.log('creating new label', label.name)
labelEntity = await getRepository(Label).save({
...label,
user,
})
}
console.log('adding label to page', label.name, pageId)
return updatePage(
pageId,
{
labels: [labelEntity],
},
ctx
)
}

View File

@ -1,5 +1,3 @@
import { PubsubClient } from '../datalayer/pubsub'
import { DataModels } from '../resolvers/types'
import { generateSlug, stringToHash, validatedDate } from '../utils/helpers'
import {
parseOriginalContent,
@ -7,13 +5,13 @@ import {
parseUrlMetadata,
} from '../utils/parser'
import normalizeUrl from 'normalize-url'
import { kx } from '../datalayer/knex_config'
import { UserArticleData } from '../datalayer/links/model'
import { setClaims } from '../datalayer/helpers'
import { PubsubClient } from '../datalayer/pubsub'
import { Page } from '../elastic/types'
import { createPage, getPageByParam, updatePage } from '../elastic'
export type SaveContext = {
pubsub: PubsubClient
models: DataModels
uid: string
}
export type SaveEmailInput = {
@ -25,9 +23,8 @@ export type SaveEmailInput = {
export const saveEmail = async (
ctx: SaveContext,
saverId: string,
input: SaveEmailInput
): Promise<UserArticleData | undefined> => {
): Promise<Page | undefined> => {
const url = input.url
const parseResult = await parsePreparedContent(
url,
@ -47,7 +44,9 @@ export const saveEmail = async (
const pageType = parseOriginalContent(url, input.originalContent)
const metadata = await parseUrlMetadata(url)
const articleToSave = {
const articleToSave: Page = {
id: '',
userId: ctx.uid,
originalHtml: input.originalContent,
content: content,
description: metadata?.description || parseResult.parsedContent?.excerpt,
@ -62,58 +61,27 @@ export const saveEmail = async (
hash: stringToHash(content),
image: metadata?.previewImage || parseResult.parsedContent?.previewImage,
publishedAt: validatedDate(parseResult.parsedContent?.publishedDate),
slug: slug,
createdAt: new Date(),
}
if (parseResult.canonicalUrl && parseResult.domContent) {
// await ctx.pubsub.pageSaved(
// saverId,
// parseResult.canonicalUrl,
// parseResult.domContent
// )
const page = await getPageByParam({ url: articleToSave.url })
if (page) {
const result = await updatePage(page.id, { archivedAt: null }, ctx)
console.log('updated page from email', result)
return page
}
const matchedUserArticleRecord = await ctx.models.userArticle.getByParameters(
saverId,
{
articleUrl: articleToSave.url,
}
)
const pageId = await createPage(articleToSave, ctx)
if (!pageId) {
console.log('failed to create new page')
let result: UserArticleData | undefined = undefined
if (matchedUserArticleRecord) {
// await ctx.pubsub.pageCreated(saverId, url, input.originalContent)
await kx.transaction(async (tx) => {
await setClaims(tx, saverId)
result = await ctx.models.userArticle.update(
matchedUserArticleRecord.id,
{
savedAt: new Date(),
archivedAt: null,
}
)
})
console.log('created matched email', result)
} else {
// await ctx.pubsub.pageCreated(saverId, url, input.originalContent)
await kx.transaction(async (tx) => {
await setClaims(tx, saverId)
const articleRecord = await ctx.models.article.create(articleToSave, tx)
result = await ctx.models.userArticle.create(
{
userId: saverId,
slug: slug,
articleId: articleRecord.id,
articleUrl: articleRecord.url,
articleHash: articleRecord.hash,
},
tx
)
console.log('created new email', result)
})
return undefined
}
return result
console.log('created new page from email', pageId)
articleToSave.id = pageId
return articleToSave
}

View File

@ -22,6 +22,7 @@ type SaveContext = {
cb: (tx: Knex.Transaction) => TResult,
userRole?: string
) => Promise<TResult>
uid: string
}
export const saveFile = async (
@ -44,11 +45,7 @@ export const saveFile = async (
}
}
const savingRequest = await createSavingRequest(
ctx,
saver.id,
input.clientRequestId
)
const savingRequest = await createSavingRequest(ctx, input.clientRequestId)
const uploadFileDetails = await getStorageFileDetails(
input.uploadFileId,

View File

@ -1,17 +1,15 @@
import { MulticastMessage } from 'firebase-admin/messaging'
import { kx } from '../datalayer/knex_config'
import { createPubSubClient } from '../datalayer/pubsub'
import { UserDeviceToken } from '../entity/user_device_tokens'
import { env } from '../env'
import { ContentReader } from '../generated/graphql'
import { initModels } from '../server'
import { analytics } from '../utils/analytics'
import { sendMulticastPushNotifications } from '../utils/sendNotification'
import { getNewsletterEmail } from './newsletters'
import { SaveContext, saveEmail, SaveEmailInput } from './save_email'
import { getDeviceTokensByUserId } from './user_device_tokens'
import { getPageByParam } from '../elastic'
import { Page } from '../elastic/types'
import { addLabelToPage } from './labels'
interface NewsletterMessage {
email: string
@ -46,9 +44,10 @@ export const saveNewsletterEmail = async (
})
const ctx: SaveContext = {
models: initModels(kx, false),
pubsub: createPubSubClient(),
uid: newsletterEmail.user.id,
}
const input: SaveEmailInput = {
url: data.url,
originalContent: data.content,
@ -56,12 +55,19 @@ export const saveNewsletterEmail = async (
author: data.author,
}
const result = await saveEmail(ctx, newsletterEmail.user.id, input)
if (!result) {
const page = await saveEmail(ctx, input)
if (!page) {
console.log('newsletter not created:', input)
return false
}
// add newsletters label to page
const result = await addLabelToPage(ctx, page.id, {
name: 'Newsletter',
color: '#07D2D1',
})
console.log('newsletter label added:', result)
// send push notification
const deviceTokens = await getDeviceTokensByUserId(newsletterEmail.user.id)
@ -70,22 +76,8 @@ export const saveNewsletterEmail = async (
return true
}
const link = await getPageByParam({
_id: result.articleId,
userId: newsletterEmail.user.id,
})
if (!link) {
console.log(
'Newsletter link not found:',
newsletterEmail.user.id,
result.articleId
)
return true
}
if (deviceTokens.length) {
const multicastMessage = messageForLink(link, deviceTokens)
const multicastMessage = messageForLink(page, deviceTokens)
await sendMulticastPushNotifications(
newsletterEmail.user.id,
multicastMessage,

View File

@ -20,6 +20,7 @@ import { Page } from '../elastic/types'
type SaveContext = {
pubsub: PubsubClient
models: DataModels
uid: string
}
type SaverUserData = {
@ -56,11 +57,10 @@ const shouldParseInBackend = (input: SavePageInput): boolean => {
export const createSavingRequest = (
ctx: SaveContext,
userId: string,
clientRequestId: string
) => {
return ctx.models.articleSavingRequest.create({
userId: userId,
userId: ctx.uid,
id: clientRequestId,
})
}
@ -70,11 +70,7 @@ export const savePage = async (
saver: SaverUserData,
input: SavePageInput
): Promise<SaveResult> => {
const savingRequest = await createSavingRequest(
ctx,
saver.userId,
input.clientRequestId
)
const savingRequest = await createSavingRequest(ctx, input.clientRequestId)
const [slug, croppedPathname] = createSlug(input.url, input.title)
const parseResult = await parsePreparedContent(input.url, {

View File

@ -14,8 +14,12 @@ import { Page, PageContext } from '../../src/elastic/types'
import { createPubSubClient } from '../../src/datalayer/pubsub'
describe('elastic api', () => {
const ctx: PageContext = { pubsub: createPubSubClient(), refresh: true }
const userId = 'userId'
const ctx: PageContext = {
pubsub: createPubSubClient(),
refresh: true,
uid: userId,
}
let page: Page
@ -59,7 +63,7 @@ describe('elastic api', () => {
after(async () => {
// delete the testing page
await deletePage(page.id, userId, ctx)
await deletePage(page.id, ctx)
})
describe('createPage', () => {
@ -67,7 +71,7 @@ describe('elastic api', () => {
after(async () => {
if (newPageId) {
await deletePage(newPageId, userId, ctx)
await deletePage(newPageId, ctx)
}
})

View File

@ -25,7 +25,6 @@ import { createPubSubClient } from '../../src/datalayer/pubsub'
chai.use(chaiString)
const ctx: PageContext = { pubsub: createPubSubClient(), refresh: true }
const archiveLink = async (authToken: string, linkId: string) => {
const query = `
mutation {
@ -244,6 +243,7 @@ describe('Article API', () => {
const username = 'fakeUser'
let authToken: string
let user: User
let ctx: PageContext
before(async () => {
// create test user and login
@ -253,6 +253,12 @@ describe('Article API', () => {
.send({ fakeEmail: user.email })
authToken = res.body.authToken
ctx = {
pubsub: createPubSubClient(),
refresh: true,
uid: user.id,
}
})
after(async () => {
@ -281,7 +287,7 @@ describe('Article API', () => {
})
after(async () => {
await deletePage(pageId, user.id, ctx)
await deletePage(pageId, ctx)
})
it('should create an article', async () => {
@ -320,7 +326,7 @@ describe('Article API', () => {
after(async () => {
if (pageId) {
await deletePage(pageId, user.id, ctx)
await deletePage(pageId, ctx)
}
})
@ -390,7 +396,6 @@ describe('Article API', () => {
await updatePage(
pages[0].id,
{
...pages[0],
labels: [{ id: label.id, name: label.name, color: label.color }],
},
ctx
@ -564,7 +569,7 @@ describe('Article API', () => {
after(async () => {
if (pageId) {
await deletePage(pageId, user.id, ctx)
await deletePage(pageId, ctx)
}
})
@ -612,7 +617,7 @@ describe('Article API', () => {
after(async () => {
if (pageId) {
await deletePage(pageId, user.id, ctx)
await deletePage(pageId, ctx)
}
})

View File

@ -12,10 +12,10 @@ import { User } from '../../src/entity/user'
import chaiString from 'chai-string'
import { deletePage } from '../../src/elastic'
import { createPubSubClient } from '../../src/datalayer/pubsub'
import { PageContext } from '../../src/elastic/types'
chai.use(chaiString)
const ctx = { pubsub: createPubSubClient() }
const createHighlightQuery = (
authToken: string,
linkId: string,
@ -57,6 +57,7 @@ describe('Highlights API', () => {
let authToken: string
let user: User
let pageId: string
let ctx: PageContext
before(async () => {
// create test user and login
@ -67,12 +68,13 @@ describe('Highlights API', () => {
authToken = res.body.authToken
pageId = (await createTestElasticPage(user)).id
ctx = { pubsub: createPubSubClient(), uid: user.id }
})
after(async () => {
await deleteTestUser(username)
if (pageId) {
await deletePage(pageId, user.id, ctx)
await deletePage(pageId, ctx)
}
})

View File

@ -1,16 +1,10 @@
import 'mocha'
import { expect } from 'chai'
import 'chai/register-should'
import {
createTestUser,
deleteTestUser,
} from '../db'
import { createTestUser, deleteTestUser } from '../db'
import { SaveContext, saveEmail } from '../../src/services/save_email'
import { getRepository } from 'typeorm'
import { Link } from '../../src/entity/link'
import { initModels } from '../../src/server'
import { kx } from '../../src/datalayer/knex_config'
import { createPubSubClient } from '../../src/datalayer/pubsub'
import { getPageByParam } from '../../src/elastic'
describe('saveEmail', () => {
const username = 'fakeUser'
@ -21,11 +15,11 @@ describe('saveEmail', () => {
it('doesnt fail if saved twice', async () => {
const user = await createTestUser(username)
const ctx: SaveContext = {
models: initModels(kx, false),
pubsub: createPubSubClient(),
uid: user.id,
}
await saveEmail(ctx, user.id, {
await saveEmail(ctx, {
originalContent: 'fake content',
url: 'https://example.com',
title: 'fake title',
@ -34,7 +28,7 @@ describe('saveEmail', () => {
// This ensures row level security doesnt prevent
// resaving the same URL
const secondResult = await saveEmail(ctx, user.id, {
const secondResult = await saveEmail(ctx, {
originalContent: 'fake content',
url: 'https://example.com',
title: 'fake title',
@ -42,17 +36,15 @@ describe('saveEmail', () => {
})
expect(secondResult).to.not.be.undefined
const links = await getRepository(Link).find({
where: {
user: user,
},
relations: ['page'],
setTimeout(async () => {
const page = await getPageByParam({ userId: user.id })
if (!page) {
expect.fail('page not found')
}
expect(page.url).to.equal('https://example.com')
expect(page.title).to.equal('fake title')
expect(page.author).to.equal('fake author')
expect(page.content).to.contain('fake content')
})
expect(links.length).to.equal(1)
expect(links[0].page.url).to.equal('https://example.com')
expect(links[0].page.title).to.equal('fake title')
expect(links[0].page.author).to.equal('fake author')
expect(links[0].page.content).to.contain('fake content')
}).timeout(10000)
})
})

View File

@ -1,14 +1,10 @@
import 'mocha'
import { expect } from 'chai'
import 'chai/register-should'
import {
createTestUser,
deleteTestUser,
} from '../db'
import { createTestUser, deleteTestUser } from '../db'
import { createNewsletterEmail } from '../../src/services/newsletters'
import { saveNewsletterEmail } from '../../src/services/save_newsletter_email'
import { getRepository } from 'typeorm'
import { Link } from '../../src/entity/link'
import { getPageByParam } from '../../src/elastic'
describe('saveNewsletterEmail', () => {
const username = 'fakeUser'
@ -28,17 +24,15 @@ describe('saveNewsletterEmail', () => {
author: 'fake author',
})
const links = await getRepository(Link).find({
where: {
user: user,
},
relations: ['page'],
setTimeout(async () => {
const page = await getPageByParam({ userId: user.id })
if (!page) {
expect.fail('page not found')
}
expect(page.url).to.equal('https://example.com')
expect(page.title).to.equal('fake title')
expect(page.author).to.equal('fake author')
expect(page.content).to.contain('fake content')
})
expect(links.length).to.equal(1)
expect(links[0].page.url).to.equal('https://example.com')
expect(links[0].page.title).to.equal('fake title')
expect(links[0].page.author).to.equal('fake author')
expect(links[0].page.content).to.contain('fake content')
}).timeout(10000)
})
})

View File

@ -55,6 +55,7 @@ export const createTestElasticPage = async (
const pageId = await createPage(page, {
pubsub: createPubSubClient(),
refresh: true,
uid: user.id,
})
if (pageId) {
page.id = pageId