diff --git a/package-lock.json b/package-lock.json index cf56795b..eeca953a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9158,6 +9158,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sql-template-tag": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/sql-template-tag/-/sql-template-tag-5.2.1.tgz", + "integrity": "sha512-lFdvXCOqWhV40A7w4oQVDyuaNFb5yO+dhsHStZzOdtDJWCBWYv4+hhATK5nPpY5v/T1OMVcLMPeN4519qIyb9Q==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -10463,6 +10472,9 @@ "name": "@gouvfr-lasuite/proconnect.identite", "version": "0.2.0", "license": "MIT", + "dependencies": { + "sql-template-tag": "^5.2.1" + }, "devDependencies": { "@electric-sql/pglite": "^0.2.15", "@gouvfr-lasuite/proconnect.core": "^0.2.0", diff --git a/packages/identite/package.json b/packages/identite/package.json index e13963a4..076fcc36 100644 --- a/packages/identite/package.json +++ b/packages/identite/package.json @@ -33,6 +33,7 @@ }, "scripts": { "build": "tsc --project tsconfig.lib.json", + "clean": "rm -rf dist tsconfig*.tsbuildinfo", "check": "npm run build -- --noEmit", "dev": "npm run build -- --watch --preserveWatchOutput", "test": "mocha" @@ -44,7 +45,9 @@ ], "spec": "src/**/*.test.ts" }, - "dependencies": {}, + "dependencies": { + "sql-template-tag": "^5.2.1" + }, "devDependencies": { "@electric-sql/pglite": "^0.2.15", "@gouvfr-lasuite/proconnect.core": "^0.2.0", diff --git a/packages/identite/src/organization/index.ts b/packages/identite/src/organization/index.ts new file mode 100644 index 00000000..8ed86e54 --- /dev/null +++ b/packages/identite/src/organization/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./upsert.js"; diff --git a/packages/identite/src/organization/upsert.ts b/packages/identite/src/organization/upsert.ts new file mode 100644 index 00000000..a42c8054 --- /dev/null +++ b/packages/identite/src/organization/upsert.ts @@ -0,0 +1,140 @@ +// + +import type { + DatabaseContext, + Organization, + OrganizationInfo, +} from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function upsertFactory({ pg }: DatabaseContext) { + return async function upsert({ + siret, + organizationInfo: { + libelle: cached_libelle, + nomComplet: cached_nom_complet, + enseigne: cached_enseigne, + trancheEffectifs: cached_tranche_effectifs, + trancheEffectifsUniteLegale: cached_tranche_effectifs_unite_legale, + libelleTrancheEffectif: cached_libelle_tranche_effectif, + etatAdministratif: cached_etat_administratif, + estActive: cached_est_active, + statutDiffusion: cached_statut_diffusion, + estDiffusible: cached_est_diffusible, + adresse: cached_adresse, + codePostal: cached_code_postal, + codeOfficielGeographique: cached_code_officiel_geographique, + activitePrincipale: cached_activite_principale, + libelleActivitePrincipale: cached_libelle_activite_principale, + categorieJuridique: cached_categorie_juridique, + libelleCategorieJuridique: cached_libelle_categorie_juridique, + }, + }: { + siret: string; + organizationInfo: OrganizationInfo; + }) { + const { rows }: QueryResult = await pg.query( + ` + INSERT INTO organizations + ( + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + organization_info_fetched_at, + updated_at, + created_at + ) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) + ON CONFLICT (siret) + DO UPDATE + SET ( + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + organization_info_fetched_at, + updated_at + ) = ( + EXCLUDED.siret, + EXCLUDED.cached_libelle, + EXCLUDED.cached_nom_complet, + EXCLUDED.cached_enseigne, + EXCLUDED.cached_tranche_effectifs, + EXCLUDED.cached_tranche_effectifs_unite_legale, + EXCLUDED.cached_libelle_tranche_effectif, + EXCLUDED.cached_etat_administratif, + EXCLUDED.cached_est_active, + EXCLUDED.cached_statut_diffusion, + EXCLUDED.cached_est_diffusible, + EXCLUDED.cached_adresse, + EXCLUDED.cached_code_postal, + EXCLUDED.cached_code_officiel_geographique, + EXCLUDED.cached_activite_principale, + EXCLUDED.cached_libelle_activite_principale, + EXCLUDED.cached_categorie_juridique, + EXCLUDED.cached_libelle_categorie_juridique, + EXCLUDED.organization_info_fetched_at, + EXCLUDED.updated_at + ) + RETURNING * + `, + [ + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + new Date(), + new Date(), + new Date(), + ], + ); + + return rows.shift()!; + }; +} diff --git a/packages/identite/src/organization/upset.test.ts b/packages/identite/src/organization/upset.test.ts new file mode 100644 index 00000000..c440652b --- /dev/null +++ b/packages/identite/src/organization/upset.test.ts @@ -0,0 +1,54 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { upsertFactory } from "./upsert.js"; + +// + +const pg = new PGlite(); +const upset = upsertFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + migrationsTable: "pg-migrate", + log: noop, + }); +}); + +describe("upset", () => { + it("should create the Tau Empire organization", async () => { + const organization = await upset({ + organizationInfo: { + libelle: "Tau Empire", + nomComplet: "Tau Empire", + } as any, + siret: "👽️", + }); + expect(organization.created_at).to.deep.equal(organization.updated_at); + }); + + it("should update the Necron organization", async () => { + await pg.sql`insert into organizations + (siret, created_at, updated_at) + VALUES + ('⚰️', '1967-12-19', '1967-12-19'); + `; + const organization = await upset({ + organizationInfo: { + libelle: "Necron", + nomComplet: "Necrontyr", + } as any, + siret: "⚰️", + }); + expect(organization.created_at).to.not.deep.equal(organization.updated_at); + expect(organization.cached_libelle).to.equal("Necron"); + }); +}); diff --git a/packages/identite/src/services/hash-to-postgres-params.ts b/packages/identite/src/services/hash-to-postgres-params.ts new file mode 100644 index 00000000..d3f6d570 --- /dev/null +++ b/packages/identite/src/services/hash-to-postgres-params.ts @@ -0,0 +1,34 @@ +// + +import { chain } from "lodash-es"; + +// + +export function hashToPostgresParams(fieldsToUpdate: Partial): { + // postgres column-list syntax + paramsString: string; + // postgres column-list syntax for prepared statement + valuesString: string; + values: any[]; +} { + const paramsString = "(" + Object.keys(fieldsToUpdate).join(", ") + ")"; + // 'email, encrypted_password' + + const valuesString = + "(" + + chain(fieldsToUpdate) + // { email: 'email@xy.z', encrypted_password: 'hash' } + .toPairs() + // [[ 'email', 'email@xy.z'], ['encrypted_password', 'hash' ]] + .map((_value, index) => `$${index + 1}`) + // [ '$1', '$2' ] + .join(", ") + // '$1, $2' + .value() + + ")"; + + const values = Object.values(fieldsToUpdate); + // [ 'email@xy.z', 'hash' ] + + return { paramsString, valuesString, values }; +} diff --git a/packages/identite/src/services/index.ts b/packages/identite/src/services/index.ts new file mode 100644 index 00000000..8d832022 --- /dev/null +++ b/packages/identite/src/services/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./hash-to-postgres-params.js"; diff --git a/packages/identite/src/types/index.ts b/packages/identite/src/types/index.ts index 3ff3a81a..68505bb0 100644 --- a/packages/identite/src/types/index.ts +++ b/packages/identite/src/types/index.ts @@ -1,4 +1,7 @@ // export * from "./contexts.js"; +export * from "./organization-info.js"; +export * from "./organization.js"; +export * from "./tranche-effectifs.js"; export * from "./user.js"; diff --git a/packages/identite/src/types/organization-info.ts b/packages/identite/src/types/organization-info.ts new file mode 100644 index 00000000..4ee941a7 --- /dev/null +++ b/packages/identite/src/types/organization-info.ts @@ -0,0 +1,26 @@ +// + +import type { TrancheEffectifs } from "./tranche-effectifs.js"; + +// + +export interface OrganizationInfo { + siret: string; + libelle: string; + nomComplet: string; + enseigne: string; + trancheEffectifs: TrancheEffectifs; + trancheEffectifsUniteLegale: TrancheEffectifs; + libelleTrancheEffectif: string; + etatAdministratif: string; + estActive: boolean; + statutDiffusion: string; + estDiffusible: boolean; + adresse: string; + codePostal: string; + codeOfficielGeographique: string; + activitePrincipale: string; + libelleActivitePrincipale: string; + categorieJuridique: string; + libelleCategorieJuridique: string; +} diff --git a/packages/identite/src/types/organization.ts b/packages/identite/src/types/organization.ts new file mode 100644 index 00000000..2b21b95f --- /dev/null +++ b/packages/identite/src/types/organization.ts @@ -0,0 +1,30 @@ +// + +import type { TrancheEffectifs } from "./tranche-effectifs.js"; + +// + +export interface Organization { + id: number; + siret: string; + created_at: Date; + updated_at: Date; + cached_libelle: string | null; + cached_nom_complet: string | null; + cached_enseigne: string | null; + cached_tranche_effectifs: TrancheEffectifs; + cached_tranche_effectifs_unite_legale: string | null; + cached_libelle_tranche_effectif: string | null; + cached_etat_administratif: string | null; + cached_est_active: string | null; + cached_statut_diffusion: string | null; + cached_est_diffusible: string | null; + cached_adresse: string | null; + cached_code_postal: string | null; + cached_code_officiel_geographique: string | null; + cached_activite_principale: string | null; + cached_libelle_activite_principale: string | null; + cached_categorie_juridique: string | null; + cached_libelle_categorie_juridique: string | null; + organization_info_fetched_at: Date | null; +} diff --git a/src/types/organization-info.d.ts b/packages/identite/src/types/tranche-effectifs.ts similarity index 62% rename from src/types/organization-info.d.ts rename to packages/identite/src/types/tranche-effectifs.ts index 83eadaa5..5994996a 100644 --- a/src/types/organization-info.d.ts +++ b/packages/identite/src/types/tranche-effectifs.ts @@ -1,5 +1,5 @@ // source : https://www.sirene.fr/sirene/public/variable/trancheEffectifsEtablissement -type TrancheEffectifs = +export type TrancheEffectifs = // le champ peut être null bien que la documentation ne spécifie pas à quoi correspond ce cas | null // Etablissement non employeur (pas de salarié au cours de l'année de référence et pas d'effectif au 31/12)NN @@ -34,24 +34,3 @@ type TrancheEffectifs = | "52" // 10 000 salariés et plus | "53"; - -interface OrganizationInfo { - siret: string; - libelle: string; - nomComplet: string; - enseigne: string; - trancheEffectifs: TrancheEffectifs; - trancheEffectifsUniteLegale: TrancheEffectifs; - libelleTrancheEffectif: string; - etatAdministratif: string; - estActive: boolean; - statutDiffusion: string; - estDiffusible: boolean; - adresse: string; - codePostal: string; - codeOfficielGeographique: string; - activitePrincipale: string; - libelleActivitePrincipale: string; - categorieJuridique: string; - libelleCategorieJuridique: string; -} diff --git a/packages/identite/src/user/create.test.ts b/packages/identite/src/user/create.test.ts new file mode 100644 index 00000000..0396e2b2 --- /dev/null +++ b/packages/identite/src/user/create.test.ts @@ -0,0 +1,31 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { createUserFactory } from "./create.js"; + +// + +const pg = new PGlite(); +const createUser = createUserFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + log: noop, + migrationsTable: "pg-migrate", + }); +}); + +describe("CreateUser", () => { + it("should create the god-emperor of mankind", async () => { + const user = await createUser({ email: "god-emperor@mankind" }); + expect(user.email).to.equal("god-emperor@mankind"); + }); +}); diff --git a/packages/identite/src/user/create.ts b/packages/identite/src/user/create.ts new file mode 100644 index 00000000..d7a6ea6b --- /dev/null +++ b/packages/identite/src/user/create.ts @@ -0,0 +1,42 @@ +// + +import { hashToPostgresParams } from "#src/services"; +import type { DatabaseContext, User } from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function createUserFactory({ pg }: DatabaseContext) { + return async function createUser({ + email, + encrypted_password = null, + }: { + email: string; + encrypted_password?: string | null; + }) { + const userWithTimestamps = { + email, + email_verified: false, + verify_email_token: null, + verify_email_sent_at: null, + encrypted_password, + magic_link_token: null, + magic_link_sent_at: null, + reset_password_token: null, + reset_password_sent_at: null, + sign_in_count: 0, + last_sign_in_at: null, + created_at: new Date(), + updated_at: new Date(), + }; + + const { paramsString, valuesString, values } = + hashToPostgresParams(userWithTimestamps); + + const { rows }: QueryResult = await pg.query( + `INSERT INTO users ${paramsString} VALUES ${valuesString} RETURNING *;`, + values, + ); + return rows.shift()!; + }; +} diff --git a/packages/identite/src/user/find-by-email.test.ts b/packages/identite/src/user/find-by-email.test.ts index 8e449a9e..03872c22 100644 --- a/packages/identite/src/user/find-by-email.test.ts +++ b/packages/identite/src/user/find-by-email.test.ts @@ -2,6 +2,7 @@ import { PGlite } from "@electric-sql/pglite"; import { expect } from "chai"; +import { noop } from "lodash-es"; import { before, describe, it } from "mocha"; import { runner } from "node-pg-migrate"; import { join } from "path"; @@ -18,47 +19,22 @@ before(async function migrate() { dir: join(import.meta.dirname, "../../../../migrations"), direction: "up", migrationsTable: "pg-migrate", + log: noop, }); }); describe("FindByEmail", () => { it("should find a user by email", async () => { - await pg.sql`INSERT INTO users + await pg.sql`insert into users (id, email, created_at, updated_at, given_name, family_name, phone_number, job) - VALUES - (1, 'lion.eljonson@darkangels.world', '4444-04-04', '4444-04-04', 'Lion', 'El''Jonson', 'I', 'Primarque'), - (2, 'perturabo@ironwarriors.world', '4444-04-04', '4444-04-04', 'Lion', 'El''Jonson', 'IV', 'Primarque'); + values + (1, 'lion.eljonson@darkangels.world', '4444-04-04', '4444-04-04', 'lion', 'el''jonson', 'i', 'primarque'), + (2, 'perturabo@ironwarriors.world', '4444-04-04', '4444-04-04', 'lion', 'el''jonson', 'iv', 'primarque'); `; const user = await findByEmail("lion.eljonson@darkangels.world"); - expect(user).to.deep.equal({ - created_at: new Date("4444-04-04T00:00:00.000Z"), - current_challenge: null, - email_verified_at: null, - email_verified: false, - email: "lion.eljonson@darkangels.world", - encrypted_password: "", - encrypted_totp_key: null, - family_name: "El'Jonson", - force_2fa: false, - given_name: "Lion", - id: 1, - job: "Primarque", - last_sign_in_at: null, - magic_link_sent_at: null, - magic_link_token: null, - needs_inclusionconnect_onboarding_help: false, - needs_inclusionconnect_welcome_page: false, - phone_number: "I", - reset_password_sent_at: null, - reset_password_token: null, - sign_in_count: 0, - totp_key_verified_at: null, - updated_at: new Date("4444-04-04T00:00:00.000Z"), - verify_email_sent_at: null, - verify_email_token: null, - }); + expect(user?.email).to.equal("lion.eljonson@darkangels.world"); }); it("❎ fail to find the God-Emperor of Mankind", async () => { diff --git a/packages/identite/src/user/index.ts b/packages/identite/src/user/index.ts index 843716a0..466f4bce 100644 --- a/packages/identite/src/user/index.ts +++ b/packages/identite/src/user/index.ts @@ -1,3 +1,5 @@ // +export * from "./create.js"; export * from "./find-by-email.js"; +export * from "./update.js"; diff --git a/packages/identite/src/user/update.test.ts b/packages/identite/src/user/update.test.ts new file mode 100644 index 00000000..5bba1028 --- /dev/null +++ b/packages/identite/src/user/update.test.ts @@ -0,0 +1,36 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { updateUserFactory } from "./update.js"; + +// + +const pg = new PGlite(); +const updateUser = updateUserFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + log: noop, + migrationsTable: "pg-migrate", + }); +}); + +describe("UpdateUser", () => { + it("should update the user job", async () => { + await pg.sql`INSERT INTO users + (id, email, created_at, updated_at, given_name, family_name, phone_number, job) + VALUES + (1, 'lion.eljonson@darkangels.world', '4444-04-04', '4444-04-04', 'Lion', 'El''Jonson', 'I', 'Primarque'); + `; + const user = await updateUser(1, { job: "Chevalier de l'Ordre" }); + expect(user.job).to.equal("Chevalier de l'Ordre"); + }); +}); diff --git a/packages/identite/src/user/update.ts b/packages/identite/src/user/update.ts new file mode 100644 index 00000000..e1299fbe --- /dev/null +++ b/packages/identite/src/user/update.ts @@ -0,0 +1,29 @@ +// + +import { hashToPostgresParams } from "#src/services"; +import type { DatabaseContext, User } from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function updateUserFactory({ pg }: DatabaseContext) { + return async function updateUser(id: number, fieldsToUpdate: Partial) { + const fieldsToUpdateWithTimestamps = { + ...fieldsToUpdate, + updated_at: new Date(), + }; + + const { paramsString, valuesString, values } = hashToPostgresParams( + fieldsToUpdateWithTimestamps, + ); + + const { rows }: QueryResult = await pg.query( + `UPDATE users SET ${paramsString} = ${valuesString} WHERE id = $${ + values.length + 1 + } RETURNING *`, + [...values, id], + ); + + return rows.shift()!; + }; +} diff --git a/scripts/import-accounts-coop.ts b/scripts/import-accounts-coop.ts index 93508c92..aa602304 100644 --- a/scripts/import-accounts-coop.ts +++ b/scripts/import-accounts-coop.ts @@ -5,7 +5,11 @@ import { isPhoneNumberValid, isSiretValid, } from "@gouvfr-lasuite/proconnect.core/security"; -import { findByEmailFactory } from "@gouvfr-lasuite/proconnect.identite/user"; +import { + createUserFactory, + findByEmailFactory, + updateUserFactory, +} from "@gouvfr-lasuite/proconnect.identite/user"; import { getInseeAccessTokenFactory } from "@gouvfr-lasuite/proconnect.insee/token"; import { AxiosError } from "axios"; import { parse, stringify, transform } from "csv"; @@ -24,7 +28,6 @@ import { linkUserToOrganization, upsert, } from "../src/repositories/organization/setters"; -import { create, update } from "../src/repositories/user"; import { logger } from "../src/services/log"; import { getNumberOfLineInFile, @@ -48,6 +51,8 @@ const getInseeAccessToken = getInseeAccessTokenFactory( const pg = getDatabaseConnection(); const findByEmail = findByEmailFactory({ pg }); +const create = createUserFactory({ pg }); +const update = updateUserFactory({ pg }); // diff --git a/scripts/import-domains.ts b/scripts/import-domains.ts index e967b79f..68fad590 100644 --- a/scripts/import-domains.ts +++ b/scripts/import-domains.ts @@ -3,6 +3,7 @@ import { isDomainValid, isSiretValid, } from "@gouvfr-lasuite/proconnect.core/security"; +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { AxiosError } from "axios"; import { parse, stringify, transform } from "csv"; import fs from "fs"; diff --git a/src/connectors/api-sirene/codes-effectifs.ts b/src/connectors/api-sirene/codes-effectifs.ts index 6caa1719..3d4b00cb 100644 --- a/src/connectors/api-sirene/codes-effectifs.ts +++ b/src/connectors/api-sirene/codes-effectifs.ts @@ -1,3 +1,5 @@ +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.identite/types"; + export const codesEffectifs: { [K in NonNullable]: string } = { NN: "Unité non employeuse (pas de salarié au cours de l'année de référence et pas d'effectif au 31/12)", diff --git a/src/connectors/api-sirene/index.ts b/src/connectors/api-sirene/index.ts index 1e357591..659d798e 100644 --- a/src/connectors/api-sirene/index.ts +++ b/src/connectors/api-sirene/index.ts @@ -1,3 +1,7 @@ +import type { + OrganizationInfo, + TrancheEffectifs, +} from "@gouvfr-lasuite/proconnect.identite/types"; import { getInseeAccessTokenFactory } from "@gouvfr-lasuite/proconnect.insee/token"; import axios, { AxiosError, type AxiosResponse } from "axios"; import { cloneDeep, set } from "lodash-es"; diff --git a/src/managers/organization/join.ts b/src/managers/organization/join.ts index 1e5adfa7..130eb59d 100644 --- a/src/managers/organization/join.ts +++ b/src/managers/organization/join.ts @@ -1,5 +1,9 @@ import { isEmailValid } from "@gouvfr-lasuite/proconnect.core/security"; import { Welcome } from "@gouvfr-lasuite/proconnect.email"; +import type { + Organization, + OrganizationInfo, +} from "@gouvfr-lasuite/proconnect.identite/types"; import * as Sentry from "@sentry/node"; import { isEmpty, some } from "lodash-es"; import { diff --git a/src/managers/organization/main.ts b/src/managers/organization/main.ts index a3ae80da..6a676aa7 100644 --- a/src/managers/organization/main.ts +++ b/src/managers/organization/main.ts @@ -1,3 +1,4 @@ +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty, some } from "lodash-es"; import { NotFoundError } from "../../config/errors"; import { diff --git a/src/repositories/organization/getters.ts b/src/repositories/organization/getters.ts index fbb2c8df..7d66fa2e 100644 --- a/src/repositories/organization/getters.ts +++ b/src/repositories/organization/getters.ts @@ -1,4 +1,7 @@ -import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; +import type { + Organization, + User, +} from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; diff --git a/src/repositories/organization/setters.ts b/src/repositories/organization/setters.ts index 88b90631..3429b39d 100644 --- a/src/repositories/organization/setters.ts +++ b/src/repositories/organization/setters.ts @@ -1,137 +1,10 @@ +import { upsertFactory } from "@gouvfr-lasuite/proconnect.identite/organization"; import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; import { hashToPostgresParams } from "../../services/hash-to-postgres-params"; -export const upsert = async ({ - siret, - organizationInfo: { - libelle: cached_libelle, - nomComplet: cached_nom_complet, - enseigne: cached_enseigne, - trancheEffectifs: cached_tranche_effectifs, - trancheEffectifsUniteLegale: cached_tranche_effectifs_unite_legale, - libelleTrancheEffectif: cached_libelle_tranche_effectif, - etatAdministratif: cached_etat_administratif, - estActive: cached_est_active, - statutDiffusion: cached_statut_diffusion, - estDiffusible: cached_est_diffusible, - adresse: cached_adresse, - codePostal: cached_code_postal, - codeOfficielGeographique: cached_code_officiel_geographique, - activitePrincipale: cached_activite_principale, - libelleActivitePrincipale: cached_libelle_activite_principale, - categorieJuridique: cached_categorie_juridique, - libelleCategorieJuridique: cached_libelle_categorie_juridique, - }, -}: { - siret: string; - organizationInfo: OrganizationInfo; -}) => { - const connection = getDatabaseConnection(); - - const { rows }: QueryResult = await connection.query( - ` -INSERT INTO organizations - ( - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - organization_info_fetched_at, - updated_at, - created_at - ) -VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) -ON CONFLICT (siret) -DO UPDATE - SET ( - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - organization_info_fetched_at, - updated_at - ) = ( - EXCLUDED.siret, - EXCLUDED.cached_libelle, - EXCLUDED.cached_nom_complet, - EXCLUDED.cached_enseigne, - EXCLUDED.cached_tranche_effectifs, - EXCLUDED.cached_tranche_effectifs_unite_legale, - EXCLUDED.cached_libelle_tranche_effectif, - EXCLUDED.cached_etat_administratif, - EXCLUDED.cached_est_active, - EXCLUDED.cached_statut_diffusion, - EXCLUDED.cached_est_diffusible, - EXCLUDED.cached_adresse, - EXCLUDED.cached_code_postal, - EXCLUDED.cached_code_officiel_geographique, - EXCLUDED.cached_activite_principale, - EXCLUDED.cached_libelle_activite_principale, - EXCLUDED.cached_categorie_juridique, - EXCLUDED.cached_libelle_categorie_juridique, - EXCLUDED.organization_info_fetched_at, - EXCLUDED.updated_at - ) -RETURNING * -`, - [ - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - new Date(), - new Date(), - new Date(), - ], - ); - - return rows.shift()!; -}; +export const upsert = upsertFactory({ pg: getDatabaseConnection() }); export const linkUserToOrganization = async ({ organization_id, diff --git a/src/repositories/user.ts b/src/repositories/user.ts index 78a6a8f8..75783d27 100644 --- a/src/repositories/user.ts +++ b/src/repositories/user.ts @@ -1,7 +1,11 @@ import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; +import { + createUserFactory, + findByEmailFactory, + updateUserFactory, +} from "@gouvfr-lasuite/proconnect.identite/user"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../connectors/postgres"; -import { hashToPostgresParams } from "../services/hash-to-postgres-params"; export const findById = async (id: number) => { const connection = getDatabaseConnection(); @@ -16,19 +20,7 @@ FROM users WHERE id = $1 return rows.shift(); }; -export const findByEmail = async (email: string) => { - const connection = getDatabaseConnection(); - - const { rows }: QueryResult = await connection.query( - ` -SELECT * -FROM users WHERE email = $1 -`, - [email], - ); - - return rows.shift(); -}; +export const findByEmail = findByEmailFactory({ pg: getDatabaseConnection() }); export const findByMagicLinkToken = async (magic_link_token: string) => { const connection = getDatabaseConnection(); @@ -60,63 +52,9 @@ FROM users WHERE reset_password_token = $1 return rows.shift(); }; -export const update = async (id: number, fieldsToUpdate: Partial) => { - const connection = getDatabaseConnection(); - - const fieldsToUpdateWithTimestamps = { - ...fieldsToUpdate, - updated_at: new Date(), - }; - - const { paramsString, valuesString, values } = hashToPostgresParams( - fieldsToUpdateWithTimestamps, - ); +export const update = updateUserFactory({ pg: getDatabaseConnection() }); - const { rows }: QueryResult = await connection.query( - `UPDATE users SET ${paramsString} = ${valuesString} WHERE id = $${ - values.length + 1 - } RETURNING *`, - [...values, id], - ); - - return rows.shift()!; -}; - -export const create = async ({ - email, - encrypted_password = null, -}: { - email: string; - encrypted_password?: string | null; -}) => { - const connection = getDatabaseConnection(); - - const userWithTimestamps = { - email, - email_verified: false, - verify_email_token: null, - verify_email_sent_at: null, - encrypted_password, - magic_link_token: null, - magic_link_sent_at: null, - reset_password_token: null, - reset_password_sent_at: null, - sign_in_count: 0, - last_sign_in_at: null, - created_at: new Date(), - updated_at: new Date(), - }; - - const { paramsString, valuesString, values } = - hashToPostgresParams(userWithTimestamps); - - const { rows }: QueryResult = await connection.query( - `INSERT INTO users ${paramsString} VALUES ${valuesString} RETURNING *;`, - values, - ); - - return rows.shift()!; -}; +export const create = createUserFactory({ pg: getDatabaseConnection() }); export const deleteUser = async (id: number) => { const connection = getDatabaseConnection(); diff --git a/src/services/organization.ts b/src/services/organization.ts index 668ecacb..2eeea3f2 100644 --- a/src/services/organization.ts +++ b/src/services/organization.ts @@ -1,4 +1,5 @@ import { isDomainValid } from "@gouvfr-lasuite/proconnect.core/security"; +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; /** * These fonctions return approximate results. As the data tranche effectifs is diff --git a/src/services/script-helpers.ts b/src/services/script-helpers.ts index a532b106..994bd0f8 100644 --- a/src/services/script-helpers.ts +++ b/src/services/script-helpers.ts @@ -1,4 +1,5 @@ // from https://ipirozhenko.com/blog/measuring-requests-duration-nodejs-express/ +import type { OrganizationInfo } from "@gouvfr-lasuite/proconnect.identite/types"; import fs from "fs"; import { isEmpty } from "lodash-es"; diff --git a/src/types/organization.d.ts b/src/types/organization.d.ts index 2f779a1f..e69de29b 100644 --- a/src/types/organization.d.ts +++ b/src/types/organization.d.ts @@ -1,24 +0,0 @@ -interface Organization { - id: number; - siret: string; - created_at: Date; - updated_at: Date; - cached_libelle: string | null; - cached_nom_complet: string | null; - cached_enseigne: string | null; - cached_tranche_effectifs: TrancheEffectifs; - cached_tranche_effectifs_unite_legale: string | null; - cached_libelle_tranche_effectif: string | null; - cached_etat_administratif: string | null; - cached_est_active: string | null; - cached_statut_diffusion: string | null; - cached_est_diffusible: string | null; - cached_adresse: string | null; - cached_code_postal: string | null; - cached_code_officiel_geographique: string | null; - cached_activite_principale: string | null; - cached_libelle_activite_principale: string | null; - cached_categorie_juridique: string | null; - cached_libelle_categorie_juridique: string | null; - organization_info_fetched_at: Date | null; -} diff --git a/test/organization.test.ts b/test/organization.test.ts index 67eb9f15..44d73f4b 100644 --- a/test/organization.test.ts +++ b/test/organization.test.ts @@ -1,3 +1,4 @@ +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { assert } from "chai"; import { isCommune,