From a5056b6fcf1e7457526508478e3761e2dbd744ca Mon Sep 17 00:00:00 2001 From: Douglas DUTEIL Date: Mon, 27 Jan 2025 16:35:01 +0100 Subject: [PATCH] refactor(debounce): extract debounce connector to individual pkg --- .changeset/witty-ducks-hammer.md | 7 +++ Dockerfile | 2 + package-lock.json | 32 ++++++++-- package.json | 4 +- packages/debounce/LICENSE | 21 +++++++ packages/debounce/README.md | 25 ++++++++ packages/debounce/package.json | 59 +++++++++++++++++++ packages/debounce/src/api/index.ts | 3 + .../src/api/single-validation.test.ts | 32 ++++++++++ .../debounce/src/api/single-validation.ts | 34 +++++++++++ packages/debounce/src/types/index.ts | 48 +++++++++++++++ packages/debounce/tsconfig.json | 18 ++++++ packages/debounce/tsconfig.lib.json | 9 +++ src/config/env.zod.ts | 2 +- src/connectors/debounce.ts | 50 +++------------- test/env.zod.test.ts | 1 + 16 files changed, 298 insertions(+), 49 deletions(-) create mode 100644 .changeset/witty-ducks-hammer.md create mode 100644 packages/debounce/LICENSE create mode 100644 packages/debounce/README.md create mode 100644 packages/debounce/package.json create mode 100644 packages/debounce/src/api/index.ts create mode 100644 packages/debounce/src/api/single-validation.test.ts create mode 100644 packages/debounce/src/api/single-validation.ts create mode 100644 packages/debounce/src/types/index.ts create mode 100644 packages/debounce/tsconfig.json create mode 100644 packages/debounce/tsconfig.lib.json diff --git a/.changeset/witty-ducks-hammer.md b/.changeset/witty-ducks-hammer.md new file mode 100644 index 000000000..11c719a33 --- /dev/null +++ b/.changeset/witty-ducks-hammer.md @@ -0,0 +1,7 @@ +--- +"@gouvfr-lasuite/proconnect.debounce": minor +--- + +♻️ Prélèvement d'un partie du connecteur Debounce + +Dans le cadres la suggestion d'email sur PCF, une partie du connecteur Debounce est maintenant dans le package `@gouvfr-lasuite/proconnect.debounce`. diff --git a/Dockerfile b/Dockerfile index 44b8dd3e2..879545c05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ --mount=type=bind,source=packages/crisp/package.json,target=packages/crisp/package.json \ + --mount=type=bind,source=packages/debounce/package.json,target=packages/debounce/package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ @@ -23,6 +24,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ --mount=type=bind,source=packages/crisp/package.json,target=packages/crisp/package.json \ + --mount=type=bind,source=packages/debounce/package.json,target=packages/debounce/package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ diff --git a/package-lock.json b/package-lock.json index b0e6e6419..5e70c51a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,16 @@ "license": "MIT", "workspaces": [ "packages/core", + "packages/crisp", + "packages/debounce", "packages/email", "packages/insee", - "packages/crisp", "packages/identite" ], "dependencies": { "@gouvfr-lasuite/proconnect.core": "workspace:*", "@gouvfr-lasuite/proconnect.crisp": "workspace:*", + "@gouvfr-lasuite/proconnect.debounce": "workspace:*", "@gouvfr-lasuite/proconnect.email": "workspace:*", "@gouvfr-lasuite/proconnect.identite": "workspace:*", "@gouvfr-lasuite/proconnect.insee": "workspace:*", @@ -1317,6 +1319,10 @@ "resolved": "packages/crisp", "link": true }, + "node_modules/@gouvfr-lasuite/proconnect.debounce": { + "resolved": "packages/debounce", + "link": true + }, "node_modules/@gouvfr-lasuite/proconnect.email": { "resolved": "packages/email", "link": true @@ -11583,9 +11589,9 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -11658,6 +11664,24 @@ "tsx": "^4.19.2" } }, + "packages/debounce": { + "name": "@gouvfr-lasuite/proconnect.debounce", + "version": "0.3.2", + "license": "MIT", + "dependencies": { + "axios": "^1.7.7", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "pkgroll": "^2.6.1", + "tsx": "^4.19.2" + } + }, "packages/email": { "name": "@gouvfr-lasuite/proconnect.email", "version": "0.1.0", diff --git a/package.json b/package.json index d4bde7869..8ad00c2a5 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,10 @@ "main": "src/index.js", "workspaces": [ "packages/core", + "packages/crisp", + "packages/debounce", "packages/email", "packages/insee", - "packages/crisp", "packages/identite" ], "scripts": { @@ -53,6 +54,7 @@ }, "dependencies": { "@gouvfr-lasuite/proconnect.core": "workspace:*", + "@gouvfr-lasuite/proconnect.debounce": "workspace:*", "@gouvfr-lasuite/proconnect.crisp": "workspace:*", "@gouvfr-lasuite/proconnect.email": "workspace:*", "@gouvfr-lasuite/proconnect.identite": "workspace:*", diff --git a/packages/debounce/LICENSE b/packages/debounce/LICENSE new file mode 100644 index 000000000..883045232 --- /dev/null +++ b/packages/debounce/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Direction Interministérielle du Numérique - Gouvernement Français + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/debounce/README.md b/packages/debounce/README.md new file mode 100644 index 000000000..10c36d734 --- /dev/null +++ b/packages/debounce/README.md @@ -0,0 +1,25 @@ +# 📦 @gouvfr-lasuite/proconnect.debounce + +> ⚡ Typed Debounce API for ProConnect + +## ⚙️ Installation + +```bash +npm install @gouvfr-lasuite/proconnect.debounce +``` + +## 📖 Usage + +### [Single Validation](https://developers.debounce.io/reference/single-validation) + +```ts +import { singleValidationFactory } from "@gouvfr-lasuite/proconnect.debounce/api"; + +const singleValidation = singleValidationFactory(DEBOUNCE_API_KEY); + +const response = await singleValidation("test@test.com"); +``` + +## 📖 License + +[MIT](./LICENSE.md) diff --git a/packages/debounce/package.json b/packages/debounce/package.json new file mode 100644 index 000000000..5740d1846 --- /dev/null +++ b/packages/debounce/package.json @@ -0,0 +1,59 @@ +{ + "name": "@gouvfr-lasuite/proconnect.debounce", + "version": "0.3.2", + "homepage": "https://github.com/numerique-gouv/moncomptepro/tree/master/packages/debounce#readme", + "bugs": "https://github.com/numerique-gouv/moncomptepro/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/numerique-gouv/moncomptepro.git", + "directory": "packages/debounce" + }, + "license": "MIT", + "sideEffects": false, + "type": "module", + "imports": {}, + "exports": { + "./api": { + "require": "./dist/api/index.cjs", + "import": "./dist/api/index.js", + "types": "./dist/api/index.d.ts" + } + }, + "typesVersions": { + "*": { + "api": [ + "./dist/api/index.d.ts" + ] + } + }, + "scripts": { + "build": "pkgroll --tsconfig=tsconfig.lib.json", + "check": "npm run build -- --noEmit", + "dev": "npm run build -- --watch --preserveWatchOutput", + "test": "mocha" + }, + "mocha": { + "reporter": "spec", + "require": [ + "tsx" + ], + "spec": "src/**/*.test.ts" + }, + "dependencies": { + "axios": "^1.7.7", + "zod": "^3.24.1" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "pkgroll": "^2.6.1", + "tsx": "^4.19.2" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/debounce/src/api/index.ts b/packages/debounce/src/api/index.ts new file mode 100644 index 000000000..b68418db5 --- /dev/null +++ b/packages/debounce/src/api/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./single-validation.js"; diff --git a/packages/debounce/src/api/single-validation.test.ts b/packages/debounce/src/api/single-validation.test.ts new file mode 100644 index 000000000..6139b7e68 --- /dev/null +++ b/packages/debounce/src/api/single-validation.test.ts @@ -0,0 +1,32 @@ +// + +import { expect } from "chai"; +import { before, describe } from "mocha"; +import { singleValidationFactory } from "./single-validation.js"; + +// + +const { DEBOUNCE_API_KEY } = process.env; +const singleValidation = singleValidationFactory(DEBOUNCE_API_KEY ?? ""); + +// + +describe("singleValidationFactory", () => { + before(function () { + if (!DEBOUNCE_API_KEY) this.skip(); + }); + + it("should return a valid response", async function () { + const response = await singleValidation("test@test.com"); + expect(response).includes({ + email: "test@test.com", + code: "3", + role: "true", + free_email: "true", + result: "Invalid", + reason: "Disposable, Role", + send_transactional: "0", + did_you_mean: "test@toke.com", + }); + }); +}); diff --git a/packages/debounce/src/api/single-validation.ts b/packages/debounce/src/api/single-validation.ts new file mode 100644 index 000000000..28eda108e --- /dev/null +++ b/packages/debounce/src/api/single-validation.ts @@ -0,0 +1,34 @@ +// + +import type { DebounceSuccessResponse } from "#src/types/index.js"; +import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; + +// + +/** + * Perform a single email validation request. + * + * @see https://developers.debounce.io/reference/single-validation#response-parameters + * @param apiKey the debounce.io API key + * @param config the Axios request config + * @returns Debounce Single Validation response + */ +export function singleValidationFactory( + apiKey: string, + config?: AxiosRequestConfig, +) { + return async function singleValidation(email: string) { + const { + data: { debounce }, + }: AxiosResponse = await axios({ + method: "get", + url: `https://api.debounce.io/v1/?email=${email}&api=${apiKey}`, + headers: { + accept: "application/json", + }, + ...config, + }); + + return debounce; + }; +} diff --git a/packages/debounce/src/types/index.ts b/packages/debounce/src/types/index.ts new file mode 100644 index 000000000..83426ff16 --- /dev/null +++ b/packages/debounce/src/types/index.ts @@ -0,0 +1,48 @@ +// + +/** + * @see https://developers.debounce.io/reference/responses#error-response + */ + +export interface DebounceErrorResponse { + debounce: { + error: string; + code: "0"; + }; + success: "0"; +} + +/** + * @see https://developers.debounce.io/reference/responses#success-response + */ +export interface DebounceSuccessResponse { + debounce: { + // The email address you are requesting to validate. + // ex: 'test@wanadoo.rf' + email: string; + // DeBounce validation response code. + // ex: '6' + code: string; + // Is the email role-based or not. Role emails such as "sales@", "webmaster@" etc. are not suitable for sending marketing emails to. + // ['true', 'false'] + role: string; + // Is the email from a free email provider - like Gmail - or not. + // ['true', 'false'] + free_email: string; + // The final result of the validation process. This response will help to determine whether you should send marketing emails to a recipient or not. + // ['Invalid', 'Risky', 'Safe to Send', 'Unknown'] + result: string; + // The reason why the result is given. + // ex: 'Bounce, Role' + reason: string; + // Is it suggested that you send transactional emails to the recipient or not (0: no, 1: yes). Generally, it is suggested to send transactional emails to Valid, Accept-all, and Unknown emails. + // ['0', '1'] + send_transactional: string; + // If you use a misspelled email address like someemail@gmial.com, the validation engine tries to suggest you the corrected email address. + // ex: 'test@wanadoo.fr', '' + did_you_mean: string; + }; + + success: "1"; + balance: `${number}`; +} diff --git a/packages/debounce/tsconfig.json b/packages/debounce/tsconfig.json new file mode 100644 index 000000000..ec2a63458 --- /dev/null +++ b/packages/debounce/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "src", + "types": ["node"], + "module": "NodeNext", + "moduleResolution": "nodenext", + "verbatimModuleSyntax": true, + "paths": { + "#src/*": ["./src/*"] + } + }, + "extends": "@tsconfig/node22/tsconfig.json", + "references": [] +} diff --git a/packages/debounce/tsconfig.lib.json b/packages/debounce/tsconfig.lib.json new file mode 100644 index 000000000..3ba354418 --- /dev/null +++ b/packages/debounce/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "exclude": ["src/**/*.test.ts"], + "extends": "./tsconfig.json", + "include": ["src"] +} diff --git a/src/config/env.zod.ts b/src/config/env.zod.ts index 75667a0a9..de40b000a 100644 --- a/src/config/env.zod.ts +++ b/src/config/env.zod.ts @@ -14,7 +14,7 @@ export const connectorEnvSchema = z.object({ CRISP_WEBSITE_ID: z.string().default(""), CRISP_MODERATION_TAG: zCoerceArray(z.string()).default("identite,moderation"), DATABASE_URL: z.string().url(), - DEBOUNCE_API_KEY: z.string().optional(), + DEBOUNCE_API_KEY: z.string().default(""), INSEE_CONSUMER_KEY: z.string(), INSEE_CONSUMER_SECRET: z.string(), REDIS_URL: z.string().url().default("redis://:@127.0.0.1:6379"), diff --git a/src/connectors/debounce.ts b/src/connectors/debounce.ts index 55aba9aad..7491399a4 100644 --- a/src/connectors/debounce.ts +++ b/src/connectors/debounce.ts @@ -1,4 +1,4 @@ -import axios, { type AxiosResponse } from "axios"; +import { singleValidationFactory } from "@gouvfr-lasuite/proconnect.debounce/api"; import { DEBOUNCE_API_KEY, EMAIL_DELIVERABILITY_WHITELIST, @@ -8,41 +8,15 @@ import { import { getEmailDomain } from "../services/email"; import { logger } from "../services/log"; -// documentation: https://developers.debounce.io/reference/single-validation#response-parameters -type DebounceResponse = { - debounce: { - // The email address you are requesting to validate. - // ex: 'test@wanadoo.rf' - email: string; - // DeBounce validation response code. - // ex: '6' - code: string; - // Is the email role-based or not. Role emails such as "sales@", "webmaster@" etc. are not suitable for sending marketing emails to. - // ['true', 'false'] - role: string; - // Is the email from a free email provider - like Gmail - or not. - // ['true', 'false'] - free_email: string; - // The final result of the validation process. This response will help to determine whether you should send marketing emails to a recipient or not. - // ['Invalid', 'Risky', 'Safe to Send', 'Unknown'] - result: string; - // The reason why the result is given. - // ex: 'Bounce, Role' - reason: string; - // Is it suggested that you send transactional emails to the recipient or not (0: no, 1: yes). Generally, it is suggested to send transactional emails to Valid, Accept-all, and Unknown emails. - // ['0', '1'] - send_transactional: string; - // If you use a misspelled email address like someemail@gmial.com, the validation engine tries to suggest you the corrected email address. - // ex: 'test@wanadoo.fr', '' - did_you_mean: string; - }; -}; - type EmailDebounceInfo = { isEmailSafeToSend: boolean; didYouMean?: string; }; +const singleValidation = singleValidationFactory(DEBOUNCE_API_KEY, { + timeout: HTTP_CLIENT_TIMEOUT, +}); + export const isEmailSafeToSendTransactional = async ( email: string, ): Promise => { @@ -60,18 +34,8 @@ export const isEmailSafeToSendTransactional = async ( } try { - const { - data: { - debounce: { send_transactional, did_you_mean: didYouMean }, - }, - }: AxiosResponse = await axios({ - method: "get", - url: `https://api.debounce.io/v1/?email=${email}&api=${DEBOUNCE_API_KEY}`, - headers: { - accept: "application/json", - }, - timeout: HTTP_CLIENT_TIMEOUT, - }); + const { send_transactional, did_you_mean: didYouMean } = + await singleValidation(email); logger.info( `Email address "${email}" is ${ diff --git a/test/env.zod.test.ts b/test/env.zod.test.ts index ed44cec61..a44ee2f1a 100644 --- a/test/env.zod.test.ts +++ b/test/env.zod.test.ts @@ -38,6 +38,7 @@ test("default sample env with configured INSEE secrets", () => { CRISP_WEBSITE_ID: "", DATABASE_URL: "postgres://moncomptepro:moncomptepro@127.0.0.1:5432/moncomptepro", + DEBOUNCE_API_KEY: "", DEPLOY_ENV: "localhost", DIRTY_DS_REDIRECTION_URL: "https://www.demarches-simplifiees.fr/agent_connect/logout_from_mcp",