Skip to content

Commit

Permalink
feat(backend): calcule des indicateurs calcules lors de la mise à jou…
Browse files Browse the repository at this point in the history
…r d'une valeur d'indicateur, import de la définition des indicateurs depuis un spreadsheet
  • Loading branch information
dthib committed Feb 26, 2025
1 parent de06e40 commit ef9ddcb
Show file tree
Hide file tree
Showing 238 changed files with 4,027 additions and 32,022 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cd-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
--TRAJECTOIRE_SNBC_XLSX_ID=${{ vars.TRAJECTOIRE_SNBC_XLSX_ID }}
--TRAJECTOIRE_SNBC_RESULT_FOLDER_ID=${{ vars.TRAJECTOIRE_SNBC_RESULT_FOLDER_ID }}
--REFERENTIEL_TE_SHEET_ID=${{ vars.REFERENTIEL_TE_SHEET_ID }}
--INDICATEUR_DEFINITIONS_SHEET_ID=${{ vars.INDICATEUR_DEFINITIONS_SHEET_ID }}
deploy-test-backend:
name: Déploie le backend de test
Expand Down
10 changes: 10 additions & 0 deletions Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,9 @@ app-deploy:
# BACKEND ENTRYPOINTS
# -------------------

backend-local-seed:
BUILD --pass-args ./backend+local-seed

backend-docker:
BUILD --pass-args ./backend+docker

Expand Down Expand Up @@ -775,6 +778,13 @@ dev:

RUN earthly +load-json --SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY --API_URL=$API_URL

# Seed des indicateurs et des referentiels à partir des spreadsheets
IF [ "$CI" = "true" ]

ELSE
RUN earthly +backend-local-seed
END

# Seed si aucune collectivité en base
RUN docker run --rm \

Check failure on line 789 in Earthfile

View workflow job for this annotation

GitHub Actions / Lecture et conversion des fichiers Markdown en JSON

Error

The command RUN docker run --rm --network $network psql:latest $DB_URL -v ON_ERROR_STOP=1 -c "select 1 / count(*) from collectivite;" || earthly +seed --DB_URL=$DB_URL did not complete successfully. Exit code 1
--network $network \
Expand Down
17 changes: 11 additions & 6 deletions backend/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ build:

COPY . ./backend

RUN pnpm build:backend
RUN pnpm build:backend

SAVE ARTIFACT dist /dist AS LOCAL earthly-build/dist

local-seed:
LOCALLY
# Trigger import indicateur & referentiel to seed the database
RUN pnpm test:backend src/indicateurs/import-indicateurs/import-indicateur-definition.controller.e2e-spec.ts src/referentiels/import-referentiel/import-referentiel.controller.e2e-spec.ts

docker:
FROM ../+prod-deps

Expand All @@ -33,6 +38,7 @@ deploy: ## Déploie le backend dans une app Koyeb existante
ARG --required TRAJECTOIRE_SNBC_XLSX_ID
ARG --required TRAJECTOIRE_SNBC_RESULT_FOLDER_ID
ARG --required REFERENTIEL_TE_SHEET_ID
ARG --required INDICATEUR_DEFINITIONS_SHEET_ID
ARG --required DEPLOYMENT_TIMESTAMP
ARG SERVICE_NAME=$ENV_NAME-backend
FROM ../+koyeb --KOYEB_API_KEY=$KOYEB_API_KEY
Expand All @@ -54,16 +60,13 @@ deploy: ## Déploie le backend dans une app Koyeb existante
--env MATTERMOST_NOTIFICATIONS_WEBHOOK_URL=@MATTERMOST_NOTIFICATIONS_WEBHOOK_URL_$ENV_NAME \
--env BREVO_API_KEY=@BREVO_API_KEY \
--env DIRECTUS_API_KEY=@DIRECTUS_API_KEY \
--env REFERENTIEL_TE_SHEET_ID=$REFERENTIEL_TE_SHEET_ID
--env REFERENTIEL_TE_SHEET_ID=$REFERENTIEL_TE_SHEET_ID \
--env INDICATEUR_DEFINITIONS_SHEET_ID=$INDICATEUR_DEFINITIONS_SHEET_ID

test-build: ## construit une image pour exécuter les tests du backend
FROM ../+front-deps
COPY . ./backend

#FROM ../+prod-deps
#ENV SUPABASE_URL
#ENV SUPABASE_ANON_KEY
#ENV SUPABASE_SERVICE_ROLE_KEY
# la commande utilisée pour lancer les tests
CMD pnpm --version && pnpm run test:backend
SAVE IMAGE backend-test:latest
Expand All @@ -73,6 +76,7 @@ test: ## lance les tests
ARG --required TRAJECTOIRE_SNBC_XLSX_ID
ARG --required TRAJECTOIRE_SNBC_RESULT_FOLDER_ID
ARG --required REFERENTIEL_TE_SHEET_ID
ARG --required INDICATEUR_DEFINITIONS_SHEET_ID
ARG --required GCLOUD_SERVICE_ACCOUNT_KEY
ARG --required SUPABASE_DATABASE_URL
ARG --required SUPABASE_URL
Expand All @@ -91,6 +95,7 @@ test: ## lance les tests
--env TRAJECTOIRE_SNBC_XLSX_ID=$TRAJECTOIRE_SNBC_XLSX_ID \
--env TRAJECTOIRE_SNBC_RESULT_FOLDER_ID=$TRAJECTOIRE_SNBC_RESULT_FOLDER_ID \
--env REFERENTIEL_TE_SHEET_ID=$REFERENTIEL_TE_SHEET_ID \
--env INDICATEUR_DEFINITIONS_SHEET_ID=$INDICATEUR_DEFINITIONS_SHEET_ID \
--env GCLOUD_SERVICE_ACCOUNT_KEY=$GCLOUD_SERVICE_ACCOUNT_KEY \
--env SUPABASE_DATABASE_URL=$SUPABASE_DATABASE_URL \
--env SUPABASE_URL=$SUPABASE_URL \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@ export default class ListCategoriesService {
)
: // Récupère seulement les catégories propres à la collectivité
eq(categorieTagTable.collectiviteId, collectiviteId)
);
) as Promise<Tag[]>; // We know that the collectiviteId is not null in this case
}
}
2 changes: 1 addition & 1 deletion backend/src/collectivites/identite-collectivite.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const identiteCollectiviteSchema = z.object({
soustype: z.nativeEnum(CollectiviteSousTypeEnum).nullable(),
populationTags: z.array(z.nativeEnum(CollectivitePopulationTypeEnum)),
drom: z.boolean().nullable(),
test: z.boolean(),
test: z.boolean().optional(),
});

export type IdentiteCollectivite = z.infer<typeof identiteCollectiviteSchema>;
Expand Down
8 changes: 8 additions & 0 deletions backend/src/collectivites/tags/categorie-tag.table.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { collectiviteTable } from '@/backend/collectivites/shared/models/collectivite.table';
import { InferInsertModel, InferSelectModel } from 'drizzle-orm';
import { boolean, integer, pgTable } from 'drizzle-orm/pg-core';
import { createdAt, createdBy } from '../../utils/column.utils';
import { tagTableBase } from './tag.table-base';

export const categorieTagTable = pgTable('categorie_tag', {
...tagTableBase,
collectiviteId: integer('collectivite_id').references(
() => collectiviteTable.id
),
groupementId: integer('groupement_id'), // TODO .references(() => groupementTable.id)
visible: boolean('visible').default(true),
createdAt,
createdBy,
});

export type CategorieTagType = InferSelectModel<typeof categorieTagTable>;
export type CreateCategorieTagType = InferInsertModel<typeof categorieTagTable>;
7 changes: 7 additions & 0 deletions backend/src/collectivites/tags/tag.table-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export const tagSchema = z.object({
});
export type Tag = z.infer<typeof tagSchema>;

export const tagWithOptionalCollectiviteSchema = tagSchema.extend({
collectiviteId: z.number().optional().nullable(),
});
export type TagWithOptionalCollectivite = z.infer<
typeof tagWithOptionalCollectiviteSchema
>;

export const tagUpdateSchema = tagSchema.partial();
export type TagUpdate = z.input<typeof tagUpdateSchema>;

Expand Down
206 changes: 199 additions & 7 deletions backend/src/indicateurs/definitions/list-definitions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import {
eq,
getTableColumns,
inArray,
isNotNull,
isNull,
like,
or,
SQL,
sql,
SQLWrapper,
} from 'drizzle-orm';
import { objectToCamel } from 'ts-case-convert';
import { PermissionService } from '../../auth/authorizations/permission.service';
import { groupementCollectiviteTable } from '../../collectivites/shared/models/groupement-collectivite.table';
import { groupementTable } from '../../collectivites/shared/models/groupement.table';
import { actionDefinitionTable } from '../../referentiels/models/action-definition.table';
import { DatabaseService } from '../../utils/database/database.service';
import {
indicateurSourceMetadonneeTable,
Expand Down Expand Up @@ -46,21 +51,160 @@ export default class ListDefinitionsService {
private readonly permissionService: PermissionService
) {}

async getReferentielIndicateurDefinitions(identifiantsReferentiel: string[]) {
private getIndicateurDefinitionThematiquesQuery() {
return this.databaseService.db
.select({
indicateur_id: indicateurThematiqueTable.indicateurId,
thematique_ids: sql<
number[]
>`array_agg(${indicateurThematiqueTable.thematiqueId})`.as(
'thematique_ids'
),
thematique_mids: sql<string[]>`array_agg(${thematiqueTable.mdId})`.as(
'thematique_mids'
),
thematiques: sql<
{ id: number; nom: string; mdId?: string }[]
>`array_agg(json_build_object('id', ${indicateurThematiqueTable.thematiqueId}, 'nom', ${thematiqueTable.nom}, 'mdId', ${thematiqueTable.mdId} ))`.as(
'thematiques'
),
})
.from(indicateurThematiqueTable)
.leftJoin(
thematiqueTable,
eq(thematiqueTable.id, indicateurThematiqueTable.thematiqueId)
)
.groupBy(indicateurThematiqueTable.indicateurId)
.as('indicateurThematiques');
}

private getIndicateurDefinitionParentsQuery() {
const parentDefinition = aliasedTable(
indicateurDefinitionTable,
'parentDefinition'
);

return this.databaseService.db
.select({
indicateur_id: indicateurGroupeTable.enfant,
parent_ids: sql<
string[]
>`array_agg(${indicateurGroupeTable.parent})`.as('parent_ids'),
parents: sql<
{ id: string; identifiantReferentiel: string; titre: string }[]
>`array_agg(json_build_object('id', ${indicateurGroupeTable.parent}, 'identifiantReferentiel', ${parentDefinition.identifiantReferentiel}, 'titre', ${parentDefinition.titre} ))`.as(
'parents'
),
})
.from(indicateurGroupeTable)
.leftJoin(
parentDefinition,
eq(parentDefinition.id, indicateurGroupeTable.parent)
)
.groupBy(indicateurGroupeTable.enfant)
.as('indicateurParents');
}

private getIndicateurDefinitionActionIdsQuery() {
return this.databaseService.db
.select({
indicateur_id: indicateurActionTable.indicateurId,
action_ids: sql<
string[]
>`array_agg(${indicateurActionTable.actionId})`.as('action_ids'),
actions: sql<
{ id: string; nom: string }[]
>`array_agg(json_build_object('id', ${indicateurActionTable.actionId}, 'nom', ${actionDefinitionTable.nom} ))`.as(
'actions'
),
})
.from(indicateurActionTable)
.leftJoin(
actionDefinitionTable,
eq(actionDefinitionTable.actionId, indicateurActionTable.actionId)
)
.groupBy(indicateurActionTable.indicateurId)
.as('indicateurActionIds');
}

private getIndicateurDefinitionCategoriesQuery() {
return this.databaseService.db
.select({
indicateur_id: indicateurCategorieTagTable.indicateurId,
categorie_ids: sql<
number[]
>`array_agg(${indicateurCategorieTagTable.categorieTagId})`.as(
'categorie_ids'
),
categories: sql<
{ id: number; nom: string }[]
>`array_agg(json_build_object('id', ${indicateurCategorieTagTable.categorieTagId}, 'nom', ${categorieTagTable.nom} ))`.as(
'categories'
),
})
.from(indicateurCategorieTagTable)
.leftJoin(
categorieTagTable,
eq(categorieTagTable.id, indicateurCategorieTagTable.categorieTagId)
)
.groupBy(indicateurCategorieTagTable.indicateurId)
.as('indicateurCategories');
}

async getReferentielIndicateurDefinitions(
identifiantsReferentiel?: string[]
) {
this.logger.log(
`Récupération des définitions des indicateurs ${identifiantsReferentiel.join(
`Récupération des définitions des indicateurs ${identifiantsReferentiel?.join(
','
)}`
);
const definitions = await this.databaseService.db
.select()
.from(indicateurDefinitionTable)
.where(
const conditions: (SQLWrapper | SQL)[] = [
isNotNull(indicateurDefinitionTable.identifiantReferentiel),
isNull(indicateurDefinitionTable.collectiviteId),
];
if (identifiantsReferentiel) {
conditions.push(
inArray(
indicateurDefinitionTable.identifiantReferentiel,
identifiantsReferentiel
)
);
}

const indicateurThematiques =
this.getIndicateurDefinitionThematiquesQuery();
const indicateurCategories = this.getIndicateurDefinitionCategoriesQuery();
const indicateurActionIds = this.getIndicateurDefinitionActionIdsQuery();
const indicateurParents = this.getIndicateurDefinitionParentsQuery();

const definitions = await this.databaseService.db
.select({
...getTableColumns(indicateurDefinitionTable),
thematiques: indicateurThematiques.thematiques,
categories: indicateurCategories.categories,
actions: indicateurActionIds.actions,
parents: indicateurParents.parents,
})
.from(indicateurDefinitionTable)
.leftJoin(
indicateurThematiques,
eq(indicateurThematiques.indicateur_id, indicateurDefinitionTable.id)
)
.leftJoin(
indicateurCategories,
eq(indicateurCategories.indicateur_id, indicateurDefinitionTable.id)
)
.leftJoin(
indicateurActionIds,
eq(indicateurActionIds.indicateur_id, indicateurDefinitionTable.id)
)
.leftJoin(
indicateurParents,
eq(indicateurParents.indicateur_id, indicateurDefinitionTable.id)
)
.where(and(...conditions))
.orderBy(indicateurDefinitionTable.identifiantReferentiel);
this.logger.log(`${definitions.length} définitions trouvées`);
return definitions;
}
Expand All @@ -70,7 +214,7 @@ export default class ListDefinitionsService {
* ainsi que les définitions des indicateurs "enfant" associés.
* (utilisé pour l'export)
*/
async getIndicateurDefinitions(
async getIndicateurDefinitionsAvecEnfants(
collectiviteId: number,
indicateurIds: number[]
): Promise<IndicateurDefinitionAvecEnfantsType[]> {
Expand Down Expand Up @@ -131,6 +275,54 @@ export default class ListDefinitionsService {
);
}

async getIndicateurDefinitions(indicateurIds: number[]) {
const indicateurDefinitions = await this.databaseService.db
.select()
.from(indicateurDefinitionTable)
.where(inArray(indicateurDefinitionTable.id, indicateurIds));
return indicateurDefinitions;
}

getIndicateurIdToIdentifiant(
indicateurDefinitions: IndicateurDefinition[]
): Record<number, string> {
return indicateurDefinitions.reduce((acc, def) => {
if (!def.identifiantReferentiel) {
return acc;
} else {
return { ...acc, [def.id]: def.identifiantReferentiel };
}
}, {});
}

async getComputedIndicateurDefinitions(
sourceIndicateurIdentifiants: string[]
): Promise<IndicateurDefinition[]> {
const sqlConditions: (SQLWrapper | SQL)[] =
sourceIndicateurIdentifiants.map((identifiant) =>
like(indicateurDefinitionTable.valeurCalcule, `%${identifiant}%`)
);

const computedIndicateurDefinitions = await this.databaseService.db
.select()
.from(indicateurDefinitionTable)
.where(
and(
isNotNull(indicateurDefinitionTable.valeurCalcule),
or(...sqlConditions)
)
);
this.logger.log(
`Found ${
computedIndicateurDefinitions.length
} computed indicateur definitions: ${computedIndicateurDefinitions
.map((def) => def.identifiantReferentiel || `${def.id}`)
.join(',')} for indicateurs ${sourceIndicateurIdentifiants.join(',')}`
);

return computedIndicateurDefinitions;
}

/**
* Renvoi les définitions détaillées d'indicateur à partir de leur id ou de
* leur identifiant référentiel
Expand Down
Loading

0 comments on commit ef9ddcb

Please sign in to comment.