fix label constraint

This commit is contained in:
Hongbo Wu
2023-09-05 17:23:58 +08:00
parent b72acf8e6f
commit 5ed7e819e8
27 changed files with 227 additions and 240 deletions

View File

@ -57,7 +57,7 @@ export class Highlight {
createdAt!: Date
@UpdateDateColumn()
updatedAt!: Date
updatedAt?: Date | null
@Column('timestamp')
sharedAt?: Date

View File

@ -5,6 +5,7 @@ import {
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import { User } from './user'
@ -34,4 +35,7 @@ export class Label {
@Column('boolean', { default: false })
internal!: boolean
@UpdateDateColumn()
updatedAt?: Date | null
}

View File

@ -103,7 +103,7 @@ export class LibraryItem {
readAt?: Date | null
@UpdateDateColumn()
updatedAt!: Date
updatedAt?: Date | null
@Column('text', { nullable: true })
itemLanguage?: string | null

View File

@ -1,43 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import { LibraryItem } from './library_item'
import { User } from './user'
@Entity({ name: 'library_item_preview' })
export class LibraryItemPreview {
@PrimaryGeneratedColumn('uuid')
id?: string
@OneToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'sender_id' })
sender!: User
@Column('text', { name: 'recipient_ids', array: true })
recipientIds!: string[]
@OneToOne(() => LibraryItem, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'library_item_id' })
libraryItem!: LibraryItem
@Column('text')
thumbnail?: string
@Column('bool', { default: false })
includesNote?: boolean
@Column('bool', { default: false })
includesHighlight?: boolean
@CreateDateColumn()
createdAt?: Date
@UpdateDateColumn()
updatedAt?: Date
}

View File

@ -19,7 +19,7 @@ export class PublishEntitySubscriber implements EntitySubscriberInterface {
const msg = JSON.stringify({
type: 'EntityCreated',
entity: event.entity,
entityClass: event.entity.constructor.name,
entityClass: event.entity?.constructor?.name,
})
if (env.dev.isLocal) {

View File

@ -124,7 +124,7 @@ export type Article = {
title: Scalars['String'];
unsubHttpUrl?: Maybe<Scalars['String']>;
unsubMailTo?: Maybe<Scalars['String']>;
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
uploadFileId?: Maybe<Scalars['ID']>;
url: Scalars['String'];
wordsCount?: Maybe<Scalars['Int']>;
@ -167,7 +167,7 @@ export type ArticleSavingRequest = {
id: Scalars['ID'];
slug: Scalars['String'];
status: ArticleSavingRequestStatus;
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
url: Scalars['String'];
user: User;
/** @deprecated userId has been replaced with user */
@ -721,7 +721,7 @@ export type Feature = {
id: Scalars['ID'];
name: Scalars['String'];
token: Scalars['String'];
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
};
export type FeedArticle = {
@ -771,7 +771,7 @@ export type Filter = {
id: Scalars['ID'];
name: Scalars['String'];
position: Scalars['Int'];
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
visible?: Maybe<Scalars['Boolean']>;
};
@ -928,7 +928,7 @@ export type Highlight = {
shortId: Scalars['String'];
suffix?: Maybe<Scalars['String']>;
type: HighlightType;
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
user: User;
};
@ -938,7 +938,7 @@ export type HighlightReply = {
highlight: Highlight;
id: Scalars['ID'];
text: Scalars['String'];
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
user: User;
};
@ -979,7 +979,7 @@ export type Integration = {
taskName?: Maybe<Scalars['String']>;
token: Scalars['String'];
type: IntegrationType;
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
};
export enum IntegrationType {
@ -1082,7 +1082,7 @@ export type Link = {
shareInfo: LinkShareInfo;
shareStats: ShareStats;
slug: Scalars['String'];
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
url: Scalars['String'];
};
@ -1646,7 +1646,7 @@ export type Page = {
readableHtml: Scalars['String'];
title: Scalars['String'];
type: PageType;
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
url: Scalars['String'];
};
@ -1953,7 +1953,7 @@ export type RecommendationGroup = {
members: Array<User>;
name: Scalars['String'];
topics?: Maybe<Array<Scalars['String']>>;
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
};
export type RecommendingUser = {
@ -2037,7 +2037,7 @@ export type Rule = {
filter: Scalars['String'];
id: Scalars['ID'];
name: Scalars['String'];
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
};
export type RuleAction = {
@ -2642,7 +2642,7 @@ export type Subscription = {
type: SubscriptionType;
unsubscribeHttpUrl?: Maybe<Scalars['String']>;
unsubscribeMailTo?: Maybe<Scalars['String']>;
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
url?: Maybe<Scalars['String']>;
};
@ -3191,7 +3191,7 @@ export type Webhook = {
eventTypes: Array<WebhookEvent>;
id: Scalars['ID'];
method: Scalars['String'];
updatedAt: Scalars['Date'];
updatedAt?: Maybe<Scalars['Date']>;
url: Scalars['String'];
};
@ -4314,7 +4314,7 @@ export type ArticleResolvers<ContextType = ResolverContext, ParentType extends R
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
unsubHttpUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
unsubMailTo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
uploadFileId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
wordsCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
@ -4343,7 +4343,7 @@ export type ArticleSavingRequestResolvers<ContextType = ResolverContext, ParentT
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
slug?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
status?: Resolver<ResolversTypes['ArticleSavingRequestStatus'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
user?: Resolver<ResolversTypes['User'], ParentType, ContextType>;
userId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
@ -4712,7 +4712,7 @@ export type FeatureResolvers<ContextType = ResolverContext, ParentType extends R
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
token?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -4760,7 +4760,7 @@ export type FilterResolvers<ContextType = ResolverContext, ParentType extends Re
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
position?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
visible?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -4882,7 +4882,7 @@ export type HighlightResolvers<ContextType = ResolverContext, ParentType extends
shortId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
suffix?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
type?: Resolver<ResolversTypes['HighlightType'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
user?: Resolver<ResolversTypes['User'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -4892,7 +4892,7 @@ export type HighlightReplyResolvers<ContextType = ResolverContext, ParentType ex
highlight?: Resolver<ResolversTypes['Highlight'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
text?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
user?: Resolver<ResolversTypes['User'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -4924,7 +4924,7 @@ export type IntegrationResolvers<ContextType = ResolverContext, ParentType exten
taskName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
token?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
type?: Resolver<ResolversTypes['IntegrationType'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -5007,7 +5007,7 @@ export type LinkResolvers<ContextType = ResolverContext, ParentType extends Reso
shareInfo?: Resolver<ResolversTypes['LinkShareInfo'], ParentType, ContextType>;
shareStats?: Resolver<ResolversTypes['ShareStats'], ParentType, ContextType>;
slug?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -5216,7 +5216,7 @@ export type PageResolvers<ContextType = ResolverContext, ParentType extends Reso
readableHtml?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
type?: Resolver<ResolversTypes['PageType'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -5379,7 +5379,7 @@ export type RecommendationGroupResolvers<ContextType = ResolverContext, ParentTy
members?: Resolver<Array<ResolversTypes['User']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
topics?: Resolver<Maybe<Array<ResolversTypes['String']>>, ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -5440,7 +5440,7 @@ export type RuleResolvers<ContextType = ResolverContext, ParentType extends Reso
filter?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -5790,7 +5790,7 @@ export type SubscriptionResolvers<ContextType = ResolverContext, ParentType exte
type?: SubscriptionResolver<ResolversTypes['SubscriptionType'], "type", ParentType, ContextType>;
unsubscribeHttpUrl?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "unsubscribeHttpUrl", ParentType, ContextType>;
unsubscribeMailTo?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "unsubscribeMailTo", ParentType, ContextType>;
updatedAt?: SubscriptionResolver<ResolversTypes['Date'], "updatedAt", ParentType, ContextType>;
updatedAt?: SubscriptionResolver<Maybe<ResolversTypes['Date']>, "updatedAt", ParentType, ContextType>;
url?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "url", ParentType, ContextType>;
};
@ -6138,7 +6138,7 @@ export type WebhookResolvers<ContextType = ResolverContext, ParentType extends R
eventTypes?: Resolver<Array<ResolversTypes['WebhookEvent']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
method?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
updatedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
url?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

View File

@ -100,7 +100,7 @@ type Article {
title: String!
unsubHttpUrl: String
unsubMailTo: String
updatedAt: Date!
updatedAt: Date
uploadFileId: ID
url: String!
wordsCount: Int
@ -134,7 +134,7 @@ type ArticleSavingRequest {
id: ID!
slug: String!
status: ArticleSavingRequestStatus!
updatedAt: Date!
updatedAt: Date
url: String!
user: User!
userId: ID! @deprecated(reason: "userId has been replaced with user")
@ -638,7 +638,7 @@ type Feature {
id: ID!
name: String!
token: String!
updatedAt: Date!
updatedAt: Date
}
type FeedArticle {
@ -683,7 +683,7 @@ type Filter {
id: ID!
name: String!
position: Int!
updatedAt: Date!
updatedAt: Date
visible: Boolean
}
@ -825,7 +825,7 @@ type Highlight {
shortId: String!
suffix: String
type: HighlightType!
updatedAt: Date!
updatedAt: Date
user: User!
}
@ -834,7 +834,7 @@ type HighlightReply {
highlight: Highlight!
id: ID!
text: String!
updatedAt: Date!
updatedAt: Date
user: User!
}
@ -871,7 +871,7 @@ type Integration {
taskName: String
token: String!
type: IntegrationType!
updatedAt: Date!
updatedAt: Date
}
enum IntegrationType {
@ -964,7 +964,7 @@ type Link {
shareInfo: LinkShareInfo!
shareStats: ShareStats!
slug: String!
updatedAt: Date!
updatedAt: Date
url: String!
}
@ -1217,7 +1217,7 @@ type Page {
readableHtml: String!
title: String!
type: PageType!
updatedAt: Date!
updatedAt: Date
url: String!
}
@ -1444,7 +1444,7 @@ type RecommendationGroup {
members: [User!]!
name: String!
topics: [String!]
updatedAt: Date!
updatedAt: Date
}
type RecommendingUser {
@ -1520,7 +1520,7 @@ type Rule {
filter: String!
id: ID!
name: String!
updatedAt: Date!
updatedAt: Date
}
type RuleAction {
@ -2082,7 +2082,7 @@ type Subscription {
type: SubscriptionType!
unsubscribeHttpUrl: String
unsubscribeMailTo: String
updatedAt: Date!
updatedAt: Date
url: String
}
@ -2585,7 +2585,7 @@ type Webhook {
eventTypes: [WebhookEvent!]!
id: ID!
method: String!
updatedAt: Date!
updatedAt: Date
url: String!
}

View File

@ -26,10 +26,15 @@ export const highlightRepository = entityManager
})
},
createAndSave(highlight: DeepPartial<Highlight>, userId: string) {
createAndSave(
highlight: DeepPartial<Highlight>,
libraryItemId: string,
userId: string
) {
return this.save({
...unescapeHighlight(highlight),
user: { id: userId },
libraryItem: { id: libraryItemId },
})
},
})

View File

@ -4,7 +4,6 @@
/* 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 {
LibraryItem,
LibraryItemState,
@ -12,7 +11,6 @@ import {
} from '../../entity/library_item'
import { env } from '../../env'
import {
Article,
ArticleError,
ArticleErrorCode,
ArticleSavingRequestStatus,
@ -25,13 +23,11 @@ import {
CreateArticleError,
CreateArticleErrorCode,
CreateArticleSuccess,
FeedArticle,
MutationBulkActionArgs,
MutationCreateArticleArgs,
MutationSaveArticleReadingProgressArgs,
MutationSetBookmarkArticleArgs,
MutationSetFavoriteArticleArgs,
PageInfo,
PageType,
QueryArticleArgs,
QuerySearchArgs,
@ -48,7 +44,6 @@ import {
SetFavoriteArticleError,
SetFavoriteArticleErrorCode,
SetFavoriteArticleSuccess,
SetShareArticleSuccess,
TypeaheadSearchError,
TypeaheadSearchErrorCode,
TypeaheadSearchSuccess,
@ -80,7 +75,6 @@ import {
setFileUploadComplete,
} from '../../services/upload_file'
import { traceAs } from '../../tracing'
import { Merge } from '../../util'
import { analytics } from '../../utils/analytics'
import { isSiteBlockedForParse } from '../../utils/blocked'
import {
@ -89,7 +83,7 @@ import {
generateSlug,
isBase64Image,
isParsingTimeout,
libraryItemToPartialArticle,
libraryItemToArticle,
libraryItemToSearchItem,
pageError,
titleForFilePath,
@ -117,16 +111,6 @@ export enum ArticleFormat {
HighlightedMarkdown = 'highlightedMarkdown',
}
export type PartialArticle = Omit<
Article,
| 'updatedAt'
| 'readingProgressPercent'
| 'readingProgressAnchorIndex'
| 'savedAt'
| 'highlights'
| 'contentReader'
>
// These two page types are better handled by the backend
// where we can use APIs to fetch their underlying content.
const FORCE_PUPPETEER_URLS = [
@ -136,12 +120,8 @@ const FORCE_PUPPETEER_URLS = [
]
const UNPARSEABLE_CONTENT = '<p>We were unable to parse this page.</p>'
export type CreateArticlesSuccessPartial = Merge<
CreateArticleSuccess,
{ createdArticle: PartialArticle }
>
export const createArticleResolver = authorized<
CreateArticlesSuccessPartial,
CreateArticleSuccess,
CreateArticleError,
MutationCreateArticleArgs
>(
@ -232,6 +212,11 @@ export const createArticleResolver = authorized<
url,
hash: '',
isArchived: false,
readingProgressAnchorIndex: 0,
readingProgressPercent: 0,
highlights: [],
savedAt: new Date(),
updatedAt: new Date(),
},
}
@ -378,7 +363,7 @@ export const createArticleResolver = authorized<
return {
user,
created: true,
createdArticle: libraryItemToPartialArticle(libraryItemToReturn),
createdArticle: libraryItemToArticle(libraryItemToReturn),
}
} catch (error) {
log.error('Error creating article', error)
@ -394,29 +379,28 @@ export const createArticleResolver = authorized<
}
)
export type ArticleSuccessPartial = Merge<
ArticleSuccess,
{ article: PartialArticle }
>
export const getArticleResolver = authorized<
ArticleSuccessPartial,
ArticleSuccess,
ArticleError,
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) =>
tx
.withRepository(libraryItemRepository)
.createQueryBuilder('library_item')
.leftJoinAndSelect('library_item.labels', 'labels')
.leftJoinAndSelect('library_item.highlights', 'highlights')
.where('library_item.id = :id', { id: slug })
.getOne()
tx.withRepository(libraryItemRepository).findOne({
where: { slug },
relations: {
labels: true,
highlights: {
user: true,
labels: true,
},
},
})
)
if (!libraryItem || libraryItem.state === LibraryItemState.Deleted) {
@ -444,7 +428,7 @@ export const getArticleResolver = authorized<
}
return {
article: libraryItemToPartialArticle(libraryItem),
article: libraryItemToArticle(libraryItem),
}
} catch (error) {
log.error(error)
@ -452,26 +436,26 @@ export const getArticleResolver = authorized<
}
})
type PaginatedPartialArticles = {
edges: { cursor: string; node: PartialArticle }[]
pageInfo: PageInfo
}
// type PaginatedPartialArticles = {
// edges: { cursor: string; node: PartialArticle }[]
// pageInfo: PageInfo
// }
export type SetShareArticleSuccessPartial = Merge<
SetShareArticleSuccess,
{
updatedFeedArticle?: Omit<
FeedArticle,
| 'sharedBy'
| 'article'
| 'highlightsCount'
| 'annotationsCount'
| 'reactions'
>
updatedFeedArticleId?: string
updatedArticle: PartialArticle
}
>
// export type SetShareArticleSuccessPartial = Merge<
// SetShareArticleSuccess,
// {
// updatedFeedArticle?: Omit<
// FeedArticle,
// | 'sharedBy'
// | 'article'
// | 'highlightsCount'
// | 'annotationsCount'
// | 'reactions'
// >
// updatedFeedArticleId?: string
// updatedArticle: PartialArticle
// }
// >
// export const setShareArticleResolver = authorized<
// SetShareArticleSuccessPartial,
@ -532,12 +516,8 @@ export type SetShareArticleSuccessPartial = Merge<
// }
// )
export type SetBookmarkArticleSuccessPartial = Merge<
SetBookmarkArticleSuccess,
{ bookmarkedArticle: PartialArticle }
>
export const setBookmarkArticleResolver = authorized<
SetBookmarkArticleSuccessPartial,
SetBookmarkArticleSuccess,
SetBookmarkArticleError,
MutationSetBookmarkArticleArgs
>(async (_, { input: { articleID } }, { uid, log, pubsub }) => {
@ -574,16 +554,12 @@ export const setBookmarkArticleResolver = authorized<
})
// Make sure article.id instead of userArticle.id has passed. We use it for cache updates
return {
bookmarkedArticle: libraryItemToPartialArticle(deletedLibraryItem),
bookmarkedArticle: libraryItemToArticle(deletedLibraryItem),
}
})
export type SaveArticleReadingProgressSuccessPartial = Merge<
SaveArticleReadingProgressSuccess,
{ updatedArticle: PartialArticle }
>
export const saveArticleReadingProgressResolver = authorized<
SaveArticleReadingProgressSuccessPartial,
SaveArticleReadingProgressSuccess,
SaveArticleReadingProgressError,
MutationSaveArticleReadingProgressArgs
>(
@ -648,7 +624,7 @@ export const saveArticleReadingProgressResolver = authorized<
const updatedItem = await updateLibraryItem(id, updatedPart, uid, pubsub)
return {
updatedArticle: libraryItemToPartialArticle(updatedItem),
updatedArticle: libraryItemToArticle(updatedItem),
}
}
)

View File

@ -29,9 +29,10 @@ import {
} from '../../generated/graphql'
import { highlightRepository } from '../../repository/highlight'
import {
createHighlight,
deleteHighlightById,
mergeHighlights,
saveHighlight,
updateHighlight,
} from '../../services/highlights'
import { analytics } from '../../utils/analytics'
import { authorized } from '../../utils/helpers'
@ -54,7 +55,12 @@ export const createHighlightResolver = authorized<
MutationCreateHighlightArgs
>(async (_, { input }, { log, pubsub, uid }) => {
try {
const newHighlight = await saveHighlight(input, uid, pubsub)
const newHighlight = await createHighlight(
input,
input.articleId,
uid,
pubsub
)
analytics.track({
userId: uid,
@ -131,6 +137,7 @@ export const mergeHighlightResolver = authorized<
const newHighlight = await mergeHighlights(
overlapHighlightIdList,
highlight,
input.articleId,
uid,
pubsub
)
@ -162,7 +169,12 @@ export const updateHighlightResolver = authorized<
MutationUpdateHighlightArgs
>(async (_, { input }, { pubsub, uid, log }) => {
try {
const updatedHighlight = await saveHighlight(input, uid, pubsub)
const updatedHighlight = await updateHighlight(
input.highlightId,
input,
uid,
pubsub
)
return { highlight: highlightDataToHighlight(updatedHighlight) }
} catch (error) {

View File

@ -276,6 +276,7 @@ export const setLabelsForHighlightResolver = authorized<
}
}
}
// save labels in the library item
await saveLabelsInHighlight(labelsSet, input.highlightId, uid, pubsub)
@ -293,7 +294,7 @@ export const setLabelsForHighlightResolver = authorized<
labels: labelsSet,
}
} catch (error) {
log.error(error)
log.error('setLabelsForHighlightResolver error', error)
return {
errorCodes: [SetLabelsErrorCode.BadRequest],
}

View File

@ -291,7 +291,7 @@ const schema = gql`
slug: String!
savedBy: User!
savedAt: Date!
updatedAt: Date!
updatedAt: Date
savedByViewer: Boolean!
postedByViewer: Boolean!
@ -333,7 +333,7 @@ const schema = gql`
originalHtml: String!
readableHtml: String!
createdAt: Date!
updatedAt: Date!
updatedAt: Date
}
type RecommendingUser {
@ -368,7 +368,7 @@ const schema = gql`
originalHtml: String
createdAt: Date!
savedAt: Date!
updatedAt: Date!
updatedAt: Date
publishedAt: Date
readingProgressTopPercent: Float
readingProgressPercent: Float!
@ -705,7 +705,7 @@ const schema = gql`
replies: [HighlightReply!]!
sharedAt: Date
createdAt: Date!
updatedAt: Date!
updatedAt: Date
reactions: [Reaction!]!
createdByMe: Boolean!
highlightPositionPercent: Float
@ -835,7 +835,7 @@ const schema = gql`
highlight: Highlight!
text: String!
createdAt: Date!
updatedAt: Date!
updatedAt: Date
}
input CreateHighlightReplyInput {
@ -1109,7 +1109,7 @@ const schema = gql`
status: ArticleSavingRequestStatus!
errorCode: CreateArticleErrorCode
createdAt: Date!
updatedAt: Date!
updatedAt: Date
url: String!
}
@ -1652,7 +1652,7 @@ const schema = gql`
count: Int!
lastFetchedAt: Date
createdAt: Date!
updatedAt: Date!
updatedAt: Date
}
enum SubscriptionStatus {
@ -1757,7 +1757,7 @@ const schema = gql`
method: String!
enabled: Boolean!
createdAt: Date!
updatedAt: Date!
updatedAt: Date
}
type SetWebhookError {
@ -1949,7 +1949,7 @@ const schema = gql`
token: String!
enabled: Boolean!
createdAt: Date!
updatedAt: Date!
updatedAt: Date
taskName: String
}
@ -2047,7 +2047,7 @@ const schema = gql`
name: String!
token: String!
createdAt: Date!
updatedAt: Date!
updatedAt: Date
grantedAt: Date
expiresAt: Date
}
@ -2074,7 +2074,7 @@ const schema = gql`
actions: [RuleAction!]!
enabled: Boolean!
createdAt: Date!
updatedAt: Date!
updatedAt: Date
eventTypes: [RuleEventType!]!
}
@ -2188,7 +2188,7 @@ const schema = gql`
category: String!
description: String
createdAt: Date!
updatedAt: Date!
updatedAt: Date
defaultFilter: Boolean
visible: Boolean
}
@ -2304,7 +2304,7 @@ const schema = gql`
admins: [User!]!
members: [User!]!
createdAt: Date!
updatedAt: Date!
updatedAt: Date
canPost: Boolean!
description: String
topics: [String!]

View File

@ -6,7 +6,7 @@ import { createPubSubClient, EntityType } from '../pubsub'
import { authTrx, setClaims } from '../repository'
import { highlightRepository } from '../repository/highlight'
type HighlightEvent = Highlight & { pageId: string }
type HighlightEvent = DeepPartial<Highlight> & { pageId: string }
export const getHighlightLocation = (patch: string): number | undefined => {
const dmp = new diff_match_patch()
@ -17,8 +17,9 @@ export const getHighlightLocation = (patch: string): number | undefined => {
export const getHighlightUrl = (slug: string, highlightId: string): string =>
`${homePageURL()}/me/${slug}#${highlightId}`
export const saveHighlight = async (
export const createHighlight = async (
highlight: DeepPartial<Highlight>,
libraryItemId: string,
userId: string,
pubsub = createPubSubClient()
) => {
@ -27,12 +28,12 @@ export const saveHighlight = async (
return tx
.withRepository(highlightRepository)
.createAndSave(highlight, userId)
.createAndSave(highlight, libraryItemId, userId)
})
await pubsub.entityCreated<HighlightEvent>(
EntityType.HIGHLIGHT,
{ ...newHighlight, pageId: newHighlight.libraryItem.id },
{ ...newHighlight, pageId: libraryItemId },
userId
)
@ -42,6 +43,7 @@ export const saveHighlight = async (
export const mergeHighlights = async (
highlightsToRemove: string[],
highlightToAdd: DeepPartial<Highlight>,
libraryItemId: string,
userId: string,
pubsub = createPubSubClient()
) => {
@ -50,18 +52,46 @@ export const mergeHighlights = async (
await highlightRepo.delete(highlightsToRemove)
return highlightRepo.createAndSave(highlightToAdd, userId)
return highlightRepo.createAndSave(highlightToAdd, libraryItemId, userId)
})
await pubsub.entityCreated<HighlightEvent>(
EntityType.HIGHLIGHT,
{ ...newHighlight, pageId: newHighlight.libraryItem.id },
{ ...newHighlight, pageId: libraryItemId },
userId
)
return newHighlight
}
export const updateHighlight = async (
highlightId: string,
highlight: DeepPartial<Highlight>,
userId: string,
pubsub = createPubSubClient()
) => {
const updatedHighlight = await authTrx(async (tx) => {
await tx.withRepository(highlightRepository).save({
...highlight,
id: highlightId,
})
return tx.withRepository(highlightRepository).findById(highlightId)
})
if (!updatedHighlight) {
throw new Error(`Highlight ${highlightId} not found`)
}
await pubsub.entityUpdated<HighlightEvent>(
EntityType.HIGHLIGHT,
{ ...highlight, id: highlightId, pageId: updatedHighlight.libraryItem.id },
userId
)
return updatedHighlight
}
export const deleteHighlightById = async (highlightId: string) => {
return authTrx(async (tx) => {
const highlightRepo = tx.withRepository(highlightRepository)
@ -70,6 +100,8 @@ export const deleteHighlightById = async (highlightId: string) => {
throw new Error(`Highlight ${highlightId} not found`)
}
return highlightRepo.remove(highlight)
await highlightRepo.remove(highlight)
return highlight
})
}

View File

@ -57,7 +57,7 @@ export const saveLabelsInLibraryItem = async (
await authTrx(async (tx) => {
await tx
.withRepository(libraryItemRepository)
.update(libraryItemId, { labels })
.save({ id: libraryItemId, labels })
})
// create pubsub event
@ -98,7 +98,9 @@ export const saveLabelsInHighlight = async (
pubsub = createPubSubClient()
) => {
await authTrx(async (tx) => {
await tx.withRepository(highlightRepository).update(highlightId, { labels })
await tx
.withRepository(highlightRepository)
.save({ id: highlightId, labels })
})
// create pubsub event

View File

@ -242,6 +242,8 @@ export const searchLibraryItems = async (
.createQueryBuilder(LibraryItem, 'library_item')
.leftJoinAndSelect('library_item.labels', 'labels')
.leftJoinAndSelect('library_item.highlights', 'highlights')
.leftJoinAndSelect('highlights.user', 'user')
.leftJoinAndSelect('user.profile', 'profile')
.where('library_item.user_id = :userId', { userId })
// build the where clause

View File

@ -29,7 +29,7 @@ import {
import { logger } from '../utils/logger'
import { parsePreparedContent } from '../utils/parser'
import { createPageSaveRequest } from './create_page_save_request'
import { saveHighlight } from './highlights'
import { createHighlight } from './highlights'
import { findOrCreateLabels } from './labels'
import { createLibraryItem, updateLibraryItem } from './library_item'
@ -174,7 +174,7 @@ export const savePage = async (
type: HighlightType.Highlight,
}
if (!(await saveHighlight(highlight, user.id))) {
if (!(await createHighlight(highlight, clientRequestId, user.id))) {
return {
errorCodes: [SaveErrorCode.EmbeddedHighlightFailed],
message: 'Failed to save highlight',

View File

@ -10,10 +10,12 @@ import { LibraryItem, LibraryItemState } from '../entity/library_item'
import { Recommendation as RecommendationData } from '../entity/recommendation'
import { RegistrationType, User } from '../entity/user'
import {
Article,
ArticleSavingRequest,
ArticleSavingRequestStatus,
ContentReader,
CreateArticleError,
CreateArticleSuccess,
FeedArticle,
Highlight,
HighlightType,
@ -24,7 +26,6 @@ import {
SearchItem,
} from '../generated/graphql'
import { createPubSubClient } from '../pubsub'
import { CreateArticlesSuccessPartial, PartialArticle } from '../resolvers'
import { Claims, WithDataSourcesContext } from '../resolvers/types'
import { validateUrl } from '../services/create_page_save_request'
import { updateLibraryItem } from '../services/library_item'
@ -182,7 +183,7 @@ export const pageError = async (
userId: string,
pageId?: string | null,
pubsub = createPubSubClient()
): Promise<CreateArticleError | CreateArticlesSuccessPartial> => {
): Promise<CreateArticleError | CreateArticleSuccess> => {
if (!pageId) return result
await updateLibraryItem(
@ -225,9 +226,7 @@ export const libraryItemToArticleSavingRequest = (
userId: user.id,
})
export const libraryItemToPartialArticle = (
item: LibraryItem
): PartialArticle => ({
export const libraryItemToArticle = (item: LibraryItem): Article => ({
...item,
url: item.originalUrl,
state: item.state as unknown as ArticleSavingRequestStatus,
@ -239,6 +238,10 @@ export const libraryItemToPartialArticle = (
),
subscription: item.subscription?.name,
image: item.thumbnail,
contentReader: item.contentReader as unknown as ContentReader,
readingProgressAnchorIndex: item.readingProgressHighestReadAnchor,
readingProgressPercent: item.readingProgressTopPercent,
highlights: item.highlights?.map(highlightDataToHighlight) || [],
})
export const libraryItemToSearchItem = (item: LibraryItem): SearchItem => ({

View File

@ -27,7 +27,7 @@ CREATE TABLE omnivore.library_item (
archived_at timestamptz,
deleted_at timestamptz,
read_at timestamptz,
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
updated_at timestamptz,
item_language text,
word_count integer,
site_name text,

View File

@ -8,7 +8,9 @@ CREATE TABLE omnivore.entity_labels (
id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(),
library_item_id uuid REFERENCES omnivore.library_item(id) ON DELETE CASCADE,
highlight_id uuid REFERENCES omnivore.highlight(id) ON DELETE CASCADE,
label_id uuid NOT NULL REFERENCES omnivore.labels(id) ON DELETE CASCADE
label_id uuid NOT NULL REFERENCES omnivore.labels(id) ON DELETE CASCADE,
unique(library_item_id, label_id),
unique(highlight_id, label_id)
);
GRANT SELECT, INSERT, DELETE ON omnivore.entity_labels TO omnivore_user;
@ -36,7 +38,7 @@ BEGIN
)
-- Update label_names on library_item
UPDATE omnivore.library_item li
SET label_names = l.names_agg
SET label_names = coalesce(l.names_agg, array[]::text[])
FROM labels_agg l
WHERE li.id = current_library_item_id;
ELSIF current_highlight_id IS NOT NULL THEN
@ -51,7 +53,7 @@ BEGIN
)
-- Update highlight_labels on library_item
UPDATE omnivore.library_item li
SET highlight_labels = l.names_agg
SET highlight_labels = coalesce(l.names_agg, array[]::text[])
FROM labels_agg l
WHERE li.id = current_library_item_id;
END IF;

View File

@ -0,0 +1,17 @@
-- Type: DO
-- Name: alter_labels_table
-- Description: Alter labels table
BEGIN;
ALTER TABLE omnivore.labels ADD COLUMN updated_at timestamptz;
CREATE TRIGGER update_labels_modtime BEFORE UPDATE ON omnivore.labels
FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
ALTER TABLE omnivore.abuse_report DROP COLUMN page_id;
ALTER TABLE omnivore.abuse_report RENAME COLUMN elastic_page_id TO library_item_id;
ALTER TABLE omnivore.content_display_report DROP COLUMN page_id;
ALTER TABLE omnivore.content_display_report RENAME COLUMN elastic_page_id TO library_item_id;
COMMIT;

View File

@ -1,23 +0,0 @@
-- Type: DO
-- Name: library_item_preview
-- Description: Create library_item_preview table
BEGIN;
CREATE TABLE omnivore.library_item_preview (
id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(),
sender_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE,
recipient_ids uuid[] NOT NULL, -- Array of user ids
library_item_id uuid NOT NULL REFERENCES omnivore.library_item(id) ON DELETE CASCADE,
thumbnail text,
includes_note bool NOT NULL DEFAULT false,
includes_highlight bool NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT current_timestamp,
updated_at timestamptz NOT NULL DEFAULT current_timestamp
);
GRANT SELECT, INSERT ON omnivore.library_item_preview TO omnivore_user;
CREATE TRIGGER update_library_item_preview_modtime BEFORE UPDATE ON omnivore.library_item_preview FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
COMMIT;

View File

@ -0,0 +1,16 @@
-- Type: UNDO
-- Name: alter_labels_table
-- Description: Alter labels table
BEGIN;
ALTER TABLE omnivore.abuse_report RENAME COLUMN library_item_id TO elastic_page_id;
ALTER TABLE omnivore.abuse_report ADD COLUMN page_id text;
ALTER TABLE omnivore.content_display_report RENAME COLUMN library_item_id TO elastic_page_id;
ALTER TABLE omnivore.content_display_report ADD COLUMN page_id text;
DROP TRIGGER update_labels_modtime ON omnivore.labels;
ALTER TABLE omnivore.labels DROP COLUMN updated_at;
COMMIT;

View File

@ -1,11 +0,0 @@
-- Type: UNDO
-- Name: library_item_preview
-- Description: Create library_item_preview table
BEGIN;
DROP TRIGGER update_library_item_preview_modtime ON omnivore.library_item_preview;
DROP TABLE omnivore.library_item_preview;
COMMIT;

View File

@ -51,7 +51,7 @@ BEGIN
WHERE library_item_id = current_library_item_id
)
UPDATE omnivore.library_item li
SET highlight_annotations = h.annotation_agg
SET highlight_annotations = coalesce(h.annotation_agg, array[]::text[])
FROM highlight_agg h
WHERE li.id = current_library_item_id;

View File

@ -61,9 +61,6 @@ CREATE POLICY search_history_policy on omnivore.search_history
WITH CHECK (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, DELETE ON omnivore.search_history TO omnivore_user;
ALTER TABLE omnivore.abuse_report DROP COLUMN page_id;
ALTER TABLE omnivore.abuse_report RENAME COLUMN elastic_page_id TO library_item_id;
ALTER TABLE omnivore.content_display_report DROP COLUMN page_id;
ALTER TABLE omnivore.content_display_report RENAME COLUMN elastic_page_id TO library_item_id;
COMMIT;

View File

@ -32,9 +32,4 @@ DROP POLICY user_device_tokens_policy on omnivore.user_device_tokens;
ALTER TABLE omnivore.search_history DISABLE ROW LEVEL SECURITY;
DROP POLICY search_history_policy on omnivore.search_history;
ALTER TABLE omnivore.abuse_report RENAME COLUMN library_item_id TO elastic_page_id;
ALTER TABLE omnivore.abuse_report ADD COLUMN page_id text;
ALTER TABLE omnivore.content_display_report RENAME COLUMN library_item_id TO elastic_page_id;
ALTER TABLE omnivore.content_display_report ADD COLUMN page_id text;
COMMIT;

View File

@ -10725,9 +10725,9 @@ camelcase@^6.0.0, camelcase@^6.2.0:
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001251, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001332:
version "1.0.30001462"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001462.tgz"
integrity sha512-PDd20WuOBPiasZ7KbFnmQRyuLE7cFXW2PVd7dmALzbkUXEP46upAuCDm9eY9vho8fgNMGmbAX92QBZHzcnWIqw==
version "1.0.30001527"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001527.tgz"
integrity sha512-YkJi7RwPgWtXVSgK4lG9AHH57nSzvvOp9MesgXmw4Q7n0C3H04L0foHqfxcmSAm5AcWb8dW9AYj2tR7/5GnddQ==
capital-case@^1.0.4:
version "1.0.4"