add migration script

This commit is contained in:
Hongbo Wu 2023-09-18 22:52:54 +08:00
parent dd292879a4
commit fdd771bc06
23 changed files with 255 additions and 115 deletions

View file

@ -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

View file

@ -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({

View file

@ -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

View file

@ -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>;

View file

@ -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

View file

@ -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)
},
})

View file

@ -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,
},
},
})
)

View file

@ -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

View file

@ -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],

View file

@ -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' })
}

View file

@ -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 {

View file

@ -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()

View file

@ -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)

View file

@ -115,6 +115,7 @@ export const saveEmail = async (
siteIcon,
siteName: parseResult.parsedContent?.siteName ?? undefined,
wordCount: wordsCount(content),
subscription: input.author,
},
input.userId
)

View file

@ -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,
}
}

View file

@ -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,
}

View file

@ -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
),

View 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')

View file

@ -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

View file

@ -4,7 +4,7 @@
BEGIN;
ALTER TABLE omnivore.filters
DROP COLUMN default,
DROP COLUMN default_filter,
DROP COLUMN visible;
COMMIT;

View file

@ -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());

View file

@ -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,

View file

@ -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;