mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
replace highlight
This commit is contained in:
parent
381e9bc173
commit
c655f78f8c
26 changed files with 794 additions and 815 deletions
|
|
@ -22,7 +22,7 @@ export enum HighlightType {
|
|||
@Entity({ name: 'highlight' })
|
||||
export class Highlight {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id?: string
|
||||
id!: string
|
||||
|
||||
@Column({ type: 'varchar', length: 14 })
|
||||
shortId!: string
|
||||
|
|
@ -36,37 +36,37 @@ export class Highlight {
|
|||
libraryItem!: LibraryItem
|
||||
|
||||
@Column('text')
|
||||
quote!: string
|
||||
quote?: string | null
|
||||
|
||||
@Column({ type: 'varchar', length: 5000 })
|
||||
prefix?: string
|
||||
prefix?: string | null
|
||||
|
||||
@Column({ type: 'varchar', length: 5000 })
|
||||
suffix?: string
|
||||
suffix?: string | null
|
||||
|
||||
@Column('text')
|
||||
patch!: string
|
||||
patch?: string | null
|
||||
|
||||
@Column('text')
|
||||
annotation?: string
|
||||
annotation?: string | null
|
||||
|
||||
@Column('boolean')
|
||||
deleted?: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt?: Date
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt?: Date
|
||||
updatedAt!: Date
|
||||
|
||||
@Column('timestamp')
|
||||
sharedAt?: Date
|
||||
|
||||
@Column('real')
|
||||
highlightPositionPercent!: number
|
||||
highlightPositionPercent?: number | null
|
||||
|
||||
@Column('integer')
|
||||
highlightPositionAnchorIndex!: number
|
||||
highlightPositionAnchorIndex?: number | null
|
||||
|
||||
@Column('enum', {
|
||||
enum: HighlightType,
|
||||
|
|
|
|||
|
|
@ -85,10 +85,10 @@ export class LibraryItem {
|
|||
description?: string | null
|
||||
|
||||
@Column('timestamptz')
|
||||
savedAt?: Date
|
||||
savedAt!: Date
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt?: Date
|
||||
createdAt!: Date
|
||||
|
||||
@Column('timestamptz', { nullable: true })
|
||||
publishedAt?: Date | null
|
||||
|
|
@ -103,7 +103,7 @@ export class LibraryItem {
|
|||
readAt?: Date | null
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt?: Date
|
||||
updatedAt!: Date
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
itemLanguage?: string | null
|
||||
|
|
|
|||
35
packages/api/src/repository/highlight.ts
Normal file
35
packages/api/src/repository/highlight.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { DeepPartial } from 'typeorm'
|
||||
import { entityManager } from '.'
|
||||
import { Highlight } from '../entity/highlight'
|
||||
import { unescapeHtml } from '../utils/helpers'
|
||||
|
||||
const unescapeHighlight = (highlight: DeepPartial<Highlight>) => {
|
||||
// unescape HTML entities
|
||||
highlight.annotation = highlight.annotation
|
||||
? unescapeHtml(highlight.annotation)
|
||||
: undefined
|
||||
highlight.quote = highlight.quote ? unescapeHtml(highlight.quote) : undefined
|
||||
|
||||
return highlight
|
||||
}
|
||||
|
||||
export const highlightRepository = entityManager
|
||||
.getRepository(Highlight)
|
||||
.extend({
|
||||
findById(id: string) {
|
||||
return this.findOneBy({ id })
|
||||
},
|
||||
|
||||
findByLibraryItemId(libraryItemId: string) {
|
||||
return this.findBy({
|
||||
libraryItem: { id: libraryItemId },
|
||||
})
|
||||
},
|
||||
|
||||
createAndSave(highlight: DeepPartial<Highlight>, userId: string) {
|
||||
return this.save({
|
||||
...unescapeHighlight(highlight),
|
||||
user: { id: userId },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
@ -6,14 +6,11 @@ import { Follower } from '../entity/follower'
|
|||
import { Group } from '../entity/groups/group'
|
||||
import { GroupMembership } from '../entity/groups/group_membership'
|
||||
import { Invite } from '../entity/groups/invite'
|
||||
import { Highlight } from '../entity/highlight'
|
||||
import { Integration } from '../entity/integration'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
import { NewsletterEmail } from '../entity/newsletter_email'
|
||||
import { Profile } from '../entity/profile'
|
||||
import { ReceivedEmail } from '../entity/received_email'
|
||||
import { Recommendation } from '../entity/recommendation'
|
||||
import { Reminder } from '../entity/reminder'
|
||||
import { AbuseReport } from '../entity/reports/abuse_report'
|
||||
import { ContentDisplayReport } from '../entity/reports/content_display_report'
|
||||
import { Rule } from '../entity/rule'
|
||||
|
|
@ -37,10 +34,19 @@ export const getRepository = <T>(entity: EntityTarget<T>): Repository<T> => {
|
|||
return entityManager.getRepository(entity)
|
||||
}
|
||||
|
||||
export const authTrx = async <T>(
|
||||
fn: (manager: EntityManager) => Promise<T>,
|
||||
uid = '00000000-0000-0000-0000-000000000000',
|
||||
dbRole = 'omnivore_user'
|
||||
): Promise<T> => {
|
||||
return entityManager.transaction(async (tx) => {
|
||||
await setClaims(tx, uid, dbRole)
|
||||
return fn(tx)
|
||||
})
|
||||
}
|
||||
|
||||
export const entityManager = appDataSource.manager
|
||||
|
||||
export const reminderRepository = getRepository(Reminder)
|
||||
export const libraryItemRepository = getRepository(LibraryItem)
|
||||
export const groupMembershipRepository = getRepository(GroupMembership)
|
||||
export const groupRepository = getRepository(Group)
|
||||
export const inviteRepository = getRepository(Invite)
|
||||
|
|
@ -50,7 +56,6 @@ export const contentDisplayReportRepository =
|
|||
export const featureRepository = getRepository(Feature)
|
||||
export const filterRepository = getRepository(Filter)
|
||||
export const followerRepository = getRepository(Follower)
|
||||
export const highlightRepository = getRepository(Highlight)
|
||||
export const integrationRepository = getRepository(Integration)
|
||||
export const newsletterEmailRepository = getRepository(NewsletterEmail)
|
||||
export const profileRepository = getRepository(Profile)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { In } from 'typeorm'
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
|
||||
import { entityManager } from '.'
|
||||
import { Label } from '../entity/label'
|
||||
import { generateRandomColor } from '../utils/helpers'
|
||||
|
||||
export interface CreateLabelInput {
|
||||
name: string
|
||||
color?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
const INTERNAL_LABELS_WITH_COLOR = new Map<
|
||||
string,
|
||||
{ name: string; color: string }
|
||||
|
|
@ -21,14 +28,7 @@ const isLabelInternal = (name: string): boolean => {
|
|||
return INTERNAL_LABELS_WITH_COLOR.has(name.toLowerCase())
|
||||
}
|
||||
|
||||
const toPartialLabel = (
|
||||
label: {
|
||||
name: string
|
||||
color?: string | null
|
||||
description?: string | null
|
||||
},
|
||||
userId: string
|
||||
) => {
|
||||
const convertToLabel = (label: CreateLabelInput, userId: string) => {
|
||||
return {
|
||||
user: { id: userId },
|
||||
name: label.name,
|
||||
|
|
@ -64,25 +64,20 @@ export const labelRepository = entityManager.getRepository(Label).extend({
|
|||
})
|
||||
},
|
||||
|
||||
createLabel(
|
||||
label: {
|
||||
name: string
|
||||
color?: string | null
|
||||
description?: string | null
|
||||
},
|
||||
userId: string
|
||||
) {
|
||||
return this.save(toPartialLabel(label, userId))
|
||||
createLabel(label: CreateLabelInput, userId: string) {
|
||||
return this.save(convertToLabel(label, userId))
|
||||
},
|
||||
|
||||
createLabels(
|
||||
labels: {
|
||||
name: string
|
||||
color?: string | null
|
||||
description?: string | null
|
||||
}[],
|
||||
userId: string
|
||||
) {
|
||||
return this.save(labels.map((l) => toPartialLabel(l, userId)))
|
||||
createLabels(labels: CreateLabelInput[], userId: string) {
|
||||
return this.save(labels.map((l) => convertToLabel(l, userId)))
|
||||
},
|
||||
|
||||
deleteById(id: string) {
|
||||
return this.delete({ id, internal: false })
|
||||
},
|
||||
|
||||
updateLabel(id: string, label: QueryDeepPartialEntity<Label>) {
|
||||
// internal labels should not be updated
|
||||
return this.update({ id, internal: false }, label)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { libraryItemRepository } from '.'
|
||||
import { entityManager } from '.'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
|
||||
export const getLibraryItemById = async (id: string) => {
|
||||
return libraryItemRepository.findOneBy({ id })
|
||||
|
|
@ -9,3 +10,21 @@ export const getLibraryItemByUrl = async (url: string) => {
|
|||
originalUrl: url,
|
||||
})
|
||||
}
|
||||
|
||||
export const libraryItemRepository = entityManager
|
||||
.getRepository(LibraryItem)
|
||||
.extend({
|
||||
findById(id: string) {
|
||||
return this.findOneBy({ id })
|
||||
},
|
||||
|
||||
findByUrl(url: string) {
|
||||
return this.findOneBy({
|
||||
originalUrl: url,
|
||||
})
|
||||
},
|
||||
|
||||
countByCreatedAt(createdAt: Date) {
|
||||
return this.countBy({ createdAt })
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ export const createArticleSavingRequestResolver = authorized<
|
|||
CreateArticleSavingRequestSuccess,
|
||||
CreateArticleSavingRequestError,
|
||||
MutationCreateArticleSavingRequestArgs
|
||||
>(async (_, { input: { url } }, { claims, pubsub, log }) => {
|
||||
>(async (_, { input: { url } }, { uid, pubsub, log }) => {
|
||||
analytics.track({
|
||||
userId: claims.uid,
|
||||
userId: uid,
|
||||
event: 'link_saved',
|
||||
properties: {
|
||||
url: url,
|
||||
|
|
@ -40,16 +40,16 @@ export const createArticleSavingRequestResolver = authorized<
|
|||
})
|
||||
|
||||
try {
|
||||
const request = await createPageSaveRequest({
|
||||
userId: claims.uid,
|
||||
const articleSavingRequest = await createPageSaveRequest({
|
||||
userId: uid,
|
||||
url,
|
||||
pubsub,
|
||||
})
|
||||
return {
|
||||
articleSavingRequest: request,
|
||||
articleSavingRequest,
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('error saving article', err)
|
||||
log.error('createArticleSavingRequestResolver error', err)
|
||||
if (isErrorWithCode(err)) {
|
||||
return {
|
||||
errorCodes: [err.errorCode as CreateArticleSavingRequestErrorCode],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import {
|
||||
addHighlightToPage,
|
||||
deleteHighlight,
|
||||
getHighlightById,
|
||||
updateHighlight,
|
||||
} from '../../elastic/highlights'
|
||||
import { getPageById, updatePage } from '../../elastic/pages'
|
||||
import { DeepPartial } from 'typeorm'
|
||||
import {
|
||||
Highlight as HighlightData,
|
||||
HighlightType,
|
||||
Label,
|
||||
} from '../../elastic/types'
|
||||
} from '../../entity/highlight'
|
||||
import { Label } from '../../entity/label'
|
||||
import { env } from '../../env'
|
||||
import {
|
||||
CreateHighlightError,
|
||||
|
|
@ -28,101 +22,54 @@ import {
|
|||
MutationCreateHighlightArgs,
|
||||
MutationDeleteHighlightArgs,
|
||||
MutationMergeHighlightArgs,
|
||||
MutationSetShareHighlightArgs,
|
||||
MutationUpdateHighlightArgs,
|
||||
SetShareHighlightError,
|
||||
SetShareHighlightErrorCode,
|
||||
SetShareHighlightSuccess,
|
||||
UpdateHighlightError,
|
||||
UpdateHighlightErrorCode,
|
||||
UpdateHighlightSuccess,
|
||||
User,
|
||||
} from '../../generated/graphql'
|
||||
import { highlightRepository } from '../../repository/highlight'
|
||||
import {
|
||||
deleteHighlightById,
|
||||
mergeHighlights,
|
||||
saveHighlight,
|
||||
} from '../../services/highlights'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { authorized, unescapeHtml } from '../../utils/helpers'
|
||||
import { authorized } from '../../utils/helpers'
|
||||
|
||||
const highlightDataToHighlight = (highlight: HighlightData): Highlight => ({
|
||||
...highlight,
|
||||
user: highlight.userId as unknown as User,
|
||||
updatedAt: highlight.updatedAt || highlight.createdAt,
|
||||
replies: [],
|
||||
reactions: [],
|
||||
createdByMe: undefined as never,
|
||||
type: highlight.highlightType,
|
||||
createdByMe: true,
|
||||
user: {
|
||||
...highlight.user,
|
||||
sharedArticles: [],
|
||||
},
|
||||
})
|
||||
|
||||
export const createHighlightResolver = authorized<
|
||||
CreateHighlightSuccess,
|
||||
CreateHighlightError,
|
||||
MutationCreateHighlightArgs
|
||||
>(async (_, { input }, { claims, log, pubsub }) => {
|
||||
const { articleId: pageId } = input
|
||||
const page = await getPageById(pageId)
|
||||
if (!page) {
|
||||
return {
|
||||
errorCodes: [CreateHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (page.userId !== claims.uid) {
|
||||
return {
|
||||
errorCodes: [CreateHighlightErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
if (input.annotation && input.annotation.length > 4000) {
|
||||
return {
|
||||
errorCodes: [CreateHighlightErrorCode.BadData],
|
||||
}
|
||||
}
|
||||
|
||||
// unescape HTML entities
|
||||
const annotation = input.annotation
|
||||
? unescapeHtml(input.annotation)
|
||||
: undefined
|
||||
|
||||
>(async (_, { input }, { log, pubsub, uid }) => {
|
||||
try {
|
||||
const highlight: HighlightData = {
|
||||
...input,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
userId: claims.uid,
|
||||
annotation,
|
||||
type: input.type || HighlightType.Highlight,
|
||||
}
|
||||
|
||||
if (
|
||||
!(await addHighlightToPage(pageId, highlight, {
|
||||
pubsub,
|
||||
uid: claims.uid,
|
||||
refresh: true,
|
||||
}))
|
||||
) {
|
||||
return {
|
||||
errorCodes: [CreateHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
log.info('Creating a new highlight', {
|
||||
highlight,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'createHighlightResolver',
|
||||
uid: claims.uid,
|
||||
},
|
||||
})
|
||||
const newHighlight = await saveHighlight(input, uid, pubsub)
|
||||
|
||||
analytics.track({
|
||||
userId: claims.uid,
|
||||
userId: uid,
|
||||
event: 'highlight_created',
|
||||
properties: {
|
||||
pageId,
|
||||
libraryItemId: input.articleId,
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
return { highlight: highlightDataToHighlight(highlight) }
|
||||
return { highlight: highlightDataToHighlight(newHighlight) }
|
||||
} catch (err) {
|
||||
log.error('Error creating highlight', err)
|
||||
return {
|
||||
errorCodes: [CreateHighlightErrorCode.AlreadyExists],
|
||||
errorCodes: [CreateHighlightErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -131,101 +78,80 @@ export const mergeHighlightResolver = authorized<
|
|||
MergeHighlightSuccess,
|
||||
MergeHighlightError,
|
||||
MutationMergeHighlightArgs
|
||||
>(async (_, { input }, { claims, log, pubsub }) => {
|
||||
const { articleId: pageId } = input
|
||||
>(async (_, { input }, { authTrx, log, pubsub, uid }) => {
|
||||
const { overlapHighlightIdList, ...newHighlightInput } = input
|
||||
const page = await getPageById(pageId)
|
||||
if (!page || !page.highlights) {
|
||||
return {
|
||||
errorCodes: [MergeHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (page.userId !== claims.uid) {
|
||||
return {
|
||||
errorCodes: [MergeHighlightErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
/* Compute merged annotation form the order of highlights appearing on page */
|
||||
const mergedAnnotations: string[] = []
|
||||
const mergedLabels: Label[] = []
|
||||
const mergedColors: string[] = []
|
||||
const pageHighlights = page.highlights.filter((highlight) => {
|
||||
// filter out highlights that are in the overlap list
|
||||
// and are of type highlight (not annotation or note)
|
||||
if (
|
||||
overlapHighlightIdList.includes(highlight.id) &&
|
||||
highlight.type === HighlightType.Highlight
|
||||
) {
|
||||
if (highlight.annotation) {
|
||||
mergedAnnotations.push(highlight.annotation)
|
||||
}
|
||||
if (highlight.labels) {
|
||||
// remove duplicates from labels by checking id
|
||||
highlight.labels.forEach((label) => {
|
||||
if (
|
||||
!mergedLabels.find((mergedLabel) => mergedLabel.id === label.id)
|
||||
) {
|
||||
mergedLabels.push(label)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const existingHighlights = await authTrx(async (tx) => {
|
||||
return tx
|
||||
.withRepository(highlightRepository)
|
||||
.findByLibraryItemId(input.articleId)
|
||||
})
|
||||
|
||||
existingHighlights.forEach((highlight) => {
|
||||
// filter out highlights that are in the overlap list
|
||||
// and are of type highlight (not annotation or note)
|
||||
if (
|
||||
overlapHighlightIdList.includes(highlight.id) &&
|
||||
highlight.highlightType === HighlightType.Highlight
|
||||
) {
|
||||
highlight.annotation && mergedAnnotations.push(highlight.annotation)
|
||||
|
||||
if (highlight.labels) {
|
||||
// remove duplicates from labels by checking id
|
||||
highlight.labels.forEach((label) => {
|
||||
if (
|
||||
!mergedLabels.find((mergedLabel) => mergedLabel.id === label.id)
|
||||
) {
|
||||
mergedLabels.push(label)
|
||||
}
|
||||
})
|
||||
}
|
||||
// collect colors of overlap highlights
|
||||
highlight.color && mergedColors.push(highlight.color)
|
||||
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
// use new color or the color of the last overlap highlight
|
||||
const color = newHighlightInput.color || mergedColors[mergedColors.length - 1]
|
||||
try {
|
||||
const highlight: HighlightData = {
|
||||
|
||||
const highlight: DeepPartial<HighlightData> = {
|
||||
...newHighlightInput,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
userId: claims.uid,
|
||||
annotation:
|
||||
mergedAnnotations.length > 0 ? mergedAnnotations.join('\n') : null,
|
||||
type: HighlightType.Highlight,
|
||||
labels: mergedLabels,
|
||||
color,
|
||||
}
|
||||
|
||||
const merged = await updatePage(
|
||||
pageId,
|
||||
{ highlights: pageHighlights.concat(highlight) },
|
||||
{ pubsub, uid: claims.uid, refresh: true }
|
||||
)
|
||||
if (!merged) {
|
||||
throw new Error('Failed to create merged highlight')
|
||||
}
|
||||
|
||||
log.info('Creating a merged highlight', {
|
||||
const newHighlight = await mergeHighlights(
|
||||
overlapHighlightIdList,
|
||||
highlight,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'mergeHighlightResolver',
|
||||
uid: claims.uid,
|
||||
pageId,
|
||||
uid,
|
||||
pubsub
|
||||
)
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'highlight_created',
|
||||
properties: {
|
||||
libraryItemId: input.articleId,
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
highlight: highlightDataToHighlight(highlight),
|
||||
highlight: highlightDataToHighlight(newHighlight),
|
||||
overlapHighlightIdList: input.overlapHighlightIdList,
|
||||
}
|
||||
} catch (e) {
|
||||
log.info('Failed to create a merged highlight', {
|
||||
error: e,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'mergeHighlightResolver',
|
||||
uid: claims.uid,
|
||||
},
|
||||
})
|
||||
|
||||
log.error('Error merging highlight', e)
|
||||
return {
|
||||
errorCodes: [MergeHighlightErrorCode.AlreadyExists],
|
||||
errorCodes: [MergeHighlightErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -234,149 +160,89 @@ export const updateHighlightResolver = authorized<
|
|||
UpdateHighlightSuccess,
|
||||
UpdateHighlightError,
|
||||
MutationUpdateHighlightArgs
|
||||
>(async (_, { input }, { pubsub, claims, log }) => {
|
||||
const highlight = await getHighlightById(input.highlightId)
|
||||
>(async (_, { input }, { pubsub, uid, log }) => {
|
||||
try {
|
||||
const updatedHighlight = await saveHighlight(input, uid, pubsub)
|
||||
|
||||
if (!highlight?.id) {
|
||||
return {
|
||||
errorCodes: [UpdateHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
if (highlight.userId !== claims.uid) {
|
||||
return { highlight: highlightDataToHighlight(updatedHighlight) }
|
||||
} catch (error) {
|
||||
log.error('updateHighlightResolver error', error)
|
||||
return {
|
||||
errorCodes: [UpdateHighlightErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
|
||||
// unescape HTML entities
|
||||
const annotation = input.annotation
|
||||
? unescapeHtml(input.annotation)
|
||||
: undefined
|
||||
const quote = input.quote ? unescapeHtml(input.quote) : highlight.quote
|
||||
|
||||
const updatedHighlight: HighlightData = {
|
||||
...highlight,
|
||||
annotation,
|
||||
quote,
|
||||
updatedAt: new Date(),
|
||||
color: input.color,
|
||||
}
|
||||
|
||||
log.info('Updating a highlight', {
|
||||
updatedHighlight,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'updateHighlightResolver',
|
||||
uid: claims.uid,
|
||||
},
|
||||
})
|
||||
|
||||
const updated = await updateHighlight(updatedHighlight, {
|
||||
pubsub,
|
||||
uid: claims.uid,
|
||||
refresh: true,
|
||||
})
|
||||
|
||||
if (!updated) {
|
||||
return {
|
||||
errorCodes: [UpdateHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
return { highlight: highlightDataToHighlight(updatedHighlight) }
|
||||
})
|
||||
|
||||
export const deleteHighlightResolver = authorized<
|
||||
DeleteHighlightSuccess,
|
||||
DeleteHighlightError,
|
||||
MutationDeleteHighlightArgs
|
||||
>(async (_, { highlightId }, { claims, log, pubsub }) => {
|
||||
const highlight = await getHighlightById(highlightId)
|
||||
>(async (_, { highlightId }, { uid, log }) => {
|
||||
try {
|
||||
const deletedHighlight = await deleteHighlightById(highlightId, uid)
|
||||
|
||||
if (!highlight?.id) {
|
||||
return {
|
||||
errorCodes: [DeleteHighlightErrorCode.NotFound],
|
||||
if (!deletedHighlight) {
|
||||
return {
|
||||
errorCodes: [DeleteHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highlight.userId !== claims.uid) {
|
||||
return { highlight: highlightDataToHighlight(deletedHighlight) }
|
||||
} catch (error) {
|
||||
log.error('deleteHighlightResolver error', error)
|
||||
return {
|
||||
errorCodes: [DeleteHighlightErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await deleteHighlight(highlightId, {
|
||||
pubsub,
|
||||
uid: claims.uid,
|
||||
refresh: true,
|
||||
})
|
||||
|
||||
if (!deleted) {
|
||||
return {
|
||||
errorCodes: [DeleteHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
log.info('Deleting a highlight', {
|
||||
highlight,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'deleteHighlightResolver',
|
||||
uid: claims.uid,
|
||||
},
|
||||
})
|
||||
|
||||
return { highlight: highlightDataToHighlight(highlight) }
|
||||
})
|
||||
|
||||
export const setShareHighlightResolver = authorized<
|
||||
SetShareHighlightSuccess,
|
||||
SetShareHighlightError,
|
||||
MutationSetShareHighlightArgs
|
||||
>(async (_, { input: { id, share } }, { pubsub, claims, log }) => {
|
||||
const highlight = await getHighlightById(id)
|
||||
// export const setShareHighlightResolver = authorized<
|
||||
// SetShareHighlightSuccess,
|
||||
// SetShareHighlightError,
|
||||
// MutationSetShareHighlightArgs
|
||||
// >(async (_, { input: { id, share } }, { pubsub, claims, log }) => {
|
||||
// const highlight = await getHighlightById(id)
|
||||
|
||||
if (!highlight?.id) {
|
||||
return {
|
||||
errorCodes: [SetShareHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
// if (!highlight?.id) {
|
||||
// return {
|
||||
// errorCodes: [SetShareHighlightErrorCode.NotFound],
|
||||
// }
|
||||
// }
|
||||
|
||||
if (highlight.userId !== claims.uid) {
|
||||
return {
|
||||
errorCodes: [SetShareHighlightErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
// if (highlight.userId !== claims.uid) {
|
||||
// return {
|
||||
// errorCodes: [SetShareHighlightErrorCode.Forbidden],
|
||||
// }
|
||||
// }
|
||||
|
||||
const sharedAt = share ? new Date() : null
|
||||
// const sharedAt = share ? new Date() : null
|
||||
|
||||
log.info(`${share ? 'S' : 'Uns'}haring a highlight`, {
|
||||
highlight,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'setShareHighlightResolver',
|
||||
userId: highlight.userId,
|
||||
},
|
||||
})
|
||||
// log.info(`${share ? 'S' : 'Uns'}haring a highlight`, {
|
||||
// highlight,
|
||||
// labels: {
|
||||
// source: 'resolver',
|
||||
// resolver: 'setShareHighlightResolver',
|
||||
// userId: highlight.userId,
|
||||
// },
|
||||
// })
|
||||
|
||||
const updatedHighlight: HighlightData = {
|
||||
...highlight,
|
||||
sharedAt,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
// const updatedHighlight: HighlightData = {
|
||||
// ...highlight,
|
||||
// sharedAt,
|
||||
// updatedAt: new Date(),
|
||||
// }
|
||||
|
||||
const updated = await updateHighlight(updatedHighlight, {
|
||||
pubsub,
|
||||
uid: claims.uid,
|
||||
refresh: true,
|
||||
})
|
||||
// const updated = await updateHighlight(updatedHighlight, {
|
||||
// pubsub,
|
||||
// uid: claims.uid,
|
||||
// refresh: true,
|
||||
// })
|
||||
|
||||
if (!updated) {
|
||||
return {
|
||||
errorCodes: [SetShareHighlightErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
// if (!updated) {
|
||||
// return {
|
||||
// errorCodes: [SetShareHighlightErrorCode.NotFound],
|
||||
// }
|
||||
// }
|
||||
|
||||
return { highlight: highlightDataToHighlight(updatedHighlight) }
|
||||
})
|
||||
// return { highlight: highlightDataToHighlight(updatedHighlight) }
|
||||
// })
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
import { Between } from 'typeorm'
|
||||
import { appDataSource } from '../../data_source'
|
||||
import { getHighlightById } from '../../elastic/highlights'
|
||||
import {
|
||||
deleteLabel,
|
||||
setLabelsForHighlight,
|
||||
updateLabel,
|
||||
updateLabelsInPage,
|
||||
} from '../../elastic/labels'
|
||||
import { getPageById } from '../../elastic/pages'
|
||||
import { Label } from '../../entity/label'
|
||||
import { User } from '../../entity/user'
|
||||
import { env } from '../../env'
|
||||
import {
|
||||
CreateLabelError,
|
||||
|
|
@ -37,34 +27,25 @@ import {
|
|||
UpdateLabelErrorCode,
|
||||
UpdateLabelSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { createPubSubClient } from '../../pubsub'
|
||||
import { getRepository, setClaims } from '../../repository'
|
||||
import { labelRepository } from '../../repository/label'
|
||||
import {
|
||||
createLabel,
|
||||
getLabelByName,
|
||||
getLabelsAndCreateIfNotExist,
|
||||
getLabelsByIds,
|
||||
saveLabelsInHighlight,
|
||||
saveLabelsInLibraryItem,
|
||||
} from '../../services/labels'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { authorized } from '../../utils/helpers'
|
||||
|
||||
export const labelsResolver = authorized<LabelsSuccess, LabelsError>(
|
||||
async (_obj, _params, { claims: { uid }, log }) => {
|
||||
async (_obj, _params, { log, authTrx }) => {
|
||||
try {
|
||||
const user = await getRepository(User).findOne({
|
||||
where: { id: uid },
|
||||
relations: ['labels'],
|
||||
order: {
|
||||
labels: {
|
||||
const labels = await authTrx(async (tx) => {
|
||||
return tx.find(Label, {
|
||||
order: {
|
||||
position: 'ASC',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [LabelsErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
|
|
@ -79,10 +60,10 @@ export const labelsResolver = authorized<LabelsSuccess, LabelsError>(
|
|||
})
|
||||
|
||||
return {
|
||||
labels: user.labels || [],
|
||||
labels,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
log.error('labelsResolver', error)
|
||||
return {
|
||||
errorCodes: [LabelsErrorCode.BadRequest],
|
||||
}
|
||||
|
|
@ -94,26 +75,11 @@ export const createLabelResolver = authorized<
|
|||
CreateLabelSuccess,
|
||||
CreateLabelError,
|
||||
MutationCreateLabelArgs
|
||||
>(async (_, { input }, { claims: { uid }, log }) => {
|
||||
log.info('createLabelResolver')
|
||||
|
||||
>(async (_, { input }, { authTrx, log, uid }) => {
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [CreateLabelErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
// Check if label already exists ignoring case of name
|
||||
const existingLabel = await getLabelByName(uid, input.name)
|
||||
if (existingLabel) {
|
||||
return {
|
||||
errorCodes: [CreateLabelErrorCode.LabelAlreadyExists],
|
||||
}
|
||||
}
|
||||
|
||||
const label = await createLabel(uid, input)
|
||||
const label = await authTrx(async (tx) => {
|
||||
return tx.withRepository(labelRepository).createLabel(input, uid)
|
||||
})
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
|
|
@ -128,7 +94,7 @@ export const createLabelResolver = authorized<
|
|||
label,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
log.error('createLabelResolver', error)
|
||||
return {
|
||||
errorCodes: [CreateLabelErrorCode.BadRequest],
|
||||
}
|
||||
|
|
@ -139,53 +105,18 @@ export const deleteLabelResolver = authorized<
|
|||
DeleteLabelSuccess,
|
||||
DeleteLabelError,
|
||||
MutationDeleteLabelArgs
|
||||
>(async (_, { id: labelId }, { claims: { uid }, log }) => {
|
||||
log.info('deleteLabelResolver')
|
||||
|
||||
>(async (_, { id: labelId }, { authTrx, log, uid }) => {
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [DeleteLabelErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const label = await getRepository(Label).findOne({
|
||||
where: { id: labelId, user: { id: uid } },
|
||||
relations: ['user'],
|
||||
const deleteResult = await authTrx(async (tx) => {
|
||||
return tx.withRepository(labelRepository).deleteById(labelId)
|
||||
})
|
||||
if (!label) {
|
||||
|
||||
if (!deleteResult.affected) {
|
||||
return {
|
||||
errorCodes: [DeleteLabelErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
// internal labels cannot be deleted
|
||||
if (label.internal) {
|
||||
log.info('internal labels cannot be deleted')
|
||||
return {
|
||||
errorCodes: [DeleteLabelErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
|
||||
const result = await appDataSource.transaction(async (t) => {
|
||||
await setClaims(t, uid)
|
||||
return t.getRepository(Label).delete(labelId)
|
||||
})
|
||||
if (!result.affected) {
|
||||
log.error('Failed to delete label', labelId)
|
||||
return {
|
||||
errorCodes: [DeleteLabelErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
// delete label in elastic pages and highlights
|
||||
await deleteLabel(label.name, {
|
||||
pubsub: createPubSubClient(),
|
||||
uid,
|
||||
refresh: true,
|
||||
})
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'label_deleted',
|
||||
|
|
@ -196,7 +127,7 @@ export const deleteLabelResolver = authorized<
|
|||
})
|
||||
|
||||
return {
|
||||
label,
|
||||
label: deleteResult.raw as Label,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('error deleting label', error)
|
||||
|
|
@ -210,177 +141,110 @@ export const setLabelsResolver = authorized<
|
|||
SetLabelsSuccess,
|
||||
SetLabelsError,
|
||||
MutationSetLabelsArgs
|
||||
>(async (_, { input }, { claims: { uid }, log, pubsub }) => {
|
||||
log.info('setLabelsResolver')
|
||||
|
||||
const { pageId, labelIds, labels } = input
|
||||
|
||||
if (!labelIds && !labels) {
|
||||
log.info('labelIds or labels must be provided')
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
>(
|
||||
async (
|
||||
_,
|
||||
{ input: { pageId, labelIds, labels } },
|
||||
{ uid, log, authTrx, pubsub }
|
||||
) => {
|
||||
if (!labelIds && !labels) {
|
||||
log.error('labelIds or labels must be provided')
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.Unauthorized],
|
||||
errorCodes: [SetLabelsErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
const page = await getPageById(pageId)
|
||||
if (!page) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (page.userId !== uid) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
try {
|
||||
let labelsSet: Label[] = []
|
||||
|
||||
const ctx = {
|
||||
uid,
|
||||
pubsub,
|
||||
refresh: true,
|
||||
}
|
||||
let labelsSet: Label[] = []
|
||||
|
||||
if (labels && labels.length > 0) {
|
||||
// for new clients that send label names
|
||||
// create labels if they don't exist
|
||||
labelsSet = await getLabelsAndCreateIfNotExist(ctx, labels)
|
||||
} else if (labelIds && labelIds.length > 0) {
|
||||
// for old clients that send labelIds
|
||||
labelsSet = await getLabelsByIds(uid, labelIds)
|
||||
if (labelsSet.length !== labelIds.length) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.NotFound],
|
||||
if (labels && labels.length > 0) {
|
||||
// for new clients that send label names
|
||||
// create labels if they don't exist
|
||||
labelsSet = await getLabelsAndCreateIfNotExist(labels, uid)
|
||||
} else if (labelIds && labelIds.length > 0) {
|
||||
// for old clients that send labelIds
|
||||
labelsSet = await authTrx(async (tx) => {
|
||||
return tx.withRepository(labelRepository).findByIds(labelIds)
|
||||
})
|
||||
if (labelsSet.length !== labelIds.length) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// filter out labels that are already set
|
||||
const labelsToAdd = labelsSet.filter(
|
||||
(label) => !page.labels?.some((pageLabel) => pageLabel.id === label.id)
|
||||
)
|
||||
// update labels in the page
|
||||
const updated = await updateLabelsInPage(
|
||||
pageId,
|
||||
labelsSet,
|
||||
ctx,
|
||||
labelsToAdd
|
||||
)
|
||||
if (!updated) {
|
||||
// save labels in the library item
|
||||
await saveLabelsInLibraryItem(labelsSet, pageId, uid, pubsub)
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'labels_set',
|
||||
properties: {
|
||||
pageId,
|
||||
labelIds,
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.NotFound],
|
||||
labels: labelsSet,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('setLabelsResolver error', error)
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'labels_set',
|
||||
properties: {
|
||||
pageId,
|
||||
labelIds,
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
labels: labelsSet,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export const updateLabelResolver = authorized<
|
||||
UpdateLabelSuccess,
|
||||
UpdateLabelError,
|
||||
MutationUpdateLabelArgs
|
||||
>(async (_, { input }, { claims: { uid }, log, pubsub }) => {
|
||||
log.info('updateLabelResolver')
|
||||
>(
|
||||
async (
|
||||
_,
|
||||
{ input: { name, color, description, labelId } },
|
||||
{ authTrx, log }
|
||||
) => {
|
||||
try {
|
||||
log.info('Updating a label', {
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'updateLabelResolver',
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const { name, color, description, labelId } = input
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.Unauthorized],
|
||||
const result = await authTrx(async (tx) => {
|
||||
return tx.withRepository(labelRepository).updateLabel(labelId, {
|
||||
name,
|
||||
color,
|
||||
description,
|
||||
})
|
||||
})
|
||||
|
||||
if (!result.affected) {
|
||||
log.error('failed to update')
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const label = await getRepository(Label).findOne({
|
||||
where: { id: labelId, user: { id: uid } },
|
||||
select: ['id', 'name', 'color', 'description', 'createdAt'],
|
||||
})
|
||||
if (!label) {
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
// internal labels cannot be updated
|
||||
if (label.internal && label.name.toLowerCase() !== name.toLowerCase()) {
|
||||
log.info('internal labels cannot be updated')
|
||||
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
|
||||
log.info('Updating a label', {
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'updateLabelResolver',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await appDataSource.transaction(async (t) => {
|
||||
await setClaims(t, uid)
|
||||
label.name = name
|
||||
label.color = color
|
||||
label.description = description || undefined
|
||||
label.createdAt = new Date()
|
||||
|
||||
return t.getRepository(Label).update({ id: labelId }, label)
|
||||
})
|
||||
|
||||
if (!result.affected) {
|
||||
log.error('failed to update')
|
||||
return { label: result.raw as Label }
|
||||
} catch (error) {
|
||||
log.error('error updating label', error)
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
await updateLabel(label, {
|
||||
pubsub,
|
||||
uid,
|
||||
refresh: true,
|
||||
})
|
||||
|
||||
return { label }
|
||||
} catch (error) {
|
||||
log.error('error updating label', error)
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export const setLabelsForHighlightResolver = authorized<
|
||||
SetLabelsSuccess,
|
||||
SetLabelsError,
|
||||
MutationSetLabelsForHighlightArgs
|
||||
>(async (_, { input }, { claims: { uid }, log, pubsub }) => {
|
||||
log.info('setLabelsForHighlightResolver')
|
||||
|
||||
>(async (_, { input }, { uid, log, pubsub, authTrx }) => {
|
||||
const { highlightId, labelIds, labels } = input
|
||||
|
||||
if (!labelIds && !labels) {
|
||||
|
|
@ -391,63 +255,25 @@ export const setLabelsForHighlightResolver = authorized<
|
|||
}
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const highlight = await getHighlightById(highlightId)
|
||||
if (!highlight) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (highlight.userId !== uid) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
uid,
|
||||
pubsub,
|
||||
refresh: true,
|
||||
}
|
||||
let labelsSet: Label[] = []
|
||||
|
||||
if (labels && labels.length > 0) {
|
||||
// for new clients that send label names
|
||||
// create labels if they don't exist
|
||||
labelsSet = await getLabelsAndCreateIfNotExist(ctx, labels)
|
||||
labelsSet = await getLabelsAndCreateIfNotExist(labels, uid)
|
||||
} else if (labelIds && labelIds.length > 0) {
|
||||
// for old clients that send labelIds
|
||||
labelsSet = await getLabelsByIds(uid, labelIds)
|
||||
labelsSet = await authTrx(async (tx) => {
|
||||
return tx.withRepository(labelRepository).findByIds(labelIds)
|
||||
})
|
||||
if (labelsSet.length !== labelIds.length) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
}
|
||||
// filter out labels that are already set
|
||||
const labelsToAdd = labelsSet.filter(
|
||||
(label) =>
|
||||
!highlight.labels?.some(
|
||||
(highlightLabel) => highlightLabel.id === label.id
|
||||
)
|
||||
)
|
||||
// set labels in the highlights
|
||||
const updated = await setLabelsForHighlight(
|
||||
highlightId,
|
||||
labelsSet,
|
||||
ctx,
|
||||
labelsToAdd
|
||||
)
|
||||
if (!updated) {
|
||||
return {
|
||||
errorCodes: [SetLabelsErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
// save labels in the library item
|
||||
await saveLabelsInHighlight(labelsSet, input.highlightId, uid, pubsub)
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
|
|
@ -474,33 +300,18 @@ export const moveLabelResolver = authorized<
|
|||
MoveLabelSuccess,
|
||||
MoveLabelError,
|
||||
MutationMoveLabelArgs
|
||||
>(async (_, { input }, { claims: { uid }, log, pubsub }) => {
|
||||
log.info('moveLabelResolver')
|
||||
|
||||
>(async (_, { input }, { authTrx, log, uid }) => {
|
||||
const { labelId, afterLabelId } = input
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [MoveLabelErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const label = await getRepository(Label).findOne({
|
||||
where: { id: labelId },
|
||||
relations: ['user'],
|
||||
const label = await authTrx(async (tx) => {
|
||||
return tx.withRepository(labelRepository).findById(labelId)
|
||||
})
|
||||
if (!label) {
|
||||
return {
|
||||
errorCodes: [MoveLabelErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (label.user.id !== uid) {
|
||||
return {
|
||||
errorCodes: [MoveLabelErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
if (label.id === afterLabelId) {
|
||||
// nothing to do
|
||||
|
|
@ -511,32 +322,25 @@ export const moveLabelResolver = authorized<
|
|||
// if afterLabelId is not provided, move to the top
|
||||
let newPosition = 1
|
||||
if (afterLabelId) {
|
||||
const afterLabel = await getRepository(Label).findOne({
|
||||
where: { id: afterLabelId },
|
||||
relations: ['user'],
|
||||
const afterLabel = await authTrx(async (tx) => {
|
||||
return tx.withRepository(labelRepository).findById(labelId)
|
||||
})
|
||||
if (!afterLabel) {
|
||||
return {
|
||||
errorCodes: [MoveLabelErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (afterLabel.user.id !== uid) {
|
||||
return {
|
||||
errorCodes: [MoveLabelErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
newPosition = afterLabel.position
|
||||
}
|
||||
const moveUp = newPosition < oldPosition
|
||||
|
||||
// move label to the new position
|
||||
const updated = await appDataSource.transaction(async (t) => {
|
||||
await setClaims(t, uid)
|
||||
|
||||
const updated = await authTrx(async (tx) => {
|
||||
const labelRepo = tx.withRepository(labelRepository)
|
||||
// update the position of the other labels
|
||||
const updated = await t.getRepository(Label).update(
|
||||
const updated = await labelRepo.update(
|
||||
{
|
||||
user: { id: uid },
|
||||
position: Between(
|
||||
Math.min(newPosition, oldPosition),
|
||||
Math.max(newPosition, oldPosition)
|
||||
|
|
@ -551,7 +355,7 @@ export const moveLabelResolver = authorized<
|
|||
}
|
||||
|
||||
// update the position of the label
|
||||
return t.getRepository(Label).save({
|
||||
return labelRepo.save({
|
||||
...label,
|
||||
position: newPosition,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
AddPopularReadSuccess,
|
||||
MutationAddPopularReadArgs,
|
||||
} from '../../generated/graphql'
|
||||
import { userRepository } from '../../repository'
|
||||
import { addPopularRead } from '../../services/popular_reads'
|
||||
import { authorized } from '../../utils/helpers'
|
||||
export const addPopularReadResolver = authorized<
|
||||
|
|
@ -12,13 +11,6 @@ export const addPopularReadResolver = authorized<
|
|||
AddPopularReadError,
|
||||
MutationAddPopularReadArgs
|
||||
>(async (_, { name }, { uid }) => {
|
||||
const user = await userRepository.findOneBy({
|
||||
id: uid,
|
||||
})
|
||||
if (!user) {
|
||||
return { errorCodes: [AddPopularReadErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
const pageId = await addPopularRead(uid, name)
|
||||
if (!pageId) {
|
||||
return { errorCodes: [AddPopularReadErrorCode.NotFound] }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { User } from '../../entity/user'
|
||||
import { env } from '../../env'
|
||||
import {
|
||||
MutationSaveFileArgs,
|
||||
|
|
@ -8,7 +7,7 @@ import {
|
|||
SaveErrorCode,
|
||||
SaveSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { getRepository, userRepository } from '../../repository'
|
||||
import { userRepository } from '../../repository/user'
|
||||
import { saveFile } from '../../services/save_file'
|
||||
import { savePage } from '../../services/save_page'
|
||||
import { saveUrl } from '../../services/save_url'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
UpdatePageErrorCode,
|
||||
UpdatePageSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { userRepository } from '../../repository'
|
||||
import { Merge } from '../../util'
|
||||
import { authorized } from '../../utils/helpers'
|
||||
|
||||
|
|
@ -21,10 +20,6 @@ export const updatePageResolver = authorized<
|
|||
MutationUpdatePageArgs
|
||||
>(async (_, { input }, ctx) => {
|
||||
const { pubsub, uid } = ctx
|
||||
const user = await userRepository.findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return { errorCodes: [UpdatePageErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
const page = await getPageById(input.pageId)
|
||||
|
||||
|
|
@ -32,10 +27,6 @@ export const updatePageResolver = authorized<
|
|||
return { errorCodes: [UpdatePageErrorCode.NotFound] }
|
||||
}
|
||||
|
||||
if (page.userId !== user.id) {
|
||||
return { errorCodes: [UpdatePageErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
const pageData = {
|
||||
id: input.pageId,
|
||||
title: input.title ?? undefined,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
UploadFileRequestSuccess,
|
||||
UploadFileStatus,
|
||||
} from '../../generated/graphql'
|
||||
import { uploadFileRepository } from '../../repository'
|
||||
import { uploadFileRepository } from '../../repository/upload_file'
|
||||
import { validateUrl } from '../../services/create_page_save_request'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { authorized, generateSlug } from '../../utils/helpers'
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ import {
|
|||
UsersError,
|
||||
UsersSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { setClaims, userRepository } from '../../repository'
|
||||
import { createUser, getTopUsers } from '../../services/create_user'
|
||||
import { setClaims } from '../../repository'
|
||||
import { userRepository } from '../../repository/user'
|
||||
import { createUser } from '../../services/create_user'
|
||||
import { sendVerificationEmail } from '../../services/send_emails'
|
||||
import { authorized, userDataToUser } from '../../utils/helpers'
|
||||
import { validateUsername } from '../../utils/usernamePolicy'
|
||||
|
|
@ -295,7 +296,7 @@ export const getUserResolver: ResolverFn<
|
|||
|
||||
export const getAllUsersResolver = authorized<UsersSuccess, UsersError>(
|
||||
async (_obj, _params) => {
|
||||
const users = await getTopUsers()
|
||||
const users = await userRepository.findTopUsers()
|
||||
const result = { users: users.map((userData) => userDataToUser(userData)) }
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -759,7 +759,7 @@ const schema = gql`
|
|||
quote: String! @sanitize(maxLength: 12000, minLength: 1)
|
||||
prefix: String @sanitize
|
||||
suffix: String @sanitize
|
||||
annotation: String @sanitize(maxLength: 8000)
|
||||
annotation: String @sanitize(maxLength: 4000)
|
||||
overlapHighlightIdList: [String!]!
|
||||
highlightPositionPercent: Float
|
||||
highlightPositionAnchorIndex: Int
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
import * as privateIpLib from 'private-ip'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
countByCreatedAt,
|
||||
createPage,
|
||||
getPageByParam,
|
||||
updatePage,
|
||||
} from '../elastic/pages'
|
||||
import { ArticleSavingRequestStatus, Label, PageType } from '../elastic/types'
|
||||
import { countByCreatedAt, createPage, updatePage } from '../elastic/pages'
|
||||
import { ArticleSavingRequestStatus, PageType } from '../elastic/types'
|
||||
import { LibraryItemState } from '../entity/library_item'
|
||||
import { User } from '../entity/user'
|
||||
import {
|
||||
ArticleSavingRequest,
|
||||
CreateArticleSavingRequestErrorCode,
|
||||
CreateLabelInput
|
||||
} from '../generated/graphql'
|
||||
import { createPubSubClient, PubsubClient } from '../pubsub'
|
||||
import { getRepository } from '../repository'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { userRepository } from '../repository/user'
|
||||
import { enqueueParseRequest } from '../utils/createTask'
|
||||
import {
|
||||
cleanUrl,
|
||||
generateSlug,
|
||||
pageToArticleSavingRequest,
|
||||
pageToArticleSavingRequest
|
||||
} from '../utils/helpers'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
|
|
@ -28,7 +26,7 @@ interface PageSaveRequest {
|
|||
pubsub?: PubsubClient
|
||||
articleSavingRequestId?: string
|
||||
archivedAt?: Date | null
|
||||
labels?: Label[]
|
||||
labels?: CreateLabelInput[]
|
||||
priority?: 'low' | 'high'
|
||||
user?: User | null
|
||||
locale?: string
|
||||
|
|
@ -101,9 +99,7 @@ export const createPageSaveRequest = async ({
|
|||
}
|
||||
// if user is not specified, get it from the database
|
||||
if (!user) {
|
||||
user = await getRepository(User).findOneBy({
|
||||
id: userId,
|
||||
})
|
||||
user = await userRepository.findById(userId)
|
||||
if (!user) {
|
||||
logger.info('User not found', userId)
|
||||
return Promise.reject({
|
||||
|
|
@ -112,26 +108,14 @@ export const createPageSaveRequest = async ({
|
|||
}
|
||||
}
|
||||
|
||||
// get priority by checking rate limit if not specified
|
||||
priority = priority || (await getPriorityByRateLimit(userId))
|
||||
|
||||
// look for existing page
|
||||
url = cleanUrl(url)
|
||||
|
||||
const ctx = {
|
||||
pubsub,
|
||||
uid: userId,
|
||||
refresh: true,
|
||||
}
|
||||
let page = await getPageByParam({
|
||||
userId,
|
||||
url,
|
||||
})
|
||||
if (!page) {
|
||||
logger.info('Page not exists', url)
|
||||
page = {
|
||||
// look for existing library item
|
||||
const existingLibraryItem = await libraryItemRepository.findByUrl(url)
|
||||
if (!existingLibraryItem) {
|
||||
logger.info('libraryItem does not exist', { url })
|
||||
libraryItem = {
|
||||
id: articleSavingRequestId,
|
||||
userId,
|
||||
user: { id: userId },
|
||||
content: SAVING_CONTENT,
|
||||
hash: '',
|
||||
pageType: PageType.Unknown,
|
||||
|
|
@ -140,12 +124,11 @@ export const createPageSaveRequest = async ({
|
|||
slug: generateSlug(url),
|
||||
title: url,
|
||||
url,
|
||||
state: ArticleSavingRequestStatus.Processing,
|
||||
state: LibraryItemState.Processing,
|
||||
createdAt: new Date(),
|
||||
savedAt: savedAt || new Date(),
|
||||
publishedAt,
|
||||
archivedAt,
|
||||
labels,
|
||||
}
|
||||
|
||||
// create processing page
|
||||
|
|
@ -158,7 +141,7 @@ export const createPageSaveRequest = async ({
|
|||
}
|
||||
}
|
||||
// reset state to processing
|
||||
if (page.state !== ArticleSavingRequestStatus.Processing) {
|
||||
if (existingLibraryItem.state !== ArticleSavingRequestStatus.Processing) {
|
||||
await updatePage(
|
||||
page.id,
|
||||
{
|
||||
|
|
@ -167,24 +150,22 @@ export const createPageSaveRequest = async ({
|
|||
ctx
|
||||
)
|
||||
}
|
||||
const labelsInput = labels?.map((label) => ({
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description,
|
||||
}))
|
||||
|
||||
// get priority by checking rate limit if not specified
|
||||
priority = priority || (await getPriorityByRateLimit(userId))
|
||||
|
||||
// enqueue task to parse page
|
||||
await enqueueParseRequest({
|
||||
url,
|
||||
userId,
|
||||
saveRequestId: page.id,
|
||||
saveRequestId: articleSavingRequestId,
|
||||
priority,
|
||||
state: archivedAt ? ArticleSavingRequestStatus.Archived : undefined,
|
||||
labels: labelsInput,
|
||||
labels,
|
||||
locale,
|
||||
timezone,
|
||||
// unix timestamp
|
||||
savedAt: savedAt?.getTime(),
|
||||
publishedAt: publishedAt?.getTime(),
|
||||
savedAt,
|
||||
publishedAt,
|
||||
})
|
||||
|
||||
return pageToArticleSavingRequest(user, page)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { diff_match_patch } from 'diff-match-patch'
|
||||
import { DeepPartial } from 'typeorm'
|
||||
import { Highlight } from '../entity/highlight'
|
||||
import { homePageURL } from '../env'
|
||||
import { createPubSubClient, EntityType } from '../pubsub'
|
||||
import { entityManager, setClaims } from '../repository'
|
||||
import { highlightRepository } from '../repository/highlight'
|
||||
|
||||
type HighlightEvent = Highlight & { pageId: string }
|
||||
|
||||
export const getHighlightLocation = (patch: string): number | undefined => {
|
||||
const dmp = new diff_match_patch()
|
||||
|
|
@ -9,3 +16,69 @@ export const getHighlightLocation = (patch: string): number | undefined => {
|
|||
|
||||
export const getHighlightUrl = (slug: string, highlightId: string): string =>
|
||||
`${homePageURL()}/me/${slug}#${highlightId}`
|
||||
|
||||
export const saveHighlight = async (
|
||||
highlight: DeepPartial<Highlight>,
|
||||
userId: string,
|
||||
pubsub = createPubSubClient(),
|
||||
em = entityManager
|
||||
) => {
|
||||
const newHighlight = await em.transaction(async (tx) => {
|
||||
await setClaims(tx, userId)
|
||||
|
||||
return tx
|
||||
.withRepository(highlightRepository)
|
||||
.createAndSave(highlight, userId)
|
||||
})
|
||||
|
||||
await pubsub.entityCreated<HighlightEvent>(
|
||||
EntityType.HIGHLIGHT,
|
||||
{ ...newHighlight, pageId: newHighlight.libraryItem.id },
|
||||
userId
|
||||
)
|
||||
|
||||
return newHighlight
|
||||
}
|
||||
|
||||
export const mergeHighlights = async (
|
||||
highlightsToRemove: string[],
|
||||
highlightToAdd: DeepPartial<Highlight>,
|
||||
userId: string,
|
||||
pubsub = createPubSubClient(),
|
||||
em = entityManager
|
||||
) => {
|
||||
const newHighlight = await em.transaction(async (tx) => {
|
||||
await setClaims(tx, userId)
|
||||
|
||||
const highlightRepo = tx.withRepository(highlightRepository)
|
||||
|
||||
await highlightRepo.delete(highlightsToRemove)
|
||||
|
||||
return highlightRepo.createAndSave(highlightToAdd, userId)
|
||||
})
|
||||
|
||||
await pubsub.entityCreated<HighlightEvent>(
|
||||
EntityType.HIGHLIGHT,
|
||||
{ ...newHighlight, pageId: newHighlight.libraryItem.id },
|
||||
userId
|
||||
)
|
||||
|
||||
return newHighlight
|
||||
}
|
||||
|
||||
export const deleteHighlightById = async (
|
||||
highlightId: string,
|
||||
userId: string
|
||||
) => {
|
||||
return entityManager.transaction(async (tx) => {
|
||||
await setClaims(tx, userId)
|
||||
|
||||
const highlightRepo = tx.withRepository(highlightRepository)
|
||||
const highlight = await highlightRepo.findById(highlightId)
|
||||
if (!highlight) {
|
||||
throw new Error(`Highlight ${highlightId} not found`)
|
||||
}
|
||||
|
||||
return highlightRepo.remove(highlight)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,14 @@
|
|||
import DataLoader from 'dataloader'
|
||||
import { In } from 'typeorm'
|
||||
import { addLabelInPage } from '../elastic/labels'
|
||||
import { PageContext } from '../elastic/types'
|
||||
import { Highlight } from '../entity/highlight'
|
||||
import { Label } from '../entity/label'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
import { Link } from '../entity/link'
|
||||
import { User } from '../entity/user'
|
||||
import { CreateLabelInput } from '../generated/graphql'
|
||||
import { entityManager, getRepository, setClaims } from '../repository'
|
||||
import { labelRepository } from '../repository/label'
|
||||
import { generateRandomColor } from '../utils/helpers'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
const INTERNAL_LABELS_WITH_COLOR = new Map<
|
||||
string,
|
||||
{ name: string; color: string }
|
||||
>([
|
||||
['favorites', { name: 'Favorites', color: '#FFD700' }],
|
||||
['library', { name: 'Library', color: '#584C42' }],
|
||||
['rss', { name: 'RSS', color: '#F26522' }],
|
||||
['newsletter', { name: 'Newsletter', color: '#07D2D1' }],
|
||||
])
|
||||
|
||||
export const getInternalLabelWithColor = (name: string) => {
|
||||
return INTERNAL_LABELS_WITH_COLOR.get(name.toLowerCase())
|
||||
}
|
||||
|
||||
const isLabelInternal = (name: string): boolean => {
|
||||
return INTERNAL_LABELS_WITH_COLOR.has(name.toLowerCase())
|
||||
}
|
||||
import { EntityType, PubsubClient } from '../pubsub'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
import { highlightRepository } from '../repository/highlight'
|
||||
import { CreateLabelInput, labelRepository } from '../repository/label'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
|
||||
const batchGetLabelsFromLinkIds = async (
|
||||
linkIds: readonly string[]
|
||||
|
|
@ -44,90 +25,11 @@ 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).findOneBy({
|
||||
id: ctx.uid,
|
||||
})
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
let labelEntity = await getLabelByName(user.id, label.name)
|
||||
|
||||
if (!labelEntity) {
|
||||
logger.info('creating new label', label.name)
|
||||
|
||||
labelEntity = await createLabel(user.id, label)
|
||||
}
|
||||
|
||||
logger.info('adding label to page', label.name, pageId)
|
||||
|
||||
return addLabelInPage(
|
||||
pageId,
|
||||
{
|
||||
id: labelEntity.id,
|
||||
name: labelEntity.name,
|
||||
color: labelEntity.color,
|
||||
description: labelEntity.description,
|
||||
createdAt: labelEntity.createdAt,
|
||||
},
|
||||
ctx
|
||||
)
|
||||
}
|
||||
|
||||
export const getLabelsByIds = async (
|
||||
userId: string,
|
||||
labelIds: string[]
|
||||
): Promise<Label[]> => {
|
||||
return getRepository(Label).find({
|
||||
where: { id: In(labelIds), user: { id: userId } },
|
||||
select: ['id', 'name', 'color', 'description', 'createdAt'],
|
||||
})
|
||||
}
|
||||
|
||||
export const getLabelByName = async (
|
||||
userId: string,
|
||||
name: string
|
||||
): Promise<Label | null> => {
|
||||
return getRepository(Label)
|
||||
.createQueryBuilder()
|
||||
.where({ user: { id: userId } })
|
||||
.andWhere('LOWER(name) = LOWER(:name)', { name })
|
||||
.getOne()
|
||||
}
|
||||
|
||||
export const createLabel = async (
|
||||
userId: string,
|
||||
label: {
|
||||
name: string
|
||||
color?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
): Promise<Label> => {
|
||||
return getRepository(Label).save({
|
||||
user: { id: userId },
|
||||
name: label.name,
|
||||
color: label.color || generateRandomColor(), // assign a random color if not provided
|
||||
description: label.description,
|
||||
internal: isLabelInternal(label.name),
|
||||
})
|
||||
}
|
||||
|
||||
export const getLabelsAndCreateIfNotExist = async (
|
||||
labels: CreateLabelInput[],
|
||||
userId: string
|
||||
): Promise<Label[]> => {
|
||||
return entityManager.transaction(async (tx) => {
|
||||
await setClaims(tx, userId)
|
||||
|
||||
return authTrx(async (tx) => {
|
||||
const labelRepo = tx.withRepository(labelRepository)
|
||||
// find existing labels
|
||||
const labelEntities = await labelRepo.findByNames(labels.map((l) => l.name))
|
||||
|
|
@ -148,3 +50,72 @@ export const getLabelsAndCreateIfNotExist = async (
|
|||
return [...labelEntities, ...newLabelEntities]
|
||||
})
|
||||
}
|
||||
|
||||
export const saveLabelsInLibraryItem = async (
|
||||
labels: Label[],
|
||||
libraryItemId: string,
|
||||
userId: string,
|
||||
pubsub: PubsubClient
|
||||
) => {
|
||||
await authTrx(async (tx) => {
|
||||
await tx
|
||||
.withRepository(libraryItemRepository)
|
||||
.createQueryBuilder()
|
||||
.relation(LibraryItem, 'labels')
|
||||
.of(libraryItemId)
|
||||
.set(labels)
|
||||
})
|
||||
|
||||
// create pubsub event
|
||||
await pubsub.entityCreated<(Label & { pageId: string })[]>(
|
||||
EntityType.LABEL,
|
||||
labels.map((l) => ({ ...l, pageId: libraryItemId })),
|
||||
userId
|
||||
)
|
||||
}
|
||||
|
||||
export const addLabelsToLibraryItem = async (
|
||||
labels: Label[],
|
||||
libraryItemId: string,
|
||||
userId: string,
|
||||
pubsub: PubsubClient
|
||||
) => {
|
||||
await authTrx(async (tx) => {
|
||||
await tx
|
||||
.withRepository(libraryItemRepository)
|
||||
.createQueryBuilder()
|
||||
.relation(LibraryItem, 'labels')
|
||||
.of(libraryItemId)
|
||||
.add(labels)
|
||||
})
|
||||
|
||||
// create pubsub event
|
||||
await pubsub.entityCreated<(Label & { pageId: string })[]>(
|
||||
EntityType.LABEL,
|
||||
labels.map((l) => ({ ...l, pageId: libraryItemId })),
|
||||
userId
|
||||
)
|
||||
}
|
||||
|
||||
export const saveLabelsInHighlight = async (
|
||||
labels: Label[],
|
||||
highlightId: string,
|
||||
userId: string,
|
||||
pubsub: PubsubClient
|
||||
) => {
|
||||
await authTrx(async (tx) => {
|
||||
await tx
|
||||
.withRepository(highlightRepository)
|
||||
.createQueryBuilder()
|
||||
.relation(Highlight, 'labels')
|
||||
.of(highlightId)
|
||||
.set(labels)
|
||||
})
|
||||
|
||||
// create pubsub event
|
||||
await pubsub.entityCreated<(Label & { highlightId: string })[]>(
|
||||
EntityType.LABEL,
|
||||
labels.map((l) => ({ ...l, highlightId })),
|
||||
userId
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import {
|
|||
LibraryItemState,
|
||||
LibraryItemType,
|
||||
} from '../entity/library_item'
|
||||
import { createPubSubClient, EntityType } from '../pubsub'
|
||||
import { entityManager } from '../repository'
|
||||
import { wordsCount } from '../utils/helpers'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { logger } from '../utils/logger'
|
||||
import {
|
||||
DateFilter,
|
||||
|
|
@ -84,13 +85,13 @@ export interface SearchItem {
|
|||
|
||||
export const createLibraryItem = async (
|
||||
libraryItem: DeepPartial<LibraryItem>,
|
||||
em = entityManager
|
||||
pubsub = createPubSubClient()
|
||||
): Promise<LibraryItem> => {
|
||||
if (
|
||||
libraryItem.readableContent &&
|
||||
libraryItem.readableContent.length > MAX_CONTENT_LENGTH
|
||||
) {
|
||||
logger.info('page content is too large', {
|
||||
logger.warn('page content is too large', {
|
||||
url: libraryItem.originalUrl,
|
||||
contentLength: libraryItem.readableContent.length,
|
||||
})
|
||||
|
|
@ -98,12 +99,15 @@ export const createLibraryItem = async (
|
|||
libraryItem.readableContent = CONTENT_LENGTH_ERROR
|
||||
}
|
||||
|
||||
return em.getRepository(LibraryItem).save({
|
||||
...libraryItem,
|
||||
savedAt: libraryItem.savedAt || new Date(),
|
||||
wordCount:
|
||||
libraryItem.wordCount ?? wordsCount(libraryItem.readableContent ?? ''),
|
||||
})
|
||||
const newItem = await libraryItemRepository.save(libraryItem)
|
||||
|
||||
await pubsub.entityCreated<LibraryItem>(
|
||||
EntityType.PAGE,
|
||||
newItem,
|
||||
newItem.user.id
|
||||
)
|
||||
|
||||
return newItem
|
||||
}
|
||||
|
||||
const buildWhereClause = (
|
||||
|
|
|
|||
|
|
@ -241,8 +241,8 @@ export const enqueueParseRequest = async ({
|
|||
labels?: CreateLabelInput[]
|
||||
locale?: string
|
||||
timezone?: string
|
||||
savedAt?: number // unix timestamp
|
||||
publishedAt?: number // unix timestamp
|
||||
savedAt?: Date
|
||||
publishedAt?: Date
|
||||
}): Promise<string> => {
|
||||
const { GOOGLE_CLOUD_PROJECT } = process.env
|
||||
const payload = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import slugify from 'voca/slugify'
|
|||
import wordsCounter from 'word-counting'
|
||||
import { updatePage } from '../elastic/pages'
|
||||
import { ArticleSavingRequestStatus, Page } from '../elastic/types'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
import { RegistrationType, User } from '../entity/user'
|
||||
import {
|
||||
ArticleSavingRequest,
|
||||
|
|
@ -149,7 +150,7 @@ export const userDataToUser = (
|
|||
} => ({
|
||||
...user,
|
||||
source: user.source as RegistrationType,
|
||||
createdAt: user.createdAt || new Date(),
|
||||
createdAt: user.createdAt,
|
||||
friendsCount: user.friendsCount || 0,
|
||||
followersCount: user.followersCount || 0,
|
||||
isFullUser: true,
|
||||
|
|
@ -187,12 +188,13 @@ export const pageError = async (
|
|||
|
||||
export const pageToArticleSavingRequest = (
|
||||
user: User,
|
||||
page: Page
|
||||
item: LibraryItem
|
||||
): ArticleSavingRequest => ({
|
||||
...page,
|
||||
...item,
|
||||
user: userDataToUser(user),
|
||||
status: page.state,
|
||||
updatedAt: page.updatedAt || new Date(),
|
||||
status: item.state as unknown as ArticleSavingRequestStatus,
|
||||
url: item.originalUrl,
|
||||
userId: user.id,
|
||||
})
|
||||
|
||||
export const isParsingTimeout = (page: Page): boolean => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { AppDataSource } from '../src/data-source'
|
||||
import { appDataSource } from '../src/data_source'
|
||||
import { stopApolloServer } from './util'
|
||||
|
||||
export const mochaGlobalTeardown = async () => {
|
||||
await stopApolloServer()
|
||||
console.log('apollo server stopped')
|
||||
|
||||
await AppDataSource.destroy()
|
||||
await appDataSource.destroy()
|
||||
console.log('db connection closed')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,4 +100,13 @@ $$ LANGUAGE plpgsql;
|
|||
CREATE TRIGGER library_item_tsv_update BEFORE INSERT OR UPDATE
|
||||
ON omnivore.library_item FOR EACH ROW EXECUTE PROCEDURE update_library_item_tsv();
|
||||
|
||||
ALTER TABLE omnivore.library_item ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY select_library_item ON omnivore.library_item FOR SELECT USING (user_id = omnivore.get_current_user_id());
|
||||
CREATE POLICY insert_library_item ON omnivore.library_item FOR INSERT WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
CREATE POLICY update_library_item ON omnivore.library_item FOR UPDATE USING (user_id = omnivore.get_current_user_id());
|
||||
CREATE POLICY delete_library_item ON omnivore.library_item FOR DELETE USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.library_item TO omnivore_user;
|
||||
|
||||
COMMIT;
|
||||
|
|
|
|||
|
|
@ -17,9 +17,25 @@ ALTER TABLE omnivore.highlight
|
|||
ADD COLUMN highlight_type highlight_type NOT NULL DEFAULT 'HIGHLIGHT',
|
||||
ADD COLUMN color text,
|
||||
ADD COLUMN html text,
|
||||
ALTER COLUMN quote DROP NOT NULL,
|
||||
ALTER COLUMN patch DROP NOT NULL,
|
||||
ALTER COLUMN highlight_position_percent DROP NOT NULL,
|
||||
ALTER COLUMN highlight_position_anchor_index DROP NOT NULL,
|
||||
DROP COLUMN article_id,
|
||||
DROP COLUMN elastic_page_id;
|
||||
|
||||
ALTER POLICY read_highlight on omnivore.highlight
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
ALTER POLICY create_highlight on omnivore.highlight
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_highlight on omnivore.highlight
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT DELETE ON omnivore.highlight TO omnivore_user;
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_library_item_highlight_annotations()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
|
|
|
|||
209
packages/db/migrations/0123.do.add_rls.sql
Executable file
209
packages/db/migrations/0123.do.add_rls.sql
Executable file
|
|
@ -0,0 +1,209 @@
|
|||
-- Type: DO
|
||||
-- Name: add_rls
|
||||
-- Description: Add RLS to the tables
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.api_key ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_api_key on omnivore.api_key
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_api_key on omnivore.api_key
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_api_key on omnivore.api_key
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_api_key on omnivore.api_key
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.api_key TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.features ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_features on omnivore.features
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_features on omnivore.features
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_features on omnivore.features
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_features on omnivore.features
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.features TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.filters ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_filters on omnivore.filters
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_filters on omnivore.filters
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_filters on omnivore.filters
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_filters on omnivore.filters
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.filters TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.integrations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_integrations on omnivore.integrations
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_integrations on omnivore.integrations
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_integrations on omnivore.integrations
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_integrations on omnivore.integrations
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.integrations TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.newsletter_emails ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_newsletter_emails on omnivore.newsletter_emails
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_newsletter_emails on omnivore.newsletter_emails
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_newsletter_emails on omnivore.newsletter_emails
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, DELETE ON omnivore.newsletter_emails TO omnivore_user;
|
||||
|
||||
ALTER POLICY read_labels on omnivore.labels
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
ALTER POLICY create_labels on omnivore.labels
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
ALTER TABLE omnivore.received_emails ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_received_emails on omnivore.received_emails
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_received_emails on omnivore.received_emails
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_received_emails on omnivore.received_emails
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_received_emails on omnivore.received_emails
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.received_emails TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.rules ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_rules on omnivore.rules
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_rules on omnivore.rules
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_rules on omnivore.rules
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_rules on omnivore.rules
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.rules TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.subscriptions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_subscriptions on omnivore.subscriptions
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_subscriptions on omnivore.subscriptions
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_subscriptions on omnivore.subscriptions
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_subscriptions on omnivore.subscriptions
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.subscriptions TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.upload_files ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_upload_files on omnivore.upload_files
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_upload_files on omnivore.upload_files
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_upload_files on omnivore.upload_files
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_upload_files on omnivore.upload_files
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.upload_files TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.webhooks ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_webhooks on omnivore.webhooks
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY create_webhooks on omnivore.webhooks
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_webhooks on omnivore.webhooks
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_webhooks on omnivore.webhooks
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.webhooks TO omnivore_user;
|
||||
|
||||
COMMIT;
|
||||
7
packages/db/migrations/0123.undo.add_rls.sql
Executable file
7
packages/db/migrations/0123.undo.add_rls.sql
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
-- Type: UNDO
|
||||
-- Name: add_rls
|
||||
-- Description: Add RLS to the tables
|
||||
|
||||
BEGIN;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in a new issue