diff --git a/packages/api/src/datalayer/links/index.ts b/packages/api/src/datalayer/links/index.ts index 10a8d991e..48885a347 100644 --- a/packages/api/src/datalayer/links/index.ts +++ b/packages/api/src/datalayer/links/index.ts @@ -36,6 +36,7 @@ type UserArticleStats = { } const LINK_COLS = [ + 'omnivore.links.id as linkId', 'omnivore.links.userId', 'omnivore.links.slug', 'omnivore.links.article_url as url', @@ -403,6 +404,7 @@ class UserArticleModel extends DataModel< inFilter: InFilter readFilter: ReadFilter typeFilter: PageType | undefined + labelFilters?: string[] }, userId: string, tx = this.kx, @@ -441,6 +443,15 @@ class UserArticleModel extends DataModel< } } + // search by labels using lowercase + if (args.labelFilters) { + queryPromise + .innerJoin(Table.LINK_LABELS, 'link_labels.link_id', 'links.id') + .innerJoin(Table.LABELS, 'labels.id', 'link_labels.label_id') + .whereRaw('LOWER(omnivore.labels.name) = ANY(?)', [args.labelFilters]) + .distinct('links.id') + } + if (notNullField) { queryPromise.whereNotNull(notNullField) } @@ -469,12 +480,11 @@ class UserArticleModel extends DataModel< .orderBy('omnivore.links.id', sortOrder) .limit(limit) - // console.log('query', queryPromise.toString()) const rows = await queryPromise - for (const row of rows) { this.loader.prime(row.id, row) } + return [rows, parseInt(totalCount as string)] } diff --git a/packages/api/src/entity/label.ts b/packages/api/src/entity/label.ts index b7081e0a7..0b36eddec 100644 --- a/packages/api/src/entity/label.ts +++ b/packages/api/src/entity/label.ts @@ -8,7 +8,6 @@ import { PrimaryGeneratedColumn, } from 'typeorm' import { User } from './user' -import { Link } from './link' @Entity({ name: 'labels' }) export class Label extends BaseEntity { @@ -22,9 +21,11 @@ export class Label extends BaseEntity { @JoinColumn({ name: 'user_id' }) user!: User - @ManyToOne(() => Link) - @JoinColumn({ name: 'link_id' }) - link!: Link + @Column('text') + color!: string + + @Column('text', { nullable: true }) + description?: string @CreateDateColumn() createdAt!: Date diff --git a/packages/api/src/entity/link.ts b/packages/api/src/entity/link.ts index cc3550cb1..40c0f208a 100644 --- a/packages/api/src/entity/link.ts +++ b/packages/api/src/entity/link.ts @@ -15,7 +15,8 @@ import { CreateDateColumn, Entity, JoinColumn, - OneToMany, + JoinTable, + ManyToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, @@ -62,6 +63,11 @@ export class Link extends BaseEntity { @UpdateDateColumn() updatedAt?: Date - @OneToMany(() => Label, (label) => label.link) + @ManyToMany(() => Label) + @JoinTable({ + name: 'link_labels', + joinColumn: { name: 'link_id' }, + inverseJoinColumn: { name: 'label_id' }, + }) labels?: Label[] } diff --git a/packages/api/src/entity/link_label.ts b/packages/api/src/entity/link_label.ts new file mode 100644 index 000000000..5bcdd64d6 --- /dev/null +++ b/packages/api/src/entity/link_label.ts @@ -0,0 +1,27 @@ +import { + BaseEntity, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm' +import { Link } from './link' +import { Label } from './label' + +@Entity({ name: 'link_labels' }) +export class LinkLabel extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string + + @ManyToOne(() => Link) + @JoinColumn({ name: 'link_id' }) + link!: Link + + @ManyToOne(() => Label) + @JoinColumn({ name: 'label_id' }) + label!: Label + + @CreateDateColumn() + createdAt!: Date +} diff --git a/packages/api/src/entity/user.ts b/packages/api/src/entity/user.ts index cc6af11c3..5aa1e5d05 100644 --- a/packages/api/src/entity/user.ts +++ b/packages/api/src/entity/user.ts @@ -11,6 +11,7 @@ import { import { MembershipTier, RegistrationType } from '../datalayer/user/model' import { NewsletterEmail } from './newsletter_email' import { Profile } from './profile' +import { Label } from './label' @Entity() export class User extends BaseEntity { @@ -46,4 +47,7 @@ export class User extends BaseEntity { @Column('varchar', { length: 255, nullable: true }) password?: string + + @OneToMany(() => Label, (label) => label.user) + labels?: Label[] } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 622bf586d..35ed9737b 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -53,6 +53,8 @@ export type Article = { id: Scalars['ID']; image?: Maybe; isArchived: Scalars['Boolean']; + labels?: Maybe>; + linkId?: Maybe; originalArticleUrl?: Maybe; originalHtml?: Maybe; pageType?: Maybe; @@ -276,12 +278,14 @@ export type CreateLabelError = { export enum CreateLabelErrorCode { BadRequest = 'BAD_REQUEST', + LabelAlreadyExists = 'LABEL_ALREADY_EXISTS', NotFound = 'NOT_FOUND', Unauthorized = 'UNAUTHORIZED' } export type CreateLabelInput = { - linkId: Scalars['ID']; + color: Scalars['String']; + description?: InputMaybe; name: Scalars['String']; }; @@ -624,6 +628,9 @@ export type HighlightStats = { export type Label = { __typename?: 'Label'; + color: Scalars['String']; + createdAt: Scalars['Date']; + description?: Maybe; id: Scalars['ID']; name: Scalars['String']; }; @@ -773,6 +780,7 @@ export type Mutation = { setBookmarkArticle: SetBookmarkArticleResult; setDeviceToken: SetDeviceTokenResult; setFollow: SetFollowResult; + setLabels: SetLabelsResult; setLinkArchived: ArchiveLinkResult; setShareArticle: SetShareArticleResult; setShareHighlight: SetShareHighlightResult; @@ -914,6 +922,11 @@ export type MutationSetFollowArgs = { }; +export type MutationSetLabelsArgs = { + input: SetLabelsInput; +}; + + export type MutationSetLinkArchivedArgs = { input: ArchiveLinkInput; }; @@ -1120,11 +1133,6 @@ export type QueryGetFollowingArgs = { }; -export type QueryLabelsArgs = { - linkId: Scalars['ID']; -}; - - export type QueryReminderArgs = { linkId: Scalars['ID']; }; @@ -1350,6 +1358,29 @@ export type SetFollowSuccess = { updatedUser: User; }; +export type SetLabelsError = { + __typename?: 'SetLabelsError'; + errorCodes: Array; +}; + +export enum SetLabelsErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type SetLabelsInput = { + labelIds: Array; + linkId: Scalars['ID']; +}; + +export type SetLabelsResult = SetLabelsError | SetLabelsSuccess; + +export type SetLabelsSuccess = { + __typename?: 'SetLabelsSuccess'; + labels: Array