Merge pull request #2332 from omnivore-app/feature/favorites
feature/favorites
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?: Maybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
position?: Maybe<Scalars['Int']>;
|
||||
};
|
||||
@ -1269,6 +1271,7 @@ export type Mutation = {
|
||||
saveUrl: SaveResult;
|
||||
setBookmarkArticle: SetBookmarkArticleResult;
|
||||
setDeviceToken: SetDeviceTokenResult;
|
||||
setFavoriteArticle: SetFavoriteArticleResult;
|
||||
setFollow: SetFollowResult;
|
||||
setIntegration: SetIntegrationResult;
|
||||
setLabels: SetLabelsResult;
|
||||
@ -1514,6 +1517,11 @@ export type MutationSetDeviceTokenArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetFavoriteArticleArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetFollowArgs = {
|
||||
input: SetFollowInput;
|
||||
};
|
||||
@ -2391,6 +2399,25 @@ export type SetDeviceTokenSuccess = {
|
||||
deviceToken: DeviceToken;
|
||||
};
|
||||
|
||||
export type SetFavoriteArticleError = {
|
||||
__typename?: 'SetFavoriteArticleError';
|
||||
errorCodes: Array<SetFavoriteArticleErrorCode>;
|
||||
};
|
||||
|
||||
export enum SetFavoriteArticleErrorCode {
|
||||
AlreadyExists = 'ALREADY_EXISTS',
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type SetFavoriteArticleResult = SetFavoriteArticleError | SetFavoriteArticleSuccess;
|
||||
|
||||
export type SetFavoriteArticleSuccess = {
|
||||
__typename?: 'SetFavoriteArticleSuccess';
|
||||
favoriteArticle: Article;
|
||||
};
|
||||
|
||||
export type SetFollowError = {
|
||||
__typename?: 'SetFollowError';
|
||||
errorCodes: Array<SetFollowErrorCode>;
|
||||
@ -3598,6 +3625,10 @@ export type ResolversTypes = {
|
||||
SetDeviceTokenInput: SetDeviceTokenInput;
|
||||
SetDeviceTokenResult: ResolversTypes['SetDeviceTokenError'] | ResolversTypes['SetDeviceTokenSuccess'];
|
||||
SetDeviceTokenSuccess: ResolverTypeWrapper<SetDeviceTokenSuccess>;
|
||||
SetFavoriteArticleError: ResolverTypeWrapper<SetFavoriteArticleError>;
|
||||
SetFavoriteArticleErrorCode: SetFavoriteArticleErrorCode;
|
||||
SetFavoriteArticleResult: ResolversTypes['SetFavoriteArticleError'] | ResolversTypes['SetFavoriteArticleSuccess'];
|
||||
SetFavoriteArticleSuccess: ResolverTypeWrapper<SetFavoriteArticleSuccess>;
|
||||
SetFollowError: ResolverTypeWrapper<SetFollowError>;
|
||||
SetFollowErrorCode: SetFollowErrorCode;
|
||||
SetFollowInput: SetFollowInput;
|
||||
@ -4014,6 +4045,9 @@ export type ResolversParentTypes = {
|
||||
SetDeviceTokenInput: SetDeviceTokenInput;
|
||||
SetDeviceTokenResult: ResolversParentTypes['SetDeviceTokenError'] | ResolversParentTypes['SetDeviceTokenSuccess'];
|
||||
SetDeviceTokenSuccess: SetDeviceTokenSuccess;
|
||||
SetFavoriteArticleError: SetFavoriteArticleError;
|
||||
SetFavoriteArticleResult: ResolversParentTypes['SetFavoriteArticleError'] | ResolversParentTypes['SetFavoriteArticleSuccess'];
|
||||
SetFavoriteArticleSuccess: SetFavoriteArticleSuccess;
|
||||
SetFollowError: SetFollowError;
|
||||
SetFollowInput: SetFollowInput;
|
||||
SetFollowResult: ResolversParentTypes['SetFollowError'] | ResolversParentTypes['SetFollowSuccess'];
|
||||
@ -4874,6 +4908,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<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
position?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@ -5062,6 +5097,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
||||
saveUrl?: Resolver<ResolversTypes['SaveResult'], ParentType, ContextType, RequireFields<MutationSaveUrlArgs, 'input'>>;
|
||||
setBookmarkArticle?: Resolver<ResolversTypes['SetBookmarkArticleResult'], ParentType, ContextType, RequireFields<MutationSetBookmarkArticleArgs, 'input'>>;
|
||||
setDeviceToken?: Resolver<ResolversTypes['SetDeviceTokenResult'], ParentType, ContextType, RequireFields<MutationSetDeviceTokenArgs, 'input'>>;
|
||||
setFavoriteArticle?: Resolver<ResolversTypes['SetFavoriteArticleResult'], ParentType, ContextType, RequireFields<MutationSetFavoriteArticleArgs, 'id'>>;
|
||||
setFollow?: Resolver<ResolversTypes['SetFollowResult'], ParentType, ContextType, RequireFields<MutationSetFollowArgs, 'input'>>;
|
||||
setIntegration?: Resolver<ResolversTypes['SetIntegrationResult'], ParentType, ContextType, RequireFields<MutationSetIntegrationArgs, 'input'>>;
|
||||
setLabels?: Resolver<ResolversTypes['SetLabelsResult'], ParentType, ContextType, RequireFields<MutationSetLabelsArgs, 'input'>>;
|
||||
@ -5539,6 +5575,20 @@ export type SetDeviceTokenSuccessResolvers<ContextType = ResolverContext, Parent
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SetFavoriteArticleErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SetFavoriteArticleError'] = ResolversParentTypes['SetFavoriteArticleError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['SetFavoriteArticleErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SetFavoriteArticleResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SetFavoriteArticleResult'] = ResolversParentTypes['SetFavoriteArticleResult']> = {
|
||||
__resolveType: TypeResolveFn<'SetFavoriteArticleError' | 'SetFavoriteArticleSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SetFavoriteArticleSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SetFavoriteArticleSuccess'] = ResolversParentTypes['SetFavoriteArticleSuccess']> = {
|
||||
favoriteArticle?: Resolver<ResolversTypes['Article'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SetFollowErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SetFollowError'] = ResolversParentTypes['SetFollowError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['SetFollowErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@ -6260,6 +6310,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
||||
SetDeviceTokenError?: SetDeviceTokenErrorResolvers<ContextType>;
|
||||
SetDeviceTokenResult?: SetDeviceTokenResultResolvers<ContextType>;
|
||||
SetDeviceTokenSuccess?: SetDeviceTokenSuccessResolvers<ContextType>;
|
||||
SetFavoriteArticleError?: SetFavoriteArticleErrorResolvers<ContextType>;
|
||||
SetFavoriteArticleResult?: SetFavoriteArticleResultResolvers<ContextType>;
|
||||
SetFavoriteArticleSuccess?: SetFavoriteArticleSuccessResolvers<ContextType>;
|
||||
SetFollowError?: SetFollowErrorResolvers<ContextType>;
|
||||
SetFollowResult?: SetFollowResultResolvers<ContextType>;
|
||||
SetFollowSuccess?: SetFollowSuccessResolvers<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
|
||||
}
|
||||
@ -1137,6 +1139,7 @@ type Mutation {
|
||||
saveUrl(input: SaveUrlInput!): SaveResult!
|
||||
setBookmarkArticle(input: SetBookmarkArticleInput!): SetBookmarkArticleResult!
|
||||
setDeviceToken(input: SetDeviceTokenInput!): SetDeviceTokenResult!
|
||||
setFavoriteArticle(id: ID!): SetFavoriteArticleResult!
|
||||
setFollow(input: SetFollowInput!): SetFollowResult!
|
||||
setIntegration(input: SetIntegrationInput!): SetIntegrationResult!
|
||||
setLabels(input: SetLabelsInput!): SetLabelsResult!
|
||||
@ -1772,6 +1775,23 @@ type SetDeviceTokenSuccess {
|
||||
deviceToken: DeviceToken!
|
||||
}
|
||||
|
||||
type SetFavoriteArticleError {
|
||||
errorCodes: [SetFavoriteArticleErrorCode!]!
|
||||
}
|
||||
|
||||
enum SetFavoriteArticleErrorCode {
|
||||
ALREADY_EXISTS
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union SetFavoriteArticleResult = SetFavoriteArticleError | SetFavoriteArticleSuccess
|
||||
|
||||
type SetFavoriteArticleSuccess {
|
||||
favoriteArticle: Article!
|
||||
}
|
||||
|
||||
type SetFollowError {
|
||||
errorCodes: [SetFollowErrorCode!]!
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ import {
|
||||
MutationCreateArticleArgs,
|
||||
MutationSaveArticleReadingProgressArgs,
|
||||
MutationSetBookmarkArticleArgs,
|
||||
MutationSetFavoriteArticleArgs,
|
||||
MutationSetShareArticleArgs,
|
||||
PageInfo,
|
||||
QueryArticleArgs,
|
||||
@ -60,6 +61,9 @@ import {
|
||||
SetBookmarkArticleError,
|
||||
SetBookmarkArticleErrorCode,
|
||||
SetBookmarkArticleSuccess,
|
||||
SetFavoriteArticleError,
|
||||
SetFavoriteArticleErrorCode,
|
||||
SetFavoriteArticleSuccess,
|
||||
SetShareArticleError,
|
||||
SetShareArticleErrorCode,
|
||||
SetShareArticleSuccess,
|
||||
@ -74,7 +78,11 @@ import {
|
||||
UpdatesSinceSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { createPageSaveRequest } from '../../services/create_page_save_request'
|
||||
import { createLabels, getLabelsByIds } from '../../services/labels'
|
||||
import {
|
||||
addLabelToPage,
|
||||
createLabels,
|
||||
getLabelsByIds,
|
||||
} from '../../services/labels'
|
||||
import { parsedContentToPage } from '../../services/save_page'
|
||||
import { traceAs } from '../../tracing'
|
||||
import { Merge } from '../../util'
|
||||
@ -1152,6 +1160,70 @@ export const bulkActionResolver = authorized<
|
||||
}
|
||||
)
|
||||
|
||||
export type SetFavoriteArticleSuccessPartial = Merge<
|
||||
SetFavoriteArticleSuccess,
|
||||
{ favoriteArticle: PartialArticle }
|
||||
>
|
||||
export const setFavoriteArticleResolver = authorized<
|
||||
SetFavoriteArticleSuccessPartial,
|
||||
SetFavoriteArticleError,
|
||||
MutationSetFavoriteArticleArgs
|
||||
>(async (_, { id }, { claims: { uid }, log, pubsub }) => {
|
||||
log.info('setFavoriteArticleResolver', { id })
|
||||
|
||||
if (!uid) {
|
||||
return { errorCodes: [SetFavoriteArticleErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
try {
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'setFavoriteArticle',
|
||||
properties: {
|
||||
env: env.server.apiEnv,
|
||||
id,
|
||||
},
|
||||
})
|
||||
|
||||
const page = await getPageByParam({ userId: uid, _id: id })
|
||||
if (!page) {
|
||||
return { errorCodes: [SetFavoriteArticleErrorCode.NotFound] }
|
||||
}
|
||||
|
||||
const label = {
|
||||
id: '',
|
||||
name: 'Favorites',
|
||||
color: '#FFD700', // gold
|
||||
}
|
||||
|
||||
// adds Favorites label to page
|
||||
const result = await addLabelToPage(
|
||||
{
|
||||
uid,
|
||||
pubsub,
|
||||
},
|
||||
page.id,
|
||||
label
|
||||
)
|
||||
if (!result) {
|
||||
return { errorCodes: [SetFavoriteArticleErrorCode.AlreadyExists] }
|
||||
}
|
||||
|
||||
log.debug('Favorites label added:', result)
|
||||
|
||||
return {
|
||||
favoriteArticle: {
|
||||
...page,
|
||||
labels: page.labels ? [...page.labels, label] : [label],
|
||||
isArchived: !!page.archivedAt,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
log.info('Error adding Favorites label:', error)
|
||||
return { errorCodes: [SetFavoriteArticleErrorCode.BadRequest] }
|
||||
}
|
||||
})
|
||||
|
||||
const getUpdateReason = (page: Page, since: Date) => {
|
||||
if (page.state === ArticleSavingRequestStatus.Deleted) {
|
||||
return UpdateReason.Deleted
|
||||
|
||||
@ -87,6 +87,7 @@ import {
|
||||
sendInstallInstructionsResolver,
|
||||
setBookmarkArticleResolver,
|
||||
setDeviceTokenResolver,
|
||||
setFavoriteArticleResolver,
|
||||
setFollowResolver,
|
||||
setIntegrationResolver,
|
||||
setLabelsForHighlightResolver,
|
||||
@ -204,6 +205,7 @@ export const functionResolvers = {
|
||||
markEmailAsItem: markEmailAsItemResolver,
|
||||
bulkAction: bulkActionResolver,
|
||||
importFromIntegration: importFromIntegrationResolver,
|
||||
setFavoriteArticle: setFavoriteArticleResolver,
|
||||
},
|
||||
Query: {
|
||||
me: getMeUserResolver,
|
||||
@ -659,4 +661,5 @@ export const functionResolvers = {
|
||||
...resultResolveTypeResolver('MarkEmailAsItem'),
|
||||
...resultResolveTypeResolver('BulkAction'),
|
||||
...resultResolveTypeResolver('ImportFromIntegration'),
|
||||
...resultResolveTypeResolver('SetFavoriteArticle'),
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
@ -2452,6 +2454,25 @@ const schema = gql`
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
union SetFavoriteArticleResult =
|
||||
SetFavoriteArticleSuccess
|
||||
| SetFavoriteArticleError
|
||||
|
||||
type SetFavoriteArticleSuccess {
|
||||
favoriteArticle: Article!
|
||||
}
|
||||
|
||||
type SetFavoriteArticleError {
|
||||
errorCodes: [SetFavoriteArticleErrorCode!]!
|
||||
}
|
||||
|
||||
enum SetFavoriteArticleErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
ALREADY_EXISTS
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
@ -2549,6 +2570,7 @@ const schema = gql`
|
||||
async: Boolean # if true, return immediately and process in the background
|
||||
): BulkActionResult!
|
||||
importFromIntegration(integrationId: ID!): ImportFromIntegrationResult!
|
||||
setFavoriteArticle(id: ID!): SetFavoriteArticleResult!
|
||||
}
|
||||
|
||||
# FIXME: remove sort from feedArticles after all cached tabs are closed
|
||||
|
||||
@ -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,
|
||||
}))
|
||||
)
|
||||
|
||||
@ -1293,4 +1293,67 @@ describe('Article API', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SetFavoriteArticle API', () => {
|
||||
const setFavoriteArticleQuery = (articleId: string) => `
|
||||
mutation {
|
||||
setFavoriteArticle(id: "${articleId}") {
|
||||
... on SetFavoriteArticleSuccess {
|
||||
favoriteArticle {
|
||||
id
|
||||
labels {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
... on SetFavoriteArticleError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
let articleId = ''
|
||||
|
||||
before(async () => {
|
||||
const page: Page = {
|
||||
id: '',
|
||||
hash: '',
|
||||
userId: user.id,
|
||||
pageType: PageType.Article,
|
||||
title: 'test setFavoriteArticle',
|
||||
content: '',
|
||||
slug: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
readingProgressPercent: 0,
|
||||
readingProgressAnchorIndex: 0,
|
||||
url: 'https://test.omnivore.app/setFavoriteArticle',
|
||||
savedAt: new Date(),
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
}
|
||||
articleId = (await createPage(page, ctx))!
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Delete the page
|
||||
await deletePagesByParam({ userId: user.id }, ctx)
|
||||
})
|
||||
|
||||
it('favorites the article', async () => {
|
||||
const res = await graphqlRequest(
|
||||
setFavoriteArticleQuery(articleId),
|
||||
authToken
|
||||
).expect(200)
|
||||
console.log(res.body.data.setFavoriteArticle.favoriteArticle)
|
||||
expect(res.body.data.setFavoriteArticle.favoriteArticle.id).to.eq(
|
||||
articleId
|
||||
)
|
||||
expect(
|
||||
res.body.data.setFavoriteArticle.favoriteArticle.labels[0].name
|
||||
).to.eq('Favorites')
|
||||
|
||||
const page = await getPageById(articleId)
|
||||
expect(page?.labels?.map((l) => l.name)).to.eql(['Favorites'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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