From 056a5df4e1410c48195d2c93e25a8ea194e0984b Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 9 Feb 2023 12:07:26 +0530 Subject: [PATCH] feat: bringing shortcodes from central to selfhost --- .../src/shortcode/shortcode.model.ts | 19 + .../src/shortcode/shortcode.module.ts | 14 + .../src/shortcode/shortcode.resolver.ts | 183 ++++++ .../src/shortcode/shortcode.service.spec.ts | 520 ++++++++++++++++++ .../src/shortcode/shortcode.service.ts | 236 ++++++++ 5 files changed, 972 insertions(+) create mode 100644 packages/hoppscotch-backend/src/shortcode/shortcode.model.ts create mode 100644 packages/hoppscotch-backend/src/shortcode/shortcode.module.ts create mode 100644 packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts create mode 100644 packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts create mode 100644 packages/hoppscotch-backend/src/shortcode/shortcode.service.ts diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts new file mode 100644 index 00000000..79ab0132 --- /dev/null +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts @@ -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; +} diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.module.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.module.ts new file mode 100644 index 00000000..58b6dd00 --- /dev/null +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.module.ts @@ -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 {} diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts new file mode 100644 index 00000000..ab04c3dc --- /dev/null +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts @@ -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 { + 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 { + 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 { + // 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 { + // 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 { + return this.pubsub.asyncIterator(`shortcode/${user.uid}/revoked`); + } +} diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts new file mode 100644 index 00000000..57455fd6 --- /dev/null +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts @@ -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(); + +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({ + 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({ + // 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([ + { + 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([ + { + 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({ + // 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(); + // }); + // }); +}); diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts new file mode 100644 index 00000000..36eff88f --- /dev/null +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts @@ -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 { + return TO.none; + } + + onUserDelete(user: User): T.Task { + // 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 { + while (true) { + const code = this.generateShortcodeID(); + + const data = await this.resolveShortcode(code)(); + + if (O.isNone(data)) return code; + } + } + + resolveShortcode(shortcode: string): TO.TaskOption { + 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 { + 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 { + // 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) => + // { + // 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) => + { + 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) => + { + 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) => + // { + // 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), + // ); + // } +}