Skip to content

Commit

Permalink
feat(api): implement CandidacyContestationCaducite module with resolv…
Browse files Browse the repository at this point in the history
…ers and security rules

- Added CandidacyContestationCaducite model and related GraphQL types for contestation handling.
- Introduced resolvers for creating and retrieving contestations linked to candidacies.
- Implemented security rules to ensure only owners can create contestations.
- Refactored existing resolvers to integrate new contestation logic.
  • Loading branch information
ThomasDos committed Dec 11, 2024
1 parent 728df55 commit 5cf5024
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type CandidacyContestationCaducite {
id: ID!
candidacyId: ID!
contestationSentAt: Timestamp!
contestationReason: String!
contestationDecision: CertificationAuthorityContestationDecision!
}

enum CertificationAuthorityContestationDecision {
DECISION_PENDING
CADUCITE_INVALIDATED
CADUCITE_CONFIRMED
}

type Candidacy {
candidacyContestationsCaducite: [CandidacyContestationCaducite]
}

type Mutation {
candidacy_contestation_caducite_create_contestation(
candidacyId: ID!
contestationReason: String!
readyForJuryEstimatedAt: Timestamp!
): CandidacyContestationCaducite
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { composeResolvers } from "@graphql-tools/resolvers-composition";

import { Candidacy } from "@prisma/client";
import { resolversSecurityMap } from "./candidacy-contestation-caducite.security";
import { CreateCandidacyContestationCaduciteInput } from "./candidacy-contestation-caducite.types";
import { createCandidacyContestationCaducite } from "./features/createCandidacyContestationCaducite";
import { getCandidacyContestationsCaduciteByCandidacyId } from "./features/getCandidacyContestationsCaduciteByCandidacyId";

const unsafeResolvers = {
Candidacy: {
candidacyContestationsCaducite: ({ id: candidacyId }: Candidacy) =>
getCandidacyContestationsCaduciteByCandidacyId({ candidacyId }),
},
Mutation: {
candidacy_contestation_caducite_create_contestation: (
_: unknown,
input: CreateCandidacyContestationCaduciteInput,
) => createCandidacyContestationCaducite(input),
},
};

export const candidacyContestationCaduciteResolvers = composeResolvers(
unsafeResolvers,
resolversSecurityMap,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
defaultSecurity,
isOwnerOfCandidacy,
} from "../../shared/security/presets";

export const resolversSecurityMap = {
// Sécurité par défaut
// cf https://the-guild.dev/graphql/tools/docs/resolvers-composition#supported-path-matcher-format
"Query.*": defaultSecurity,

"Mutation.*": defaultSecurity,

"Mutation.candidacy_contestation_caducite_create_contestation":
isOwnerOfCandidacy,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type CreateCandidacyContestationCaduciteInput = {
candidacyId: string;
contestationReason: string;
readyForJuryEstimatedAt: Date;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { CertificationAuthorityContestationDecision } from "@prisma/client";
import { isBefore, startOfToday } from "date-fns";
import { prismaClient } from "../../../../prisma/client";
import { CreateCandidacyContestationCaduciteInput } from "../candidacy-contestation-caducite.types";

export const createCandidacyContestationCaducite = async ({
candidacyId,
contestationReason,
readyForJuryEstimatedAt,
}: CreateCandidacyContestationCaduciteInput) => {
if (!contestationReason) {
throw new Error("La raison de la contestation est obligatoire");
}

if (isBefore(readyForJuryEstimatedAt, startOfToday())) {
throw new Error("La date prévisionnelle ne peut pas être dans le passé");
}

const candidacy = await prismaClient.candidacy.findUnique({
where: {
id: candidacyId,
},
include: { candidacyContestationCaducite: true },
});

if (!candidacy) {
throw new Error("La candidature n'a pas été trouvée");
}

const contestationPending = candidacy.candidacyContestationCaducite.find(
(contestation) =>
contestation.certificationAuthorityContestationDecision ===
CertificationAuthorityContestationDecision.DECISION_PENDING,
);

if (contestationPending) {
throw new Error(
"Une contestation est déjà en cours pour cette candidature",
);
}

await prismaClient.candidacy.update({
where: { id: candidacyId },
data: {
readyForJuryEstimatedAt,
},
});

return prismaClient.candidacyContestationCaducite.create({
data: {
candidacyId,
contestationReason,
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { prismaClient } from "../../../../prisma/client";

export const getCandidacyContestationsCaduciteByCandidacyId = async ({
candidacyId,
}: {
candidacyId: string;
}) =>
prismaClient.candidacyContestationCaducite.findMany({
where: {
candidacyId,
},
});
2 changes: 1 addition & 1 deletion packages/reva-api/modules/candidacy/candidacy.resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ const unsafeResolvers = {
},
};

export const resolvers = composeResolvers(
export const candidacyResolvers = composeResolvers(
unsafeResolvers,
resolversSecurityMap,
);
8 changes: 5 additions & 3 deletions packages/reva-api/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from "path";
import { loadFilesSync } from "@graphql-tools/load-files";
import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { NoSchemaIntrospectionCustomRule } from "graphql";
import {
TimestampResolver,
TimestampTypeDefinition,
Expand All @@ -12,15 +13,15 @@ import {
VoidTypeDefinition,
} from "graphql-scalars";
import mercurius, { MercuriusOptions } from "mercurius";
import { NoSchemaIntrospectionCustomRule } from "graphql";

import { GraphQLUpload } from "graphql-upload-minimal";
import { loaders as accountLoaders } from "./account/account.loaders";
import { resolvers as accountResolvers } from "./account/account.resolvers";
import { candidacyLogLoaders } from "./candidacy-log/candidacy-log.loaders";
import { candidacyLogResolvers } from "./candidacy-log/candidacy-log.resolvers";
import { candidacyMenuResolvers } from "./candidacy-menu/candidacy-menu.resolvers";
import * as candidacy from "./candidacy/candidacy.resolvers";
import { candidacyContestationCaduciteResolvers } from "./candidacy/candidacy-contestation-caducite/candidacy-contestation-caducite.resolvers";
import { candidacyResolvers } from "./candidacy/candidacy.resolvers";
import { certificationResolvers } from "./candidacy/certification/certification.resolvers";
import { trainingResolvers } from "./candidacy/training/training.resolvers";
import { candidateResolvers } from "./candidate/candidate.resolvers";
Expand Down Expand Up @@ -49,7 +50,7 @@ const typeDefs = loadFilesSync(
);

const resolvers = mergeResolvers([
candidacy.resolvers,
candidacyResolvers,
referentialResolvers,
accountResolvers,
candidateResolvers,
Expand All @@ -68,6 +69,7 @@ const resolvers = mergeResolvers([
feasibilityUploadedPdfResolvers,
trainingResolvers,
certificationResolvers,
candidacyContestationCaduciteResolvers,
]);
resolvers.Void = VoidResolver;
resolvers.Timestamp = TimestampResolver;
Expand Down

0 comments on commit 5cf5024

Please sign in to comment.