Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP model Convention with pagination and filters #3074

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ConventionId,
ConventionReadDto,
ConventionScope,
DataWithPagination,
DateRange,
FindSimilarConventionsParams,
SiretDto,
Expand All @@ -22,6 +23,7 @@ import {
ConventionQueries,
GetConventionsFilters,
GetConventionsParams,
GetPaginatedConventionsForAgencyUserParams,
} from "../ports/ConventionQueries";
import { InMemoryConventionRepository } from "./InMemoryConventionRepository";

Expand Down Expand Up @@ -119,6 +121,12 @@ export class InMemoryConventionQueries implements ConventionQueries {
});
}

public async getPaginatedConventionsForAgencyUser(
params: GetPaginatedConventionsForAgencyUserParams,
): Promise<DataWithPagination<ConventionDto>> {
throw new Error("Method not implemented.");
}

public async getConventionsByScope(params: {
scope: ConventionScope;
limit: number;
Expand Down
166 changes: 159 additions & 7 deletions back/src/domains/convention/adapters/PgConventionQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import {
ConventionReadDto,
ConventionScope,
ConventionStatus,
DataWithPagination,
DateFilter,
DateRange,
FindSimilarConventionsParams,
NotEmptyArray,
PaginationQueryParams,
SiretDto,
UserId,
conventionReadSchema,
conventionSchema,
pipeWithValue,
Expand All @@ -25,9 +30,12 @@ import {
GetConventionsFilters,
GetConventionsParams,
GetConventionsSortBy,
GetPaginatedConventionsFilters,
GetPaginatedConventionsSortBy,
} from "../ports/ConventionQueries";
import {
createConventionQueryBuilder,
createConventionQueryBuilderForAgencyUser,
getConventionAgencyFieldsForAgencies,
getReadConventionById,
} from "./pgConventionSql";
Expand Down Expand Up @@ -236,11 +244,157 @@ export class PgConventionQueries implements ConventionQueries {
);
});
}

public async getPaginatedConventionsForAgencyUser({
filters = {},
pagination,
sortBy,
agencyUserId,
}: {
agencyUserId: UserId;
pagination: Required<PaginationQueryParams>;
filters?: GetPaginatedConventionsFilters;
sortBy: GetPaginatedConventionsSortBy;
}): Promise<DataWithPagination<ConventionDto>> {
const {
actorEmailContains,
beneficiaryNameContains,
establishmentNameContains,
statuses,
agencyIds,
agencyDepartmentCodes,
dateStart,
dateEnd,
dateSubmission,
...rest
} = filters;

rest satisfies Record<string, never>;

const data = await pipeWithValue(
createConventionQueryBuilderForAgencyUser({
transaction: this.transaction,
agencyUserId,
}),
filterEmail(actorEmailContains?.trim()),
filterByBeneficiaryName(beneficiaryNameContains?.trim()),
filterEstablishmentName(establishmentNameContains?.trim()),
filterDate("date_start", dateStart),
filterDate("date_end", dateEnd),
filterDate("date_submission", dateSubmission),
filterInList("status", statuses),
filterInList("agency_id", agencyIds),
filterByAgencyDepartmentCodes(agencyDepartmentCodes),
sortConventions(sortBy),
(b) => b.limit(pagination.perPage),
).execute();

return {
data: data.map(({ dto }) => dto),
pagination: {
currentPage: 1,
totalPages: 1,
numberPerPage: 1,
totalRecords: 1,
},
};
}
}

type ConventionReadQueryBuilder = ReturnType<
typeof createConventionQueryBuilder
>;
type ConventionQueryBuilder = ReturnType<typeof createConventionQueryBuilder>;

const sortConventions =
(sortBy?: GetPaginatedConventionsSortBy) =>
(builder: ConventionQueryBuilder): ConventionQueryBuilder => {
const sortByByKey: Record<GetPaginatedConventionsSortBy, keyof Database["conventions"]> = {
dateSubmission: "date_submission",
dateStart: "date_start",
dateValidation: "date_validation",
};

if (!sortBy) return builder.orderBy(sortByByKey.dateStart, "desc");
return builder.orderBy(sortByByKey[sortBy], "desc");
};

const filterByAgencyDepartmentCodes =
(agencyDepartmentCodes: NotEmptyArray<string> | undefined) =>
(builder: ConventionQueryBuilder): ConventionQueryBuilder => {
if (!agencyDepartmentCodes) return builder;
return builder.where(
"agencies.department_code",
"in",
agencyDepartmentCodes,
);
};

const filterDate =
(
fieldName: keyof Database["conventions"],
dateFilter: DateFilter | undefined,
) =>
(builder: ConventionQueryBuilder): ConventionQueryBuilder => {
if (!dateFilter) return builder;
if (dateFilter.from && dateFilter.to)
return builder
.where(fieldName, ">=", dateFilter.from)
.where(fieldName, "<=", dateFilter.to);

if (dateFilter.from) return builder.where(fieldName, ">=", dateFilter.from);
if (dateFilter.to) return builder.where(fieldName, "<=", dateFilter.to);
return builder;
};

const filterEmail =
(emailContains: string | undefined) =>
(builder: ConventionQueryBuilder): ConventionQueryBuilder => {
if (!emailContains) return builder;
return builder.where((eb) =>
eb.or([
eb("b.email", "ilike", sql`%${emailContains}%`),
eb("br.email", "ilike", sql`%${emailContains}%`),
eb("bce.email", "ilike", sql`%${emailContains}%`),
eb("er.email", "ilike", sql`%${emailContains}%`),
eb("et.email", "ilike", sql`%${emailContains}%`),
]),
);
};

const filterEstablishmentName =
(establishmentNameContains: string | undefined) =>
(builder: ConventionQueryBuilder): ConventionQueryBuilder => {
if (!establishmentNameContains) return builder;
return builder.where(
"business_name",
"ilike",
`%${establishmentNameContains}%`,
);
};

const filterByBeneficiaryName =
(beneficiaryNameContains: string | undefined) =>
(builder: ConventionQueryBuilder): ConventionQueryBuilder => {
if (!beneficiaryNameContains) return builder;
const nameWords = beneficiaryNameContains.split(" ");
return builder.where((eb) =>
eb.or(
nameWords.flatMap((nameWord) => [
eb(eb.ref("b.first_name"), "ilike", `%${nameWord}%`),
eb(eb.ref("b.last_name"), "ilike", `%${nameWord}%`),
]),
),

);
};

const filterInList =
<T extends string>(
fieldName: keyof Database["conventions"],
list: T[] | undefined,
) =>
(builder: ConventionQueryBuilder): ConventionQueryBuilder => {
if (!list) return builder;
return builder.where(fieldName, "in", list);
};

const addFiltersToBuilder =
({
Expand All @@ -251,7 +405,7 @@ const addFiltersToBuilder =
dateSubmissionSince,
withSirets,
}: GetConventionsFilters) =>
(builder: ConventionReadQueryBuilder) => {
(builder: ConventionQueryBuilder) => {
const addWithStatusFilterIfNeeded: AddToBuilder = (b) =>
withStatuses && withStatuses.length > 0
? b.where("conventions.status", "in", withStatuses)
Expand Down Expand Up @@ -300,6 +454,4 @@ export const validateConventionResults = (
validateAndParseZodSchemaV2(conventionSchema, pgResult.dto, logger),
);

type AddToBuilder = (
b: ConventionReadQueryBuilder,
) => ConventionReadQueryBuilder;
type AddToBuilder = (b: ConventionQueryBuilder) => ConventionQueryBuilder;
86 changes: 74 additions & 12 deletions back/src/domains/convention/adapters/pgConventionSql.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { sql } from "kysely";
import { SelectQueryBuilder } from "kysely";
import {
AgencyId,
AgencyKind,
Expand All @@ -7,6 +8,7 @@ import {
AppellationLabel,
Beneficiary,
ConventionAgencyFields,
ConventionDto,
ConventionId,
ConventionReadDto,
DateString,
Expand All @@ -26,22 +28,23 @@ import {
jsonBuildObject,
jsonStripNulls,
} from "../../../config/pg/kysely/kyselyUtils";
import { Database } from "../../../config/pg/kysely/model/database";
import { createLogger } from "../../../utils/logger";
import { parseZodSchemaAndLogErrorOnParsingFailure } from "../../../utils/schema.utils";

export const createConventionQueryBuilder = (transaction: KyselyDb) => {
// biome-ignore format: reads better without formatting
const builder = transaction
.selectFrom("conventions")
.innerJoin("actors as b", "b.id", "conventions.beneficiary_id")
.innerJoin("actors as er", "er.id", "conventions.establishment_representative_id")
.innerJoin("actors as et", "et.id", "conventions.establishment_tutor_id")
.leftJoin("actors as br", "br.id", "conventions.beneficiary_representative_id")
.leftJoin("actors as bce", "bce.id", "conventions.beneficiary_current_employer_id")
.leftJoin("partners_pe_connect as p", "p.convention_id", "conventions.id")
.leftJoin("view_appellations_dto as vad", "vad.appellation_code", "conventions.immersion_appellation")
.leftJoin("agencies", "agencies.id", "conventions.agency_id")
// Common type for the query builder with proper return type
type ConventionQueryBuilder = SelectQueryBuilder<
any,
any,
{ dto: ConventionDto }
>;

// Function to create the common selection part with proper return type
const createConventionSelection = <
QB extends SelectQueryBuilder<any, any, any>,
>(
builder: QB,
): SelectQueryBuilder<Database, any, { dto: ConventionDto }> => {
return builder.select(({ ref, ...eb }) =>
jsonStripNulls(
jsonBuildObject({
Expand Down Expand Up @@ -240,6 +243,65 @@ export const createConventionQueryBuilder = (transaction: KyselyDb) => {
);
};

// Function to create the common joins for both query builders
const createCommonJoins = <QB extends SelectQueryBuilder<Database, any, any>>(
builder: QB,
): QB => {
return builder
.innerJoin("actors as b", "b.id", "conventions.beneficiary_id")
.innerJoin(
"actors as er",
"er.id",
"conventions.establishment_representative_id",
)
.innerJoin("actors as et", "et.id", "conventions.establishment_tutor_id")
.leftJoin(
"actors as br",
"br.id",
"conventions.beneficiary_representative_id",
)
.leftJoin(
"actors as bce",
"bce.id",
"conventions.beneficiary_current_employer_id",
)
.leftJoin("partners_pe_connect as p", "p.convention_id", "conventions.id")
.leftJoin(
"view_appellations_dto as vad",
"vad.appellation_code",
"conventions.immersion_appellation",
)
.leftJoin("agencies", "agencies.id", "conventions.agency_id") as QB;
};

export const createConventionQueryBuilder = (
transaction: KyselyDb,
): ConventionQueryBuilder => {
// biome-ignore format: reads better without formatting
const builder = transaction
.selectFrom("conventions");

const builderWithJoins = createCommonJoins(builder);
return createConventionSelection(builderWithJoins);
};

export const createConventionQueryBuilderForAgencyUser = ({
transaction,
agencyUserId,
}: {
transaction: KyselyDb;
agencyUserId: UserId;
}): ConventionQueryBuilder => {
// biome-ignore format: reads better without formatting
const builder = transaction
.selectFrom("users__agencies")
.innerJoin("conventions", "users__agencies.agency_id", "conventions.agency_id")
.where("users__agencies.user_id", "=", agencyUserId);

const builderWithJoins = createCommonJoins(builder);
return createConventionSelection(builderWithJoins);
};

export const getConventionAgencyFieldsForAgencies = async (
transaction: KyselyDb,
agencyIds: AgencyId[],
Expand Down
Loading
Loading