feat: make newsletters and favorites internal labels
This commit is contained in:
@ -254,6 +254,8 @@ export const updateLabel = async (
|
||||
},
|
||||
refresh: ctx.refresh,
|
||||
conflicts: 'proceed', // ignore conflicts
|
||||
requests_per_second: 500, // throttle the requests
|
||||
slices: 'auto', // parallelize the requests
|
||||
})
|
||||
|
||||
body.updated > 0 &&
|
||||
|
||||
@ -31,4 +31,7 @@ export class Label {
|
||||
|
||||
@Column('integer', { default: 0 })
|
||||
position!: number
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
internal!: boolean
|
||||
}
|
||||
|
||||
@ -586,6 +586,7 @@ export type DeleteLabelError = {
|
||||
|
||||
export enum DeleteLabelErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Forbidden = 'FORBIDDEN',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
@ -1021,6 +1022,7 @@ export type Label = {
|
||||
createdAt?: Maybe<Scalars['Date']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
internal: Scalars['Boolean'];
|
||||
name: Scalars['String'];
|
||||
position?: Maybe<Scalars['Int']>;
|
||||
};
|
||||
@ -4874,6 +4876,7 @@ export type LabelResolvers<ContextType = ResolverContext, ParentType extends Res
|
||||
createdAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
internal?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
position?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
||||
@ -518,6 +518,7 @@ type DeleteLabelError {
|
||||
|
||||
enum DeleteLabelErrorCode {
|
||||
BAD_REQUEST
|
||||
FORBIDDEN
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
@ -908,6 +909,7 @@ type Label {
|
||||
createdAt: Date
|
||||
description: String
|
||||
id: ID!
|
||||
internal: Boolean!
|
||||
name: String!
|
||||
position: Int
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Between, ILike } from 'typeorm'
|
||||
import { Between } from 'typeorm'
|
||||
import { createPubSubClient } from '../../datalayer/pubsub'
|
||||
import { getHighlightById } from '../../elastic/highlights'
|
||||
import {
|
||||
@ -39,9 +39,13 @@ import {
|
||||
UpdateLabelSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { AppDataSource } from '../../server'
|
||||
import { getLabelsByIds } from '../../services/labels'
|
||||
import {
|
||||
createLabel,
|
||||
getLabelByName,
|
||||
getLabelsByIds,
|
||||
} from '../../services/labels'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { authorized, generateRandomColor } from '../../utils/helpers'
|
||||
import { authorized } from '../../utils/helpers'
|
||||
|
||||
export const labelsResolver = authorized<LabelsSuccess, LabelsError>(
|
||||
async (_obj, _params, { claims: { uid }, log }) => {
|
||||
@ -90,8 +94,6 @@ export const createLabelResolver = authorized<
|
||||
>(async (_, { input }, { claims: { uid }, log }) => {
|
||||
log.info('createLabelResolver')
|
||||
|
||||
const { name, color, description } = input
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
@ -101,30 +103,20 @@ export const createLabelResolver = authorized<
|
||||
}
|
||||
|
||||
// Check if label already exists ignoring case of name
|
||||
const existingLabel = await getRepository(Label).findOneBy({
|
||||
user: { id: user.id },
|
||||
name: ILike(name),
|
||||
})
|
||||
const existingLabel = await getLabelByName(uid, input.name)
|
||||
if (existingLabel) {
|
||||
return {
|
||||
errorCodes: [CreateLabelErrorCode.LabelAlreadyExists],
|
||||
}
|
||||
}
|
||||
|
||||
const label = await getRepository(Label).save({
|
||||
user,
|
||||
name,
|
||||
color: color || generateRandomColor(),
|
||||
description: description || '',
|
||||
})
|
||||
const label = await createLabel(uid, input)
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'label_created',
|
||||
properties: {
|
||||
name,
|
||||
color,
|
||||
description,
|
||||
...input,
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
@ -156,7 +148,7 @@ export const deleteLabelResolver = authorized<
|
||||
}
|
||||
|
||||
const label = await getRepository(Label).findOne({
|
||||
where: { id: labelId },
|
||||
where: { id: labelId, user: { id: uid } },
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!label) {
|
||||
@ -165,9 +157,11 @@ export const deleteLabelResolver = authorized<
|
||||
}
|
||||
}
|
||||
|
||||
if (label.user.id !== uid) {
|
||||
// internal labels cannot be deleted
|
||||
if (label.internal) {
|
||||
log.info('internal labels cannot be deleted')
|
||||
return {
|
||||
errorCodes: [DeleteLabelErrorCode.Unauthorized],
|
||||
errorCodes: [DeleteLabelErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,6 +304,15 @@ export const updateLabelResolver = authorized<
|
||||
}
|
||||
}
|
||||
|
||||
// internal labels cannot be updated
|
||||
if (label.internal && label.name.toLowerCase() !== name.toLowerCase()) {
|
||||
log.info('internal labels cannot be updated')
|
||||
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.Forbidden],
|
||||
}
|
||||
}
|
||||
|
||||
log.info('Updating a label', {
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
|
||||
@ -1418,6 +1418,7 @@ const schema = gql`
|
||||
description: String
|
||||
createdAt: Date
|
||||
position: Int
|
||||
internal: Boolean!
|
||||
}
|
||||
|
||||
type LabelsSuccess {
|
||||
@ -1467,6 +1468,7 @@ const schema = gql`
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
FORBIDDEN
|
||||
}
|
||||
|
||||
type DeleteLabelError {
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { User } from '../entity/user'
|
||||
import { Group } from '../entity/groups/group'
|
||||
import { Invite } from '../entity/groups/invite'
|
||||
import { GroupMembership } from '../entity/groups/group_membership'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { AppDataSource } from '../server'
|
||||
import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql'
|
||||
import { Group } from '../entity/groups/group'
|
||||
import { GroupMembership } from '../entity/groups/group_membership'
|
||||
import { Invite } from '../entity/groups/invite'
|
||||
import { RuleActionType } from '../entity/rule'
|
||||
import { User } from '../entity/user'
|
||||
import { getRepository } from '../entity/utils'
|
||||
import { homePageURL } from '../env'
|
||||
import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql'
|
||||
import { AppDataSource } from '../server'
|
||||
import { userDataToUser } from '../utils/helpers'
|
||||
import { createLabel } from './labels'
|
||||
import { createLabel, getLabelByName } from './labels'
|
||||
import { createRule } from './rules'
|
||||
import { RuleActionType } from '../entity/rule'
|
||||
|
||||
export const createGroup = async (input: {
|
||||
admin: User
|
||||
@ -228,8 +228,11 @@ export const createLabelAndRuleForGroup = async (
|
||||
userId: string,
|
||||
groupName: string
|
||||
) => {
|
||||
// create a new label for the group
|
||||
const label = await createLabel(userId, { name: groupName })
|
||||
let label = await getLabelByName(userId, groupName)
|
||||
if (!label) {
|
||||
// create a new label for the group
|
||||
label = await createLabel(userId, { name: groupName })
|
||||
}
|
||||
|
||||
// create a rule to add the label to all pages in the group
|
||||
const addLabelPromise = createRule(userId, {
|
||||
|
||||
@ -9,6 +9,12 @@ import { getRepository } from '../entity/utils'
|
||||
import { CreateLabelInput } from '../generated/graphql'
|
||||
import { generateRandomColor } from '../utils/helpers'
|
||||
|
||||
const INTERNAL_LABELS_IN_LOWERCASE = ['newsletters', 'favorites']
|
||||
|
||||
const isLabelInternal = (name: string): boolean => {
|
||||
return INTERNAL_LABELS_IN_LOWERCASE.includes(name.toLowerCase())
|
||||
}
|
||||
|
||||
const batchGetLabelsFromLinkIds = async (
|
||||
linkIds: readonly string[]
|
||||
): Promise<Label[][]> => {
|
||||
@ -40,19 +46,12 @@ export const addLabelToPage = async (
|
||||
return false
|
||||
}
|
||||
|
||||
let labelEntity = await getRepository(Label)
|
||||
.createQueryBuilder()
|
||||
.where({ user: { id: user.id } })
|
||||
.andWhere('LOWER(name) = LOWER(:name)', { name: label.name })
|
||||
.getOne()
|
||||
let labelEntity = await getLabelByName(user.id, label.name)
|
||||
|
||||
if (!labelEntity) {
|
||||
console.log('creating new label', label.name)
|
||||
|
||||
labelEntity = await getRepository(Label).save({
|
||||
...label,
|
||||
user,
|
||||
})
|
||||
labelEntity = await createLabel(user.id, label)
|
||||
}
|
||||
|
||||
console.log('adding label to page', label.name, pageId)
|
||||
@ -80,30 +79,31 @@ export const getLabelsByIds = async (
|
||||
})
|
||||
}
|
||||
|
||||
export const getLabelByName = async (
|
||||
userId: string,
|
||||
name: string
|
||||
): Promise<Label | null> => {
|
||||
return getRepository(Label)
|
||||
.createQueryBuilder()
|
||||
.where({ user: { id: userId } })
|
||||
.andWhere('LOWER(name) = LOWER(:name)', { name })
|
||||
.getOne()
|
||||
}
|
||||
|
||||
export const createLabel = async (
|
||||
userId: string,
|
||||
label: {
|
||||
name: string
|
||||
color?: string
|
||||
description?: string
|
||||
color?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
): Promise<Label> => {
|
||||
const existingLabel = await getRepository(Label)
|
||||
.createQueryBuilder()
|
||||
.where({ user: { id: userId } })
|
||||
.andWhere('LOWER(name) = LOWER(:name)', { name: label.name })
|
||||
.getOne()
|
||||
|
||||
if (existingLabel) {
|
||||
return existingLabel
|
||||
}
|
||||
|
||||
// create a new label and assign a random color if not provided
|
||||
label.color = label.color || generateRandomColor()
|
||||
|
||||
return getRepository(Label).save({
|
||||
...label,
|
||||
user: { id: userId },
|
||||
name: label.name,
|
||||
color: label.color || generateRandomColor(), // assign a random color if not provided
|
||||
description: label.description,
|
||||
internal: isLabelInternal(label.name),
|
||||
})
|
||||
}
|
||||
|
||||
@ -141,6 +141,7 @@ export const createLabels = async (
|
||||
name: l.name,
|
||||
description: l.description,
|
||||
color: l.color || generateRandomColor(),
|
||||
internal: isLabelInternal(l.name),
|
||||
user,
|
||||
}))
|
||||
)
|
||||
|
||||
11
packages/db/migrations/0113.do.add_internal_field_in_labels.sql
Executable file
11
packages/db/migrations/0113.do.add_internal_field_in_labels.sql
Executable file
@ -0,0 +1,11 @@
|
||||
-- Type: DO
|
||||
-- Name: add_internal_field_in_labels
|
||||
-- Description: Add a new boolean field internal in labels table and set it to true for newsletters and favorites
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.labels ADD COLUMN internal boolean NOT NULL DEFAULT false;
|
||||
|
||||
UPDATE omnivore.labels SET internal = true WHERE LOWER(name) = 'newsletters' OR LOWER(name) = 'favorites';
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0113.undo.add_internal_field_in_labels.sql
Executable file
9
packages/db/migrations/0113.undo.add_internal_field_in_labels.sql
Executable file
@ -0,0 +1,9 @@
|
||||
-- Type: UNDO
|
||||
-- Name: add_internal_field_in_labels
|
||||
-- Description: Add a new boolean field internal in labels table
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.labels DROP COLUMN IF EXISTS internal;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user