Merge pull request #3246 from omnivore-app/feature/label-source

feature/label source
This commit is contained in:
Hongbo Wu 2023-12-14 10:51:18 +08:00 committed by GitHub
commit 6d0dcee488
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 136 additions and 60 deletions

View file

@ -1,4 +1,21 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm'
import { Label } from './label'
// for labels created by rules, we use the rule name as the source, for example: 'rule:my-rule'
// for labels created by users, we use 'user'
// for labels created by system, we use 'system'
type RuleSourceType = `rule:${string}`
export type LabelSource = 'user' | 'system' | RuleSourceType
export const isLabelSource = (source: string): source is LabelSource => {
return ['user', 'system'].indexOf(source) !== -1 || source.startsWith('rule:')
}
@Entity({ name: 'entity_labels' })
export class EntityLabel {
@ -8,9 +25,16 @@ export class EntityLabel {
@Column('uuid')
labelId!: string
@ManyToOne(() => Label)
@JoinColumn({ name: 'label_id' })
label!: Label
@Column('uuid')
libraryItemId?: string | null
@Column('uuid')
highlightId?: string | null
@Column('text', { default: 'user' })
source!: LabelSource
}

View file

@ -1091,6 +1091,7 @@ export type Label = {
internal?: Maybe<Scalars['Boolean']>;
name: Scalars['String'];
position?: Maybe<Scalars['Int']>;
source?: Maybe<Scalars['String']>;
};
export type LabelsError = {
@ -2539,6 +2540,7 @@ export type SetLabelsInput = {
labelIds?: InputMaybe<Array<Scalars['ID']>>;
labels?: InputMaybe<Array<CreateLabelInput>>;
pageId: Scalars['ID'];
source?: InputMaybe<Scalars['String']>;
};
export type SetLabelsResult = SetLabelsError | SetLabelsSuccess;
@ -5180,6 +5182,7 @@ export type LabelResolvers<ContextType = ResolverContext, ParentType extends Res
internal?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
position?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
source?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

View file

@ -975,6 +975,7 @@ type Label {
internal: Boolean
name: String!
position: Int
source: String
}
type LabelsError {
@ -1971,6 +1972,7 @@ input SetLabelsInput {
labelIds: [ID!]
labels: [CreateLabelInput!]
pageId: ID!
source: String
}
union SetLabelsResult = SetLabelsError | SetLabelsSuccess

View file

@ -359,8 +359,7 @@ export const createArticleResolver = authorized<
libraryItemToReturn.id,
uid,
inputLabels,
rssFeedUrl,
pubsub
rssFeedUrl
)
log.info(

View file

@ -1,4 +1,5 @@
import { Between } from 'typeorm'
import { isLabelSource, LabelSource } from '../../entity/entity_label'
import { Label } from '../../entity/label'
import { env } from '../../env'
import {
@ -160,7 +161,7 @@ export const setLabelsResolver = authorized<
>(
async (
_,
{ input: { pageId, labelIds, labels } },
{ input: { pageId, labelIds, labels, source } },
{ uid, log, authTrx, pubsub }
) => {
if (!labelIds && !labels) {
@ -170,6 +171,21 @@ export const setLabelsResolver = authorized<
}
}
let labelSource: LabelSource | undefined
// check if source is valid
if (source) {
if (!isLabelSource(source)) {
log.error('invalid source', source)
return {
errorCodes: [SetLabelsErrorCode.BadRequest],
}
}
labelSource = source
}
try {
let labelsSet: Label[] = []
@ -191,7 +207,7 @@ export const setLabelsResolver = authorized<
}
// save labels in the library item
await saveLabelsInLibraryItem(labelsSet, pageId, uid, pubsub)
await saveLabelsInLibraryItem(labelsSet, pageId, uid, labelSource, pubsub)
analytics.track({
userId: uid,

View file

@ -71,9 +71,7 @@ export function followingServiceRouter() {
result.identifiers[0].id,
userId,
[{ name: 'RSS' }],
undefined,
undefined,
true
req.body.addedToFollowingBy
)
logger.info('RSS label added to the item')

View file

@ -1446,6 +1446,7 @@ const schema = gql`
createdAt: Date
position: Int
internal: Boolean
source: String
}
type LabelsSuccess {
@ -1532,6 +1533,7 @@ const schema = gql`
pageId: ID!
labelIds: [ID!]
labels: [CreateLabelInput!]
source: String
}
union SetLabelsResult = SetLabelsSuccess | SetLabelsError

View file

@ -1,6 +1,6 @@
import { DeepPartial, FindOptionsWhere, In } from 'typeorm'
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
import { EntityLabel } from '../entity/entity_label'
import { EntityLabel, LabelSource } from '../entity/entity_label'
import { Label } from '../entity/label'
import { LibraryItem } from '../entity/library_item'
import { createPubSubClient, EntityType, PubsubClient } from '../pubsub'
@ -11,6 +11,7 @@ import { libraryItemRepository } from '../repository/library_item'
type AddLabelsToLibraryItemEvent = {
pageId: string
labels: DeepPartial<Label>[]
source?: LabelSource
}
type AddLabelsToHighlightEvent = {
highlightId: string
@ -70,12 +71,13 @@ export const createAndSaveLabelsInLibraryItem = async (
userId: string,
labels?: CreateLabelInput[] | null,
rssFeedUrl?: string | null,
pubsub?: PubsubClient,
skipPubSub?: boolean
source?: LabelSource,
pubsub?: PubsubClient
) => {
if (rssFeedUrl) {
// add rss label to labels
labels = (labels || []).concat({ name: 'RSS' })
source = 'system'
}
// save labels in item
@ -86,8 +88,8 @@ export const createAndSaveLabelsInLibraryItem = async (
newLabels,
libraryItemId,
userId,
pubsub,
skipPubSub
source,
pubsub
)
}
}
@ -96,8 +98,8 @@ export const saveLabelsInLibraryItem = async (
labels: Label[],
libraryItemId: string,
userId: string,
pubsub = createPubSubClient(),
skipPubSub = false
source: LabelSource = 'user',
pubsub = createPubSubClient()
) => {
await authTrx(
async (tx) => {
@ -113,6 +115,7 @@ export const saveLabelsInLibraryItem = async (
labels.map((l) => ({
labelId: l.id,
libraryItemId,
source,
}))
)
},
@ -120,24 +123,22 @@ export const saveLabelsInLibraryItem = async (
userId
)
if (skipPubSub) {
return
if (source === 'user') {
// create pubsub event
await pubsub.entityCreated<AddLabelsToLibraryItemEvent>(
EntityType.LABEL,
{ pageId: libraryItemId, labels, source },
userId
)
}
// create pubsub event
await pubsub.entityCreated<AddLabelsToLibraryItemEvent>(
EntityType.LABEL,
{ pageId: libraryItemId, labels },
userId
)
}
export const addLabelsToLibraryItem = async (
labels: Label[],
libraryItemId: string,
userId: string,
pubsub = createPubSubClient(),
skipPubSub = false
source: LabelSource = 'user',
pubsub = createPubSubClient()
) => {
await authTrx(
async (tx) => {
@ -154,6 +155,7 @@ export const addLabelsToLibraryItem = async (
labels.map((l) => ({
labelId: l.id,
libraryItemId,
source,
}))
)
},
@ -161,16 +163,14 @@ export const addLabelsToLibraryItem = async (
userId
)
if (skipPubSub) {
return
if (source === 'user') {
// create pubsub event
await pubsub.entityCreated<AddLabelsToLibraryItemEvent>(
EntityType.LABEL,
{ pageId: libraryItemId, labels, source },
userId
)
}
// create pubsub event
await pubsub.entityCreated<AddLabelsToLibraryItemEvent>(
EntityType.LABEL,
{ pageId: libraryItemId, labels },
userId
)
}
export const saveLabelsInHighlight = async (
@ -287,23 +287,19 @@ export const findLabelById = async (id: string, userId: string) => {
export const findLabelsByLibraryItemId = async (
libraryItemId: string,
userId: string
) => {
): Promise<(Label & { source: string })[]> => {
return authTrx(
async (tx) =>
tx
.createQueryBuilder(Label, 'label')
.innerJoin(
EntityLabel,
'entityLabel',
'entityLabel.label_id = label.id'
)
.innerJoin(
LibraryItem,
'LibraryItem',
'LibraryItem.id = entityLabel.library_item_id'
)
.where('LibraryItem.id = :libraryItemId', { libraryItemId })
.getMany(),
async (tx) => {
const entityLabels = await tx.getRepository(EntityLabel).find({
where: { libraryItemId },
relations: ['label'],
})
return entityLabels.map((el) => ({
...el.label,
source: el.source,
}))
},
undefined,
userId
)

View file

@ -121,9 +121,13 @@ export const saveEmail = async (
}
// save newsletter label in the item
await createAndSaveLabelsInLibraryItem(newLibraryItem.id, input.userId, [
{ name: 'Newsletter' },
])
await createAndSaveLabelsInLibraryItem(
newLibraryItem.id,
input.userId,
[{ name: 'Newsletter' }],
undefined,
'system'
)
await updateReceivedEmail(input.receivedEmailId, 'article', input.userId)

View file

@ -13,6 +13,7 @@ import {
createLabel,
deleteLabels,
findLabelById,
findLabelsByLibraryItemId,
findLabelsByUserId,
saveLabelsInHighlight,
} from '../../src/services/labels'
@ -320,6 +321,7 @@ describe('Labels API', () => {
let labelIds: string[] = []
let labels: Label[]
let item: LibraryItem
let source: string
before(async () => {
// create testing labels
@ -327,6 +329,7 @@ describe('Labels API', () => {
const label2 = await createLabel('label_2', '#eeeeee', user.id)
labels = [label1, label2]
item = await createTestLibraryItem(user.id)
source = 'user'
})
after(async () => {
@ -347,7 +350,8 @@ describe('Labels API', () => {
labelIds: [
"${labelIds[0]}",
"${labelIds[1]}"
]
],
source: "${source}"
}
) {
... on SetLabelsSuccess {
@ -368,12 +372,14 @@ describe('Labels API', () => {
before(() => {
itemId = item.id
labelIds = [labels[0].id, labels[1].id]
source = 'rule:my-rule'
})
it('should set labels', async () => {
it('sets labels', async () => {
await graphqlRequest(query, authToken).expect(200)
const page = await findLibraryItemById(itemId, user.id)
expect(page?.labels?.map((l) => l.id)).to.eql(labelIds)
const labels = await findLabelsByLibraryItemId(itemId, user.id)
expect(labels.map((l) => l.id)).to.eql(labelIds)
expect(labels[0].source).to.eql(source)
})
})

View file

@ -0,0 +1,9 @@
-- Type: DO
-- Name: add_source_to_entity_labels
-- Description: Add source column to omnivore.entity_labels table
BEGIN;
ALTER TABLE omnivore.entity_labels ADD COLUMN source TEXT NOT NULL DEFAULT 'user';
COMMIT;

View file

@ -0,0 +1,9 @@
-- Type: UNDO
-- Name: add_source_to_entity_labels
-- Description: Add source column to omnivore.entity_labels table
BEGIN;
ALTER TABLE omnivore.entity_labels DROP COLUMN source;
COMMIT;

View file

@ -4,7 +4,8 @@ export const setLabels = async (
apiEndpoint: string,
auth: string,
pageId: string,
labelIds: string[]
labelIds: string[],
ruleName: string
) => {
const data = JSON.stringify({
query: `mutation SetLabels($input: SetLabelsInput!) {
@ -23,6 +24,7 @@ export const setLabels = async (
input: {
pageId,
labelIds,
source: `rule:${ruleName}`,
},
},
})

View file

@ -120,7 +120,13 @@ export const triggerActions = async (
return (
labelIdsToSet.length > existingLabelIds.length &&
actionPromises.push(
setLabels(apiEndpoint, authToken, data.id, labelIdsToSet)
setLabels(
apiEndpoint,
authToken,
data.id,
labelIdsToSet,
rule.name
)
)
)
}