diff --git a/.boilerplate-version b/.boilerplate-version index e983f039..5ed78356 100644 --- a/.boilerplate-version +++ b/.boilerplate-version @@ -1 +1 @@ -4939fd0f79a2ffcc5c6b5e77aa52d5341257b597 +813c05c22fe6ab819ad616e30ae2c8f86efadb35 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c4c77cf..5e67e514 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,13 +3,9 @@ on: push: branches: - main - - next - - next-major - - alpha - - beta jobs: release: - name: '/' + name: 'Release' uses: technology-studio/github-workflows/.github/workflows/_release.yml@main secrets: inherit diff --git a/.github/workflows/resolve-yarn-lock.yml b/.github/workflows/resolve-yarn-lock.yml index ed662863..cecb6985 100644 --- a/.github/workflows/resolve-yarn-lock.yml +++ b/.github/workflows/resolve-yarn-lock.yml @@ -6,7 +6,7 @@ on: jobs: resolve-yarn-lock-on-comment: - name: '/' + name: 'Resolve yarn.lock' if: contains(github.event.comment.body, '/resolve yarn.lock') uses: technology-studio/github-workflows/.github/workflows/_resolve-yarn-lock.yml@main secrets: inherit diff --git a/.husky/pre-commit b/.husky/pre-commit index 8e90f798..dd191715 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1 @@ -yarn git-hook - +yarn -s lint-staged diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg index f029db69..b2b7c2fb 100755 --- a/.husky/prepare-commit-msg +++ b/.husky/prepare-commit-msg @@ -1,2 +1 @@ yarn commitlint --edit || exec < /dev/tty && yarn txo-cz --hook > /dev/null 2>&1 || true - diff --git a/.vscode/launch.json b/.vscode/launch.json index 5c037145..560584d5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,17 @@ "name": "Debug Jest Tests", "type": "node", "request": "launch", - "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand", "--coverage", "false"], + "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand", "--coverage", "false", "${file}"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Debug Jest Tests with yarn", + "type": "node", + "request": "launch", + "runtimeExecutable": "yarn", + "args": ["test", "--runInBand", "--coverage=false", "${file}"], + "runtimeArgs": ["--inspect-brk"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" }, diff --git a/__tests__/Setup.ts b/__tests__/Setup.ts index 9f905132..e01160fa 100644 --- a/__tests__/Setup.ts +++ b/__tests__/Setup.ts @@ -4,7 +4,7 @@ * @Copyright: Technology Studio **/ -import './Config/LogConfig' +import 'Config/LogConfig' // Mock your external modules here if needed // jest diff --git a/__tests__/tsconfig.json b/__tests__/tsconfig.json index 34b4c6e1..588d8211 100644 --- a/__tests__/tsconfig.json +++ b/__tests__/tsconfig.json @@ -1,6 +1,15 @@ { "extends": "../tsconfig-base.json", "compilerOptions": { + "rootDir": "../", + "baseUrl": "../", + "paths": { + "Config/*": ["./__tests__/Config/*"], + "Data/*": ["./__tests__/Data/*"], + "Utils/*": ["./__tests__/Utils/*"], + "src": ["./src"], + "src/*": ["./src/*"] + } }, "include": [ "./**/*.ts" diff --git a/jest.config.js b/jest.config.js index 61eed57f..14195b57 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,19 +5,18 @@ **/ const { pathsToModuleNameMapper } = require('ts-jest') -const { compilerOptions } = require('./tsconfig.json'); +const { compilerOptions } = require('./__tests__/tsconfig.json'); const { defaults } = require('jest-config'); module.exports = { preset: 'ts-jest', + cache: true, + cacheDirectory: '/node_modules/.cache/jest', testEnvironment: 'node', testMatch: [ '/__tests__/Tests/**/?(*.)(spec|test).ts' ], - transformIgnorePatterns: [ - '/node_modules/(?!@txo).+\\.js$' - ], testPathIgnorePatterns: [ '/node_modules/' ], @@ -29,7 +28,7 @@ module.exports = { ], transform: { '^.+\\.tsx?$': ['ts-jest', { - tsconfig: './__tests__/tsconfig.json' + tsconfig: '/__tests__/tsconfig.json' }] }, moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths , { prefix: '/' } ), diff --git a/package.json b/package.json index 49bdf80d..e9a630c7 100644 --- a/package.json +++ b/package.json @@ -16,32 +16,31 @@ "license": "UNLICENSED", "private": false, "scripts": { - "build": "yarn build:clean && yarn build:lib", "build:clean": "yarn rimraf lib", "build:lib": "yarn tsc", "build:watch": "yarn tsc --watch", - "test": "jest", - "test:watch": "concurrently \"yarn build:watch\" \"jest --watch\"", - "coverage": "jest --coverage && open coverage/lcov-report/index.html || xdg-open coverage/lcov-report/index.html", + "build": "yarn build:clean && yarn build:lib", "compare-boilerplate-version": "./scripts/compare-boilerplate-version.sh", + "coverage": "jest --coverage && open coverage/lcov-report/index.html || xdg-open coverage/lcov-report/index.html", + "lint:ci": "yarn lint", + "lint:fix": "eslint . --fix", "lint": "eslint --max-warnings 0 .", - "fixcode": "eslint . --fix", - "git-hook": "yarn -s lint-staged", "prepare": "husky && yarn build", "print-txo-packages": "yarn list 2> /dev/null|grep @txo|sed 's/.*\\(@txo[^@]*\\)@^*\\([^ ]*\\).*/\\1@\\2/g'|sort|uniq", "sanity": "yarn lint:ci && yarn build && tsc --noEmit && yarn test --coverage && yarn compare-boilerplate-version && echo 'success'", "semantic-release": "semantic-release", - "update-boilerplate-version": "./scripts/update-boilerplate-version.sh", - "lint:ci": "yarn lint", - "type-check": "tsc --noEmit" + "test:watch": "concurrently \"yarn build:watch\" \"yarn test --watch\"", + "test": "jest", + "type-check": "tsc --noEmit", + "update-boilerplate-version": "./scripts/update-boilerplate-version.sh" }, "engines": { "node": ">=18.0.0" }, "dependencies": { "@txo/hooks-react": "^2.3.21", - "@txo/service-graphql": "^4.4.6", - "@txo/service-prop": "^2.2.20", + "@txo/service-graphql": "^5.0.0", + "@txo/service-prop": "^3.0.2", "@txo/types": "^1.7.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", @@ -49,14 +48,14 @@ }, "peerDependencies": { "@apollo/client": "^3.11.8", - "@txo-peer-dep/service-error-handler-react": "^1.2.29", + "@txo-peer-dep/error-handler": "^3.0.0", "@txo-peer-dep/service-graphql": "^3.3.3", "graphql": "^16.9.0" }, "devDependencies": { "@apollo/client": "^3.11.8", + "@txo-peer-dep/error-handler": "^3.0.0", "@txo-peer-dep/log": "^4.0.4", - "@txo-peer-dep/service-error-handler-react": "^1.2.29", "@txo-peer-dep/service-graphql": "^3.3.3", "@txo/commitlint": "^1.0.19", "@txo/log-console": "^3.0.0", diff --git a/src/Api/VoidError.ts b/src/Api/VoidError.ts new file mode 100644 index 00000000..a38130fe --- /dev/null +++ b/src/Api/VoidError.ts @@ -0,0 +1,11 @@ +/** + * @Author: Erik Slovak + * @Date: 2024-10-25T22:32:09+02:00 + * @Copyright: Technology Studio +**/ + +export class VoidError extends Error { + constructor () { + super('Void validation error') + } +} diff --git a/src/Hooks/UseServiceMutation.ts b/src/Hooks/UseServiceMutation.ts index 344ef848..a6003e59 100644 --- a/src/Hooks/UseServiceMutation.ts +++ b/src/Hooks/UseServiceMutation.ts @@ -7,14 +7,12 @@ import type { DependencyList } from 'react' import { useCallback, - useContext, useMemo, - useRef, } from 'react' import type { CallAttributes, ServiceProp, - ServiceErrorException, + ServiceOperationError, } from '@txo/service-prop' import { useMemoObject } from '@txo/hooks-react' import type { Typify } from '@txo/types' @@ -29,20 +27,20 @@ import type { import { useMutation, } from '@apollo/client' -import { ErrorHandlerContext } from '@txo-peer-dep/service-error-handler-react' import { operationPromiseProcessor } from '@txo/service-graphql' import { serviceContext } from '../Api/ContextHelper' import { getName } from '../Api/OperationHelper' import type { ErrorMap } from '../Model/Types' import { applyErrorMap } from '../Api/ErrorMapHelper' +import { VoidError } from '../Api/VoidError' const calculateContext = (mutation: DocumentNode, variables?: Record): string => ( serviceContext(getName(mutation), variables ?? {}) ) export type MutationServiceProp> = - Omit>, 'clear' | 'options' | 'clearException' | 'exception'> + Omit>, 'options' | 'error'> & { mutation: MutationResult, } @@ -62,10 +60,10 @@ export const useServiceMutation = < ATTRIBUTES extends Record, DATA, CALL_ATTRIBUTES extends CallAttributes, - >( - mutationDocument: TypedDocumentNode, - options?: MutationOptions, - ): MutationServiceProp => { +> ( + mutationDocument: TypedDocumentNode, + options?: MutationOptions, +): MutationServiceProp => { const { onFieldErrors: defaultOnFieldErrors, onFieldErrorsDependencyList, @@ -74,7 +72,6 @@ export const useServiceMutation = < options: mutationOptions, mutateFactory, } = options ?? {} - const exceptionRef = useRef(null) const memoizedErrorMap = useMemo( () => errorMap, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -86,13 +83,9 @@ export const useServiceMutation = < onFieldErrorsDependencyList ?? [], ) const [mutate, mutation] = useMutation< - DATA, - ATTRIBUTES + DATA, + ATTRIBUTES >(mutationDocument, mutationOptions) - const { - addServiceErrorException, - removeServiceErrorException, - } = useContext(ErrorHandlerContext) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const memoizedOptions = useMemoObject(mutationOptions!) const wrappedCall = useCallback(async ( @@ -102,8 +95,6 @@ export const useServiceMutation = < const attributes = { variables, mutation: mutationDocument, ...memoizedOptions } const onFieldErrors = callAttributes?.onFieldErrors ?? memoizedDefaultOnFieldErrors const context = calculateContext(mutationDocument, variables) - ;(exceptionRef.current != null) && removeServiceErrorException(context) - exceptionRef.current = null const operationName = getName(mutationDocument) const mutateWithErrorProcessor: typeof mutate = async (options) => ( await operationPromiseProcessor(mutate(options), { @@ -116,19 +107,20 @@ export const useServiceMutation = < operationName, context, }) - .catch(async (serviceErrorException: ServiceErrorException) => { + .catch(async (serviceOperationError: ServiceOperationError) => { if (memoizedErrorMap != null) { - serviceErrorException.serviceErrorList = applyErrorMap( - serviceErrorException.serviceErrorList, + serviceOperationError.serviceErrorList = applyErrorMap( + serviceOperationError.serviceErrorList, memoizedErrorMap, onFieldErrors, ) } - addServiceErrorException(serviceErrorException) - exceptionRef.current = serviceErrorException - throw serviceErrorException + if (serviceOperationError.serviceErrorList.length === 0) { + throw new VoidError() + } + throw serviceOperationError }) - }, [mutationDocument, memoizedOptions, memoizedDefaultOnFieldErrors, removeServiceErrorException, mutateFactory, mutate, memoizedErrorMap, addServiceErrorException]) + }, [mutationDocument, memoizedOptions, memoizedDefaultOnFieldErrors, mutateFactory, mutate, memoizedErrorMap]) const memoizedMutation = useMemoObject>>(mutation) diff --git a/src/Hooks/UseServiceQuery.ts b/src/Hooks/UseServiceQuery.ts index f0a063db..08757ee1 100644 --- a/src/Hooks/UseServiceQuery.ts +++ b/src/Hooks/UseServiceQuery.ts @@ -27,17 +27,16 @@ import get from 'lodash.get' import type { Get } from 'type-fest' import type { CallAttributes, - ServiceError, ServiceProp, } from '@txo/service-prop' import { - ServiceErrorException, + ServiceOperationError, } from '@txo/service-prop' import { configManager } from '@txo-peer-dep/service-graphql' import { useMemoObject, } from '@txo/hooks-react' -import { ErrorHandlerContext } from '@txo-peer-dep/service-error-handler-react' +import { reportError } from '@txo-peer-dep/error-handler' import type { Typify } from '@txo/types' import { serviceContext } from '../Api/ContextHelper' @@ -50,7 +49,7 @@ const calculateContext = (query: DocumentNode, variables: Record> = - Omit, 'call' | 'clear' | 'options' | 'clearException'> + Omit, 'call' | 'options'> & { query: QueryResult, promiselessRefetch: (variables?: Partial) => void, @@ -63,26 +62,16 @@ type QueryOptions { - if (a.length !== b.length) { - return false - } - if (a.every((error, index) => (b[index].key === error.key) && (b[index].message === error.message))) { - return true - } - return false -} - // TODO: find a better way to parse type of dataPath (from attribute) export const useServiceQuery = < ATTRIBUTES extends Record, DATA, CALL_ATTRIBUTES extends CallAttributes, DATA_PATH extends string ->( - queryDocument: TypedDocumentNode, - options: QueryOptions, - ): QueryServiceProp, CALL_ATTRIBUTES> => { +> ( + queryDocument: TypedDocumentNode, + options: QueryOptions, +): QueryServiceProp, CALL_ATTRIBUTES> => { const { dataPath, options: _queryOptions, @@ -95,14 +84,14 @@ export const useServiceQuery = < skip: isSkipped, }) const query: QueryResult = useQuery(queryDocument, queryOptions) - const shownExceptionListRef = useRef<(ServiceErrorException)[]>([]) - const { - addServiceErrorException, - removeServiceErrorException, - } = useContext(ErrorHandlerContext) + const reportedOperationErrorListRef = useRef<(ServiceOperationError)[]>([]) const [fetchMoreFetching, setFetchMoreFetching] = useState(false) const memoizedVariables = useMemoObject(queryOptions?.variables) const memoizedQuery = useMemoObject>>(query) + useMemo(() => { + reportedOperationErrorListRef.current = [] + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [memoizedQuery, queryDocument]) const recentData = useRef(memoizedQuery.data) if (!isSkipped) { recentData.current = memoizedQuery.data @@ -111,39 +100,36 @@ export const useServiceQuery = < const context = useMemo(() => ( calculateContext(queryDocument, memoizedVariables) ), [queryDocument, memoizedVariables]) - const exception = useMemo(() => { + const error = useMemo(() => { const operationName = getName(queryDocument) if (memoizedQuery.error != null) { - const errorList = configManager.config.errorResponseTranslator(memoizedQuery.error, { + const serviceErrorList = configManager.config.errorResponseTranslator(memoizedQuery.error, { context, operationName, }) - const exception = new ServiceErrorException({ - serviceErrorList: errorList, - serviceName: operationName, + const serviceOperationError = new ServiceOperationError({ + serviceErrorList, + operationName, context, }) - return exception + return serviceOperationError } return null - }, [context, memoizedQuery, queryDocument]) + }, [context, memoizedQuery.error, queryDocument]) useLayoutEffect(() => { - if ((exception != null) && (shownExceptionListRef.current.find(shownException => ( - isServiceErrorListEqual(shownException.serviceErrorList, exception.serviceErrorList) - )) == null)) { - addServiceErrorException(exception) - shownExceptionListRef.current.push(exception) - } - return () => { - (exception != null) && removeServiceErrorException(context) + if ((error != null) && !reportedOperationErrorListRef.current.includes(error)) { + reportError(error) + reportedOperationErrorListRef.current.push(error) } - }, [addServiceErrorException, context, exception, memoizedVariables, queryDocument, removeServiceErrorException]) + }, [context, error, memoizedVariables, queryDocument]) const promiselessRefetch = useCallback((...args: Parameters) => { + reportedOperationErrorListRef.current = [] asyncToCallback(memoizedQuery.refetch(...args)) }, [memoizedQuery]) const fetchMore: QueryResult['fetchMore'] = useCallback(async (...args) => { + reportedOperationErrorListRef.current = [] setFetchMoreFetching(true) return ( await memoizedQuery.fetchMore(...args) @@ -153,19 +139,18 @@ export const useServiceQuery = < context, operationName, }) - const exception = new ServiceErrorException({ + const serviceOperationError = new ServiceOperationError({ serviceErrorList: errorList, - serviceName: operationName, + operationName, context, }) - addServiceErrorException(exception) - throw error + throw serviceOperationError }) .finally(() => { setFetchMoreFetching(false) }) ) - }, [addServiceErrorException, context, memoizedQuery, queryDocument, setFetchMoreFetching]) + }, [context, memoizedQuery, queryDocument]) return useMemo(() => ({ query: memoizedQuery, @@ -175,6 +160,6 @@ export const useServiceQuery = < fetchMoreFetching, promiselessRefetch, fetchMore, - exception, - }), [memoizedQuery, data, dataPath, fetchMoreFetching, promiselessRefetch, fetchMore, exception]) + error, + }), [memoizedQuery, data, dataPath, fetchMoreFetching, promiselessRefetch, fetchMore, error]) } diff --git a/src/index.ts b/src/index.ts index 2dccd34a..6bcc44e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export { getName } from './Api/OperationHelper' export * from './Api/ErrorMapHelper' export * from './Api/ObservableContext' export * from './Api/PromiseHelper' +export * from './Api/VoidError' export * from './Hooks/UseServiceMutation' export { useServiceQuery, diff --git a/yarn.lock b/yarn.lock index f2195eb5..2f44de9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1366,6 +1366,13 @@ "@tufjs/canonical-json" "2.0.0" minimatch "^9.0.3" +"@txo-peer-dep/error-handler@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@txo-peer-dep/error-handler/-/error-handler-3.0.0.tgz#2934a962557ccb43e8a25d5c2cb0f46fbc602847" + integrity sha512-m+FGNtkn5J0ZsTLJ32RMej+VgxVJWaWvROhyPrhfr4gK4rHimMG4LYaIu2lgFCYm+JPl2KG+VwlRdilZzEPgKA== + dependencies: + "@txo/log" "^2.0.16" + "@txo-peer-dep/log@^4.0.4": version "4.0.4" resolved "https://registry.yarnpkg.com/@txo-peer-dep/log/-/log-4.0.4.tgz#73611601423b0ec5c134964419bd642a61bd1b56" @@ -1373,14 +1380,6 @@ dependencies: "@txo/config-manager" "^3.2.0" -"@txo-peer-dep/service-error-handler-react@^1.2.29": - version "1.2.29" - resolved "https://registry.yarnpkg.com/@txo-peer-dep/service-error-handler-react/-/service-error-handler-react-1.2.29.tgz#406e2323baa1c57b64c4d919764d36e00b51a2c3" - integrity sha512-XXZdJp3Y0sNVdY5lgK98V1nyyN3KRjS0LFuwY923Ri3ixWO0nEb8k7ycg1Oc/xlNaN1JXPbIUa3ttma28NEdGw== - dependencies: - "@txo/log" "^2.0.16" - "@txo/service-prop" "^2.2.18" - "@txo-peer-dep/service-graphql@^3.3.3": version "3.3.3" resolved "https://registry.yarnpkg.com/@txo-peer-dep/service-graphql/-/service-graphql-3.3.3.tgz#ee602845001a28af0260afc1e5454a75deb33f48" @@ -1438,21 +1437,21 @@ semantic-release "^23.0.8" semantic-release-slack-bot "^4.0.2" -"@txo/service-graphql@^4.4.6": - version "4.4.6" - resolved "https://registry.yarnpkg.com/@txo/service-graphql/-/service-graphql-4.4.6.tgz#babb31c0a25256e0a8904007ba8c7d28fb4f1fca" - integrity sha512-QdXQ9TxRPTfZ3D9Mr4XWiVCAbPy1+diwRsWEcoAYt4GJWRv+qqZkLQFDLF7KZL0lZJlOmbFEycEq6zi/ul9uCA== +"@txo/service-graphql@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@txo/service-graphql/-/service-graphql-5.0.0.tgz#5995fa28aae290b5c6f75296b00bc2cddb4884f1" + integrity sha512-qbs5GZdwfdfCkK15pMu2c4YMwBUoEzGE+qUscwIpZJVNSYzddpM2SiFrUSvWiZwouVu6rtlWNgDuO78RboeLHg== dependencies: "@txo/log" "^2.0.16" - "@txo/service-prop" "^2.2.17" + "@txo/service-prop" "^3.0.0" lodash.get "^4.4.2" -"@txo/service-prop@^2.2.17", "@txo/service-prop@^2.2.18", "@txo/service-prop@^2.2.20": - version "2.2.20" - resolved "https://registry.yarnpkg.com/@txo/service-prop/-/service-prop-2.2.20.tgz#a7d3f5e237e19f80e269c4f5bd92b9d08a9b6d4f" - integrity sha512-LjTFX/iBiFvG2yuJm/Kk3Qsp6DNWqVNPeMfaaTe80sbFS/a4n5uBhrRVG4/oqe9NJg4O+8HllNKcqJYxwolAPw== +"@txo/service-prop@^3.0.0", "@txo/service-prop@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@txo/service-prop/-/service-prop-3.0.2.tgz#5f64a473108cd23e92ce3367cc6a8723a875680a" + integrity sha512-CgXFpL7h85vMXWZPKwGsY6zey78/ZOyjpN/SiD52r1S8gZt/TzvfBOrSXQ8yk8RxvxwX4hPtBe2UwJHp9sw8bQ== dependencies: - "@txo/functional" "^4.6.19" + "@txo/types" "^1.7.0" "@txo/tsconfig@^1.1.1": version "1.1.1"