@saulo.martins/api-client 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ export type ApiErrorResponse = {
2
+ message?: string | {
3
+ message: string;
4
+ code: string;
5
+ };
6
+ statusCode?: number;
7
+ };
8
+ export declare function getApiErrorFromResponse(data: unknown): {
9
+ message: string;
10
+ code?: string;
11
+ };
12
+ export declare function attachCodeToError(err: Error, code: string): Error & {
13
+ code: string;
14
+ };
15
+ export declare function getDisplayMessage(err: unknown, t: (key: string) => string, fallbackKey?: string): string;
@@ -0,0 +1,28 @@
1
+ export function getApiErrorFromResponse(data) {
2
+ if (data == null || typeof data !== 'object' || !('message' in data))
3
+ return { message: '' };
4
+ const msg = data.message;
5
+ if (typeof msg === 'object' &&
6
+ msg !== null &&
7
+ 'message' in msg &&
8
+ 'code' in msg) {
9
+ return { message: msg.message, code: msg.code };
10
+ }
11
+ return { message: typeof msg === 'string' ? msg : '' };
12
+ }
13
+ export function attachCodeToError(err, code) {
14
+ err.code = code;
15
+ return err;
16
+ }
17
+ export function getDisplayMessage(err, t, fallbackKey = 'errors.generic') {
18
+ const code = err instanceof Error ? err.code : undefined;
19
+ if (code) {
20
+ const translated = t(`errors.${code}`);
21
+ if (translated !== `errors.${code}`)
22
+ return translated;
23
+ }
24
+ const message = err instanceof Error ? err.message : '';
25
+ if (message)
26
+ return message;
27
+ return t(fallbackKey);
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getApiErrorFromResponse, attachCodeToError, getDisplayMessage, } from './apiError';
3
+ describe('apiError', () => {
4
+ it('getApiErrorFromResponse extracts message and code', () => {
5
+ const r = getApiErrorFromResponse({
6
+ message: { message: 'Invalid', code: 'INVALID_PACK' },
7
+ });
8
+ expect(r.message).toBe('Invalid');
9
+ expect(r.code).toBe('INVALID_PACK');
10
+ });
11
+ it('attachCodeToError adds code to error', () => {
12
+ const err = new Error('test');
13
+ const out = attachCodeToError(err, 'CODE');
14
+ expect(out.code).toBe('CODE');
15
+ });
16
+ it('getDisplayMessage uses t when code present', () => {
17
+ const err = attachCodeToError(new Error('x'), 'INVALID_PACK');
18
+ const t = (key) => (key === 'errors.INVALID_PACK' ? 'Pacote inválido' : key);
19
+ expect(getDisplayMessage(err, t)).toBe('Pacote inválido');
20
+ });
21
+ });
@@ -0,0 +1,3 @@
1
+ export declare function fetchJson<T = unknown>(url: string, options?: RequestInit & {
2
+ parseResponse?: boolean;
3
+ }): Promise<T>;
@@ -0,0 +1,14 @@
1
+ import { getApiErrorFromResponse, attachCodeToError } from './apiError';
2
+ export async function fetchJson(url, options) {
3
+ const { parseResponse = true, ...init } = options ?? {};
4
+ const res = await fetch(url, init);
5
+ const data = await res.json().catch(() => ({}));
6
+ if (!res.ok) {
7
+ const { message, code } = getApiErrorFromResponse(data);
8
+ const err = new Error(message || `Request failed: ${res.status}`);
9
+ if (code)
10
+ attachCodeToError(err, code);
11
+ throw err;
12
+ }
13
+ return (parseResponse ? data : res);
14
+ }
@@ -0,0 +1,2 @@
1
+ export * from './apiError';
2
+ export * from './fetchJson';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './apiError';
2
+ export * from './fetchJson';
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@saulo.martins/api-client",
3
+ "version": "1.0.1",
4
+ "private": false,
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "test": "vitest run"
10
+ },
11
+ "devDependencies": {
12
+ "typescript": "~5.6.2",
13
+ "vitest": "^2.1.6"
14
+ }
15
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getApiErrorFromResponse,
4
+ attachCodeToError,
5
+ getDisplayMessage,
6
+ } from './apiError';
7
+
8
+ describe('apiError', () => {
9
+ it('getApiErrorFromResponse extracts message and code', () => {
10
+ const r = getApiErrorFromResponse({
11
+ message: { message: 'Invalid', code: 'INVALID_PACK' },
12
+ });
13
+ expect(r.message).toBe('Invalid');
14
+ expect(r.code).toBe('INVALID_PACK');
15
+ });
16
+
17
+ it('attachCodeToError adds code to error', () => {
18
+ const err = new Error('test');
19
+ const out = attachCodeToError(err, 'CODE');
20
+ expect((out as any).code).toBe('CODE');
21
+ });
22
+
23
+ it('getDisplayMessage uses t when code present', () => {
24
+ const err = attachCodeToError(new Error('x'), 'INVALID_PACK');
25
+ const t = (key: string) => (key === 'errors.INVALID_PACK' ? 'Pacote inválido' : key);
26
+ expect(getDisplayMessage(err, t)).toBe('Pacote inválido');
27
+ });
28
+ });
@@ -0,0 +1,46 @@
1
+ export type ApiErrorResponse = {
2
+ message?: string | { message: string; code: string };
3
+ statusCode?: number;
4
+ };
5
+
6
+ export function getApiErrorFromResponse(data: unknown): {
7
+ message: string;
8
+ code?: string;
9
+ } {
10
+ if (data == null || typeof data !== 'object' || !('message' in data))
11
+ return { message: '' };
12
+ const msg = (data as ApiErrorResponse).message;
13
+ if (
14
+ typeof msg === 'object' &&
15
+ msg !== null &&
16
+ 'message' in msg &&
17
+ 'code' in msg
18
+ ) {
19
+ return { message: msg.message, code: msg.code };
20
+ }
21
+ return { message: typeof msg === 'string' ? msg : '' };
22
+ }
23
+
24
+ export function attachCodeToError(
25
+ err: Error,
26
+ code: string,
27
+ ): Error & { code: string } {
28
+ (err as Error & { code?: string }).code = code;
29
+ return err as Error & { code: string };
30
+ }
31
+
32
+ export function getDisplayMessage(
33
+ err: unknown,
34
+ t: (key: string) => string,
35
+ fallbackKey = 'errors.generic',
36
+ ): string {
37
+ const code =
38
+ err instanceof Error ? (err as Error & { code?: string }).code : undefined;
39
+ if (code) {
40
+ const translated = t(`errors.${code}`);
41
+ if (translated !== `errors.${code}`) return translated;
42
+ }
43
+ const message = err instanceof Error ? err.message : '';
44
+ if (message) return message;
45
+ return t(fallbackKey);
46
+ }
@@ -0,0 +1,17 @@
1
+ import { getApiErrorFromResponse, attachCodeToError } from './apiError';
2
+
3
+ export async function fetchJson<T = unknown>(
4
+ url: string,
5
+ options?: RequestInit & { parseResponse?: boolean },
6
+ ): Promise<T> {
7
+ const { parseResponse = true, ...init } = options ?? {};
8
+ const res = await fetch(url, init);
9
+ const data = await res.json().catch(() => ({}));
10
+ if (!res.ok) {
11
+ const { message, code } = getApiErrorFromResponse(data);
12
+ const err = new Error(message || `Request failed: ${res.status}`);
13
+ if (code) attachCodeToError(err, code);
14
+ throw err;
15
+ }
16
+ return (parseResponse ? data : res) as T;
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './apiError';
2
+ export * from './fetchJson';
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "declaration": true
7
+ },
8
+ "include": ["src"]
9
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ },
7
+ });