Add Filters, and editing filters.

This commit is contained in:
Thomas Rogers
2023-08-30 19:37:24 +02:00
parent 27a5ab5042
commit d433d48365
18 changed files with 1577 additions and 58 deletions

View File

@ -23,13 +23,13 @@ export class Filter {
@Column('varchar', { length: 255 })
name!: string
@Column('varchar', { length: 255, nullable: true })
@Column('varchar', { length: 255, nullable: true, default: null })
description?: string | null
@Column('varchar', { length: 255 })
filter!: string
@Column('varchar', { length: 255 })
@Column('varchar', { length: 255, default: 'Search' })
category!: string
@Column('integer', { default: 0 })
@ -40,4 +40,10 @@ export class Filter {
@UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date
@Column('boolean', { default: false })
defaultFilter!: boolean
@Column('boolean', { default: true })
visible!: boolean
}

View File

@ -765,12 +765,14 @@ export type Filter = {
__typename?: 'Filter';
category: Scalars['String'];
createdAt: Scalars['Date'];
defaultFilter?: Maybe<Scalars['Boolean']>;
description?: Maybe<Scalars['String']>;
filter: Scalars['String'];
id: Scalars['ID'];
name: Scalars['String'];
position: Scalars['Int'];
updatedAt: Scalars['Date'];
visible?: Maybe<Scalars['Boolean']>;
};
export type FiltersError = {
@ -1289,6 +1291,7 @@ export type Mutation = {
setWebhook: SetWebhookResult;
subscribe: SubscribeResult;
unsubscribe: UnsubscribeResult;
updateFilter: UpdateFilterResult;
updateHighlight: UpdateHighlightResult;
updateHighlightReply: UpdateHighlightReplyResult;
updateLabel: UpdateLabelResult;
@ -1589,6 +1592,11 @@ export type MutationUnsubscribeArgs = {
};
export type MutationUpdateFilterArgs = {
input: UpdateFilterInput;
};
export type MutationUpdateHighlightArgs = {
input: UpdateHighlightInput;
};
@ -2248,11 +2256,11 @@ export enum SaveFilterErrorCode {
}
export type SaveFilterInput = {
category: Scalars['String'];
category?: InputMaybe<Scalars['String']>;
description?: InputMaybe<Scalars['String']>;
filter: Scalars['String'];
id?: InputMaybe<Scalars['ID']>;
name: Scalars['String'];
position?: InputMaybe<Scalars['Int']>;
};
export type SaveFilterResult = SaveFilterError | SaveFilterSuccess;
@ -2837,6 +2845,34 @@ export type UnsubscribeSuccess = {
subscription: Subscription;
};
export type UpdateFilterError = {
__typename?: 'UpdateFilterError';
errorCodes: Array<UpdateFilterErrorCode>;
};
export enum UpdateFilterErrorCode {
BadRequest = 'BAD_REQUEST',
NotFound = 'NOT_FOUND',
Unauthorized = 'UNAUTHORIZED'
}
export type UpdateFilterInput = {
category?: InputMaybe<Scalars['String']>;
description?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<Scalars['String']>;
id: Scalars['String'];
name?: InputMaybe<Scalars['String']>;
position?: InputMaybe<Scalars['Int']>;
visible?: InputMaybe<Scalars['Boolean']>;
};
export type UpdateFilterResult = UpdateFilterError | UpdateFilterSuccess;
export type UpdateFilterSuccess = {
__typename?: 'UpdateFilterSuccess';
filter: Filter;
};
export type UpdateHighlightError = {
__typename?: 'UpdateHighlightError';
errorCodes: Array<UpdateHighlightErrorCode>;
@ -3777,6 +3813,11 @@ export type ResolversTypes = {
UnsubscribeErrorCode: UnsubscribeErrorCode;
UnsubscribeResult: ResolversTypes['UnsubscribeError'] | ResolversTypes['UnsubscribeSuccess'];
UnsubscribeSuccess: ResolverTypeWrapper<UnsubscribeSuccess>;
UpdateFilterError: ResolverTypeWrapper<UpdateFilterError>;
UpdateFilterErrorCode: UpdateFilterErrorCode;
UpdateFilterInput: UpdateFilterInput;
UpdateFilterResult: ResolversTypes['UpdateFilterError'] | ResolversTypes['UpdateFilterSuccess'];
UpdateFilterSuccess: ResolverTypeWrapper<UpdateFilterSuccess>;
UpdateHighlightError: ResolverTypeWrapper<UpdateHighlightError>;
UpdateHighlightErrorCode: UpdateHighlightErrorCode;
UpdateHighlightInput: UpdateHighlightInput;
@ -4185,6 +4226,10 @@ export type ResolversParentTypes = {
UnsubscribeError: UnsubscribeError;
UnsubscribeResult: ResolversParentTypes['UnsubscribeError'] | ResolversParentTypes['UnsubscribeSuccess'];
UnsubscribeSuccess: UnsubscribeSuccess;
UpdateFilterError: UpdateFilterError;
UpdateFilterInput: UpdateFilterInput;
UpdateFilterResult: ResolversParentTypes['UpdateFilterError'] | ResolversParentTypes['UpdateFilterSuccess'];
UpdateFilterSuccess: UpdateFilterSuccess;
UpdateHighlightError: UpdateHighlightError;
UpdateHighlightInput: UpdateHighlightInput;
UpdateHighlightReplyError: UpdateHighlightReplyError;
@ -4793,12 +4838,14 @@ export type FeedArticlesSuccessResolvers<ContextType = ResolverContext, ParentTy
export type FilterResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Filter'] = ResolversParentTypes['Filter']> = {
category?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
defaultFilter?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
filter?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
position?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
visible?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -5200,6 +5247,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
setWebhook?: Resolver<ResolversTypes['SetWebhookResult'], ParentType, ContextType, RequireFields<MutationSetWebhookArgs, 'input'>>;
subscribe?: Resolver<ResolversTypes['SubscribeResult'], ParentType, ContextType, RequireFields<MutationSubscribeArgs, 'input'>>;
unsubscribe?: Resolver<ResolversTypes['UnsubscribeResult'], ParentType, ContextType, RequireFields<MutationUnsubscribeArgs, 'name'>>;
updateFilter?: Resolver<ResolversTypes['UpdateFilterResult'], ParentType, ContextType, RequireFields<MutationUpdateFilterArgs, 'input'>>;
updateHighlight?: Resolver<ResolversTypes['UpdateHighlightResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightArgs, 'input'>>;
updateHighlightReply?: Resolver<ResolversTypes['UpdateHighlightReplyResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightReplyArgs, 'input'>>;
updateLabel?: Resolver<ResolversTypes['UpdateLabelResult'], ParentType, ContextType, RequireFields<MutationUpdateLabelArgs, 'input'>>;
@ -5907,6 +5955,20 @@ export type UnsubscribeSuccessResolvers<ContextType = ResolverContext, ParentTyp
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateFilterErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFilterError'] = ResolversParentTypes['UpdateFilterError']> = {
errorCodes?: Resolver<Array<ResolversTypes['UpdateFilterErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateFilterResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFilterResult'] = ResolversParentTypes['UpdateFilterResult']> = {
__resolveType: TypeResolveFn<'UpdateFilterError' | 'UpdateFilterSuccess', ParentType, ContextType>;
};
export type UpdateFilterSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFilterSuccess'] = ResolversParentTypes['UpdateFilterSuccess']> = {
filter?: Resolver<ResolversTypes['Filter'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type UpdateHighlightErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateHighlightError'] = ResolversParentTypes['UpdateHighlightError']> = {
errorCodes?: Resolver<Array<ResolversTypes['UpdateHighlightErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -6466,6 +6528,9 @@ export type Resolvers<ContextType = ResolverContext> = {
UnsubscribeError?: UnsubscribeErrorResolvers<ContextType>;
UnsubscribeResult?: UnsubscribeResultResolvers<ContextType>;
UnsubscribeSuccess?: UnsubscribeSuccessResolvers<ContextType>;
UpdateFilterError?: UpdateFilterErrorResolvers<ContextType>;
UpdateFilterResult?: UpdateFilterResultResolvers<ContextType>;
UpdateFilterSuccess?: UpdateFilterSuccessResolvers<ContextType>;
UpdateHighlightError?: UpdateHighlightErrorResolvers<ContextType>;
UpdateHighlightReplyError?: UpdateHighlightReplyErrorResolvers<ContextType>;
UpdateHighlightReplyResult?: UpdateHighlightReplyResultResolvers<ContextType>;

View File

@ -677,12 +677,14 @@ type FeedArticlesSuccess {
type Filter {
category: String!
createdAt: Date!
defaultFilter: Boolean
description: String
filter: String!
id: ID!
name: String!
position: Int!
updatedAt: Date!
visible: Boolean
}
type FiltersError {
@ -1157,6 +1159,7 @@ type Mutation {
setWebhook(input: SetWebhookInput!): SetWebhookResult!
subscribe(input: SubscribeInput!): SubscribeResult!
unsubscribe(name: String!, subscriptionId: ID): UnsubscribeResult!
updateFilter(input: UpdateFilterInput!): UpdateFilterResult!
updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
updateHighlightReply(input: UpdateHighlightReplyInput!): UpdateHighlightReplyResult!
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
@ -1629,11 +1632,11 @@ enum SaveFilterErrorCode {
}
input SaveFilterInput {
category: String!
category: String
description: String
filter: String!
id: ID
name: String!
position: Int
}
union SaveFilterResult = SaveFilterError | SaveFilterSuccess
@ -2174,6 +2177,32 @@ type UnsubscribeSuccess {
subscription: Subscription!
}
type UpdateFilterError {
errorCodes: [UpdateFilterErrorCode!]!
}
enum UpdateFilterErrorCode {
BAD_REQUEST
NOT_FOUND
UNAUTHORIZED
}
input UpdateFilterInput {
category: String
description: String
filter: String
id: String!
name: String
position: Int
visible: Boolean
}
union UpdateFilterResult = UpdateFilterError | UpdateFilterSuccess
type UpdateFilterSuccess {
filter: Filter!
}
type UpdateHighlightError {
errorCodes: [UpdateHighlightErrorCode!]!
}

View File

@ -12,9 +12,13 @@ import {
MutationDeleteFilterArgs,
MutationMoveFilterArgs,
MutationSaveFilterArgs,
MutationUpdateFilterArgs,
SaveFilterError,
SaveFilterErrorCode,
SaveFilterSuccess,
UpdateFilterError,
UpdateFilterSuccess,
UpdateFilterErrorCode,
} from '../../generated/graphql'
import { Filter } from '../../entity/filter'
import { getRepository, setClaims } from '../../entity/utils'
@ -23,23 +27,24 @@ import { AppDataSource } from '../../server'
import { Between } from 'typeorm'
import { analytics } from '../../utils/analytics'
import { env } from '../../env'
import { isNil, mergeWith } from 'lodash'
export const saveFilterResolver = authorized<
SaveFilterSuccess,
SaveFilterError,
MutationSaveFilterArgs
>(async (_, { input }, { claims, log }) => {
>(async (_, { input }, { claims: { uid }, log }) => {
log.info('Saving filters', {
input,
labels: {
source: 'resolver',
resolver: 'saveFilterResolver',
uid: claims.uid,
uid,
},
})
try {
const user = await getRepository(User).findOneBy({ id: claims.uid })
const user = await getRepository(User).findOneBy({ id: uid })
if (!user) {
return {
errorCodes: [SaveFilterErrorCode.Unauthorized],
@ -47,9 +52,14 @@ export const saveFilterResolver = authorized<
}
const filter = await getRepository(Filter).save({
...input,
id: input.id ?? undefined,
user: { id: claims.uid },
user: { id: uid },
name: input.name,
category: 'Search',
description: '',
position: input.position ?? 0,
filter: input.filter,
defaultFilter: false,
visible: true,
})
return {
@ -61,7 +71,7 @@ export const saveFilterResolver = authorized<
labels: {
source: 'resolver',
resolver: 'saveFilterResolver',
uid: claims.uid,
uid,
},
})
@ -166,6 +176,113 @@ export const filtersResolver = authorized<FiltersSuccess, FiltersError>(
}
}
)
const updatePosition = async (
uid: string,
filter: Filter,
newPosition: number
) => {
const { position } = filter
const moveUp = newPosition < position
// move filter to the new position
const updated = await AppDataSource.transaction(async (t) => {
await setClaims(t, uid)
// update the position of the other filters
const updated = await t.getRepository(Filter).update(
{
user: { id: uid },
position: Between(
Math.min(newPosition, position),
Math.max(newPosition, position)
),
},
{
position: () => `position + ${moveUp ? 1 : -1}`,
}
)
if (!updated.affected) {
return null
}
// update the position of the filter
return t.getRepository(Filter).save({
...filter,
position: newPosition,
})
})
if (!updated) {
throw new Error('unable to update')
}
return updated
}
export const updateFilterResolver = authorized<
UpdateFilterSuccess,
UpdateFilterError,
MutationUpdateFilterArgs
>(async (_, { input }, { claims: { uid }, log }) => {
const repo = getRepository(Filter)
const { id } = input
try {
const user = await getRepository(User).findOneBy({ id: uid })
if (!user) {
return {
errorCodes: [UpdateFilterErrorCode.Unauthorized],
}
}
const filter = await getRepository(Filter).findOne({
where: { id },
relations: ['user'],
})
if (!filter) {
return {
__typename: 'UpdateFilterError',
errorCodes: [UpdateFilterErrorCode.NotFound],
}
}
if (filter.user.id !== uid) {
return {
__typename: 'UpdateFilterError',
errorCodes: [UpdateFilterErrorCode.Unauthorized],
}
}
if (input.position && filter.position != input.position) {
await updatePosition(uid, filter, input.position)
}
const updated = await repo.save({
...mergeWith({}, filter, input, (a: unknown, b: unknown) =>
isNil(b) ? a : undefined
),
})
return {
__typename: 'UpdateFilterSuccess',
filter: updated,
}
} catch (error) {
log.error('Error Updating filters', {
error,
labels: {
source: 'resolver',
resolver: 'UpdateFilterResolver',
uid,
},
})
return {
__typename: 'UpdateFilterError',
errorCodes: [UpdateFilterErrorCode.BadRequest],
}
}
})
export const moveFilterResolver = authorized<
MoveFilterSuccess,

View File

@ -41,6 +41,7 @@ import {
createReminderResolver,
deleteAccountResolver,
deleteFilterResolver,
updateFilterResolver,
deleteHighlightResolver,
deleteIntegrationResolver,
deleteLabelResolver,
@ -210,6 +211,7 @@ export const functionResolvers = {
importFromIntegration: importFromIntegrationResolver,
setFavoriteArticle: setFavoriteArticleResolver,
updateSubscription: updateSubscriptionResolver,
updateFilter: updateFilterResolver,
},
Query: {
me: getMeUserResolver,
@ -391,8 +393,7 @@ export const functionResolvers = {
return undefined
}
const filePath = generateUploadFilePathName(upload.id, upload.fileName)
const url = await generateDownloadSignedUrl(filePath)
return url
return generateDownloadSignedUrl(filePath)
}
return article.url
},

View File

@ -2165,11 +2165,11 @@ const schema = gql`
}
input SaveFilterInput {
id: ID
name: String!
filter: String!
category: String!
category: String
description: String
position: Int
}
union SaveFilterResult = SaveFilterSuccess | SaveFilterError
@ -2187,6 +2187,8 @@ const schema = gql`
description: String
createdAt: Date!
updatedAt: Date!
defaultFilter: Boolean
visible: Boolean
}
type SaveFilterError {
@ -2230,6 +2232,32 @@ const schema = gql`
NOT_FOUND
}
input UpdateFilterInput {
id: String!
name: String
filter: String
position: Int
category: String
description: String
visible: Boolean
}
enum UpdateFilterErrorCode {
UNAUTHORIZED
BAD_REQUEST
NOT_FOUND
}
union UpdateFilterResult = UpdateFilterSuccess | UpdateFilterError
type UpdateFilterSuccess {
filter: Filter!
}
type UpdateFilterError {
errorCodes: [UpdateFilterErrorCode!]!
}
input MoveFilterInput {
filterId: ID!
afterFilterId: ID # null to move to the top
@ -2616,6 +2644,7 @@ const schema = gql`
saveFilter(input: SaveFilterInput!): SaveFilterResult!
deleteFilter(id: ID!): DeleteFilterResult!
moveFilter(input: MoveFilterInput!): MoveFilterResult!
updateFilter(input: UpdateFilterInput!): UpdateFilterResult!
createGroup(input: CreateGroupInput!): CreateGroupResult!
recommend(input: RecommendInput!): RecommendResult!
joinGroup(inviteCode: String!): JoinGroupResult!

View File

@ -11,6 +11,7 @@ import { AppDataSource } from '../server'
import { logger } from '../utils/logger'
import { validateUsername } from '../utils/usernamePolicy'
import { sendConfirmationEmail } from './send_emails'
import { Filter } from '../entity/filter'
export const createUser = async (input: {
provider: AuthProvider
@ -85,6 +86,9 @@ export const createUser = async (input: {
group: invite.group,
})
}
await createDefaultFiltersForUser(t)(user.id)
return [user, profile]
}
)
@ -98,6 +102,27 @@ export const createUser = async (input: {
return [user, profile]
}
const createDefaultFiltersForUser =
(t: EntityManager) =>
async (userId: string): Promise<Filter[]> => {
const defaultFilters = [
{ name: 'Inbox', filter: 'in:inbox' },
{ name: 'Continue Reading', filter: 'in:inbox sort:read-desc is:unread' },
{ name: 'Read Later', filter: 'in:library' },
{ name: 'Highlights', filter: 'has:highlights mode:highlights' },
{ name: 'Unlabelled', filter: 'no:label' },
{ name: 'Archived', filter: 'in:archive' },
].map((it, position) => ({
...it,
user: { id: userId },
position,
defaultFilter: true,
category: 'Search',
}))
return t.getRepository(Filter).save(defaultFilters)
}
// TODO: Maybe this should be moved into a service
const validateInvite = async (
entityManager: EntityManager,