Add labels and state to saveUrl API
This commit is contained in:
@ -2187,14 +2187,15 @@ export type SaveError = {
|
||||
|
||||
export enum SaveErrorCode {
|
||||
EmbeddedHighlightFailed = 'EMBEDDED_HIGHLIGHT_FAILED',
|
||||
EmbeddedLabelFailed = 'EMBEDDED_LABEL_FAILED',
|
||||
Unauthorized = 'UNAUTHORIZED',
|
||||
Unknown = 'UNKNOWN'
|
||||
}
|
||||
|
||||
export type SaveFileInput = {
|
||||
clientRequestId: Scalars['ID'];
|
||||
labels?: InputMaybe<Array<CreateLabelInput>>;
|
||||
source: Scalars['String'];
|
||||
state?: InputMaybe<ArticleSavingRequestStatus>;
|
||||
uploadFileId: Scalars['ID'];
|
||||
url: Scalars['String'];
|
||||
};
|
||||
@ -2245,7 +2246,9 @@ export type SaveSuccess = {
|
||||
|
||||
export type SaveUrlInput = {
|
||||
clientRequestId: Scalars['ID'];
|
||||
labels?: InputMaybe<Array<CreateLabelInput>>;
|
||||
source: Scalars['String'];
|
||||
state?: InputMaybe<ArticleSavingRequestStatus>;
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
@ -1584,14 +1584,15 @@ type SaveError {
|
||||
|
||||
enum SaveErrorCode {
|
||||
EMBEDDED_HIGHLIGHT_FAILED
|
||||
EMBEDDED_LABEL_FAILED
|
||||
UNAUTHORIZED
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
input SaveFileInput {
|
||||
clientRequestId: ID!
|
||||
labels: [CreateLabelInput!]
|
||||
source: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
uploadFileId: ID!
|
||||
url: String!
|
||||
}
|
||||
@ -1639,7 +1640,9 @@ type SaveSuccess {
|
||||
|
||||
input SaveUrlInput {
|
||||
clientRequestId: ID!
|
||||
labels: [CreateLabelInput!]
|
||||
source: String!
|
||||
state: ArticleSavingRequestStatus
|
||||
url: String!
|
||||
}
|
||||
|
||||
|
||||
@ -248,7 +248,7 @@ export const createArticleResolver = authorized<
|
||||
source !== 'puppeteer-parse' &&
|
||||
FORCE_PUPPETEER_URLS.some((regex) => regex.test(url))
|
||||
) {
|
||||
await createPageSaveRequest(uid, url, models)
|
||||
await createPageSaveRequest({ userId: uid, url })
|
||||
return DUMMY_RESPONSE
|
||||
} else if (!skipParsing && preparedDocument?.document) {
|
||||
const parseResults = await traceAs<Promise<ParsedContentPuppeteer>>(
|
||||
@ -264,7 +264,7 @@ export const createArticleResolver = authorized<
|
||||
} else if (!preparedDocument?.document) {
|
||||
// We have a URL but no document, so we try to send this to puppeteer
|
||||
// and return a dummy response.
|
||||
await createPageSaveRequest(uid, url, models)
|
||||
await createPageSaveRequest({ userId: uid, url })
|
||||
return DUMMY_RESPONSE
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export const createArticleSavingRequestResolver = authorized<
|
||||
CreateArticleSavingRequestSuccess,
|
||||
CreateArticleSavingRequestError,
|
||||
MutationCreateArticleSavingRequestArgs
|
||||
>(async (_, { input: { url } }, { models, claims, pubsub }) => {
|
||||
>(async (_, { input: { url } }, { claims, pubsub }) => {
|
||||
analytics.track({
|
||||
userId: claims.uid,
|
||||
event: 'link_saved',
|
||||
@ -37,7 +37,11 @@ export const createArticleSavingRequestResolver = authorized<
|
||||
})
|
||||
|
||||
try {
|
||||
const request = await createPageSaveRequest(claims.uid, url, models, pubsub)
|
||||
const request = await createPageSaveRequest({
|
||||
userId: claims.uid,
|
||||
url,
|
||||
pubsub,
|
||||
})
|
||||
return {
|
||||
articleSavingRequest: request,
|
||||
}
|
||||
|
||||
@ -59,8 +59,7 @@ export function articleRouter() {
|
||||
return res.status(400).send({ errorCode: 'BAD_DATA' })
|
||||
}
|
||||
|
||||
const models = initModels(kx, false)
|
||||
const result = await createPageSaveRequest(uid, url, models)
|
||||
const result = await createPageSaveRequest({ userId: uid, url })
|
||||
|
||||
if (isSiteBlockedForParse(url)) {
|
||||
return res
|
||||
|
||||
@ -3,9 +3,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import express from 'express'
|
||||
import { readPushSubscription } from '../../datalayer/pubsub'
|
||||
import { kx } from '../../datalayer/knex_config'
|
||||
import { createPageSaveRequest } from '../../services/create_page_save_request'
|
||||
import { initModels } from '../../server'
|
||||
|
||||
interface CreateLinkRequestMessage {
|
||||
url: string
|
||||
@ -39,10 +37,11 @@ export function linkServiceRouter() {
|
||||
}
|
||||
const msg = data as CreateLinkRequestMessage
|
||||
|
||||
const models = initModels(kx, false)
|
||||
|
||||
try {
|
||||
const request = await createPageSaveRequest(msg.userId, msg.url, models)
|
||||
const request = await createPageSaveRequest({
|
||||
userId: msg.userId,
|
||||
url: msg.url,
|
||||
})
|
||||
console.log('create link request', request)
|
||||
|
||||
res.status(200).send(request)
|
||||
|
||||
@ -513,7 +513,6 @@ const schema = gql`
|
||||
UNKNOWN
|
||||
UNAUTHORIZED
|
||||
EMBEDDED_HIGHLIGHT_FAILED
|
||||
EMBEDDED_LABEL_FAILED
|
||||
}
|
||||
|
||||
type SaveError {
|
||||
@ -531,6 +530,8 @@ const schema = gql`
|
||||
source: String!
|
||||
clientRequestId: ID!
|
||||
uploadFileId: ID!
|
||||
state: ArticleSavingRequestStatus
|
||||
labels: [CreateLabelInput!]
|
||||
}
|
||||
|
||||
input ParseResult {
|
||||
@ -563,6 +564,8 @@ const schema = gql`
|
||||
url: String!
|
||||
source: String!
|
||||
clientRequestId: ID!
|
||||
state: ArticleSavingRequestStatus
|
||||
labels: [CreateLabelInput!]
|
||||
}
|
||||
|
||||
union SaveResult = SaveSuccess | SaveError
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
getPageByParam,
|
||||
updatePage,
|
||||
} from '../elastic/pages'
|
||||
import { ArticleSavingRequestStatus, PageType } from '../elastic/types'
|
||||
import { ArticleSavingRequestStatus, Label, PageType } from '../elastic/types'
|
||||
import {
|
||||
ArticleSavingRequest,
|
||||
CreateArticleSavingRequestErrorCode,
|
||||
@ -17,6 +17,19 @@ import {
|
||||
import { DataModels } from '../resolvers/types'
|
||||
import { enqueueParseRequest } from '../utils/createTask'
|
||||
import { generateSlug, pageToArticleSavingRequest } from '../utils/helpers'
|
||||
import * as privateIpLib from 'private-ip'
|
||||
import { getRepository } from '../entity/utils'
|
||||
import { User } from '../entity/user'
|
||||
|
||||
interface PageSaveRequest {
|
||||
userId: string
|
||||
url: string
|
||||
pubsub?: PubsubClient
|
||||
articleSavingRequestId?: string
|
||||
archivedAt?: Date | null
|
||||
labels?: Label[]
|
||||
priority?: 'low' | 'high'
|
||||
}
|
||||
|
||||
const SAVING_CONTENT = 'Your link is being saved...'
|
||||
|
||||
@ -58,15 +71,15 @@ export const validateUrl = (url: string): URL => {
|
||||
return u
|
||||
}
|
||||
|
||||
export const createPageSaveRequest = async (
|
||||
userId: string,
|
||||
url: string,
|
||||
models: DataModels,
|
||||
pubsub: PubsubClient = createPubSubClient(),
|
||||
export const createPageSaveRequest = async ({
|
||||
userId,
|
||||
url,
|
||||
pubsub = createPubSubClient(),
|
||||
articleSavingRequestId = uuidv4(),
|
||||
archivedAt?: Date | null,
|
||||
priority?: 'low' | 'high'
|
||||
): Promise<ArticleSavingRequest> => {
|
||||
archivedAt,
|
||||
priority,
|
||||
labels,
|
||||
}: PageSaveRequest): Promise<ArticleSavingRequest> => {
|
||||
try {
|
||||
validateUrl(url)
|
||||
} catch (error) {
|
||||
@ -76,7 +89,10 @@ export const createPageSaveRequest = async (
|
||||
})
|
||||
}
|
||||
|
||||
const user = await models.user.get(userId)
|
||||
const user = await getRepository(User).findOne({
|
||||
where: { id: userId },
|
||||
relations: ['profile'],
|
||||
})
|
||||
if (!user) {
|
||||
console.log('User not found', userId)
|
||||
return Promise.reject({
|
||||
@ -118,6 +134,7 @@ export const createPageSaveRequest = async (
|
||||
createdAt: new Date(),
|
||||
savedAt: new Date(),
|
||||
archivedAt,
|
||||
labels,
|
||||
}
|
||||
|
||||
// create processing page
|
||||
|
||||
@ -2,11 +2,12 @@ import { Label } from '../entity/label'
|
||||
import { ILike, In } from 'typeorm'
|
||||
import { PageContext } from '../elastic/types'
|
||||
import { User } from '../entity/user'
|
||||
import { addLabelInPage, updateLabelsInPage } from '../elastic/labels'
|
||||
import { addLabelInPage } from '../elastic/labels'
|
||||
import { getRepository } from '../entity/utils'
|
||||
import { Link } from '../entity/link'
|
||||
import DataLoader from 'dataloader'
|
||||
import { generateRandomColor } from '../utils/helpers'
|
||||
import { CreateLabelInput } from '../generated/graphql'
|
||||
|
||||
const batchGetLabelsFromLinkIds = async (
|
||||
linkIds: readonly string[]
|
||||
@ -104,21 +105,16 @@ export const createLabel = async (
|
||||
})
|
||||
}
|
||||
|
||||
export const addLabelsToNewPage = async (
|
||||
export const createLabels = async (
|
||||
ctx: PageContext,
|
||||
pageId: string,
|
||||
labels: {
|
||||
name: string
|
||||
color?: string | null
|
||||
description?: string | null
|
||||
}[]
|
||||
): Promise<boolean> => {
|
||||
labels: CreateLabelInput[]
|
||||
): Promise<Label[]> => {
|
||||
const user = await getRepository(User).findOneBy({
|
||||
id: ctx.uid,
|
||||
})
|
||||
if (!user) {
|
||||
console.log('user not found')
|
||||
return false
|
||||
console.error('user not found')
|
||||
return []
|
||||
}
|
||||
|
||||
const labelEntities = await getRepository(Label).findBy({
|
||||
@ -136,10 +132,5 @@ export const addLabelsToNewPage = async (
|
||||
user,
|
||||
}))
|
||||
)
|
||||
// add all labels to page
|
||||
return updateLabelsInPage(
|
||||
pageId,
|
||||
[...newLabelEntities, ...labelEntities],
|
||||
ctx
|
||||
)
|
||||
return [...labelEntities, ...newLabelEntities]
|
||||
}
|
||||
|
||||
@ -36,10 +36,18 @@ export const saveFile = async (
|
||||
|
||||
await getStorageFileDetails(input.uploadFileId, uploadFile.fileName)
|
||||
|
||||
await ctx.authTrx(async (tx) => {
|
||||
const uploadFileData = await ctx.authTrx(async (tx) => {
|
||||
return ctx.models.uploadFile.setFileUploadComplete(input.uploadFileId, tx)
|
||||
})
|
||||
|
||||
if (!uploadFileData) {
|
||||
return {
|
||||
errorCodes: [SaveErrorCode.Unknown],
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: save labels and archive state
|
||||
|
||||
return {
|
||||
clientRequestId: input.clientRequestId,
|
||||
url: `${homePageURL()}/${saver.profile.username}/links/${
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from '../utils/helpers'
|
||||
import { parsePreparedContent } from '../utils/parser'
|
||||
import { createPageSaveRequest } from './create_page_save_request'
|
||||
import { addLabelsToNewPage } from './labels'
|
||||
import { createLabels } from './labels'
|
||||
|
||||
type SaveContext = {
|
||||
pubsub: PubsubClient
|
||||
@ -108,8 +108,13 @@ export const savePage = async (
|
||||
userId: saver.userId,
|
||||
url: articleToSave.url,
|
||||
})
|
||||
// save state
|
||||
const archivedAt =
|
||||
input.state === ArticleSavingRequestStatus.Archived ? new Date() : null
|
||||
// add labels to page
|
||||
const labels = input.labels
|
||||
? await createLabels(ctx, input.labels)
|
||||
: undefined
|
||||
|
||||
if (existingPage) {
|
||||
pageId = existingPage.id
|
||||
@ -124,6 +129,7 @@ export const savePage = async (
|
||||
id: pageId, // we don't want to update the id
|
||||
slug, // we don't want to update the slug
|
||||
createdAt: existingPage.createdAt, // we don't want to update the createdAt
|
||||
labels,
|
||||
},
|
||||
ctx
|
||||
))
|
||||
@ -135,14 +141,14 @@ export const savePage = async (
|
||||
}
|
||||
} else if (shouldParseInBackend(input)) {
|
||||
try {
|
||||
await createPageSaveRequest(
|
||||
saver.userId,
|
||||
articleToSave.url,
|
||||
ctx.models,
|
||||
ctx.pubsub,
|
||||
input.clientRequestId,
|
||||
archivedAt
|
||||
)
|
||||
await createPageSaveRequest({
|
||||
userId: saver.userId,
|
||||
url: articleToSave.url,
|
||||
pubsub: ctx.pubsub,
|
||||
articleSavingRequestId: input.clientRequestId,
|
||||
archivedAt,
|
||||
labels,
|
||||
})
|
||||
} catch (e) {
|
||||
return {
|
||||
errorCodes: [SaveErrorCode.Unknown],
|
||||
@ -154,6 +160,7 @@ export const savePage = async (
|
||||
{
|
||||
...articleToSave,
|
||||
archivedAt,
|
||||
labels,
|
||||
},
|
||||
ctx
|
||||
)
|
||||
@ -183,15 +190,6 @@ export const savePage = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
// add labels to page
|
||||
if (pageId && input.labels) {
|
||||
if (!(await addLabelsToNewPage(ctx, pageId, input.labels))) {
|
||||
return {
|
||||
errorCodes: [SaveErrorCode.EmbeddedLabelFailed],
|
||||
message: 'Failed to save labels',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
clientRequestId: pageId,
|
||||
|
||||
@ -4,6 +4,8 @@ import { homePageURL } from '../env'
|
||||
import { SaveErrorCode, SaveResult, SaveUrlInput } from '../generated/graphql'
|
||||
import { DataModels } from '../resolvers/types'
|
||||
import { createPageSaveRequest } from './create_page_save_request'
|
||||
import { ArticleSavingRequestStatus } from '../elastic/types'
|
||||
import { createLabels } from './labels'
|
||||
|
||||
type SaveContext = {
|
||||
pubsub: PubsubClient
|
||||
@ -16,13 +18,22 @@ export const saveUrl = async (
|
||||
input: SaveUrlInput
|
||||
): Promise<SaveResult> => {
|
||||
try {
|
||||
const pageSaveRequest = await createPageSaveRequest(
|
||||
saver.id,
|
||||
input.url,
|
||||
ctx.models,
|
||||
ctx.pubsub,
|
||||
input.clientRequestId
|
||||
)
|
||||
// save state
|
||||
const archivedAt =
|
||||
input.state === ArticleSavingRequestStatus.Archived ? new Date() : null
|
||||
// add labels to page
|
||||
const labels = input.labels
|
||||
? await createLabels({ ...ctx, uid: saver.id }, input.labels)
|
||||
: undefined
|
||||
|
||||
const pageSaveRequest = await createPageSaveRequest({
|
||||
userId: saver.id,
|
||||
url: input.url,
|
||||
pubsub: ctx.pubsub,
|
||||
articleSavingRequestId: input.clientRequestId,
|
||||
archivedAt,
|
||||
labels,
|
||||
})
|
||||
|
||||
return {
|
||||
clientRequestId: pageSaveRequest.id,
|
||||
|
||||
@ -18,6 +18,7 @@ import path from 'path'
|
||||
import normalizeUrl from 'normalize-url'
|
||||
import wordsCounter from 'word-counting'
|
||||
import _ from 'underscore'
|
||||
import { User } from '../entity/user'
|
||||
|
||||
interface InputObject {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -187,7 +188,7 @@ export const pageError = async (
|
||||
}
|
||||
|
||||
export const pageToArticleSavingRequest = (
|
||||
user: UserData,
|
||||
user: User,
|
||||
page: Page
|
||||
): ArticleSavingRequest => ({
|
||||
...page,
|
||||
|
||||
@ -37,6 +37,8 @@ import {
|
||||
graphqlRequest,
|
||||
request,
|
||||
} from '../util'
|
||||
import sinon from 'sinon'
|
||||
import * as createTask from '../../src/utils/createTask'
|
||||
|
||||
chai.use(chaiString)
|
||||
|
||||
@ -266,7 +268,11 @@ const saveFileQuery = (url: string, uploadFileId: string) => {
|
||||
`
|
||||
}
|
||||
|
||||
const saveUrlQuery = (url: string) => {
|
||||
const saveUrlQuery = (
|
||||
url: string,
|
||||
state: ArticleSavingRequestStatus | null = null,
|
||||
labels: string[] | null = null
|
||||
) => {
|
||||
return `
|
||||
mutation {
|
||||
saveUrl(
|
||||
@ -274,6 +280,12 @@ const saveUrlQuery = (url: string) => {
|
||||
url: "${url}",
|
||||
source: "test",
|
||||
clientRequestId: "${generateFakeUuid()}",
|
||||
state: ${state}
|
||||
labels: ${
|
||||
labels
|
||||
? '[' + labels.map((label) => `{ name: "${label}" }`) + ']'
|
||||
: null
|
||||
}
|
||||
}
|
||||
) {
|
||||
... on SaveSuccess {
|
||||
@ -650,15 +662,23 @@ describe('Article API', () => {
|
||||
let query = ''
|
||||
let url = 'https://blog.omnivore.app/new-url-1'
|
||||
|
||||
before(() => {
|
||||
sinon.replace(createTask, 'enqueueParseRequest', sinon.fake.resolves(''))
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
query = saveUrlQuery(url)
|
||||
})
|
||||
|
||||
context('when we save a new url', () => {
|
||||
after(async () => {
|
||||
await deletePagesByParam({ url }, ctx)
|
||||
})
|
||||
after(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deletePagesByParam({ url }, ctx)
|
||||
})
|
||||
|
||||
context('when we save a new url', () => {
|
||||
it('should return a slugged url', async () => {
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
expect(res.body.data.saveUrl.url).to.startsWith(
|
||||
@ -666,6 +686,23 @@ describe('Article API', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
context('when we save labels', () => {
|
||||
it('saves the labels and archives the page', async () => {
|
||||
url = 'https://blog.omnivore.app/new-url-2'
|
||||
const state = ArticleSavingRequestStatus.Archived
|
||||
const labels = ['test name', 'test name 2']
|
||||
await graphqlRequest(
|
||||
saveUrlQuery(url, state, labels),
|
||||
authToken
|
||||
).expect(200)
|
||||
await refreshIndex()
|
||||
|
||||
const savedPage = await getPageByParam({ url })
|
||||
expect(savedPage?.archivedAt).to.not.be.null
|
||||
expect(savedPage?.labels?.map((l) => l.name)).to.eql(labels)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setBookmarkArticle', () => {
|
||||
|
||||
Reference in New Issue
Block a user