Merge pull request #2 from hoppscotch/chore/backend-integration

chore: backend integration for existing modules and docker fix
This commit is contained in:
Ankit Sridhar
2022-12-09 11:59:49 +05:30
committed by GitHub
25 changed files with 1464 additions and 186 deletions

View File

@@ -0,0 +1 @@
./node_modules

View File

@@ -2,7 +2,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],

View File

@@ -1,4 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
}

View File

@@ -0,0 +1,23 @@
FROM node:lts
WORKDIR /usr/src/app
# # Install pnpm
RUN npm i -g pnpm
# Prisma bits
COPY prisma ./
RUN pnpx prisma generate
# # NPM package install
COPY . .
RUN pnpm i
EXPOSE 3170
EXPOSE 9229
ENV APP_PORT=${PORT}
ENV DB_URL=${DATABASE_URL}
ENV PRODUCTION=true
CMD ["pnpm", "run", "start"]

View File

@@ -0,0 +1,26 @@
version: '3.0'
services:
local:
build: .
environment:
- PRODUCTION=false
- DATABASE_URL=postgresql://postgres:testpass@dev-db:5432/hoppscotch?connect_timeout=300
- PORT=3000
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
- dev-db
ports:
- "3170:3000"
- "9229:9229"
dev-db:
image: postgres
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch

View File

@@ -21,9 +21,21 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/apollo": "^10.1.6",
"@nestjs/common": "^9.2.1",
"@nestjs/core": "^9.2.1",
"@nestjs/graphql": "^10.1.6",
"@nestjs/platform-express": "^9.2.1",
"@prisma/client": "^4.7.1",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1",
"fp-ts": "^2.13.1",
"graphql": "^15.5.0",
"graphql-query-complexity": "^0.12.0",
"graphql-redis-subscriptions": "^2.5.0",
"graphql-subscriptions": "^2.0.0",
"ioredis": "^5.2.4",
"prisma": "^4.7.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.6.0"

View File

@@ -0,0 +1,92 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"]
}
model Team {
id String @id @default(cuid())
name String
members TeamMember[]
TeamInvitation TeamInvitation[]
TeamCollection TeamCollection[]
TeamRequest TeamRequest[]
TeamEnvironment TeamEnvironment[]
}
model TeamMember {
id String @id @default(uuid()) // Membership ID
role TeamMemberRole
userUid String
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
@@unique([teamID, userUid])
}
model TeamInvitation {
id String @id @default(cuid())
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
creatorUid String
inviteeEmail String
inviteeRole TeamMemberRole
@@unique([teamID, inviteeEmail])
@@index([teamID])
}
model TeamCollection {
id String @id @default(cuid())
parentID String?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[]
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
}
model TeamRequest {
id String @id @default(cuid())
collectionID String
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
request Json
}
model Shortcode {
id String @id
request Json
creatorUid String?
createdOn DateTime @default(now())
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
}
model TeamEnvironment {
id String @id @default(cuid())
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
name String
variables Json
}
model User {
uid String @id @default(cuid())
displayName String?
email String?
photoURL String?
}
enum TeamMemberRole {
OWNER
VIEWER
EDITOR
}

View File

@@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@@ -1,10 +1,50 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { UserModule } from './user/user.module';
import { GQLComplexityPlugin } from './plugins/GQLComplexityPlugin';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
playground: process.env.PRODUCTION !== 'true',
debug: process.env.PRODUCTION !== 'true',
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql',
onConnect: (connectionParams: any) => {
return {
reqHeaders: Object.fromEntries(
Object.entries(connectionParams).map(([k, v]) => [
k.toLowerCase(),
v,
]),
),
};
},
},
},
context: async ({ req, connection }) => {
if (req) {
return { reqHeaders: req.headers };
} else {
return {
// Lowercase the keys
reqHeaders: Object.fromEntries(
Object.entries(connection.context).map(([k, v]) => [
k.toLowerCase(),
v,
]),
),
};
}
},
driver: ApolloDriver,
}),
UserModule,
],
providers: [GQLComplexityPlugin],
})
export class AppModule {}

View File

@@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,17 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '../user/user.model';
import { GqlExecutionContext } from '@nestjs/graphql';
export const GqlUser = createParamDecorator<any, any, User>(
(_data: any, context: ExecutionContext) => {
const { user } = GqlExecutionContext.create(context).getContext<{
user: User;
}>();
if (!user)
throw new Error(
'@GqlUser decorator use with null user. Make sure the resolve has the @GqlAuthGuard present.',
);
return user;
},
);

View File

@@ -0,0 +1,211 @@
export const INVALID_EMAIL = 'invalid/email' as const;
export const EMAIL_FAILED = 'email/failed' as const;
/**
* Token Authorization failed (Check 'Authorization' Header)
* (GqlAuthGuard)
*/
export const AUTH_FAIL = 'auth/fail';
/**
* Tried to delete an user data document from fb firestore but failed.
* (FirebaseService)
*/
export const USER_FB_DOCUMENT_DELETION_FAILED =
'fb/firebase_document_deletion_failed' as const;
/**
* Tried to do an action on a user where user is not found
*/
export const USER_NOT_FOUND = 'user/not_found' as const;
/**
* User deletion failure
* (UserService)
*/
export const USER_DELETION_FAILED = 'user/deletion_failed' as const;
/**
* User deletion failure error due to user being a team owner
* (UserService)
*/
export const USER_IS_OWNER = 'user/is_owner' as const;
/**
* Tried to perform action on a team which they are not a member of
* (GqlTeamMemberGuard)
*/
export const TEAM_MEMBER_NOT_FOUND = 'team/member_not_found' as const;
/**
* Tried to perform action on a team that doesn't accept their member role level
* (GqlTeamMemberGuard)
*/
export const TEAM_NOT_REQUIRED_ROLE = 'team/not_required_role' as const;
/**
* Team name validation failure
* (TeamService)
*/
export const TEAM_NAME_INVALID = 'team/name_invalid';
/**
* Couldn't find the sync data from the user
* (TeamCollectionService)
*/
export const TEAM_USER_NO_FB_SYNCDATA = 'team/user_no_fb_syncdata';
/**
* There was a problem resolving the firebase collection path
* (TeamCollectionService)
*/
export const TEAM_FB_COLL_PATH_RESOLVE_FAIL = 'team/fb_coll_path_resolve_fail';
/**
* Tried to update the team to a state it doesn't have any owners
* (TeamService)
*/
export const TEAM_ONLY_ONE_OWNER = 'team/only_one_owner';
/**
* Invalid or non-existent Team ID
* (TeamService)
*/
export const TEAM_INVALID_ID = 'team/invalid_id' as const;
/**
* Invalid or non-existent collection id
* (GqlCollectionTeamMemberGuard)
*/
export const TEAM_INVALID_COLL_ID = 'team/invalid_coll_id' as const;
/**
* Invalid team id or user id
* (TeamService)
*/
export const TEAM_INVALID_ID_OR_USER = 'team/invalid_id_or_user';
/**
* The provided title for the team collection is short (less than 3 characters)
* (TeamCollectionService)
*/
export const TEAM_COLL_SHORT_TITLE = 'team_coll/short_title';
/**
* The JSON used is not valid
* (TeamCollectionService)
*/
export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
/**
* Tried to perform action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard)
*/
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
/**
* Tried to operate on a request which does not exist
* (TeamRequestService)
*/
export const TEAM_REQ_NOT_FOUND = 'team_req/not_found' as const;
/**
* Invalid or non-existent collection id
* (TeamRequestService)
*/
export const TEAM_REQ_INVALID_TARGET_COLL_ID =
'team_req/invalid_target_id' as const;
/**
* Tried to perform action on a request when the user is not even member of the team
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
*/
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
export const TEAM_INVITE_MEMBER_HAS_INVITE =
'team_invite/member_has_invite' as const;
export const TEAM_INVITE_NO_INVITE_FOUND =
'team_invite/no_invite_found' as const;
export const TEAM_INVITE_ALREADY_MEMBER = 'team_invite/already_member' as const;
export const TEAM_INVITE_EMAIL_DO_NOT_MATCH =
'team_invite/email_do_not_match' as const;
export const TEAM_INVITE_NOT_VALID_VIEWER =
'team_invite/not_valid_viewer' as const;
export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
/**
* Invalid or non-existent TEAM ENVIRONMMENT ID
* (TeamEnvironmentsService)
*/
export const TEAM_ENVIRONMMENT_NOT_FOUND =
'team_environment/not_found' as const;
/**
* The user is not a member of the team of the given environment
* (GqlTeamEnvTeamGuard)
*/
export const TEAM_ENVIRONMENT_NOT_TEAM_MEMBER =
'team_environment/not_team_member' as const;
/*
|------------------------------------|
|Server errors that are actually bugs|
|------------------------------------|
*/
/**
* Couldn't find user data from the GraphQL context (Check if GqlAuthGuard is applied)
* (GqlTeamMemberGuard, GqlCollectionTeamMemberGuard)
*/
export const BUG_AUTH_NO_USER_CTX = 'bug/auth/auth_no_user_ctx' as const;
/**
* Couldn't find teamID parameter in the attached GraphQL operation. (Check if teamID is present)
* (GqlTeamMemberGuard, GQLEAAdminGuard, GqlCollectionTeamMemberGuard)
*/
export const BUG_TEAM_NO_TEAM_ID = 'bug/team/no_team_id';
/**
* Couldn't find RequireTeamRole decorator. (Check if it is applied)
* (GqlTeamMemberGuard)
*/
export const BUG_TEAM_NO_REQUIRE_TEAM_ROLE = 'bug/team/no_require_team_role';
/**
* Couldn't find 'collectionID' param to the attached GQL operation. (Check if exists)
* (GqlCollectionTeamMemberGuard)
*/
export const BUG_TEAM_COLL_NO_COLL_ID = 'bug/team_coll/no_coll_id';
/**
* Couldn't find 'requestID' param to the attached GQL operation. (Check if exists)
* (GqlRequestTeamMemberGuard)
*/
export const BUG_TEAM_REQ_NO_REQ_ID = 'bug/team_req/no_req_id';
export const BUG_TEAM_INVITE_NO_INVITE_ID =
'bug/team_invite/no_invite_id' as const;
/**
* Couldn't find RequireTeamRole decorator. (Check if it is applied)
* (GqlTeamEnvTeamGuard)
*/
export const BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES =
'bug/team_env/guard_no_require_roles' as const;
/**
* Couldn't find 'id' param to the operation. (Check if it is applied)
* (GqlTeamEnvTeamGuard)
*/
export const BUG_TEAM_ENV_GUARD_NO_ENV_ID =
'bug/team_env/guard_no_env_id' as const;

View File

@@ -0,0 +1,42 @@
import { CanActivate, Injectable, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { User } from '../user/user.model';
import { IncomingHttpHeaders } from 'http2';
import { AUTH_FAIL } from 'src/errors';
@Injectable()
export class GqlAuthGuard implements CanActivate {
// eslint-disable-next-line @typescript-eslint/no-empty-function
constructor() {}
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const ctx = GqlExecutionContext.create(context).getContext<{
reqHeaders: IncomingHttpHeaders;
user: User | null;
}>();
if (
ctx.reqHeaders.authorization &&
ctx.reqHeaders.authorization.startsWith('Bearer ')
) {
const idToken = ctx.reqHeaders.authorization.split(' ')[1];
const authUser: User = {
uid: 'aabb22ccdd',
displayName: 'exampleUser',
photoURL: 'http://example.com/avatar',
email: 'me@example.com',
};
ctx.user = authUser;
return true;
} else {
return false;
}
} catch (e) {
throw new Error(AUTH_FAIL);
}
}
}

View File

@@ -1,8 +1,33 @@
import { NestFactory } from '@nestjs/core';
import { json } from 'express';
import { AppModule } from './app.module';
async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`);
console.log(`Database: ${process.env.DATABASE_URL}`);
const app = await NestFactory.create(AppModule);
await app.listen(3000);
// Increase fil upload limit to 50MB
app.use(
json({
limit: '100mb',
}),
);
if (process.env.PRODUCTION === 'false') {
console.log('Enabling CORS with development settings');
app.enableCors({
origin: true,
});
} else {
console.log('Enabling CORS with production settings');
app.enableCors({
origin: true,
});
}
await app.listen(process.env.PORT || 3170);
}
bootstrap();

View File

@@ -0,0 +1,44 @@
import { GraphQLSchemaHost } from '@nestjs/graphql';
import {
ApolloServerPlugin,
GraphQLRequestListener,
} from 'apollo-server-plugin-base';
import { Plugin } from '@nestjs/apollo';
import { GraphQLError } from 'graphql';
import {
fieldExtensionsEstimator,
getComplexity,
simpleEstimator,
} from 'graphql-query-complexity';
const COMPLEXITY_LIMIT = 50;
@Plugin()
export class GQLComplexityPlugin implements ApolloServerPlugin {
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
async requestDidStart(): Promise<GraphQLRequestListener> {
const { schema } = this.gqlSchemaHost;
return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > COMPLEXITY_LIMIT) {
throw new GraphQLError(
`Query is too complex: ${complexity}. Maximum allowed complexity: ${COMPLEXITY_LIMIT}`,
);
}
console.log('Query Complexity:', complexity);
},
};
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common/decorators';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,19 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client/scripts/default-index';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
super();
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { PubSubService } from './pubsub.service';
@Module({
providers: [PubSubService],
exports: [PubSubService],
})
export class PubSubModule {}

View File

@@ -0,0 +1,76 @@
import { OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { default as Redis, RedisOptions } from 'ioredis';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { PubSub as LocalPubSub } from 'graphql-subscriptions';
/**
* RedisPubSub uses JSON parsing for back and forth conversion, which loses Date objects, hence this reviver brings them back
* This function implementation should function like the JSON.parse reviver function
* @param key The key being parsed
* @param value The value being parsed
* @returns The updated value for the key
*/
const dateReviver = (key: string, value: unknown) => {
const isISO8601Z =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/;
if (typeof value === 'string' && isISO8601Z.test(value)) {
const tempDateNumber = Date.parse(value);
if (!isNaN(tempDateNumber)) {
return new Date(tempDateNumber);
}
}
return value;
};
/*
* Figure out which PubSub to use (simple/local for dev and Redis for production)
* and expose it
*/
@Injectable()
export class PubSubService implements OnModuleInit {
private pubsub: RedisPubSub | LocalPubSub;
onModuleInit() {
if (process.env.PRODUCTION === 'false') {
console.log('Initialize PubSub in development mode');
this.pubsub = new LocalPubSub();
} else {
console.log('Initialize PubSub in production mode');
console.log(
`REDIS_IP: ${process.env.REDIS_IP}, REDIS_PORT: ${process.env.REDIS_PORT}`,
);
const redisOptions: RedisOptions = {
port: parseInt(process.env.REDIS_PORT || '6379'),
host: process.env.REDIS_IP,
retryStrategy: (times: number) => {
return Math.min(times * 50, 2000);
},
};
this.pubsub = new RedisPubSub({
publisher: new Redis(redisOptions),
subscriber: new Redis(redisOptions),
reviver: dateReviver,
});
}
}
asyncIterator<T>(
topic: string | string[],
options?: unknown,
): AsyncIterator<T> {
return this.pubsub.asyncIterator(topic, options);
}
async publish(topic: string, payload: any) {
await this.pubsub.publish(topic, payload);
}
}

View File

@@ -0,0 +1,27 @@
import { ObjectType, ID, Field } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(() => ID, {
description: 'Firebase UID of the user',
})
uid: string;
@Field({
nullable: true,
description: 'Displayed name of the user (if given)',
})
displayName?: string;
@Field({
nullable: true,
description: 'Email of the user (if given)',
})
email?: string;
@Field({
nullable: true,
description: 'URL to the profile photo of the user (if given)',
})
photoURL?: string;
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { UserResolver } from './user.resolver';
import { PubSubModule } from 'src/pubsub/pubsub.module';
@Module({
imports: [PubSubModule],
providers: [UserResolver],
exports: [],
})
export class UserModule {}

View File

@@ -0,0 +1,30 @@
import { Resolver, Query } from '@nestjs/graphql';
import { User } from './user.model';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../guards/gql-auth.guard';
import { GqlUser } from '../decorators/gql-user.decorator';
@Resolver(() => User)
export class UserResolver {
// TODO: remove the eslint-disable line below once dependencies are added to user.service file
// eslint-disable-next-line @typescript-eslint/no-empty-function
constructor() {}
@Query(() => User, {
description:
"Gives details of the user executing this query (pass Authorization 'Bearer' header)",
})
@UseGuards(GqlAuthGuard)
me(@GqlUser() user: User): User {
return user;
}
@Query(() => User, {
description:
"Gives details of the user executing this query (pass Authorization 'Bearer' header)",
})
@UseGuards(GqlAuthGuard)
me2(@GqlUser() user: User): User {
return user;
}
}

View File

@@ -0,0 +1,110 @@
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/Option';
import * as TE from 'fp-ts/TaskEither';
import * as T from 'fp-ts/Task';
import { User } from './user/user.model';
import * as A from 'fp-ts/Array';
/**
* A workaround to throw an exception in an expression.
* JS throw keyword creates a statement not an expression.
* This function allows throw to be used as an expression
* @param errMessage Message present in the error message
*/
export function throwErr(errMessage: string): never {
throw new Error(errMessage);
}
/**
* Prints the given value to log and returns the same value.
* Used for debugging functional pipelines.
* @param val The value to print
* @returns `val` itself
*/
export const trace = <T>(val: T) => {
console.log(val);
return val;
};
/**
* Similar to `trace` but allows for labels and also an
* optional transform function.
* @param name The label to given to the trace log (log outputs like this "<name>: <value>")
* @param transform An optional function to transform the log output value (useful for checking specific aspects or transforms (duh))
* @returns A function which takes a value, and is traced.
*/
export const namedTrace =
<T>(name: string, transform?: (val: T) => unknown) =>
(val: T) => {
console.log(`${name}:`, transform ? transform(val) : val);
return val;
};
/**
* Returns the list of required roles annotated on a GQL Operation
* @param reflector NestJS Reflector instance
* @param context NestJS Execution Context
* @returns An Option which contains the defined roles
*/
// export const getAnnotatedRequiredRoles = (
// reflector: Reflector,
// context: ExecutionContext,
// ) =>
// pipe(
// reflector.get<TeamMemberRole[]>('requiresTeamRole', context.getHandler()),
// O.fromNullable,
// );
/**
* Gets the user from the NestJS GQL Execution Context.
* Usually used within guards.
* @param ctx The Execution Context to use to get it
* @returns An Option of the user
*/
export const getUserFromGQLContext = (ctx: ExecutionContext) =>
pipe(
ctx,
GqlExecutionContext.create,
(ctx) => ctx.getContext<{ user?: User }>(),
({ user }) => user,
O.fromNullable,
);
/**
* Gets a GQL Argument in the defined operation.
* Usually used in guards.
* @param argName The name of the argument to get
* @param ctx The NestJS Execution Context to use to get it.
* @returns The argument value typed as `unknown`
*/
export const getGqlArg = <ArgName extends string>(
argName: ArgName,
ctx: ExecutionContext,
) =>
pipe(
ctx,
GqlExecutionContext.create,
(ctx) => ctx.getArgs<object>(),
// We are not sure if this thing will even exist
// We pass that worry to the caller
(args) => args[argName as any] as unknown,
);
/**
* Sequences an array of TaskEither values while maintaining an array of all the error values
* @param arr Array of TaskEithers
* @returns A TaskEither saying all the errors possible on the left or all the success values on the right
*/
export const taskEitherValidateArraySeq = <A, B>(
arr: TE.TaskEither<A, B>[],
): TE.TaskEither<A[], B[]> =>
pipe(
arr,
A.map(TE.mapLeft(A.of)),
A.sequence(
TE.getApplicativeTaskValidation(T.ApplicativeSeq, A.getMonoid<A>()),
),
);

771
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff