From 06e9b8d83b2c2265b5015000c2168a5d41b50423 Mon Sep 17 00:00:00 2001 From: Ali Amori Kadhim Date: Fri, 27 Sep 2024 16:27:29 +0200 Subject: [PATCH 1/2] feat: create utils package --- .eslintrc.js | 2 + packages/utils/global.d.ts | 1 + packages/utils/jest.config.ts | 19 +++ packages/utils/package.json | 56 +++++++ packages/utils/rollup.config.ts | 44 +++++ packages/utils/src/errorHandler/index.test.ts | 47 ++++++ packages/utils/src/errorHandler/index.ts | 16 ++ .../src/get-strapi-plugin-name/index.test.ts | 7 + .../utils/src/get-strapi-plugin-name/index.ts | 11 ++ packages/utils/src/index.ts | 3 + packages/utils/tsconfig.json | 27 ++++ packages/utils/tsconfig.test.json | 12 ++ yarn.lock | 153 +++++++++++++++++- 13 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 packages/utils/global.d.ts create mode 100644 packages/utils/jest.config.ts create mode 100644 packages/utils/package.json create mode 100644 packages/utils/rollup.config.ts create mode 100644 packages/utils/src/errorHandler/index.test.ts create mode 100644 packages/utils/src/errorHandler/index.ts create mode 100644 packages/utils/src/get-strapi-plugin-name/index.test.ts create mode 100644 packages/utils/src/get-strapi-plugin-name/index.ts create mode 100644 packages/utils/src/index.ts create mode 100644 packages/utils/tsconfig.json create mode 100644 packages/utils/tsconfig.test.json diff --git a/.eslintrc.js b/.eslintrc.js index 0a6282ee..20c57715 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -80,6 +80,8 @@ module.exports = { './packages/ui/tsconfig.json', './packages/ui/tsconfig.test.json', './packages/upl/tsconfig.json', + './packages/utils/tsconfig.json', + './packages/utils/tsconfig.test.json', ], tsconfigRootDir: __dirname, }, diff --git a/packages/utils/global.d.ts b/packages/utils/global.d.ts new file mode 100644 index 00000000..8a1c33c7 --- /dev/null +++ b/packages/utils/global.d.ts @@ -0,0 +1 @@ +declare module 'rollup-plugin-peer-deps-external'; diff --git a/packages/utils/jest.config.ts b/packages/utils/jest.config.ts new file mode 100644 index 00000000..7c2fda9d --- /dev/null +++ b/packages/utils/jest.config.ts @@ -0,0 +1,19 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + // to obtain access to the matchers. + moduleFileExtensions: ['ts', 'js', 'json', 'node'], + modulePaths: [''], + testEnvironment: 'node', + transform: { + '^.+\\.(ts)$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.test.json', + }, + ], + }, +}; + +export default config; diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 00000000..3e68e8ea --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,56 @@ +{ + "name": "@frameless/utils", + "version": "0.0.0", + "description": "A shared utils library", + "main": "./dist/index.cjs.js", + "module": "./dist/index.esm.js", + "types": "./dist/src/index.d.ts", + "private": true, + "files": [ + "dist/" + ], + "keywords": [], + "repository": { + "type": "git+ssh", + "url": "git@github.com:frameless/strapi.git", + "directory": "packages/utils" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com/" + }, + "author": { + "name": "@frameless" + }, + "engines": { + "node": "20.x.x" + }, + "license": "EUPL-1.2", + "peerDependencies": {}, + "scripts": { + "prebuild": "yarn clean", + "build": "rollup --config rollup.config.ts --configPlugin typescript --bundleConfigAsCjs", + "watch": "rollup --config rollup.config.ts --configPlugin typescript -w --bundleConfigAsCjs", + "clean": "rimraf dist .rollup.cach dist", + "test": "jest --coverage", + "test:watch": "jest --watch", + "lint-build": "tsc --noEmit --project tsconfig.json" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "25.0.8", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-node-resolve": "15.3.0", + "@rollup/plugin-typescript": "11.1.6", + "@types/jest": "29.5.12", + "jest": "29.7.0", + "rimraf": "6.0.1", + "rollup": "3.29.5", + "rollup-plugin-copy": "3.5.0", + "rollup-plugin-peer-deps-external": "2.2.4", + "rollup-plugin-terser": "7.0.2", + "rollup-plugin-typescript2": "0.36.0", + "ts-jest": "29.2.3", + "ts-node": "10.9.2", + "typescript": "5.0.4" + }, + "dependencies": {} +} diff --git a/packages/utils/rollup.config.ts b/packages/utils/rollup.config.ts new file mode 100644 index 00000000..984f6566 --- /dev/null +++ b/packages/utils/rollup.config.ts @@ -0,0 +1,44 @@ +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import { readFileSync } from 'node:fs'; +import { RollupOptions } from 'rollup'; +import copy from 'rollup-plugin-copy'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import { terser } from 'rollup-plugin-terser'; +import typescript from 'rollup-plugin-typescript2'; + +const packageJson = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); +export const outputGlobals = {}; + +const config: RollupOptions = { + input: 'src/index.ts', // Entry point to your library + output: [ + { + file: packageJson.main, + format: 'cjs', // CommonJS format + exports: 'auto', // Automatic exports for CommonJS + globals: outputGlobals, // Global variables exposed to the browser + }, + { + file: packageJson.module, + format: 'esm', // ES Module format + globals: outputGlobals, + }, + ], + plugins: [ + typescript({ + tsconfig: 'tsconfig.json', + }), + nodeResolve(), + commonjs(), + terser(), // Minify the output + peerDepsExternal(), // Treat peer dependencies as externals + copy({ + targets: [{ src: 'src/types/*.d.ts', dest: 'dist/types' }], // Copy type declarations to the dist folder + }), + json(), + ], +}; + +export default config; diff --git a/packages/utils/src/errorHandler/index.test.ts b/packages/utils/src/errorHandler/index.test.ts new file mode 100644 index 00000000..423349b7 --- /dev/null +++ b/packages/utils/src/errorHandler/index.test.ts @@ -0,0 +1,47 @@ +import { ErrorHandler } from './index'; + +describe('ErrorHandler', () => { + it('should create an instance of ErrorHandler', () => { + const error = new ErrorHandler('Test error', { statusCode: 500 }); + expect(error).toBeInstanceOf(ErrorHandler); + expect(error.message).toBe('Test error'); + expect(error.options?.statusCode).toBe(500); + }); + it('should create an instance of ErrorHandler with isOperational', () => { + const error = new ErrorHandler('Test error', { statusCode: 500 }); + expect(error).toBeInstanceOf(ErrorHandler); + expect(error.message).toBe('Test error'); + expect(error.options?.statusCode).toBe(500); + expect(error.isOperational).toBe(true); + }); + it('should log the error message and status code', () => { + const error = new ErrorHandler('Test error', { statusCode: 500 }); + expect(error).toBeInstanceOf(ErrorHandler); + expect(error.message).toBe('Test error'); + expect(error.options?.statusCode).toBe(500); + }); + it('should create an instance of ErrorHandler without options', () => { + const error = new ErrorHandler('Test error'); + expect(error).toBeInstanceOf(ErrorHandler); + expect(error.message).toBe('Test error'); + expect(error.options).toBeUndefined(); + }); + it('should create an instance of ErrorHandler without message and options', () => { + const error = new ErrorHandler(); + expect(error).toBeInstanceOf(ErrorHandler); + expect(error.message).toBe(''); + expect(error.options).toBeUndefined(); + }); + it('should create an instance of ErrorHandler with only message', () => { + const error = new ErrorHandler('Test error'); + expect(error).toBeInstanceOf(ErrorHandler); + expect(error.message).toBe('Test error'); + expect(error.options).toBeUndefined(); + }); + it('should create an instance of ErrorHandler with only options', () => { + const error = new ErrorHandler(undefined, { statusCode: 500 }); + expect(error).toBeInstanceOf(ErrorHandler); + expect(error.message).toBe(''); + expect(error.options?.statusCode).toBe(500); + }); +}); diff --git a/packages/utils/src/errorHandler/index.ts b/packages/utils/src/errorHandler/index.ts new file mode 100644 index 00000000..800d096b --- /dev/null +++ b/packages/utils/src/errorHandler/index.ts @@ -0,0 +1,16 @@ +export type Options = { + statusCode: number; +}; +export class ErrorHandler extends Error { + isOperational: boolean; // this flag for custom error identification + + constructor( + message?: string, + public options?: Options, + ) { + super(message); + this.name = 'ErrorHandler'; + this.options = options; + this.isOperational = true; // Operational errors should be marked + } +} diff --git a/packages/utils/src/get-strapi-plugin-name/index.test.ts b/packages/utils/src/get-strapi-plugin-name/index.test.ts new file mode 100644 index 00000000..beaa1fbe --- /dev/null +++ b/packages/utils/src/get-strapi-plugin-name/index.test.ts @@ -0,0 +1,7 @@ +import { getStrapiPluginName } from './index'; + +describe('getStrapiPluginName', () => { + it('should return the plugin name without the @frameless/strapi-plugin- prefix', () => { + expect(getStrapiPluginName('@frameless/strapi-plugin-example')).toBe('example'); + }); +}); diff --git a/packages/utils/src/get-strapi-plugin-name/index.ts b/packages/utils/src/get-strapi-plugin-name/index.ts new file mode 100644 index 00000000..2002f349 --- /dev/null +++ b/packages/utils/src/get-strapi-plugin-name/index.ts @@ -0,0 +1,11 @@ +// describe the function +/** + * @param {string} name - The name of the plugin + * @returns {string} The name of the plugin without the prefix + * @example + * getStrapiPluginName('@frameless/strapi-plugin-preview-button') // 'preview-button' + * + * */ + +export const getStrapiPluginName = (name: string): string => + name && name.replace(/^@frameless\/(@[^-,.][\w,-]+\/|strapi-)plugin-/i, ''); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 00000000..87783f0a --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,3 @@ +export { ErrorHandler } from './errorHandler'; +export { fetchData } from './fetchData'; +export { getStrapiPluginName } from './get-strapi-plugin-name'; diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 00000000..44b92e2d --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "composite": true, + "declaration": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "isolatedModules": true, + "lib": ["dom", "es2020"], + "module": "es2020", + "moduleResolution": "node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": ".", + "skipLibCheck": true, + "strict": true, + "target": "es2020" + }, + "include": ["src/**/*.ts", "./rollup.config.ts", "./global.d.ts"], + "exclude": ["**/node_modules/*", "**/*.test.ts"] +} diff --git a/packages/utils/tsconfig.test.json b/packages/utils/tsconfig.test.json new file mode 100644 index 00000000..adb8b93a --- /dev/null +++ b/packages/utils/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2016", + "module": "ES6", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "types": ["jest"] + }, + "include": ["**/*.test.tsx", "**/*.test.ts", "./jest.config.ts", "tests", "src"] +} diff --git a/yarn.lock b/yarn.lock index af20220f..ad799225 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5255,6 +5255,18 @@ is-reference "1.2.1" magic-string "^0.30.3" +"@rollup/plugin-commonjs@25.0.8": + version "25.0.8" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz#c77e608ab112a666b7f2a6bea625c73224f7dd34" + integrity sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.30.3" + "@rollup/plugin-json@6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.0.1.tgz#7e2efcf5ed549963f1444e010611d22f463931c0" @@ -5262,6 +5274,13 @@ dependencies: "@rollup/pluginutils" "^5.0.1" +"@rollup/plugin-json@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.1.0.tgz#fbe784e29682e9bb6dee28ea75a1a83702e7b805" + integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA== + dependencies: + "@rollup/pluginutils" "^5.1.0" + "@rollup/plugin-node-resolve@15.2.3": version "15.2.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" @@ -5274,6 +5293,17 @@ is-module "^1.0.0" resolve "^1.22.1" +"@rollup/plugin-node-resolve@15.3.0": + version "15.3.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz#efbb35515c9672e541c08d59caba2eff492a55d5" + integrity sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + "@rollup/plugin-terser@0.4.4": version "0.4.4" resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz#15dffdb3f73f121aa4fbb37e7ca6be9aeea91962" @@ -9614,6 +9644,13 @@ browserslist@^4.0.0, browserslist@^4.17.3, browserslist@^4.18.1, browserslist@^4 node-releases "^2.0.18" update-browserslist-db "^1.1.0" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -9937,7 +9974,7 @@ ccount@^2.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -12167,6 +12204,13 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +ejs@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.5.4: version "1.5.13" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" @@ -13349,7 +13393,7 @@ fast-json-patch@^3.1.0, fast-json-patch@^3.1.1: resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ== -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -13502,6 +13546,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + filesize@^8.0.6: version "8.0.7" resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" @@ -14320,6 +14371,18 @@ glob@^10.2.2, glob@^10.3.7: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" + integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -16507,6 +16570,23 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015" + integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw== + dependencies: + "@isaacs/cliui" "^8.0.2" + +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + java-properties@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" @@ -16826,7 +16906,7 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" -jest-util@^29.7.0: +jest-util@^29.0.0, jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== @@ -17974,7 +18054,7 @@ lodash.kebabcase@4.1.1, lodash.kebabcase@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== -lodash.memoize@^4.1.2: +lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== @@ -18146,6 +18226,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.1.tgz#3a732fbfedb82c5ba7bca6564ad3f42afcb6e147" + integrity sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ== + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -18218,7 +18303,7 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -19389,6 +19474,13 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" @@ -21283,6 +21375,14 @@ path-scurry@^1.11.1, path-scurry@^1.7.0: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -23896,6 +23996,14 @@ rimraf@5.0.5: dependencies: glob "^10.3.7" +rimraf@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e" + integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A== + dependencies: + glob "^11.0.0" + package-json-from-dist "^1.0.0" + rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -23973,6 +24081,17 @@ rollup-plugin-typescript2@0.35.0: semver "^7.3.7" tslib "^2.4.0" +rollup-plugin-typescript2@0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz#309564eb70d710412f5901344ca92045e180ed53" + integrity sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw== + dependencies: + "@rollup/pluginutils" "^4.1.2" + find-cache-dir "^3.3.2" + fs-extra "^10.0.0" + semver "^7.5.4" + tslib "^2.6.2" + rollup-pluginutils@^2.3.3, rollup-pluginutils@^2.8.2: version "2.8.2" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" @@ -23987,6 +24106,13 @@ rollup@3.29.4: optionalDependencies: fsevents "~2.3.2" +rollup@3.29.5: + version "3.29.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" + integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== + optionalDependencies: + fsevents "~2.3.2" + rollup@^4.2.0: version "4.21.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.0.tgz#28db5f5c556a5180361d35009979ccc749560b9d" @@ -26339,6 +26465,21 @@ ts-easing@^0.2.0: resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== +ts-jest@29.2.3: + version "29.2.3" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.3.tgz#3d226ac36b8b820151a38f164414f9f6b412131f" + integrity sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ== + dependencies: + bs-logger "0.x" + ejs "^3.1.10" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "^7.5.3" + yargs-parser "^21.0.1" + ts-log@^2.2.3: version "2.2.5" resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" @@ -27821,7 +27962,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3, yargs-parser@^20.2.9: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.1.1: +yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== From 73aa569319b5977e132b0797899328b803d35d35 Mon Sep 17 00:00:00 2001 From: Ali Amori Kadhim Date: Fri, 27 Sep 2024 17:31:19 +0200 Subject: [PATCH 2/2] feat: add `fetchData` to utils package - improve the code - add unit test --- packages/utils/src/fetchData/index.test.ts | 236 +++++++++++++++++++++ packages/utils/src/fetchData/index.ts | 146 +++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 packages/utils/src/fetchData/index.test.ts create mode 100644 packages/utils/src/fetchData/index.ts diff --git a/packages/utils/src/fetchData/index.test.ts b/packages/utils/src/fetchData/index.test.ts new file mode 100644 index 00000000..04f26069 --- /dev/null +++ b/packages/utils/src/fetchData/index.test.ts @@ -0,0 +1,236 @@ +/* eslint-disable no-undef */ +import { fetchData } from './index'; + +const url = 'https://api.example.com/graphql'; +const query = ` +query { + users { + id + name + } +} +`; + +describe('fetchData', () => { + it('should fetch data successfully', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + + const data = await fetchData({ url, query }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith(url, { + method: 'POST', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + }); + it('should have POST method by default', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + const data = await fetchData({ url, query }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith(url, { + method: 'POST', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + }); + it('should handle GET method', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + const data = await fetchData({ url: 'https://example.com/api/users', method: 'GET' }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith('https://example.com/api/users', { + method: 'GET', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + it('should handle cache', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data: { users: [] } }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + + const data = await fetchData({ url, query }); + expect(data).toEqual({ data: { users: [] } }); + expect(mockFetch).toHaveBeenCalledWith(url, { + cache: 'no-store', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + }); + it('should handle network error', async () => { + const mockFetch = jest.fn(() => Promise.reject(new Error('Network error'))); + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Network error'); + }); + it('should handle error', async () => { + const mockFetch = jest.fn(() => Promise.reject(new Error('Fetch failed'))); + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow(); + }); + it('should handle error with status code', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 404, + }), + ) as jest.Mock; + global.fetch = mockFetch; + + expect(fetchData({ url, query })).rejects.toThrow('Resource Not Found'); + }); + it('should handle error with status code 400', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 400, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Bad Request'); + }); + it('should handle error with status code 401', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 401, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Unauthorized'); + }); + it('should handle error with status code 403', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ errors: [{ message: 'Forbidden' }] }), + ok: false, + status: 403, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Forbidden'); + }); + it('should handle error with status code 404', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 404, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Resource Not Found'); + }); + it('should handle error with status code 422', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 422, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Unprocessable Entity'); + }); + it('should handle error with status code 500', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 500, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Internal Server Error'); + }); + it('should handle error with status code 503', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 503, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Service Unavailable'); + }); + it('should handle error with status code 504', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 504, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Gateway Timeout'); + }); + it('should handle error with status code 505', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 505, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('HTTP Version Not Supported'); + }); + + it('should handle error with status code 506', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 506, + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('Unexpected error: 506'); + }); + + it('should handle GraphQL error', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ errors: [{ message: 'GraphQL error' }] }), + ok: true, + status: 200, + statusText: 'OK', + }), + ) as jest.Mock; + global.fetch = mockFetch; + expect(fetchData({ url, query })).rejects.toThrow('GraphQL error'); + }); +}); diff --git a/packages/utils/src/fetchData/index.ts b/packages/utils/src/fetchData/index.ts new file mode 100644 index 00000000..9a01eb3d --- /dev/null +++ b/packages/utils/src/fetchData/index.ts @@ -0,0 +1,146 @@ +import { ErrorHandler } from '../errorHandler'; + +interface HandleGraphqlRequestProps { + query?: string; + variables: any; + headers?: HeadersInit; +} + +const handleGraphqlRequest = ({ query, variables, headers }: HandleGraphqlRequestProps) => + ({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...headers, // Merge custom headers (including the ability to overwrite 'Content-Type') + }, + body: JSON.stringify({ query, variables }), + cache: 'no-store', + }) as RequestInit; + +export interface FetchDataProps { + url: string; + query?: string; + variables?: any; + method?: string; + headers?: HeadersInit; // Allow custom headers to be passed +} + +/** + * @description Fetches data from the server (GraphQL or REST). + * @param {string} url - The URL to fetch data from. + * @param {string} query - The GraphQL query (if applicable). + * @param {any} variables - The variables to pass to the GraphQL queries. + * @param {string} method - The HTTP method, default is POST for GraphQL. + * @param {HeadersInit} headers - Custom headers to pass to the request. + * @returns {Promise} - The fetched data. + */ +export const fetchData = async ({ + url, + query, + variables, + method = 'POST', + headers = {}, // Default to an empty object if no headers are provided +}: FetchDataProps): Promise => { + // Default headers, which can be overwritten by custom headers (e.g., Content-Type) + const defaultHeaders: HeadersInit = { + 'Content-Type': 'application/json', + }; + + const requestOptions: RequestInit = query + ? handleGraphqlRequest({ query, variables, headers: { ...defaultHeaders, ...headers } }) + : { + method, + cache: 'no-store', + headers: { + ...defaultHeaders, + ...headers, // Merge custom headers with default ones (overwriting defaults if needed) + }, + }; + + try { + const response = await fetch(url, requestOptions); + + // Check for non-successful responses (status not in the 2xx range) + if (!response.ok) { + handleHttpError(response); + } + + const data = await response.json(); + + // Handle GraphQL-specific errors + if (data.errors && data.errors.length > 0) { + data.errors.forEach(handleGraphqlError); // Process each error + } + + return data; + } catch (error: any) { + // Handle and log client-side or unexpected errors + throw new ErrorHandler(error.message || 'Unknown error occurred', { + statusCode: error?.options?.statusCode || 500, + }); + } +}; + +/** + * Handle common HTTP errors and throw the appropriate ErrorHandler + * @param response - Fetch API Response object + */ +const handleHttpError = (response: Response) => { + const status = response.status; + + let errorMessage = response.statusText || 'Unknown error'; + + // Specific error messages based on status codes + switch (status) { + case 400: + errorMessage = 'Bad Request'; + break; + case 401: + errorMessage = 'Unauthorized'; + break; + case 403: + errorMessage = 'Forbidden'; + break; + case 404: + errorMessage = 'Resource Not Found'; + break; + case 422: + errorMessage = 'Unprocessable Entity'; + break; + case 500: + errorMessage = 'Internal Server Error'; + break; + case 503: + errorMessage = 'Service Unavailable'; + break; + case 504: + errorMessage = 'Gateway Timeout'; + break; + case 505: + errorMessage = 'HTTP Version Not Supported'; + break; + default: + errorMessage = `Unexpected error: ${status}`; + break; + } + throw new ErrorHandler(errorMessage, { statusCode: status }); +}; + +/** + * Handle GraphQL-specific errors like 'Forbidden access' + * @param error - The error object returned by GraphQL + */ +const handleGraphqlError = (error: any) => { + const errorMessage = error?.message || 'GraphQL error'; + const errorCode = error?.extensions?.code || 400; // Handle extensions (specific to GraphQL) + // Handle known GraphQL error messages + if (errorCode === 'FORBIDDEN') { + throw new ErrorHandler('Forbidden access: You do not have the required permissions.', { + statusCode: 403, + }); + } + + throw new ErrorHandler(errorMessage, { + statusCode: errorCode, + }); +};