Skip to content

Commit

Permalink
✨ feat(members): add by email (#553)
Browse files Browse the repository at this point in the history
* ✨ feat: add member by email

* ♻️ refactor: use union response for success and error

* chore: use member constructor in repository
  • Loading branch information
larwaa authored Mar 15, 2024
1 parent c63cc59 commit ab47070
Show file tree
Hide file tree
Showing 20 changed files with 617 additions and 133 deletions.
4 changes: 4 additions & 0 deletions codegen.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const config: CodegenConfig = {
documentMode: "string",
enumsAsTypes: true,
useTypeImports: true,
// We must explicitly call include this as we don't do so automatically
skipTypename: true,
},
presetConfig: {
useTypeImports: true,
Expand Down Expand Up @@ -78,6 +80,8 @@ const config: CodegenConfig = {
config: {
enumsAsTypes: true,
useTypeImports: true,
// We must explicitly call include this as we don't do so automatically
skipTypename: true,
},
presetConfig: {
/* Fragment masking is only useful for actual clients, and it's not relevant for testing */
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/modules/blob_storage/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ resource "azurerm_storage_account" "main" {
account_replication_type = var.account_replication_type
blob_properties {
cors_rule {
allowed_headers = ["x-ms-blob-content-type","x-ms-blob-type","x-ms-client-request-id","x-ms-version"]
allowed_headers = ["*"]
allowed_methods = ["GET", "DELETE", "PUT", "HEAD", "MERGE", "POST", "OPTIONS", "PUT", "PATCH"]
allowed_origins = ["http://localhost:3000", "localhost:3000"]
exposed_headers = ["x-ms-meta-*"]
Expand Down
19 changes: 17 additions & 2 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,33 @@ Please do not edit this file directly.
To regenerate this file, run `npm run generate:gql`
"""

enum AddMemberErrorCode {
ALREADY_MEMBER
USER_NOT_FOUND
}

type AddMemberErrorResponse {
code: AddMemberErrorCode!
message: String!
}

input AddMemberInput {
"""The email of the user ot add to the organization"""
email: String

"""The ID of the organization to add the user to"""
organizationId: ID!

"""The role of the user in the organization, defaults to Role.MEMBER"""
role: Role

"""The ID of the user to add to the organization"""
userId: ID!
userId: ID
}

type AddMemberResponse {
union AddMemberResponse = AddMemberErrorResponse | AddMemberSuccessResponse

type AddMemberSuccessResponse {
member: Member!
}

Expand Down
18 changes: 15 additions & 3 deletions src/domain/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,17 +198,27 @@ const DomainErrorType = {

type ErrorType = (typeof DomainErrorType)[keyof typeof DomainErrorType];

class DomainError<TErrorType extends ErrorType> extends Error {
class DomainError<
TErrorType extends ErrorType,
TName extends string,
> extends Error {
public code: ErrorCode;
public type: TErrorType;
public name: TName;

constructor(
message: string,
options: { code: ErrorCode; type: TErrorType; cause?: unknown },
options: {
code: ErrorCode;
type: TErrorType;
cause?: unknown;
name: TName;
},
) {
super(message, { cause: options.cause });
this.code = options.code;
this.type = options.type;
this.name = options.name;
Error.captureStackTrace(this);
}

Expand All @@ -223,7 +233,8 @@ class DomainError<TErrorType extends ErrorType> extends Error {
}

class InvalidArgumentErrorV2 extends DomainError<
typeof DomainErrorType.InvalidArgumentError
typeof DomainErrorType.InvalidArgumentError,
"InvalidArgumentError"
> {
public reason: Record<string, string[] | undefined> | undefined;

Expand All @@ -238,6 +249,7 @@ class InvalidArgumentErrorV2 extends DomainError<
super(message, {
code: errorCodes.ERR_BAD_USER_INPUT,
type: DomainErrorType.InvalidArgumentError,
name: "InvalidArgumentError",
...rest,
});
this.reason = reason;
Expand Down
44 changes: 24 additions & 20 deletions src/graphql/organizations/__tests__/unit/organizations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,17 @@ describe("OrganizationResolvers", () => {
mutation: graphql(`
mutation addMember2 {
addMember(data: { userId: "user", organizationId: "org" }) {
member {
id
organization {
id
members {
id
}
}
}
... on AddMemberSuccessResponse {
member {
id
organization {
id
members {
id
}
}
}
}
}
}
`),
Expand Down Expand Up @@ -245,18 +247,20 @@ describe("OrganizationResolvers", () => {
{
mutation: graphql(`
mutation addMember2 {
addMember(data: { userId: "user", organizationId: "org" }) {
member {
id
organization {
id
members {
id
}
addMember(data: { userId: "user", organizationId: "org" }) {
... on AddMemberSuccessResponse {
member {
id
organization {
id
members {
id
}
}
}
}
}
}
}
}
}
`),
},
{
Expand Down
19 changes: 19 additions & 0 deletions src/graphql/organizations/resolvers/AddMemberErrorResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AddMemberErrorResponseResolvers } from "./../../types.generated.js";
export const AddMemberErrorResponse: AddMemberErrorResponseResolvers = {
code: ({ error }) => {
switch (error.name) {
case "InvalidArgumentError":
return "ALREADY_MEMBER";
case "NotFoundError":
return "USER_NOT_FOUND";
}
},
message: ({ error }) => {
switch (error.name) {
case "InvalidArgumentError":
return "User is already a member of the organization";
case "NotFoundError":
return "User not found";
}
},
};
4 changes: 0 additions & 4 deletions src/graphql/organizations/resolvers/AddMemberResponse.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import type { AddMemberSuccessResponseResolvers } from "./../../types.generated.js";
export const AddMemberSuccessResponse: AddMemberSuccessResponseResolvers = {};
56 changes: 43 additions & 13 deletions src/graphql/organizations/resolvers/Mutation/addMember.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,53 @@
import { ApolloServerErrorCode } from "@apollo/server/errors";
import { GraphQLError } from "graphql";
import { OrganizationRole } from "~/domain/organizations.js";
import type { MutationResolvers } from "./../../../types.generated.js";
export const addMember: NonNullable<MutationResolvers["addMember"]> = async (
_parent,
{ data },
ctx,
) => {
const { userId: memberId, organizationId, role } = data;
const addMemberResult = await ctx.organizations.members.addMember(ctx, {
userId: memberId,
organizationId,
role: role ?? OrganizationRole.MEMBER,
});
if (!addMemberResult.ok) {
switch (addMemberResult.error.name) {
case "PermissionDeniedError":
case "UnauthorizedError":
throw addMemberResult.error;
const { userId, email, organizationId, role } = data;
let addMemberResult: Awaited<
ReturnType<typeof ctx.organizations.members.addMember>
>;
if (userId) {
addMemberResult = await ctx.organizations.members.addMember(ctx, {
userId,
organizationId,
role: role ?? OrganizationRole.MEMBER,
});
} else if (email) {
addMemberResult = await ctx.organizations.members.addMember(ctx, {
email,
organizationId,
role: role ?? OrganizationRole.MEMBER,
});
} else {
throw new GraphQLError("You must provide either a userId or an email.", {
extensions: {
code: ApolloServerErrorCode.BAD_USER_INPUT,
},
});
}

if (addMemberResult.ok) {
return {
__typename: "AddMemberSuccessResponse",
member: addMemberResult.data.member,
};
}

switch (addMemberResult.error.name) {
case "InvalidArgumentError":
case "NotFoundError": {
return {
__typename: "AddMemberErrorResponse",
error: addMemberResult.error,
};
}
default: {
throw addMemberResult.error;
}
}
const { member } = addMemberResult.data;
return { member };
};
Loading

0 comments on commit ab47070

Please sign in to comment.