@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 CHANGED
@@ -1,18 +1,350 @@
1
1
  # @nxgt/datasource-rest
2
2
 
3
- An Apollo-style REST datasource over `openapi-fetch`: auth forwarding, caching
4
- and error translation, so a GraphQL resolver can call a REST service with the
5
- same typed client the REST apps use.
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
- Errors come back as `CustomException` from `@nxgt/shared-exceptions`, so a
8
- downstream 404 surfaces as a `NotFound` rather than a transport failure.
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
- Public on npmjs; no token needed to install. TypeScript is a peer, pinned to
17
- `^6.0.3` across every `@nxgt/*` package — the set is unsatisfiable if one of
18
- them widens it.
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 * from './helpers';
2
- export * from './middlewares';
3
- export * from './rest-datasource';
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,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/helpers/error-handling.ts
2
- import { CustomException, ErrorCode } from "@nxgt/shared-exceptions";
3
- function errorToException(error, response) {
4
- let status = ErrorCode.BadRequest;
5
- switch (response.status) {
6
- case 401:
7
- status = ErrorCode.Unauthenticated;
8
- break;
9
- case 403:
10
- status = ErrorCode.Forbidden;
11
- break;
12
- case 404:
13
- status = ErrorCode.NotFound;
14
- break;
15
- case 500:
16
- status = ErrorCode.InternalServerError;
17
- break;
18
- default:
19
- status = ErrorCode.BadRequest;
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
- return CustomException.from({
22
- message: error?.message || "An error occurred",
23
- code: status,
24
- options: {},
25
- debugMessage: error?.debugMessage
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/helpers/pagination.utils.ts
29
- function relayPaginate(data) {
30
- const { data: items, metadata } = data;
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
- authMiddleware,
105
- cacheMiddleware,
106
- errorToException,
107
- relayPaginate
150
+ codeOf,
151
+ defaultCache,
152
+ relayPaginate,
153
+ toDataSourceError
108
154
  };
109
155
 
110
- //# debugId=B0900E11F81FD7B064756E2164756E21
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/helpers/error-handling.ts", "../src/helpers/pagination.utils.ts", "../src/middlewares/auth.middleware.ts", "../src/middlewares/cache.middleware.ts", "../src/rest-datasource.ts"],
3
+ "sources": ["../src/datasource/rest-datasource.ts", "../src/errors/data-source-error.ts", "../src/pagination/relay.ts"],
4
4
  "sourcesContent": [
5
- "import { CustomException, ErrorCode } from '@nxgt/shared-exceptions';\n\nexport function errorToException(error: any, response: Response) {\n\tlet status = ErrorCode.BadRequest;\n\tswitch (response.status) {\n\t\tcase 401:\n\t\t\tstatus = ErrorCode.Unauthenticated;\n\t\t\tbreak;\n\t\tcase 403:\n\t\t\tstatus = ErrorCode.Forbidden;\n\t\t\tbreak;\n\t\tcase 404:\n\t\t\tstatus = ErrorCode.NotFound;\n\t\t\tbreak;\n\t\tcase 500:\n\t\t\tstatus = ErrorCode.InternalServerError;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tstatus = ErrorCode.BadRequest;\n\t}\n\n\treturn CustomException.from({\n\t\tmessage: error?.message || 'An error occurred',\n\t\tcode: status,\n\t\toptions: {},\n\t\tdebugMessage: error?.debugMessage,\n\t});\n}\n",
6
- "import type { Connection, Paginated } from '../types';\n\nexport function relayPaginate<T extends { id: string }>(\n\tdata: Paginated<T>,\n): Connection<T> {\n\tconst { data: items, metadata } = data;\n\treturn {\n\t\tedges: (items ?? []).map((item) => ({\n\t\t\tnode: item,\n\t\t\tcursor: item.id,\n\t\t})),\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",
7
- "import type { Middleware } from 'openapi-fetch';\n\nexport type AuthMiddlewareOptions = {\n\ttoken?: string | (() => Promise<string>);\n\tshouldUseToken?: (request: Request) => boolean;\n};\n\nexport const authMiddleware = (options: AuthMiddlewareOptions): Middleware => ({\n\tonRequest({ request }) {\n\t\tif (!options.token) {\n\t\t\treturn;\n\t\t}\n\t\tif (options.shouldUseToken && !options.shouldUseToken(request)) {\n\t\t\treturn;\n\t\t}\n\t\trequest.headers.set(\n\t\t\t'Authorization',\n\t\t\t`Bearer ${typeof options.token === 'function' ? options.token() : options.token}`,\n\t\t);\n\t},\n});\n",
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": "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",
12
- "debugId": "B0900E11F81FD7B064756E2164756E21",
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": "1.0.2",
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-core.git",
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
- "typecheck": "tsc --noEmit"
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
- "@types/lodash": "^4.17.24"
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,3 +0,0 @@
1
- import { CustomException } from '@nxgt/shared-exceptions';
2
- export declare function errorToException(error: any, response: Response): CustomException<any>;
3
- //# sourceMappingURL=error-handling.d.ts.map
@@ -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"}
@@ -1,3 +0,0 @@
1
- export * from './error-handling';
2
- export * from './pagination.utils';
3
- //# sourceMappingURL=index.d.ts.map
@@ -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,5 +0,0 @@
1
- import type { Connection, Paginated } from '../types';
2
- export declare function relayPaginate<T extends {
3
- id: string;
4
- }>(data: Paginated<T>): Connection<T>;
5
- //# sourceMappingURL=pagination.utils.d.ts.map
@@ -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,3 +0,0 @@
1
- export * from './auth.middleware';
2
- export * from './cache.middleware';
3
- //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -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"}