Skip to content

Commit

Permalink
Refacto export (#2370)
Browse files Browse the repository at this point in the history
* add @pdc/service-export
* add migrations
  • Loading branch information
jonathanfallon authored Feb 13, 2024
1 parent 8ddbda6 commit af9e135
Show file tree
Hide file tree
Showing 166 changed files with 4,992 additions and 2,122 deletions.
8 changes: 8 additions & 0 deletions api/db/migrations/20240119000000-create_export_tables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

var { createMigration } = require('../helpers/createMigration');
var { setup, up, down } = createMigration(['export/20240119000000_create_export_tables'], __dirname);

exports.setup = setup;
exports.up = up;
exports.down = down;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP SCHEMA IF EXISTS export CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
CREATE SCHEMA IF NOT EXISTS export;

CREATE TABLE IF NOT EXISTS export.exports
(
_id serial PRIMARY KEY,
uuid uuid NOT NULL DEFAULT uuid_generate_v4(),
type varchar(255) NOT NULL DEFAULT 'opendata',
status varchar(255) NOT NULL DEFAULT 'pending',
progress integer NOT NULL DEFAULT 0,
created_by integer NOT NULL,
created_at timestamp NOT NULL DEFAULT NOW(),
updated_at timestamp NOT NULL DEFAULT NOW(),
download_url_expire_at timestamp with time zone,
download_url varchar(255),
params json NOT NULL,
error json,
stats json
);

CREATE UNIQUE INDEX ON export.exports(uuid);
CREATE INDEX ON export.exports(status) WHERE status = 'pending';
CREATE TRIGGER touch_exports_updated_at
BEFORE UPDATE ON export.exports
FOR EACH ROW EXECUTE PROCEDURE common.touch_updated_at();

CREATE TABLE IF NOT EXISTS export.recipients
(
_id serial PRIMARY KEY,
export_id integer NOT NULL,
scrambled_at timestamp with time zone,
email varchar(255) NOT NULL,
fullname varchar(255),
message text,
FOREIGN KEY (export_id) REFERENCES export.exports(_id) ON DELETE CASCADE
);

CREATE UNIQUE INDEX ON export.recipients(export_id, email);

CREATE TABLE IF NOT EXISTS export.logs
(
_id serial PRIMARY KEY,
export_id integer NOT NULL,
created_at timestamp NOT NULL DEFAULT NOW(),
type varchar(255) NOT NULL,
message text NOT NULL,
FOREIGN KEY (export_id) REFERENCES export.exports(_id) ON DELETE CASCADE
);

CREATE INDEX ON export.logs(export_id);
6 changes: 5 additions & 1 deletion api/helpers/dates.helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Timezone } from '@pdc/provider-validator';
import { addDays, addMonths, startOfMonth, subDays } from 'date-fns';
import { addDays, addMonths, startOfMonth, subDays, subMonths } from 'date-fns';
import { formatInTimeZone, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';

export const defaultTz = 'Europe/Paris';
Expand Down Expand Up @@ -63,6 +63,10 @@ export function addMonthsTz(d: Date, months: number, tz?: Timezone): Date {
return zonedTimeToUtc(addMonths(utcToZonedTime(d, tz || defaultTz), months), tz || defaultTz);
}

export function subMonthsTz(d: Date, months: number, tz?: Timezone): Date {
return zonedTimeToUtc(subMonths(utcToZonedTime(d, tz || defaultTz), months), tz || defaultTz);
}

export function startOfMonthTz(d: Date, tz?: Timezone): Date {
return zonedTimeToUtc(startOfMonth(utcToZonedTime(d, tz || defaultTz)), tz || defaultTz);
}
32 changes: 32 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@pdc/service-cee": "~0",
"@pdc/service-certificate": "~0",
"@pdc/service-company": "~0",
"@pdc/service-export": "~0",
"@pdc/service-geo": "~0",
"@pdc/service-honor": "~0",
"@pdc/service-monitoring": "~0",
Expand Down
2 changes: 2 additions & 0 deletions api/proxy/src/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { bootstrap as carpoolBootstrap } from '@pdc/service-carpool';
import { bootstrap as ceeBootstrap } from '@pdc/service-cee';
import { bootstrap as certificateBootstrap } from '@pdc/service-certificate';
import { bootstrap as companyBootstrap } from '@pdc/service-company';
import { bootstrap as exportBootstrap } from '@pdc/service-export';
import { bootstrap as honorBootstrap } from '@pdc/service-honor';
import { bootstrap as monitoringBootstrap } from '@pdc/service-monitoring';
import { bootstrap as operatorBootstrap } from '@pdc/service-operator';
Expand All @@ -33,6 +34,7 @@ import { config } from './config';
...ceeBootstrap.serviceProviders,
...certificateBootstrap.serviceProviders,
...companyBootstrap.serviceProviders,
...exportBootstrap.serviceProviders,
...honorBootstrap.serviceProviders,
...monitoringBootstrap.serviceProviders,
...operatorBootstrap.serviceProviders,
Expand Down
3 changes: 3 additions & 0 deletions api/services/export/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
coverage
.nyc_output
140 changes: 140 additions & 0 deletions api/services/export/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
title: Service export
---

Export des trajets (carpools) avec filtres territoire, opérateurs et géo. L'export est "commandé" et executé en arrière plan par un worker lancé chaque minute.

Les demandes d'export sont stockées dans une table `exports` avec un statut `pending` et un identifiant unique `uuid`. L'utilisateur peut suivre l'état de l'export avec l'identifiant `uuid` et télécharger le fichier une fois l'export terminé.

Les exports expirent après un délai fixé en configuration. Les données personnelles sont alors anonymisées en remplaçant les données email, fullname et message par des données aléatoires dans la table `recipients` et les fichiers XLSX/ZIP sont supprimés du S3 (tâche de nettoyage du bucket).

Des points d'API (clients et admins) et des commandes (admins) permettent de créer, lister, annuler et télécharger les exports.

Les exports Open-data sont gérés uniquement par le CLI.

Les actions sur les exports sont enregistrées dans la table `logs` pour garder une traçabilité.

### Spec

- format XLSX
- export configurable
- anonymisation open-data

> ### Commandes existantes
>
> - `trip:replayOpendata` #TODO analyser l'utilisation
> - uses `trip:buildExport`
> - `trip:publish` #TODO analyser l'utilisation
> - uses `trip:buildExport`
>
> ### Actions existantes
>
> - `trip:export` -> `trip:sendExport` -> `trip:buildExport`
> - `trip:publishOpenData`
### Actions utilisateur

- `create` (create)
- `status` (read)
- `list` (read list of exports filtered by `created_by`)
- `process` (process des exports en `pending`)
- `cancel` (annule une demande)
- `download` (create download link and redirect)
- `send` (create download link and send email from template)

### Commandes utilisateur

- `export:create`: crée une demande d'export
- `export:status`: retourne le statut d'un export
- `export:list`: liste les exports
- `export:cancel`: annule une demande d'export
- `export:download`: télécharge un export
- `export:send`: envoi un export par email
- `export:process`: process des exports en `pending` (CRON)
- `opendata:create`: crée un export open-data
- `opendata:upload`: upload un export open-data sur le S3
- `opendata:publish`: crée et upload un export open-data (CRON)

### CRON jobs

`* * * * * export:process`

- Expiration des process avec scrambling des données personnelles après le délai d'expiration fixé en configuration
- Process des exports en `pending`

`* * * * * opendata:publish`

- crée l'export open-data
- upload sur data.gouv.fr

### Steps

1. Collecter les inputs utilisateur -> stocker dans un table d'exports
2. CRON job toutes les minutes qui process et envoie les exports
3. Charger les fichiers de config des campagnes pour avoir accès aux tranches / périodes normales ou booster
4. Définir si on fait un export normal ou open-data
5. Streamer les données de la base -> transform -> enrich -> write dans le fichier XLSX
6. Zipper le XSLX
7. Upload sur le S3
8. Envoi du mail

### Structures de données

##### Table `export.exports`

```
_id
created_at
updated_at
created_by
uuid
status (pending|processing|uploading|sending|error|expired)
progress (0 - 100)
type (regular|opendata|operator|territory|registry)
download_url
params
error
stats
```

##### Table `export.recipients`

Table one-to-many des destinataires

```
_id
scrambled_at
export_id
email
fullname
message
```

##### Table `export.logs`

```
_id
created_at
export_id
type (=status|)
message
```

En cas d'erreur :

- notifier l'utilisateur
- notifier l'administration technique du site

# TODO

- [x] gérer les champs en fonction du type d'export (config)
- [x] spread des champs incentives
- [ ] tester les repositories
- [ ] migrations
- [ ] tests
- [ ] models (export, recipient, log)
- [ ] validation (schemas)
- [ ] permissions
- [ ] actions
- [ ] commands
- [ ] CRON jobs (infra)
5 changes: 5 additions & 0 deletions api/services/export/ava.coverage.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { coverage } = require('../../ava.common.cjs');

module.exports = {
...coverage,
}
5 changes: 5 additions & 0 deletions api/services/export/ava.integration.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { integration } = require('../../ava.common.cjs');

module.exports = {
...integration,
}
5 changes: 5 additions & 0 deletions api/services/export/ava.unit.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { unit } = require('../../ava.common.cjs');

module.exports = {
...unit,
}
38 changes: 38 additions & 0 deletions api/services/export/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@pdc/service-export",
"version": "0.0.1",
"private": true,
"license": "Apache-2.0",
"scripts": {
"ilos": "ilos",
"build": "tsc",
"watch": "tsc -w",
"test:unit": "ava --config ava.unit.cjs",
"test:integration": "ava --config ava.integration.cjs",
"coverage:ci": "nyc --nycrc-path ../../nyc.config.cjs --reporter=lcov ava --config ava.coverage.cjs",
"coverage": "nyc --nycrc-path ../../nyc.config.cjs --reporter=text ava --config ava.coverage.cjs"
},
"main": "./dist/bootstrap.js",
"config": {
"workingDir": "./dist",
"bootstrap": "./bootstrap.js",
"app": {}
},
"dependencies": {
"@ilos/cli": "~0",
"@ilos/common": "~0",
"@ilos/connection-postgres": "~0",
"@ilos/connection-redis": "~0",
"@ilos/core": "~0",
"@pdc/helper-test": "~0",
"@pdc/provider-middleware": "~0",
"@pdc/provider-storage": "~0",
"@pdc/provider-validator": "~0",
"adm-zip": "^0.5.10",
"csv-stringify": "^6.4.0",
"form-data": "^4.0.0"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0"
}
}
5 changes: 5 additions & 0 deletions api/services/export/src/ServiceProvider.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { serviceProviderMacro } from '@pdc/helper-test';
import { ServiceProvider } from './ServiceProvider';

const { test, boot } = serviceProviderMacro(ServiceProvider);
test(boot);
Loading

0 comments on commit af9e135

Please sign in to comment.