HBE-147 refactor: Introduce shortcodes into self-host refactored to pseudo-fp format (#22)

* refactor: refactor all queries,mutations and subscriptions for shortcode module

* test: rewrote test cases for shortcodes

* chore: modified shortcode error code

* chore: created helper function to do shortcode type conversion in service file

* chore: simplifed logic to fetch user shortcodes with cursor pagination

* chore: removed migrations file

* chore: removed unused imports in shortcodes module

* chore: modified generateUniqueShortCodeID function

* chore: modified generateUniqueShortCodeID function

* chore: changed jwtService to use verify instead of decode

* docs: added teacher comments to all shortcodes service methods

* chore: removed stale test cases from shortcode modules
This commit is contained in:
Balu Babu
2023-02-22 17:40:53 +05:30
committed by GitHub
parent 24dd535d9e
commit 1860057a25
7 changed files with 470 additions and 743 deletions

View File

@@ -13,6 +13,7 @@ import { TeamEnvironmentsModule } from './team-environments/team-environments.mo
import { TeamCollectionModule } from './team-collection/team-collection.module'; import { TeamCollectionModule } from './team-collection/team-collection.module';
import { TeamRequestModule } from './team-request/team-request.module'; import { TeamRequestModule } from './team-request/team-request.module';
import { TeamInvitationModule } from './team-invitation/team-invitation.module'; import { TeamInvitationModule } from './team-invitation/team-invitation.module';
import { ShortcodeModule } from './shortcode/shortcode.module';
@Module({ @Module({
imports: [ imports: [
@@ -55,6 +56,7 @@ import { TeamInvitationModule } from './team-invitation/team-invitation.module';
TeamCollectionModule, TeamCollectionModule,
TeamRequestModule, TeamRequestModule,
TeamInvitationModule, TeamInvitationModule,
ShortcodeModule,
], ],
providers: [GQLComplexityPlugin], providers: [GQLComplexityPlugin],
}) })

View File

@@ -155,16 +155,29 @@ export const TEAM_INVITE_EMAIL_DO_NOT_MATCH =
export const TEAM_INVITE_NOT_VALID_VIEWER = export const TEAM_INVITE_NOT_VALID_VIEWER =
'team_invite/not_valid_viewer' as const; 'team_invite/not_valid_viewer' as const;
/**
* ShortCode not found in DB
* (ShortcodeService)
*/
export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const; export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
/**
* Invalid ShortCode format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const; export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
/**
* ShortCode already exists in DB
* (ShortcodeService)
*/
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
/** /**
* Invalid or non-existent TEAM ENVIRONMMENT ID * Invalid or non-existent TEAM ENVIRONMMENT ID
* (TeamEnvironmentsService) * (TeamEnvironmentsService)
*/ */
export const TEAM_ENVIRONMENT_NOT_FOUND = export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
'team_environment/not_found' as const;
/** /**
* The user is not a member of the team of the given environment * The user is not a member of the team of the given environment

View File

@@ -7,6 +7,7 @@ import { TeamEnvironment } from 'src/team-environments/team-environments.model';
import { TeamCollection } from 'src/team-collection/team-collection.model'; import { TeamCollection } from 'src/team-collection/team-collection.model';
import { TeamRequest } from 'src/team-request/team-request.model'; import { TeamRequest } from 'src/team-request/team-request.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { Shortcode } from 'src/shortcode/shortcode.model';
// A custom message type that defines the topic and the corresponding payload. // A custom message type that defines the topic and the corresponding payload.
// For every module that publishes a subscription add its type def and the possible subscription type. // For every module that publishes a subscription add its type def and the possible subscription type.
@@ -36,4 +37,5 @@ export type TopicDef = {
[topic: `team_req/${string}/req_deleted`]: string; [topic: `team_req/${string}/req_deleted`]: string;
[topic: `team/${string}/invite_added`]: TeamInvitation; [topic: `team/${string}/invite_added`]: TeamInvitation;
[topic: `team/${string}/invite_removed`]: string; [topic: `team/${string}/invite_removed`]: string;
[topic: `shortcode/${string}/${'created' | 'revoked'}`]: Shortcode;
}; };

View File

@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaModule } from 'src/prisma/prisma.module';
import { PubSubModule } from 'src/pubsub/pubsub.module'; import { PubSubModule } from 'src/pubsub/pubsub.module';
import { UserModule } from 'src/user/user.module'; import { UserModule } from 'src/user/user.module';
@@ -7,7 +7,14 @@ import { ShortcodeResolver } from './shortcode.resolver';
import { ShortcodeService } from './shortcode.service'; import { ShortcodeService } from './shortcode.service';
@Module({ @Module({
imports: [PrismaModule, UserModule, PubSubModule], imports: [
PrismaModule,
UserModule,
PubSubModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
}),
],
providers: [ShortcodeService, ShortcodeResolver], providers: [ShortcodeService, ShortcodeResolver],
exports: [ShortcodeService], exports: [ShortcodeService],
}) })

View File

@@ -7,23 +7,19 @@ import {
Resolver, Resolver,
Subscription, Subscription,
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import { pipe } from 'fp-ts/function';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import * as T from 'fp-ts/Task';
import * as TO from 'fp-ts/TaskOption';
import * as TE from 'fp-ts/TaskEither';
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
import { Shortcode } from './shortcode.model'; import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service'; import { ShortcodeService } from './shortcode.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { SHORTCODE_INVALID_JSON } from 'src/errors';
import { GqlUser } from 'src/decorators/gql-user.decorator'; import { GqlUser } from 'src/decorators/gql-user.decorator';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard'; import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from '../types/AuthUser'; import { AuthUser } from '../types/AuthUser';
import { JwtService } from '@nestjs/jwt';
import { PaginationArgs } from 'src/types/input-types.args';
@Resolver(() => Shortcode) @Resolver(() => Shortcode)
export class ShortcodeResolver { export class ShortcodeResolver {
@@ -31,155 +27,95 @@ export class ShortcodeResolver {
private readonly shortcodeService: ShortcodeService, private readonly shortcodeService: ShortcodeService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
private jwtService: JwtService,
) {} ) {}
/* Queries */ /* Queries */
@Query(() => Shortcode, { @Query(() => Shortcode, {
description: 'Resolves and returns a shortcode data', description: 'Resolves and returns a shortcode data',
nullable: true, nullable: true,
}) })
shortcode( async shortcode(
@Args({ @Args({
name: 'code', name: 'code',
type: () => ID, type: () => ID,
description: 'The shortcode to resolve', description: 'The shortcode to resolve',
}) })
code: string, code: string,
): Promise<Shortcode | null> { ) {
return pipe( const result = await this.shortcodeService.getShortCode(code);
this.shortcodeService.resolveShortcode(code),
TO.getOrElseW(() => T.of(null)), if (E.isLeft(result)) throwErr(result.left);
)(); return result.right;
} }
@Query(() => [Shortcode], { @Query(() => [Shortcode], {
description: 'List all shortcodes the current user has generated', description: 'List all shortcodes the current user has generated',
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
myShortcodes( async myShortcodes(@GqlUser() user: AuthUser, @Args() args: PaginationArgs) {
@GqlUser() user: AuthUser, return this.shortcodeService.fetchUserShortCodes(user.uid, args);
@Args({
name: 'cursor',
type: () => ID,
description:
'The ID of the last returned shortcode (used for pagination)',
nullable: true,
})
cursor?: string,
): Promise<Shortcode[]> {
return this.shortcodeService.fetchUserShortCodes(
user.uid,
cursor ?? null,
)();
} }
/* Mutations */ /* Mutations */
@Mutation(() => Shortcode, {
description: 'Create a shortcode for the given request.',
})
async createShortcode(
@Args({
name: 'request',
description: 'JSON string of the request object',
})
request: string,
@Context() ctx: any,
) {
const decodedAccessToken = this.jwtService.verify(
ctx.req.cookies['access_token'],
);
const result = await this.shortcodeService.createShortcode(
request,
decodedAccessToken?.sub,
);
// TODO: Create a shortcode resolver pending implementation if (E.isLeft(result)) throwErr(result.left);
// @Mutation(() => Shortcode, { return result.right;
// description: 'Create a shortcode for the given request.', }
// })
// createShortcode(
// @Args({
// name: 'request',
// description: 'JSON string of the request object',
// })
// request: string,
// @Context() ctx: any,
// ): Promise<Shortcode> {
// return pipe(
// TE.Do,
//
// // Get the user
// TE.bind('user', () =>
// pipe(
// TE.tryCatch(
// () => {
// const authString: string | undefined | null =
// ctx.reqHeaders.authorization;
//
// if (
// !authString ||
// !authString.includes(' ') ||
// !authString.startsWith('Bearer ')
// ) {
// return Promise.reject('no auth token');
// }
//
// const authToken = authString.split(' ')[1];
//
// return this.userService.authenticateWithIDToken(authToken);
// },
// (e) => e,
// ),
// TE.getOrElseW(() => T.of(undefined)),
// TE.fromTask,
// ),
// ),
//
// // Get the Request JSON
// TE.bind('reqJSON', () =>
// pipe(
// E.tryCatch(
// () => JSON.parse(request),
// () => SHORTCODE_INVALID_JSON,
// ),
// TE.fromEither,
// ),
// ),
//
// // Create the shortcode
// TE.chain(({ reqJSON, user }) => {
// return TE.fromTask(
// this.shortcodeService.createShortcode(reqJSON, user),
// );
// }),
//
// // Return or throw if there is an error
// TE.getOrElse(throwErr),
// )();
// }
// TODO: Implement revoke shortcode @Mutation(() => Boolean, {
// @Mutation(() => Boolean, { description: 'Revoke a user generated shortcode',
// description: 'Revoke a user generated shortcode', })
// }) @UseGuards(GqlAuthGuard)
// @UseGuards(GqlAuthGuard) async revokeShortcode(
// revokeShortcode( @GqlUser() user: User,
// @GqlUser() user: User, @Args({
// @Args({ name: 'code',
// name: 'code', type: () => ID,
// type: () => ID, description: 'The shortcode to resolve',
// description: 'The shortcode to resolve', })
// }) code: string,
// code: string, ) {
// ): Promise<boolean> { const result = await this.shortcodeService.revokeShortCode(code, user.uid);
// return pipe(
// this.shortcodeService.revokeShortCode(code, user.uid), if (E.isLeft(result)) throwErr(result.left);
// TE.map(() => true), // Just return true on success, no resource to return return result.right;
// TE.getOrElse(throwErr), }
// )();
// }
/* Subscriptions */ /* Subscriptions */
@Subscription(() => Shortcode, {
description: 'Listen for shortcode creation',
resolve: (value) => value,
})
@UseGuards(GqlAuthGuard)
myShortcodesCreated(@GqlUser() user: AuthUser) {
return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`);
}
// TODO: update subscription after fixing service methods @Subscription(() => Shortcode, {
// @Subscription(() => Shortcode, { description: 'Listen for shortcode deletion',
// description: 'Listen for shortcode creation', resolve: (value) => value,
// resolve: (value) => value, })
// }) @UseGuards(GqlAuthGuard)
// @UseGuards(GqlAuthGuard) myShortcodesRevoked(@GqlUser() user: AuthUser): AsyncIterator<Shortcode> {
// myShortcodesCreated(@GqlUser() user: AuthUser) { return this.pubsub.asyncIterator(`shortcode/${user.uid}/revoked`);
// return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`); }
// }
//
// @Subscription(() => Shortcode, {
// description: 'Listen for shortcode deletion',
// resolve: (value) => value,
// })
// @UseGuards(GqlAuthGuard)
// myShortcodesRevoked(@GqlUser() user: AuthUser): AsyncIterator<Shortcode> {
// return this.pubsub.asyncIterator(`shortcode/${user.uid}/revoked`);
// }
} }

View File

@@ -1,8 +1,10 @@
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import {
import { SHORTCODE_NOT_FOUND } from 'src/errors'; SHORTCODE_ALREADY_EXISTS,
import { User } from 'src/user/user.model'; SHORTCODE_INVALID_JSON,
SHORTCODE_NOT_FOUND,
} from 'src/errors';
import { Shortcode } from './shortcode.model'; import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service'; import { ShortcodeService } from './shortcode.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
@@ -34,258 +36,76 @@ beforeEach(() => {
mockReset(mockPrisma); mockReset(mockPrisma);
mockPubSub.publish.mockClear(); mockPubSub.publish.mockClear();
}); });
const createdOn = new Date();
const shortCodeWithOutUser = {
id: '123',
request: '{}',
createdOn: createdOn,
creatorUid: null,
};
const shortCodeWithUser = {
id: '123',
request: '{}',
createdOn: createdOn,
creatorUid: 'user_uid_1',
};
const shortcodes = [
{
id: 'blablabla',
request: {
hello: 'there',
},
creatorUid: 'testuser',
createdOn: new Date(),
},
{
id: 'blablabla1',
request: {
hello: 'there',
},
creatorUid: 'testuser',
createdOn: new Date(),
},
];
describe('ShortcodeService', () => { describe('ShortcodeService', () => {
describe('resolveShortcode', () => { describe('getShortCode', () => {
test('returns Some for a valid existent shortcode', () => { test('should return a valid shortcode with valid shortcode ID', async () => {
mockPrisma.shortcode.findFirst.mockResolvedValueOnce({ mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(
id: 'blablablabla', shortCodeWithOutUser,
createdOn: new Date(), );
request: {
hello: 'there',
},
creatorUid: 'testuser',
});
return expect( const result = await shortcodeService.getShortCode(
shortcodeService.resolveShortcode('blablablabla')(), shortCodeWithOutUser.id,
).resolves.toBeSome(); );
}); expect(result).toEqualRight(<Shortcode>{
id: shortCodeWithOutUser.id,
test('returns the correct info for a valid shortcode', () => { createdOn: shortCodeWithOutUser.createdOn,
const shortcode = { request: JSON.stringify(shortCodeWithOutUser.request),
id: 'blablablabla',
createdOn: new Date(),
request: {
hello: 'there',
},
creatorUid: 'testuser',
};
mockPrisma.shortcode.findFirst.mockResolvedValueOnce(shortcode);
return expect(
shortcodeService.resolveShortcode('blablablabla')(),
).resolves.toEqualSome(<Shortcode>{
id: shortcode.id,
request: JSON.stringify(shortcode.request),
createdOn: shortcode.createdOn,
}); });
}); });
test('returns None for non-existent shortcode', () => { test('should throw SHORTCODE_NOT_FOUND error when shortcode ID is invalid', async () => {
mockPrisma.shortcode.findFirst.mockResolvedValueOnce(null); mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
return expect( const result = await shortcodeService.getShortCode('invalidID');
shortcodeService.resolveShortcode('blablablabla')(), expect(result).toEqualLeft(SHORTCODE_NOT_FOUND);
).resolves.toBeNone();
}); });
}); });
// TODO: Implement create shortcode
// describe('createShortcode', () => {
// test('creates the shortcode entry in the db', async () => {
// mockPrisma.shortcode.create.mockResolvedValueOnce({
// id: 'itvalidreqid',
// request: {
// hello: 'there',
// },
// creatorUid: null,
// createdOn: new Date(),
// });
//
// await shortcodeService.createShortcode({ hello: 'there' })();
// });
//
// test('returns a valid Shortcode Model object', () => {
// const shortcode = {
// id: 'blablablabla',
// createdOn: new Date(),
// request: {
// hello: 'there',
// },
// creatorUid: 'testuser',
// };
// mockPrisma.shortcode.create.mockResolvedValueOnce(shortcode);
//
// expect(
// shortcodeService.createShortcode({ hello: 'there' })(),
// ).resolves.toEqual(<Shortcode>{
// id: shortcode.id,
// request: JSON.stringify(shortcode.request),
// createdOn: shortcode.createdOn,
// });
// });
//
// test('if a creator is specified, their UID is stored in the DB', async () => {
// const testUser: User = {
// uid: 'testuid',
// displayName: 'Test User',
// email: 'test@hoppscotch.io',
// };
//
// const shortcode = {
// id: 'blablablabla',
// createdOn: new Date(),
// request: {
// hello: 'there',
// },
// creatorUid: testUser.uid,
// };
//
// mockPrisma.shortcode.create.mockResolvedValueOnce(shortcode);
//
// const result = await shortcodeService.createShortcode(
// { hello: 'there' },
// testUser,
// )();
//
// expect(mockPrisma.shortcode.create).toHaveBeenCalledWith(
// expect.objectContaining({
// data: {
// id: expect.any(String),
// request: {
// hello: 'there',
// },
// creatorUid: testUser.uid,
// },
// }),
// );
// });
//
// test('if a creator is not specified the creator uid is stored as null', async () => {
// mockPrisma.shortcode.create.mockResolvedValueOnce({
// id: 'itvalidreqid',
// request: {
// hello: 'there',
// },
// creatorUid: null,
// createdOn: new Date(),
// });
//
// await shortcodeService.createShortcode({ hello: 'there' })();
//
// expect(mockPrisma.shortcode.create).toHaveBeenCalledWith(
// expect.objectContaining({
// data: {
// id: expect.any(String),
// request: {
// hello: 'there',
// },
// creatorUid: undefined,
// },
// }),
// );
// });
//
// test('generates shortcodes which are 12 character alphanumerics', async () => {
// mockPrisma.shortcode.create.mockImplementation((args) => {
// return Promise.resolve({
// id: args.data.id,
// request: args.data.request,
// creatorUid: args.data.creatorUid,
// createdOn: args.data.createdOn,
// }) as any;
// });
//
// // Generate 100 shortcodes
// const shortcodeEntries: Shortcode[] = [];
// for (let i = 0; i < 100; i++) {
// shortcodeEntries.push(
// await shortcodeService.createShortcode({ hello: 'there' })(),
// );
// }
//
// expect(shortcodeEntries.every((entry) => entry.id.length === 12)).toBe(
// true,
// );
// expect(
// shortcodeEntries.every((entry) => /^[a-zA-Z0-9]*$/.test(entry.id)),
// ).toBe(true);
// });
//
// test('if creator is not specified, doesnt publish to pubsub anything', async () => {
// mockPrisma.shortcode.create.mockResolvedValueOnce({
// id: 'itvalidreqid',
// request: {
// hello: 'there',
// },
// creatorUid: null,
// createdOn: new Date(),
// });
//
// await shortcodeService.createShortcode({ hello: 'there' })();
//
// expect(mockPubSub.publish).not.toHaveBeenCalled();
// });
//
// test('if creator is specified, publishes to the proper pubsub topic `shortcode.{uid}.created`', async () => {
// const testUser: User = {
// uid: 'testuid',
// displayName: 'Test User',
// email: 'test@hoppscotch.io',
// };
//
// const shortcode = {
// id: 'blablablabla',
// createdOn: new Date(),
// request: {
// hello: 'there',
// },
// creatorUid: testUser.uid,
// };
//
// mockPrisma.shortcode.create.mockResolvedValueOnce(shortcode);
//
// const result = await shortcodeService.createShortcode(
// { hello: 'there' },
// testUser,
// )();
//
// expect(mockPubSub.publish).toHaveBeenCalledWith(
// `shortcode/testuid/created`,
// { ...result },
// );
// });
// });
describe('fetchUserShortCodes', () => { describe('fetchUserShortCodes', () => {
test('returns all shortcodes for a user with no provided cursor', async () => { test('should return list of shortcodes with valid inputs and no cursor', async () => {
const shortcodes = [ mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodes);
{
id: 'blablabla',
request: {
hello: 'there',
},
creatorUid: 'testuser',
createdOn: new Date(),
},
{
id: 'blablabla1',
request: {
hello: 'there',
},
creatorUid: 'testuser',
createdOn: new Date(),
},
];
mockPrisma.shortcode.findMany.mockResolvedValue(shortcodes);
const result = await shortcodeService.fetchUserShortCodes( const result = await shortcodeService.fetchUserShortCodes('testuser', {
'testuser', cursor: null,
null,
)();
expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({
take: 10, take: 10,
where: {
creatorUid: 'testuser',
},
orderBy: {
createdOn: 'desc',
},
}); });
expect(result).toEqual(<Shortcode[]>[ expect(result).toEqual(<Shortcode[]>[
{ {
id: shortcodes[0].id, id: shortcodes[0].id,
@@ -300,221 +120,192 @@ describe('ShortcodeService', () => {
]); ]);
}); });
test('return shortcodes for a user with a provided cursor', async () => { test('should return list of shortcodes with valid inputs and cursor', async () => {
const shortcodes = [ mockPrisma.shortcode.findMany.mockResolvedValue([shortcodes[1]]);
{
id: 'blablabla1',
request: {
hello: 'there',
},
creatorUid: 'testuser',
createdOn: new Date(),
},
];
mockPrisma.shortcode.findMany.mockResolvedValue(shortcodes);
const result = await shortcodeService.fetchUserShortCodes( const result = await shortcodeService.fetchUserShortCodes('testuser', {
'testuser', cursor: 'blablabla',
'blablabla',
)();
expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({
take: 10, take: 10,
skip: 1,
cursor: {
id: 'blablabla',
},
where: {
creatorUid: 'testuser',
},
orderBy: {
createdOn: 'desc',
},
}); });
expect(result).toEqual(<Shortcode[]>[ expect(result).toEqual(<Shortcode[]>[
{ {
id: shortcodes[0].id, id: shortcodes[1].id,
request: JSON.stringify(shortcodes[0].request), request: JSON.stringify(shortcodes[1].request),
createdOn: shortcodes[0].createdOn, createdOn: shortcodes[1].createdOn,
}, },
]); ]);
}); });
test('returns an empty array for an invalid cursor', async () => { test('should return an empty array for an invalid cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([]); mockPrisma.shortcode.findMany.mockResolvedValue([]);
const result = await shortcodeService.fetchUserShortCodes( const result = await shortcodeService.fetchUserShortCodes('testuser', {
'testuser', cursor: 'invalidcursor',
'invalidcursor', take: 10,
)(); });
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
}); });
test('returns an empty array for an invalid user id and null cursor', async () => { test('should return an empty array for an invalid user id and null cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([]); mockPrisma.shortcode.findMany.mockResolvedValue([]);
const result = await shortcodeService.fetchUserShortCodes( const result = await shortcodeService.fetchUserShortCodes('invalidid', {
'invalidid', cursor: null,
null, take: 10,
)(); });
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
}); });
test('returns an empty array for an invalid user id and an invalid cursor', async () => { test('should return an empty array for an invalid user id and an invalid cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([]); mockPrisma.shortcode.findMany.mockResolvedValue([]);
const result = await shortcodeService.fetchUserShortCodes( const result = await shortcodeService.fetchUserShortCodes('invalidid', {
'invalidid', cursor: 'invalidcursor',
'invalidcursor', take: 10,
)(); });
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
}); });
}); });
// TODO: Implement revoke shortcode and user shortcode deletion describe('createShortcode', () => {
// describe('revokeShortCode', () => { test('should throw SHORTCODE_INVALID_JSON error if incoming request data is invalid', async () => {
// test('returns details of deleted shortcode, when user uid and shortcode is valid', async () => { const result = await shortcodeService.createShortcode(
// const shortcode = { 'invalidRequest',
// id: 'blablablabla', 'user_uid_1',
// createdOn: new Date(), );
// request: { expect(result).toEqualLeft(SHORTCODE_INVALID_JSON);
// hello: 'there', });
// },
// creatorUid: 'testuser', test('should successfully create a new shortcode with valid user uid', async () => {
// }; // generateUniqueShortCodeID --> getShortCode
// mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
// mockPrisma.shortcode.delete.mockResolvedValueOnce(shortcode); 'NotFoundError',
// );
// const result = await shortcodeService.revokeShortCode( mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
// shortcode.id,
// shortcode.creatorUid, const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
// )(); expect(result).toEqualRight({
// id: shortCodeWithUser.id,
// expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({ createdOn: shortCodeWithUser.createdOn,
// where: { request: JSON.stringify(shortCodeWithUser.request),
// creator_uid_shortcode_unique: { });
// creatorUid: shortcode.creatorUid, });
// id: shortcode.id,
// }, test('should successfully create a new shortcode with null user uid', async () => {
// }, // generateUniqueShortCodeID --> getShortCode
// }); mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
// 'NotFoundError',
// expect(result).toEqualRight(<Shortcode>{ );
// id: shortcode.id, mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
// request: JSON.stringify(shortcode.request),
// createdOn: shortcode.createdOn, const result = await shortcodeService.createShortcode('{}', null);
// }); expect(result).toEqualRight({
// }); id: shortCodeWithUser.id,
// createdOn: shortCodeWithUser.createdOn,
// test('returns SHORTCODE_NOT_FOUND error when shortcode is invalid and user uid is valid', async () => { request: JSON.stringify(shortCodeWithOutUser.request),
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); });
// expect( });
// shortcodeService.revokeShortCode('invalid', 'testuser')(),
// ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); test('should send pubsub message to `shortcode/{uid}/created` on successful creation of shortcode', async () => {
// }); // generateUniqueShortCodeID --> getShortCode
// mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
// test('returns SHORTCODE_NOT_FOUND error when shortcode is valid and user uid is invalid', async () => { 'NotFoundError',
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); );
// expect( mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
// shortcodeService.revokeShortCode('blablablabla', 'invalidUser')(),
// ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
// }); expect(mockPubSub.publish).toHaveBeenCalledWith(
// `shortcode/${shortCodeWithUser.creatorUid}/created`,
// test('returns SHORTCODE_NOT_FOUND error when both shortcode and user uid are invalid', async () => { {
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); id: shortCodeWithUser.id,
// expect( createdOn: shortCodeWithUser.createdOn,
// shortcodeService.revokeShortCode('invalid', 'invalid')(), request: JSON.stringify(shortCodeWithUser.request),
// ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); },
// }); );
// });
// test('if creator is specified in the deleted shortcode, pubsub message is sent to `shortcode/{uid}/revoked`', async () => { });
// const shortcode = {
// id: 'blablablabla', describe('revokeShortCode', () => {
// createdOn: new Date(), test('should return true on successful deletion of shortcode with valid inputs', async () => {
// request: { mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
// hello: 'there',
// }, const result = await shortcodeService.revokeShortCode(
// creatorUid: 'testuser', shortCodeWithUser.id,
// }; shortCodeWithUser.creatorUid,
// );
// mockPrisma.shortcode.delete.mockResolvedValueOnce(shortcode);
// expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({
// const result = await shortcodeService.revokeShortCode( where: {
// shortcode.id, creator_uid_shortcode_unique: {
// shortcode.creatorUid, creatorUid: shortCodeWithUser.creatorUid,
// )(); id: shortCodeWithUser.id,
// },
// expect(result).toBeRight(); },
// expect(mockPubSub.publish).toHaveBeenCalledWith( });
// `shortcode/testuser/revoked`,
// { ...(result as any).right }, expect(result).toEqualRight(true);
// ); });
// });
// }); test('should return SHORTCODE_NOT_FOUND error when shortcode is invalid and user uid is valid', async () => {
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
// describe('deleteUserShortcodes', () => { expect(
// test('should return undefined when the user uid is valid and contains shortcodes data', async () => { shortcodeService.revokeShortCode('invalid', 'testuser'),
// const testUserUID = 'testuser1'; ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
// const shortcodesList = [ });
// {
// id: 'blablablabla', test('should return SHORTCODE_NOT_FOUND error when shortcode is valid and user uid is invalid', async () => {
// createdOn: new Date(), mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
// request: { expect(
// hello: 'there', shortcodeService.revokeShortCode('blablablabla', 'invalidUser'),
// }, ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
// creatorUid: testUserUID, });
// },
// ]; test('should return SHORTCODE_NOT_FOUND error when both shortcode and user uid are invalid', async () => {
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
// mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodesList); expect(
// mockPrisma.shortcode.delete.mockResolvedValueOnce(shortcodesList[0]); shortcodeService.revokeShortCode('invalid', 'invalid'),
// ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
// const result = await shortcodeService.deleteUserShortcodes(testUserUID)(); });
//
// expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({ test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of shortcode', async () => {
// where: { mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
// creatorUid: testUserUID,
// }, const result = await shortcodeService.revokeShortCode(
// }); shortCodeWithUser.id,
// shortCodeWithUser.creatorUid,
// expect(result).toBeUndefined(); );
// });
// expect(mockPubSub.publish).toHaveBeenCalledWith(
// test('should return undefined when user uid is valid but user has no shortcode data', async () => { `shortcode/${shortCodeWithUser.creatorUid}/revoked`,
// const testUserUID = 'testuser1'; {
// const shortcodesList = []; id: shortCodeWithUser.id,
// createdOn: shortCodeWithUser.createdOn,
// mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodesList); request: JSON.stringify(shortCodeWithUser.request),
// },
// const result = await shortcodeService.deleteUserShortcodes(testUserUID)(); );
// });
// expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({ });
// where: {
// creatorUid: testUserUID, describe('deleteUserShortCodes', () => {
// }, test('should successfully delete all users shortcodes with valid user uid', async () => {
// }); mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 });
//
// expect(result).toBeUndefined(); const result = await shortcodeService.deleteUserShortCodes(
// }); shortCodeWithUser.creatorUid,
// );
// test('should return undefined when the user uid is invalid', async () => { expect(result).toEqual(1);
// const testUserUID = 'invalidtestuser'; });
// const shortcodesList = [];
// test('should return 0 when user uid is invalid', async () => {
// mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodesList); mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 });
// const result = await shortcodeService.deleteUserShortcodes(testUserUID)();
// const result = await shortcodeService.deleteUserShortCodes(
// expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({ shortCodeWithUser.creatorUid,
// where: { );
// creatorUid: testUserUID, expect(result).toEqual(0);
// }, });
// }); });
//
// expect(result).toBeUndefined();
// });
// });
}); });

View File

@@ -1,18 +1,22 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { flow, pipe } from 'fp-ts/function';
import * as T from 'fp-ts/Task'; import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as TO from 'fp-ts/TaskOption'; import * as TO from 'fp-ts/TaskOption';
import * as A from 'fp-ts/Array'; import * as E from 'fp-ts/Either';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { SHORTCODE_NOT_FOUND } from 'src/errors'; import {
SHORTCODE_ALREADY_EXISTS,
SHORTCODE_INVALID_JSON,
SHORTCODE_NOT_FOUND,
} from 'src/errors';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { UserDataHandler } from 'src/user/user.data.handler'; import { UserDataHandler } from 'src/user/user.data.handler';
import { Shortcode } from './shortcode.model'; import { Shortcode } from './shortcode.model';
import { Shortcode as DBShortCode } from '@prisma/client';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import { stringToJson } from 'src/utils';
import { PaginationArgs } from 'src/types/input-types.args';
const SHORT_CODE_LENGTH = 12; const SHORT_CODE_LENGTH = 12;
const SHORT_CODE_CHARS = const SHORT_CODE_CHARS =
@@ -39,7 +43,26 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
return undefined; return undefined;
} }
private generateShortcodeID(): string { /**
* Converts a Prisma Shortcode type into the Shortcode model
*
* @param shortcodeInfo Prisma Shortcode type
* @returns GQL Shortcode
*/
private returnShortCode(shortcodeInfo: DBShortCode): Shortcode {
return <Shortcode>{
id: shortcodeInfo.id,
request: JSON.stringify(shortcodeInfo.request),
createdOn: shortcodeInfo.createdOn,
};
}
/**
* Generate a shortcode
*
* @returns generated shortcode
*/
private generateShortCodeID(): string {
let result = ''; let result = '';
for (let i = 0; i < SHORT_CODE_LENGTH; i++) { for (let i = 0; i < SHORT_CODE_LENGTH; i++) {
result += result +=
@@ -48,189 +71,142 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
return result; return result;
} }
private async generateUniqueShortcodeID(): Promise<string> { /**
* Check to see if ShortCode is already present in DB
*
* @returns Shortcode
*/
private async generateUniqueShortCodeID() {
while (true) { while (true) {
const code = this.generateShortcodeID(); const code = this.generateShortCodeID();
const data = await this.resolveShortcode(code)(); const data = await this.getShortCode(code);
if (O.isNone(data)) return code; if (E.isLeft(data)) return E.right(code);
} }
} }
resolveShortcode(shortcode: string): TO.TaskOption<Shortcode> { /**
return pipe( * Fetch details regarding a ShortCode
// The task to perform *
() => this.prisma.shortcode.findFirst({ where: { id: shortcode } }), * @param shortcode ShortCode
TO.fromTask, // Convert to Task to TaskOption * @returns Either of ShortCode details or error
TO.chain(TO.fromNullable), // Remove nullability */
TO.map((data) => { async getShortCode(shortcode: string) {
return <Shortcode>{ try {
id: data.id, const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({
request: JSON.stringify(data.request), where: { id: shortcode },
createdOn: data.createdOn, });
}; return E.right(this.returnShortCode(shortcodeInfo));
}), } catch (error) {
); return E.left(SHORTCODE_NOT_FOUND);
}
} }
// TODO: Implement create shortcode and the user service method /**
// createShortcode(request: any, creator?: User): T.Task<Shortcode> { * Create a new ShortCode
// return pipe( *
// T.Do, * @param request JSON string of request details
// * @param userUID user UID, if present
// // Get shortcode * @returns Either of ShortCode or error
// T.bind('shortcode', () => () => this.generateUniqueShortcodeID()), */
// async createShortcode(request: string, userUID: string | null) {
// // Create const shortcodeData = stringToJson(request);
// T.chain( if (E.isLeft(shortcodeData)) return E.left(SHORTCODE_INVALID_JSON);
// ({ shortcode }) =>
// () =>
// this.prisma.shortcode.create({
// data: {
// id: shortcode,
// request: request,
// creatorUid: creator?.uid,
// },
// }),
// ),
//
// T.chainFirst((shortcode) => async () => {
// // Only publish event if creator is not null
// if (shortcode.creatorUid) {
// this.pubsub.publish(`shortcode/${shortcode.creatorUid}/created`, <
// Shortcode
// >{
// id: shortcode.id,
// request: JSON.stringify(shortcode.request),
// createdOn: shortcode.createdOn,
// });
// }
// }),
//
// // Map to valid return type
// T.map(
// (data) =>
// <Shortcode>{
// id: data.id,
// request: JSON.stringify(data.request),
// createdOn: data.createdOn,
// },
// ),
// );
// }
fetchUserShortCodes(uid: string, cursor: string | null) { const user = await this.userService.findUserById(userUID);
return pipe(
cursor, const generatedShortCode = await this.generateUniqueShortCodeID();
O.fromNullable, if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left);
O.fold(
() => const createdShortCode = await this.prisma.shortcode.create({
pipe( data: {
() => id: generatedShortCode.right,
this.prisma.shortcode.findMany({ request: shortcodeData.right,
take: 10, creatorUid: O.isNone(user) ? null : user.value.uid,
where: { },
creatorUid: uid, });
},
orderBy: { // Only publish event if creator is not null
createdOn: 'desc', if (createdShortCode.creatorUid) {
}, this.pubsub.publish(
}), `shortcode/${createdShortCode.creatorUid}/created`,
T.map((codes) => this.returnShortCode(createdShortCode),
codes.map( );
(data) => }
<Shortcode>{
id: data.id, return E.right(this.returnShortCode(createdShortCode));
request: JSON.stringify(data.request),
createdOn: data.createdOn,
},
),
),
),
(cursor) =>
pipe(
() =>
this.prisma.shortcode.findMany({
take: 10,
skip: 1,
cursor: {
id: cursor,
},
where: {
creatorUid: uid,
},
orderBy: {
createdOn: 'desc',
},
}),
T.map((codes) =>
codes.map(
(data) =>
<Shortcode>{
id: data.id,
request: JSON.stringify(data.request),
createdOn: data.createdOn,
},
),
),
),
),
);
} }
// TODO: Implement revoke shortcode and user shortcode deletion feature /**
// revokeShortCode(shortcode: string, uid: string) { * Fetch ShortCodes created by a User
// return pipe( *
// TE.tryCatch( * @param uid User Uid
// () => * @param args Pagination arguments
// this.prisma.shortcode.delete({ * @returns Array of ShortCodes
// where: { */
// creator_uid_shortcode_unique: { async fetchUserShortCodes(uid: string, args: PaginationArgs) {
// creatorUid: uid, const shortCodes = await this.prisma.shortcode.findMany({
// id: shortcode, where: {
// }, creatorUid: uid,
// }, },
// }), orderBy: {
// () => SHORTCODE_NOT_FOUND, createdOn: 'desc',
// ), },
// TE.chainFirst((shortcode) => skip: 1,
// TE.fromTask(() => take: args.take,
// this.pubsub.publish(`shortcode/${shortcode.creatorUid}/revoked`, < cursor: args.cursor ? { id: args.cursor } : undefined,
// Shortcode });
// >{
// id: shortcode.id,
// request: JSON.stringify(shortcode.request),
// createdOn: shortcode.createdOn,
// }),
// ),
// ),
// TE.map(
// (data) =>
// <Shortcode>{
// id: data.id,
// request: JSON.stringify(data.request),
// createdOn: data.createdOn,
// },
// ),
// );
// }
// deleteUserShortcodes(uid: string) { const fetchedShortCodes: Shortcode[] = shortCodes.map((code) =>
// return pipe( this.returnShortCode(code),
// () => );
// this.prisma.shortcode.findMany({
// where: { return fetchedShortCodes;
// creatorUid: uid, }
// },
// }), /**
// T.chain( * Delete a ShortCode
// flow( *
// A.map((shortcode) => this.revokeShortCode(shortcode.id, uid)), * @param shortcode ShortCode
// T.sequenceArray, * @param uid User Uid
// ), * @returns Boolean on successful deletion
// ), */
// T.map(() => undefined), async revokeShortCode(shortcode: string, uid: string) {
// ); try {
// } const deletedShortCodes = await this.prisma.shortcode.delete({
where: {
creator_uid_shortcode_unique: {
creatorUid: uid,
id: shortcode,
},
},
});
this.pubsub.publish(
`shortcode/${deletedShortCodes.creatorUid}/revoked`,
this.returnShortCode(deletedShortCodes),
);
return E.right(true);
} catch (error) {
return E.left(SHORTCODE_NOT_FOUND);
}
}
/**
* Delete all of Users ShortCodes
*
* @param uid User Uid
* @returns number of all deleted user ShortCodes
*/
async deleteUserShortCodes(uid: string) {
const deletedShortCodes = await this.prisma.shortcode.deleteMany({
where: {
creatorUid: uid,
},
});
return deletedShortCodes.count;
}
} }