Created
April 6, 2023 13:45
-
-
Save zerkalica/732e399d9597786c72a2c3774713e689 to your computer and use it in GitHub Desktop.
GqlApiServiceError
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type ErrorObjectBase = { __typename: string | undefined; message?: string; path?: readonly string[] } | |
export type GqlApiServiceErrorDto = { | |
__typename: undefined; | |
locations?: readonly { line: number; column: number }[]; | |
message?: string; | |
path?: readonly string[]; | |
} | |
type WithNullable<El> = El | undefined | null | false | |
export class GqlApiServiceError<ErrorObject extends ErrorObjectBase = ErrorObjectBase> extends Error { | |
readonly errors: readonly (ErrorObject | GqlApiServiceErrorDto)[]; | |
constructor( | |
errorsRaw: readonly WithNullable<ErrorObject | GqlApiServiceErrorDto>[], | |
readonly cause?: unknown, | |
) { | |
const errors = errorsRaw.filter(<T>(el: T | undefined | null | false): el is T => Boolean(el)); | |
// Не может быть пустого массива с ошибками, но если он передан пустым или все элементы в нем null/false/undefined, | |
// добавляем UNKNOWN тип | |
if (!errors.length) { | |
errors.push({ | |
__typename: undefined, | |
message: 'Mg2ApiError UNKNOWN', | |
} as GqlApiServiceErrorDto); | |
} | |
const first = errors[0]; | |
// @ts-ignore | |
super(first?.message ?? first?.__typename ?? 'Mg2ApiError UNKNOWN', { cause }); | |
this.cause = cause; | |
this.errors = errors; | |
this.name = this.first.__typename ?? 'Mg2ApiError'; | |
} | |
get first() { | |
return this.errors[0]; | |
} | |
filterByType<Name extends (ErrorObject['__typename'] | undefined)>(name: Name) { | |
type MatchedType = Extract<GqlApiServiceErrorDto | ErrorObject, {__typename: Name}>; | |
return this.errors.filter((error): error is MatchedType => error.__typename === name) | |
} | |
/** | |
* В catch может прийти что-то не instanceOf Error, оборачиваем в Error, для упрощения работы с ошибками | |
*/ | |
static normalize(e: unknown) { | |
if (e instanceof Error) return e; | |
let message = 'UNKNOWN'; | |
if (e instanceof Error) message = e.message ?? message; | |
else if (typeof e === 'string') message = e; | |
else if (e && typeof e === 'object') message = JSON.stringify(e); | |
return this.create({ __typename: undefined, message }); | |
} | |
/** | |
* @example | |
* ```ts | |
* class My extends GqlApiServiceError<{__typename: 'test', some: number}> {} | |
* const m = My.create({__typename: 'test', some: 12}); | |
* ``` | |
*/ | |
static create<Instance, Err>( | |
this: { new (errors: Err[], cause?: unknown): Instance }, | |
error: Err, | |
cause?: unknown | |
): Instance { | |
const obj = new this([error], cause) | |
return obj; | |
} | |
/** | |
* Оборачивает в прокси, бросающий ошибку при обращении к ветви объекта данных, | |
* путь к которой опционально есть в path[] каждого объекта ошибки. | |
* | |
* Если могут быть ошибки, релевантные для некоторой части объекта (например сфейлилась только одна кверя из нескольких). | |
* То можно не передавать их вместе с данными явно и не обрабатывать в коде в месте использования. | |
* А просто обернуть в прокси те ветви, которые содержат ошибки, тогда ошибка будет брошена, если явно обратиться к этому полю. | |
* | |
* Можно не фейлить все приложение, а только тот компонент или те вычисления, которые обратились к сбойному полю. | |
* | |
* @example | |
* ```ts | |
* // Например, сервер может вернуть | |
* const res = { | |
* errors: [ { path: ['data', 'mortgage', 'calculation', 'input' ], message: 'some' } ], | |
* data: { | |
* mortgage: { | |
* calculation: { | |
* some: 123, | |
* input: null | |
* } | |
* } | |
* } | |
* } | |
* | |
* const next = GqlApiServiceError.wrap(res.data, res.errors) | |
* next.mortgage.calculation.some === 123 // ошибки не произойдет | |
* next.mortgage.calculation.input // бросится ошибка | |
* ``` | |
* | |
* @param data результат квери или мутации из gql, мутируется | |
*/ | |
static wrap<Data extends Record<string | symbol, unknown>>( | |
data: Data | null | undefined, | |
errorsDto: readonly (WithNullable<ErrorObjectBase | GqlApiServiceErrorDto>)[] | |
): Data { | |
if (! data) data = { } as Data; | |
for (const errorDto of errorsDto) { | |
const error = this.create(errorDto) | |
const path = (error ? error.first?.path : undefined) ?? []; | |
// Если ошибка на первом уровне (без path) - весь объект оборачиваем в прокси, | |
// который кинет ошибку при обращении к любому полю | |
if (! path.length) return proxyfiObject(data, '', error); | |
let ptr = data as Tree; | |
let seg = ''; | |
// Идем вглубь объекта до предпоследнего сегмента в path ошибки, создавая отсутствующие ветви | |
for (let i = 0; i < path.length; i++) { | |
seg = path[i]; | |
if (i === path.length - 1) break; | |
if (! ptr[seg]) ptr[seg] = {} | |
ptr = ptr[seg]; | |
} | |
ptr[seg] = proxyfiObject(ptr[seg], seg, error); | |
} | |
return data; | |
} | |
} | |
type Tree = { | |
[id: string]: Tree; | |
} | |
const proxyfiKey = Symbol('proxyfiObject_key'); | |
/** | |
* Оборачивает в прокси объект, содержащий поле с ошибкой | |
* | |
* При обращении к такому полю, кидает ошибку | |
* @param key имя сбойного поля, если пустое, то при обращении к любому полю объекта будет брошена ошибка | |
*/ | |
function proxyfiObject<Obj extends Record<string | symbol, unknown>>( | |
obj: Obj, | |
key: string, | |
error: Error | |
): Obj { | |
// Если в уже проксированный объект добавляется еще один сбойный ключ, | |
// То не перезатераем прокси, а добавляем пару ключ-ошибка в существующий. | |
const errorMap = obj[proxyfiKey] as Record<string | symbol, Error> | undefined; | |
const errors = errorMap ?? {}; | |
errors[key] = error; | |
// Если value - уже прокси, то не надо оборачивать | |
if (errorMap) return obj; | |
return new Proxy(obj, { | |
get(_t, k) { | |
if (k === proxyfiKey) return errors; | |
const err = errors[k]; | |
if (err) throw err; | |
return obj[k]; | |
}, | |
has(_t, k) { | |
return k in obj; | |
}, | |
ownKeys() { | |
return Object.keys(obj); | |
} | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment