From b139fae711d4bddac4465e4c40b3dc0860725dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Bari=C4=87?= Date: Thu, 8 Aug 2024 08:56:24 +0200 Subject: [PATCH] feat(strict-ts): apply to source schemas (#2111) --- src/common/post.ts | 2 +- src/entity/Source.ts | 4 +- src/schema/sourceRequests.ts | 45 ++++--- src/schema/sources.ts | 255 +++++++++++++++++++++++------------ 4 files changed, 203 insertions(+), 103 deletions(-) diff --git a/src/common/post.ts b/src/common/post.ts index 762dc8281..001bd586f 100644 --- a/src/common/post.ts +++ b/src/common/post.ts @@ -121,7 +121,7 @@ We hope you will find ${name} useful!`; export const createSquadWelcomePost = async ( con: DataSource | EntityManager, - source: SquadSource, + source: Pick, adminId: string, args: Partial = {}, ) => { diff --git a/src/entity/Source.ts b/src/entity/Source.ts index 7c4bb0859..d74ee1cb6 100644 --- a/src/entity/Source.ts +++ b/src/entity/Source.ts @@ -140,8 +140,8 @@ export class MachineSource extends Source { @ChildEntity(SourceType.Squad) export class SquadSource extends Source { @Column({ default: 0 }) - memberPostingRank?: number; + memberPostingRank: number; @Column({ default: 0 }) - memberInviteRank?: number; + memberInviteRank: number; } diff --git a/src/schema/sourceRequests.ts b/src/schema/sourceRequests.ts index 425e1595b..cc67de280 100644 --- a/src/schema/sourceRequests.ts +++ b/src/schema/sourceRequests.ts @@ -10,7 +10,7 @@ import { offsetPageGenerator, } from './common'; import { traceResolvers } from './trace'; -import { Context } from '../Context'; +import { AuthContext, BaseContext, Context } from '../Context'; import { MachineSource, Source, @@ -25,7 +25,11 @@ import { getRelayNodeInfo, uploadLogo } from '../common'; import { GraphQLResolveInfo } from 'graphql'; import { FileUpload } from 'graphql-upload/GraphQLUpload.js'; import { GQLSubmissionAvailability, hasSubmissionAccess } from './submissions'; -import { ConflictError, SourceRequestErrorMessage } from '../errors'; +import { + ConflictError, + SourceRequestErrorMessage, + TypeORMQueryFailedError, +} from '../errors'; import { ConnectionArguments } from 'graphql-relay'; import { SourceMemberRoles } from '../roles'; import { MoreThan } from 'typeorm'; @@ -482,13 +486,15 @@ const ensureNotRejected = async ( } }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const resolvers: IResolvers = traceResolvers({ +export const resolvers: IResolvers = traceResolvers< + unknown, + BaseContext +>({ Mutation: { requestSource: async ( source, { data }: GQLDataInput, - ctx, + ctx: AuthContext, ): Promise => { const user = await ctx .getRepository(User) @@ -511,14 +517,14 @@ export const resolvers: IResolvers = traceResolvers({ updateSourceRequest: async ( source, { id, data }: GQLDataIdInput, - ctx, + ctx: AuthContext, ): Promise => { return partialUpdateSourceRequest(ctx, id, data); }, declineSourceRequest: async ( source, { id, data }: GQLDataIdInput, - ctx, + ctx: AuthContext, ): Promise => { return partialUpdateSourceRequest(ctx, id, { approved: false, @@ -529,7 +535,7 @@ export const resolvers: IResolvers = traceResolvers({ approveSourceRequest: async ( source, { id }: GQLIdInput, - ctx, + ctx: AuthContext, ): Promise => { return partialUpdateSourceRequest(ctx, id, { approved: true, @@ -538,7 +544,7 @@ export const resolvers: IResolvers = traceResolvers({ publishSourceRequest: async ( source, { id }: GQLIdInput, - ctx, + ctx: AuthContext, ): Promise => { const req = await findOrFail(ctx, id); if ( @@ -566,7 +572,7 @@ export const resolvers: IResolvers = traceResolvers({ uploadSourceRequestLogo: async ( source, { id, file }: GQLIdInput & { file: FileUpload }, - ctx, + ctx: AuthContext, ): Promise => { const req = await findOrFail(ctx, id); const { createReadStream } = await file; @@ -586,7 +592,7 @@ export const resolvers: IResolvers = traceResolvers({ submitSquadForReview: async ( _, { sourceId }: GQLPublicSquadRequestInput, - ctx, + ctx: AuthContext, ): Promise => { // we need to check that the user is squad admin, moderator is not enough const squad = await ensureSourceRole( @@ -613,7 +619,9 @@ export const resolvers: IResolvers = traceResolvers({ try { const result = await repo.save(publicReq); return result; - } catch (err) { + } catch (originalError) { + const err = originalError as TypeORMQueryFailedError; + if (err.name === 'QueryFailedError' && err.code === '23505') { throw new ConflictError('Request already exists!'); } @@ -650,7 +658,7 @@ export const resolvers: IResolvers = traceResolvers({ sourceRequestAvailability: async ( _, __, - ctx, + ctx: AuthContext, ): Promise => { const user = await ctx .getRepository(User) @@ -663,18 +671,23 @@ export const resolvers: IResolvers = traceResolvers({ publicSquadRequests: forwardPagination( async ( _, - args: PublicSquadRequestsArgs, + args, ctx, { limit, offset }, info: GraphQLResolveInfo, ): Promise> => { + const sourceArgs = args as PublicSquadRequestsArgs; // we need to check that the user is squad admin, moderator is not enough - await ensureSourceRole(ctx, args.sourceId, SourceMemberRoles.Admin); + await ensureSourceRole( + ctx, + sourceArgs.sourceId, + SourceMemberRoles.Admin, + ); const [rows, total] = await ctx.loader.loadManyPaginated( SquadPublicRequest, - { sourceId: args.sourceId }, + { sourceId: sourceArgs.sourceId }, getRelayNodeInfo(info), { limit, diff --git a/src/schema/sources.ts b/src/schema/sources.ts index 34df8f195..c9a9dcef8 100644 --- a/src/schema/sources.ts +++ b/src/schema/sources.ts @@ -1,8 +1,7 @@ import { ForbiddenError, ValidationError } from 'apollo-server-errors'; import { IResolvers } from '@graphql-tools/utils'; import { ConnectionArguments } from 'graphql-relay'; -import { traceResolverObject } from './trace'; -import { Context } from '../Context'; +import { AuthContext, BaseContext, Context } from '../Context'; import { createSharePost, generateMemberToken, @@ -50,6 +49,7 @@ import { SourcePermissionErrorKeys, SourceRequestErrorMessage, TypeOrmError, + TypeORMQueryFailedError, } from '../errors'; import { descriptionRegex, @@ -66,6 +66,7 @@ import { TrendingSource } from '../entity/TrendingSource'; import { PopularSource } from '../entity/PopularSource'; import { PopularVideoSource } from '../entity/PopularVideoSource'; import { EntityTarget } from 'typeorm/common/EntityTarget'; +import { traceResolvers } from './trace'; export interface GQLSource { id: string; @@ -805,7 +806,7 @@ export const sourceTypesWithMembers = ['squad']; export const canAccessSource = async ( ctx: Context, source: Source, - member: SourceMember, + member: SourceMember | null, permission: SourcePermissions, validateRankAgainstId?: string, ): Promise => { @@ -828,7 +829,7 @@ export const canAccessSource = async ( const repo = ctx.getRepository(SourceMember); const validateRankAgainst = await (requireGreaterAccessPrivilege[permission] ? repo.findOneByOrFail({ sourceId, userId: validateRankAgainstId }) - : Promise.resolve(null)); + : Promise.resolve(undefined)); return hasPermissionCheck(source, member, permission, validateRankAgainst); }; @@ -836,7 +837,7 @@ export const canAccessSource = async ( export const canPostToSquad = ( ctx: Context, squad: SquadSource, - sourceMember: SourceMember, + sourceMember: SourceMember | null, ): boolean => { if (!sourceMember) { return false; @@ -917,7 +918,7 @@ export const ensureSourcePermissions = async ( if ( source.type === SourceType.Squad && permission === SourcePermissions.Post && - !canPostToSquad(ctx, source, sourceMember) + !canPostToSquad(ctx, source as SquadSource, sourceMember) ) { throw new ForbiddenError('Posting not allowed!'); } @@ -927,7 +928,10 @@ export const ensureSourcePermissions = async ( throw new ForbiddenError('Access denied!'); }; -const sourceByFeed = async (feed: string, ctx: Context): Promise => { +const sourceByFeed = async ( + feed: string, + ctx: Context, +): Promise => { const res = await ctx.con .createQueryBuilder() .select('source.*') @@ -998,7 +1002,7 @@ const addNewSourceMember = async ( export const getPermissionsForMember = ( member: Pick, - source: Pick, + source: Partial>, ): SourcePermissions[] => { const permissions = roleSourcePermissions[member.role] ?? roleSourcePermissions.member; @@ -1006,11 +1010,11 @@ export const getPermissionsForMember = ( sourceRoleRank[member.role] ?? sourceRoleRank[SourceMemberRoles.Member]; const permissionsToRemove: SourcePermissions[] = []; - if (memberRank < source.memberPostingRank) { + if (source.memberPostingRank && memberRank < source.memberPostingRank) { permissionsToRemove.push(SourcePermissions.Post); } - if (memberRank < source.memberInviteRank) { + if (source.memberInviteRank && memberRank < source.memberInviteRank) { permissionsToRemove.push(SourcePermissions.Invite); } @@ -1127,14 +1131,15 @@ const paginateSourceMembers = ( ); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const resolvers: IResolvers = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Query: traceResolverObject({ +export const resolvers: IResolvers = traceResolvers< + unknown, + BaseContext +>({ + Query: { sources: async ( _, args: SourcesArgs, - ctx, + ctx: Context, info, ): Promise> => { const filter: FindOptionsWhere = { active: true }; @@ -1174,7 +1179,7 @@ export const resolvers: IResolvers = { sourcesByTag: async ( _, args: SourcesByTag, - ctx, + ctx: Context, info, ): Promise> => { const alwaysExcludeSources = ['unknown', 'community', 'collections']; @@ -1216,7 +1221,7 @@ export const resolvers: IResolvers = { similarSources: async ( _, args: SimilarSources, - ctx, + ctx: Context, info, ): Promise> => { const alwaysExcludeSources = ['unknown', 'community', 'collections']; @@ -1257,7 +1262,7 @@ export const resolvers: IResolvers = { .andWhere('id IN (:...subQuery)', { subQuery: subQuery.length > 0 - ? subQuery.map((s) => s.sourceId) + ? subQuery.map((s: { sourceId: string }) => s.sourceId) : ['NULL'], }) .andWhere(filter) @@ -1266,7 +1271,12 @@ export const resolvers: IResolvers = { }, ); }, - mostRecentSources: async (_, args, ctx, info): Promise => { + mostRecentSources: async ( + _, + args, + ctx: Context, + info, + ): Promise => { const { limit = 10 } = args; return await graphorm.query(ctx, info, (builder) => { builder.queryBuilder @@ -1276,27 +1286,41 @@ export const resolvers: IResolvers = { return builder; }); }, - trendingSources: async (_, args, ctx, info): Promise => + trendingSources: async ( + _, + args, + ctx: Context, + info, + ): Promise => getFormattedSources(TrendingSource, args, ctx, info), - popularSources: async (_, args, ctx, info): Promise => + popularSources: async (_, args, ctx: Context, info): Promise => getFormattedSources(PopularSource, args, ctx, info), - topVideoSources: async (_, args, ctx, info): Promise => + topVideoSources: async ( + _, + args, + ctx: Context, + info, + ): Promise => getFormattedSources(PopularVideoSource, args, ctx, info), sourceByFeed: async ( _, { feed }: { feed: string }, - ctx, - ): Promise => sourceByFeed(feed, ctx), + ctx: AuthContext, + ): Promise => sourceByFeed(feed, ctx), source: async ( _, { id }: { id: string }, - ctx, + ctx: Context, info, ): Promise => { await ensureSourcePermissions(ctx, id); return getSourceById(ctx, info, id); }, - sourceHandleExists: async (_, { handle }: { handle: string }, ctx) => { + sourceHandleExists: async ( + _, + { handle }: { handle: string }, + ctx: AuthContext, + ) => { try { const transformed = await validateAndTransformHandle( handle, @@ -1321,7 +1345,7 @@ export const resolvers: IResolvers = { sourceMembers: async ( _, { role, sourceId, query, ...args }: SourceMemberArgs, - ctx, + ctx: Context, info, ): Promise> => { const permission = @@ -1332,15 +1356,27 @@ export const resolvers: IResolvers = { await ensureSourcePermissions(ctx, sourceId, permission); return paginateSourceMembers( (queryBuilder, alias) => { - queryBuilder - .andWhere(`${alias}."sourceId" = :source`, { + queryBuilder = queryBuilder.andWhere( + `${alias}."sourceId" = :source`, + { source: sourceId, - }) - .addOrderBy( - graphorm.mappings.SourceMember.fields.roleRank.select as string, + }, + ); + + if ( + typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === + 'string' + ) { + queryBuilder = queryBuilder.addOrderBy( + graphorm.mappings.SourceMember.fields?.roleRank.select, 'DESC', - ) - .addOrderBy(`${alias}."createdAt"`, 'DESC'); + ); + } + + queryBuilder = queryBuilder.addOrderBy( + `${alias}."createdAt"`, + 'DESC', + ); if (query) { queryBuilder = queryBuilder @@ -1354,11 +1390,12 @@ export const resolvers: IResolvers = { queryBuilder = queryBuilder.andWhere(`${alias}.role = :role`, { role, }); - } else { + } else if ( + typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === + 'string' + ) { queryBuilder = queryBuilder.andWhere( - `${ - graphorm.mappings.SourceMember.fields.roleRank.select as string - } >= 0`, + `${graphorm.mappings.SourceMember.fields.roleRank.select} >= 0`, ); } return queryBuilder; @@ -1371,24 +1408,33 @@ export const resolvers: IResolvers = { mySourceMemberships: async ( _, args: SourcesByType, - ctx, + ctx: AuthContext, info, ): Promise> => { const { type, ...connectionArgs } = args; return paginateSourceMembers( (queryBuilder, alias) => { - queryBuilder - .andWhere(`${alias}."userId" = :user`, { user: ctx.userId }) - .andWhere( - `${ - graphorm.mappings.SourceMember.fields.roleRank.select as string - } >= 0`, - ) - .addOrderBy(`${alias}."createdAt"`, 'DESC'); + queryBuilder = queryBuilder.andWhere(`${alias}."userId" = :userId`, { + userId: ctx.userId, + }); + + if ( + typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === + 'string' + ) { + queryBuilder = queryBuilder.andWhere( + `${graphorm.mappings.SourceMember.fields.roleRank.select} >= 0`, + ); + } + + queryBuilder = queryBuilder.addOrderBy( + `${alias}."createdAt"`, + 'DESC', + ); if (type) { - queryBuilder + queryBuilder = queryBuilder .innerJoin(Source, 's', `${alias}."sourceId" = s.id`) .andWhere(`s."type" = :type`, { type, @@ -1404,22 +1450,30 @@ export const resolvers: IResolvers = { publicSourceMemberships: async ( _, { userId, ...args }: { userId: string } & ConnectionArguments, - ctx, + ctx: Context, info, ): Promise> => { return paginateSourceMembers( (queryBuilder, alias) => { + queryBuilder = queryBuilder.andWhere(`${alias}."userId" = :userId`, { + userId, + }); + + if ( + typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === + 'string' + ) { + queryBuilder = queryBuilder + .andWhere( + `${graphorm.mappings.SourceMember.fields.roleRank.select} >= 0`, + ) + .addOrderBy( + graphorm.mappings.SourceMember.fields.roleRank.select, + 'DESC', + ); + } + return queryBuilder - .andWhere(`${alias}."userId" = :user`, { user: userId }) - .andWhere( - `${ - graphorm.mappings.SourceMember.fields.roleRank.select as string - } >= 0`, - ) - .addOrderBy( - graphorm.mappings.SourceMember.fields.roleRank.select as string, - 'DESC', - ) .addOrderBy(`${alias}."createdAt"`, 'DESC') .innerJoin(Source, 's', `${alias}."sourceId" = s.id`) .andWhere('s.private = false'); @@ -1432,7 +1486,7 @@ export const resolvers: IResolvers = { sourceMemberByToken: async ( _, { token }: { token: string }, - ctx, + ctx: AuthContext, info, ): Promise => { const res = await graphorm.query( @@ -1450,7 +1504,11 @@ export const resolvers: IResolvers = { } return res[0]; }, - relatedTags: async (_, { sourceId }, ctx): Promise => { + relatedTags: async ( + _, + { sourceId }, + ctx: Context, + ): Promise => { const keywords = await ctx.con .createQueryBuilder() .from(SourceTagView, 'stv') @@ -1463,9 +1521,8 @@ export const resolvers: IResolvers = { hits: keywords.map(({ tag }) => ({ name: tag })), }; }, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Mutation: traceResolverObject({ + }, + Mutation: { createSquad: async ( _, { @@ -1478,7 +1535,7 @@ export const resolvers: IResolvers = { memberPostingRole = SourceMemberRoles.Member, memberInviteRole = SourceMemberRoles.Member, }: CreateSquadArgs, - ctx, + ctx: AuthContext, info, ): Promise => { const handle = await validateSquadData( @@ -1522,7 +1579,7 @@ export const resolvers: IResolvers = { id, ctx.userId, postId, - commentary, + commentary || null, ); } // Upload the image (if provided) @@ -1535,7 +1592,9 @@ export const resolvers: IResolvers = { return id; }); return getSourceById(ctx, info, sourceId); - } catch (err) { + } catch (originalError) { + const err = originalError as TypeORMQueryFailedError; + if (err.code === TypeOrmError.DUPLICATE_ENTRY) { if (err.message.indexOf('source_handle') > -1) { throw new ValidationError( @@ -1558,7 +1617,7 @@ export const resolvers: IResolvers = { memberInviteRole, isPrivate, }: EditSquadArgs, - ctx, + ctx: AuthContext, info, ): Promise => { const current = await ensureSourcePermissions( @@ -1582,8 +1641,12 @@ export const resolvers: IResolvers = { name, handle, description, - memberPostingRank: sourceRoleRank[memberPostingRole], - memberInviteRank: sourceRoleRank[memberInviteRole], + memberPostingRank: memberPostingRole + ? sourceRoleRank[memberPostingRole] + : undefined, + memberInviteRank: memberInviteRole + ? sourceRoleRank[memberInviteRole] + : undefined, }; if (!isNullOrUndefined(isPrivate) && current.private !== isPrivate) { @@ -1621,7 +1684,9 @@ export const resolvers: IResolvers = { }, ); return getSourceById(ctx, info, editedSourceId); - } catch (err) { + } catch (originalError) { + const err = originalError as TypeORMQueryFailedError; + if (err.code === TypeOrmError.DUPLICATE_ENTRY) { if (err.message.indexOf('source_handle') > -1) { throw new ValidationError( @@ -1635,7 +1700,7 @@ export const resolvers: IResolvers = { deleteSource: async ( _, { sourceId }: { sourceId: string }, - ctx, + ctx: AuthContext, ): Promise => { await ensureSourcePermissions(ctx, sourceId, SourcePermissions.Delete); await ctx.con.getRepository(Source).delete({ @@ -1646,7 +1711,7 @@ export const resolvers: IResolvers = { leaveSource: async ( _, { sourceId }: { sourceId: string }, - ctx, + ctx: AuthContext, ): Promise => { await ensureSourcePermissions(ctx, sourceId, SourcePermissions.Leave); await ctx.con.getRepository(SourceMember).delete({ @@ -1658,7 +1723,7 @@ export const resolvers: IResolvers = { updateMemberRole: async ( _, { sourceId, memberId, role }: UpdateMemberRoleArgs, - ctx, + ctx: AuthContext, ): Promise => { if (role === SourceMemberRoles.Blocked) { await ensureSourcePermissions( @@ -1688,7 +1753,7 @@ export const resolvers: IResolvers = { unblockMember: async ( _, { sourceId, memberId }: UpdateMemberRoleArgs, - ctx, + ctx: AuthContext, ): Promise => { await ensureSourcePermissions( ctx, @@ -1705,7 +1770,7 @@ export const resolvers: IResolvers = { joinSource: async ( _, { sourceId, token }: { sourceId: string; token: string }, - ctx, + ctx: AuthContext, info, ): Promise => { const source = await ctx.con @@ -1756,7 +1821,9 @@ export const resolvers: IResolvers = { .getRepository(Source) .update({ id: sourceId }, { active: true }); }); - } catch (err) { + } catch (originalError) { + const err = originalError as TypeORMQueryFailedError; + if (err?.code !== TypeOrmError.DUPLICATE_ENTRY) { throw err; } @@ -1764,22 +1831,42 @@ export const resolvers: IResolvers = { return getSourceById(ctx, info, sourceId); }, - hideSourceFeedPosts: async (_, { sourceId }: { sourceId: string }, ctx) => { + hideSourceFeedPosts: async ( + _, + { sourceId }: { sourceId: string }, + ctx: AuthContext, + ) => { return updateHideFeedPostsFlag(ctx, sourceId, true); }, - showSourceFeedPosts: async (_, { sourceId }: { sourceId: string }, ctx) => { + showSourceFeedPosts: async ( + _, + { sourceId }: { sourceId: string }, + ctx: AuthContext, + ) => { return updateHideFeedPostsFlag(ctx, sourceId, false); }, - collapsePinnedPosts: async (_, { sourceId }: { sourceId: string }, ctx) => { + collapsePinnedPosts: async ( + _, + { sourceId }: { sourceId: string }, + ctx: AuthContext, + ) => { return togglePinnedPosts(ctx, sourceId, true); }, - expandPinnedPosts: async (_, { sourceId }: { sourceId: string }, ctx) => { + expandPinnedPosts: async ( + _, + { sourceId }: { sourceId: string }, + ctx: AuthContext, + ) => { return togglePinnedPosts(ctx, sourceId, false); }, - }), + }, Source: { permalink: (source: GQLSource): string => getSourceLink(source), - referralUrl: async (source: GQLSource, _, ctx): Promise => { + referralUrl: async ( + source: GQLSource, + _, + ctx: Context, + ): Promise => { if (!ctx.userId) { return null; } @@ -1796,4 +1883,4 @@ export const resolvers: IResolvers = { return referralUrl; }, }, -}; +});