Skip to content

Commit

Permalink
[FEATURE] Connecte un datamart externe pour servir les résultats de c…
Browse files Browse the repository at this point in the history
…ertification pour ParcourSup (PIX-15800).

 #10896
  • Loading branch information
pix-service-auto-merge authored Dec 26, 2024
2 parents 4f52a4a + 55b127a commit ee6e7e4
Show file tree
Hide file tree
Showing 20 changed files with 382 additions and 51 deletions.
14 changes: 14 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,19 @@ jobs:
environment:
NODE_ENV: test
TEST_DATABASE_URL: postgres://circleci@localhost:5432/circleci_integration
- run:
name: Prepare datamart
command: npm run datamart:prepare
environment:
NODE_ENV: test
TEST_DATAMART_DATABASE_URL: postgres://circleci@localhost:5432/circleci_integration_datamart
- run:
name: Test
command: npm run test:api:integration
environment:
NODE_ENV: test
TEST_DATABASE_URL: postgres://circleci@localhost:5432/circleci_integration
TEST_DATAMART_DATABASE_URL: postgres://circleci@localhost:5432/circleci_integration_datamart
TEST_REDIS_URL: redis://localhost:6379
TEST_IMPORT_STORAGE_ENDPOINT: http://localhost:9090
TEST_IMPORT_STORAGE_BUCKET_NAME: pix-import-test
Expand All @@ -428,6 +435,12 @@ jobs:
environment:
NODE_ENV: test
TEST_DATABASE_URL: postgres://circleci@localhost:5432/circleci_acceptance
- run:
name: Prepare datamart
command: npm run datamart:prepare
environment:
NODE_ENV: test
TEST_DATAMART_DATABASE_URL: postgres://circleci@localhost:5432/circleci_integration_datamart
- run:
name: Test
command: |
Expand All @@ -437,6 +450,7 @@ jobs:
environment:
NODE_ENV: test
TEST_DATABASE_URL: postgres://circleci@localhost:5432/circleci_acceptance
TEST_DATAMART_DATABASE_URL: postgres://circleci@localhost:5432/circleci_integration_datamart
TEST_REDIS_URL: redis://localhost:6379
TEST_IMPORT_STORAGE_ENDPOINT: http://localhost:9090
TEST_IMPORT_STORAGE_BUCKET_NAME: pix-import-test
Expand Down
15 changes: 15 additions & 0 deletions api/datamart/datamart-builder/datamart-buffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const datamartBuffer = {
objectsToInsert: [],

pushInsertable({ tableName, values }) {
this.objectsToInsert.push({ tableName, values });

return values;
},

purge() {
this.objectsToInsert = [];
},
};

export { datamartBuffer };
58 changes: 58 additions & 0 deletions api/datamart/datamart-builder/datamart-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { datamartBuffer } from './datamart-buffer.js';
import { factory } from './factory/index.js';

/**
* @class DatamartBuilder
* @property {Factory} factory
*/
class DatamartBuilder {
constructor({ knex }) {
this.knex = knex;
this.datamartBuffer = datamartBuffer;
this.factory = factory;
}

static async create({ knex }) {
const datamartBuilder = new DatamartBuilder({ knex });

try {
await datamartBuilder._init();
} catch (_) {
// Error thrown only with unit tests
}

return datamartBuilder;
}

async commit() {
try {
const trx = await this.knex.transaction();
for (const objectToInsert of this.datamartBuffer.objectsToInsert) {
await trx(objectToInsert.tableName).insert(objectToInsert.values);
}
await trx.commit();
} catch (err) {
// eslint-disable-next-line no-console
console.error(`Erreur dans datamartBuilder.commit() : ${err}`);
throw err;
} finally {
this.datamartBuffer.purge();
}
}

async clean() {
let rawQuery = '';

['data_export_parcoursup_certif_result'].forEach((tableName) => {
rawQuery += `DELETE FROM ${tableName};`;
});

try {
await this.knex.raw(rawQuery);
} catch {
// ignore error
}
}
}

export { DatamartBuilder };
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { datamartBuffer } from '../datamart-buffer.js';

const buildCertificationResult = function ({ nationalStudentId } = {}) {
const values = {
national_student_id: nationalStudentId,
};

datamartBuffer.pushInsertable({
tableName: 'data_export_parcoursup_certif_result',
values,
});

return {
nationalStudentId,
};
};

export { buildCertificationResult };
16 changes: 16 additions & 0 deletions api/datamart/datamart-builder/factory/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { importNamedExportsFromDirectory } from '../../../src/shared/infrastructure/utils/import-named-exports-from-directory.js';

const path = dirname(fileURLToPath(import.meta.url));
const unwantedFiles = ['index.js'];

const datamartBuilders = await importNamedExportsFromDirectory({
path: join(path, './'),
ignoredFileNames: unwantedFiles,
});

export const factory = {
...datamartBuilders,
};
22 changes: 22 additions & 0 deletions api/datamart/knexfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as url from 'node:url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
import * as dotenv from 'dotenv';

import { buildPostgresEnvironment } from '../db/utils/build-postgres-environment.js';
dotenv.config({ path: `${__dirname}/../.env` });

const baseConfiguration = {
migrationsDirectory: './migrations/',
seedsDirectory: './seeds/',
databaseUrl: process.env.DATAMART_DATABASE_URL,
};

const environments = {
development: buildPostgresEnvironment(baseConfiguration),

test: buildPostgresEnvironment({ ...baseConfiguration, databaseUrl: process.env.TEST_DATAMART_DATABASE_URL }),

production: buildPostgresEnvironment(baseConfiguration),
};

export default environments;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const TABLE_NAME = 'data_export_parcoursup_certif_result';

const up = async function (knex) {
await knex.schema.createTable(TABLE_NAME, function (table) {
table.string('national_student_id');
table.index('national_student_id');
});
};

const down = async function (knex) {
await knex.schema.dropTable(TABLE_NAME);
};

export { down, up };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
national_student_id
123456789OK
25 changes: 25 additions & 0 deletions api/datamart/seeds/populate-certification-results.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { readdirSync } from 'node:fs';
import path from 'node:path';

import { parseCsvWithHeader } from '../../scripts/helpers/csvHelpers.js';
import { logger } from '../../src/shared/infrastructure/utils/logger.js';

// data are populated from exported data in csv files
// csv files shall be named with the table name they represent
const csvFolder = path.join(import.meta.dirname, 'csv');

export async function seed(knex) {
const csvFilesToImport = readdirSync(csvFolder);
for (const file of csvFilesToImport) {
await insertDataFromFile(path.join(csvFolder, file), knex);
}
}

async function insertDataFromFile(file, knex) {
const tableName = path.basename(file, '.csv');
logger.info(`Inserting data from ${file} to ${tableName}`);

const data = await parseCsvWithHeader(file);
await knex(tableName).truncate();
await knex.batchInsert(tableName, data);
}
13 changes: 12 additions & 1 deletion api/db/knex-database-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@ Links :
*/
types.setTypeParser(types.builtins.INT8, (value) => parseInt(value));

import * as datamartKnexConfigs from '../datamart/knexfile.js';
import * as knexConfigs from './knexfile.js';

const { logging, environment } = config;
const knexConfig = knexConfigs.default[environment];
const configuredKnex = Knex(knexConfig);

const datamartKnexConfig = datamartKnexConfigs.default[environment];
const configuredDatamartKnex = Knex(datamartKnexConfig);

/* QueryBuilder Extension */
try {
Knex.QueryBuilder.extend('whereInArray', function (column, values) {
Expand Down Expand Up @@ -79,6 +83,7 @@ configuredKnex.on('query-response', function (response, data) {
});

async function disconnect() {
await configuredDatamartKnex?.destroy();
return configuredKnex.destroy();
}

Expand Down Expand Up @@ -123,4 +128,10 @@ async function prepareDatabaseConnection() {
logger.info('Connection to database established.');
}

export { disconnect, emptyAllTables, configuredKnex as knex, prepareDatabaseConnection };
export {
configuredDatamartKnex as datamartKnex,
disconnect,
emptyAllTables,
configuredKnex as knex,
prepareDatabaseConnection,
};
60 changes: 19 additions & 41 deletions api/db/knexfile.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,29 @@
import * as url from 'node:url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
import * as dotenv from 'dotenv';

import { buildPostgresEnvironment } from './utils/build-postgres-environment.js';
dotenv.config({ path: `${__dirname}/../.env` });

function localPostgresEnv(databaseUrl, knexAsyncStacktraceEnabled) {
return {
client: 'postgresql',
connection: databaseUrl,
pool: {
min: 1,
max: 4,
},
migrations: {
tableName: 'knex_migrations',
directory: './migrations',
loadExtensions: ['.js'],
},
seeds: {
directory: './seeds',
loadExtensions: ['.js'],
},
asyncStackTraces: knexAsyncStacktraceEnabled !== 'false',
};
}
const environments = {
development: localPostgresEnv(process.env.DATABASE_URL, process.env.KNEX_ASYNC_STACKTRACE_ENABLED),
const baseConfiguration = {
migrationsDirectory: './migrations/',
seedsDirectory: './seeds/',
databaseUrl: process.env.DATABASE_URL,
};

export default {
development: buildPostgresEnvironment(baseConfiguration),

test: localPostgresEnv(process.env.TEST_DATABASE_URL, process.env.KNEX_ASYNC_STACKTRACE_ENABLED),
test: buildPostgresEnvironment({
...baseConfiguration,
databaseUrl: process.env.TEST_DATABASE_URL,
}),

production: {
client: 'postgresql',
connection: process.env.DATABASE_URL,
production: buildPostgresEnvironment({
...baseConfiguration,
pool: {
min: parseInt(process.env.DATABASE_CONNECTION_POOL_MIN_SIZE, 10) || 1,
max: parseInt(process.env.DATABASE_CONNECTION_POOL_MAX_SIZE, 10) || 4,
},
migrations: {
tableName: 'knex_migrations',
directory: './migrations',
loadExtensions: ['.js'],
min: parseInt(process.env.DATABASE_CONNECTION_POOL_MIN_SIZE, 10),
max: parseInt(process.env.DATABASE_CONNECTION_POOL_MAX_SIZE, 10),
},
seeds: {
directory: './seeds',
loadExtensions: ['.js'],
},
asyncStackTraces: process.env.KNEX_ASYNC_STACKTRACE_ENABLED !== 'false',
},
}),
};

export default environments;
20 changes: 20 additions & 0 deletions api/db/utils/build-postgres-environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function buildPostgresEnvironment({ databaseUrl, pool, migrationsDirectory, seedsDirectory }) {
return {
client: 'postgresql',
connection: databaseUrl,
pool: {
min: pool?.min || 1,
max: pool?.max || 4,
},
migrations: {
tableName: 'knex_migrations',
directory: migrationsDirectory,
loadExtensions: ['.js'],
},
seeds: {
directory: seedsDirectory,
loadExtensions: ['.js'],
},
asyncStackTraces: process.env.KNEX_ASYNC_STACKTRACE_ENABLED !== 'false',
};
}
8 changes: 7 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@
"scripts": {
"clean": "rm -rf node_modules",
"cache:refresh": "node scripts/refresh-cache",
"datamart:create": "node scripts/datamart/create-datamart",
"datamart:delete": "node scripts/datamart/drop-datamart",
"datamart:new-migration": "npx knex --knexfile datamart/knexfile.js migrate:make --stub $PWD/db/template.js $migrationname",
"datamart:migrate": "knex --knexfile datamart/knexfile.js migrate:latest",
"datamart:prepare": "run-s datamart:delete datamart:create datamart:migrate",
"datamart:seed": "knex --knexfile datamart/knexfile.js seed:run",
"db:new-migration": "npx knex --knexfile db/knexfile.js migrate:make --stub $PWD/db/template.js $migrationname",
"db:create": "node scripts/database/create-database",
"db:delete": "node scripts/database/drop-database",
Expand Down Expand Up @@ -161,7 +167,7 @@
"start:job:watch": "nodemon worker.js",
"start:job:fast:watch": "nodemon worker.js fast",
"test": "npm run test:db:reset && npm run test:api",
"test:db:reset": "NODE_ENV=test npm run db:prepare",
"test:db:reset": "NODE_ENV=test run-p db:prepare datamart:prepare",
"test:api": "for testType in 'unit' 'integration' 'acceptance'; do npm run test:api:$testType || status=1 ; done ; exit $status",
"test:api:path": "NODE_ENV=test mocha --exit --recursive --reporter=${MOCHA_REPORTER:-dot}",
"test:api:scripts": "npm run test:api:path -- tests/integration/scripts",
Expand Down
19 changes: 19 additions & 0 deletions api/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,25 @@ PGBOSS_CONNECTION_POOL_MAX_SIZE=
# sample: START_JOB_IN_WEB_PROCESS=true
START_JOB_IN_WEB_PROCESS=

# URL of the PostgreSQL database used for storing datamart
#
# If not present, the application will not be able to responds to requests involving
# data from datamart (i.e. cold data)
#
# presence: optional
# type: Url
# default: none
DATAMART_DATABASE_URL=postgresql://postgres@localhost/datamart

# URL of the PostgreSQL database used for API local testing.
#
# If not present, the tests will fail.
#
# presence: required
# type: Url
# default: none
TEST_DATAMART_DATABASE_URL=postgresql://postgres@localhost/datamart_test

# ========
# EMAILING
# ========
Expand Down
Loading

0 comments on commit ee6e7e4

Please sign in to comment.