Skip to content

Commit

Permalink
feat(admin): list and delete index and repeq (#1779)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Pierre-Olivier Mauguet <[email protected]>
  • Loading branch information
lsagetlethias and pom421 authored Nov 23, 2023
1 parent 9e2a6f9 commit 18f9ec4
Show file tree
Hide file tree
Showing 31 changed files with 1,698 additions and 655 deletions.
28 changes: 25 additions & 3 deletions packages/api/egapro/sql/create_indexes.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
-- declaration
CREATE INDEX IF NOT EXISTS idx_effectifs ON declaration ((data->'entreprise'->'effectifs'->>'tranche'));
CREATE INDEX IF NOT EXISTS idx_status ON declaration (declared_at) WHERE declared_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_declaration_ues_name ON declaration ((data->'entreprise'->'ues'->>'name'))
WHERE data->'entreprise'->'ues'->>'name' IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_status ON declaration (declared_at)
WHERE declared_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_declaration_email ON declaration ((data->'déclarant'->>'email'));
CREATE INDEX IF NOT EXISTS idx_declaration_siren ON declaration (siren);
CREATE INDEX IF NOT EXISTS idx_declaration_year ON declaration (year);
-- search
CREATE INDEX IF NOT EXISTS idx_ft ON search USING GIN (ft);
CREATE INDEX IF NOT EXISTS idx_region ON search(region);
CREATE INDEX IF NOT EXISTS idx_departement ON search(departement);
CREATE INDEX IF NOT EXISTS idx_naf ON search(section_naf);
CREATE INDEX IF NOT EXISTS idx_declared_at ON search (declared_at);
CREATE INDEX IF NOT EXISTS idx_email ON representation_equilibree((data->'déclarant'->>'email'));
CREATE INDEX IF NOT EXISTS idx_siren ON representation_equilibree(siren);
CREATE INDEX IF NOT EXISTS idx_search_siren ON search (siren);
CREATE INDEX IF NOT EXISTS idx_search_year ON search (year);
-- representation_equilibree
CREATE INDEX IF NOT EXISTS idx_email ON representation_equilibree ((data->'déclarant'->>'email'));
CREATE INDEX IF NOT EXISTS idx_siren ON representation_equilibree (siren);
CREATE INDEX IF NOT EXISTS idx_representation_equilibree_status ON representation_equilibree (declared_at)
WHERE declared_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_representation_equilibree_year ON representation_equilibree (year);
-- search
CREATE INDEX IF NOT EXISTS idx_representation_equilibree_ft ON search_representation_equilibree USING GIN (ft);
CREATE INDEX IF NOT EXISTS idx_representation_equilibree_region ON search_representation_equilibree(region);
CREATE INDEX IF NOT EXISTS idx_representation_equilibree_departement ON search_representation_equilibree(departement);
CREATE INDEX IF NOT EXISTS idx_representation_equilibree_naf ON search_representation_equilibree(section_naf);
CREATE INDEX IF NOT EXISTS idx_representation_equilibree_declared_at ON search_representation_equilibree (declared_at);
CREATE INDEX IF NOT EXISTS idx_search_representation_equilibree_siren ON search_representation_equilibree (siren);
CREATE INDEX IF NOT EXISTS idx_search_representation_equilibree_year ON search_representation_equilibree (year);
1 change: 0 additions & 1 deletion packages/app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const nextConfig = {
// output: "standalone",
experimental: {
// typedRoutes: true, // TODO activate <3
serverActions: true,
// outputFileTracingRoot: path.join(__dirname, "../../"),
serverComponentsExternalPackages: ["@react-pdf/renderer", "xlsx", "xlsx", "js-xlsx", "@json2csv/node"],
},
Expand Down
9 changes: 5 additions & 4 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"migrateSimulations": "ts-node --project scripts/tsconfig.json scripts/sql-scripts/migrate-simulations.ts"
},
"dependencies": {
"@codegouvfr/react-dsfr": "0.77.0-rc.1",
"@codegouvfr/react-dsfr": "0.78.2",
"@formkit/auto-animate": "^0.8.0",
"@hookform/resolvers": "^3.1.1",
"@json2csv/node": "^7.0.1",
Expand All @@ -39,7 +39,7 @@
"lru-cache": "^10.0.0",
"mime": "^3.0.0",
"moize": "^6.1.3",
"next": "13.5.3",
"next": "14.0.0",
"next-auth": "^4.22.1",
"nodemailer": "^6.8.0",
"pg": "^8.8.0",
Expand All @@ -66,7 +66,7 @@
"@babel/core": "^7.23.0",
"@faker-js/faker": "^8.1.0",
"@hookform/devtools": "^4.3.0",
"@next/eslint-plugin-next": "13.5.3",
"@next/eslint-plugin-next": "14.0.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.2",
"@tsconfig/next": "^2.0.0",
Expand All @@ -86,9 +86,10 @@
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.2",
"babel-loader": "^9.1.3",
"concurrently": "^8.2.0",
"dotenv": "^16.0.1",
"eslint": "^8.50.0",
"eslint-config-next": "^13.5.3",
"eslint-config-next": "^14.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-prettier": "^5.0.0-alpha.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/api/core-domain/infra/auth/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const authConfig: AuthOptions = {
// force session to be stored as jwt in cookie instead of database
session: {
strategy: "jwt",
maxAge: 24 * 60 * 60, // 24 hours
maxAge: config.env === "dev" ? 24 * 60 * 60 * 7 : 24 * 60 * 60, // 24 hours in prod and preprod, 7 days in dev
},
providers: [
GithubProvider({
Expand Down
13 changes: 13 additions & 0 deletions packages/app/src/api/core-domain/infra/db/raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ export interface DeclarationSearchResultRaw {
siren: string;
}

export type AdminDeclarationRaw = {
created_at: Date;
declarant_email: string;
declarant_firstname: string;
declarant_lastname: string;
name: string;
siren: string;
type: "index" | "repeq";
year: number;
} & (
| { index: null; type: "repeq"; ues: null }
| { index: number; type: "index"; ues: DeclarationDataRaw["entreprise"]["ues"] }
);
export { type DeclarationStatsDTO as DeclarationStatsRaw } from "@common/core-domain/dtos/SearchDeclarationDTO";

export interface RepresentationEquilibreeSearchResultRaw {
Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/api/core-domain/repo/IAdminDeclarationRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type AdminDeclarationDTO } from "@common/core-domain/dtos/AdminDeclarationDTO";
import { type SearchAdminDeclarationInput } from "@common/core-domain/dtos/SearchDeclarationDTO";
import { type SearchDefaultCriteria, type SearchDTORepo } from "@common/shared-domain";

import { type AdminDeclarationRaw } from "../infra/db/raw";

export type AdminDeclarationSearchCriteria = SearchAdminDeclarationInput & SearchDefaultCriteria;

export interface IAdminDeclarationRepo extends SearchDTORepo<AdminDeclarationSearchCriteria, AdminDeclarationDTO> {}

export const orderByMap = {
createdAt: "created_at",
declarantEmail: "declarant_email",
declarantFirstName: "declarant_firstname",
declarantLastName: "declarant_lastname",
index: "index",
name: "name",
siren: "siren",
type: "type",
year: "year",
} as const satisfies Record<Exclude<AdminDeclarationSearchCriteria["orderBy"], undefined>, keyof AdminDeclarationRaw>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { type AdminDeclarationRaw } from "@api/core-domain/infra/db/raw";
import { sql } from "@api/shared-domain/infra/db/postgres";
import { type AdminDeclarationDTO } from "@common/core-domain/dtos/AdminDeclarationDTO";
import { type SQLCount } from "@common/shared-domain";
import { cleanFullTextSearch } from "@common/utils/postgres";
import { isFinite } from "lodash";
import { type Helper } from "postgres";

import { type AdminDeclarationSearchCriteria, type IAdminDeclarationRepo, orderByMap } from "../IAdminDeclarationRepo";

export class PostgresAdminDeclarationRepo implements IAdminDeclarationRepo {
private declarationTable = sql("declaration");
private representationEquilibreeTable = sql("representation_equilibree");
private searchTable = sql("search");
private searchRepresentationEquilibreeTable = sql("search_representation_equilibree");

public async search(criteria: AdminDeclarationSearchCriteria): Promise<AdminDeclarationDTO[]> {
const cteCombined = sql("cte_combined");

const raws = await sql<AdminDeclarationRaw[]>`
WITH ${cteCombined} AS (
SELECT ${this.declarationTable}.declared_at AS created_at,
${this.declarationTable}.data->'déclarant'->>'email' AS declarant_email,
${this.declarationTable}.data->'déclarant'->>'prénom' AS declarant_firstname,
${this.declarationTable}.data->'déclarant'->>'nom' AS declarant_lastname,
${this.declarationTable}.data->'entreprise'->>'raison_sociale' AS name,
${this.declarationTable}.data->'entreprise'->>'siren' AS siren,
'index' AS type,
${this.declarationTable}.year AS year,
(${this.declarationTable}.data->'déclaration'->>'index')::int AS index,
${this.declarationTable}.data->'entreprise'->'ues' AS ues
FROM ${this.declarationTable}
JOIN ${this.searchTable} ON ${this.declarationTable}.siren = ${this.searchTable}.siren
AND ${this.searchTable}.year = ${this.declarationTable}.year
${this.buildSearchWhereClause(criteria, this.searchTable, this.declarationTable)}
UNION ALL
SELECT ${this.representationEquilibreeTable}.declared_at AS created_at,
${this.representationEquilibreeTable}.data->'déclarant'->>'email' AS declarant_email,
${this.representationEquilibreeTable}.data->'déclarant'->>'prénom' AS declarant_firstname,
${this.representationEquilibreeTable}.data->'déclarant'->>'nom' AS declarant_lastname,
${this.representationEquilibreeTable}.data->'entreprise'->>'raison_sociale' AS name,
${this.representationEquilibreeTable}.data->'entreprise'->>'siren' AS siren,
'repeq' AS type,
${this.representationEquilibreeTable}.year,
NULL AS index,
NULL AS ues
FROM ${this.representationEquilibreeTable}
JOIN ${this.searchRepresentationEquilibreeTable} ON ${this.representationEquilibreeTable}.siren = ${
this.searchRepresentationEquilibreeTable
}.siren
AND ${this.searchRepresentationEquilibreeTable}.year = ${this.representationEquilibreeTable}.year
${this.buildSearchWhereClause(
criteria,
this.searchRepresentationEquilibreeTable,
this.representationEquilibreeTable,
)}
)
SELECT *
FROM ${cteCombined}
ORDER BY ${sql(criteria.orderBy ? orderByMap[criteria.orderBy] : sql("created_at"))} ${
criteria.orderDirection === "asc" ? sql`asc` : sql`desc`
}
LIMIT ${criteria.limit ?? 100}
OFFSET ${criteria.offset ?? 0};`;

return raws.map(
raw =>
({
createdAt: raw.created_at,
declarantEmail: raw.declarant_email,
declarantFirstName: raw.declarant_firstname,
declarantLastName: raw.declarant_lastname,
name: raw.name,
siren: raw.siren,
type: raw.type,
year: raw.year,
...(raw.type === "index"
? {
index: raw.index,
ues: raw.ues
? {
name: raw.ues.nom!,
companies: raw.ues.entreprises?.map(entreprise => ({
name: entreprise.raison_sociale,
siren: entreprise.siren,
})),
}
: void 0,
}
: {}),
}) as AdminDeclarationDTO,
);
}

public async count(criteria: AdminDeclarationSearchCriteria): Promise<number> {
const cteCountDeclaration = sql("cte_count_declaration");
const cteCountRepresentationEquilibree = sql("cte_count_representation_equilibree");

const [{ count }] = await sql<SQLCount>`
WITH
${cteCountDeclaration} AS (
SELECT COUNT(distinct(${this.searchTable}.siren)) AS count1
FROM ${this.searchTable}
JOIN ${this.declarationTable} ON ${this.searchTable}.siren = ${this.declarationTable}.siren
AND ${this.declarationTable}.year = ${this.searchTable}.year
${this.buildSearchWhereClause(criteria, this.searchTable, this.declarationTable)}
),
${cteCountRepresentationEquilibree} AS (
SELECT COUNT(distinct(${this.searchRepresentationEquilibreeTable}.siren)) AS count2
FROM ${this.searchRepresentationEquilibreeTable}
JOIN ${this.representationEquilibreeTable} ON ${this.searchRepresentationEquilibreeTable}.siren = ${
this.representationEquilibreeTable
}.siren
AND ${this.representationEquilibreeTable}.year = ${this.searchRepresentationEquilibreeTable}.year
${this.buildSearchWhereClause(
criteria,
this.searchRepresentationEquilibreeTable,
this.representationEquilibreeTable,
)}
)
SELECT
(count1 + count2) AS count
FROM
${cteCountDeclaration}, ${cteCountRepresentationEquilibree};`;
return +count;
}

private buildSearchWhereClause(
criteria: AdminDeclarationSearchCriteria,
searchTable: Helper<string>,
table: Helper<string>,
) {
let hasWhere = false;

let sqlYear = sql``;
if (typeof criteria.year === "number") {
// no sql`and` here because it's the first condition
sqlYear = sql`${searchTable}.year=${criteria.year}`;
hasWhere = true;
}

let sqlEmail = sql``;
if (criteria.email) {
sqlEmail = sql`${hasWhere ? sql`and` : sql``} ${table}.data->'déclarant'->>'email' like ${`%${criteria.email}%`}`;
hasWhere = true;
}

let sqlIndexComparison = sql``;
if (typeof criteria.index === "number" && criteria.indexComparison) {
sqlIndexComparison = sql`${hasWhere ? sql`and` : sql``} (${table}.data->'déclaration'->>'index')::int ${
criteria.indexComparison === "gt" ? sql`>` : criteria.indexComparison === "lt" ? sql`<` : sql`=`
} ${criteria.index}`;
hasWhere = true;
}

let sqlMinDate = sql``;
if (criteria.minDate) {
sqlMinDate = sql`${hasWhere ? sql`and` : sql``} ${searchTable}.declared_at >= ${criteria.minDate}`;
hasWhere = true;
}

let sqlMaxDate = sql``;
if (criteria.maxDate) {
sqlMaxDate = sql`${hasWhere ? sql`and` : sql``} ${searchTable}.declared_at <= ${criteria.maxDate}`;
hasWhere = true;
}

let sqlUes = sql``;
if (criteria.ues) {
sqlUes = sql`${hasWhere ? sql`and` : sql``} ${table}.data->'entreprise'->'ues' IS NOT NULL`;
hasWhere = true;
}

let sqlQuery = sql``;
if (criteria.query) {
if (criteria.query.length === 9 && isFinite(+criteria.query)) {
sqlQuery = sql`${hasWhere ? sql`and` : sql``} ${searchTable}.siren=${criteria.query}`;
} else {
sqlQuery = sql`${hasWhere ? sql`and` : sql``} ${searchTable}.ft @@ to_tsquery('ftdict', ${cleanFullTextSearch(
criteria.query,
)})`;
}
hasWhere = true;
}

return hasWhere
? sql`where ${sqlYear} ${sqlEmail} ${sqlIndexComparison} ${sqlMinDate} ${sqlMaxDate} ${sqlUes} ${sqlQuery}`
: sql``;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export class PostgresDeclarationRepo implements IDeclarationRepo {
}
}

public delete(_id: DeclarationPK): Promise<void> {
throw new Error("Method not implemented.");
public async delete([siren, year]: DeclarationPK): Promise<void> {
await this.sql`delete from ${this.table} where siren=${siren.getValue()} and year=${year.getValue()}`;
}
public exists(_id: DeclarationPK): Promise<boolean> {
throw new Error("Method not implemented.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export class PostgresRepresentationEquilibreeRepo implements IRepresentationEqui
}
}

public delete(_id: RepresentationEquilibreePK): Promise<void> {
throw new Error("Method not implemented.");
public async delete([siren, year]: RepresentationEquilibreePK): Promise<void> {
await this.sql`delete from ${this.table} where siren=${siren.getValue()} and year=${year.getValue()}`;
}
public exists(_id: RepresentationEquilibreePK): Promise<boolean> {
throw new Error("Method not implemented.");
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/api/core-domain/repo/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { services } from "@common/config";

import { type IAdminDeclarationRepo } from "./IAdminDeclarationRepo";
import { type IDeclarationRepo } from "./IDeclarationRepo";
import { type IDeclarationSearchRepo } from "./IDeclarationSearchRepo";
import { PostgresAdminDeclarationRepo } from "./impl/PostgresAdminDeclarationRepo";
import { PostgresDeclarationRepo } from "./impl/PostgresDeclarationRepo";
import { PostgresDeclarationSearchRepo } from "./impl/PostgresDeclarationSearchRepo";
import { PostgresOwnershipRepo } from "./impl/PostgresOwnershipRepo";
Expand All @@ -25,6 +27,7 @@ export let referentRepo: IReferentRepo;
export let representationEquilibreeSearchRepo: IRepresentationEquilibreeSearchRepo;
export let declarationSearchRepo: IDeclarationSearchRepo;
export let publicStatsRepo: IPublicStatsRepo;
export let adminDeclarationRepo: IAdminDeclarationRepo;

if (services.db === "postgres") {
declarationRepo = new PostgresDeclarationRepo();
Expand All @@ -35,4 +38,5 @@ if (services.db === "postgres") {
representationEquilibreeSearchRepo = new PostgresRepresentationEquilibreeSearchRepo();
declarationSearchRepo = new PostgresDeclarationSearchRepo();
publicStatsRepo = new PostgresPublicStatsRepo();
adminDeclarationRepo = new PostgresAdminDeclarationRepo();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ImgRepresentationEquilibree } from "@design-system";
import { ImageResponse } from "next/server";
import { ImageResponse } from "next/og";

export const alt = "Représentation Équilibrée Egapro";
export const size = {
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/app/admin/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export const adminMenuItems = [
href: "/admin/impersonate",
segment: "impersonate",
},
{
text: "Liste des déclarations d'Index et de Représentation Équilibrée",
href: "/admin/declarations",
segment: "declarations",
},
];

export const Navigation = () => {
Expand Down
Loading

0 comments on commit 18f9ec4

Please sign in to comment.