@nxgt/datasource-rest 1.0.2 → 2.0.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 +341 -9
- package/dist/datasource/rest-datasource.d.ts +70 -0
- package/dist/datasource/rest-datasource.d.ts.map +1 -0
- package/dist/errors/data-source-error.d.ts +32 -0
- package/dist/errors/data-source-error.d.ts.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -94
- package/dist/index.js.map +6 -8
- package/dist/pagination/relay.d.ts +10 -0
- package/dist/pagination/relay.d.ts.map +1 -0
- package/dist/pagination/types.d.ts +25 -0
- package/dist/pagination/types.d.ts.map +1 -0
- package/package.json +11 -10
- package/dist/helpers/error-handling.d.ts +0 -3
- package/dist/helpers/error-handling.d.ts.map +0 -1
- package/dist/helpers/index.d.ts +0 -3
- package/dist/helpers/index.d.ts.map +0 -1
- package/dist/helpers/pagination.utils.d.ts +0 -5
- package/dist/helpers/pagination.utils.d.ts.map +0 -1
- package/dist/middlewares/auth.middleware.d.ts +0 -7
- package/dist/middlewares/auth.middleware.d.ts.map +0 -1
- package/dist/middlewares/cache.middleware.d.ts +0 -7
- package/dist/middlewares/cache.middleware.d.ts.map +0 -1
- package/dist/middlewares/index.d.ts +0 -3
- package/dist/middlewares/index.d.ts.map +0 -1
- package/dist/rest-datasource.d.ts +0 -21
- package/dist/rest-datasource.d.ts.map +0 -1
- package/dist/types.d.ts +0 -39
- package/dist/types.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,18 +1,350 @@
|
|
|
1
1
|
# @nxgt/datasource-rest
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A REST service, called from a GraphQL resolver or any server code, through
|
|
4
|
+
the client [`@nxgt/openapi-httpyz`](https://www.npmjs.com/package/@nxgt/openapi-httpyz)
|
|
5
|
+
binds to its spec. On top of that client it adds three things:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
- it forwards the caller's token;
|
|
8
|
+
- it caches reads across the datasources of every request;
|
|
9
|
+
- it throws one `DataSourceError`, with a code a GraphQL server reports,
|
|
10
|
+
whenever a call fails.
|
|
9
11
|
|
|
10
12
|
## Install
|
|
11
13
|
|
|
12
14
|
```bash
|
|
13
|
-
bun add @nxgt/datasource-rest
|
|
15
|
+
bun add @nxgt/datasource-rest @nxgt/httpyz @nxgt/openapi-httpyz zod
|
|
16
|
+
bun add -d @nxgt/openapi-codegen typescript
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
`@nxgt/httpyz` and `@nxgt/openapi-httpyz` are required peers. The generated
|
|
20
|
+
`operations.ts` imports `zod`, so the app needs it too. TypeScript is a peer,
|
|
21
|
+
`^6.0.3`, as in every `@nxgt/*` package. `@nxgt/openapi-codegen` generates
|
|
22
|
+
the spec's files, with `nxgt-openapi generate`.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Extend the datasource with the calls your resolvers make, and make one per
|
|
27
|
+
request:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { RESTDataSource } from '@nxgt/datasource-rest';
|
|
31
|
+
import { operations } from './generated/bookmarks/operations.js';
|
|
32
|
+
import type { ClientOperations } from './generated/bookmarks/types.js';
|
|
33
|
+
|
|
34
|
+
export class Bookmarks extends RESTDataSource<ClientOperations> {
|
|
35
|
+
bookmark(id: string) {
|
|
36
|
+
return this.data(this.get('/bookmarks/{id}', { param: { id } }));
|
|
37
|
+
}
|
|
38
|
+
search(q: string) {
|
|
39
|
+
return this.data(this.query('/bookmarks', { json: { q } }));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Per request, in the GraphQL context:
|
|
44
|
+
const bookmarks = new Bookmarks({
|
|
45
|
+
baseUrl: 'https://bookmarks.example.com',
|
|
46
|
+
operations,
|
|
47
|
+
token: () => context.token,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **`this.get`, `this.post`, `this.query`…** call the operation at a path,
|
|
52
|
+
typed by the spec. There is one for each method the spec has an operation
|
|
53
|
+
for, and it offers only the paths that method has. Each call resolves to
|
|
54
|
+
one of the declared replies, narrowed on its status.
|
|
55
|
+
- **`this.api`** is the bound client they come from, for `op(operationId)`,
|
|
56
|
+
`stream()` and `group()`. The shorthands are getters on the class, so a
|
|
57
|
+
subclass's own method of the same name wins.
|
|
58
|
+
- **`this.data(call)`** returns the data of a 2xx reply. Any other reply, or
|
|
59
|
+
none, throws a `DataSourceError`.
|
|
60
|
+
|
|
61
|
+
Every option is in [the API](#restdatasource).
|
|
62
|
+
|
|
63
|
+
## Cache
|
|
64
|
+
|
|
65
|
+
`defaultCache` is one store for every datasource, since a server makes them
|
|
66
|
+
per request. It is `cache()` from `@nxgt/httpyz`, with its defaults: it
|
|
67
|
+
keeps the `ok` replies of the reads, `GET`, `HEAD` and `QUERY`, for five
|
|
68
|
+
minutes. A search is a `QUERY`; a `POST` is never cached.
|
|
69
|
+
|
|
70
|
+
Its key is the method, the URL, the `Authorization` header and the body. So
|
|
71
|
+
no caller is answered with another's reply, and a search is cached by what it
|
|
72
|
+
searches for. It holds at most 500 replies, the least recently used going
|
|
73
|
+
first.
|
|
74
|
+
|
|
75
|
+
`defaultCache.clear()` empties it, after a write, say. A cache of your own
|
|
76
|
+
takes `cache()`'s options; `cache: false` turns it off:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { cache } from '@nxgt/httpyz';
|
|
80
|
+
|
|
81
|
+
const bookmarksCache = cache({ ttl: 30_000 });
|
|
82
|
+
new Bookmarks({ baseUrl, operations, cache: bookmarksCache });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Errors
|
|
86
|
+
|
|
87
|
+
`this.data` throws a `DataSourceError`, the package's own. graphql-js reports
|
|
88
|
+
its `extensions`, `{ code, status }`, with the error a resolver throws.
|
|
89
|
+
|
|
90
|
+
| What happened | `code` |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| 401 | `UNAUTHENTICATED` |
|
|
93
|
+
| 403 | `FORBIDDEN` |
|
|
94
|
+
| 404 | `NOT_FOUND` |
|
|
95
|
+
| any 5xx | `INTERNAL_SERVER_ERROR` |
|
|
96
|
+
| any other status | `BAD_REQUEST` |
|
|
97
|
+
| no reply: network, timeout | `SERVICE_UNAVAILABLE` |
|
|
98
|
+
| a request `validate` refuses | `BAD_REQUEST` |
|
|
99
|
+
| a reply `validate` refuses | `INTERNAL_SERVER_ERROR` |
|
|
100
|
+
| anything else thrown | `INTERNAL_SERVER_ERROR` |
|
|
101
|
+
|
|
102
|
+
Its message is the reply body's `message`, when it has a non-empty one. For
|
|
103
|
+
a call you make without `this.data`, `toDataSourceError` makes one out of
|
|
104
|
+
whatever the client threw. To map it onto your app's exceptions, catch it
|
|
105
|
+
where you throw yours:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { DataSourceError } from '@nxgt/datasource-rest';
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
return await bookmarks.bookmark(id);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof DataSourceError && error.code === 'NOT_FOUND') {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Pagination
|
|
121
|
+
|
|
122
|
+
`relayPaginate(page)` turns a service's `{ data, metadata }` page into a Relay
|
|
123
|
+
`{ edges, pageInfo }` connection, using `item.id` as the cursor:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { RESTDataSource, relayPaginate } from '@nxgt/datasource-rest';
|
|
127
|
+
|
|
128
|
+
export class Bookmarks extends RESTDataSource<ClientOperations> {
|
|
129
|
+
async bookmarks(first: number) {
|
|
130
|
+
return relayPaginate(
|
|
131
|
+
await this.data(this.get('/bookmarks', { query: { first } })),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## API
|
|
138
|
+
|
|
139
|
+
### Classes
|
|
140
|
+
|
|
141
|
+
#### `RESTDataSource`
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
class RESTDataSource<
|
|
145
|
+
Ops extends OperationsShape<Ops>, // ClientOperations, generated
|
|
146
|
+
Routes = RoutesOf<Ops>,
|
|
147
|
+
Decoded extends boolean = false,
|
|
148
|
+
> {
|
|
149
|
+
constructor(options: RESTDataSourceOptions<Ops> & { decode?: Decoded });
|
|
150
|
+
readonly api: OpenApiClient<Ops, Routes, Decoded>;
|
|
151
|
+
data<R extends { readonly status: number; readonly data: unknown }>(
|
|
152
|
+
call: Promise<R>,
|
|
153
|
+
): Promise<Success<R>['data']>;
|
|
154
|
+
// and PathMethods<Ops, Routes, Decoded>: get, post, query…
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The class to extend, one per service; see [Usage](#usage). `RESTDataSource`
|
|
159
|
+
is both a constructor and a type, the datasource's own members and the
|
|
160
|
+
spec's path methods. With `Decoded` set to `true`, the options require
|
|
161
|
+
`decode: true`.
|
|
162
|
+
|
|
163
|
+
| Option | Type | Default | Description |
|
|
164
|
+
| --- | --- | --- | --- |
|
|
165
|
+
| `baseUrl` | `string` | required | The service's base URL. |
|
|
166
|
+
| `operations` | `OperationTable<Ops>` | required | The `operations` table generated from the service's spec. |
|
|
167
|
+
| `token` | `string \| null \| undefined \| (() => … \| Promise<…>)` | none | Sent as `Authorization: Bearer <token>`. A function is read, and awaited, before each request. None, or a nullish result, sends no header. |
|
|
168
|
+
| `cache` | `Cache \| false` | `defaultCache` | The replies cache: your own `cache()` from `@nxgt/httpyz`, or `false` for none. See [Cache](#cache). |
|
|
169
|
+
| `http` | `Omit<HttpClientOptions, 'baseUrl' \| 'auth'>` | `{}` | What else the core client takes: `headers`, `timeout`, `retry`, `use`, `fetch`… Its `use` runs inside the cache. |
|
|
170
|
+
| `validate` | `boolean \| { request?: boolean; response?: boolean }` | neither | Checks the request and the reply with the spec's schemas, as `createOpenApiClient` does. |
|
|
171
|
+
| `decode` | `Decoded` | `false` | `true` returns each reply as its schema outputs it. |
|
|
172
|
+
|
|
173
|
+
| Member | Type | Description |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `api` | `OpenApiClient<Ops, Routes, Decoded>` | The client bound to the spec, for `op()`, `stream()`, `group()` and `operations`. |
|
|
176
|
+
| `data(call)` | `Promise<Success<R>['data']>` | The data of `call`'s 2xx reply. Any other reply, or none, throws a `DataSourceError`; an abort is rethrown as it is. The body of a status the spec does not declare is read for its message. |
|
|
177
|
+
| `get`, `put`, `post`, `delete`, `options`, `head`, `patch`, `trace`, `query` | `PathMethods<Ops, Routes, Decoded>[M]` | `this.api[method]`, for the methods the spec has an operation for only: `(path, ...args) => Promise<reply>`. Getters on the prototype, so a subclass's method of the same name wins. |
|
|
178
|
+
|
|
179
|
+
#### `DataSourceError`
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
class DataSourceError extends Error {
|
|
183
|
+
constructor(message: string, options: DataSourceErrorOptions);
|
|
184
|
+
readonly name: 'DataSourceError';
|
|
185
|
+
readonly code: DataSourceErrorCode;
|
|
186
|
+
readonly status: number | undefined;
|
|
187
|
+
readonly data: unknown;
|
|
188
|
+
readonly extensions: {
|
|
189
|
+
readonly code: DataSourceErrorCode;
|
|
190
|
+
readonly status: number | undefined;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Every way a call to a service fails; see [Errors](#errors). The constructor
|
|
196
|
+
passes `cause`, and any other `ErrorOptions`, on to `Error`.
|
|
197
|
+
|
|
198
|
+
| Property | Type | Description |
|
|
199
|
+
| --- | --- | --- |
|
|
200
|
+
| `name` | `'DataSourceError'` | |
|
|
201
|
+
| `message` | `string` | The reply body's `message`, or the client error's; `'An error occurred'` when what was thrown is not an `Error`. |
|
|
202
|
+
| `code` | `DataSourceErrorCode` | What failed, from the reply's status or the lack of one. |
|
|
203
|
+
| `status` | `number \| undefined` | The status of the service's reply; none when no reply came back. |
|
|
204
|
+
| `data` | `unknown` | The reply's body, as the service sent it, or the issues of a `validate` check that failed. |
|
|
205
|
+
| `extensions` | `{ code, status }` | What a GraphQL server reports with the error: graphql-js reads `extensions` off the error a resolver throws. |
|
|
206
|
+
| `cause` | `unknown` | What was thrown, when `toDataSourceError` made the error: the client's own error, or anything else. |
|
|
207
|
+
|
|
208
|
+
### Functions
|
|
209
|
+
|
|
210
|
+
#### `toDataSourceError`
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
function toDataSourceError(error: unknown, data?: unknown): DataSourceError;
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`error` as a `DataSourceError`, whatever the client threw; a
|
|
217
|
+
`DataSourceError` is returned as it is. `data` is the body of a reply the
|
|
218
|
+
error does not carry, an `UndeclaredStatusError`'s, which only a read of its
|
|
219
|
+
response gives. It codes as the table in [Errors](#errors) says, and never
|
|
220
|
+
throws.
|
|
221
|
+
|
|
222
|
+
#### `codeOf`
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
function codeOf(status: number): DataSourceErrorCode;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The code of a reply's status: 401, 403 and 404 by name, any 5xx
|
|
229
|
+
`INTERNAL_SERVER_ERROR`, any other `BAD_REQUEST`.
|
|
230
|
+
|
|
231
|
+
#### `relayPaginate`
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
function relayPaginate<T extends { id: string }>(
|
|
235
|
+
page: Paginated<T>,
|
|
236
|
+
): Connection<T>;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A service's page as a Relay connection: each item an edge, its `id` the
|
|
240
|
+
cursor, `metadata` the `pageInfo`. A page without `data` has no edges; one
|
|
241
|
+
without `metadata` gets the `pageInfo` of an empty last page,
|
|
242
|
+
`{ hasNextPage: false, totalElements: 0 }`, even when it has items. See
|
|
243
|
+
[Pagination](#pagination).
|
|
244
|
+
|
|
245
|
+
### Constants
|
|
246
|
+
|
|
247
|
+
#### `defaultCache`
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const defaultCache: Cache; // Middleware & { clear(): void }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`cache()` from `@nxgt/httpyz` with its defaults, shared by every datasource
|
|
254
|
+
not given its own `cache`. `defaultCache.clear()` empties it. See
|
|
255
|
+
[Cache](#cache).
|
|
256
|
+
|
|
257
|
+
### Types
|
|
258
|
+
|
|
259
|
+
#### `RESTDataSourceOptions`
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
interface RESTDataSourceOptions<Ops> extends OpenApiOptions {
|
|
263
|
+
baseUrl: string;
|
|
264
|
+
operations: OperationTable<Ops>;
|
|
265
|
+
token?: Token | (() => Token | Promise<Token>); // Token: string | null | undefined
|
|
266
|
+
cache?: Cache | false;
|
|
267
|
+
http?: Omit<HttpClientOptions, 'baseUrl' | 'auth'>;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
What the `RESTDataSource` constructor takes, with `decode`; each option is in
|
|
272
|
+
[its table](#restdatasource).
|
|
273
|
+
|
|
274
|
+
#### `DataSourceErrorCode`
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
type DataSourceErrorCode =
|
|
278
|
+
| 'BAD_REQUEST'
|
|
279
|
+
| 'UNAUTHENTICATED'
|
|
280
|
+
| 'FORBIDDEN'
|
|
281
|
+
| 'NOT_FOUND'
|
|
282
|
+
| 'INTERNAL_SERVER_ERROR'
|
|
283
|
+
| 'SERVICE_UNAVAILABLE';
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
A `DataSourceError`'s `code`, and what `codeOf` returns.
|
|
287
|
+
|
|
288
|
+
#### `DataSourceErrorOptions`
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
interface DataSourceErrorOptions extends ErrorOptions {
|
|
292
|
+
readonly code: DataSourceErrorCode;
|
|
293
|
+
readonly status?: number | undefined;
|
|
294
|
+
readonly data?: unknown;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The second argument of `new DataSourceError()`.
|
|
299
|
+
|
|
300
|
+
#### `Paginated`
|
|
301
|
+
|
|
302
|
+
| Field | Type | Description |
|
|
303
|
+
| --- | --- | --- |
|
|
304
|
+
| `data` | `T[]`, optional | The page's items. |
|
|
305
|
+
| `metadata` | `PageInfo`, optional | Where the page stands. |
|
|
306
|
+
|
|
307
|
+
A page as a service sends it: what `relayPaginate` takes.
|
|
308
|
+
|
|
309
|
+
#### `Connection`
|
|
310
|
+
|
|
311
|
+
| Field | Type | Description |
|
|
312
|
+
| --- | --- | --- |
|
|
313
|
+
| `edges` | `Edge<T>[]` | One per item. |
|
|
314
|
+
| `pageInfo` | `PageInfo` | Where the page stands. |
|
|
315
|
+
|
|
316
|
+
The Relay connection `relayPaginate` returns.
|
|
317
|
+
|
|
318
|
+
#### `Edge`
|
|
319
|
+
|
|
320
|
+
| Field | Type | Description |
|
|
321
|
+
| --- | --- | --- |
|
|
322
|
+
| `node` | `T` | The item. |
|
|
323
|
+
| `cursor` | `string` | The item's `id`. |
|
|
324
|
+
|
|
325
|
+
An item of a `Connection`.
|
|
326
|
+
|
|
327
|
+
#### `PageInfo`
|
|
328
|
+
|
|
329
|
+
| Field | Type | Description |
|
|
330
|
+
| --- | --- | --- |
|
|
331
|
+
| `startCursor` | `string`, optional | The cursor of the first item in the result set. |
|
|
332
|
+
| `endCursor` | `string`, optional | The cursor of the last item in the result set. |
|
|
333
|
+
| `hasNextPage` | `boolean` | Whether there are more items after the current page. |
|
|
334
|
+
| `totalElements` | `number` | The number of items across all pages. |
|
|
335
|
+
|
|
336
|
+
A `Paginated`'s `metadata`, and a `Connection`'s `pageInfo`, as it is.
|
|
337
|
+
|
|
338
|
+
## Traps
|
|
339
|
+
|
|
340
|
+
- **The cache is per process.** Two instances of a service do not share it,
|
|
341
|
+
so each may answer from its own for up to five minutes: give reads that must
|
|
342
|
+
be fresh `cache: false`, or a `cache({ ttl })` of your own.
|
|
343
|
+
- **A write does not empty the cache.** A read made after it may be answered
|
|
344
|
+
from memory: call `defaultCache.clear()` after the write.
|
|
345
|
+
- **An aborted call throws its abort**, not a `DataSourceError`: it was not
|
|
346
|
+
the service failing. Tell it apart with `isAbortError(error)` from
|
|
347
|
+
`@nxgt/httpyz`.
|
|
348
|
+
- **`this.data` reads the body of an undeclared status** to find its message.
|
|
349
|
+
A reply the spec declares is already read; declare the error statuses the
|
|
350
|
+
service replies with.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RESTDataSource`: a REST service, called from a GraphQL resolver or any
|
|
3
|
+
* server code, through the client `@nxgt/openapi-httpyz` binds to its spec.
|
|
4
|
+
* The caller's token is forwarded, reads are cached, and a call that fails
|
|
5
|
+
* becomes one `DataSourceError`.
|
|
6
|
+
*/
|
|
7
|
+
import { type Cache, type HttpClientOptions, type Success } from '@nxgt/httpyz';
|
|
8
|
+
import { type OpenApiClient, type OpenApiOptions, type OperationsShape, type OperationTable, type PathMethods, type RoutesOf } from '@nxgt/openapi-httpyz';
|
|
9
|
+
type Token = string | null | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The cache every datasource shares unless it is given its own: a server
|
|
12
|
+
* makes its datasources per request, and each would otherwise start empty.
|
|
13
|
+
* It keeps the reads, `GET`, `HEAD` and `QUERY`, a search by what it searches
|
|
14
|
+
* for. It is keyed by the caller's token, so no caller is answered with
|
|
15
|
+
* another's reply. `defaultCache.clear()` empties it.
|
|
16
|
+
*/
|
|
17
|
+
export declare const defaultCache: Cache;
|
|
18
|
+
export interface RESTDataSourceOptions<Ops> extends OpenApiOptions {
|
|
19
|
+
/** The service's base URL. */
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
/** The `operations` table generated from the service's spec. */
|
|
22
|
+
operations: OperationTable<Ops>;
|
|
23
|
+
/**
|
|
24
|
+
* Sent as `Authorization: Bearer <token>`: the caller's, forwarded. A
|
|
25
|
+
* function is read, and awaited, before each request. None sends no header.
|
|
26
|
+
*/
|
|
27
|
+
token?: Token | (() => Token | Promise<Token>);
|
|
28
|
+
/** Default: `defaultCache`, shared by every datasource. `false`: none. */
|
|
29
|
+
cache?: Cache | false;
|
|
30
|
+
/** What else the core client takes: `headers`, `timeout`, `retry`, `use`, `fetch`… */
|
|
31
|
+
http?: Omit<HttpClientOptions, 'baseUrl' | 'auth'>;
|
|
32
|
+
}
|
|
33
|
+
/** `decode`, as the binding takes it: a datasource typed as decoding must decode. */
|
|
34
|
+
type Decoding<Decoded extends boolean> = {
|
|
35
|
+
readonly decode?: Decoded;
|
|
36
|
+
} & (Decoded extends true ? {
|
|
37
|
+
readonly decode: true;
|
|
38
|
+
} : unknown);
|
|
39
|
+
/** The datasource's own members; `RESTDataSource` adds the client's path methods. */
|
|
40
|
+
declare class DataSource<Ops extends OperationsShape<Ops>, Routes, Decoded extends boolean> {
|
|
41
|
+
/** The client bound to the spec, for `op()`, `stream()` and `group()`. */
|
|
42
|
+
readonly api: OpenApiClient<Ops, Routes, Decoded>;
|
|
43
|
+
constructor(options: RESTDataSourceOptions<Ops> & Decoding<Decoded>);
|
|
44
|
+
/**
|
|
45
|
+
* The data of a call's 2xx reply. Any other reply, or none, throws a
|
|
46
|
+
* `DataSourceError`; an aborted call throws its abort, as it is.
|
|
47
|
+
*/
|
|
48
|
+
data<R extends {
|
|
49
|
+
readonly status: number;
|
|
50
|
+
readonly data: unknown;
|
|
51
|
+
}>(call: Promise<R>): Promise<Success<R>['data']>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A datasource for a service, typed by the `ClientOperations` generated from
|
|
55
|
+
* its spec. It has the client's `get`, `post`… for each method the spec has
|
|
56
|
+
* an operation for. Extend it with the calls a resolver makes:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* class Bookmarks extends RESTDataSource<ClientOperations> {
|
|
60
|
+
* bookmark(id: string) {
|
|
61
|
+
* return this.data(this.get('/bookmarks/{id}', { param: { id } }));
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* new Bookmarks({ baseUrl, operations, token: () => context.token });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export type RESTDataSource<Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>, Decoded extends boolean = false> = DataSource<Ops, Routes, Decoded> & PathMethods<Ops, Routes, Decoded>;
|
|
68
|
+
export declare const RESTDataSource: new <Ops extends OperationsShape<Ops>, Routes = RoutesOf<Ops>, Decoded extends boolean = false>(options: RESTDataSourceOptions<Ops> & Decoding<Decoded>) => RESTDataSource<Ops, Routes, Decoded>;
|
|
69
|
+
export {};
|
|
70
|
+
//# sourceMappingURL=rest-datasource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-datasource.d.ts","sourceRoot":"","sources":["../../src/datasource/rest-datasource.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,KAAK,KAAK,EAEV,KAAK,iBAAiB,EAItB,KAAK,OAAO,EAEZ,MAAM,cAAc,CAAC;AAEtB,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,MAAM,sBAAsB,CAAC;AAG9B,KAAK,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,KAAiB,CAAC;AAE7C,MAAM,WAAW,qBAAqB,CAAC,GAAG,CAAE,SAAQ,cAAc;IACjE,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,UAAU,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,CAAC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/C,0EAA0E;IAC1E,KAAK,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACtB,sFAAsF;IACtF,IAAI,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,SAAS,GAAG,MAAM,CAAC,CAAC;CACnD;AAED,qFAAqF;AACrF,KAAK,QAAQ,CAAC,OAAO,SAAS,OAAO,IAAI;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,CAAC,OAAO,SAAS,IAAI,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,CAAC;AAEjE,qFAAqF;AACrF,cAAM,UAAU,CACf,GAAG,SAAS,eAAe,CAAC,GAAG,CAAC,EAChC,MAAM,EACN,OAAO,SAAS,OAAO;IAEvB,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtC,OAAO,EAAE,qBAAqB,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC;IA4BnE;;;OAGG;IACG,IAAI,CAAC,CAAC,SAAS;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EACvE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;CAa9B;AAaD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,CACzB,GAAG,SAAS,eAAe,CAAC,GAAG,CAAC,EAChC,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,EACtB,OAAO,SAAS,OAAO,GAAG,KAAK,IAC5B,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAEzE,eAAO,MAAM,cAAc,EAA4B,KACtD,GAAG,SAAS,eAAe,CAAC,GAAG,CAAC,EAChC,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,EACtB,OAAO,SAAS,OAAO,GAAG,KAAK,EAE/B,OAAO,EAAE,qBAAqB,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,KACnD,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type DataSourceErrorCode = 'BAD_REQUEST' | 'UNAUTHENTICATED' | 'FORBIDDEN' | 'NOT_FOUND' | 'INTERNAL_SERVER_ERROR' | 'SERVICE_UNAVAILABLE';
|
|
2
|
+
export interface DataSourceErrorOptions extends ErrorOptions {
|
|
3
|
+
readonly code: DataSourceErrorCode;
|
|
4
|
+
readonly status?: number | undefined;
|
|
5
|
+
readonly data?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare class DataSourceError extends Error {
|
|
8
|
+
readonly name = "DataSourceError";
|
|
9
|
+
readonly code: DataSourceErrorCode;
|
|
10
|
+
/** The status of the service's reply; none when no reply came back. */
|
|
11
|
+
readonly status: number | undefined;
|
|
12
|
+
/** The body of the service's reply, as it sent it, or the issues of a check that failed. */
|
|
13
|
+
readonly data: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* What a GraphQL server reports with the error: graphql-js reads
|
|
16
|
+
* `extensions` off the error a resolver throws.
|
|
17
|
+
*/
|
|
18
|
+
readonly extensions: {
|
|
19
|
+
readonly code: DataSourceErrorCode;
|
|
20
|
+
readonly status: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
constructor(message: string, { code, status, data, ...options }: DataSourceErrorOptions);
|
|
23
|
+
}
|
|
24
|
+
/** The code of a reply's status: any 5xx is the service's failure, any other 4xx a bad request. */
|
|
25
|
+
export declare function codeOf(status: number): DataSourceErrorCode;
|
|
26
|
+
/**
|
|
27
|
+
* `error` as a `DataSourceError`, whatever the client threw. `data` is the
|
|
28
|
+
* body of a reply the error does not carry: an `UndeclaredStatusError`'s,
|
|
29
|
+
* which only a read of its response gives.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toDataSourceError(error: unknown, data?: unknown): DataSourceError;
|
|
32
|
+
//# sourceMappingURL=data-source-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-source-error.d.ts","sourceRoot":"","sources":["../../src/errors/data-source-error.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,mBAAmB,GAC5B,aAAa,GACb,iBAAiB,GACjB,WAAW,GACX,WAAW,GACX,uBAAuB,GACvB,qBAAqB,CAAC;AAEzB,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC3D,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACzC,SAAkB,IAAI,qBAAqB;IAC3C,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE;QACpB,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;QACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;KACpC,CAAC;gBAGD,OAAO,EAAE,MAAM,EACf,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,EAAE,sBAAsB;CAQ3D;AAED,mGAAmG;AACnG,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAM1D;AAYD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE,OAAO,GACZ,eAAe,CAuCjB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
1
|
+
export { defaultCache, RESTDataSource, type RESTDataSourceOptions, } from './datasource/rest-datasource';
|
|
2
|
+
export { codeOf, DataSourceError, type DataSourceErrorCode, type DataSourceErrorOptions, toDataSourceError, } from './errors/data-source-error';
|
|
3
|
+
export { relayPaginate } from './pagination/relay';
|
|
4
|
+
export type { Connection, Edge, PageInfo, Paginated } from './pagination/types';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,YAAY,EACZ,cAAc,EACd,KAAK,qBAAqB,GAC1B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACN,MAAM,EACN,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,iBAAiB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,141 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
// src/datasource/rest-datasource.ts
|
|
2
|
+
import {
|
|
3
|
+
createHttpClient,
|
|
4
|
+
isAbortError,
|
|
5
|
+
ok,
|
|
6
|
+
cache as replies,
|
|
7
|
+
UndeclaredStatusError as UndeclaredStatusError2
|
|
8
|
+
} from "@nxgt/httpyz";
|
|
9
|
+
import { METHODS } from "@nxgt/httpyz/integration";
|
|
10
|
+
import {
|
|
11
|
+
createOpenApiClient
|
|
12
|
+
} from "@nxgt/openapi-httpyz";
|
|
13
|
+
|
|
14
|
+
// src/errors/data-source-error.ts
|
|
15
|
+
import {
|
|
16
|
+
NetworkError,
|
|
17
|
+
ReplyStatusError,
|
|
18
|
+
TimeoutError,
|
|
19
|
+
UndeclaredStatusError,
|
|
20
|
+
ValidationError
|
|
21
|
+
} from "@nxgt/httpyz";
|
|
22
|
+
|
|
23
|
+
class DataSourceError extends Error {
|
|
24
|
+
constructor(message, { code, status, data, ...options }) {
|
|
25
|
+
super(message, options);
|
|
26
|
+
this.name = "DataSourceError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.data = data;
|
|
30
|
+
this.extensions = { code, status };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function codeOf(status) {
|
|
34
|
+
if (status === 401)
|
|
35
|
+
return "UNAUTHENTICATED";
|
|
36
|
+
if (status === 403)
|
|
37
|
+
return "FORBIDDEN";
|
|
38
|
+
if (status === 404)
|
|
39
|
+
return "NOT_FOUND";
|
|
40
|
+
if (status >= 500)
|
|
41
|
+
return "INTERNAL_SERVER_ERROR";
|
|
42
|
+
return "BAD_REQUEST";
|
|
43
|
+
}
|
|
44
|
+
var messageOf = (data, fallback) => typeof data === "object" && data !== null && ("message" in data) && typeof data.message === "string" && data.message !== "" ? data.message : fallback;
|
|
45
|
+
function toDataSourceError(error, data) {
|
|
46
|
+
if (error instanceof DataSourceError)
|
|
47
|
+
return error;
|
|
48
|
+
if (error instanceof ReplyStatusError) {
|
|
49
|
+
return new DataSourceError(messageOf(error.data, error.message), {
|
|
50
|
+
code: codeOf(error.status),
|
|
51
|
+
status: error.status,
|
|
52
|
+
data: error.data,
|
|
53
|
+
cause: error
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (error instanceof UndeclaredStatusError) {
|
|
57
|
+
return new DataSourceError(messageOf(data, error.message), {
|
|
58
|
+
code: codeOf(error.status),
|
|
59
|
+
status: error.status,
|
|
60
|
+
data,
|
|
61
|
+
cause: error
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (error instanceof NetworkError || error instanceof TimeoutError) {
|
|
65
|
+
return new DataSourceError(error.message, {
|
|
66
|
+
code: "SERVICE_UNAVAILABLE",
|
|
67
|
+
cause: error
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (error instanceof ValidationError) {
|
|
71
|
+
return new DataSourceError(error.message, {
|
|
72
|
+
code: error.failure.kind === "request" ? "BAD_REQUEST" : "INTERNAL_SERVER_ERROR",
|
|
73
|
+
data: error.failure.issues,
|
|
74
|
+
cause: error
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return new DataSourceError(error instanceof Error ? error.message : "An error occurred", { code: "INTERNAL_SERVER_ERROR", cause: error });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/datasource/rest-datasource.ts
|
|
81
|
+
var defaultCache = replies();
|
|
82
|
+
|
|
83
|
+
class DataSource {
|
|
84
|
+
constructor(options) {
|
|
85
|
+
const {
|
|
86
|
+
baseUrl,
|
|
87
|
+
operations,
|
|
88
|
+
token,
|
|
89
|
+
cache = defaultCache,
|
|
90
|
+
http = {},
|
|
91
|
+
...binding
|
|
92
|
+
} = options;
|
|
93
|
+
const client = createHttpClient({
|
|
94
|
+
...http,
|
|
95
|
+
baseUrl,
|
|
96
|
+
...token === undefined ? {} : {
|
|
97
|
+
auth: { token: typeof token === "function" ? token : () => token }
|
|
98
|
+
},
|
|
99
|
+
use: [...cache ? [cache] : [], ...http.use ?? []]
|
|
100
|
+
});
|
|
101
|
+
this.api = createOpenApiClient(client, operations, ...[binding]);
|
|
102
|
+
}
|
|
103
|
+
async data(call) {
|
|
104
|
+
try {
|
|
105
|
+
return ok(await call);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isAbortError(error))
|
|
108
|
+
throw error;
|
|
109
|
+
throw toDataSourceError(error, error instanceof UndeclaredStatusError2 ? await bodyOf(error.response) : undefined);
|
|
110
|
+
}
|
|
20
111
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
112
|
+
}
|
|
113
|
+
for (const method of METHODS) {
|
|
114
|
+
Object.defineProperty(DataSource.prototype, method, {
|
|
115
|
+
configurable: true,
|
|
116
|
+
get() {
|
|
117
|
+
return this.api[method];
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
var RESTDataSource = DataSource;
|
|
122
|
+
async function bodyOf(response) {
|
|
123
|
+
const text = await response.clone().text().catch(() => {
|
|
124
|
+
return;
|
|
26
125
|
});
|
|
126
|
+
if (!text)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(text);
|
|
130
|
+
} catch {
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
27
133
|
}
|
|
28
|
-
// src/
|
|
29
|
-
function relayPaginate(
|
|
30
|
-
const { data: items, metadata } =
|
|
134
|
+
// src/pagination/relay.ts
|
|
135
|
+
function relayPaginate(page) {
|
|
136
|
+
const { data: items, metadata } = page;
|
|
31
137
|
return {
|
|
32
|
-
edges: (items ?? []).map((item) => ({
|
|
33
|
-
node: item,
|
|
34
|
-
cursor: item.id
|
|
35
|
-
})),
|
|
138
|
+
edges: (items ?? []).map((item) => ({ node: item, cursor: item.id })),
|
|
36
139
|
pageInfo: metadata ?? {
|
|
37
140
|
startCursor: undefined,
|
|
38
141
|
endCursor: undefined,
|
|
@@ -41,71 +144,14 @@ function relayPaginate(data) {
|
|
|
41
144
|
}
|
|
42
145
|
};
|
|
43
146
|
}
|
|
44
|
-
// src/middlewares/auth.middleware.ts
|
|
45
|
-
var authMiddleware = (options) => ({
|
|
46
|
-
onRequest({ request }) {
|
|
47
|
-
if (!options.token) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (options.shouldUseToken && !options.shouldUseToken(request)) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
request.headers.set("Authorization", `Bearer ${typeof options.token === "function" ? options.token() : options.token}`);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
// src/middlewares/cache.middleware.ts
|
|
57
|
-
var cache = new Map;
|
|
58
|
-
var getCacheKey = (request) => `${request.method}:${request.url}`;
|
|
59
|
-
var cacheMiddleware = ({
|
|
60
|
-
ttl = 5 * 60 * 1000,
|
|
61
|
-
shouldCachePostRequest = (req) => req.url.includes("/search")
|
|
62
|
-
} = {}) => ({
|
|
63
|
-
onRequest({ request }) {
|
|
64
|
-
const key = getCacheKey(request);
|
|
65
|
-
const cached = cache.get(key);
|
|
66
|
-
if (cached && cached.expiry > Date.now()) {
|
|
67
|
-
return cached.value.clone();
|
|
68
|
-
}
|
|
69
|
-
cache.delete(key);
|
|
70
|
-
},
|
|
71
|
-
onResponse({ request, response }) {
|
|
72
|
-
if (response.ok) {
|
|
73
|
-
if (request.method === "POST" && !shouldCachePostRequest?.(request)) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const key = getCacheKey(request);
|
|
77
|
-
cache.set(key, {
|
|
78
|
-
expiry: Date.now() + ttl,
|
|
79
|
-
value: response.clone()
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
// src/rest-datasource.ts
|
|
85
|
-
class RESTDataSource {
|
|
86
|
-
constructor(options) {
|
|
87
|
-
this.client = options.client;
|
|
88
|
-
this.client.use(cacheMiddleware(options.cacheOptions));
|
|
89
|
-
if (options.authOptions) {
|
|
90
|
-
this.client.use(authMiddleware(options.authOptions));
|
|
91
|
-
}
|
|
92
|
-
this.get = this.client.GET;
|
|
93
|
-
this.post = this.client.POST;
|
|
94
|
-
this.put = this.client.PUT;
|
|
95
|
-
this.patch = this.client.PATCH;
|
|
96
|
-
this.delete = this.client.DELETE;
|
|
97
|
-
this.head = this.client.HEAD;
|
|
98
|
-
this.options = this.client.OPTIONS;
|
|
99
|
-
this.trace = this.client.TRACE;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
147
|
export {
|
|
148
|
+
DataSourceError,
|
|
103
149
|
RESTDataSource,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
codeOf,
|
|
151
|
+
defaultCache,
|
|
152
|
+
relayPaginate,
|
|
153
|
+
toDataSourceError
|
|
108
154
|
};
|
|
109
155
|
|
|
110
|
-
//# debugId=
|
|
156
|
+
//# debugId=6D7A6C634E40DB2764756E2164756E21
|
|
111
157
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/
|
|
3
|
+
"sources": ["../src/datasource/rest-datasource.ts", "../src/errors/data-source-error.ts", "../src/pagination/relay.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"import type {
|
|
8
|
-
"import type { Middleware } from 'openapi-fetch';\n\nconst cache = new Map<string, { expiry: number; value: Response }>();\nconst getCacheKey = (request: Request) => `${request.method}:${request.url}`;\n\nexport type CacheMiddlewareOptions = {\n\tttl?: number;\n\tshouldCachePostRequest?: (req: Request) => boolean;\n};\n\nexport const cacheMiddleware = ({\n\tttl = 5 * 60 * 1000, // default TTL: 5 minutes\n\tshouldCachePostRequest = (req) => req.url.includes('/search'),\n}: CacheMiddlewareOptions = {}): Middleware => ({\n\tonRequest({ request }) {\n\t\tconst key = getCacheKey(request);\n\t\tconst cached = cache.get(key);\n\t\tif (cached && cached.expiry > Date.now()) {\n\t\t\treturn cached.value.clone();\n\t\t}\n\t\tcache.delete(key);\n\t},\n\tonResponse({ request, response }) {\n\t\tif (response.ok) {\n\t\t\tif (request.method === 'POST' && !shouldCachePostRequest?.(request)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst key = getCacheKey(request);\n\t\t\tcache.set(key, {\n\t\t\t\texpiry: Date.now() + ttl,\n\t\t\t\tvalue: response.clone(),\n\t\t\t});\n\t\t}\n\t},\n});\n",
|
|
9
|
-
"import type { Client, ClientMethod } from 'openapi-fetch';\nimport {\n\ttype AuthMiddlewareOptions,\n\tauthMiddleware,\n\ttype CacheMiddlewareOptions,\n\tcacheMiddleware,\n} from './middlewares';\nimport type { MediaType } from './types';\n\nexport type RESTDataSourceOptions<\n\tPaths extends {},\n\tMedia extends MediaType = MediaType,\n> = {\n\tclient: Client<Paths, Media>;\n\tcacheOptions?: CacheMiddlewareOptions;\n\tauthOptions?: AuthMiddlewareOptions;\n};\n\nexport class RESTDataSource<\n\tPaths extends {},\n\tMedia extends MediaType = MediaType,\n> {\n\tprotected client: Client<Paths, Media>;\n\n\tget: ClientMethod<Paths, 'get', Media>;\n\tpost: ClientMethod<Paths, 'post', Media>;\n\tput: ClientMethod<Paths, 'put', Media>;\n\tpatch: ClientMethod<Paths, 'patch', Media>;\n\tdelete: ClientMethod<Paths, 'delete', Media>;\n\thead: ClientMethod<Paths, 'head', Media>;\n\toptions: ClientMethod<Paths, 'options', Media>;\n\ttrace: ClientMethod<Paths, 'trace', Media>;\n\n\tconstructor(options: RESTDataSourceOptions<Paths, Media>) {\n\t\tthis.client = options.client;\n\t\tthis.client.use(cacheMiddleware(options.cacheOptions));\n\t\tif (options.authOptions) {\n\t\t\tthis.client.use(authMiddleware(options.authOptions));\n\t\t}\n\t\tthis.get = this.client.GET;\n\t\tthis.post = this.client.POST;\n\t\tthis.put = this.client.PUT;\n\t\tthis.patch = this.client.PATCH;\n\t\tthis.delete = this.client.DELETE;\n\t\tthis.head = this.client.HEAD;\n\t\tthis.options = this.client.OPTIONS;\n\t\tthis.trace = this.client.TRACE;\n\t}\n}\n"
|
|
5
|
+
"/**\n * `RESTDataSource`: a REST service, called from a GraphQL resolver or any\n * server code, through the client `@nxgt/openapi-httpyz` binds to its spec.\n * The caller's token is forwarded, reads are cached, and a call that fails\n * becomes one `DataSourceError`.\n */\nimport {\n\ttype Cache,\n\tcreateHttpClient,\n\ttype HttpClientOptions,\n\tisAbortError,\n\tok,\n\tcache as replies,\n\ttype Success,\n\tUndeclaredStatusError,\n} from '@nxgt/httpyz';\nimport { METHODS } from '@nxgt/httpyz/integration';\nimport {\n\tcreateOpenApiClient,\n\ttype OpenApiArgs,\n\ttype OpenApiClient,\n\ttype OpenApiOptions,\n\ttype OperationsShape,\n\ttype OperationTable,\n\ttype PathMethods,\n\ttype RoutesOf,\n} from '@nxgt/openapi-httpyz';\nimport { toDataSourceError } from '../errors/data-source-error';\n\ntype Token = string | null | undefined;\n\n/**\n * The cache every datasource shares unless it is given its own: a server\n * makes its datasources per request, and each would otherwise start empty.\n * It keeps the reads, `GET`, `HEAD` and `QUERY`, a search by what it searches\n * for. It is keyed by the caller's token, so no caller is answered with\n * another's reply. `defaultCache.clear()` empties it.\n */\nexport const defaultCache: Cache = replies();\n\nexport interface RESTDataSourceOptions<Ops> extends OpenApiOptions {\n\t/** The service's base URL. */\n\tbaseUrl: string;\n\t/** The `operations` table generated from the service's spec. */\n\toperations: OperationTable<Ops>;\n\t/**\n\t * Sent as `Authorization: Bearer <token>`: the caller's, forwarded. A\n\t * function is read, and awaited, before each request. None sends no header.\n\t */\n\ttoken?: Token | (() => Token | Promise<Token>);\n\t/** Default: `defaultCache`, shared by every datasource. `false`: none. */\n\tcache?: Cache | false;\n\t/** What else the core client takes: `headers`, `timeout`, `retry`, `use`, `fetch`… */\n\thttp?: Omit<HttpClientOptions, 'baseUrl' | 'auth'>;\n}\n\n/** `decode`, as the binding takes it: a datasource typed as decoding must decode. */\ntype Decoding<Decoded extends boolean> = {\n\treadonly decode?: Decoded;\n} & (Decoded extends true ? { readonly decode: true } : unknown);\n\n/** The datasource's own members; `RESTDataSource` adds the client's path methods. */\nclass DataSource<\n\tOps extends OperationsShape<Ops>,\n\tRoutes,\n\tDecoded extends boolean,\n> {\n\t/** The client bound to the spec, for `op()`, `stream()` and `group()`. */\n\treadonly api: OpenApiClient<Ops, Routes, Decoded>;\n\n\tconstructor(options: RESTDataSourceOptions<Ops> & Decoding<Decoded>) {\n\t\tconst {\n\t\t\tbaseUrl,\n\t\t\toperations,\n\t\t\ttoken,\n\t\t\tcache = defaultCache,\n\t\t\thttp = {},\n\t\t\t...binding\n\t\t} = options;\n\t\tconst client = createHttpClient({\n\t\t\t...http,\n\t\t\tbaseUrl,\n\t\t\t...(token === undefined\n\t\t\t\t? {}\n\t\t\t\t: {\n\t\t\t\t\t\tauth: { token: typeof token === 'function' ? token : () => token },\n\t\t\t\t\t}),\n\t\t\t// Inside `auth`, so the cache is keyed by the token it sent.\n\t\t\tuse: [...(cache ? [cache] : []), ...(http.use ?? [])],\n\t\t});\n\t\tthis.api = createOpenApiClient<Ops, Routes, Decoded>(\n\t\t\tclient,\n\t\t\toperations,\n\t\t\t// `Decoding` already holds `decode` to `Decoded`.\n\t\t\t...([binding] as unknown as OpenApiArgs<Decoded>),\n\t\t);\n\t}\n\n\t/**\n\t * The data of a call's 2xx reply. Any other reply, or none, throws a\n\t * `DataSourceError`; an aborted call throws its abort, as it is.\n\t */\n\tasync data<R extends { readonly status: number; readonly data: unknown }>(\n\t\tcall: Promise<R>,\n\t): Promise<Success<R>['data']> {\n\t\ttry {\n\t\t\treturn ok(await call);\n\t\t} catch (error) {\n\t\t\tif (isAbortError(error)) throw error;\n\t\t\tthrow toDataSourceError(\n\t\t\t\terror,\n\t\t\t\terror instanceof UndeclaredStatusError\n\t\t\t\t\t? await bodyOf(error.response)\n\t\t\t\t\t: undefined,\n\t\t\t);\n\t\t}\n\t}\n}\n\n// `this.get(...)` for `this.api.get(...)`: getters on the prototype, so that a\n// subclass's own `get()` or `delete()` still wins.\nfor (const method of METHODS) {\n\tObject.defineProperty(DataSource.prototype, method, {\n\t\tconfigurable: true,\n\t\tget(this: { readonly api: Record<string, unknown> }) {\n\t\t\treturn this.api[method];\n\t\t},\n\t});\n}\n\n/**\n * A datasource for a service, typed by the `ClientOperations` generated from\n * its spec. It has the client's `get`, `post`… for each method the spec has\n * an operation for. Extend it with the calls a resolver makes:\n *\n * ```ts\n * class Bookmarks extends RESTDataSource<ClientOperations> {\n * \tbookmark(id: string) {\n * \t\treturn this.data(this.get('/bookmarks/{id}', { param: { id } }));\n * \t}\n * }\n * new Bookmarks({ baseUrl, operations, token: () => context.token });\n * ```\n */\nexport type RESTDataSource<\n\tOps extends OperationsShape<Ops>,\n\tRoutes = RoutesOf<Ops>,\n\tDecoded extends boolean = false,\n> = DataSource<Ops, Routes, Decoded> & PathMethods<Ops, Routes, Decoded>;\n\nexport const RESTDataSource = DataSource as unknown as new <\n\tOps extends OperationsShape<Ops>,\n\tRoutes = RoutesOf<Ops>,\n\tDecoded extends boolean = false,\n>(\n\toptions: RESTDataSourceOptions<Ops> & Decoding<Decoded>,\n) => RESTDataSource<Ops, Routes, Decoded>;\n\n/** A reply's body: JSON when it parses, its text otherwise, nothing when it cannot be read. */\nasync function bodyOf(response: Response): Promise<unknown> {\n\tconst text = await response\n\t\t.clone()\n\t\t.text()\n\t\t.catch(() => undefined);\n\tif (!text) return undefined;\n\ttry {\n\t\treturn JSON.parse(text);\n\t} catch {\n\t\treturn text;\n\t}\n}\n",
|
|
6
|
+
"/**\n * `DataSourceError`: every way a call to a service can fail, as one error\n * with a code a GraphQL server reports. It is the package's own, and depends\n * on no exception package of the app's: map it to yours where you catch it.\n */\nimport {\n\tNetworkError,\n\tReplyStatusError,\n\tTimeoutError,\n\tUndeclaredStatusError,\n\tValidationError,\n} from '@nxgt/httpyz';\n\nexport type DataSourceErrorCode =\n\t| 'BAD_REQUEST'\n\t| 'UNAUTHENTICATED'\n\t| 'FORBIDDEN'\n\t| 'NOT_FOUND'\n\t| 'INTERNAL_SERVER_ERROR'\n\t| 'SERVICE_UNAVAILABLE';\n\nexport interface DataSourceErrorOptions extends ErrorOptions {\n\treadonly code: DataSourceErrorCode;\n\treadonly status?: number | undefined;\n\treadonly data?: unknown;\n}\n\nexport class DataSourceError extends Error {\n\toverride readonly name = 'DataSourceError';\n\treadonly code: DataSourceErrorCode;\n\t/** The status of the service's reply; none when no reply came back. */\n\treadonly status: number | undefined;\n\t/** The body of the service's reply, as it sent it, or the issues of a check that failed. */\n\treadonly data: unknown;\n\t/**\n\t * What a GraphQL server reports with the error: graphql-js reads\n\t * `extensions` off the error a resolver throws.\n\t */\n\treadonly extensions: {\n\t\treadonly code: DataSourceErrorCode;\n\t\treadonly status: number | undefined;\n\t};\n\n\tconstructor(\n\t\tmessage: string,\n\t\t{ code, status, data, ...options }: DataSourceErrorOptions,\n\t) {\n\t\tsuper(message, options);\n\t\tthis.code = code;\n\t\tthis.status = status;\n\t\tthis.data = data;\n\t\tthis.extensions = { code, status };\n\t}\n}\n\n/** The code of a reply's status: any 5xx is the service's failure, any other 4xx a bad request. */\nexport function codeOf(status: number): DataSourceErrorCode {\n\tif (status === 401) return 'UNAUTHENTICATED';\n\tif (status === 403) return 'FORBIDDEN';\n\tif (status === 404) return 'NOT_FOUND';\n\tif (status >= 500) return 'INTERNAL_SERVER_ERROR';\n\treturn 'BAD_REQUEST';\n}\n\n/** The `message` of an error body, as the services write it, or `fallback`. */\nconst messageOf = (data: unknown, fallback: string): string =>\n\ttypeof data === 'object' &&\n\tdata !== null &&\n\t'message' in data &&\n\ttypeof data.message === 'string' &&\n\tdata.message !== ''\n\t\t? data.message\n\t\t: fallback;\n\n/**\n * `error` as a `DataSourceError`, whatever the client threw. `data` is the\n * body of a reply the error does not carry: an `UndeclaredStatusError`'s,\n * which only a read of its response gives.\n */\nexport function toDataSourceError(\n\terror: unknown,\n\tdata?: unknown,\n): DataSourceError {\n\tif (error instanceof DataSourceError) return error;\n\tif (error instanceof ReplyStatusError) {\n\t\treturn new DataSourceError(messageOf(error.data, error.message), {\n\t\t\tcode: codeOf(error.status),\n\t\t\tstatus: error.status,\n\t\t\tdata: error.data,\n\t\t\tcause: error,\n\t\t});\n\t}\n\tif (error instanceof UndeclaredStatusError) {\n\t\treturn new DataSourceError(messageOf(data, error.message), {\n\t\t\tcode: codeOf(error.status),\n\t\t\tstatus: error.status,\n\t\t\tdata,\n\t\t\tcause: error,\n\t\t});\n\t}\n\tif (error instanceof NetworkError || error instanceof TimeoutError) {\n\t\treturn new DataSourceError(error.message, {\n\t\t\tcode: 'SERVICE_UNAVAILABLE',\n\t\t\tcause: error,\n\t\t});\n\t}\n\tif (error instanceof ValidationError) {\n\t\t// A request the spec refuses is the caller's fault; a reply it refuses, the service's.\n\t\treturn new DataSourceError(error.message, {\n\t\t\tcode:\n\t\t\t\terror.failure.kind === 'request'\n\t\t\t\t\t? 'BAD_REQUEST'\n\t\t\t\t\t: 'INTERNAL_SERVER_ERROR',\n\t\t\tdata: error.failure.issues,\n\t\t\tcause: error,\n\t\t});\n\t}\n\treturn new DataSourceError(\n\t\terror instanceof Error ? error.message : 'An error occurred',\n\t\t{ code: 'INTERNAL_SERVER_ERROR', cause: error },\n\t);\n}\n",
|
|
7
|
+
"import type { Connection, Paginated } from './types';\n\n/**\n * A service's `{ data, metadata }` page as a Relay connection: each item an\n * edge, its `id` the cursor. Without `metadata`, its page info is an empty\n * last page's, even when it has items.\n */\nexport function relayPaginate<T extends { id: string }>(\n\tpage: Paginated<T>,\n): Connection<T> {\n\tconst { data: items, metadata } = page;\n\treturn {\n\t\tedges: (items ?? []).map((item) => ({ node: item, cursor: item.id })),\n\t\tpageInfo: metadata ?? {\n\t\t\tstartCursor: undefined,\n\t\t\tendCursor: undefined,\n\t\t\thasNextPage: false,\n\t\t\ttotalElements: 0,\n\t\t},\n\t};\n}\n"
|
|
10
8
|
],
|
|
11
|
-
"mappings": ";AAAA;
|
|
12
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "6D7A6C634E40DB2764756E2164756E21",
|
|
13
11
|
"names": []
|
|
14
12
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Connection, Paginated } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A service's `{ data, metadata }` page as a Relay connection: each item an
|
|
4
|
+
* edge, its `id` the cursor. Without `metadata`, its page info is an empty
|
|
5
|
+
* last page's, even when it has items.
|
|
6
|
+
*/
|
|
7
|
+
export declare function relayPaginate<T extends {
|
|
8
|
+
id: string;
|
|
9
|
+
}>(page: Paginated<T>): Connection<T>;
|
|
10
|
+
//# sourceMappingURL=relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/pagination/relay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACrD,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GAChB,UAAU,CAAC,CAAC,CAAC,CAWf"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** A service's page, and the Relay connection a GraphQL schema serves it as. */
|
|
2
|
+
export interface PageInfo {
|
|
3
|
+
/** The cursor of the first item in the result set. */
|
|
4
|
+
startCursor?: string;
|
|
5
|
+
/** The cursor of the last item in the result set. */
|
|
6
|
+
endCursor?: string;
|
|
7
|
+
/** Whether there are more items after the current page. */
|
|
8
|
+
hasNextPage: boolean;
|
|
9
|
+
/** The number of items across all pages. */
|
|
10
|
+
totalElements: number;
|
|
11
|
+
}
|
|
12
|
+
/** A page as the services send it. */
|
|
13
|
+
export type Paginated<T> = {
|
|
14
|
+
data?: T[];
|
|
15
|
+
metadata?: PageInfo;
|
|
16
|
+
};
|
|
17
|
+
export type Edge<T> = {
|
|
18
|
+
node: T;
|
|
19
|
+
cursor: string;
|
|
20
|
+
};
|
|
21
|
+
export type Connection<T> = {
|
|
22
|
+
edges: Edge<T>[];
|
|
23
|
+
pageInfo: PageInfo;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/pagination/types.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,WAAW,QAAQ;IACxB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,WAAW,EAAE,OAAO,CAAC;IACrB,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,sCAAsC;AACtC,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IAC1B,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IACX,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI;IACrB,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC3B,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACnB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/datasource-rest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/softistx/nxgt-
|
|
23
|
+
"url": "git+https://github.com/softistx/nxgt-http.git",
|
|
24
24
|
"directory": "packages/datasource-rest"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
@@ -29,24 +29,25 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "bun run ../../build.ts",
|
|
32
|
-
"
|
|
32
|
+
"fixtures": "bun run test/generate.ts",
|
|
33
|
+
"test": "bun run fixtures && bun test src",
|
|
34
|
+
"typecheck": "bun run fixtures && tsc --noEmit"
|
|
33
35
|
},
|
|
34
36
|
"nxgt": {
|
|
35
37
|
"entrypoints": [
|
|
36
38
|
"src/index.ts"
|
|
37
39
|
]
|
|
38
40
|
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"lodash": "^4.18.1",
|
|
41
|
-
"@nxgt/shared": "^1.0.1",
|
|
42
|
-
"@nxgt/shared-exceptions": "^1.0.1",
|
|
43
|
-
"openapi-fetch": "^0.17.0"
|
|
44
|
-
},
|
|
45
41
|
"devDependencies": {
|
|
42
|
+
"@nxgt/httpyz": "^0.0.0",
|
|
43
|
+
"@nxgt/openapi-codegen": "^0.1.0",
|
|
44
|
+
"@nxgt/openapi-httpyz": "^0.0.0",
|
|
46
45
|
"@types/bun": "^1.4.0",
|
|
47
|
-
"
|
|
46
|
+
"zod": "^4.5.4"
|
|
48
47
|
},
|
|
49
48
|
"peerDependencies": {
|
|
49
|
+
"@nxgt/httpyz": "^0.0.0",
|
|
50
|
+
"@nxgt/openapi-httpyz": "^0.0.0",
|
|
50
51
|
"typescript": "^6.0.3"
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"error-handling.d.ts","sourceRoot":"","sources":["../../src/helpers/error-handling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAa,MAAM,yBAAyB,CAAC;AAErE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,wBAyB9D"}
|
package/dist/helpers/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pagination.utils.d.ts","sourceRoot":"","sources":["../../src/helpers/pagination.utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEtD,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACrD,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GAChB,UAAU,CAAC,CAAC,CAAC,CAcf"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Middleware } from 'openapi-fetch';
|
|
2
|
-
export type AuthMiddlewareOptions = {
|
|
3
|
-
token?: string | (() => Promise<string>);
|
|
4
|
-
shouldUseToken?: (request: Request) => boolean;
|
|
5
|
-
};
|
|
6
|
-
export declare const authMiddleware: (options: AuthMiddlewareOptions) => Middleware;
|
|
7
|
-
//# sourceMappingURL=auth.middleware.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.middleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/auth.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,MAAM,qBAAqB,GAAG;IACnC,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACzC,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,qBAAqB,KAAG,UAa9D,CAAC"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Middleware } from 'openapi-fetch';
|
|
2
|
-
export type CacheMiddlewareOptions = {
|
|
3
|
-
ttl?: number;
|
|
4
|
-
shouldCachePostRequest?: (req: Request) => boolean;
|
|
5
|
-
};
|
|
6
|
-
export declare const cacheMiddleware: ({ ttl, shouldCachePostRequest, }?: CacheMiddlewareOptions) => Middleware;
|
|
7
|
-
//# sourceMappingURL=cache.middleware.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cache.middleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/cache.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKhD,MAAM,MAAM,sBAAsB,GAAG;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sBAAsB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;CACnD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,mCAG7B,sBAA2B,KAAG,UAqB/B,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middlewares/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { Client, ClientMethod } from 'openapi-fetch';
|
|
2
|
-
import { type AuthMiddlewareOptions, type CacheMiddlewareOptions } from './middlewares';
|
|
3
|
-
import type { MediaType } from './types';
|
|
4
|
-
export type RESTDataSourceOptions<Paths extends {}, Media extends MediaType = MediaType> = {
|
|
5
|
-
client: Client<Paths, Media>;
|
|
6
|
-
cacheOptions?: CacheMiddlewareOptions;
|
|
7
|
-
authOptions?: AuthMiddlewareOptions;
|
|
8
|
-
};
|
|
9
|
-
export declare class RESTDataSource<Paths extends {}, Media extends MediaType = MediaType> {
|
|
10
|
-
protected client: Client<Paths, Media>;
|
|
11
|
-
get: ClientMethod<Paths, 'get', Media>;
|
|
12
|
-
post: ClientMethod<Paths, 'post', Media>;
|
|
13
|
-
put: ClientMethod<Paths, 'put', Media>;
|
|
14
|
-
patch: ClientMethod<Paths, 'patch', Media>;
|
|
15
|
-
delete: ClientMethod<Paths, 'delete', Media>;
|
|
16
|
-
head: ClientMethod<Paths, 'head', Media>;
|
|
17
|
-
options: ClientMethod<Paths, 'options', Media>;
|
|
18
|
-
trace: ClientMethod<Paths, 'trace', Media>;
|
|
19
|
-
constructor(options: RESTDataSourceOptions<Paths, Media>);
|
|
20
|
-
}
|
|
21
|
-
//# sourceMappingURL=rest-datasource.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rest-datasource.d.ts","sourceRoot":"","sources":["../src/rest-datasource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EACN,KAAK,qBAAqB,EAE1B,KAAK,sBAAsB,EAE3B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,qBAAqB,CAChC,KAAK,SAAS,EAAE,EAChB,KAAK,SAAS,SAAS,GAAG,SAAS,IAChC;IACH,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7B,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACpC,CAAC;AAEF,qBAAa,cAAc,CAC1B,KAAK,SAAS,EAAE,EAChB,KAAK,SAAS,SAAS,GAAG,SAAS;IAEnC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAEvC,GAAG,EAAE,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACvC,IAAI,EAAE,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IACzC,GAAG,EAAE,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACvC,KAAK,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC3C,MAAM,EAAE,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC7C,IAAI,EAAE,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IACzC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC/C,KAAK,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;gBAE/B,OAAO,EAAE,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC;CAexD"}
|
package/dist/types.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export type MediaType = `${string}/${string}`;
|
|
2
|
-
export type UpperHttpMethod = 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH' | 'TRACE';
|
|
3
|
-
export type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';
|
|
4
|
-
export type ErrorResponse = {
|
|
5
|
-
status?: number;
|
|
6
|
-
message?: string;
|
|
7
|
-
debugMessage?: string;
|
|
8
|
-
};
|
|
9
|
-
export interface PageInfo {
|
|
10
|
-
/**
|
|
11
|
-
* The cursor of the first item in the result set.
|
|
12
|
-
*/
|
|
13
|
-
startCursor?: string;
|
|
14
|
-
/**
|
|
15
|
-
* The cursor of the last item in the result set.
|
|
16
|
-
*/
|
|
17
|
-
endCursor?: string;
|
|
18
|
-
/**
|
|
19
|
-
* Indicates whether there are more items after the current page.
|
|
20
|
-
*/
|
|
21
|
-
hasNextPage: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* The total number of elements across all pages.
|
|
24
|
-
*/
|
|
25
|
-
totalElements: number;
|
|
26
|
-
}
|
|
27
|
-
export type Paginated<T> = {
|
|
28
|
-
data?: T[];
|
|
29
|
-
metadata?: PageInfo;
|
|
30
|
-
};
|
|
31
|
-
export type Edge<T> = {
|
|
32
|
-
node: T;
|
|
33
|
-
cursor: string;
|
|
34
|
-
};
|
|
35
|
-
export type Connection<T> = {
|
|
36
|
-
edges: Edge<T>[];
|
|
37
|
-
pageInfo: PageInfo;
|
|
38
|
-
};
|
|
39
|
-
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;AAE9C,MAAM,MAAM,eAAe,GACxB,KAAK,GACL,KAAK,GACL,MAAM,GACN,QAAQ,GACR,SAAS,GACT,MAAM,GACN,OAAO,GACP,OAAO,CAAC;AAEX,MAAM,MAAM,UAAU,GACnB,KAAK,GACL,KAAK,GACL,MAAM,GACN,QAAQ,GACR,SAAS,GACT,MAAM,GACN,OAAO,GACP,OAAO,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,WAAW,QAAQ;IACxB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,WAAW,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IAC1B,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IACX,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI;IACrB,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC3B,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACnB,CAAC"}
|