mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
add migration script
This commit is contained in:
parent
dd292879a4
commit
fdd771bc06
23 changed files with 255 additions and 115 deletions
|
|
@ -16,7 +16,6 @@ import { User } from './user'
|
|||
export enum HighlightType {
|
||||
Highlight = 'HIGHLIGHT',
|
||||
Redaction = 'REDACTION', // allowing people to remove text from the page
|
||||
Note = 'NOTE', // allowing people to add a note at the document level
|
||||
}
|
||||
|
||||
@Entity({ name: 'highlight' })
|
||||
|
|
@ -50,9 +49,6 @@ export class Highlight {
|
|||
@Column('text')
|
||||
annotation?: string | null
|
||||
|
||||
@Column('boolean')
|
||||
deleted?: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
import { Highlight } from './highlight'
|
||||
import { Label } from './label'
|
||||
import { Recommendation } from './recommendation'
|
||||
import { Subscription } from './subscription'
|
||||
import { UploadFile } from './upload_file'
|
||||
import { User } from './user'
|
||||
|
||||
|
|
@ -164,9 +163,8 @@ export class LibraryItem {
|
|||
@Column('text', { nullable: true })
|
||||
gcsArchiveId?: string | null
|
||||
|
||||
@OneToOne(() => Subscription, { cascade: true })
|
||||
@JoinColumn({ name: 'subscription_id' })
|
||||
subscription?: Subscription
|
||||
@Column('text', { nullable: true })
|
||||
subscription?: string | null
|
||||
|
||||
@ManyToMany(() => Label, { cascade: true })
|
||||
@JoinTable({
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm'
|
||||
import { Group } from './groups/group'
|
||||
import { LibraryItem } from './library_item'
|
||||
import { User } from './user'
|
||||
|
||||
|
|
@ -22,6 +23,10 @@ export class Recommendation {
|
|||
@JoinColumn({ name: 'library_item_id' })
|
||||
libraryItem!: LibraryItem
|
||||
|
||||
@ManyToOne(() => Group, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'group_id' })
|
||||
group!: Group
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
note?: string | null
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export type Article = {
|
|||
labels?: Maybe<Array<Label>>;
|
||||
language?: Maybe<Scalars['String']>;
|
||||
linkId?: Maybe<Scalars['ID']>;
|
||||
note?: Maybe<Scalars['String']>;
|
||||
originalArticleUrl?: Maybe<Scalars['String']>;
|
||||
originalHtml?: Maybe<Scalars['String']>;
|
||||
pageType?: Maybe<PageType>;
|
||||
|
|
@ -949,7 +950,6 @@ export type HighlightStats = {
|
|||
|
||||
export enum HighlightType {
|
||||
Highlight = 'HIGHLIGHT',
|
||||
Note = 'NOTE',
|
||||
Redaction = 'REDACTION'
|
||||
}
|
||||
|
||||
|
|
@ -2212,6 +2212,7 @@ export type SearchItem = {
|
|||
isArchived: Scalars['Boolean'];
|
||||
labels?: Maybe<Array<Label>>;
|
||||
language?: Maybe<Scalars['String']>;
|
||||
note?: Maybe<Scalars['String']>;
|
||||
originalArticleUrl?: Maybe<Scalars['String']>;
|
||||
ownedByViewer?: Maybe<Scalars['Boolean']>;
|
||||
pageId?: Maybe<Scalars['ID']>;
|
||||
|
|
@ -4292,6 +4293,7 @@ export type ArticleResolvers<ContextType = ResolverContext, ParentType extends R
|
|||
labels?: Resolver<Maybe<Array<ResolversTypes['Label']>>, ParentType, ContextType>;
|
||||
language?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
linkId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
|
||||
note?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
originalArticleUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
originalHtml?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
pageType?: Resolver<Maybe<ResolversTypes['PageType']>, ParentType, ContextType>;
|
||||
|
|
@ -5528,6 +5530,7 @@ export type SearchItemResolvers<ContextType = ResolverContext, ParentType extend
|
|||
isArchived?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
labels?: Resolver<Maybe<Array<ResolversTypes['Label']>>, ParentType, ContextType>;
|
||||
language?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
note?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
originalArticleUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
ownedByViewer?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
pageId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ type Article {
|
|||
labels: [Label!]
|
||||
language: String
|
||||
linkId: ID
|
||||
note: String
|
||||
originalArticleUrl: String
|
||||
originalHtml: String
|
||||
pageType: PageType
|
||||
|
|
@ -844,7 +845,6 @@ type HighlightStats {
|
|||
|
||||
enum HighlightType {
|
||||
HIGHLIGHT
|
||||
NOTE
|
||||
REDACTION
|
||||
}
|
||||
|
||||
|
|
@ -1684,6 +1684,7 @@ type SearchItem {
|
|||
isArchived: Boolean!
|
||||
labels: [Label!]
|
||||
language: String
|
||||
note: String
|
||||
originalArticleUrl: String
|
||||
ownedByViewer: Boolean
|
||||
pageId: ID
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ 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
|
||||
if (highlight.annotation !== undefined && highlight.annotation !== null) {
|
||||
highlight.annotation = unescapeHtml(highlight.annotation.toString())
|
||||
}
|
||||
if (highlight.quote !== undefined && highlight.quote !== null) {
|
||||
highlight.quote = unescapeHtml(highlight.quote.toString())
|
||||
}
|
||||
|
||||
return highlight
|
||||
}
|
||||
|
|
@ -21,9 +23,10 @@ export const highlightRepository = entityManager
|
|||
return this.findOneBy({ id })
|
||||
},
|
||||
|
||||
findByLibraryItemId(libraryItemId: string) {
|
||||
findByLibraryItemId(libraryItemId: string, userId: string) {
|
||||
return this.findBy({
|
||||
libraryItem: { id: libraryItemId },
|
||||
user: { id: userId },
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -35,14 +38,12 @@ export const highlightRepository = entityManager
|
|||
highlightId: string,
|
||||
highlight: QueryDeepPartialEntity<Highlight>
|
||||
) {
|
||||
return this.update(highlightId, {
|
||||
...highlight,
|
||||
annotation: highlight.annotation
|
||||
? unescapeHtml(highlight.annotation.toString())
|
||||
: undefined,
|
||||
quote: highlight.quote
|
||||
? unescapeHtml(highlight.quote.toString())
|
||||
: undefined,
|
||||
})
|
||||
if (highlight.annotation !== undefined && highlight.annotation !== null) {
|
||||
highlight.annotation = unescapeHtml(highlight.annotation.toString())
|
||||
}
|
||||
if (highlight.quote !== undefined && highlight.quote !== null) {
|
||||
highlight.quote = unescapeHtml(highlight.quote.toString())
|
||||
}
|
||||
return this.update(highlightId, highlight)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { Readability } from '@omnivore/readability'
|
||||
import graphqlFields from 'graphql-fields'
|
||||
import { DeepPartial } from 'typeorm'
|
||||
import {
|
||||
LibraryItem,
|
||||
|
|
@ -377,9 +378,9 @@ export const getArticleResolver = authorized<
|
|||
QueryArticleArgs
|
||||
>(async (_obj, { slug, format }, { authTrx, uid, log }, info) => {
|
||||
try {
|
||||
// const includeOriginalHtml =
|
||||
// format === ArticleFormat.Distiller ||
|
||||
// !!graphqlFields(info).article.originalHtml
|
||||
const includeOriginalHtml =
|
||||
format === ArticleFormat.Distiller ||
|
||||
!!graphqlFields(info).article.originalHtml
|
||||
|
||||
// We allow the backend to use the ID instead of a slug to fetch the article
|
||||
const libraryItem = await authTrx((tx) =>
|
||||
|
|
@ -392,6 +393,10 @@ export const getArticleResolver = authorized<
|
|||
labels: true,
|
||||
},
|
||||
uploadFile: true,
|
||||
recommendations: {
|
||||
recommender: true,
|
||||
group: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export const mergeHighlightResolver = authorized<
|
|||
MergeHighlightSuccess,
|
||||
MergeHighlightError,
|
||||
MutationMergeHighlightArgs
|
||||
>(async (_, { input }, { authTrx, log, pubsub, uid }) => {
|
||||
>(async (_, { input }, { log, pubsub, uid }) => {
|
||||
const { overlapHighlightIdList, ...newHighlightInput } = input
|
||||
|
||||
/* Compute merged annotation form the order of highlights appearing on page */
|
||||
|
|
@ -97,11 +97,10 @@ export const mergeHighlightResolver = authorized<
|
|||
const mergedColors: string[] = []
|
||||
|
||||
try {
|
||||
const existingHighlights = await authTrx(async (tx) => {
|
||||
return tx
|
||||
.withRepository(highlightRepository)
|
||||
.findByLibraryItemId(input.articleId)
|
||||
})
|
||||
const existingHighlights = await highlightRepository.findByLibraryItemId(
|
||||
input.articleId,
|
||||
uid
|
||||
)
|
||||
|
||||
existingHighlights.forEach((highlight) => {
|
||||
// filter out highlights that are in the overlap list
|
||||
|
|
@ -178,9 +177,9 @@ export const updateHighlightResolver = authorized<
|
|||
const updatedHighlight = await updateHighlight(
|
||||
input.highlightId,
|
||||
{
|
||||
annotation: input.annotation ?? undefined,
|
||||
html: input.html ?? undefined,
|
||||
quote: input.quote ?? undefined,
|
||||
annotation: input.annotation,
|
||||
html: input.html,
|
||||
quote: input.quote,
|
||||
},
|
||||
uid,
|
||||
pubsub
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ export const recommendResolver = authorized<
|
|||
member.user.id,
|
||||
item.id,
|
||||
{
|
||||
id: group.id,
|
||||
group,
|
||||
note: input.note ?? null,
|
||||
recommender: item.user,
|
||||
createdAt: new Date(),
|
||||
|
|
@ -227,14 +227,7 @@ export const recommendResolver = authorized<
|
|||
success: true,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error recommending', {
|
||||
error,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'recommendResolver',
|
||||
uid,
|
||||
},
|
||||
})
|
||||
log.error('Error recommending', error)
|
||||
|
||||
return {
|
||||
errorCodes: [RecommendErrorCode.BadRequest],
|
||||
|
|
|
|||
|
|
@ -152,17 +152,17 @@ export function pageRouter() {
|
|||
}
|
||||
const claims = jwt.decode(token) as Claims
|
||||
|
||||
const { userId, pageId, recommendation, highlightIds } = req.body as {
|
||||
const { userId, itemId, recommendation, highlightIds } = req.body as {
|
||||
userId: string
|
||||
pageId: string
|
||||
itemId: string
|
||||
recommendation: Recommendation
|
||||
highlightIds?: string[]
|
||||
}
|
||||
if (!userId || !pageId || !recommendation) {
|
||||
if (!userId || !itemId || !recommendation) {
|
||||
return res.status(400).send({ errorCode: 'BAD_DATA' })
|
||||
}
|
||||
|
||||
const item = await findLibraryItemById(pageId, userId)
|
||||
const item = await findLibraryItemById(itemId, userId)
|
||||
if (!item) {
|
||||
return res.status(404).send({ errorCode: 'NOT_FOUND' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ const schema = gql`
|
|||
readAt: Date
|
||||
recommendations: [Recommendation!]
|
||||
wordsCount: Int
|
||||
note: String
|
||||
}
|
||||
|
||||
# Query: article
|
||||
|
|
@ -686,7 +687,6 @@ const schema = gql`
|
|||
enum HighlightType {
|
||||
HIGHLIGHT
|
||||
REDACTION
|
||||
NOTE
|
||||
}
|
||||
|
||||
# Highlight
|
||||
|
|
@ -1606,6 +1606,7 @@ const schema = gql`
|
|||
wordsCount: Int
|
||||
content: String
|
||||
archivedAt: Date
|
||||
note: String
|
||||
}
|
||||
|
||||
type SearchItemEdge {
|
||||
|
|
|
|||
|
|
@ -123,12 +123,12 @@ const buildWhereClause = (
|
|||
break
|
||||
case InFilter.SUBSCRIPTION:
|
||||
queryBuilder
|
||||
.andWhere('library_item.subscription_id IS NOT NULL')
|
||||
.andWhere('library_item.subscription IS NOT NULL')
|
||||
.andWhere('library_item.archived_at IS NULL')
|
||||
break
|
||||
case InFilter.LIBRARY:
|
||||
queryBuilder
|
||||
.andWhere('library_item.subscription_id IS NULL')
|
||||
.andWhere('library_item.subscription IS NULL')
|
||||
.andWhere('library_item.archived_at IS NULL')
|
||||
break
|
||||
}
|
||||
|
|
@ -248,19 +248,24 @@ const buildWhereClause = (
|
|||
}
|
||||
|
||||
if (args.recommendedBy) {
|
||||
queryBuilder.innerJoin('library_item.recommendations', 'recommendations')
|
||||
const recommendedByInLowerCase = args.recommendedBy.toLowerCase()
|
||||
queryBuilder
|
||||
.innerJoin('library_item.recommendations', 'recommendations')
|
||||
.innerJoin('recommendations.recommender', 'recommender')
|
||||
.innerJoin('recommendations.group', 'group')
|
||||
.andWhere((qb) => {
|
||||
qb.where('lower(recommender.name) = :recommendedBy', {
|
||||
recommendedBy: recommendedByInLowerCase,
|
||||
}).orWhere('lower(group.name) = :recommendedBy', {
|
||||
recommendedBy: recommendedByInLowerCase,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (args.subscription) {
|
||||
queryBuilder
|
||||
.innerJoin('library_item.subscription', 'subscription')
|
||||
.andWhere((qb) => {
|
||||
qb.where('subscription.name = :subscription', {
|
||||
subscription: args.subscription,
|
||||
}).orWhere('subscription.url = :subscription', {
|
||||
subscription: args.subscription,
|
||||
})
|
||||
})
|
||||
queryBuilder.andWhere('lower(library_item.subscription) = :subscription', {
|
||||
subscription: args.subscription.toLowerCase(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -314,8 +319,7 @@ export const findLibraryItemById = async (
|
|||
.createQueryBuilder(LibraryItem, 'library_item')
|
||||
.leftJoinAndSelect('library_item.labels', 'labels')
|
||||
.leftJoinAndSelect('library_item.highlights', 'highlights')
|
||||
.where('library_item.user_id = :userId', { userId })
|
||||
.andWhere('library_item.id = :id', { id })
|
||||
.where('library_item.id = :id', { id })
|
||||
.getOne(),
|
||||
undefined,
|
||||
userId
|
||||
|
|
@ -332,8 +336,11 @@ export const findLibraryItemByUrl = async (
|
|||
.createQueryBuilder(LibraryItem, 'library_item')
|
||||
.leftJoinAndSelect('library_item.labels', 'labels')
|
||||
.leftJoinAndSelect('library_item.highlights', 'highlights')
|
||||
.where('library_item.user_id = :userId', { userId })
|
||||
.andWhere('library_item.original_url = :url', { url })
|
||||
.leftJoinAndSelect('library_item.recommendations', 'recommendations')
|
||||
.leftJoinAndSelect('recommendations.recommender', 'recommender')
|
||||
.leftJoinAndSelect('recommender.profile', 'profile')
|
||||
.leftJoinAndSelect('recommendations.group', 'group')
|
||||
.where('library_item.original_url = :url', { url })
|
||||
.getOne(),
|
||||
undefined,
|
||||
userId
|
||||
|
|
@ -416,11 +423,15 @@ export const findLibraryItemsByPrefix = async (
|
|||
prefix: string,
|
||||
limit = 5
|
||||
): Promise<LibraryItem[]> => {
|
||||
const prefixWildcard = `${prefix}%`
|
||||
|
||||
return authTrx(async (tx) =>
|
||||
tx
|
||||
.createQueryBuilder(LibraryItem, 'library_item')
|
||||
.where('library_item.title ILIKE :prefix', { prefix: `${prefix}%` })
|
||||
.orWhere('library_item.site_name ILIKE :prefix', { prefix: `${prefix}%` })
|
||||
.where('library_item.title ILIKE :prefix', { prefix: prefixWildcard })
|
||||
.orWhere('library_item.site_name ILIKE :prefix', {
|
||||
prefix: prefixWildcard,
|
||||
})
|
||||
.orderBy('library_item.saved_at', 'DESC')
|
||||
.limit(limit)
|
||||
.getMany()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { DeepPartial } from 'typeorm'
|
||||
import { LibraryItem, LibraryItemState } from '../entity/library_item'
|
||||
import { LibraryItem } from '../entity/library_item'
|
||||
import { Recommendation } from '../entity/recommendation'
|
||||
import { logger } from '../utils/logger'
|
||||
import {
|
||||
|
|
@ -34,23 +34,12 @@ export const addRecommendation = async (
|
|||
) || []
|
||||
|
||||
const existingRecommendations = existingItem.recommendations || []
|
||||
const isRecommended = existingRecommendations.some(
|
||||
(existingRecommendation) =>
|
||||
existingRecommendation.id === recommendation.id
|
||||
)
|
||||
if (isRecommended && newHighlights.length === 0) {
|
||||
return existingItem
|
||||
}
|
||||
|
||||
// update recommendations in the existing item
|
||||
const recommendations = isRecommended
|
||||
? undefined
|
||||
: existingRecommendations.concat(recommendation)
|
||||
|
||||
await updateLibraryItem(
|
||||
existingItem.id,
|
||||
{
|
||||
recommendations,
|
||||
recommendations: existingRecommendations.concat(recommendation),
|
||||
highlights: existingHighlights.concat(newHighlights),
|
||||
},
|
||||
userId
|
||||
|
|
@ -61,17 +50,25 @@ export const addRecommendation = async (
|
|||
|
||||
// create a new item
|
||||
const newItem: DeepPartial<LibraryItem> = {
|
||||
...item,
|
||||
id: '',
|
||||
recommendations: [recommendation],
|
||||
user: { id: userId },
|
||||
readingProgressTopPercent: 0,
|
||||
readingProgressBottomPercent: 0,
|
||||
highlights,
|
||||
readAt: null,
|
||||
labels: [],
|
||||
archivedAt: null,
|
||||
state: LibraryItemState.Succeeded,
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
author: item.author,
|
||||
description: item.description,
|
||||
originalUrl: item.originalUrl,
|
||||
originalContent: item.originalContent,
|
||||
contentReader: item.contentReader,
|
||||
directionality: item.directionality,
|
||||
itemLanguage: item.itemLanguage,
|
||||
itemType: item.itemType,
|
||||
readableContent: item.readableContent,
|
||||
siteIcon: item.siteIcon,
|
||||
siteName: item.siteName,
|
||||
thumbnail: item.thumbnail,
|
||||
uploadFile: item.uploadFile,
|
||||
wordCount: item.wordCount,
|
||||
}
|
||||
|
||||
return createLibraryItem(newItem, userId)
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export const saveEmail = async (
|
|||
siteIcon,
|
||||
siteName: parseResult.parsedContent?.siteName ?? undefined,
|
||||
wordCount: wordsCount(content),
|
||||
subscription: input.author,
|
||||
},
|
||||
input.userId
|
||||
)
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export const savePage = async (
|
|||
saveTime: input.savedAt ? new Date(input.savedAt) : undefined,
|
||||
publishedAt: input.publishedAt ? new Date(input.publishedAt) : undefined,
|
||||
state: input.state || undefined,
|
||||
rssFeedUrl: input.rssFeedUrl,
|
||||
})
|
||||
const isImported = input.source === 'csv-importer'
|
||||
|
||||
|
|
@ -122,17 +123,14 @@ export const savePage = async (
|
|||
|
||||
// check if the page already exists
|
||||
const existingLibraryItem = await authTrx((t) =>
|
||||
t.getRepository(LibraryItem).findOne({
|
||||
where: { user: { id: user.id }, originalUrl: itemToSave.originalUrl },
|
||||
relations: ['subscription'],
|
||||
t.getRepository(LibraryItem).findOneBy({
|
||||
user: { id: user.id },
|
||||
originalUrl: itemToSave.originalUrl,
|
||||
})
|
||||
)
|
||||
if (existingLibraryItem) {
|
||||
// we don't want to update an rss feed page if rss-feeder is tring to re-save it
|
||||
if (
|
||||
existingLibraryItem.subscription &&
|
||||
existingLibraryItem.subscription.url === input.rssFeedUrl
|
||||
) {
|
||||
if (existingLibraryItem.subscription === input.rssFeedUrl) {
|
||||
return {
|
||||
clientRequestId,
|
||||
url: `${homePageURL()}/${user.profile.username}/${slug}`,
|
||||
|
|
@ -206,6 +204,7 @@ export const parsedContentToLibraryItem = ({
|
|||
saveTime,
|
||||
publishedAt,
|
||||
state,
|
||||
rssFeedUrl,
|
||||
}: {
|
||||
url: string
|
||||
userId: string
|
||||
|
|
@ -223,6 +222,7 @@ export const parsedContentToLibraryItem = ({
|
|||
saveTime?: Date
|
||||
publishedAt?: Date | null
|
||||
state?: ArticleSavingRequestStatus | null
|
||||
rssFeedUrl?: string | null
|
||||
}): DeepPartial<LibraryItem> & { originalUrl: string } => {
|
||||
return {
|
||||
id: itemId || undefined,
|
||||
|
|
@ -260,5 +260,6 @@ export const parsedContentToLibraryItem = ({
|
|||
siteIcon: parsedContent?.siteIcon,
|
||||
wordCount: wordsCount(parsedContent?.textContent || ''),
|
||||
contentReader: contentReaderForLibraryItem(itemType, uploadFileId),
|
||||
subscription: rssFeedUrl,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -442,15 +442,15 @@ export const enqueueTextToSpeech = async ({
|
|||
|
||||
export const enqueueRecommendation = async (
|
||||
userId: string,
|
||||
pageId: string,
|
||||
recommendation: Recommendation,
|
||||
itemId: string,
|
||||
recommendation: Partial<Recommendation>,
|
||||
authToken: string,
|
||||
highlightIds?: string[]
|
||||
): Promise<string> => {
|
||||
const { GOOGLE_CLOUD_PROJECT } = process.env
|
||||
const payload = {
|
||||
userId,
|
||||
pageId,
|
||||
itemId,
|
||||
recommendation,
|
||||
highlightIds,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,7 +211,13 @@ const recommandationDataToRecommendation = (
|
|||
recommendation: RecommendationData
|
||||
): Recommendation => ({
|
||||
...recommendation,
|
||||
name: recommendation.recommender.name,
|
||||
user: {
|
||||
userId: recommendation.recommender.id,
|
||||
username: recommendation.recommender.profile.username,
|
||||
profileImageURL: recommendation.recommender.profile.pictureUrl,
|
||||
name: recommendation.recommender.name,
|
||||
},
|
||||
name: recommendation.group.name,
|
||||
recommendedAt: recommendation.createdAt,
|
||||
})
|
||||
|
||||
|
|
@ -236,7 +242,6 @@ export const libraryItemToArticle = (item: LibraryItem): Article => ({
|
|||
recommendations: item.recommendations?.map(
|
||||
recommandationDataToRecommendation
|
||||
),
|
||||
subscription: item.subscription?.name,
|
||||
image: item.thumbnail,
|
||||
contentReader: item.contentReader as unknown as ContentReader,
|
||||
readingProgressAnchorIndex: item.readingProgressHighestReadAnchor,
|
||||
|
|
@ -256,7 +261,6 @@ export const libraryItemToSearchItem = (item: LibraryItem): SearchItem => ({
|
|||
readingProgressPercent: item.readingProgressBottomPercent,
|
||||
contentReader: item.contentReader as unknown as ContentReader,
|
||||
readingProgressAnchorIndex: item.readingProgressHighestReadAnchor,
|
||||
subscription: item.subscription?.name,
|
||||
recommendations: item.recommendations?.map(
|
||||
recommandationDataToRecommendation
|
||||
),
|
||||
|
|
|
|||
129
packages/db/elastic_migrations/migrate_from_elastic.py
Normal file
129
packages/db/elastic_migrations/migrate_from_elastic.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/python
|
||||
import hashlib
|
||||
import os
|
||||
import uuid
|
||||
from collections import deque
|
||||
|
||||
import pandas as pd
|
||||
from elasticsearch import Elasticsearch
|
||||
from elasticsearch.helpers import scan as escan
|
||||
|
||||
PG_HOST = os.getenv('PG_HOST', 'localhost')
|
||||
PG_PORT = os.getenv('PG_PORT', 5432)
|
||||
PG_USER = os.getenv('PG_USER', 'app_user')
|
||||
PG_PASSWORD = os.getenv('PG_PASSWORD', 'app_pass')
|
||||
PG_DB = os.getenv('PG_DB', 'omnivore')
|
||||
ES_URL = os.getenv('ES_URL', 'http://localhost:9200')
|
||||
ES_USERNAME = os.getenv('ES_USERNAME', 'elastic')
|
||||
ES_PASSWORD = os.getenv('ES_PASSWORD', 'password')
|
||||
ES_SCAN_SIZE = os.getenv('ES_SCAN_SIZE', 100)
|
||||
ES_INDEX = os.getenv('ES_INDEX', 'pages_alias')
|
||||
|
||||
CUT_OFF_DATE = os.getenv('CUT_OFF_DATE', '2000-01-01')
|
||||
|
||||
|
||||
def convert_string_to_uuid(val: str):
|
||||
hex_string = hashlib.md5(val.encode('UTF-8')).hexdigest()
|
||||
return uuid.UUID(hex=hex_string)
|
||||
|
||||
|
||||
# def assertData(conn, client):
|
||||
# # get all users from postgres
|
||||
# try:
|
||||
# success = 0
|
||||
# failure = 0
|
||||
# cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
# cursor.execute('''SELECT id FROM omnivore.user''')
|
||||
# result = cursor.fetchall()
|
||||
# for row in result:
|
||||
# userId = row['id']
|
||||
# cursor.execute(
|
||||
# f'SELECT COUNT(*) FROM omnivore.links WHERE user_id = \'{userId}\'''')
|
||||
# countInPostgres = cursor.fetchone()['count']
|
||||
# countInElastic = client.count(
|
||||
# index='pages_alias', body={'query': {'term': {'userId': userId}}})['count']
|
||||
|
||||
# if countInPostgres == countInElastic:
|
||||
# success += 1
|
||||
# print(f'User {userId} OK')
|
||||
# else:
|
||||
# failure += 1
|
||||
# print(
|
||||
# f'User {userId} ERROR: postgres: {countInPostgres}, elastic: {countInElastic}')
|
||||
# cursor.close()
|
||||
# print(f'Asserted data, success: {success}, failure: {failure}')
|
||||
# except Exception as err:
|
||||
# print('Assert data ERROR:', err)
|
||||
# exit(1)
|
||||
|
||||
|
||||
def update_postgres_data(conn, query, table):
|
||||
try:
|
||||
print('Executing query: {}'.format(query))
|
||||
# update data in postgres
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query)
|
||||
count = cursor.rowcount
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
print(f'Updated {table} in postgres, rows: ', count)
|
||||
except Exception as err:
|
||||
print('Update postgres data ERROR:', err)
|
||||
|
||||
|
||||
def get_data_from_es():
|
||||
# elastic client
|
||||
client = Elasticsearch(ES_URL, http_auth=(
|
||||
ES_USERNAME, ES_PASSWORD), retry_on_timeout=True)
|
||||
try:
|
||||
print('Elasticsearch client connected', client.info())
|
||||
except Exception as err:
|
||||
print('Elasticsearch client ERROR:', err)
|
||||
exit(1)
|
||||
|
||||
query = {
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"range": {
|
||||
"updatedAt": {
|
||||
"gte": CUT_OFF_DATE
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"updatedAt": {
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
# Scan API for larger library
|
||||
response = escan(client=client, index=ES_INDEX, query=query,
|
||||
preserve_order=True, size=ES_SCAN_SIZE,
|
||||
request_timeout=30)
|
||||
|
||||
# Initialize a double ended queue
|
||||
output = deque()
|
||||
# Extend deque with iterator
|
||||
output.extend(response)
|
||||
# Convert deque to DataFrame
|
||||
df = pd.json_normalize(output)
|
||||
df = df[[x for x in df.columns if "_source." in x or x == '_id']]
|
||||
|
||||
client.close()
|
||||
return df
|
||||
|
||||
|
||||
print('Starting migration')
|
||||
|
||||
# get data from elastic
|
||||
df = get_data_from_es()
|
||||
print(df.head())
|
||||
|
||||
print('Migration complete')
|
||||
|
|
@ -54,7 +54,7 @@ CREATE TABLE omnivore.library_item (
|
|||
text_content_hash text,
|
||||
gcs_archive_id text,
|
||||
directionality directionality_type NOT NULL DEFAULT 'LTR',
|
||||
subscription_id uuid REFERENCES omnivore.subscriptions ON DELETE CASCADE,
|
||||
subscription text,
|
||||
label_names text[] NOT NULL DEFAULT array[]::text[], -- array of label names of the item
|
||||
highlight_labels text[] NOT NULL DEFAULT array[]::text[], -- array of label names of the item's highlights
|
||||
highlight_annotations text[] NOT NULL DEFAULT array[]::text[], -- array of highlight annotations of the item
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.filters
|
||||
DROP COLUMN default,
|
||||
DROP COLUMN default_filter,
|
||||
DROP COLUMN visible;
|
||||
|
||||
COMMIT;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ BEGIN;
|
|||
|
||||
CREATE TYPE highlight_type AS ENUM (
|
||||
'HIGHLIGHT',
|
||||
'REDACTION',
|
||||
'NOTE'
|
||||
'REDACTION'
|
||||
);
|
||||
|
||||
ALTER TABLE omnivore.highlight
|
||||
|
|
@ -19,15 +18,10 @@ ALTER TABLE omnivore.highlight
|
|||
ADD COLUMN html text,
|
||||
ALTER COLUMN quote DROP NOT NULL,
|
||||
ALTER COLUMN patch DROP NOT NULL,
|
||||
DROP COLUMN deleted,
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ BEGIN;
|
|||
DROP TRIGGER IF EXISTS library_item_highlight_annotations_update ON omnivore.highlight;
|
||||
DROP FUNCTION IF EXISTS update_library_item_highlight_annotations();
|
||||
|
||||
ALTER POLICY read_highlight on omnivore.highlight USING (true);
|
||||
ALTER POLICY create_highlight on omnivore.highlight WITH CHECK (true);
|
||||
DROP POLICY delete_highlight on omnivore.highlight;
|
||||
REVOKE DELETE ON omnivore.highlight FROM omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.highlight
|
||||
ADD COLUMN deleted boolean DEFAULT false,
|
||||
ADD COLUMN article_id uuid,
|
||||
ADD COLUMN elastic_page_id uuid,
|
||||
DROP COLUMN library_item_id,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ CREATE TABLE omnivore.recommendation (
|
|||
id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(),
|
||||
library_item_id uuid NOT NULL REFERENCES omnivore.library_item ON DELETE CASCADE,
|
||||
recommender_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE,
|
||||
group_id uuid NOT NULL REFERENCES omnivore.group ON DELETE CASCADE,
|
||||
note text,
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
UNIQUE (library_item_id, recommender_id, group_id)
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT ON omnivore.recommendation TO omnivore_user;
|
||||
|
|
|
|||
Loading…
Reference in a new issue