From 9c7e9e26be5d27ed873b0d5f12c15ec2fbe95119 Mon Sep 17 00:00:00 2001 From: Kilian Panot Date: Mon, 2 Sep 2024 11:39:10 +0900 Subject: [PATCH] feat(ama-sdk): move ApiFetchClient to dedicated package --- apps/showcase/package.json | 1 + apps/showcase/src/app/app.module.ts | 4 +- packages/@ama-sdk/client-fetch/.eslintrc.js | 19 ++ packages/@ama-sdk/client-fetch/.gitignore | 10 + packages/@ama-sdk/client-fetch/.npmignore | 0 packages/@ama-sdk/client-fetch/README.md | 33 +++ .../@ama-sdk/client-fetch/collection.json | 11 + packages/@ama-sdk/client-fetch/jest.config.js | 10 + packages/@ama-sdk/client-fetch/package.json | 127 ++++++++++ packages/@ama-sdk/client-fetch/project.json | 84 +++++++ .../client-fetch/schematics/ng-add/index.js | 13 + .../client-fetch/schematics/ng-add/index.ts | 83 +++++++ .../schematics/ng-add/migration/import-map.ts | 36 +++ .../schematics/ng-add/schema.json | 18 ++ .../client-fetch/schematics/ng-add/schema.ts | 6 + .../client-fetch/src/api-fetch-client.ts | 187 +++++++++++++++ .../@ama-sdk/client-fetch/src/fetch-plugin.ts | 45 ++++ .../src/plugins/abort/abort.fetch.ts | 90 +++++++ .../src/plugins/abort/abort.spec.ts | 44 ++++ .../client-fetch/src/plugins/abort/index.ts | 1 + .../client-fetch/src/plugins/abort/readme.md | 59 +++++ .../plugins/concurrent/concurrent.fetch.ts | 70 ++++++ .../src/plugins/concurrent/concurrent.spec.ts | 61 +++++ .../src/plugins/concurrent/index.ts | 1 + .../src/plugins/concurrent/readme.md | 7 + .../client-fetch/src/plugins/index.ts | 8 + .../src/plugins/keepalive/index.ts | 1 + .../plugins/keepalive/keepalive.request.ts | 43 ++++ .../src/plugins/keepalive/keepalive.spec.ts | 18 ++ .../src/plugins/keepalive/readme.md | 7 + .../src/plugins/mock-intercept/README.md | 28 +++ .../src/plugins/mock-intercept/index.ts | 2 + .../mock-intercept/mock-intercept.fetch.ts | 58 +++++ .../mock-intercept.interface.ts | 10 + .../mock-intercept/mock-intercept.spec.ts | 189 +++++++++++++++ .../src/plugins/perf-metric/index.ts | 1 + .../plugins/perf-metric/perf-metric.fetch.ts | 222 ++++++++++++++++++ .../perf-metric/perf-metric.probe.spec.ts | 58 +++++ .../src/plugins/perf-metric/readme.md | 7 + .../client-fetch/src/plugins/retry/index.ts | 1 + .../client-fetch/src/plugins/retry/readme.md | 7 + .../src/plugins/retry/retry.fetch.ts | 107 +++++++++ .../src/plugins/retry/retry.spec.ts | 105 +++++++++ .../client-fetch/src/plugins/timeout/index.ts | 1 + .../src/plugins/timeout/readme.md | 50 ++++ .../src/plugins/timeout/timeout.fetch.ts | 140 +++++++++++ .../src/plugins/timeout/timeout.spec.ts | 165 +++++++++++++ .../src/plugins/wait-for/index.ts | 1 + .../src/plugins/wait-for/readme.md | 7 + .../src/plugins/wait-for/wait-for.fetch.ts | 139 +++++++++++ .../src/plugins/wait-for/wait-for.spec.ts | 73 ++++++ .../@ama-sdk/client-fetch/src/public_api.ts | 3 + .../testing/jest.config.ut.builders.js | 15 ++ .../client-fetch/testing/jest.config.ut.js | 21 ++ .../client-fetch/testing/setup-jest.ts | 3 + .../@ama-sdk/client-fetch/tsconfig.build.json | 27 +++ .../client-fetch/tsconfig.builders.json | 19 ++ .../client-fetch/tsconfig.eslint.json | 9 + packages/@ama-sdk/client-fetch/tsconfig.json | 12 + .../@ama-sdk/client-fetch/tsconfig.spec.json | 25 ++ packages/@ama-sdk/core/README.md | 38 +-- packages/@ama-sdk/core/package.json | 1 - .../core/src/clients/api-fetch-client.ts | 15 +- .../@ama-sdk/core/src/fwk/core/api-client.ts | 1 + .../core/src/plugins/abort/abort.fetch.ts | 1 + .../@ama-sdk/core/src/plugins/abort/readme.md | 3 + .../plugins/concurrent/concurrent.fetch.ts | 1 + .../core/src/plugins/concurrent/readme.md | 3 + .../core/src/plugins/core/fetch-plugin.ts | 8 +- .../plugins/keepalive/keepalive.request.ts | 1 + .../core/src/plugins/keepalive/readme.md | 3 + .../core/src/plugins/mock-intercept/README.md | 3 + .../mock-intercept/mock-intercept.fetch.ts | 1 + .../mock-intercept.interface.ts | 5 +- .../plugins/perf-metric/perf-metric.fetch.ts | 3 + .../core/src/plugins/perf-metric/readme.md | 3 + .../@ama-sdk/core/src/plugins/retry/readme.md | 3 + .../core/src/plugins/retry/retry.fetch.ts | 2 + .../core/src/plugins/timeout/readme.md | 11 +- .../core/src/plugins/timeout/timeout.fetch.ts | 1 + .../core/src/plugins/wait-for/readme.md | 3 + .../src/plugins/wait-for/wait-for.fetch.ts | 12 +- packages/@ama-sdk/core/tsconfig.spec.json | 2 +- packages/@ama-sdk/create/README.md | 4 +- packages/@ama-sdk/schematics/README.md | 13 +- .../typescriptFetch/spec/api-mock.mustache | 3 +- .../templates/base/package.json.template | 5 + packages/@ama-sdk/showcase-sdk/package.json | 5 + .../showcase-sdk/src/spec/api-mock.ts | 3 +- packages/@o3r/apis-manager/README.md | 4 +- packages/@o3r/apis-manager/package.json | 5 + .../helpers/update-api-deps/index.ts | 12 +- .../apis-manager/schematics/ng-add/index.ts | 4 + .../schematics/ng-add/schema.json | 5 + .../apis-manager/schematics/ng-add/schema.ts | 6 + tsconfig.base.json | 3 + tsconfig.build.json | 1 + yarn.lock | 86 ++++++- 98 files changed, 2835 insertions(+), 50 deletions(-) create mode 100644 packages/@ama-sdk/client-fetch/.eslintrc.js create mode 100644 packages/@ama-sdk/client-fetch/.gitignore create mode 100644 packages/@ama-sdk/client-fetch/.npmignore create mode 100644 packages/@ama-sdk/client-fetch/README.md create mode 100644 packages/@ama-sdk/client-fetch/collection.json create mode 100644 packages/@ama-sdk/client-fetch/jest.config.js create mode 100644 packages/@ama-sdk/client-fetch/package.json create mode 100644 packages/@ama-sdk/client-fetch/project.json create mode 100644 packages/@ama-sdk/client-fetch/schematics/ng-add/index.js create mode 100644 packages/@ama-sdk/client-fetch/schematics/ng-add/index.ts create mode 100644 packages/@ama-sdk/client-fetch/schematics/ng-add/migration/import-map.ts create mode 100644 packages/@ama-sdk/client-fetch/schematics/ng-add/schema.json create mode 100644 packages/@ama-sdk/client-fetch/schematics/ng-add/schema.ts create mode 100644 packages/@ama-sdk/client-fetch/src/api-fetch-client.ts create mode 100644 packages/@ama-sdk/client-fetch/src/fetch-plugin.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/abort/abort.fetch.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/abort/abort.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/abort/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/abort/readme.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.fetch.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/concurrent/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/concurrent/readme.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/keepalive/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.request.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/keepalive/readme.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/README.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.fetch.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.interface.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/perf-metric/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.fetch.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.probe.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/perf-metric/readme.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/retry/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/retry/readme.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/retry/retry.fetch.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/retry/retry.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/timeout/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/timeout/readme.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.fetch.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/wait-for/index.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/wait-for/readme.md create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.fetch.ts create mode 100644 packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.spec.ts create mode 100644 packages/@ama-sdk/client-fetch/src/public_api.ts create mode 100644 packages/@ama-sdk/client-fetch/testing/jest.config.ut.builders.js create mode 100644 packages/@ama-sdk/client-fetch/testing/jest.config.ut.js create mode 100644 packages/@ama-sdk/client-fetch/testing/setup-jest.ts create mode 100644 packages/@ama-sdk/client-fetch/tsconfig.build.json create mode 100644 packages/@ama-sdk/client-fetch/tsconfig.builders.json create mode 100644 packages/@ama-sdk/client-fetch/tsconfig.eslint.json create mode 100644 packages/@ama-sdk/client-fetch/tsconfig.json create mode 100644 packages/@ama-sdk/client-fetch/tsconfig.spec.json diff --git a/apps/showcase/package.json b/apps/showcase/package.json index 67779ade4a..90eac6c030 100644 --- a/apps/showcase/package.json +++ b/apps/showcase/package.json @@ -27,6 +27,7 @@ "dependencies": { "@agnos-ui/angular-bootstrap": "~0.4.1", "@agnos-ui/core-bootstrap": "~0.4.1", + "@ama-sdk/client-fetch": "workspace:^", "@ama-sdk/core": "workspace:^", "@ama-sdk/schematics": "workspace:^", "@ama-sdk/showcase-sdk": "workspace:^", diff --git a/apps/showcase/src/app/app.module.ts b/apps/showcase/src/app/app.module.ts index 83b6cab82d..1f9ba93aa7 100644 --- a/apps/showcase/src/app/app.module.ts +++ b/apps/showcase/src/app/app.module.ts @@ -1,4 +1,4 @@ -import {ApiClient, ApiFetchClient} from '@ama-sdk/core'; +import { ApiFetchClient } from '@ama-sdk/client-fetch'; import {PetApi} from '@ama-sdk/showcase-sdk'; import { registerLocaleData } from '@angular/common'; import localeEN from '@angular/common/locales/en'; @@ -46,7 +46,7 @@ registerLocaleData(localeEN, 'en-GB'); registerLocaleData(localeFR, 'fr-FR'); function petApiFactory(logger: Logger) { - const apiConfig: ApiClient = new ApiFetchClient( + const apiConfig = new ApiFetchClient( { basePath: 'https://petstore3.swagger.io/api/v3', requestPlugins: [], diff --git a/packages/@ama-sdk/client-fetch/.eslintrc.js b/packages/@ama-sdk/client-fetch/.eslintrc.js new file mode 100644 index 0000000000..f5ca5f3878 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/.eslintrc.js @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable quote-props */ + +module.exports = { + 'root': true, + 'parserOptions': { + 'tsconfigRootDir': __dirname, + 'project': [ + 'tsconfig.build.json', + 'tsconfig.builders.json', + 'tsconfig.spec.json', + 'tsconfig.eslint.json' + ], + 'sourceType': 'module' + }, + 'extends': [ + '../../../.eslintrc.js' + ] +}; diff --git a/packages/@ama-sdk/client-fetch/.gitignore b/packages/@ama-sdk/client-fetch/.gitignore new file mode 100644 index 0000000000..03eb2be86c --- /dev/null +++ b/packages/@ama-sdk/client-fetch/.gitignore @@ -0,0 +1,10 @@ +/fwk +/helpers +/index.* +/bundles +/test +/dist-test +/plugins +/dist +/utils +/build diff --git a/packages/@ama-sdk/client-fetch/.npmignore b/packages/@ama-sdk/client-fetch/.npmignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@ama-sdk/client-fetch/README.md b/packages/@ama-sdk/client-fetch/README.md new file mode 100644 index 0000000000..8d02c285c7 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/README.md @@ -0,0 +1,33 @@ +# Ama SDK Fetch Client + +[![Stable Version](https://img.shields.io/npm/v/@ama-sdk/client-fetch?style=for-the-badge)](https://www.npmjs.com/package/@ama-sdk/client-fetch) +[![Bundle Size](https://img.shields.io/bundlephobia/min/@ama-sdk/client-fetch?color=green&style=for-the-badge)](https://www.npmjs.com/package/@ama-sdk/client-fetch) + +This package exposes the **Api Fetch Client** from an SDK based on [@ama-sdk/core](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core). + +This package contains all the [Fetch Plugins](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins), helpers and object definitions to dialog with an API following the `ama-sdk` architecture. + +> [!TIP] +> Please refer to the [SDK initializer](https://www.npmjs.com/package/@ama-sdk/create) package for getting started with an API client SDK based on `ama-sdk` architecture. + +## Setup + +The **Api Fetch Client** can be added to your project via the following command: + +```shell +ng add @ama-sdk/client-fetch +``` + +> [!NOTE] +> In case of migration from deprecated `ApiFetchClient` imported from `@ama-sdk/core`, the `ng add` command will replace, in your existing code, the import from `@ama-sdk/core` to `@ama-sdk/client-fetch` for the deprecated dependencies. + +## Available plugins + +- [abort](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/abort) +- [concurrent](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/concurrent) +- [keepalive](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/keepalive) +- [mock-intercept](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/mock-intercept) +- [perf-metric](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/perf-metric) +- [retry](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/retry) +- [timeout](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/timeout) +- [wait-for](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/wait-for) diff --git a/packages/@ama-sdk/client-fetch/collection.json b/packages/@ama-sdk/client-fetch/collection.json new file mode 100644 index 0000000000..c30f786b86 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/collection.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/angular/angular-cli/main/packages/angular_devkit/schematics/collection-schema.json", + "schematics": { + "ng-add": { + "description": "Add the SDK Fetch Client API to the project.", + "factory": "./schematics/ng-add/index#ngAdd", + "schema": "./schematics/ng-add/schema.json", + "aliases": ["install", "i"] + } + } +} diff --git a/packages/@ama-sdk/client-fetch/jest.config.js b/packages/@ama-sdk/client-fetch/jest.config.js new file mode 100644 index 0000000000..237fd70fc2 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/jest.config.js @@ -0,0 +1,10 @@ +const getJestGlobalConfig = require('../../../jest.config.ut').getJestGlobalConfig; + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestGlobalConfig(), + projects: [ + '/testing/jest.config.ut.js', + '/testing/jest.config.ut.builders.js' + ] +}; diff --git a/packages/@ama-sdk/client-fetch/package.json b/packages/@ama-sdk/client-fetch/package.json new file mode 100644 index 0000000000..459a1ef298 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/package.json @@ -0,0 +1,127 @@ +{ + "name": "@ama-sdk/client-fetch", + "version": "0.0.0-placeholder", + "publishConfig": { + "access": "public" + }, + "description": "API Request client for @ama-sdk/core based SDK", + "module": "dist/src/public_api.js", + "esm2015": "dist/esm2015/public_api.js", + "esm2020": "dist/src/public_api.js", + "typings": "dist/src/public_api.d.ts", + "sideEffects": false, + "exports": { + "./package.json": { + "default": "./package.json" + }, + ".": { + "module": "./dist/src/public_api.js", + "esm2020": "./dist/src/public_api.js", + "esm2015": "./dist/esm2015/public_api.js", + "es2020": "./dist/cjs/public_api.js", + "default": "./dist/cjs/public_api.js", + "typings": "./dist/src/public_api.d.ts", + "import": "./dist/src/public_api.js", + "node": "./dist/cjs/public_api.js", + "require": "./dist/cjs/public_api.js" + } + }, + "scripts": { + "nx": "nx", + "ng": "yarn nx", + "build": "yarn nx build ama-sdk-client-fetch", + "build:cjs": "swc src -d dist/cjs -C module.type=commonjs -q --strip-leading-paths", + "build:esm2015": "swc src -d dist/esm2015 -C module.type=es6 -q --strip-leading-paths", + "build:esm2020": "tsc -b tsconfig.build.json", + "postbuild": "yarn cpy './package.json' dist && patch-package-json-main", + "prepare:build:builders": "yarn cpy 'schematics/**/*.json' dist/schematics && yarn cpy 'collection.json' dist", + "build:builders": "tsc -b tsconfig.builders.json --pretty && yarn generate-cjs-manifest", + "prepare:publish": "prepare-publish ./dist" + }, + "dependencies": { + "@swc/helpers": "~0.5.0", + "tslib": "^2.6.2", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@ama-sdk/core": "workspace:^", + "@angular-devkit/schematics": "~18.2.0", + "@angular/cli": "~18.2.0", + "@angular/common": "~18.2.0", + "@o3r/schematics": "workspace:^", + "@schematics/angular": "~18.2.0", + "isomorphic-fetch": "^3.0.0", + "typescript": "~5.5.4" + }, + "peerDependenciesMeta": { + "@angular-devkit/schematics": { + "optional": true + }, + "@angular/cli": { + "optional": true + }, + "@angular/common": { + "optional": true + }, + "@o3r/schematics": { + "optional": true + }, + "@schematics/angular": { + "optional": true + }, + "isomorphic-fetch": { + "optional": true + }, + "typescript": { + "optional": true + } + }, + "devDependencies": { + "@ama-sdk/core": "workspace:^", + "@angular-devkit/core": "~18.2.0", + "@angular-devkit/schematics": "~18.2.0", + "@angular-eslint/eslint-plugin": "~18.3.0", + "@angular/common": "~18.2.0", + "@angular/core": "~18.2.0", + "@nx/eslint-plugin": "~19.5.0", + "@nx/jest": "~19.5.0", + "@o3r/build-helpers": "workspace:^", + "@o3r/eslint-plugin": "workspace:^", + "@o3r/test-helpers": "workspace:^", + "@schematics/angular": "~18.2.0", + "@stylistic/eslint-plugin-ts": "~2.4.0", + "@swc/cli": "~0.4.0", + "@swc/core": "~1.7.0", + "@types/jest": "~29.5.2", + "@types/node": "^20.0.0", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", + "@typescript-eslint/utils": "^7.14.1", + "cpy-cli": "^5.0.0", + "eslint": "^8.57.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-plugin-jest": "~28.8.0", + "eslint-plugin-jsdoc": "~48.11.0", + "eslint-plugin-prefer-arrow": "~1.2.3", + "eslint-plugin-unicorn": "^54.0.0", + "isomorphic-fetch": "~3.0.0", + "jest": "~29.7.0", + "jest-junit": "~16.0.0", + "jsonc-eslint-parser": "~2.4.0", + "minimist": "^1.2.6", + "pid-from-port": "^1.1.3", + "rimraf": "^5.0.1", + "rxjs": "^7.8.1", + "semver": "^7.5.2", + "ts-jest": "~29.2.0", + "ts-node": "~10.9.2", + "type-fest": "^4.10.2", + "typescript": "~5.5.4", + "zone.js": "~0.14.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "schematics": "./collection.json" +} diff --git a/packages/@ama-sdk/client-fetch/project.json b/packages/@ama-sdk/client-fetch/project.json new file mode 100644 index 0000000000..1569ee01c1 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/project.json @@ -0,0 +1,84 @@ +{ + "name": "ama-sdk-client-fetch", + "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/@ama-sdk/client-fetch/src", + "prefix": "o3r", + "targets": { + "build": { + "executor": "nx:run-script", + "outputs": ["{projectRoot}/dist/package.json"], + "options": { + "script": "postbuild" + }, + "dependsOn": [ + "build-builders", + "compile", + "build-esm2015", + "build-cjs" + ] + }, + "build-esm2015": { + "executor": "nx:run-script", + "options": { + "script": "build:esm2015" + } + }, + "build-cjs": { + "executor": "nx:run-script", + "options": { + "script": "build:cjs" + } + }, + "compile": { + "executor": "nx:run-script", + "options": { + "script": "build:esm2020" + }, + "outputs": ["{projectRoot}/dist/src"] + }, + "lint": { + "options": { + "eslintConfig": "packages/@ama-sdk/client-fetch/.eslintrc.js", + "lintFilePatterns": [ + "packages/@ama-sdk/client-fetch/src/**/*.ts", + "packages/@ama-sdk/client-fetch/package.json" + ] + }, + "dependsOn": [ + "build" + ] + }, + "test": { + "executor": "@nx/jest:jest", + "options": { + "jestConfig": "packages/@ama-sdk/client-fetch/jest.config.js" + } + }, + "prepare-publish": { + "executor": "nx:run-script", + "options": { + "script": "prepare:publish" + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "npm publish packages/@ama-sdk/client-fetch/dist" + } + }, + "prepare-build-builders": { + "executor": "nx:run-script", + "options": { + "script": "prepare:build:builders" + } + }, + "build-builders": { + "executor": "nx:run-script", + "options": { + "script": "build:builders" + } + } + }, + "tags": [] +} diff --git a/packages/@ama-sdk/client-fetch/schematics/ng-add/index.js b/packages/@ama-sdk/client-fetch/schematics/ng-add/index.js new file mode 100644 index 0000000000..31936d01bc --- /dev/null +++ b/packages/@ama-sdk/client-fetch/schematics/ng-add/index.js @@ -0,0 +1,13 @@ +/* + +This files is used to allow the usage of the builder within @o3r/framework mono-repository. +It should not be part of the package. + +*/ + +const {resolve} = require('node:path'); + +require('ts-node').register({ project: resolve(__dirname, '..', '..', 'tsconfig.builders.json') }); +require('ts-node').register = () => {}; + +module.exports = require('./index.ts'); diff --git a/packages/@ama-sdk/client-fetch/schematics/ng-add/index.ts b/packages/@ama-sdk/client-fetch/schematics/ng-add/index.ts new file mode 100644 index 0000000000..2e73dcb99a --- /dev/null +++ b/packages/@ama-sdk/client-fetch/schematics/ng-add/index.ts @@ -0,0 +1,83 @@ +import { chain, noop, Rule } from '@angular-devkit/schematics'; +import type { NgAddSchematicsSchema } from './schema'; +import * as path from 'node:path'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; +import { mapMigrationFromCoreImports } from './migration/import-map'; + +const devDependenciesToInstall: string[] = [ + +]; + + +const reportMissingSchematicsDep = (logger: { error: (message: string) => any }) => (reason: any) => { + logger.error(`[ERROR]: Adding @ama-sdk/client-fetch has failed. +If the error is related to missing @o3r dependencies you need to install '@o3r/schematics' as devDependency to be able to use this schematics. Please run 'ng add @o3r/schematics'. +Otherwise, use the error message as guidance.`); + throw reason; +}; + +/** + * Add SDk Fetch Client to an Otter Project + * @param options + */ +function ngAddFn(options: NgAddSchematicsSchema): Rule { + return async (tree, context) => { + // use dynamic import to properly raise an exception if it is not an Otter project. + const { + getPackageInstallConfig, + applyEsLintFix, + setupDependencies, + getO3rPeerDeps, + getProjectNewDependenciesTypes, + getWorkspaceConfig, + getExternalDependenciesVersionRange, + updateImports + } = await import('@o3r/schematics'); + + const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + const depsInfo = getO3rPeerDeps(packageJsonPath); + + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `${options.exactO3rVersion ? '' : '~'}${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }], + ngAddOptions: { exactO3rVersion: options.exactO3rVersion } + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName, false, !!options.exactO3rVersion)); + Object.entries(getExternalDependenciesVersionRange(devDependenciesToInstall, packageJsonPath, context.logger)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); + + return chain([ + // optional custom action dedicated to this module + options.skipLinter ? noop() : applyEsLintFix(), + // add the missing Otter modules in the current project + setupDependencies({ + projectName: options.projectName, + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps + }), + + updateImports(mapMigrationFromCoreImports) + ]); + }; +} + +/** + * Add SDk Fetch Client to an Otter Project + * @param options + */ +export const ngAdd = (options: NgAddSchematicsSchema): Rule => async (_, { logger }) => { + const { createSchematicWithMetricsIfInstalled } = await import('@o3r/schematics').catch(reportMissingSchematicsDep(logger)); + return createSchematicWithMetricsIfInstalled(ngAddFn)(options); +}; diff --git a/packages/@ama-sdk/client-fetch/schematics/ng-add/migration/import-map.ts b/packages/@ama-sdk/client-fetch/schematics/ng-add/migration/import-map.ts new file mode 100644 index 0000000000..7cbf85ccdb --- /dev/null +++ b/packages/@ama-sdk/client-fetch/schematics/ng-add/migration/import-map.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const currentPackage = { + newPackage: '@ama-sdk/client-fetch' +}; + +export const mapMigrationFromCoreImports = { + '@ama-sdk/core': [ + 'FetchCall', + 'FetchPluginContext', + 'PluginAsyncStarter', + 'FetchPlugin', + 'BaseApiFetchClientOptions', + 'BaseApiFetchClientConstructor', + 'ApiFetchClient', + 'AbortCallback', + 'AbortFetch', + 'ConcurrentFetch', + 'KeepaliveRequest', + 'MockInterceptFetch', + 'MockInterceptFetchParameters', + 'Mark', + 'PerformanceMetricOptions', + 'PerformanceMetricPlugin', + 'RetryConditionType', + 'RetryFetch', + 'TimeoutStatus', + 'TimeoutPauseEventHandler', + 'TimeoutPauseEventHandlerFactory', + 'impervaCaptchaEventHandlerFactory', + 'TimeoutFetch', + 'CallbackFunction', + 'CanStartConditionResult', + 'CanStartConditionFunction', + 'WaitForFetch' + ].reduce((acc, name) => ({...acc, [name]: currentPackage}), {} as Record) +}; diff --git a/packages/@ama-sdk/client-fetch/schematics/ng-add/schema.json b/packages/@ama-sdk/client-fetch/schematics/ng-add/schema.json new file mode 100644 index 0000000000..ad368bba30 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/schematics/ng-add/schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ngAddSchematicsSchema", + "title": "Add Otter ama-sdk-client-fetch ", + "description": "ngAdd Otter ama-sdk-client-fetch", + "properties": { + "projectName": { + "type": "string", + "description": "Project name", + "$default": { + "$source": "projectName" + } + } + }, + "additionalProperties": true, + "required": [ + ] +} diff --git a/packages/@ama-sdk/client-fetch/schematics/ng-add/schema.ts b/packages/@ama-sdk/client-fetch/schematics/ng-add/schema.ts new file mode 100644 index 0000000000..76d3853e73 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/schematics/ng-add/schema.ts @@ -0,0 +1,6 @@ +import type { SchematicOptionObject } from '@o3r/schematics'; + +export interface NgAddSchematicsSchema extends SchematicOptionObject { + /** Project name */ + projectName?: string | undefined; +} diff --git a/packages/@ama-sdk/client-fetch/src/api-fetch-client.ts b/packages/@ama-sdk/client-fetch/src/api-fetch-client.ts new file mode 100644 index 0000000000..5b35b71d9a --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/api-fetch-client.ts @@ -0,0 +1,187 @@ +import type { + ApiClient, + ApiTypes, + BaseApiClientOptions, + PartialExcept, + PluginAsyncRunner, + RequestOptions, + RequestOptionsParameters, + ReviverType, + TokenizedOptions +} from '@ama-sdk/core'; +import { + CanceledCallError, + EmptyResponseError, + ExceptionReply, + extractQueryParams, + filterUndefinedValues, + getResponseReviver, + prepareUrl, + processFormData, + ResponseJSONParseError, + ReviverReply, + tokenizeRequestOptions +} from '@ama-sdk/core'; +import type { FetchCall, FetchPlugin, PluginAsyncStarter } from './fetch-plugin'; + +/** @see BaseApiClientOptions */ +export interface BaseApiFetchClientOptions extends BaseApiClientOptions { + /** List of plugins to apply to the fetch call */ + fetchPlugins: FetchPlugin[]; +} + +/** @see BaseApiConstructor */ +export interface BaseApiFetchClientConstructor extends PartialExcept { +} + +const DEFAULT_OPTIONS: Omit = { + replyPlugins: [new ReviverReply(), new ExceptionReply()], + fetchPlugins: [], + requestPlugins: [], + enableTokenization: false, + disableFallback: false +}; + +/** Client to process the call to the API using Fetch API */ +export class ApiFetchClient implements ApiClient { + + /** @inheritdoc */ + public options: BaseApiFetchClientOptions; + + /** + * Initialize your API Client instance + * @param options Configuration of the API Client + */ + constructor(options: BaseApiFetchClientConstructor) { + this.options = { + ...DEFAULT_OPTIONS, + ...options + }; + } + + /** @inheritdoc */ + public extractQueryParams(data: T, names: (keyof T)[]): { [p in keyof T]: string; } { + return extractQueryParams(data, names); + } + + /** @inheritdoc */ + public tokenizeRequestOptions(url: string, queryParameters: { [key: string]: string }, piiParamTokens: { [key: string]: string }, data: any): TokenizedOptions | undefined { + return this.options.enableTokenization ? tokenizeRequestOptions(url, queryParameters, piiParamTokens, data) : undefined; + } + + /** @inheritdoc */ + public async getRequestOptions(requestOptionsParameters: RequestOptionsParameters): Promise { + let opts: RequestOptions = { + ...requestOptionsParameters, + headers: new Headers(filterUndefinedValues(requestOptionsParameters.headers)), + queryParams: filterUndefinedValues(requestOptionsParameters.queryParams) + }; + if (this.options.requestPlugins) { + for (const plugin of this.options.requestPlugins) { + opts = await plugin.load({ + logger: this.options.logger, + apiName: requestOptionsParameters.api?.apiName + }).transform(opts); + } + } + + return opts; + } + + /** @inheritdoc */ + public prepareUrl(url: string, queryParameters: { [key: string]: string | undefined } = {}) { + return prepareUrl(url, queryParameters); + } + + /** @inheritdoc */ + public processFormData(data: any, type: string) { + return processFormData(data, type); + } + + /** @inheritdoc */ + public async processCall(url: string, options: RequestOptions, apiType: ApiTypes | string, apiName: string, revivers?: undefined, operationId?: string): Promise; + public async processCall(url: string, options: RequestOptions, apiType: ApiTypes, apiName: string, revivers: ReviverType | { [statusCode: number]: ReviverType | undefined }, + operationId?: string): Promise; + public async processCall(url: string, options: RequestOptions, apiType: ApiTypes | string, apiName: string, + revivers?: ReviverType | undefined | { [statusCode: number]: ReviverType | undefined }, operationId?: string): Promise { + + let response: Response | undefined; + let asyncResponse: Promise; + let root: any; + let body: string | undefined; + let exception: Error | undefined; + + const origin = options.headers.get('Origin'); + + // Execute call + try { + + const metadataSignal = options.metadata?.signal; + metadataSignal?.throwIfAborted(); + + const controller = new AbortController(); + options.signal = controller.signal; + metadataSignal?.addEventListener('abort', () => controller.abort()); + + const loadedPlugins: (PluginAsyncRunner & PluginAsyncStarter)[] = []; + if (this.options.fetchPlugins) { + loadedPlugins.push(...this.options.fetchPlugins.map((plugin) => plugin.load({url, options, fetchPlugins: loadedPlugins, controller, apiClient: this, logger: this.options.logger}))); + } + + const canStart = await Promise.all(loadedPlugins.map((plugin) => !plugin.canStart || plugin.canStart())); + const isCanceledBy = canStart.indexOf(false); + if (isCanceledBy >= 0) { + // One of the fetch plugins cancelled the execution of the call + asyncResponse = Promise.reject(new CanceledCallError(`Is canceled by the plugin ${isCanceledBy}`, isCanceledBy, this.options.fetchPlugins[isCanceledBy], {apiName, operationId, url, origin})); + } else { + asyncResponse = fetch(url, options); + } + + for (const plugin of loadedPlugins) { + asyncResponse = plugin.transform(asyncResponse); + } + + response = await asyncResponse; + + body = await response.text(); + } catch (e: any) { + if (e instanceof CanceledCallError) { + exception = e; + } else { + exception = new EmptyResponseError(e.message || 'Fail to Fetch', undefined, {apiName, operationId, url, origin}); + } + } + + try { + root = body ? JSON.parse(body) : undefined; + } catch (e: any) { + exception = new ResponseJSONParseError(e.message || 'Fail to parse response body', response && response.status || 0, body, {apiName, operationId, url, origin}); + } + // eslint-disable-next-line no-console + const reviver = getResponseReviver(revivers, response, operationId, {disableFallback: this.options.disableFallback, log: console.error}); + const replyPlugins = this.options.replyPlugins ? + this.options.replyPlugins.map((plugin) => plugin.load({ + dictionaries: root && root.dictionaries, + response, + reviver, + apiType, + apiName, + exception, + operationId, + url, + origin, + logger: this.options.logger + })) : []; + + let parsedData = root; + for (const pluginRunner of replyPlugins) { + parsedData = await pluginRunner.transform(parsedData); + } + + if (exception) { + throw exception; + } + + return parsedData; + } +} diff --git a/packages/@ama-sdk/client-fetch/src/fetch-plugin.ts b/packages/@ama-sdk/client-fetch/src/fetch-plugin.ts new file mode 100644 index 0000000000..26cc8834ac --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/fetch-plugin.ts @@ -0,0 +1,45 @@ +import type { ApiClient ,Plugin, PluginAsyncRunner, PluginContext, RequestOptions } from '@ama-sdk/core'; + +/** Fetch Call Response type */ +export type FetchCall = Promise; + +/** + * Interface of an SDK reply plugin. + * The plugin will be run on the reply of a call + */ +export interface FetchPluginContext extends PluginContext { + /** URL targeted */ + url: string; + + /** Fetch call options */ + options: RequestInit | RequestOptions; + + /** List of loaded plugins apply to the fetch call */ + fetchPlugins: PluginAsyncRunner[]; + + /** Api Client processing the call the the API */ + apiClient: ApiClient; + + /** Abort controller to abort fetch call */ + controller: AbortController; +} + +/** + * Interface of an async plugin starter + */ +export interface PluginAsyncStarter { + /** Determine if the action can start */ + canStart?(): boolean | Promise; +} + +/** + * Interface of a Fetch plugin. + * The plugin will be run around the Fetch call + */ +export interface FetchPlugin extends Plugin { + /** + * Load the plugin with the context + * @param context Context of fetch plugin + */ + load(context: FetchPluginContext): PluginAsyncRunner & PluginAsyncStarter; +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/abort/abort.fetch.ts b/packages/@ama-sdk/client-fetch/src/plugins/abort/abort.fetch.ts new file mode 100644 index 0000000000..8c2ce6dd90 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/abort/abort.fetch.ts @@ -0,0 +1,90 @@ +import type { RequestOptions } from '@ama-sdk/core'; +import type { FetchCall, FetchPlugin, FetchPluginContext } from '../../fetch-plugin'; + +interface AbortCallbackParameters { + /** URL targeted */ + url: string; + + /** Fetch call options */ + options: RequestInit | RequestOptions; + + /** Abort controller to abort fetch call */ + controller: AbortController; +} + +const isPromise = (result: boolean | void | Promise | Promise): result is (Promise | Promise) => { + if (typeof result !== 'object') { + return false; + } + + return true; +}; + +/** + * Abort callback + * Returns `true` to abort a request (or access directly to the controller to cancel fetch request) + * @example Immediate abort on URL match + * ```typescript + * const abortCondition: AbortCallback = ({url}) => url.endsWith('pet'); + * + * const client = new ApiFetchClient( + * { + * basePath: 'https://petstore3.swagger.io/api/v3', + * fetchPlugins: [new AbortFetch(abortCondition)] + * } + * ); + * ``` + * @example Abort on external event + * ```typescript + * import { firstValueFrom } from 'rxjs'; + * import { myObservable } from 'somewhere'; + * + * const abortCondition: AbortCallback = ((observable: any) => () => firstValueFrom(observable).then((value) => !!value))(myObservable); + * + * const client = new ApiFetchClient( + * { + * basePath: 'https://petstore3.swagger.io/api/v3', + * fetchPlugins: [new AbortFetch(abortCondition)] + * } + * ); + * ``` + * @example Abort on Timeout + * ```typescript + * const abortCondition: AbortCallback = ({controller}) => { setTimeout(() => controller?.abort(), 3000); }; + * + * const client = new ApiFetchClient( + * { + * basePath: 'https://petstore3.swagger.io/api/v3', + * fetchPlugins: [new AbortFetch(abortCondition)] + * } + * ); + * ``` + */ +export type AbortCallback = (controller?: AbortCallbackParameters) => void | boolean | Promise | Promise; + +/** Plugin to abort a Fetch request */ +export class AbortFetch implements FetchPlugin { + + /** + * Abort Fetch plugin + * @param abortCallback Condition that should be passed to start the call + */ + constructor(public abortCallback: AbortCallback) { + } + + + /** @inheritDoc */ + public load(context: FetchPluginContext) { + return { + transform: (fetchCall: FetchCall) => { + const abortCallbackResult = this.abortCallback(); + if (isPromise(abortCallbackResult)) { + void abortCallbackResult.then((res) => res && context.controller?.abort()); + } else if (abortCallbackResult) { + context.controller?.abort(); + } + return fetchCall; + } + }; + } +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/abort/abort.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/abort/abort.spec.ts new file mode 100644 index 0000000000..3d593d3d9d --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/abort/abort.spec.ts @@ -0,0 +1,44 @@ +import { AbortFetch } from './abort.fetch'; + +describe('Abort Plugin', () => { + + it('should trigger the callback', async () => { + const fn = jest.fn(); + const plugin = new AbortFetch(fn); + + const runner = plugin.load({} as any); + await runner.transform(Promise.resolve() as Promise); + + expect(fn).toHaveBeenCalled(); + }); + + it('should trigger abort signal if true', async () => { + const defaultContext = { + controller: { + abort: jest.fn() + } + }; + const fn = jest.fn().mockResolvedValue(true); + const plugin = new AbortFetch(fn); + + const runner = plugin.load(defaultContext as any); + await runner.transform(Promise.resolve() as Promise); + + expect(defaultContext.controller.abort).toHaveBeenCalled(); + }); + + it('should not trigger abort signal if false', async () => { + const defaultContext = { + controller: { + abort: jest.fn() + } + }; + const fn = jest.fn().mockResolvedValue(false); + const plugin = new AbortFetch(fn); + + const runner = plugin.load(defaultContext as any); + await runner.transform(Promise.resolve() as Promise); + + expect(defaultContext.controller.abort).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/abort/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/abort/index.ts new file mode 100644 index 0000000000..09f439ed79 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/abort/index.ts @@ -0,0 +1 @@ +export * from './abort.fetch'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/abort/readme.md b/packages/@ama-sdk/client-fetch/src/plugins/abort/readme.md new file mode 100644 index 0000000000..3c58089d56 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/abort/readme.md @@ -0,0 +1,59 @@ +## Abort + +Plugin to abort a Fetch call. + +### Usage examples + +### Immediate abort on URL match + +```typescript +import { AbortFetch, type AbortCallback } from '@ama-sdk/core'; + +const abortCondition: AbortCallback = ({url}) => url.endsWith('pet'); + +const client = new ApiFetchClient( + { + basePath: 'https://petstore3.swagger.io/api/v3', + fetchPlugins: [new AbortFetch(abortCondition)] + } +); +``` + +### Abort on external event + +```typescript +import { AbortFetch, type AbortCallback } from '@ama-sdk/core'; +import { firstValueFrom } from 'rxjs'; +import { myObservable } from 'somewhere'; + +const abortCondition: AbortCallback = ((observable: any) => () => firstValueFrom(observable).then((value) => !!value))(myObservable); + +const client = new ApiFetchClient( + { + basePath: 'https://petstore3.swagger.io/api/v3', + fetchPlugins: [new AbortFetch(abortCondition)] + } +); +``` + +### Abort on Timeout + +```typescript +import { AbortFetch, type AbortCallback } from '@ama-sdk/core'; + +const abortCondition: AbortCallback = ({controller}) => { setTimeout(() => controller?.abort(), 3000); }; + +const client = new ApiFetchClient( + { + basePath: 'https://petstore3.swagger.io/api/v3', + fetchPlugins: [new AbortFetch(abortCondition)] + } +); +``` + +> [!WARN] +> We recommend to use the [Timeout plugin](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/timeout) to implement more easily and properly a request timeout. + +### Type of plugins + +- Fetch plugin: [AbortFetch](./abort.fetch.ts) diff --git a/packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.fetch.ts b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.fetch.ts new file mode 100644 index 0000000000..5455b71e98 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.fetch.ts @@ -0,0 +1,70 @@ +import { FetchCall, FetchPlugin, FetchPluginContext } from '../../fetch-plugin'; + +/** + * Plugin to limit the number of concurrent call + */ +export class ConcurrentFetch implements FetchPlugin { + + /** Maximum number of concurrent call */ + public maxConcurrentPoolSize: number; + + /** Pool of pending fetch calls */ + public pool: FetchCall[] = []; + + /** Size of the pool of concurrent calls */ + private poolSize = 0; + + /** List of calls waiting to start */ + private readonly waitingResolvers: ((value: boolean) => void)[] = []; + + /** + * Concurrent Fetch plugin + * @param maxConcurrentPoolSize Maximum number of concurrent call + */ + constructor(maxConcurrentPoolSize = 10) { + this.maxConcurrentPoolSize = maxConcurrentPoolSize; + } + + /** + * Return true if a new call can start + */ + private canStart() { + return this.poolSize <= this.maxConcurrentPoolSize; + } + + /** + * Unstack and resolve the promise stopping the call to start + */ + private unstackResolve() { + if (this.canStart() && this.waitingResolvers.length) { + this.waitingResolvers.shift()!(true); + } + } + + /** @inheritDoc */ + public load(_context: FetchPluginContext) { + this.poolSize++; + + return { + canStart: () => new Promise((resolve) => this.canStart() ? resolve(true) : this.waitingResolvers.push(resolve)), + + transform: async (fetchCall: FetchCall) => { + this.pool.push(fetchCall); + + try { + const fetchResponse = await fetchCall; + return fetchResponse; + // eslint-disable-next-line no-useless-catch + } catch (e) { + throw e; + } finally { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.pool = this.pool.filter((call) => call !== fetchCall); + this.poolSize--; + this.unstackResolve(); + } + } + }; + } + +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.spec.ts new file mode 100644 index 0000000000..da666066f8 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/concurrent.spec.ts @@ -0,0 +1,61 @@ +import {ConcurrentFetch} from './concurrent.fetch'; + +describe('Concurrent Fetch Plugin', () => { + + it('should start if the limit is not reach', async () => { + const plugin = new ConcurrentFetch(3); + + plugin.load({} as any); + const runner = plugin.load({} as any); + const canStart = await runner.canStart(); + + expect(canStart).toBe(true); + }); + + it('should retrieve the fetch call in the pool', () => { + const plugin = new ConcurrentFetch(3); + const call = new Promise((resolve) => setTimeout(() => resolve(undefined), 1000)); + const test = new Promise((resolve) => setTimeout(() => resolve(undefined), 1000)); + + void plugin.load({} as any).transform(call); + void plugin.load({} as any).transform(call); + void plugin.load({} as any).transform(test); + + expect(plugin.pool[2]).toBe(test); + }); + + it('should start only when the pool is available', async () => { + const plugin = new ConcurrentFetch(2); + const resolves: [any, any] = [null, null]; + + const call0: any = new Promise((resolve) => resolves[0] = resolve); + const call1: any = new Promise((resolve) => resolves[1] = resolve); + const result = {res: false}; + + void plugin.load({} as any).transform(call0); + const runner1 = plugin.load({} as any); + const canStart1 = runner1.canStart(); + await jest.runAllTimersAsync(); + + expect(await canStart1).toBe(true); + void runner1.transform(call1); + + const runner2 = plugin.load({} as any); + const pCanStart2 = runner2.canStart(); + await jest.runAllTimersAsync(); + + expect((plugin as any).waitingResolvers.length).toBe(1); + + result.res = true; + + resolves[0](); + + expect(await pCanStart2).toBe(true); + + resolves[1](); + + await jest.advanceTimersByTimeAsync(500); + + expect((plugin as any).waitingResolvers.length).toBe(0); + }); +}); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/concurrent/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/index.ts new file mode 100644 index 0000000000..92b74c5e98 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/index.ts @@ -0,0 +1 @@ +export * from './concurrent.fetch'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/concurrent/readme.md b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/readme.md new file mode 100644 index 0000000000..cd64f15932 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/concurrent/readme.md @@ -0,0 +1,7 @@ +## Concurrent + +Plugin to limit the number of concurrent calls. + +### Type of plugins + +- Fetch plugin: [ConcurrentFetch](./concurrent.fetch.ts); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/index.ts new file mode 100644 index 0000000000..489f3533d7 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/index.ts @@ -0,0 +1,8 @@ +export * from './abort/index'; +export * from './concurrent/index'; +export * from './keepalive/index'; +export * from './mock-intercept/index'; +export * from './perf-metric/index'; +export * from './retry/index'; +export * from './timeout/index'; +export * from './wait-for/index'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/keepalive/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/index.ts new file mode 100644 index 0000000000..77b8a1c872 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/index.ts @@ -0,0 +1 @@ +export * from './keepalive.request'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.request.ts b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.request.ts new file mode 100644 index 0000000000..7c3fdff78c --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.request.ts @@ -0,0 +1,43 @@ +import { PluginRunner, RequestOptions, RequestPlugin } from '@ama-sdk/core'; + +/** + * Plugin to add the keepalive flag to the request + */ +export class KeepaliveRequest implements RequestPlugin { + + private active: boolean | undefined; + + constructor(force = false) { + if (force) { + this.active = true; + } else { + void this.testKeepAlive(); + } + } + + /** + * Keepalive flag has a partial support on some browsers, especially due to custom Headers. + * For instance, using the flag with custom headers causes the browser to trigger an error: + * 'Preflight request for request with keepalive specified is currently not supported' + * https://bugs.chromium.org/p/chromium/issues/detail?id=835821 + * To avoid this issue we do a fake fetch call to check whether we can activate it or not in the browser. + */ + private async testKeepAlive() { + const customHeaders = new Headers(); + customHeaders.set('Content-Type', 'application/json'); + try { + await fetch('', {headers: customHeaders, keepalive: true}); + this.active = true; + } catch (e) { + this.active = false; + } + } + + public load(): PluginRunner { + return { + transform: (data: RequestOptions) => { + return {...data, keepalive: this.active}; + } + }; + } +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.spec.ts new file mode 100644 index 0000000000..36dedba392 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/keepalive.spec.ts @@ -0,0 +1,18 @@ +import type { RequestOptions } from '@ama-sdk/core'; +import { KeepaliveRequest } from './keepalive.request'; + +describe('Keepalive Request Plugin', () => { + + const options: RequestOptions = { headers: new Headers(), basePath: 'http://test.com/truc', method: 'get' }; + + it('keepalive should be set to true', async () => { + const plugin = new KeepaliveRequest(true); + const runner = plugin.load(); + + await runner.transform(options); + const keepalive = (await plugin.load().transform(options)).keepalive; + + expect(keepalive).toBe(true); + }); + +}); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/keepalive/readme.md b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/readme.md new file mode 100644 index 0000000000..937dc1c6be --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/keepalive/readme.md @@ -0,0 +1,7 @@ +## Keep Alive + +Plugin to add the keepalive flag to the request. + +### Type of plugins + +- Request plugin: [KeepaliveRequest](./keepalive.request.ts) diff --git a/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/README.md b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/README.md new file mode 100644 index 0000000000..d042a2cfd8 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/README.md @@ -0,0 +1,28 @@ +# Mock intercept plugin + +The mock interception strategy works based on two interceptions: request and fetch. For each interception, a plugin has been made. + +The mock mechanism provides, via the `getResponse` function, a way to completely override the fetch response. To apply the mock at FetchAPI level, we provide the `MockInterceptFetch`. +It will work with the `MockInterceptRequest` on the same mock set. + +Example of usage: + +```typescript +const baseConfig = new ApiFetchClient({ + basePath: 'http://my-api.com', + requestPlugins: [ + new MockInterceptRequest({ + adapter: myAdapter + }) + ], + fetchPlugins: [ + new MockInterceptFetch({ + adapter: myAdapter + }) + ] +}); +``` + +## References + +- [Request Plugin](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40ama-sdk/core/src/plugins/mock-intercept/README.md): full mock mechanism documentation. diff --git a/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/index.ts new file mode 100644 index 0000000000..ea9844979e --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/index.ts @@ -0,0 +1,2 @@ +export * from './mock-intercept.fetch'; +export * from './mock-intercept.interface'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.fetch.ts b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.fetch.ts new file mode 100644 index 0000000000..ce5748dc51 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.fetch.ts @@ -0,0 +1,58 @@ +import type { PluginAsyncRunner } from '@ama-sdk/core'; +import { MockInterceptFetchParameters } from './mock-intercept.interface'; +import { CUSTOM_MOCK_OPERATION_ID_HEADER, MockInterceptRequest } from '@ama-sdk/core'; +import type { FetchCall, FetchPlugin, FetchPluginContext, PluginAsyncStarter } from '../../fetch-plugin'; + +/** + * Plugin to mock and intercept the fetch of SDK + * + * This plugin should be used only with the MockInterceptRequest Plugin. + * It will allow the user to delay the response or to handle the getResponse function provided with the mock (if present). + */ +export class MockInterceptFetch implements FetchPlugin { + + constructor(protected options: MockInterceptFetchParameters) {} + + public load(context: FetchPluginContext): PluginAsyncRunner> & PluginAsyncStarter { + + if (!context.apiClient.options.requestPlugins.some((plugin) => plugin instanceof MockInterceptRequest)) { + throw new Error('MockInterceptFetch plugin should be used only with the MockInterceptRequest plugin'); + } + + return { + transform: async (fetchCall: FetchCall) => { + await this.options.adapter.initialize(); + + let responsePromise = fetchCall; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + if (!context.options.headers || !(context.options.headers instanceof Headers) || !(context.options.headers as Headers).has(CUSTOM_MOCK_OPERATION_ID_HEADER)) { + return responsePromise; + } + + if (typeof this.options.delayTiming !== 'undefined') { + const delay = typeof this.options.delayTiming === 'number' ? this.options.delayTiming : await this.options.delayTiming(context); + const resp = await responsePromise; + responsePromise = new Promise((resolve) => setTimeout(resolve, delay)).then(() => resp); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const operationId = (context.options.headers as Headers).get(CUSTOM_MOCK_OPERATION_ID_HEADER)!; + try { + const mock = this.options.adapter.getLatestMock(operationId); + + if (!mock.getResponse) { + return responsePromise; + } + + const response = mock.getResponse(); + return responsePromise.then(() => response); + + } catch { + (context.logger || console).error(`Failed to retrieve the latest mock for Operation ID ${operationId}, fallback to default mock`); + return responsePromise; + } + } + }; + } + +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.interface.ts b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.interface.ts new file mode 100644 index 0000000000..be8551fbdc --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.interface.ts @@ -0,0 +1,10 @@ +import type { MockAdapter } from '@ama-sdk/core'; +import type { FetchPluginContext } from '../../fetch-plugin'; + +/** Mock Fetch Plugin options */ +export interface MockInterceptFetchParameters { + /** List of mocks to be used */ + adapter: MockAdapter; + /** Delays the mock response, in milliseconds */ + delayTiming?: number | ((context: FetchPluginContext) => number | Promise); +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.spec.ts new file mode 100644 index 0000000000..5ca45d65dc --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/mock-intercept/mock-intercept.spec.ts @@ -0,0 +1,189 @@ +import { + ApiClient, + CUSTOM_MOCK_OPERATION_ID_HEADER, + CUSTOM_MOCK_REQUEST_HEADER, + Mock, + MockAdapter, + MockInterceptRequest, + RequestOptions, + RequestPlugin, + SequentialMockAdapter +} from '@ama-sdk/core'; +import { MockInterceptFetch } from './mock-intercept.fetch'; + +const testMock: Mock = { + mockData: {} +}; +const getMockSpy = jest.fn().mockReturnValue(testMock); +const getLatestMockSpy = jest.fn().mockReturnValue(testMock); +const retrieveOperationIdSpy = jest.fn().mockReturnValue(Promise.resolve('testOperation')); +const initializeSpy = jest.fn().mockReturnValue(Promise.resolve()); +const testMockAdapter: MockAdapter = { + getMock: getMockSpy, + getLatestMock: getLatestMockSpy, + initialize: initializeSpy, + retrieveOperationId: retrieveOperationIdSpy +}; + +const requestPlugins: RequestPlugin[] = [new MockInterceptRequest({adapter: new SequentialMockAdapter([], {})})]; +const apiClient = { + options: { + requestPlugins, + basePath: 'test', + replyPlugins: [] + } +} as ApiClient; + +describe('Mock intercept', () => { + beforeEach(() => jest.clearAllMocks()); + + describe('request plugin', () => { + it('should do nothing if disabled is true', async () => { + const plugin = new MockInterceptRequest({ disabled: true, adapter: testMockAdapter }); + const originalRequest: RequestOptions = { + method: 'get', + headers: new Headers({test: 'true'}), + basePath: 'myurl' + }; + const loaded = plugin.load(); + + expect(await loaded.transform(originalRequest)).toEqual(originalRequest); + expect(initializeSpy).toHaveBeenCalled(); + }); + + it('should not stringify provided api', async () => { + const plugin = new MockInterceptRequest({ disabled: false, adapter: testMockAdapter }); + const loaded = plugin.load(); + const originalRequest: RequestOptions = { + method: 'get', + headers: new Headers({ test: 'true' }), + basePath: 'myurl', + api: 'should not exist' as any + }; + await loaded.transform(originalRequest); + + expect(originalRequest.headers.has(CUSTOM_MOCK_REQUEST_HEADER)).toBe(true); + expect(Object.keys(JSON.parse(originalRequest.headers.get(CUSTOM_MOCK_REQUEST_HEADER)))).not.toContainEqual('api'); + }); + + it('should intercept the request', async () => { + // Disabled because Blob URL is not supported on NodeJS + const plugin = new MockInterceptRequest({ adapter: testMockAdapter }); + const originalRequest: RequestOptions = { + headers: new Headers({test: 'true'}), + basePath: 'myurl', + method: 'PATCH' + }; + const loaded = plugin.load(); + const transformed = await loaded.transform(originalRequest); + const res = await (await fetch(transformed.basePath, transformed)).text(); + + expect(getMockSpy).toHaveBeenCalled(); + expect(res).toBe(JSON.stringify(testMock.mockData)); + expect(initializeSpy).toHaveBeenCalled(); + }); + }); + + describe('fetch plugin', () => { + describe('when using an initialization function', () => { + let plugin: MockInterceptFetch; + let asyncMockAdapter: MockAdapter; + + beforeEach(() => { + asyncMockAdapter = { + initialize: initializeSpy, + getMock: getMockSpy, + getLatestMock: getLatestMockSpy, + retrieveOperationId: retrieveOperationIdSpy + }; + plugin = new MockInterceptFetch({adapter: asyncMockAdapter}); + }); + + it('should call initialize fn', async () => { + const loadedPlugin = plugin.load({ + controller: jest.fn() as any, + fetchPlugins: [], + url: 'myurl', + apiClient, + options: { + headers: new Headers({ + [CUSTOM_MOCK_OPERATION_ID_HEADER]: 'testOperation' + }) + } + }); + const testData: any = {test: true}; + await loadedPlugin.transform(Promise.resolve(testData)); + + expect(initializeSpy).toHaveBeenCalled(); + expect(getMockSpy).not.toHaveBeenCalled(); + expect(getLatestMockSpy).toHaveBeenCalledWith('testOperation'); + }); + + it('should throw if there is no request plugin', () => { + const config = { + controller: jest.fn() as any, + fetchPlugins: [], + url: 'myurl', + apiClient: { + options: { + requestPlugins: [] + } + } as ApiClient, + options: { + headers: new Headers({ + [CUSTOM_MOCK_OPERATION_ID_HEADER]: 'testOperation' + }) + } + }; + + expect(() => plugin.load(config)).toThrow(); + }); + }); + }); + + describe('with delay', () => { + it('should delay the response of the specific number', async () => { + const plugin = new MockInterceptFetch({ adapter: testMockAdapter, delayTiming: 700 }); + const loadedPlugin = plugin.load({ + controller: jest.fn() as any, + fetchPlugins: [], + url: '', + apiClient, + options: { + headers: new Headers({ + [CUSTOM_MOCK_OPERATION_ID_HEADER]: 'testOperation' + }) + } + }); + const callback = jest.fn(); + const run = loadedPlugin.transform(Promise.resolve({} as any)).then(callback); + await jest.advanceTimersByTimeAsync(699); + expect(callback).not.toHaveBeenCalled(); + await jest.advanceTimersByTimeAsync(1); + expect(callback).toHaveBeenCalled(); + await run; + }); + + it('should delay the response based on callback', async () => { + const plugin = new MockInterceptFetch({ adapter: testMockAdapter, delayTiming: () => 800 }); + const loadedPlugin = plugin.load({ + controller: jest.fn() as any, + fetchPlugins: [], + url: '', + apiClient, + options: { + headers: new Headers({ + [CUSTOM_MOCK_OPERATION_ID_HEADER]: 'testOperation' + }) + } + }); + const callback = jest.fn(); + const run = loadedPlugin.transform(Promise.resolve({} as any)).then(callback); + await jest.advanceTimersByTimeAsync(799); + expect(callback).not.toHaveBeenCalled(); + await jest.advanceTimersByTimeAsync(1); + expect(callback).toHaveBeenCalled(); + await run; + }); + }); +}); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/index.ts new file mode 100644 index 0000000000..372f115e93 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/index.ts @@ -0,0 +1 @@ +export * from './perf-metric.fetch'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.fetch.ts b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.fetch.ts new file mode 100644 index 0000000000..d7e96b026a --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.fetch.ts @@ -0,0 +1,222 @@ +import { v4 } from 'uuid'; +import type { FetchCall, FetchPlugin, FetchPluginContext } from '../../fetch-plugin'; + +/** + * Performance metric mark associated to a call. + */ +export interface Mark { + /** + * Id of the mark. + */ + markId: string; + + /** + * URL of the call. + */ + url: string; + + /** + * Options of the call. + */ + requestOptions: RequestInit; + + /** + * Start time of the call. + */ + startTime: number; + + /** + * Response of the call. + */ + response?: Response; + + /** + * Error of the call. + */ + error?: Error; + + /** + * End time of the call. + */ + endTime?: number; +} +/** Performance object supporting NodeJs Performance and Web Performance reporting */ +type CrossPlatformPerformance = { + /** @see Performance.mark */ + mark: (...x: Parameters) => ReturnType | void; + + /** @see Performance.measure */ + measure: (measureName: string, startOrMeasureOptions?: string, endMark?: string) => ReturnType | void; +}; + +/** + * Options for this plugin. + */ +export interface PerformanceMetricOptions { + /** + * Callback function to be called when a mark is closed. + */ + onMarkComplete: (mark: Mark) => void | Promise; + + /** + * Callback function to be called when a mark is closed with an error. + */ + onMarkError: (mark: Mark) => void | Promise; + + /** + * Callback function called when a mark is opened. + */ + onMarkOpen: (mark: Mark) => void | Promise; + + /** + * Instance of the performance reporter to use for performance measurements. + * @default window.performance on browser only, undefined on node + */ + performance: CrossPlatformPerformance; + + /** + * Retrieve the performance tag name + * @param status status of the call + * @param markId Mark ID + */ + getPerformanceTag: (status: string, markId: string) => string; +} + +/** + * Performance metric plugin. + */ +export class PerformanceMetricPlugin implements FetchPlugin { + /** + * Callback function called when a mark is closed. + */ + public onMarkComplete?: (mark: Mark) => void | Promise; + + /** + * Callback function called when a mark is closed with an error. + */ + public onMarkError?: (mark: Mark) => void | Promise; + + /** + * Callback function called when a mark is opened. + */ + public onMarkOpen?: (mark: Mark) => void | Promise; + + /** + * Opened marks. + */ + protected readonly openMarks: {[markId: string]: Mark} = {}; + + /** + * Performance reporter to use for performance measurements. + * @default window.performance on browser only, undefined on node + */ + protected readonly performance; + + /** + * Method used to get the current time as default implementation if no Performance API available. + * Date.now() is used by default. + */ + protected getTime: () => number = Date.now; + + constructor(options?: Partial) { + this.getPerformanceTag = options?.getPerformanceTag || this.getPerformanceTag; + this.performance = options?.performance || (typeof window !== 'undefined' ? window.performance : undefined); + this.onMarkComplete = options ? options.onMarkComplete : this.onMarkComplete; + this.onMarkError = options ? options.onMarkError : this.onMarkError; + this.onMarkOpen = options ? options.onMarkOpen : this.onMarkOpen; + } + + /** + * Retrieve the performance tag name + * @param status status of the call + * @param markId Mark ID + */ + protected getPerformanceTag = (status: string, markId: string) => `sdk:${status}:${markId}`; + + + /** + * Opens a mark associated to a call. + * @param url URL of the call associated to the mark to open + * @param requestOptions Options of the call associated to the mark to open + */ + public openMark(url: string, requestOptions: RequestInit) { + const markId = v4(); + const perfMark = this.performance?.mark(this.getPerformanceTag('start', markId)) || undefined; + const startTime = perfMark?.startTime ?? this.getTime(); + const mark: Mark = { + markId, + url, + requestOptions, + startTime + }; + this.openMarks[markId] = mark; + if (this.onMarkOpen) { + void this.onMarkOpen(mark); + } + return markId; + } + + /** + * Closes the mark matching the given mark id. + * @param markId Id of the mark to close + * @param response Response of the call associated to the mark to close + */ + public closeMark(markId: string, response: Response) { + const perfMark = this.performance?.mark(this.getPerformanceTag('end', markId)) || undefined; + const endTime = perfMark?.startTime ?? this.getTime(); + this.performance?.measure(this.getPerformanceTag('measure', markId), this.getPerformanceTag('start', markId), this.getPerformanceTag('end', markId)); + const mark = this.openMarks[markId]; + if (!mark) { + return; + } + if (this.onMarkComplete) { + void this.onMarkComplete({ + ...mark, + response, + endTime + }); + } + delete this.openMarks[markId]; + } + + /** + * Closes the mark matching the given mark id with an error. + * @param markId Id of the mark to close + * @param error Optional error of the call associated to the mark to close + */ + public closeMarkWithError(markId: string, error: Error | undefined) { + const perfMark = this.performance?.mark(this.getPerformanceTag('error', markId)) || undefined; + const endTime = perfMark?.startTime ?? this.getTime(); + this.performance?.measure(this.getPerformanceTag('measure', markId), this.getPerformanceTag('start', markId), this.getPerformanceTag('error', markId)); + const mark = this.openMarks[markId]; + if (!mark) { + return; + } + if (this.onMarkError) { + void this.onMarkError({ + ...mark, + error, + endTime + }); + } + delete this.openMarks[markId]; + } + + /** @inheritDoc */ + public load(context: FetchPluginContext) { + return { + transform: async (fetchCall: FetchCall) => { + const markId = this.openMark(context.url, context.options); + + try { + const response = await fetchCall; + this.closeMark(markId, response); + return response; + } catch (exception: any) { + this.closeMarkWithError(markId, exception); + throw exception; + } + } + }; + } +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.probe.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.probe.spec.ts new file mode 100644 index 0000000000..fc66e300e1 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/perf-metric.probe.spec.ts @@ -0,0 +1,58 @@ +import { PerformanceMetricPlugin } from './perf-metric.fetch'; + +let perfPlugin: PerformanceMetricPlugin; +describe('PerformanceMetricPlugin', () => { + let onMarkOpen!: jest.Mock; + + beforeEach(() => { + onMarkOpen = jest.fn(); + perfPlugin = new PerformanceMetricPlugin({onMarkOpen}); + }); + + it('should generate new mark ids', () => { + expect(perfPlugin.openMark('', {})).not.toEqual(perfPlugin.openMark('', {})); + }); + + it('should include a new mark when closing', () => { + const markId = perfPlugin.openMark('my-url', {}); + const ret = new Promise((resolve) =>{ + perfPlugin.onMarkComplete = (mark) => { + expect(mark).toBeDefined(); + expect(mark.markId).toBe(markId); + expect(mark.url).toBe('my-url'); + expect(mark.requestOptions).toEqual({}); + expect(mark.startTime).toBeDefined(); + expect(mark.response).toEqual({} as Response); + expect(mark.error).not.toBeDefined(); + expect(mark.endTime).toBeGreaterThanOrEqual(mark.startTime); + resolve(); + }; + }); + perfPlugin.closeMark(markId, {} as Response); + return ret; + }); + + it('should include a new mark when closing with error', () => { + const markId = perfPlugin.openMark('my-url', {}); + const ret = new Promise((resolve) => { + perfPlugin.onMarkError = (mark) => { + expect(mark).toBeDefined(); + expect(mark.markId).toBe(markId); + expect(mark.url).toBe('my-url'); + expect(mark.requestOptions).toEqual({}); + expect(mark.startTime).toBeDefined(); + expect(mark.response).not.toBeDefined(); + expect(mark.error).toEqual({} as Error); + expect(mark.endTime).toBeGreaterThanOrEqual(mark.startTime); + resolve(); + }; + }); + perfPlugin.closeMarkWithError(markId, {} as Error); + return ret; + }); + + it('should include call the open mark callback', () => { + const markId = perfPlugin.openMark('my-url', {}); + expect(onMarkOpen).toHaveBeenCalledWith(expect.objectContaining({ markId })); + }); +}); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/readme.md b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/readme.md new file mode 100644 index 0000000000..e2e289f5f7 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/perf-metric/readme.md @@ -0,0 +1,7 @@ +## Performance Metric + +Plugin to measure and report performance metrics of the SDK processes. + +### Type of plugins + +- Fetch plugin: [PerformanceMetricPlugin](./perf-metric.fetch.ts) diff --git a/packages/@ama-sdk/client-fetch/src/plugins/retry/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/retry/index.ts new file mode 100644 index 0000000000..696b492cc1 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/retry/index.ts @@ -0,0 +1 @@ +export * from './retry.fetch'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/retry/readme.md b/packages/@ama-sdk/client-fetch/src/plugins/retry/readme.md new file mode 100644 index 0000000000..2f4d0bf686 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/retry/readme.md @@ -0,0 +1,7 @@ +## Retry + +Plugin to Retry a fetch call. + +### Type of plugins + +- Fetch plugin: [RetryFetch](./retry.fetch.ts) diff --git a/packages/@ama-sdk/client-fetch/src/plugins/retry/retry.fetch.ts b/packages/@ama-sdk/client-fetch/src/plugins/retry/retry.fetch.ts new file mode 100644 index 0000000000..fd006c04f6 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/retry/retry.fetch.ts @@ -0,0 +1,107 @@ +import { CanceledCallError } from '@ama-sdk/core'; +import type { FetchCall, FetchPlugin, FetchPluginContext } from '../../fetch-plugin'; + +/** + * Function to run to determine if we need to retry the call + * @param numberOfRetry + * @param condition + * @example + * ```typescript + * const condition = async (context: FetchPluginContext, data?: Response, error?: Error) => { + * const status = data.status; + * return status !== 200; + * } + * const plugin = new RetryConditionType(5, condition); + * ``` + * @example + * ```typescript + * const condition = async (context: FetchPluginContext, data?: Response, error?: Error) => { + * const receivedData = data && await data.text(); + * return !!data && /^error$/.test(data); + * } + * const plugin = new RetryConditionType(5, condition); + * ``` + */ +export type RetryConditionType = (context: FetchPluginContext, data?: Response, error?: Error) => boolean | Promise; + +/** + * Plugin to Retry a fetch call + */ +export class RetryFetch implements FetchPlugin { + /** Number of retry */ + public numberOfRetry: number; + + /** Condition of retrying */ + public condition: RetryConditionType; + + /** If we wait between the next retry. It will be random value between minSleep and maxSleep ms */ + public sleepBetweenRetry: (numberOfRetry?: number) => number | Promise; + + /** + * Retry Fetch plugin + * @param numberOfRetry Number of retry + * @param condition Condition of retrying, return true to launch the retry process + * @param sleepBetweenRetry + */ + constructor( + numberOfRetry = 3, + condition: RetryConditionType = (_context: FetchPluginContext, _data?: Response, error?: Error) => !(error instanceof CanceledCallError), + sleepBetweenRetry: (numberOfRetry?: number) => number | Promise = () => 0) { + this.numberOfRetry = numberOfRetry; + this.condition = condition; + this.sleepBetweenRetry = sleepBetweenRetry; + } + + /** + * Launch a retry + * @param context + */ + private retry(context: FetchPluginContext) { + let asyncResponse = fetch(context.url, context.options); + for (const plugin of context.fetchPlugins) { + asyncResponse = plugin.transform(asyncResponse); + } + return asyncResponse; + } + + private async delay(countDown: number) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => setTimeout(resolve, await this.sleepBetweenRetry(countDown))); + } + + + private async waitAndRetry(context: FetchPluginContext, countDown: number) { + await this.delay(countDown); + return this.retry(context); + } + + /** @inheritDoc */ + public load(context: FetchPluginContext) { + let countDown = this.numberOfRetry; + + return { + transform: async (fetchCall: FetchCall) => { + try { + const result = await fetchCall; + if (!result.ok && countDown > 0) { + const conditionResult = await this.condition(context, result.clone()); + if (conditionResult) { + countDown--; + return this.waitAndRetry(context, countDown); + } + } + return result; + } catch (e: any) { + if (countDown) { + const conditionResult = await this.condition(context, undefined, e); + if (conditionResult) { + countDown--; + return this.waitAndRetry(context, countDown); + } + } + throw e; + } + } + }; + } +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/retry/retry.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/retry/retry.spec.ts new file mode 100644 index 0000000000..e81d3a8be3 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/retry/retry.spec.ts @@ -0,0 +1,105 @@ +import { RetryFetch } from './retry.fetch'; + +describe('Retry Fetch Plugin', () => { + + it('should not retry on success', async () => { + const condition = jest.fn().mockReturnValue(true); + const plugin = new RetryFetch(1, condition); + + const runner = plugin.load({url: 'http://www.test.com', fetchPlugins: []} as any); + const call = Promise.resolve({text: 'test', ok: true}); + + const res = runner.transform(call as any); + + expect(condition).not.toHaveBeenCalled(); + + const ret = await res; + + expect(ret).toEqual({text: 'test', ok: true} as any); + }); + + it('should not retry if refused by the condition', async () => { + const conditionFalsy = jest.fn().mockReturnValue(false); + const plugin = new RetryFetch(3, conditionFalsy); + + const runner = plugin.load({url: 'http://www.test.com', fetchPlugins: []} as any); + const call = Promise.resolve({text: 'test', ok: false, clone: () => ({})}); + + const res = runner.transform(call as any); + await res; + + expect(conditionFalsy).toHaveBeenCalledTimes(1); + + }); + + it('should retry on fetch rejection', async () => { + const condition = jest.fn().mockReturnValue(true); + const plugin = new RetryFetch(2, condition); + const runners: any[] = []; + + const runner = plugin.load({url: 'not an url', fetchPlugins: runners} as any); + runners.push(runner); + const call = Promise.reject({text: 'test', ok: true}); + + const callback = jest.fn(); + runner.transform(call as any).catch(callback); + await jest.runAllTimersAsync(); + expect(callback).toHaveBeenCalledWith(expect.objectContaining({})); + expect(condition).toHaveBeenCalledTimes(2); + }); + + it('should retry on fetch rejection with wait', async () => { + const condition = jest.fn().mockReturnValue(true); + const delay = 500; + const plugin = new RetryFetch(2, condition, () => delay); + const runners: any[] = []; + + const runner = plugin.load({url: 'not an url', fetchPlugins: runners} as any); + runners.push(runner); + const call = Promise.reject({text: 'test', ok: true}); + + const callback = jest.fn(); + runner.transform(call as any).catch(callback); + await jest.advanceTimersByTimeAsync(delay); + expect(callback).not.toHaveBeenCalled(); + await jest.advanceTimersByTimeAsync(delay); + expect(callback).toHaveBeenCalledWith(expect.objectContaining({})); + expect(condition).toHaveBeenCalledTimes(2); + }); + + it('should retry on not ok call', async () => { + const condition = jest.fn().mockReturnValue(true); + const plugin = new RetryFetch(3, condition); + const runners: any[] = []; + + const runner = plugin.load({url: 'not an url', fetchPlugins: runners} as any); + runners.push(runner); + const call = Promise.resolve({text: 'test', ok: false}); + + const callback = jest.fn(); + runner.transform(call as any).catch(callback); + await jest.runAllTimersAsync(); + expect(callback).toHaveBeenCalledWith(expect.objectContaining({})); + expect(condition).toHaveBeenCalledTimes(3); + }); + + it('should retry on not ok call with wait', async () => { + const condition = jest.fn().mockReturnValue(true); + const delay = 500; + const plugin = new RetryFetch(3, condition, () => delay); + const runners: any[] = []; + + const runner = plugin.load({url: 'not an url', fetchPlugins: runners} as any); + runners.push(runner); + const call = Promise.resolve({text: 'test', ok: false}); + + const callback = jest.fn(); + runner.transform(call as any).catch(callback); + await jest.advanceTimersByTimeAsync(2 * delay); + expect(callback).not.toHaveBeenCalled(); + await jest.advanceTimersByTimeAsync(delay); + expect(callback).toHaveBeenCalledWith(expect.objectContaining({})); + expect(condition).toHaveBeenCalledTimes(3); + }); + +}); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/timeout/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/timeout/index.ts new file mode 100644 index 0000000000..bde9ed3c45 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/timeout/index.ts @@ -0,0 +1 @@ +export * from './timeout.fetch'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/timeout/readme.md b/packages/@ama-sdk/client-fetch/src/plugins/timeout/readme.md new file mode 100644 index 0000000000..9cead6e832 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/timeout/readme.md @@ -0,0 +1,50 @@ +# Timeout + +Plugin to raise an exception on a fetch request timeout. +The timeout can be configured to stop and restart from the beginning depending on events. + +## Timeout pause/restart mechanism + +You can configure a ``TimeoutPauseEventHandler`` to stop the timeout from throwing errors upon some events. + +One of these example is the Captcha. If your user is currently resolving a Captcha, the request might not go through +until the Captcha is fully resolved. This is not something you actually want. + +### Imperva Captcha event + +Today the @ama-sdk/core plugin exposes the ``impervaCaptchaEventHandlerFactory`` that will emit an event if a Captcha has +been displayed on your website. It is only compatible with Imperva UI events and can be used as follows: + +```typescript +import {impervaCaptchaEventHandlerFactory, TimeoutFetch} from './timeout.fetch'; + +const fetchPlugin = new TimeoutFetch(60000, impervaCaptchaEventHandlerFactory({whiteListedHostNames: ['myCaptchaDomain']})); +``` + +Only events posted from the white listed domain will be listened to, make sure to correctly configure the factory. + +### Custom event + +You can create your own ``TimeoutPauseEventHandler`` that will call the timeoutPauseCallback whenever you need to pause +or restart the timeout. + +```typescript +import {TimeoutPauseEventHandlerFactory, TimeoutStatus} from '@ama-sdk/core'; + +export const myTimeoutPauseEventHandlerFactory: TimeoutPauseEventHandlerFactory = (config) => + (timeoutPauseCallback: (timeoutStatus: TimeoutStatus) => void) => { + const onCustomEvent = ((event: MyCustomEvent) => { + let pauseStatus: TimeoutStatus; + // some extra logic to define the status based on your event + timeoutPauseCallback(pauseStatus); + }); + addEventListener(MyCustomEvent, onCustomEvent); + return () => { + removeEventListener(MyCustomEvent, onCustomEvent); + }; + }; +``` + +## Type of plugins + +- Fetch plugin: [TimeoutFetch](./timeout.fetch.ts) diff --git a/packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.fetch.ts b/packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.fetch.ts new file mode 100644 index 0000000000..c733b9c161 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.fetch.ts @@ -0,0 +1,140 @@ +import { ResponseTimeoutError } from '@ama-sdk/core'; +import type { FetchCall, FetchPlugin, FetchPluginContext } from '../../fetch-plugin'; + +/** + * Representation of an Imperva Captcha message + */ +type ImpervaCaptchaMessageData = { + impervaChallenge: { + type: 'captcha'; + status: 'started' | 'ended'; + timeout: number; + url: string; + }; +}; + +/** + * Type to describe the timer status of the {@see TimeoutFetch} plugin. + * Today, only the stop and restart of the timer is supported which match the following events: + * - stop: stop the timeout timer + * - start: reset the timer and restart it + */ +export type TimeoutStatus = 'timeoutStopped' | 'timeoutStarted'; + +/** + * Check if a message can be cast as an {@link ImpervaCaptchaMessage} + * @param message + */ +function isImpervaCaptchaMessage(message: any): message is ImpervaCaptchaMessageData { + return !!message && Object.prototype.hasOwnProperty.call(message, 'impervaChallenge') && + Object.prototype.hasOwnProperty.call(message.impervaChallenge, 'status') && + Object.prototype.hasOwnProperty.call(message.impervaChallenge, 'type') && message.impervaChallenge.type === 'captcha'; +} + +/** + * Event handler that will emit event to pause the timeout + * Today the timeout only + */ +export type TimeoutPauseEventHandler = ((timeoutPauseCallback: (timeoutStatus: TimeoutStatus) => void, context: any) => () => void); +/** + * Factory to generate a {@see TimeoutPauseEventHandler} depending on various configurations + */ +export type TimeoutPauseEventHandlerFactory = (config?: Partial) => TimeoutPauseEventHandler; + +/** + * Captures Imperva captcha events and calls the event callback + * It can only be used for browser's integrating imperva captcha + * @param config: list of host names that can trigger a captcha event + * @param config + * @returns removeEventListener + */ +export const impervaCaptchaEventHandlerFactory: TimeoutPauseEventHandlerFactory<{ whiteListedHostNames: string[] }> = (config) => + (timeoutPauseCallback: (timeoutStatus: TimeoutStatus) => void) => { + const onImpervaCaptcha = ((event: MessageEvent) => { + const originHostname = (new URL(event.origin)).hostname; + if (originHostname !== location.hostname && (config?.whiteListedHostNames || []).indexOf(originHostname) === -1) { + return; + } + let message = event.data; + if (typeof event.data === 'string') { + try { + message = JSON.parse(event.data); + } catch { + // This might not be an imperva message + } + } + if (typeof message === 'object' && isImpervaCaptchaMessage(message)) { + timeoutPauseCallback(message.impervaChallenge.status === 'started' ? 'timeoutStopped' : 'timeoutStarted'); + } + }); + addEventListener('message', onImpervaCaptcha); + return () => { + removeEventListener('message', onImpervaCaptcha); + }; + }; + +/** + * Plugin to fire an exception on timeout + */ +export class TimeoutFetch implements FetchPlugin { + + /** Fetch timeout (in millisecond) */ + public timeout: number; + private timerSubscription: ((pauseStatus: TimeoutStatus) => void)[] = []; + private timerPauseState: TimeoutStatus = 'timeoutStarted'; + + /** + * Timeout Fetch plugin. + * @param timeout Timeout in millisecond + * @param timeoutPauseEvent Event that will trigger the pause and reset of the timeout + */ + constructor(timeout = 60000, private readonly timeoutPauseEvent?: TimeoutPauseEventHandler) { + this.timeout = timeout; + if (this.timeoutPauseEvent) { + this.timeoutPauseEvent((pausedStatus: TimeoutStatus) => { + this.timerPauseState = pausedStatus; + this.timerSubscription.forEach((timer) => timer.call(this, pausedStatus)); + }, this); + } + } + + public load(context: FetchPluginContext) { + return { + transform: (fetchCall: FetchCall) => + // eslint-disable-next-line no-async-promise-executor + new Promise(async (resolve, reject) => { + const timeoutCallback = () => { + reject(new ResponseTimeoutError(`in ${this.timeout}ms`)); + // Fetch abort controller is now supported by all modern browser and node 15+. It should always be defined + context.controller.abort(); + }; + let timer = this.timerPauseState === 'timeoutStopped' ? undefined : setTimeout(timeoutCallback, this.timeout); + const timerCallback = (pauseStatus: TimeoutStatus) => { + if (timer && pauseStatus === 'timeoutStopped') { + clearTimeout(timer); + (context.logger || console).log('[SDK Plugins] Timeout cancelled.'); + timer = undefined; + } else if (!timer && pauseStatus === 'timeoutStarted') { + timer = setTimeout(timeoutCallback, this.timeout); + (context.logger || console).log('[SDK Plugins] Timeout restarted.'); + } + }; + this.timerSubscription.push(timerCallback); + + try { + const response = await fetchCall; + if (!context.controller.signal.aborted) { + resolve(response); + } + } catch (ex) { + reject(ex); + } finally { + if (timer) { + clearTimeout(timer); + } + this.timerSubscription = this.timerSubscription.filter(callback => timerCallback !== callback); + } + }) + }; + } +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.spec.ts new file mode 100644 index 0000000000..c366af251d --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/timeout/timeout.spec.ts @@ -0,0 +1,165 @@ +import { EmptyResponseError, ResponseTimeoutError } from '@ama-sdk/core'; +import { + impervaCaptchaEventHandlerFactory, + TimeoutFetch, + TimeoutStatus +} from './timeout.fetch'; + +describe('Timeout Fetch Plugin', () => { + + it('should reject on timeout', async () => { + const plugin = new TimeoutFetch(100); + + const runner = plugin.load({controller: new AbortController()} as any); + const call = new Promise((resolve) => setTimeout(() => resolve(undefined), 1000)); + + const callback = jest.fn(); + runner.transform(call).catch(callback); + await jest.advanceTimersByTimeAsync(99); + expect(callback).not.toHaveBeenCalled(); + await jest.advanceTimersByTimeAsync(1); + expect(callback).toHaveBeenCalledWith(new ResponseTimeoutError('in 100ms')); + }); + + it('should not reject on fetch rejection', async () => { + const plugin = new TimeoutFetch(6000); + + const runner = plugin.load({controller: new AbortController()} as any); + const call = new Promise((_resolve, reject) => setTimeout(() => reject(new EmptyResponseError('')), 100)); + + + const callback = jest.fn(); + runner.transform(call).catch(callback); + await jest.advanceTimersByTimeAsync(6000); + expect(callback).toHaveBeenCalledWith(new EmptyResponseError('')); + }); + + it('should forward the fetch response', async () => { + const plugin = new TimeoutFetch(2000); + + const runner = plugin.load({controller: new AbortController()} as any); + const call = new Promise((resolve) => setTimeout(() => resolve({test: true}), 100)); + + const promise = runner.transform(call); + await jest.runAllTimersAsync(); + + expect(await promise).toEqual({test: true} as any); + }); + + it('should not reject if the timeout has been paused and reject if restarted', async () => { + const timeoutPauseEvent = { + emitEvent: (_status: TimeoutStatus) => { + }, + handler: (timeoutPauseCallback: (status: TimeoutStatus) => void) => { + timeoutPauseEvent.emitEvent = timeoutPauseCallback; + return () => { + }; + } + }; + const plugin = new TimeoutFetch(100, timeoutPauseEvent.handler); + + const runner = plugin.load({controller: new AbortController()} as any); + const call = new Promise((resolve) => setTimeout(() => resolve({test: true}), 500)); + const callback = jest.fn(); + runner.transform(call).catch(callback); + timeoutPauseEvent.emitEvent('timeoutStopped'); + await jest.advanceTimersByTimeAsync(200); + timeoutPauseEvent.emitEvent('timeoutStarted'); + await jest.advanceTimersByTimeAsync(200); + expect(callback).toHaveBeenCalledWith(new ResponseTimeoutError('in 100ms')); + }); + + it('should take into account pause events triggered before the call', async () => { + const timeoutPauseEvent = { + emitEvent: (_status: TimeoutStatus) => { + }, + handler: (timeoutPauseCallback: (status: TimeoutStatus) => void) => { + timeoutPauseEvent.emitEvent = timeoutPauseCallback; + return () => { + }; + } + }; + const plugin = new TimeoutFetch(250, timeoutPauseEvent.handler); + + const runner = plugin.load({controller: new AbortController()} as any); + const call = new Promise((resolve) => setTimeout(() => resolve({test: true}), 500)); + timeoutPauseEvent.emitEvent('timeoutStopped'); + const promise = runner.transform(call); + await jest.runAllTimersAsync(); + + expect(await promise).toEqual({test: true} as any); + }); +}); + +describe('impervaCaptchaEventHandlerFactory', () => { + let postMessageTemp: (msg: any, origin?: string) => any; + beforeAll(() => { + global.location ||= {hostname: 'test'} as any; + global.addEventListener ||= jest.fn().mockImplementation((event, handler) => { + if (event === 'message') { + postMessageTemp = (msg, origin?) => { + const eventObject = { + origin: origin || 'https://test', + data: msg + } as any; + if (typeof handler === 'object') { + handler.handleEvent(eventObject); + } else { + handler(eventObject); + } + }; + } + }) as any; + } + ); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should not throw on unexpected messages', () => { + const callback = jest.fn(); + impervaCaptchaEventHandlerFactory({whiteListedHostNames: []})(callback, this); + postMessageTemp('pouet'); + expect(callback).not.toHaveBeenCalled(); + postMessageTemp(JSON.stringify({impervaChallenge: {type: 'incorrectType'}})); + expect(callback).not.toHaveBeenCalled(); + postMessageTemp(JSON.stringify({impervaChallenge: {incorrectFormat: true}})); + expect(callback).not.toHaveBeenCalled(); + }); + + it('should not throw on null messages', () => { + const callback = jest.fn(); + impervaCaptchaEventHandlerFactory({whiteListedHostNames: []})(callback, this); + postMessageTemp(null); + expect(callback).not.toHaveBeenCalled(); + }); + + it('should trigger a timeoutStopped if the captcha challenge has been started', () => { + const callback = jest.fn(); + impervaCaptchaEventHandlerFactory({whiteListedHostNames: []})(callback, this); + postMessageTemp(JSON.stringify({impervaChallenge: {status: 'started', type: 'captcha'}})); + expect(callback).toHaveBeenCalledWith('timeoutStopped'); + }); + + it('should trigger a timeoutStarted if the captcha challenge has been finished', () => { + const callback = jest.fn(); + impervaCaptchaEventHandlerFactory({whiteListedHostNames: []})(callback, this); + postMessageTemp({impervaChallenge: {status: 'ended', type: 'captcha'}}); + expect(callback).toHaveBeenCalledWith('timeoutStarted'); + }); + + it('should trigger a timeoutStarted if the captcha challenge has been finished on a whitelisted domain', () => { + const callback = jest.fn(); + impervaCaptchaEventHandlerFactory({whiteListedHostNames: ['valid.domain']})(callback, this); + postMessageTemp(JSON.stringify({impervaChallenge: {status: 'ended', type: 'captcha'}}), 'http://valid.domain'); + expect(callback).toHaveBeenCalledWith('timeoutStarted'); + }); + + it('should ignore postMessage from non whitelisted domain', () => { + const callback = jest.fn(); + impervaCaptchaEventHandlerFactory({whiteListedHostNames: []})(callback, this); + postMessageTemp(JSON.stringify({impervaChallenge: {status: 'ended', type: 'captcha'}}), 'http://invalid.domain'); + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/@ama-sdk/client-fetch/src/plugins/wait-for/index.ts b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/index.ts new file mode 100644 index 0000000000..1acd0824a2 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/index.ts @@ -0,0 +1 @@ +export * from './wait-for.fetch'; diff --git a/packages/@ama-sdk/client-fetch/src/plugins/wait-for/readme.md b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/readme.md new file mode 100644 index 0000000000..999cc698a1 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/readme.md @@ -0,0 +1,7 @@ +## Wait for + +Plugin to determine if and when a call should be processed. + +### Type of plugins + +- Fetch plugin: [WaitForFetch](./wait-for.fetch.ts) diff --git a/packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.fetch.ts b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.fetch.ts new file mode 100644 index 0000000000..100fdf346b --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.fetch.ts @@ -0,0 +1,139 @@ +import type { FetchCall, FetchPlugin, FetchPluginContext } from '../../fetch-plugin'; + +/** Callback function type */ +export type CallbackFunction = (context: FetchPluginContext & {data: T | undefined}, fetchCall: FetchCall, response?: Response) => void; + +/** Result of the condition function */ +export interface CanStartConditionResult { + /** Actual result of the condition */ + result: boolean | Promise; + + /** Data can be used to store data between the condition function and the callback */ + data?: T; +} + +/** + * Condition function to determine if the call can start + * @returns True if the call can start, False if it should be canceled + */ +export type CanStartConditionFunction = (context: FetchPluginContext) => CanStartConditionResult | Promise>; + +/** + * Plugin to determine if and when a call should be processed + * @example + * ```typescript + * // Use the plugin for an orchestrator 1 per 1 + * class OrchestratorOnePerOne { + * stack: ({id: string, resolve: (result: boolean) => void})[] = []; + * + * private resolve() { + * if (this.stack.length) { + * this.stack[0].resolve(true); + * } + * } + * + * public push(): CanStartConditionResult { + * const id = uuid(); + * const ret = { + * data: id, + * result: new Promise((resolve) => { + * this.stack.push({id, resolve}); + * if (this.stack.length === 1) { + * this.resolve(); + * } + * }) + * }; + * + * return ret; + * } + * + * public pop(id: string): void { + * this.stack = this.stack.filter((item) => item.id !== id); + * this.resolve(); + * } + * } + * + * const orchestrator = new OrchestratorOnePerOne(); + * const waitForPlugin = new WaitForFetch(() => orchestrator.push(), undefined, (context) => orchestrator.pop(context.data)); + * const api = new Api('https://wwww.digitalforairlines.com/api', undefined, undefined, [waitForPlugin]); + * ``` + */ +export class WaitForFetch implements FetchPlugin { + + /** Condition to wait to start the call */ + public canStartCondition: CanStartConditionFunction; + + /** Timeout in ms (infinit if not defined) */ + public timeout?: number; + + /** Function callback called when the fetch call has been executed */ + public callback?: CallbackFunction; + + /** + * Wait For Fetch plugin + * @param canStartCondition Condition that should be passed to start the call + * @param timeout Timeout of the condition function (return false when reached) + * @param callback Callback function called when the fetch call has been processed + */ + constructor(canStartCondition: CanStartConditionFunction, timeout?: number, callback?: CallbackFunction) { + this.canStartCondition = canStartCondition; + this.timeout = timeout; + this.callback = callback; + } + + + /** @inheritDoc */ + public load(context: FetchPluginContext) { + let data: T | undefined; + + return { + // eslint-disable-next-line no-async-promise-executor + canStart: () => new Promise(async (resolve) => { + let didTimeOut = false; + let timer: any; + + if (this.timeout) { + timer = setTimeout(() => { + didTimeOut = true; + resolve(false); + }, this.timeout); + } + + try { + const canStartCondition = await this.canStartCondition(context); + data = canStartCondition.data; + const canStart = await canStartCondition.result; + + if (!didTimeOut) { + resolve(canStart); + } + } catch (ex) { + if (!didTimeOut) { + resolve(false); + } + } finally { + if (timer) { + clearTimeout(timer); + } + } + }), + + transform: async (fetchCall: FetchCall) => { + if (!this.callback) { + return fetchCall; + } + + let response: Response | undefined; + try { + response = await fetchCall; + this.callback({ ...context, data }, fetchCall, response); + return response; + } catch (e) { + this.callback({ ...context, data }, fetchCall, response); + throw e; + } + } + }; + } + +} diff --git a/packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.spec.ts b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.spec.ts new file mode 100644 index 0000000000..37ccf6cdf6 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/plugins/wait-for/wait-for.spec.ts @@ -0,0 +1,73 @@ +import { WaitForFetch } from './wait-for.fetch'; + +describe('Wait For Fetch Plugin', () => { + + const defaultContext: any = {}; + + it('should not start if timeout', async () => { + const plugin = new WaitForFetch(() => ({result: new Promise((resolve) => setTimeout(() => resolve(true), 2000))}), 100); + + const runner = plugin.load(defaultContext); + const canStart = runner.canStart(); + await jest.runAllTimersAsync(); + + expect(await canStart).toBe(false); + }); + + it('should start if promise condition passed', async () => { + const plugin = new WaitForFetch(() => ({result: Promise.resolve(true)}), 100); + + const runner = plugin.load(defaultContext); + const canStart = await runner.canStart(); + + expect(canStart).toBe(true); + }); + + it('should start if condition passed', async () => { + const plugin = new WaitForFetch(() => ({result: true}), 100); + + const runner = plugin.load(defaultContext); + const canStart = await runner.canStart(); + + expect(canStart).toBe(true); + }); + + it('should call the callback function on success', async () => { + const callback = jest.fn(); + const plugin = new WaitForFetch(() => ({result: true}), 100, callback); + + const runner = plugin.load(defaultContext); + const response: any = {test: true}; + const fetchCall = Promise.resolve(response); + await runner.transform(fetchCall); + + expect(callback).toHaveBeenCalledWith(expect.objectContaining(defaultContext), fetchCall, response); + }); + + it('should call the callback function with the correct data', async () => { + const callback = jest.fn(); + const plugin = new WaitForFetch(() => ({result: true, data: 'test'}), 100, callback); + + const runner = plugin.load(defaultContext); + const response: any = {test: true}; + const fetchCall = Promise.resolve(response); + await runner.canStart(); + await runner.transform(fetchCall); + + expect(callback).toHaveBeenCalledWith(expect.objectContaining({...defaultContext, data: 'test'}), fetchCall, response); + }); + + it('should call the callback function on failure', async () => { + const callback = jest.fn(); + const plugin = new WaitForFetch(() => ({result: true}), 100, callback); + + const runner = plugin.load(defaultContext); + const response: any = {test: true}; + const fetchCall = Promise.reject(response); + try { + await runner.transform(fetchCall); + } catch {} + + expect(callback).toHaveBeenCalledWith(expect.objectContaining(defaultContext), fetchCall, undefined); + }); +}); diff --git a/packages/@ama-sdk/client-fetch/src/public_api.ts b/packages/@ama-sdk/client-fetch/src/public_api.ts new file mode 100644 index 0000000000..3258e2be0a --- /dev/null +++ b/packages/@ama-sdk/client-fetch/src/public_api.ts @@ -0,0 +1,3 @@ +export * from './api-fetch-client'; +export * from './fetch-plugin'; +export * from './plugins/index'; diff --git a/packages/@ama-sdk/client-fetch/testing/jest.config.ut.builders.js b/packages/@ama-sdk/client-fetch/testing/jest.config.ut.builders.js new file mode 100644 index 0000000000..ed52ef11a8 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/testing/jest.config.ut.builders.js @@ -0,0 +1,15 @@ +const path = require('node:path'); +const getJestProjectConfig = require('../../../../jest.config.ut').getJestProjectConfig; +const rootDir = path.join(__dirname, '..'); + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestProjectConfig(rootDir, false), + displayName: `${require('../package.json').name}/builders`, + rootDir, + testPathIgnorePatterns: [ + '/.*/templates/.*', + '/src/.*', + '\\.it\\.spec\\.ts$' + ] +}; diff --git a/packages/@ama-sdk/client-fetch/testing/jest.config.ut.js b/packages/@ama-sdk/client-fetch/testing/jest.config.ut.js new file mode 100644 index 0000000000..a977528613 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/testing/jest.config.ut.js @@ -0,0 +1,21 @@ +const path = require('node:path'); +const getJestProjectConfig = require('../../../../jest.config.ut').getJestProjectConfig; +const rootDir = path.join(__dirname, '..'); + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestProjectConfig(rootDir, false), + displayName: require('../package.json').name, + rootDir, + fakeTimers: { + enableGlobally: true, + // TODO re-enable fake dates when issue fixed https://github.com/sinonjs/fake-timers/issues/437 and v12 is used by Jest + doNotFake: ['Date'] + }, + testPathIgnorePatterns: [ + '/.*/templates/.*', + '/builders/.*', + '/schematics/.*', + '\\.it\\.spec\\.ts$' + ] +}; diff --git a/packages/@ama-sdk/client-fetch/testing/setup-jest.ts b/packages/@ama-sdk/client-fetch/testing/setup-jest.ts new file mode 100644 index 0000000000..c409add1a8 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/testing/setup-jest.ts @@ -0,0 +1,3 @@ +import 'isomorphic-fetch'; +import '@o3r/test-helpers/setup-jest-builders'; + diff --git a/packages/@ama-sdk/client-fetch/tsconfig.build.json b/packages/@ama-sdk/client-fetch/tsconfig.build.json new file mode 100644 index 0000000000..e76c615d1c --- /dev/null +++ b/packages/@ama-sdk/client-fetch/tsconfig.build.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.build", + "compilerOptions": { + "sourceMap": true, + "incremental": true, + "composite": true, + "rootDir": ".", + "lib": [ + "dom", + "dom.iterable", + "scripthost", + "es2017.object", + "esnext" + ], + "declarationMap": true, + "target": "es2020", + "module": "es2020", + "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo", + "outDir": "./dist" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "**/*.spec.ts" + ] +} diff --git a/packages/@ama-sdk/client-fetch/tsconfig.builders.json b/packages/@ama-sdk/client-fetch/tsconfig.builders.json new file mode 100644 index 0000000000..eb1c38fbee --- /dev/null +++ b/packages/@ama-sdk/client-fetch/tsconfig.builders.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.build", + "compilerOptions": { + "incremental": true, + "composite": true, + "outDir": "./dist", + "module": "CommonJS", + "rootDir": ".", + "tsBuildInfoFile": "build/.tsbuildinfo.builders" + }, + "include": [ + "schematics/**/*.ts" + ], + "exclude": [ + "**/*.spec.ts", + "schematics/**/templates/**", + "schematics/ng-add/mocks/**" + ] +} diff --git a/packages/@ama-sdk/client-fetch/tsconfig.eslint.json b/packages/@ama-sdk/client-fetch/tsconfig.eslint.json new file mode 100644 index 0000000000..38eec16702 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/tsconfig.eslint.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig", + "include": [ + ".eslintrc.js", + "jest.config.js", + "testing/*", + "scripts/*" + ] +} diff --git a/packages/@ama-sdk/client-fetch/tsconfig.json b/packages/@ama-sdk/client-fetch/tsconfig.json new file mode 100644 index 0000000000..2a565f391f --- /dev/null +++ b/packages/@ama-sdk/client-fetch/tsconfig.json @@ -0,0 +1,12 @@ +/* IDE usage only */ +{ + "extends": "../../../tsconfig.base", + "references": [ + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.build.json" + } + ] +} diff --git a/packages/@ama-sdk/client-fetch/tsconfig.spec.json b/packages/@ama-sdk/client-fetch/tsconfig.spec.json new file mode 100644 index 0000000000..cf096bfec9 --- /dev/null +++ b/packages/@ama-sdk/client-fetch/tsconfig.spec.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.jest", + "compilerOptions": { + "esModuleInterop": true, + "strictNullChecks": false, + "strictPropertyInitialization":false, + "noImplicitAny": false, + "incremental": true, + "composite": true, + "declarationMap": false, + "strict": false, + "module": "ES2020", + "target": "ES2020" + }, + "include": [ + "./**/*.spec.ts" + ], + "exclude": [ + ], + "references": [ + { + "path": "./tsconfig.build.json" + } + ] +} diff --git a/packages/@ama-sdk/core/README.md b/packages/@ama-sdk/core/README.md index 81926d0986..3faffa245e 100644 --- a/packages/@ama-sdk/core/README.md +++ b/packages/@ama-sdk/core/README.md @@ -3,18 +3,18 @@ [![Stable Version](https://img.shields.io/npm/v/@ama-sdk/core?style=for-the-badge)](https://www.npmjs.com/package/@ama-sdk/core) [![Bundle Size](https://img.shields.io/bundlephobia/min/@ama-sdk/core?color=green&style=for-the-badge)](https://www.npmjs.com/package/@ama-sdk/core) -This package contains all the [plugins](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins), helpers and object definitions to dialog with an API following the `ama-sdk` architecture. +This package contains a set of [plugins](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins), helpers and object definitions to dialog with an API following the `ama-sdk` architecture. Please refer to the [ama-sdk-schematics](../schematics/README.md) package for getting started with an API based on `ama-sdk`. ## Available plugins -- [abort](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/abort) +- [abort](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/abort) *(deprecated, now available [here](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/abort))* - [additional-params](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/additional-params) - [api-configuration-override](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/api-configuration-override) - [api-key](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/api-key) - [bot-protection-fingerprint](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/bot-protection-fingerprint) -- [concurrent](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/concurrent) +- [concurrent](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/concurrent) *(deprecated, now available [here](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/concurrent))* - [core](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/core) - [client-facts](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/client-facts) - [custom-info](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/custom-info) @@ -22,30 +22,30 @@ Please refer to the [ama-sdk-schematics](../schematics/README.md) package for ge - [fetch-cache](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/fetch-cache) - [fetch-credentials](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/fetch-credentials) - [json-token](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/json-token) -- [keepalive](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/keepalive) +- [keepalive](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/keepalive) *(deprecated, now available [here](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/keepalive))* - [mock-intercept](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/mock-intercept) -- [perf-metric](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/perf-metric) +- [perf-metric](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/perf-metric) *(deprecated, now available [here](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/perf-metric))* - [pii-tokenizer](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/pii-tokenizer) - [raw-response-info](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/raw-response-info) -- [retry](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/retry) +- [retry](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/retry) *(deprecated, now available [here](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/retry))* - [reviver](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/reviver) - [session-id](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/session-id) - [si-token](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/si-token) - [simple-api-key-authentication](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/simple-api-key-authentication) - [url-rewrite](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/url-rewrite) -- [wait-for](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/wait-for) -- [timeout](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/timeout) +- [timeout](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/timeout) *(deprecated, now available [here](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/timeout))* +- [wait-for](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/src/plugins/wait-for) *(deprecated, now available [here](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/client-fetch/src/plugins/wait-for))* ## Available API Client The **API Clients** are mandatory to the SDK to indicate the service that should be used by the SDK to process the calls. A list of API Clients are provided by this package: -| API Client | Import | Description | -|------------------|------------------------------------------|--------------------------------------------------------------------------------| -| ApiFetchClient | @ama-sdk/core | Default API Client based on the browser FetchApi | -| ApiBeaconClient | @ama-sdk/core | API Client based on the browser BeaconApi, it is processing synchronous call | -| ApiAngularClient | @ama-sdk/core/clients/api-angular-client | API Client using the HttpClient exposed by the `@angular/common` package | +| API Client | Import | Description | +| ---------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | +| ApiFetchClient | [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch) | Default API Client based on the browser FetchApi | +| ApiBeaconClient | @ama-sdk/core | API Client based on the browser BeaconApi, it is processing synchronous call | +| ApiAngularClient | @ama-sdk/core/clients/api-angular-client | API Client using the HttpClient exposed by the `@angular/common` package | ### Logs @@ -70,15 +70,3 @@ function petApiFactory() { ``` > *Note*: Adding a third-party logging service is optional. If undefined, the fallback is the console logger. - -### CLI - -This package also comes with CLI scripts that can facilitate the upgrade and publication of an SDK. -Use --help on each command for more information - -| Script | Description | -|-----------------------------|------------------------------------------------------------------------------------------------| -| amasdk-clear-index | Remove the index files that are no longer necessary after the deletion of the associated model | -| amasdk-files-pack | Prepare the dist folder for publication | -| amasdk-update-spec-from-npm | Update the OpenAPI spec from an NPM package | - diff --git a/packages/@ama-sdk/core/package.json b/packages/@ama-sdk/core/package.json index 0191b5063e..3fadffec51 100644 --- a/packages/@ama-sdk/core/package.json +++ b/packages/@ama-sdk/core/package.json @@ -143,7 +143,6 @@ "@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/parser": "^7.14.1", "@typescript-eslint/utils": "^7.14.1", - "copyfiles": "^2.4.1", "cpy-cli": "^5.0.0", "eslint": "^8.57.0", "eslint-import-resolver-node": "^0.3.9", diff --git a/packages/@ama-sdk/core/src/clients/api-fetch-client.ts b/packages/@ama-sdk/core/src/clients/api-fetch-client.ts index d142736f28..c982d6a67d 100644 --- a/packages/@ama-sdk/core/src/clients/api-fetch-client.ts +++ b/packages/@ama-sdk/core/src/clients/api-fetch-client.ts @@ -16,13 +16,19 @@ import {BaseApiClientOptions} from '../fwk/core/base-api-constructor'; import {CanceledCallError, EmptyResponseError, ResponseJSONParseError} from '../fwk/errors'; import {ReviverType} from '../fwk/Reviver'; -/** @see BaseApiClientOptions */ +/** + * @see BaseApiClientOptions + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 + */ export interface BaseApiFetchClientOptions extends BaseApiClientOptions { /** List of plugins to apply to the fetch call */ fetchPlugins: FetchPlugin[]; } -/** @see BaseApiConstructor */ +/** + * @see BaseApiConstructor + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 + */ export interface BaseApiFetchClientConstructor extends PartialExcept { } @@ -34,7 +40,10 @@ const DEFAULT_OPTIONS: Omit = { disableFallback: false }; -/** Client to process the call to the API using Fetch API */ +/** + * Client to process the call to the API using Fetch API + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 + */ export class ApiFetchClient implements ApiClient { /** @inheritdoc */ diff --git a/packages/@ama-sdk/core/src/fwk/core/api-client.ts b/packages/@ama-sdk/core/src/fwk/core/api-client.ts index cc9410c9fe..1ba07d27f1 100644 --- a/packages/@ama-sdk/core/src/fwk/core/api-client.ts +++ b/packages/@ama-sdk/core/src/fwk/core/api-client.ts @@ -28,6 +28,7 @@ export interface RequestOptionsParameters { /** * API Client used by the SDK's APIs to call the server + * The list of official clients is available in @ama-sdk/core {@link https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/core/README.md#available-api-client|readme} */ export interface ApiClient { diff --git a/packages/@ama-sdk/core/src/plugins/abort/abort.fetch.ts b/packages/@ama-sdk/core/src/plugins/abort/abort.fetch.ts index 3495b6c3fe..fbdd254e3f 100644 --- a/packages/@ama-sdk/core/src/plugins/abort/abort.fetch.ts +++ b/packages/@ama-sdk/core/src/plugins/abort/abort.fetch.ts @@ -23,6 +23,7 @@ const isPromise = (result: boolean | void | Promise | Promise): r /** * Abort callback * Returns `true` to abort a request (or access directly to the controller to cancel fetch request) + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 * @example Immediate abort on URL match * ```typescript * const abortCondition: AbortCallback = ({url}) => url.endsWith('pet'); diff --git a/packages/@ama-sdk/core/src/plugins/abort/readme.md b/packages/@ama-sdk/core/src/plugins/abort/readme.md index 3c58089d56..66f016ed3e 100644 --- a/packages/@ama-sdk/core/src/plugins/abort/readme.md +++ b/packages/@ama-sdk/core/src/plugins/abort/readme.md @@ -1,5 +1,8 @@ ## Abort +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + Plugin to abort a Fetch call. ### Usage examples diff --git a/packages/@ama-sdk/core/src/plugins/concurrent/concurrent.fetch.ts b/packages/@ama-sdk/core/src/plugins/concurrent/concurrent.fetch.ts index 0f2b5421bb..27a6644c7d 100644 --- a/packages/@ama-sdk/core/src/plugins/concurrent/concurrent.fetch.ts +++ b/packages/@ama-sdk/core/src/plugins/concurrent/concurrent.fetch.ts @@ -2,6 +2,7 @@ import { FetchCall, FetchPlugin, FetchPluginContext } from '../core'; /** * Plugin to limit the number of concurrent call + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export class ConcurrentFetch implements FetchPlugin { diff --git a/packages/@ama-sdk/core/src/plugins/concurrent/readme.md b/packages/@ama-sdk/core/src/plugins/concurrent/readme.md index cd64f15932..dd33704347 100644 --- a/packages/@ama-sdk/core/src/plugins/concurrent/readme.md +++ b/packages/@ama-sdk/core/src/plugins/concurrent/readme.md @@ -1,5 +1,8 @@ ## Concurrent +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + Plugin to limit the number of concurrent calls. ### Type of plugins diff --git a/packages/@ama-sdk/core/src/plugins/core/fetch-plugin.ts b/packages/@ama-sdk/core/src/plugins/core/fetch-plugin.ts index d11b256356..c9658b0bab 100644 --- a/packages/@ama-sdk/core/src/plugins/core/fetch-plugin.ts +++ b/packages/@ama-sdk/core/src/plugins/core/fetch-plugin.ts @@ -2,12 +2,16 @@ import type { ApiClient } from '../../fwk/core/api-client'; import type { Plugin, PluginAsyncRunner, PluginContext } from './plugin'; import type { RequestOptions } from './request-plugin'; -/** Fetch Call Response type */ +/** + * Fetch Call Response type + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 + */ export type FetchCall = Promise; /** * Interface of an SDK reply plugin. * The plugin will be run on the reply of a call + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export interface FetchPluginContext extends PluginContext { /** URL targeted */ @@ -29,6 +33,7 @@ export interface FetchPluginContext extends PluginContext { /** * Interface of an async plugin starter + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export interface PluginAsyncStarter { /** Determine if the action can start */ @@ -38,6 +43,7 @@ export interface PluginAsyncStarter { /** * Interface of a Fetch plugin. * The plugin will be run around the Fetch call + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export interface FetchPlugin extends Plugin { /** diff --git a/packages/@ama-sdk/core/src/plugins/keepalive/keepalive.request.ts b/packages/@ama-sdk/core/src/plugins/keepalive/keepalive.request.ts index e94d309274..cf945a9f85 100644 --- a/packages/@ama-sdk/core/src/plugins/keepalive/keepalive.request.ts +++ b/packages/@ama-sdk/core/src/plugins/keepalive/keepalive.request.ts @@ -2,6 +2,7 @@ import {PluginRunner, RequestOptions, RequestPlugin} from '../core'; /** * Plugin to add the keepalive flag to the request + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export class KeepaliveRequest implements RequestPlugin { diff --git a/packages/@ama-sdk/core/src/plugins/keepalive/readme.md b/packages/@ama-sdk/core/src/plugins/keepalive/readme.md index 937dc1c6be..fb063a3229 100644 --- a/packages/@ama-sdk/core/src/plugins/keepalive/readme.md +++ b/packages/@ama-sdk/core/src/plugins/keepalive/readme.md @@ -1,5 +1,8 @@ ## Keep Alive +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + Plugin to add the keepalive flag to the request. ### Type of plugins diff --git a/packages/@ama-sdk/core/src/plugins/mock-intercept/README.md b/packages/@ama-sdk/core/src/plugins/mock-intercept/README.md index 2f1323710a..e84d939cdb 100644 --- a/packages/@ama-sdk/core/src/plugins/mock-intercept/README.md +++ b/packages/@ama-sdk/core/src/plugins/mock-intercept/README.md @@ -25,6 +25,9 @@ const baseConfig = new ApiFetchClient({ ## Mock intercept fetch plugin +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + The mock mechanism provides, via the `getResponse` function, a way to completely override the fetch response. To apply the mock at FetchAPI level, we provide the `MockInterceptFetch`. It will work with the `MockInterceptRequest` on the same mock set. diff --git a/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.fetch.ts b/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.fetch.ts index 84e50c20fa..92e1dde85e 100644 --- a/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.fetch.ts +++ b/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.fetch.ts @@ -7,6 +7,7 @@ import { MockInterceptRequest } from './mock-intercept.request'; * * This plugin should be used only with the MockInterceptRequest Plugin. * It will allow the user to delay the response or to handle the getResponse function provided with the mock (if present). + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export class MockInterceptFetch implements FetchPlugin { diff --git a/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.interface.ts b/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.interface.ts index 95c497ed7d..67053022d4 100644 --- a/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.interface.ts +++ b/packages/@ama-sdk/core/src/plugins/mock-intercept/mock-intercept.interface.ts @@ -1,7 +1,10 @@ import { MockAdapter } from '../../fwk/index'; import { FetchPluginContext, RequestOptions } from '../core/index'; -/** Mock Fetch Plugin options */ +/** + * Mock Fetch Plugin options + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 + */ export interface MockInterceptFetchParameters { /** List of mocks to be used */ adapter: MockAdapter; diff --git a/packages/@ama-sdk/core/src/plugins/perf-metric/perf-metric.fetch.ts b/packages/@ama-sdk/core/src/plugins/perf-metric/perf-metric.fetch.ts index 2ae4d0b46d..02a6079f43 100644 --- a/packages/@ama-sdk/core/src/plugins/perf-metric/perf-metric.fetch.ts +++ b/packages/@ama-sdk/core/src/plugins/perf-metric/perf-metric.fetch.ts @@ -3,6 +3,7 @@ import { FetchCall, FetchPlugin, FetchPluginContext } from '../core'; /** * Performance metric mark associated to a call. + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export interface Mark { /** @@ -51,6 +52,7 @@ type CrossPlatformPerformance = { /** * Options for this plugin. + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export interface PerformanceMetricOptions { /** @@ -84,6 +86,7 @@ export interface PerformanceMetricOptions { /** * Performance metric plugin. + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export class PerformanceMetricPlugin implements FetchPlugin { /** diff --git a/packages/@ama-sdk/core/src/plugins/perf-metric/readme.md b/packages/@ama-sdk/core/src/plugins/perf-metric/readme.md index e2e289f5f7..b96a19e3e0 100644 --- a/packages/@ama-sdk/core/src/plugins/perf-metric/readme.md +++ b/packages/@ama-sdk/core/src/plugins/perf-metric/readme.md @@ -1,5 +1,8 @@ ## Performance Metric +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + Plugin to measure and report performance metrics of the SDK processes. ### Type of plugins diff --git a/packages/@ama-sdk/core/src/plugins/retry/readme.md b/packages/@ama-sdk/core/src/plugins/retry/readme.md index 2f4d0bf686..3467d46074 100644 --- a/packages/@ama-sdk/core/src/plugins/retry/readme.md +++ b/packages/@ama-sdk/core/src/plugins/retry/readme.md @@ -1,5 +1,8 @@ ## Retry +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + Plugin to Retry a fetch call. ### Type of plugins diff --git a/packages/@ama-sdk/core/src/plugins/retry/retry.fetch.ts b/packages/@ama-sdk/core/src/plugins/retry/retry.fetch.ts index 37026bd525..2a166227cc 100644 --- a/packages/@ama-sdk/core/src/plugins/retry/retry.fetch.ts +++ b/packages/@ama-sdk/core/src/plugins/retry/retry.fetch.ts @@ -5,6 +5,7 @@ import { FetchCall, FetchPlugin, FetchPluginContext } from '../core'; * Function to run to determine if we need to retry the call * @param numberOfRetry * @param condition + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 * @example * ```typescript * const condition = async (context: FetchPluginContext, data?: Response, error?: Error) => { @@ -26,6 +27,7 @@ export type RetryConditionType = (context: FetchPluginContext, data?: Response, /** * Plugin to Retry a fetch call + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export class RetryFetch implements FetchPlugin { /** Number of retry */ diff --git a/packages/@ama-sdk/core/src/plugins/timeout/readme.md b/packages/@ama-sdk/core/src/plugins/timeout/readme.md index f0b9ba43d7..f4d1f9572d 100644 --- a/packages/@ama-sdk/core/src/plugins/timeout/readme.md +++ b/packages/@ama-sdk/core/src/plugins/timeout/readme.md @@ -1,15 +1,21 @@ # Timeout + +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + Plugin to raise an exception on a fetch request timeout. The timeout can be configured to stop and restart from the beginning depending on events. ## Timeout pause/restart mechanism + You can configure a ``TimeoutPauseEventHandler`` to stop the timeout from throwing errors upon some events. One of these example is the Captcha. If your user is currently resolving a Captcha, the request might not go through until the Captcha is fully resolved. This is not something you actually want. -### Imperva Captcha event -Today the @ama-sdk/core plugin exposes the ``impervaCaptchaEventHandlerFactory`` that will emit an event if a Captcha has +### Imperva Captcha event + +Today the @ama-sdk/core plugin exposes the ``impervaCaptchaEventHandlerFactory`` that will emit an event if a Captcha has been displayed on your website. It is only compatible with Imperva UI events and can be used as follows: ```typescript @@ -21,6 +27,7 @@ const fetchPlugin = new TimeoutFetch(60000, impervaCaptchaEventHandlerFactory({w Only events posted from the white listed domain will be listened to, make sure to correctly configure the factory. ### Custom event + You can create your own ``TimeoutPauseEventHandler`` that will call the timeoutPauseCallback whenever you need to pause or restart the timeout. diff --git a/packages/@ama-sdk/core/src/plugins/timeout/timeout.fetch.ts b/packages/@ama-sdk/core/src/plugins/timeout/timeout.fetch.ts index b8030a8b1f..f4a91408b2 100644 --- a/packages/@ama-sdk/core/src/plugins/timeout/timeout.fetch.ts +++ b/packages/@ama-sdk/core/src/plugins/timeout/timeout.fetch.ts @@ -18,6 +18,7 @@ type ImpervaCaptchaMessageData = { * Today, only the stop and restart of the timer is supported which match the following events: * - stop: stop the timeout timer * - start: reset the timer and restart it + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 */ export type TimeoutStatus = 'timeoutStopped' | 'timeoutStarted'; diff --git a/packages/@ama-sdk/core/src/plugins/wait-for/readme.md b/packages/@ama-sdk/core/src/plugins/wait-for/readme.md index 999cc698a1..bd74624639 100644 --- a/packages/@ama-sdk/core/src/plugins/wait-for/readme.md +++ b/packages/@ama-sdk/core/src/plugins/wait-for/readme.md @@ -1,5 +1,8 @@ ## Wait for +> [!WARNING] +> This package is now exposed by [@ama-sdk/client-fetch](https://npmjs.com/package/@ama-sdk/client-fetch). It will be removed from this package in v13. + Plugin to determine if and when a call should be processed. ### Type of plugins diff --git a/packages/@ama-sdk/core/src/plugins/wait-for/wait-for.fetch.ts b/packages/@ama-sdk/core/src/plugins/wait-for/wait-for.fetch.ts index cdd1770d5f..1a5f5c0ae0 100644 --- a/packages/@ama-sdk/core/src/plugins/wait-for/wait-for.fetch.ts +++ b/packages/@ama-sdk/core/src/plugins/wait-for/wait-for.fetch.ts @@ -1,9 +1,15 @@ import { FetchCall, FetchPlugin, FetchPluginContext } from '../core'; -/** Callback function type */ +/** + * Callback function type + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 + */ export type CallbackFunction = (context: FetchPluginContext & {data: T | undefined}, fetchCall: FetchCall, response?: Response) => void; -/** Result of the condition function */ +/** + * Result of the condition function + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 + */ export interface CanStartConditionResult { /** Actual result of the condition */ result: boolean | Promise; @@ -14,12 +20,14 @@ export interface CanStartConditionResult { /** * Condition function to determine if the call can start + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 * @returns True if the call can start, False if it should be canceled */ export type CanStartConditionFunction = (context: FetchPluginContext) => CanStartConditionResult | Promise>; /** * Plugin to determine if and when a call should be processed + * @deprecated Use the one exposed by {@link @ama-sdk/client-fetch}, will be removed in v13 * @example * ```typescript * // Use the plugin for an orchestrator 1 per 1 diff --git a/packages/@ama-sdk/core/tsconfig.spec.json b/packages/@ama-sdk/core/tsconfig.spec.json index 8c0d0e6bf2..cf096bfec9 100644 --- a/packages/@ama-sdk/core/tsconfig.spec.json +++ b/packages/@ama-sdk/core/tsconfig.spec.json @@ -5,7 +5,7 @@ "strictNullChecks": false, "strictPropertyInitialization":false, "noImplicitAny": false, - "incremental": false, + "incremental": true, "composite": true, "declarationMap": false, "strict": false, diff --git a/packages/@ama-sdk/create/README.md b/packages/@ama-sdk/create/README.md index c5fac6a063..b5b72383b6 100644 --- a/packages/@ama-sdk/create/README.md +++ b/packages/@ama-sdk/create/README.md @@ -44,7 +44,7 @@ npm create @ama-sdk typescript -- --package-manager=yarn [...opti - `--spec-path`: Path to the swagger/open-api specification used to generate the SDK - `--spec-package-name`: The npm package name where the spec file can be fetched -- `--spec-package-path`: The export path inside the package.json where to find the spec file. Defaults to _./openapi.[yml|yaml|json]_ +- `--spec-package-path`: The export path inside the package.json where to find the spec file. Defaults to *./openapi.[yml|yaml|json]* - `--spec-package-version`: The version to target for the npm package where the spec file can be fetched - `--spec-package-registry`: The npm registry where the spec file can be fetched @@ -52,4 +52,4 @@ npm create @ama-sdk typescript -- --package-manager=yarn [...opti > If `--spec-path` or `--spec-package-name` is specified, the SDK will be generated based on this specification at the creation time. > [!NOTE] -> > `--spec-package-registry` option assumes that the authentication is set up globally (See [npm setup](https://docs.npmjs.com/cli/v8/configuring-npm/npmrc#auth-related-configuration), [yarn setup](https://yarnpkg.com/configuration/yarnrc#npmRegistries)) +> `--spec-package-registry` option assumes that the authentication is set up globally (See [npm setup](https://docs.npmjs.com/cli/v8/configuring-npm/npmrc#auth-related-configuration), [yarn setup](https://yarnpkg.com/configuration/yarnrc#npmRegistries)) diff --git a/packages/@ama-sdk/schematics/README.md b/packages/@ama-sdk/schematics/README.md index 4d6192b034..b712804c45 100644 --- a/packages/@ama-sdk/schematics/README.md +++ b/packages/@ama-sdk/schematics/README.md @@ -127,7 +127,7 @@ Please note that revivers are generated for SDKs that use: If your specification file includes dates, there are multiple options for the generation of your SDK involving the global property option `stringifyDate`: -- By default, the option `stringifyDate` is set to `true`. Set it to `false` if you want date-time objects to be generated +- By default, the option `stringifyDate` is set to `true`. Set it to `false` if you want date-time objects to be generated as `Date` and date objects to be generated as `utils.Date`. For more information related to these types, check out this [documentation](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/schematics/schematics/typescript/shell/templates/base#manage-dates). This can be done by adding `--global-property stringifyDate=false` to the generator command or by adding the global property @@ -297,3 +297,14 @@ yarn schematics @ama-sdk/schematics:java-client-core --spec-path ./swagger-spec. ``` [Default swagger config](schematics/java/client-core/swagger-codegen-java-client/config/swagger-codegen-config.json) will be used if `--swagger-config-path` is not provided. + +## Command Line Interface + +This package also comes with CLI scripts that can facilitate the upgrade and publication of an SDK. +Use `--help` on each command for more information + +| Script | Description | +| --------------------------- | ---------------------------------------------------------------------------------------------- | +| amasdk-clear-index | Remove the index files that are no longer necessary after the deletion of the associated model | +| amasdk-files-pack | Prepare the dist folder for publication | +| amasdk-update-spec-from-npm | Update the OpenAPI spec from an NPM package | diff --git a/packages/@ama-sdk/schematics/schematics/typescript/core/openapi-codegen-typescript/src/main/resources/typescriptFetch/spec/api-mock.mustache b/packages/@ama-sdk/schematics/schematics/typescript/core/openapi-codegen-typescript/src/main/resources/typescriptFetch/spec/api-mock.mustache index e9a8908301..79c665aead 100644 --- a/packages/@ama-sdk/schematics/schematics/typescript/core/openapi-codegen-typescript/src/main/resources/typescriptFetch/spec/api-mock.mustache +++ b/packages/@ama-sdk/schematics/schematics/typescript/core/openapi-codegen-typescript/src/main/resources/typescriptFetch/spec/api-mock.mustache @@ -1,5 +1,6 @@ {{#apiInfo}} -import { ApiClient, ApiFetchClient, BaseApiFetchClientConstructor, isApiClient } from '@ama-sdk/core'; +import { type ApiClient, isApiClient } from '@ama-sdk/core'; +import { ApiFetchClient, type BaseApiFetchClientConstructor } from '@ama-sdk/client-fetch'; import * as api from '../api'; diff --git a/packages/@ama-sdk/schematics/schematics/typescript/shell/templates/base/package.json.template b/packages/@ama-sdk/schematics/schematics/typescript/shell/templates/base/package.json.template index 3fcae892ab..6f236ba4c2 100644 --- a/packages/@ama-sdk/schematics/schematics/typescript/shell/templates/base/package.json.template +++ b/packages/@ama-sdk/schematics/schematics/typescript/shell/templates/base/package.json.template @@ -62,6 +62,9 @@ "peerDependenciesMeta": { "isomorphic-fetch": { "optional": true + }, + "@ama-sdk/client-fetch": { + "optional": true } }, "devDependencies": { @@ -77,6 +80,7 @@ "@schematics/angular": "<%= angularVersion %>", "@commitlint/config-conventional": "<%= versions['@commitlint/config-conventional'] %>", "@ama-sdk/schematics": "<%= sdkCoreRange %>", + "@ama-sdk/client-fetch": "<%= sdkCoreRange %>", "@ama-sdk/core": "<%= sdkCoreRange %>", "@o3r/eslint-config-otter": "<%= sdkCoreRange %>", "@o3r/eslint-plugin": "<%= sdkCoreRange %>", @@ -116,6 +120,7 @@ "@o3r/schematics": "<%= sdkCoreRange %>" },<% } %> "peerDependencies": { + "@ama-sdk/client-fetch": "<%= sdkCoreRange %>", "@ama-sdk/core": "~<%= sdkCoreVersion %>", "isomorphic-fetch": "<%= versions['isomorphic-fetch'] %>" }, diff --git a/packages/@ama-sdk/showcase-sdk/package.json b/packages/@ama-sdk/showcase-sdk/package.json index 14509e3eb6..ec4ce878a6 100644 --- a/packages/@ama-sdk/showcase-sdk/package.json +++ b/packages/@ama-sdk/showcase-sdk/package.json @@ -59,11 +59,15 @@ "tslib": "^2.6.2" }, "peerDependenciesMeta": { + "@ama-sdk/client-fetch": { + "optional": true + }, "isomorphic-fetch": { "optional": true } }, "devDependencies": { + "@ama-sdk/client-fetch": "workspace:^", "@ama-sdk/core": "workspace:^", "@ama-sdk/schematics": "workspace:^", "@angular-devkit/core": "~18.2.0", @@ -111,6 +115,7 @@ "typescript": "~5.5.4" }, "peerDependencies": { + "@ama-sdk/client-fetch": "workspace:^", "@ama-sdk/core": "workspace:^", "isomorphic-fetch": "~3.0.0" }, diff --git a/packages/@ama-sdk/showcase-sdk/src/spec/api-mock.ts b/packages/@ama-sdk/showcase-sdk/src/spec/api-mock.ts index 47826a2975..fbc29e4368 100644 --- a/packages/@ama-sdk/showcase-sdk/src/spec/api-mock.ts +++ b/packages/@ama-sdk/showcase-sdk/src/spec/api-mock.ts @@ -1,4 +1,5 @@ -import { ApiClient, ApiFetchClient, BaseApiFetchClientConstructor, isApiClient } from '@ama-sdk/core'; +import { ApiClient, isApiClient } from '@ama-sdk/core'; +import { ApiFetchClient, BaseApiFetchClientConstructor } from '@ama-sdk/client-fetch'; import * as api from '../api'; diff --git a/packages/@o3r/apis-manager/README.md b/packages/@o3r/apis-manager/README.md index 2e76eadec2..c7856893f9 100644 --- a/packages/@o3r/apis-manager/README.md +++ b/packages/@o3r/apis-manager/README.md @@ -42,6 +42,7 @@ The plugins and fetch client come from the ``@ama-sdk/core`` module, but custom ```typescript import { ApiFetchClient, ApiKeyRequest, JsonTokenReply, JsonTokenRequest, ReviverReply, ExceptionReply } from '@ama-sdk/core'; +import { ApiFetchClient } from '@ama-sdk/client-fetch'; import { ApiManager, ApiManagerModule } from '@o3r/apis-manager'; const PROXY_SERVER = "https://your-enpoint-base-path"; @@ -72,6 +73,7 @@ The **ApiManager** instance can be customized via a *factory* function provided ```typescript import { ApiClient, ApiFetchClient, ApiKeyRequest, Mark, PerformanceMetricPlugin } from '@ama-sdk/core'; +import { ApiFetchClient } from '@ama-sdk/client-fetch'; import { ApiManager, ApiManagerModule, API_TOKEN } from '@o3r/apis-manager'; import { EventTrackService } from '@o3r/analytics'; @@ -173,7 +175,7 @@ The configuration can be overridden after the instantiation of the API. ```typescript import { ExampleApi } from '@shared/sdk'; import { ApiFactoryService } from '@o3r/apis-manager'; -import { ApiFetchClient } from '@ama-sdk/core'; +import { ApiFetchClient } from '@ama-sdk/client-fetch'; @Injectable() class MyClass { diff --git a/packages/@o3r/apis-manager/package.json b/packages/@o3r/apis-manager/package.json index f35f0bd73d..f1b2f654a1 100644 --- a/packages/@o3r/apis-manager/package.json +++ b/packages/@o3r/apis-manager/package.json @@ -19,6 +19,7 @@ "build:builders": "tsc -b tsconfig.builders.json --pretty && yarn generate-cjs-manifest" }, "peerDependencies": { + "@ama-sdk/client-fetch": "workspace:^", "@ama-sdk/core": "workspace:^", "@angular-devkit/schematics": "~18.2.0", "@angular/common": "~18.2.0", @@ -29,6 +30,9 @@ "typescript": "~5.5.4" }, "peerDependenciesMeta": { + "@ama-sdk/client-fetch": { + "optional": true + }, "@angular-devkit/schematics": { "optional": true }, @@ -43,6 +47,7 @@ "tslib": "^2.6.2" }, "devDependencies": { + "@ama-sdk/client-fetch": "workspace:^", "@ama-sdk/core": "workspace:^", "@angular-devkit/build-angular": "~18.2.0", "@angular-devkit/core": "~18.2.0", diff --git a/packages/@o3r/apis-manager/schematics/helpers/update-api-deps/index.ts b/packages/@o3r/apis-manager/schematics/helpers/update-api-deps/index.ts index a22018ec02..bd86168178 100644 --- a/packages/@o3r/apis-manager/schematics/helpers/update-api-deps/index.ts +++ b/packages/@o3r/apis-manager/schematics/helpers/update-api-deps/index.ts @@ -9,13 +9,13 @@ import { import { isImported } from '@schematics/angular/utility/ast-utils'; import { addRootImport, addRootProvider } from '@schematics/angular/utility'; import * as ts from 'typescript'; +import type { NgAddSchematicsSchema } from '../../ng-add/schema'; /** * Update app.module file with api manager, if needed * @param options - * @param options.projectName */ -export function updateApiDependencies(options: {projectName?: string | undefined}): Rule { +export function updateApiDependencies(options: NgAddSchematicsSchema): Rule { const updateAppModule: Rule = (tree: Tree, context: SchematicContext) => { const additionalRules: Rule[] = []; @@ -61,7 +61,8 @@ export function updateApiDependencies(options: {projectName?: string | undefined addImportToModuleFile('ApiManagerModule', '@o3r/apis-manager'); - insertBeforeModule(` + if (!options.skipCodeSample) { + insertBeforeModule(` export function apiManagerFactory(): ApiManager { const apiClient = new ApiFetchClient({ basePath: PROXY_SERVER, @@ -71,10 +72,11 @@ export function apiManagerFactory(): ApiManager { return new ApiManager(apiClient); }`); - addProviderToModuleFile('API_TOKEN', '@o3r/apis-manager', 'useFactory: apiManagerFactory'); + addProviderToModuleFile('API_TOKEN', '@o3r/apis-manager', 'useFactory: apiManagerFactory'); + insertImportToModuleFile('ApiFetchClient', '@ama-sdk/client-fetch', false); + } insertImportToModuleFile('ApiManager', '@o3r/apis-manager', false); - insertImportToModuleFile('ApiFetchClient', '@ama-sdk/core', false); insertImportToModuleFile('ApiKeyRequest', '@ama-sdk/core', false); tree.commitUpdate(recorder); diff --git a/packages/@o3r/apis-manager/schematics/ng-add/index.ts b/packages/@o3r/apis-manager/schematics/ng-add/index.ts index 4e79484139..2b85a70404 100644 --- a/packages/@o3r/apis-manager/schematics/ng-add/index.ts +++ b/packages/@o3r/apis-manager/schematics/ng-add/index.ts @@ -26,6 +26,10 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { rulesToExecute.push(updateApiDependencies(options)); } + if (!options.skipCodeSample) { + depsInfo.o3rPeerDeps.push('@ama-sdk/client-fetch'); + } + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { acc[dep] = { inManifest: [{ diff --git a/packages/@o3r/apis-manager/schematics/ng-add/schema.json b/packages/@o3r/apis-manager/schematics/ng-add/schema.json index 981f64b5e0..7745254f26 100644 --- a/packages/@o3r/apis-manager/schematics/ng-add/schema.json +++ b/packages/@o3r/apis-manager/schematics/ng-add/schema.json @@ -25,6 +25,11 @@ "type": "boolean", "description": "Use a pinned version for otter packages", "default": false + }, + "skipCodeSample": { + "type": "boolean", + "description": "Skip the code sample generated in application to register the ApiManager (which is relying on FetchApiClient)", + "default": false } }, "required": [ diff --git a/packages/@o3r/apis-manager/schematics/ng-add/schema.ts b/packages/@o3r/apis-manager/schematics/ng-add/schema.ts index bde06b1ea9..b709dcff00 100644 --- a/packages/@o3r/apis-manager/schematics/ng-add/schema.ts +++ b/packages/@o3r/apis-manager/schematics/ng-add/schema.ts @@ -12,4 +12,10 @@ export interface NgAddSchematicsSchema extends SchematicOptionObject { /** Use a pinned version for otter packages */ exactO3rVersion?: boolean; + + /** + * Skip the code sample generated in application to register the ApiManager + * If `false`, a dependency to @ama-sdk/client-fetch will be added + */ + skipCodeSample?: boolean; } diff --git a/tsconfig.base.json b/tsconfig.base.json index 9bd2753a8a..77c4dce291 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -34,6 +34,9 @@ "baseUrl": ".", "allowSyntheticDefaultImports": true, "paths": { + "@ama-sdk/client-fetch": [ + "packages/@ama-sdk/client-fetch/src/public_api" + ], "@ama-sdk/core": [ "packages/@ama-sdk/core/src/public_api" ], diff --git a/tsconfig.build.json b/tsconfig.build.json index 84e9065945..232eb893c9 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,6 +4,7 @@ "extends": "./tsconfig.base", "compilerOptions": { "paths": { + "@ama-sdk/client-fetch": ["packages/@ama-sdk/client-fetch/dist", "packages/@ama-sdk/client-fetch/src/public_api"], "@ama-sdk/core": ["packages/@ama-sdk/core/dist", "packages/@ama-sdk/core/src/public_api"], "@ama-sdk/showcase-sdk": ["packages/@ama-sdk/showcase-sdk/dist", "packages/@ama-sdk/showcase-sdk/src/index"], "@ama-sdk/swagger-builder": ["packages/@ama-sdk/swagger-build/dist", "packages/@ama-sdk/swagger-builder"], diff --git a/yarn.lock b/yarn.lock index 469fe313ae..2ea23f3e54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -267,6 +267,82 @@ __metadata: languageName: node linkType: hard +"@ama-sdk/client-fetch@workspace:^, @ama-sdk/client-fetch@workspace:packages/@ama-sdk/client-fetch": + version: 0.0.0-use.local + resolution: "@ama-sdk/client-fetch@workspace:packages/@ama-sdk/client-fetch" + dependencies: + "@ama-sdk/core": "workspace:^" + "@angular-devkit/core": "npm:~18.2.0" + "@angular-devkit/schematics": "npm:~18.2.0" + "@angular-eslint/eslint-plugin": "npm:~18.3.0" + "@angular/common": "npm:~18.2.0" + "@angular/core": "npm:~18.2.0" + "@nx/eslint-plugin": "npm:~19.5.0" + "@nx/jest": "npm:~19.5.0" + "@o3r/build-helpers": "workspace:^" + "@o3r/eslint-plugin": "workspace:^" + "@o3r/test-helpers": "workspace:^" + "@schematics/angular": "npm:~18.2.0" + "@stylistic/eslint-plugin-ts": "npm:~2.4.0" + "@swc/cli": "npm:~0.4.0" + "@swc/core": "npm:~1.7.0" + "@swc/helpers": "npm:~0.5.0" + "@types/jest": "npm:~29.5.2" + "@types/node": "npm:^20.0.0" + "@types/uuid": "npm:^9.0.0" + "@typescript-eslint/eslint-plugin": "npm:^7.14.1" + "@typescript-eslint/parser": "npm:^7.14.1" + "@typescript-eslint/utils": "npm:^7.14.1" + cpy-cli: "npm:^5.0.0" + eslint: "npm:^8.57.0" + eslint-import-resolver-node: "npm:^0.3.9" + eslint-plugin-jest: "npm:~28.8.0" + eslint-plugin-jsdoc: "npm:~48.11.0" + eslint-plugin-prefer-arrow: "npm:~1.2.3" + eslint-plugin-unicorn: "npm:^54.0.0" + isomorphic-fetch: "npm:~3.0.0" + jest: "npm:~29.7.0" + jest-junit: "npm:~16.0.0" + jsonc-eslint-parser: "npm:~2.4.0" + minimist: "npm:^1.2.6" + pid-from-port: "npm:^1.1.3" + rimraf: "npm:^5.0.1" + rxjs: "npm:^7.8.1" + semver: "npm:^7.5.2" + ts-jest: "npm:~29.2.0" + ts-node: "npm:~10.9.2" + tslib: "npm:^2.6.2" + type-fest: "npm:^4.10.2" + typescript: "npm:~5.5.4" + uuid: "npm:^10.0.0" + zone.js: "npm:~0.14.2" + peerDependencies: + "@ama-sdk/core": "workspace:^" + "@angular-devkit/schematics": ~18.2.0 + "@angular/cli": ~18.2.0 + "@angular/common": ~18.2.0 + "@o3r/schematics": "workspace:^" + "@schematics/angular": ~18.2.0 + isomorphic-fetch: ^3.0.0 + typescript: ~5.5.4 + peerDependenciesMeta: + "@angular-devkit/schematics": + optional: true + "@angular/cli": + optional: true + "@angular/common": + optional: true + "@o3r/schematics": + optional: true + "@schematics/angular": + optional: true + isomorphic-fetch: + optional: true + typescript: + optional: true + languageName: unknown + linkType: soft + "@ama-sdk/core@workspace:*, @ama-sdk/core@workspace:^, @ama-sdk/core@workspace:packages/@ama-sdk/core": version: 0.0.0-use.local resolution: "@ama-sdk/core@workspace:packages/@ama-sdk/core" @@ -292,7 +368,6 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^7.14.1" "@typescript-eslint/parser": "npm:^7.14.1" "@typescript-eslint/utils": "npm:^7.14.1" - copyfiles: "npm:^2.4.1" cpy-cli: "npm:^5.0.0" eslint: "npm:^8.57.0" eslint-import-resolver-node: "npm:^0.3.9" @@ -492,6 +567,7 @@ __metadata: version: 0.0.0-use.local resolution: "@ama-sdk/showcase-sdk@workspace:packages/@ama-sdk/showcase-sdk" dependencies: + "@ama-sdk/client-fetch": "workspace:^" "@ama-sdk/core": "workspace:^" "@ama-sdk/schematics": "workspace:^" "@angular-devkit/core": "npm:~18.2.0" @@ -540,9 +616,12 @@ __metadata: typedoc: "npm:~0.26.0" typescript: "npm:~5.5.4" peerDependencies: + "@ama-sdk/client-fetch": "workspace:^" "@ama-sdk/core": "workspace:^" isomorphic-fetch: ~3.0.0 peerDependenciesMeta: + "@ama-sdk/client-fetch": + optional: true isomorphic-fetch: optional: true languageName: unknown @@ -6544,6 +6623,7 @@ __metadata: version: 0.0.0-use.local resolution: "@o3r/apis-manager@workspace:packages/@o3r/apis-manager" dependencies: + "@ama-sdk/client-fetch": "workspace:^" "@ama-sdk/core": "workspace:^" "@angular-devkit/build-angular": "npm:~18.2.0" "@angular-devkit/core": "npm:~18.2.0" @@ -6596,6 +6676,7 @@ __metadata: typescript: "npm:~5.5.4" zone.js: "npm:~0.14.2" peerDependencies: + "@ama-sdk/client-fetch": "workspace:^" "@ama-sdk/core": "workspace:^" "@angular-devkit/schematics": ~18.2.0 "@angular/common": ~18.2.0 @@ -6605,6 +6686,8 @@ __metadata: rxjs: ^7.8.1 typescript: ~5.5.4 peerDependenciesMeta: + "@ama-sdk/client-fetch": + optional: true "@angular-devkit/schematics": optional: true "@o3r/schematics": @@ -8772,6 +8855,7 @@ __metadata: dependencies: "@agnos-ui/angular-bootstrap": "npm:~0.4.1" "@agnos-ui/core-bootstrap": "npm:~0.4.1" + "@ama-sdk/client-fetch": "workspace:^" "@ama-sdk/core": "workspace:^" "@ama-sdk/schematics": "workspace:^" "@ama-sdk/showcase-sdk": "workspace:^"