Files
omnivore/packages/api/src/services/highlights.ts

317 lines
7.5 KiB
TypeScript

import { diff_match_patch } from 'diff-match-patch'
import { DeepPartial, In } from 'typeorm'
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
import { EntityLabel } from '../entity/entity_label'
import { Highlight } from '../entity/highlight'
import { Label } from '../entity/label'
import { homePageURL } from '../env'
import { createPubSubClient, EntityEvent, EntityType } from '../pubsub'
import { authTrx } from '../repository'
import { highlightRepository } from '../repository/highlight'
import { Merge } from '../util'
import { enqueueUpdateHighlight } from '../utils/createTask'
import { deepDelete } from '../utils/helpers'
import { ItemEvent } from './library_item'
const columnsToDelete = ['user', 'sharedAt', 'libraryItem'] as const
type ColumnsToDeleteType = typeof columnsToDelete[number]
export type HighlightEvent = Merge<
Omit<DeepPartial<Highlight>, ColumnsToDeleteType>,
EntityEvent
>
export const batchGetHighlightsFromLibraryItemIds = async (
libraryItemIds: readonly string[]
): Promise<Highlight[][]> => {
const highlights = await authTrx(
async (tx) =>
tx.getRepository(Highlight).find({
where: { libraryItem: { id: In(libraryItemIds as string[]) } },
}),
{
replicationMode: 'replica',
}
)
return libraryItemIds.map((libraryItemId) =>
highlights.filter((highlight) => highlight.libraryItemId === libraryItemId)
)
}
export const getHighlightLocation = (patch: string): number | undefined => {
const dmp = new diff_match_patch()
const patches = dmp.patch_fromText(patch)
return patches[0].start1 || undefined
}
export const getHighlightUrl = (slug: string, highlightId: string): string =>
`${homePageURL()}/me/${slug}#${highlightId}`
export const createHighlights = async (
highlights: DeepPartial<Highlight>[],
userId: string
) => {
return authTrx(
async (tx) =>
tx.withRepository(highlightRepository).createAndSaves(highlights),
{
uid: userId,
}
)
}
export const createHighlight = async (
highlight: DeepPartial<Highlight>,
libraryItemId: string,
userId: string,
pubsub = createPubSubClient()
) => {
const newHighlight = await authTrx(
async (tx) => {
const repo = tx.withRepository(highlightRepository)
const newHighlight = await repo.createAndSave(highlight)
return repo.findOneOrFail({
where: { id: newHighlight.id },
})
},
{
uid: userId,
}
)
const data = deepDelete(newHighlight, columnsToDelete)
await pubsub.entityCreated<ItemEvent>(
EntityType.HIGHLIGHT,
{
id: libraryItemId,
highlights: [data],
updatedAt: new Date(),
},
userId
)
await enqueueUpdateHighlight({
libraryItemId,
userId,
})
return newHighlight
}
export const mergeHighlights = async (
highlightsToRemove: string[],
highlightToAdd: DeepPartial<Highlight>,
labels: Label[],
libraryItemId: string,
userId: string,
pubsub = createPubSubClient()
) => {
const newHighlight = await authTrx(async (tx) => {
const highlightRepo = tx.withRepository(highlightRepository)
await highlightRepo.delete(highlightsToRemove)
const newHighlight = await highlightRepo.createAndSave(highlightToAdd)
if (labels.length > 0) {
// save new labels
await tx.getRepository(EntityLabel).save(
labels.map((l) => ({
labelId: l.id,
highlightId: newHighlight.id,
}))
)
}
return highlightRepo.findOneOrFail({
where: { id: newHighlight.id },
})
})
await pubsub.entityCreated<ItemEvent>(
EntityType.HIGHLIGHT,
{
id: libraryItemId,
highlights: [newHighlight],
updatedAt: new Date(),
},
userId
)
await enqueueUpdateHighlight({
libraryItemId,
userId,
})
return newHighlight
}
export const updateHighlight = async (
highlightId: string,
highlight: QueryDeepPartialEntity<Highlight>,
userId: string,
pubsub = createPubSubClient()
) => {
const updatedHighlight = await authTrx(async (tx) => {
const highlightRepo = tx.withRepository(highlightRepository)
await highlightRepo.updateAndSave(highlightId, highlight)
return highlightRepo.findOneOrFail({
where: { id: highlightId },
})
})
const libraryItemId = updatedHighlight.libraryItem.id
await pubsub.entityUpdated<ItemEvent>(
EntityType.HIGHLIGHT,
{
id: libraryItemId,
highlights: [
{
...highlight,
id: highlightId,
updatedAt: new Date(),
quote: updatedHighlight.quote,
highlightType: updatedHighlight.highlightType,
},
],
} as ItemEvent,
userId
)
await enqueueUpdateHighlight({
libraryItemId,
userId,
})
return updatedHighlight
}
export const deleteHighlightById = async (
highlightId: string,
userId?: string
) => {
const deletedHighlight = await authTrx(
async (tx) => {
const highlightRepo = tx.withRepository(highlightRepository)
const highlight = await highlightRepo.findOneOrFail({
where: { id: highlightId },
})
await highlightRepo.delete(highlightId)
return highlight
},
{
uid: userId,
}
)
await enqueueUpdateHighlight({
libraryItemId: deletedHighlight.libraryItemId,
userId: deletedHighlight.user.id,
})
return deletedHighlight
}
export const deleteHighlightsByIds = async (
userId: string,
highlightIds: string[]
) => {
await authTrx(
async (tx) => tx.getRepository(Highlight).delete(highlightIds),
{
uid: userId,
}
)
}
export const findHighlightById = async (
highlightId: string,
userId: string
) => {
return authTrx(
async (tx) => {
const highlightRepo = tx.withRepository(highlightRepository)
return highlightRepo.findOneBy({
id: highlightId,
user: { id: userId },
})
},
{
uid: userId,
replicationMode: 'replica',
}
)
}
export const findHighlightsByLibraryItemId = async (
libraryItemId: string,
userId: string
) => {
return authTrx(
async (tx) =>
tx.withRepository(highlightRepository).find({
where: { libraryItem: { id: libraryItemId } },
relations: {
user: true,
labels: true,
},
}),
{
uid: userId,
replicationMode: 'replica',
}
)
}
export const searchHighlights = async (
userId: string,
query?: string,
limit?: number,
offset?: number
): Promise<Array<Highlight>> => {
return authTrx(
async (tx) => {
const queryBuilder = tx
.getRepository(Highlight)
.createQueryBuilder('highlight')
.innerJoin(
'highlight.libraryItem',
'libraryItem',
'highlight.libraryItemId = libraryItem.id AND libraryItem.deletedAt IS NULL'
)
.andWhere('highlight.userId = :userId', { userId })
.orderBy('highlight.updatedAt', 'DESC')
.take(limit)
.skip(offset)
if (query) {
// parse query and search by it
const labelRegex = /label:"([^"]+)"/g
const labels = Array.from(query.matchAll(labelRegex)).map(
(match) => match[1]
)
labels.forEach((label, index) => {
const alias = `label_${index}`
queryBuilder.innerJoin(
'highlight.labels',
alias,
`LOWER(${alias}.name) = LOWER(:${alias})`,
{
[alias]: label,
}
)
})
}
return queryBuilder.getMany()
},
{
uid: userId,
replicationMode: 'replica',
}
)
}