Add position in filter table and move filter api

This commit is contained in:
Hongbo Wu
2022-12-01 10:26:32 +08:00
parent 0487d8325c
commit 0eebfe923a
8 changed files with 282 additions and 4 deletions

View File

@ -29,6 +29,9 @@ export class Filter {
@Column('varchar', { length: 255 })
filter!: string
@Column('integer', { default: 0 })
position!: number
@CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date

View File

@ -704,6 +704,7 @@ export type Filter = {
filter: Scalars['String'];
id: Scalars['ID'];
name: Scalars['String'];
position: Scalars['Int'];
updatedAt: Scalars['Date'];
};
@ -1014,6 +1015,29 @@ export type MergeHighlightSuccess = {
overlapHighlightIdList: Array<Scalars['String']>;
};
export type MoveFilterError = {
__typename?: 'MoveFilterError';
errorCodes: Array<MoveFilterErrorCode>;
};
export enum MoveFilterErrorCode {
BadRequest = 'BAD_REQUEST',
NotFound = 'NOT_FOUND',
Unauthorized = 'UNAUTHORIZED'
}
export type MoveFilterInput = {
afterFilterId?: InputMaybe<Scalars['ID']>;
filterId: Scalars['ID'];
};
export type MoveFilterResult = MoveFilterError | MoveFilterSuccess;
export type MoveFilterSuccess = {
__typename?: 'MoveFilterSuccess';
filter: Filter;
};
export type MoveLabelError = {
__typename?: 'MoveLabelError';
errorCodes: Array<MoveLabelErrorCode>;
@ -1064,6 +1088,7 @@ export type Mutation = {
googleSignup: GoogleSignupResult;
logOut: LogOutResult;
mergeHighlight: MergeHighlightResult;
moveFilter: MoveFilterResult;
moveLabel: MoveLabelResult;
optInFeature: OptInFeatureResult;
reportItem: ReportItemResult;
@ -1215,6 +1240,11 @@ export type MutationMergeHighlightArgs = {
};
export type MutationMoveFilterArgs = {
input: MoveFilterInput;
};
export type MutationMoveLabelArgs = {
input: MoveLabelInput;
};
@ -3023,6 +3053,11 @@ export type ResolversTypes = {
MergeHighlightInput: MergeHighlightInput;
MergeHighlightResult: ResolversTypes['MergeHighlightError'] | ResolversTypes['MergeHighlightSuccess'];
MergeHighlightSuccess: ResolverTypeWrapper<MergeHighlightSuccess>;
MoveFilterError: ResolverTypeWrapper<MoveFilterError>;
MoveFilterErrorCode: MoveFilterErrorCode;
MoveFilterInput: MoveFilterInput;
MoveFilterResult: ResolversTypes['MoveFilterError'] | ResolversTypes['MoveFilterSuccess'];
MoveFilterSuccess: ResolverTypeWrapper<MoveFilterSuccess>;
MoveLabelError: ResolverTypeWrapper<MoveLabelError>;
MoveLabelErrorCode: MoveLabelErrorCode;
MoveLabelInput: MoveLabelInput;
@ -3410,6 +3445,10 @@ export type ResolversParentTypes = {
MergeHighlightInput: MergeHighlightInput;
MergeHighlightResult: ResolversParentTypes['MergeHighlightError'] | ResolversParentTypes['MergeHighlightSuccess'];
MergeHighlightSuccess: MergeHighlightSuccess;
MoveFilterError: MoveFilterError;
MoveFilterInput: MoveFilterInput;
MoveFilterResult: ResolversParentTypes['MoveFilterError'] | ResolversParentTypes['MoveFilterSuccess'];
MoveFilterSuccess: MoveFilterSuccess;
MoveLabelError: MoveLabelError;
MoveLabelInput: MoveLabelInput;
MoveLabelResult: ResolversParentTypes['MoveLabelError'] | ResolversParentTypes['MoveLabelSuccess'];
@ -4105,6 +4144,7 @@ export type FilterResolvers<ContextType = ResolverContext, ParentType extends Re
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>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -4344,6 +4384,20 @@ export type MergeHighlightSuccessResolvers<ContextType = ResolverContext, Parent
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type MoveFilterErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['MoveFilterError'] = ResolversParentTypes['MoveFilterError']> = {
errorCodes?: Resolver<Array<ResolversTypes['MoveFilterErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type MoveFilterResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['MoveFilterResult'] = ResolversParentTypes['MoveFilterResult']> = {
__resolveType: TypeResolveFn<'MoveFilterError' | 'MoveFilterSuccess', ParentType, ContextType>;
};
export type MoveFilterSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['MoveFilterSuccess'] = ResolversParentTypes['MoveFilterSuccess']> = {
filter?: Resolver<ResolversTypes['Filter'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type MoveLabelErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['MoveLabelError'] = ResolversParentTypes['MoveLabelError']> = {
errorCodes?: Resolver<Array<ResolversTypes['MoveLabelErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -4384,6 +4438,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
googleSignup?: Resolver<ResolversTypes['GoogleSignupResult'], ParentType, ContextType, RequireFields<MutationGoogleSignupArgs, 'input'>>;
logOut?: Resolver<ResolversTypes['LogOutResult'], ParentType, ContextType>;
mergeHighlight?: Resolver<ResolversTypes['MergeHighlightResult'], ParentType, ContextType, RequireFields<MutationMergeHighlightArgs, 'input'>>;
moveFilter?: Resolver<ResolversTypes['MoveFilterResult'], ParentType, ContextType, RequireFields<MutationMoveFilterArgs, 'input'>>;
moveLabel?: Resolver<ResolversTypes['MoveLabelResult'], ParentType, ContextType, RequireFields<MutationMoveLabelArgs, 'input'>>;
optInFeature?: Resolver<ResolversTypes['OptInFeatureResult'], ParentType, ContextType, RequireFields<MutationOptInFeatureArgs, 'input'>>;
reportItem?: Resolver<ResolversTypes['ReportItemResult'], ParentType, ContextType, RequireFields<MutationReportItemArgs, 'input'>>;
@ -5388,6 +5443,9 @@ export type Resolvers<ContextType = ResolverContext> = {
MergeHighlightError?: MergeHighlightErrorResolvers<ContextType>;
MergeHighlightResult?: MergeHighlightResultResolvers<ContextType>;
MergeHighlightSuccess?: MergeHighlightSuccessResolvers<ContextType>;
MoveFilterError?: MoveFilterErrorResolvers<ContextType>;
MoveFilterResult?: MoveFilterResultResolvers<ContextType>;
MoveFilterSuccess?: MoveFilterSuccessResolvers<ContextType>;
MoveLabelError?: MoveLabelErrorResolvers<ContextType>;
MoveLabelResult?: MoveLabelResultResolvers<ContextType>;
MoveLabelSuccess?: MoveLabelSuccessResolvers<ContextType>;

View File

@ -620,6 +620,7 @@ type Filter {
filter: String!
id: ID!
name: String!
position: Int!
updatedAt: Date!
}
@ -901,6 +902,27 @@ type MergeHighlightSuccess {
overlapHighlightIdList: [String!]!
}
type MoveFilterError {
errorCodes: [MoveFilterErrorCode!]!
}
enum MoveFilterErrorCode {
BAD_REQUEST
NOT_FOUND
UNAUTHORIZED
}
input MoveFilterInput {
afterFilterId: ID
filterId: ID!
}
union MoveFilterResult = MoveFilterError | MoveFilterSuccess
type MoveFilterSuccess {
filter: Filter!
}
type MoveLabelError {
errorCodes: [MoveLabelErrorCode!]!
}
@ -948,6 +970,7 @@ type Mutation {
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
logOut: LogOutResult!
mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
moveFilter(input: MoveFilterInput!): MoveFilterResult!
moveLabel(input: MoveLabelInput!): MoveLabelResult!
optInFeature(input: OptInFeatureInput!): OptInFeatureResult!
reportItem(input: ReportItemInput!): ReportItemResult!

View File

@ -6,15 +6,23 @@ import {
FiltersError,
FiltersErrorCode,
FiltersSuccess,
MoveFilterError,
MoveFilterErrorCode,
MoveFilterSuccess,
MutationDeleteFilterArgs,
MutationMoveFilterArgs,
MutationSaveFilterArgs,
SaveFilterError,
SaveFilterErrorCode,
SaveFilterSuccess,
} from '../../generated/graphql'
import { Filter } from '../../entity/filter'
import { getRepository } from '../../entity/utils'
import { getRepository, setClaims } from '../../entity/utils'
import { User } from '../../entity/user'
import { AppDataSource } from '../../server'
import { Between } from 'typeorm'
import { analytics } from '../../utils/analytics'
import { env } from '../../env'
export const saveFilterResolver = authorized<
SaveFilterSuccess,
@ -134,8 +142,9 @@ export const filtersResolver = authorized<FiltersSuccess, FiltersError>(
}
}
const filters = await getRepository(Filter).findBy({
user: { id: claims.uid },
const filters = await getRepository(Filter).find({
where: { user: { id: claims.uid } },
order: { position: 'ASC' },
})
return {
@ -157,3 +166,132 @@ export const filtersResolver = authorized<FiltersSuccess, FiltersError>(
}
}
)
export const moveFilterResolver = authorized<
MoveFilterSuccess,
MoveFilterError,
MutationMoveFilterArgs
>(async (_, { input }, { claims: { uid }, log }) => {
log.info('Moving filters', {
input,
filters: {
source: 'resolver',
resolver: 'moveFilterResolver',
uid,
},
})
const { filterId, afterFilterId } = input
try {
const user = await getRepository(User).findOneBy({ id: uid })
if (!user) {
return {
errorCodes: [MoveFilterErrorCode.Unauthorized],
}
}
const filter = await getRepository(Filter).findOne({
where: { id: filterId },
relations: ['user'],
})
if (!filter) {
return {
errorCodes: [MoveFilterErrorCode.NotFound],
}
}
if (filter.user.id !== uid) {
return {
errorCodes: [MoveFilterErrorCode.Unauthorized],
}
}
if (filter.id === afterFilterId) {
// nothing to do
return { filter }
}
const oldPosition = filter.position
// if afterFilterId is not provided, move to the top
let newPosition = 1
if (afterFilterId) {
const afterFilter = await getRepository(Filter).findOne({
where: { id: afterFilterId },
relations: ['user'],
})
if (!afterFilter) {
return {
errorCodes: [MoveFilterErrorCode.NotFound],
}
}
if (afterFilter.user.id !== uid) {
return {
errorCodes: [MoveFilterErrorCode.Unauthorized],
}
}
newPosition = afterFilter.position
}
const moveUp = newPosition < oldPosition
// 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, oldPosition),
Math.max(newPosition, oldPosition)
),
},
{
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) {
return {
errorCodes: [MoveFilterErrorCode.BadRequest],
}
}
analytics.track({
userId: uid,
event: 'filter_moved',
properties: {
filterId,
afterFilterId,
env: env.server.apiEnv,
},
})
return {
filter: updated,
}
} catch (error) {
log.error('Error moving filters', {
error,
labels: {
source: 'resolver',
resolver: 'moveFilterResolver',
uid,
},
})
return {
errorCodes: [MoveFilterErrorCode.BadRequest],
}
}
})

View File

@ -58,6 +58,7 @@ import {
labelsResolver,
logOutResolver,
mergeHighlightResolver,
moveFilterResolver,
moveLabelResolver,
newsletterEmailsResolver,
reminderResolver,
@ -184,6 +185,7 @@ export const functionResolvers = {
deleteRule: deleteRuleResolver,
saveFilter: saveFilterResolver,
deleteFilter: deleteFilterResolver,
moveFilter: moveFilterResolver,
},
Query: {
me: getMeUserResolver,
@ -631,4 +633,5 @@ export const functionResolvers = {
...resultResolveTypeResolver('SaveFilter'),
...resultResolveTypeResolver('Filters'),
...resultResolveTypeResolver('DeleteFilter'),
...resultResolveTypeResolver('MoveFilter'),
}

View File

@ -2068,6 +2068,7 @@ const schema = gql`
id: ID!
name: String!
filter: String!
position: Int!
description: String
createdAt: Date!
updatedAt: Date!
@ -2114,6 +2115,27 @@ const schema = gql`
NOT_FOUND
}
input MoveFilterInput {
filterId: ID!
afterFilterId: ID # null to move to the top
}
union MoveFilterResult = MoveFilterSuccess | MoveFilterError
type MoveFilterSuccess {
filter: Filter!
}
type MoveFilterError {
errorCodes: [MoveFilterErrorCode!]!
}
enum MoveFilterErrorCode {
UNAUTHORIZED
BAD_REQUEST
NOT_FOUND
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -2190,6 +2212,7 @@ const schema = gql`
deleteRule(id: ID!): DeleteRuleResult!
saveFilter(input: SaveFilterInput!): SaveFilterResult!
deleteFilter(id: ID!): DeleteFilterResult!
moveFilter(input: MoveFilterInput!): MoveFilterResult!
}
# FIXME: remove sort from feedArticles after all cached tabs are closed

View File

@ -10,14 +10,42 @@ CREATE TABLE omnivore.filters (
name character varying(255) NOT NULL,
description character varying(255),
filter character varying(255) NOT NULL,
position integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT current_timestamp,
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
UNIQUE (user_id, name)
);
CREATE TRIGGER filters_modtime BEFORE UPDATE ON omnivore.filters
CREATE OR REPLACE FUNCTION update_filter_position()
RETURNS TRIGGER AS $$
DECLARE
new_position INTEGER;
BEGIN
IF (TG_OP = 'DELETE') THEN
UPDATE omnivore.filters SET position = position - 1 WHERE user_id = OLD.user_id AND position > OLD.position;
RETURN OLD;
ELSIF (TG_OP = 'INSERT') THEN
SELECT COALESCE(MAX(position), 0) + 1 INTO new_position FROM omnivore.filters WHERE user_id = NEW.user_id AND name < NEW.name;
UPDATE omnivore.filters SET position = position + 1 WHERE user_id = NEW.user_id AND position >= new_position;
NEW.position = new_position;
RETURN NEW;
END IF;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER update_filter_modtime BEFORE UPDATE ON omnivore.filters
FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER increment_filter_position
BEFORE INSERT ON omnivore.filters
FOR EACH ROW
EXECUTE FUNCTION update_filter_position();
CREATE TRIGGER decrement_filter_position
AFTER DELETE ON omnivore.filters
FOR EACH ROW
EXECUTE FUNCTION update_filter_position();
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.filters TO omnivore_user;
COMMIT;

View File

@ -4,6 +4,8 @@
BEGIN;
DROP FUNCTION IF EXISTS update_filter_position;
DROP TABLE IF EXISTS omnivore.filters;
COMMIT;