feat: bringing shortcodes from central to selfhost
This commit is contained in:
committed by
Balu Babu
parent
757d1add5b
commit
056a5df4e1
19
packages/hoppscotch-backend/src/shortcode/shortcode.model.ts
Normal file
19
packages/hoppscotch-backend/src/shortcode/shortcode.model.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class Shortcode {
|
||||
@Field(() => ID, {
|
||||
description: 'The shortcode. 12 digit alphanumeric.',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@Field({
|
||||
description: 'JSON string representing the request data',
|
||||
})
|
||||
request: string;
|
||||
|
||||
@Field({
|
||||
description: 'Timestamp of when the Shortcode was created',
|
||||
})
|
||||
createdOn: Date;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { ShortcodeResolver } from './shortcode.resolver';
|
||||
import { ShortcodeService } from './shortcode.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, UserModule, PubSubModule],
|
||||
providers: [ShortcodeService, ShortcodeResolver],
|
||||
exports: [ShortcodeService],
|
||||
})
|
||||
export class ShortcodeModule {}
|
||||
183
packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts
Normal file
183
packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
ID,
|
||||
Mutation,
|
||||
Query,
|
||||
Resolver,
|
||||
Subscription,
|
||||
} from '@nestjs/graphql';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
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 { Shortcode } from './shortcode.model';
|
||||
import { ShortcodeService } from './shortcode.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { SHORTCODE_INVALID_JSON } from 'src/errors';
|
||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
|
||||
@Resolver(() => Shortcode)
|
||||
export class ShortcodeResolver {
|
||||
constructor(
|
||||
private readonly shortcodeService: ShortcodeService,
|
||||
private readonly userService: UserService,
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
/* Queries */
|
||||
|
||||
@Query(() => Shortcode, {
|
||||
description: 'Resolves and returns a shortcode data',
|
||||
nullable: true,
|
||||
})
|
||||
shortcode(
|
||||
@Args({
|
||||
name: 'code',
|
||||
type: () => ID,
|
||||
description: 'The shortcode to resolve',
|
||||
})
|
||||
code: string,
|
||||
): Promise<Shortcode | null> {
|
||||
return pipe(
|
||||
this.shortcodeService.resolveShortcode(code),
|
||||
TO.getOrElseW(() => T.of(null)),
|
||||
)();
|
||||
}
|
||||
|
||||
@Query(() => [Shortcode], {
|
||||
description: 'List all shortcodes the current user has generated',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
myShortcodes(
|
||||
@GqlUser() user: User,
|
||||
@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 */
|
||||
|
||||
// TODO: Create a shortcode resolver pending implementation
|
||||
// @Mutation(() => Shortcode, {
|
||||
// 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, {
|
||||
// description: 'Revoke a user generated shortcode',
|
||||
// })
|
||||
// @UseGuards(GqlAuthGuard)
|
||||
// revokeShortcode(
|
||||
// @GqlUser() user: User,
|
||||
// @Args({
|
||||
// name: 'code',
|
||||
// type: () => ID,
|
||||
// description: 'The shortcode to resolve',
|
||||
// })
|
||||
// code: string,
|
||||
// ): Promise<boolean> {
|
||||
// return pipe(
|
||||
// this.shortcodeService.revokeShortCode(code, user.uid),
|
||||
// TE.map(() => true), // Just return true on success, no resource to return
|
||||
// TE.getOrElse(throwErr),
|
||||
// )();
|
||||
// }
|
||||
|
||||
/* Subscriptions */
|
||||
|
||||
@Subscription(() => Shortcode, {
|
||||
description: 'Listen for shortcode creation',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
myShortcodesCreated(@GqlUser() user: User) {
|
||||
return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`);
|
||||
}
|
||||
|
||||
@Subscription(() => Shortcode, {
|
||||
description: 'Listen for shortcode deletion',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
myShortcodesRevoked(@GqlUser() user: User): AsyncIterator<Shortcode> {
|
||||
return this.pubsub.asyncIterator(`shortcode/${user.uid}/revoked`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
import { SHORTCODE_NOT_FOUND } from 'src/errors';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { Shortcode } from './shortcode.model';
|
||||
import { ShortcodeService } from './shortcode.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
|
||||
const mockPubSub = {
|
||||
publish: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
const mockDocFunc = jest.fn();
|
||||
|
||||
const mockFB = {
|
||||
firestore: {
|
||||
doc: mockDocFunc,
|
||||
},
|
||||
};
|
||||
const mockUserService = new UserService(mockFB as any, mockPubSub as any);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const shortcodeService = new ShortcodeService(
|
||||
mockPrisma,
|
||||
mockPubSub as any,
|
||||
mockUserService,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(mockPrisma);
|
||||
mockPubSub.publish.mockClear();
|
||||
});
|
||||
|
||||
describe('ShortcodeService', () => {
|
||||
describe('resolveShortcode', () => {
|
||||
test('returns Some for a valid existent shortcode', () => {
|
||||
mockPrisma.shortcode.findFirst.mockResolvedValueOnce({
|
||||
id: 'blablablabla',
|
||||
createdOn: new Date(),
|
||||
request: {
|
||||
hello: 'there',
|
||||
},
|
||||
creatorUid: 'testuser',
|
||||
});
|
||||
|
||||
return expect(
|
||||
shortcodeService.resolveShortcode('blablablabla')(),
|
||||
).resolves.toBeSome();
|
||||
});
|
||||
|
||||
test('returns the correct info for a valid shortcode', () => {
|
||||
const shortcode = {
|
||||
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', () => {
|
||||
mockPrisma.shortcode.findFirst.mockResolvedValueOnce(null);
|
||||
|
||||
return expect(
|
||||
shortcodeService.resolveShortcode('blablablabla')(),
|
||||
).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', () => {
|
||||
test('returns all shortcodes for a user with no provided cursor', async () => {
|
||||
const 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(
|
||||
'testuser',
|
||||
null,
|
||||
)();
|
||||
|
||||
expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
where: {
|
||||
creatorUid: 'testuser',
|
||||
},
|
||||
orderBy: {
|
||||
createdOn: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(<Shortcode[]>[
|
||||
{
|
||||
id: shortcodes[0].id,
|
||||
request: JSON.stringify(shortcodes[0].request),
|
||||
createdOn: shortcodes[0].createdOn,
|
||||
},
|
||||
{
|
||||
id: shortcodes[1].id,
|
||||
request: JSON.stringify(shortcodes[1].request),
|
||||
createdOn: shortcodes[1].createdOn,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('return shortcodes for a user with a provided cursor', async () => {
|
||||
const shortcodes = [
|
||||
{
|
||||
id: 'blablabla1',
|
||||
request: {
|
||||
hello: 'there',
|
||||
},
|
||||
creatorUid: 'testuser',
|
||||
createdOn: new Date(),
|
||||
},
|
||||
];
|
||||
mockPrisma.shortcode.findMany.mockResolvedValue(shortcodes);
|
||||
|
||||
const result = await shortcodeService.fetchUserShortCodes(
|
||||
'testuser',
|
||||
'blablabla',
|
||||
)();
|
||||
|
||||
expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
skip: 1,
|
||||
cursor: {
|
||||
id: 'blablabla',
|
||||
},
|
||||
where: {
|
||||
creatorUid: 'testuser',
|
||||
},
|
||||
orderBy: {
|
||||
createdOn: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(<Shortcode[]>[
|
||||
{
|
||||
id: shortcodes[0].id,
|
||||
request: JSON.stringify(shortcodes[0].request),
|
||||
createdOn: shortcodes[0].createdOn,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns an empty array for an invalid cursor', async () => {
|
||||
mockPrisma.shortcode.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await shortcodeService.fetchUserShortCodes(
|
||||
'testuser',
|
||||
'invalidcursor',
|
||||
)();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('returns an empty array for an invalid user id and null cursor', async () => {
|
||||
mockPrisma.shortcode.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await shortcodeService.fetchUserShortCodes(
|
||||
'invalidid',
|
||||
null,
|
||||
)();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('returns an empty array for an invalid user id and an invalid cursor', async () => {
|
||||
mockPrisma.shortcode.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await shortcodeService.fetchUserShortCodes(
|
||||
'invalidid',
|
||||
'invalidcursor',
|
||||
)();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Implement revoke shortcode and user shortcode deletion
|
||||
// describe('revokeShortCode', () => {
|
||||
// test('returns details of deleted shortcode, when user uid and shortcode is valid', async () => {
|
||||
// const shortcode = {
|
||||
// id: 'blablablabla',
|
||||
// createdOn: new Date(),
|
||||
// request: {
|
||||
// hello: 'there',
|
||||
// },
|
||||
// creatorUid: 'testuser',
|
||||
// };
|
||||
//
|
||||
// mockPrisma.shortcode.delete.mockResolvedValueOnce(shortcode);
|
||||
//
|
||||
// const result = await shortcodeService.revokeShortCode(
|
||||
// shortcode.id,
|
||||
// shortcode.creatorUid,
|
||||
// )();
|
||||
//
|
||||
// expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({
|
||||
// where: {
|
||||
// creator_uid_shortcode_unique: {
|
||||
// creatorUid: shortcode.creatorUid,
|
||||
// id: shortcode.id,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// expect(result).toEqualRight(<Shortcode>{
|
||||
// id: shortcode.id,
|
||||
// request: JSON.stringify(shortcode.request),
|
||||
// createdOn: shortcode.createdOn,
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// test('returns SHORTCODE_NOT_FOUND error when shortcode is invalid and user uid is valid', async () => {
|
||||
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
||||
// expect(
|
||||
// shortcodeService.revokeShortCode('invalid', 'testuser')(),
|
||||
// ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
||||
// });
|
||||
//
|
||||
// test('returns SHORTCODE_NOT_FOUND error when shortcode is valid and user uid is invalid', async () => {
|
||||
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
||||
// expect(
|
||||
// shortcodeService.revokeShortCode('blablablabla', 'invalidUser')(),
|
||||
// ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
||||
// });
|
||||
//
|
||||
// test('returns SHORTCODE_NOT_FOUND error when both shortcode and user uid are invalid', async () => {
|
||||
// mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
||||
// expect(
|
||||
// shortcodeService.revokeShortCode('invalid', 'invalid')(),
|
||||
// ).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',
|
||||
// createdOn: new Date(),
|
||||
// request: {
|
||||
// hello: 'there',
|
||||
// },
|
||||
// creatorUid: 'testuser',
|
||||
// };
|
||||
//
|
||||
// mockPrisma.shortcode.delete.mockResolvedValueOnce(shortcode);
|
||||
//
|
||||
// const result = await shortcodeService.revokeShortCode(
|
||||
// shortcode.id,
|
||||
// shortcode.creatorUid,
|
||||
// )();
|
||||
//
|
||||
// expect(result).toBeRight();
|
||||
// expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
// `shortcode/testuser/revoked`,
|
||||
// { ...(result as any).right },
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// describe('deleteUserShortcodes', () => {
|
||||
// test('should return undefined when the user uid is valid and contains shortcodes data', async () => {
|
||||
// const testUserUID = 'testuser1';
|
||||
// const shortcodesList = [
|
||||
// {
|
||||
// id: 'blablablabla',
|
||||
// createdOn: new Date(),
|
||||
// request: {
|
||||
// hello: 'there',
|
||||
// },
|
||||
// creatorUid: testUserUID,
|
||||
// },
|
||||
// ];
|
||||
//
|
||||
// mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodesList);
|
||||
// mockPrisma.shortcode.delete.mockResolvedValueOnce(shortcodesList[0]);
|
||||
//
|
||||
// const result = await shortcodeService.deleteUserShortcodes(testUserUID)();
|
||||
//
|
||||
// expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({
|
||||
// where: {
|
||||
// creatorUid: testUserUID,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// expect(result).toBeUndefined();
|
||||
// });
|
||||
//
|
||||
// test('should return undefined when user uid is valid but user has no shortcode data', async () => {
|
||||
// const testUserUID = 'testuser1';
|
||||
// const shortcodesList = [];
|
||||
//
|
||||
// mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodesList);
|
||||
//
|
||||
// const result = await shortcodeService.deleteUserShortcodes(testUserUID)();
|
||||
//
|
||||
// expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({
|
||||
// where: {
|
||||
// creatorUid: testUserUID,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// expect(result).toBeUndefined();
|
||||
// });
|
||||
//
|
||||
// test('should return undefined when the user uid is invalid', async () => {
|
||||
// const testUserUID = 'invalidtestuser';
|
||||
// const shortcodesList = [];
|
||||
//
|
||||
// mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodesList);
|
||||
// const result = await shortcodeService.deleteUserShortcodes(testUserUID)();
|
||||
//
|
||||
// expect(mockPrisma.shortcode.findMany).toHaveBeenCalledWith({
|
||||
// where: {
|
||||
// creatorUid: testUserUID,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// expect(result).toBeUndefined();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
236
packages/hoppscotch-backend/src/shortcode/shortcode.service.ts
Normal file
236
packages/hoppscotch-backend/src/shortcode/shortcode.service.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { flow, pipe } from 'fp-ts/function';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as TO from 'fp-ts/TaskOption';
|
||||
import * as A from 'fp-ts/Array';
|
||||
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { SHORTCODE_NOT_FOUND } from 'src/errors';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { UserDataHandler } from 'src/user/user.data.handler';
|
||||
import { Shortcode } from './shortcode.model';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
const SHORT_CODE_LENGTH = 12;
|
||||
const SHORT_CODE_CHARS =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
|
||||
@Injectable()
|
||||
export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly pubsub: PubSubService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.userService.registerUserDataHandler(this);
|
||||
}
|
||||
|
||||
canAllowUserDeletion(user: User): TO.TaskOption<string> {
|
||||
return TO.none;
|
||||
}
|
||||
|
||||
onUserDelete(user: User): T.Task<void> {
|
||||
// return this.deleteUserShortcodes(user.uid);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private generateShortcodeID(): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < SHORT_CODE_LENGTH; i++) {
|
||||
result +=
|
||||
SHORT_CODE_CHARS[Math.floor(Math.random() * SHORT_CODE_CHARS.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async generateUniqueShortcodeID(): Promise<string> {
|
||||
while (true) {
|
||||
const code = this.generateShortcodeID();
|
||||
|
||||
const data = await this.resolveShortcode(code)();
|
||||
|
||||
if (O.isNone(data)) return code;
|
||||
}
|
||||
}
|
||||
|
||||
resolveShortcode(shortcode: string): TO.TaskOption<Shortcode> {
|
||||
return pipe(
|
||||
// The task to perform
|
||||
() => this.prisma.shortcode.findFirst({ where: { id: shortcode } }),
|
||||
TO.fromTask, // Convert to Task to TaskOption
|
||||
TO.chain(TO.fromNullable), // Remove nullability
|
||||
TO.map((data) => {
|
||||
return <Shortcode>{
|
||||
id: data.id,
|
||||
request: JSON.stringify(data.request),
|
||||
createdOn: data.createdOn,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Implement create shortcode and the user service method
|
||||
// createShortcode(request: any, creator?: User): T.Task<Shortcode> {
|
||||
// return pipe(
|
||||
// T.Do,
|
||||
//
|
||||
// // Get shortcode
|
||||
// T.bind('shortcode', () => () => this.generateUniqueShortcodeID()),
|
||||
//
|
||||
// // Create
|
||||
// T.chain(
|
||||
// ({ 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) {
|
||||
return pipe(
|
||||
cursor,
|
||||
O.fromNullable,
|
||||
O.fold(
|
||||
() =>
|
||||
pipe(
|
||||
() =>
|
||||
this.prisma.shortcode.findMany({
|
||||
take: 10,
|
||||
where: {
|
||||
creatorUid: uid,
|
||||
},
|
||||
orderBy: {
|
||||
createdOn: 'desc',
|
||||
},
|
||||
}),
|
||||
T.map((codes) =>
|
||||
codes.map(
|
||||
(data) =>
|
||||
<Shortcode>{
|
||||
id: data.id,
|
||||
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) {
|
||||
// return pipe(
|
||||
// TE.tryCatch(
|
||||
// () =>
|
||||
// this.prisma.shortcode.delete({
|
||||
// where: {
|
||||
// creator_uid_shortcode_unique: {
|
||||
// creatorUid: uid,
|
||||
// id: shortcode,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// () => SHORTCODE_NOT_FOUND,
|
||||
// ),
|
||||
// TE.chainFirst((shortcode) =>
|
||||
// TE.fromTask(() =>
|
||||
// this.pubsub.publish(`shortcode/${shortcode.creatorUid}/revoked`, <
|
||||
// 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) {
|
||||
// return pipe(
|
||||
// () =>
|
||||
// this.prisma.shortcode.findMany({
|
||||
// where: {
|
||||
// creatorUid: uid,
|
||||
// },
|
||||
// }),
|
||||
// T.chain(
|
||||
// flow(
|
||||
// A.map((shortcode) => this.revokeShortCode(shortcode.id, uid)),
|
||||
// T.sequenceArray,
|
||||
// ),
|
||||
// ),
|
||||
// T.map(() => undefined),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user