mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3246 from omnivore-app/feature/label-source
feature/label source
This commit is contained in:
commit
6d0dcee488
14 changed files with 136 additions and 60 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -359,8 +359,7 @@ export const createArticleResolver = authorized<
|
|||
libraryItemToReturn.id,
|
||||
uid,
|
||||
inputLabels,
|
||||
rssFeedUrl,
|
||||
pubsub
|
||||
rssFeedUrl
|
||||
)
|
||||
|
||||
log.info(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
9
packages/db/migrations/0148.do.add_source_to_entity_labels.sql
Executable file
9
packages/db/migrations/0148.do.add_source_to_entity_labels.sql
Executable 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;
|
||||
9
packages/db/migrations/0148.undo.add_source_to_entity_labels.sql
Executable file
9
packages/db/migrations/0148.undo.add_source_to_entity_labels.sql
Executable 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;
|
||||
|
|
@ -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}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue