Skip to content

Commit

Permalink
add graphql implementation for organization membership
Browse files Browse the repository at this point in the history
  • Loading branch information
xoldd committed Dec 1, 2024
1 parent 2b93eb6 commit 2e67972
Show file tree
Hide file tree
Showing 10 changed files with 807 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/graphql/enums/OrganizationMembershipRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { organizationMembershipRoleEnum } from "~/src/drizzle/enums/organizationMembershipRole";
import { builder } from "~/src/graphql/builder";

export const OrganizationMembershipRole = builder.enumType(
"OrganizationMembershipRole",
{
description: "",
values: organizationMembershipRoleEnum.enumValues,
},
);
1 change: 1 addition & 0 deletions src/graphql/enums/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./Iso3166Alpha2CountryCode";
import "./OrganizationMembershipRole";
import "./UserEducationGrade";
import "./UserEmploymentStatus";
import "./UserMaritalStatus";
Expand Down
36 changes: 36 additions & 0 deletions src/graphql/inputs/MutationCreateOrganizationMembershipInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { z } from "zod";
import { organizationMembershipsTableInsertSchema } from "~/src/drizzle/tables/organizationMemberships";
import { builder } from "~/src/graphql/builder";
import { OrganizationMembershipRole } from "~/src/graphql/enums/OrganizationMembershipRole";

export const mutationCreateOrganizationMembershipInputSchema =
organizationMembershipsTableInsertSchema
.pick({
memberId: true,
organizationId: true,
})
.extend({
role: organizationMembershipsTableInsertSchema.shape.role.optional(),
});

export const MutationCreateOrganizationMembershipInput = builder
.inputRef<z.infer<typeof mutationCreateOrganizationMembershipInputSchema>>(
"MutationCreateOrganizationMembershipInput",
)
.implement({
description: "",
fields: (t) => ({
memberId: t.id({
description: "Global identifier of the associated user.",
required: true,
}),
organizationId: t.id({
description: "Global identifier of the associated organization.",
required: true,
}),
role: t.field({
description: "Role assigned to the user within the organization.",
type: OrganizationMembershipRole,
}),
}),
});
27 changes: 27 additions & 0 deletions src/graphql/inputs/MutationDeleteOrganizationMembershipInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { z } from "zod";
import { organizationMembershipsTableInsertSchema } from "~/src/drizzle/tables/organizationMemberships";
import { builder } from "~/src/graphql/builder";

export const mutationDeleteOrganizationMembershipInputSchema =
organizationMembershipsTableInsertSchema.pick({
memberId: true,
organizationId: true,
});

export const MutationDeleteOrganizationMembershipInput = builder
.inputRef<z.infer<typeof mutationDeleteOrganizationMembershipInputSchema>>(
"MutationDeleteOrganizationMembershipInput",
)
.implement({
description: "",
fields: (t) => ({
memberId: t.id({
description: "Global identifier of the associated user.",
required: true,
}),
organizationId: t.id({
description: "Global identifier of the associated organization.",
required: true,
}),
}),
});
43 changes: 43 additions & 0 deletions src/graphql/inputs/MutationUpdateOrganizationMembershipInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { z } from "zod";
import { organizationMembershipsTableInsertSchema } from "~/src/drizzle/tables/organizationMemberships";
import { builder } from "~/src/graphql/builder";
import { OrganizationMembershipRole } from "~/src/graphql/enums/OrganizationMembershipRole";

export const mutationUpdateOrganizationMembershipInputSchema =
organizationMembershipsTableInsertSchema
.pick({
memberId: true,
organizationId: true,
})
.extend({
role: organizationMembershipsTableInsertSchema.shape.role.optional(),
})
.refine(
({ memberId, organizationId, ...remainingArg }) =>
Object.values(remainingArg).some((value) => value !== undefined),
{
message: "At least one optional argument must be provided.",
},
);

export const MutationUpdateOrganizationMembershipInput = builder
.inputRef<z.infer<typeof mutationUpdateOrganizationMembershipInputSchema>>(
"MutationUpdateOrganizationMembershipInput",
)
.implement({
description: "",
fields: (t) => ({
memberId: t.id({
description: "Global identifier of the associated user.",
required: true,
}),
organizationId: t.id({
description: "Global identifier of the associated organization.",
required: true,
}),
role: t.field({
description: "Role assigned to the user within the organization.",
type: OrganizationMembershipRole,
}),
}),
});
3 changes: 3 additions & 0 deletions src/graphql/inputs/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import "./MutationCreateOrganizationInput";
import "./MutationCreateOrganizationMembershipInput";
import "./MutationCreateUserInput";
import "./MutationDeleteOrganizationInput";
import "./MutationDeleteOrganizationMembershipInput";
import "./MutationDeleteUserInput";
import "./MutationSignUpInput";
import "./MutationUpdateCurrentUserInput";
import "./MutationUpdateOrganizationInput";
import "./MutationUpdateOrganizationMembershipInput";
import "./MutationUpdateUserInput";
import "./QueryOrganizationInput";
import "./QuerySignInInput";
Expand Down
220 changes: 220 additions & 0 deletions src/graphql/types/Mutation/createOrganizationMembership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { z } from "zod";
import { organizationMembershipsTable } from "~/src/drizzle/tables/organizationMemberships";
import { builder } from "~/src/graphql/builder";
import {
MutationCreateOrganizationMembershipInput,
mutationCreateOrganizationMembershipInputSchema,
} from "~/src/graphql/inputs/MutationCreateOrganizationMembershipInput";
import { Organization } from "~/src/graphql/types/Organization/Organization";
import { getKeyPathsWithNonUndefinedValues } from "~/src/utilities/getKeyPathsWithNonUndefinedValues";
import { TalawaGraphQLError } from "~/src/utilities/talawaGraphQLError";

const mutationCreateOrganizationMembershipArgumentsSchema = z.object({
input: mutationCreateOrganizationMembershipInputSchema,
});

builder.mutationField("createOrganizationMembership", (t) =>
t.field({
args: {
input: t.arg({
description: "",
required: true,
type: MutationCreateOrganizationMembershipInput,
}),
},
description: "Mutation field to create an organization membership.",
resolve: async (_parent, args, ctx) => {
if (!ctx.currentClient.isAuthenticated) {
throw new TalawaGraphQLError({
extensions: {
code: "unauthenticated",
},
message: "Only authenticated users can perform this action.",
});
}

const {
data: parsedArgs,
error,
success,
} = mutationCreateOrganizationMembershipArgumentsSchema.safeParse(args);

if (!success) {
throw new TalawaGraphQLError({
extensions: {
code: "invalid_arguments",
issues: error.issues.map((issue) => ({
argumentPath: issue.path,
message: issue.message,
})),
},
message: "Invalid arguments provided.",
});
}

const currentUserId = ctx.currentClient.user.id;

const [
currentUser,
existingMember,
existingOrganization,
existingOrganizationMembership,
] = await Promise.all([
ctx.drizzleClient.query.usersTable.findFirst({
columns: {
role: true,
},
where: (fields, operators) => operators.eq(fields.id, currentUserId),
}),
ctx.drizzleClient.query.usersTable.findFirst({
columns: {
role: true,
},
where: (fields, operators) =>
operators.eq(fields.id, parsedArgs.input.memberId),
}),
ctx.drizzleClient.query.organizationsTable.findFirst({
where: (fields, operators) =>
operators.eq(fields.id, parsedArgs.input.organizationId),
}),
ctx.drizzleClient.query.organizationMembershipsTable.findFirst({
columns: {
role: true,
},
where: (fields, operators) =>
operators.and(
operators.eq(fields.memberId, parsedArgs.input.memberId),
operators.eq(
fields.organizationId,
parsedArgs.input.organizationId,
),
),
}),
]);

if (currentUser === undefined) {
throw new TalawaGraphQLError({
extensions: {
code: "unauthenticated",
},
message: "Only authenticated users can perform this action.",
});
}

if (existingOrganization === undefined && existingMember === undefined) {
throw new TalawaGraphQLError({
extensions: {
code: "arguments_associated_resources_not_found",
issues: [
{
argumentPath: ["input", "memberId"],
},
{
argumentPath: ["input", "organizationId"],
},
],
},
message: "No associated resources found for the provided arguments.",
});
}

if (existingMember === undefined) {
throw new TalawaGraphQLError({
extensions: {
code: "arguments_associated_resources_not_found",
issues: [
{
argumentPath: ["input", "memberId"],
},
],
},
message: "No associated resources found for the provided arguments.",
});
}

if (existingOrganization === undefined) {
throw new TalawaGraphQLError({
extensions: {
code: "arguments_associated_resources_not_found",
issues: [
{
argumentPath: ["input", "organizationId"],
},
],
},
message: "No associated resources found for the provided arguments.",
});
}

if (existingOrganizationMembership !== undefined) {
throw new TalawaGraphQLError({
extensions: {
code: "forbidden_action_on_arguments_associated_resources",
issues: [
{
argumentPath: ["input", "memberId"],
message:
"This user already has the membership of the associated organization.",
},
{
argumentPath: ["input", "organizationId"],
message: "This organization already has the associated member.",
},
],
},
message:
"This action is forbidden on the resources associated to the provided arguments.",
});
}

if (currentUser.role !== "administrator") {
const unauthorizedArgumentPaths = getKeyPathsWithNonUndefinedValues({
keyPaths: [["input", "role"]],
object: parsedArgs,
});

if (unauthorizedArgumentPaths.length !== 0) {
throw new TalawaGraphQLError({
extensions: {
code: "unauthorized_arguments",
issues: unauthorizedArgumentPaths.map((argumentPath) => ({
argumentPath,
})),
},
message:
"You are not authorized to perform this action with the provided arguments.",
});
}
}

const [createdOrganizationMembership] = await ctx.drizzleClient
.insert(organizationMembershipsTable)
.values({
creatorId: currentUserId,
memberId: parsedArgs.input.memberId,
organizationId: parsedArgs.input.organizationId,
role:
parsedArgs.input.role === undefined
? "regular"
: parsedArgs.input.role,
})
.returning();

// Inserted organization membership not being returned is an external defect unrelated to this code. It is very unlikely for this error to occur.
if (createdOrganizationMembership === undefined) {
ctx.log.error(
"Postgres insert operation unexpectedly returned an empty array instead of throwing an error.",
);
throw new TalawaGraphQLError({
extensions: {
code: "unexpected",
},
message: "Something went wrong. Please try again.",
});
}

return existingOrganization;
},
type: Organization,
}),
);
Loading

0 comments on commit 2e67972

Please sign in to comment.