@godxjp/sdk 0.3.0 → 0.4.0
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.
- package/README.md +24 -2
- package/dist/laravel/index.d.ts +32 -4
- package/dist/laravel/index.js +21 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,11 +99,20 @@ export const apiClient = createClient({
|
|
|
99
99
|
// On a 401, before `signInUrl`: resolve `true` and the request is sent once
|
|
100
100
|
// more; resolve `false` and the 401 takes its usual course. Never more than
|
|
101
101
|
// one retry per request, and 401s that arrive together share one call.
|
|
102
|
-
|
|
102
|
+
// `ctx` is the request that was answered 401 — method, URL and the headers
|
|
103
|
+
// it was sent with — so a login that failed is not mistaken for an expired
|
|
104
|
+
// token.
|
|
105
|
+
onUnauthorized: ({ headers }) =>
|
|
106
|
+
headers.Authorization === undefined
|
|
107
|
+
? Promise.resolve(false)
|
|
108
|
+
: tokens.refresh(),
|
|
109
|
+
// A request that never reached the server: `ApiError` with status 0 by
|
|
110
|
+
// default ("wrap"); "throw" rethrows fetch's own `TypeError`.
|
|
111
|
+
networkErrors: "throw",
|
|
103
112
|
});
|
|
104
113
|
```
|
|
105
114
|
|
|
106
|
-
A request body is always JSON text or `FormData`, never a stream, so it can be sent a second time. A request cancelled while the token was refreshed stays an `AbortError
|
|
115
|
+
A request body is always JSON text or `FormData`, never a stream, so it can be sent a second time. A request cancelled while the token was refreshed stays an `AbortError`, whatever `networkErrors` says.
|
|
107
116
|
|
|
108
117
|
### Downloading a file
|
|
109
118
|
|
|
@@ -129,10 +138,23 @@ createClient({
|
|
|
129
138
|
});
|
|
130
139
|
```
|
|
131
140
|
|
|
141
|
+
Everything else the server sent is on `ApiError.body` — the parsed JSON, or `undefined` when there was none, it was not JSON, or the server was never reached. Read an `error` code, a `message` that is not a string, or every message per field from there:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
if (error instanceof ApiError) {
|
|
145
|
+
const { error: code, errors } = (error.body ?? {}) as {
|
|
146
|
+
error?: string;
|
|
147
|
+
errors?: Record<string, string[]>;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
132
152
|
An unreachable server (`status: 0`) keeps its built-in text, `Could not reach the server. Check your connection and try again.`, unless `messages(0)` answers one.
|
|
133
153
|
|
|
134
154
|
**Changed in 0.3.0.** Before, a 419 always carried `Your session has expired. Reload the page and try again.` and a response without a message fell back to `statusText`. Both are now the server's message or empty. To keep the old 419 text, pass `messages: (status) => status === 419 ? "Your session has expired. Reload the page and try again." : undefined`.
|
|
135
155
|
|
|
156
|
+
**Changed in 0.4.0.** Additions only; a client configured for 0.3.0 sends and throws exactly what it did. `ApiError` gains `body` (also on a `download()` failure). `onUnauthorized` now receives the request that was answered 401 (`{ method, url, headers }`); a function that takes no argument still works. `networkErrors: "throw"` rethrows `fetch`'s own error instead of `ApiError(0)` — including a response body that breaks off mid-way.
|
|
157
|
+
|
|
136
158
|
## `./realtime`
|
|
137
159
|
|
|
138
160
|
```ts
|
package/dist/laravel/index.d.ts
CHANGED
|
@@ -13,7 +13,14 @@ declare class ApiError extends Error {
|
|
|
13
13
|
readonly status: number;
|
|
14
14
|
/** Laravel's `{errors: {field: string[]}}`, first message per key; `{}` when absent. */
|
|
15
15
|
readonly errors: Record<string, string>;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The error response's body, parsed as JSON — everything `message` and
|
|
18
|
+
* `errors` leave out: an `error` code, a `message` that is not a string,
|
|
19
|
+
* every message per field. `undefined` when there was no body, it was not
|
|
20
|
+
* JSON, or the server was never reached.
|
|
21
|
+
*/
|
|
22
|
+
readonly body: unknown;
|
|
23
|
+
constructor(status: number, message: string, errors?: Record<string, string>, body?: unknown);
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -57,6 +64,14 @@ type DownloadResult = {
|
|
|
57
64
|
headers: Headers;
|
|
58
65
|
status: number;
|
|
59
66
|
};
|
|
67
|
+
/** The request a 401 answered, as `onUnauthorized` receives it. */
|
|
68
|
+
type UnauthorizedContext = {
|
|
69
|
+
method: string;
|
|
70
|
+
/** The full URL, `baseUrl` and query string included. */
|
|
71
|
+
url: string;
|
|
72
|
+
/** Exactly the headers the request was sent with. */
|
|
73
|
+
headers: Record<string, string>;
|
|
74
|
+
};
|
|
60
75
|
type ClientConfig = {
|
|
61
76
|
/** Prefixed to every path, e.g. `/api/app/v1`. */
|
|
62
77
|
baseUrl: string;
|
|
@@ -81,9 +96,22 @@ type ClientConfig = {
|
|
|
81
96
|
* Called on a 401 before `signInUrl`. Resolve `true` (a token was refreshed,
|
|
82
97
|
* say) and the request is sent once more, headers read afresh; resolve
|
|
83
98
|
* `false` and the 401 takes its usual course. A request is retried at most
|
|
84
|
-
* once, and 401s that arrive together share one call
|
|
99
|
+
* once, and 401s that arrive together share one call — made with the
|
|
100
|
+
* context of the request that started it.
|
|
101
|
+
*
|
|
102
|
+
* `ctx` describes the request that was answered 401, with the headers it
|
|
103
|
+
* was actually sent with — so a host can refresh only when that request
|
|
104
|
+
* carried a token.
|
|
105
|
+
*/
|
|
106
|
+
onUnauthorized?: (ctx: UnauthorizedContext) => Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* What a request that never reached the server throws. `wrap` (the
|
|
109
|
+
* default) turns it into `ApiError` with status `0`; `throw` rethrows what
|
|
110
|
+
* `fetch` threw (a `TypeError`), for a host that tells a network failure
|
|
111
|
+
* from an HTTP one by exactly that. A cancelled request is an `AbortError`
|
|
112
|
+
* either way.
|
|
85
113
|
*/
|
|
86
|
-
|
|
114
|
+
networkErrors?: "wrap" | "throw";
|
|
87
115
|
/**
|
|
88
116
|
* The message for an `ApiError` whose response carried none — the client
|
|
89
117
|
* has no language of its own. Unset, or answering `undefined`, leaves
|
|
@@ -115,4 +143,4 @@ declare function queryString(query: ApiQuery | undefined): string;
|
|
|
115
143
|
declare function flattenErrors(errors: unknown): Record<string, string>;
|
|
116
144
|
declare function createClient(config: ClientConfig): ApiClient;
|
|
117
145
|
|
|
118
|
-
export { type ApiClient, ApiError, type ApiQuery, type ClientConfig, type DownloadOptions, type DownloadResult, type GetOptions, type WriteOptions, createClient, flattenErrors, queryString };
|
|
146
|
+
export { type ApiClient, ApiError, type ApiQuery, type ClientConfig, type DownloadOptions, type DownloadResult, type GetOptions, type UnauthorizedContext, type WriteOptions, createClient, flattenErrors, queryString };
|
package/dist/laravel/index.js
CHANGED
|
@@ -3,11 +3,19 @@ var ApiError = class extends Error {
|
|
|
3
3
|
status;
|
|
4
4
|
/** Laravel's `{errors: {field: string[]}}`, first message per key; `{}` when absent. */
|
|
5
5
|
errors;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The error response's body, parsed as JSON — everything `message` and
|
|
8
|
+
* `errors` leave out: an `error` code, a `message` that is not a string,
|
|
9
|
+
* every message per field. `undefined` when there was no body, it was not
|
|
10
|
+
* JSON, or the server was never reached.
|
|
11
|
+
*/
|
|
12
|
+
body;
|
|
13
|
+
constructor(status, message, errors = {}, body) {
|
|
7
14
|
super(message);
|
|
8
15
|
this.name = "ApiError";
|
|
9
16
|
this.status = status;
|
|
10
17
|
this.errors = errors;
|
|
18
|
+
this.body = body;
|
|
11
19
|
}
|
|
12
20
|
};
|
|
13
21
|
|
|
@@ -112,7 +120,10 @@ function send(url, init, onUploadProgress) {
|
|
|
112
120
|
xhr.send(init.body ?? null);
|
|
113
121
|
});
|
|
114
122
|
}
|
|
115
|
-
function unreachable(config) {
|
|
123
|
+
function unreachable(config, error) {
|
|
124
|
+
if (config.networkErrors === "throw") {
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
116
127
|
return new ApiError(0, config.messages?.(0) ?? UNREACHABLE);
|
|
117
128
|
}
|
|
118
129
|
async function read(response, as, config, signal) {
|
|
@@ -122,7 +133,7 @@ async function read(response, as, config, signal) {
|
|
|
122
133
|
if (signal?.aborted) {
|
|
123
134
|
throw error;
|
|
124
135
|
}
|
|
125
|
-
throw unreachable(config);
|
|
136
|
+
throw unreachable(config, error);
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
139
|
async function exchange({ config, unauthorized }, method, path, body, { signal, query, onUploadProgress }, retried = false) {
|
|
@@ -142,10 +153,11 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
|
|
|
142
153
|
payload = JSON.stringify(body);
|
|
143
154
|
}
|
|
144
155
|
Object.assign(headers, await config.headers?.());
|
|
156
|
+
const url = `${config.baseUrl}${path}${queryString(query)}`;
|
|
145
157
|
let response;
|
|
146
158
|
try {
|
|
147
159
|
response = await send(
|
|
148
|
-
|
|
160
|
+
url,
|
|
149
161
|
{
|
|
150
162
|
method,
|
|
151
163
|
headers,
|
|
@@ -159,12 +171,12 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
|
|
|
159
171
|
if (signal?.aborted) {
|
|
160
172
|
throw error;
|
|
161
173
|
}
|
|
162
|
-
throw unreachable(config);
|
|
174
|
+
throw unreachable(config, error);
|
|
163
175
|
}
|
|
164
176
|
if (response.status >= 200 && response.status < 300) {
|
|
165
177
|
return response;
|
|
166
178
|
}
|
|
167
|
-
if (response.status === 401 && !retried && config.onUnauthorized !== void 0 && await unauthorized()) {
|
|
179
|
+
if (response.status === 401 && !retried && config.onUnauthorized !== void 0 && await unauthorized({ method, url, headers: { ...headers } })) {
|
|
168
180
|
return exchange(
|
|
169
181
|
{ config, unauthorized },
|
|
170
182
|
method,
|
|
@@ -187,7 +199,8 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
|
|
|
187
199
|
throw new ApiError(
|
|
188
200
|
response.status,
|
|
189
201
|
typeof bag.message === "string" && bag.message !== "" ? bag.message : config.messages?.(response.status) ?? "",
|
|
190
|
-
flattenErrors(bag.errors)
|
|
202
|
+
flattenErrors(bag.errors),
|
|
203
|
+
json
|
|
191
204
|
);
|
|
192
205
|
}
|
|
193
206
|
async function request(client, method, path, body, options) {
|
|
@@ -206,7 +219,7 @@ function createClient(config) {
|
|
|
206
219
|
let refreshing;
|
|
207
220
|
const client = {
|
|
208
221
|
config,
|
|
209
|
-
unauthorized: () => refreshing ??= (config.onUnauthorized?.() ?? Promise.resolve(false)).finally(() => {
|
|
222
|
+
unauthorized: (ctx) => refreshing ??= (config.onUnauthorized?.(ctx) ?? Promise.resolve(false)).finally(() => {
|
|
210
223
|
refreshing = void 0;
|
|
211
224
|
})
|
|
212
225
|
};
|