Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use ServiceOperationError and remove context #839

Merged
merged 14 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .boilerplate-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4939fd0f79a2ffcc5c6b5e77aa52d5341257b597
c3a9be564c0db581a0dc6882ce90f80f5067ace3
2 changes: 1 addition & 1 deletion __tests__/Setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @Copyright: Technology Studio
**/

import './Config/LogConfig'
import 'Config/LogConfig'

// Mock your external modules here if needed
// jest
Expand Down
9 changes: 9 additions & 0 deletions __tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
7 changes: 2 additions & 5 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**/

const { pathsToModuleNameMapper } = require('ts-jest')
const { compilerOptions } = require('./tsconfig.json');
const { compilerOptions } = require('./__tests__/tsconfig.json');

const { defaults } = require('jest-config');

Expand All @@ -15,9 +15,6 @@ module.exports = {
testMatch: [
'<rootDir>/__tests__/Tests/**/?(*.)(spec|test).ts'
],
transformIgnorePatterns: [
'/node_modules/(?!@txo).+\\.js$'
],
testPathIgnorePatterns: [
'/node_modules/'
],
Expand All @@ -29,7 +26,7 @@ module.exports = {
],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: './__tests__/tsconfig.json'
tsconfig: '<rootDir>/__tests__/tsconfig.json'
}]
},
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths , { prefix: '<rootDir>/' } ),
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"build:lib": "yarn tsc",
"build:watch": "yarn tsc --watch",
"test": "jest",
"test:watch": "concurrently \"yarn build:watch\" \"jest --watch\"",
"test:watch": "concurrently \"yarn build:watch\" \"yarn test --watch\"",
"coverage": "jest --coverage && open coverage/lcov-report/index.html || xdg-open coverage/lcov-report/index.html",
"compare-boilerplate-version": "./scripts/compare-boilerplate-version.sh",
"lint": "eslint --max-warnings 0 .",
Expand All @@ -40,23 +40,23 @@
},
"dependencies": {
"@txo/hooks-react": "^2.3.21",
"@txo/service-graphql": "^4.4.6",
"@txo/service-prop": "^2.2.18",
"@txo/service-graphql": "^5.0.0",
"@txo/service-prop": "^3.0.1",
"@txo/types": "^1.7.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"type-fest": "^4.26.1"
},
"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",
Expand Down
11 changes: 11 additions & 0 deletions src/Api/VoidError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @Author: Erik Slovak <erik.slovak@technologystudio.sk>
* @Date: 2024-10-25T22:32:09+02:00
* @Copyright: Technology Studio
**/

export class VoidError extends Error {
constructor () {
super('Void validation error')
}
}
40 changes: 16 additions & 24 deletions src/Hooks/UseServiceMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -29,13 +27,13 @@ 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, unknown>): string => (
serviceContext(getName(mutation), variables ?? {})
Expand All @@ -62,10 +60,10 @@ export const useServiceMutation = <
ATTRIBUTES extends Record<string, unknown>,
DATA,
CALL_ATTRIBUTES extends CallAttributes<ATTRIBUTES>,
>(
mutationDocument: TypedDocumentNode<DATA, ATTRIBUTES>,
options?: MutationOptions<DATA, ATTRIBUTES>,
): MutationServiceProp<ATTRIBUTES, DATA, CALL_ATTRIBUTES> => {
> (
mutationDocument: TypedDocumentNode<DATA, ATTRIBUTES>,
options?: MutationOptions<DATA, ATTRIBUTES>,
): MutationServiceProp<ATTRIBUTES, DATA, CALL_ATTRIBUTES> => {
const {
onFieldErrors: defaultOnFieldErrors,
onFieldErrorsDependencyList,
Expand All @@ -74,7 +72,6 @@ export const useServiceMutation = <
options: mutationOptions,
mutateFactory,
} = options ?? {}
const exceptionRef = useRef<ServiceErrorException | null>(null)
const memoizedErrorMap = useMemo(
() => errorMap,
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -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 (
Expand All @@ -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), {
Expand All @@ -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<Typify<MutationResult<DATA>>>(mutation)

Expand Down
62 changes: 24 additions & 38 deletions src/Hooks/UseServiceQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -63,26 +62,16 @@ type QueryOptions<DATA, ATTRIBUTES extends OperationVariables, DATA_PATH extends
dataPath: DATA_PATH,
}

const isServiceErrorListEqual = (a: ServiceError[], b: ServiceError[]): boolean => {
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<string, unknown>,
DATA,
CALL_ATTRIBUTES extends CallAttributes<ATTRIBUTES>,
DATA_PATH extends string
>(
queryDocument: TypedDocumentNode<DATA, ATTRIBUTES>,
options: QueryOptions<DATA, ATTRIBUTES, DATA_PATH>,
): QueryServiceProp<ATTRIBUTES, DATA, Get<DATA, DATA_PATH>, CALL_ATTRIBUTES> => {
> (
queryDocument: TypedDocumentNode<DATA, ATTRIBUTES>,
options: QueryOptions<DATA, ATTRIBUTES, DATA_PATH>,
): QueryServiceProp<ATTRIBUTES, DATA, Get<DATA, DATA_PATH>, CALL_ATTRIBUTES> => {
const {
dataPath,
options: _queryOptions,
Expand All @@ -95,14 +84,14 @@ export const useServiceQuery = <
skip: isSkipped,
})
const query: QueryResult<DATA, ATTRIBUTES> = useQuery<DATA, ATTRIBUTES>(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<Typify<QueryResult<DATA, ATTRIBUTES>>>(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
Expand All @@ -118,32 +107,29 @@ export const useServiceQuery = <
context,
operationName,
})
const exception = new ServiceErrorException({
const exception = new ServiceOperationError({
rostislav-simonik marked this conversation as resolved.
Show resolved Hide resolved
serviceErrorList: errorList,
rostislav-simonik marked this conversation as resolved.
Show resolved Hide resolved
serviceName: operationName,
operationName,
context,
})
return exception
}
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 ((exception != null) && (reportedOperationErrorListRef.current.includes(exception))) {
rostislav-simonik marked this conversation as resolved.
Show resolved Hide resolved
reportError(exception)
reportedOperationErrorListRef.current.push(exception)
}
}, [addServiceErrorException, context, exception, memoizedVariables, queryDocument, removeServiceErrorException])
}, [context, exception, memoizedVariables, queryDocument])

const promiselessRefetch = useCallback((...args: Parameters<typeof memoizedQuery.refetch>) => {
reportedOperationErrorListRef.current = []
asyncToCallback(memoizedQuery.refetch(...args))
rostislav-simonik marked this conversation as resolved.
Show resolved Hide resolved
}, [memoizedQuery])

const fetchMore: QueryResult<DATA>['fetchMore'] = useCallback(async (...args) => {
reportedOperationErrorListRef.current = []
setFetchMoreFetching(true)
return (
await memoizedQuery.fetchMore(...args)
rostislav-simonik marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -153,19 +139,19 @@ export const useServiceQuery = <
context,
operationName,
})
const exception = new ServiceErrorException({
const exception = new ServiceOperationError({
rostislav-simonik marked this conversation as resolved.
Show resolved Hide resolved
serviceErrorList: errorList,
serviceName: operationName,
operationName,
context,
})
addServiceErrorException(exception)
reportError(exception)
throw error
})
.finally(() => {
setFetchMoreFetching(false)
})
)
}, [addServiceErrorException, context, memoizedQuery, queryDocument, setFetchMoreFetching])
}, [context, memoizedQuery, queryDocument])

return useMemo(() => ({
query: memoizedQuery,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading