Skip to content

Instantly share code, notes, and snippets.

@zerkalica
Created April 6, 2023 13:45
Show Gist options
  • Save zerkalica/732e399d9597786c72a2c3774713e689 to your computer and use it in GitHub Desktop.
Save zerkalica/732e399d9597786c72a2c3774713e689 to your computer and use it in GitHub Desktop.
GqlApiServiceError
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