@burojs/hasura 0.2.0 → 0.5.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 +15 -16
- package/dist/index.cjs +912 -772
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +594 -285
- package/dist/index.d.ts +594 -285
- package/dist/index.js +899 -770
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,81 +1,75 @@
|
|
|
1
|
-
import { DataOperationContext, DataProvider,
|
|
2
|
-
|
|
1
|
+
import { DataLiveProvider, DataOperationContext, DataProvider, ErasedResourceHandle, FilterExpression } from "@burojs/core";
|
|
2
|
+
//#region src/data-provider.d.ts
|
|
3
3
|
interface HasuraDataProviderConfig {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* Closed, application-owned custom operation registry. A descriptor selects
|
|
62
|
-
* an entry by its already-declared `{ resource, operation }` binding; it can
|
|
63
|
-
* never supply a GraphQL document, URL, or result mapper itself.
|
|
64
|
-
*/
|
|
65
|
-
operations?: HasuraOperationRegistry;
|
|
66
|
-
fetchImpl?: typeof fetch;
|
|
4
|
+
/** Returns a fresh JWT or null if user is not authed. */
|
|
5
|
+
getToken: () => Promise<string | null>;
|
|
6
|
+
/**
|
|
7
|
+
* GraphQL endpoint every query/mutation is POSTed to, and the endpoint the
|
|
8
|
+
* live provider derives its WebSocket URL from. May be a same-origin path
|
|
9
|
+
* (`/gql/v1/graphql`, `/tandt/gql/v1/graphql`) or an absolute http(s) URL.
|
|
10
|
+
* Defaults to {@link DEFAULT_HASURA_ENDPOINT}.
|
|
11
|
+
*/
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Hasura role sent as `X-Hasura-Role` for resources whose mapping does not
|
|
15
|
+
* declare a `role` of its own. `null` (the default) sends no role header at
|
|
16
|
+
* all, leaving the JWT's own default role in force.
|
|
17
|
+
*
|
|
18
|
+
* A resource mapping's `role` always wins when it is defined — including
|
|
19
|
+
* `role: null`, which suppresses the header for that one resource.
|
|
20
|
+
*
|
|
21
|
+
* `''` is treated as `null`, in both halves of the package: it is the shape
|
|
22
|
+
* of a missing env var, not a role. See {@link normalizeRole}.
|
|
23
|
+
*/
|
|
24
|
+
defaultRole?: string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Revalidates authentication after a request comes back with HTTP **401**.
|
|
27
|
+
* The callback owns any terminal sign-out; returning true permits one retry
|
|
28
|
+
* with a newly obtained token, while false preserves the original 401
|
|
29
|
+
* response.
|
|
30
|
+
*
|
|
31
|
+
* HTTP 401 is what a proxy or gateway in front of Hasura answers. Hasura
|
|
32
|
+
* itself rejects a credential with **HTTP 200** and an `invalid-jwt` errors
|
|
33
|
+
* envelope (`packages/mock-kit/fixtures/hasura/error-unauthorized.json`,
|
|
34
|
+
* captured from a live backend). This provider reports that envelope as
|
|
35
|
+
* `{ kind: 'auth', code: 'invalid-jwt' }`. When the app has an
|
|
36
|
+
* `authProvider`, the runtime hands it that error; if the provider chooses
|
|
37
|
+
* to refresh, a read is replayed once, and a mutation once when it is
|
|
38
|
+
* replay-safe. `recoverAuth` is
|
|
39
|
+
* not consulted for the envelope, so one rejection is never refreshed twice.
|
|
40
|
+
* Pinned by `tests/fixture-fidelity.test.ts`'s `error-unauthorized` case.
|
|
41
|
+
*/
|
|
42
|
+
recoverAuth?: () => Promise<boolean>;
|
|
43
|
+
/** Map our resource name → table + field selection. */
|
|
44
|
+
resources: Record<string, ResourceMapping>;
|
|
45
|
+
/**
|
|
46
|
+
* Optional global scope read on every getList. Returns key → value pairs
|
|
47
|
+
* that get merged into the where clause as `{ <key>: { _eq: <value> } }`,
|
|
48
|
+
* but only for resources that whitelist the key in `scopeFields`.
|
|
49
|
+
*
|
|
50
|
+
* Example: `() => ({ pg: 'radio', location_id: 5 })` — applied to documents
|
|
51
|
+
* (which have both fields) but not products (which only have pg).
|
|
52
|
+
*/
|
|
53
|
+
getScope?: (context: DataOperationContext) => Record<string, unknown>;
|
|
54
|
+
/**
|
|
55
|
+
* Closed, application-owned custom operation registry. A descriptor selects
|
|
56
|
+
* an entry by its already-declared `{ resource, operation }` binding; it can
|
|
57
|
+
* never supply a GraphQL document, URL, or result mapper itself.
|
|
58
|
+
*/
|
|
59
|
+
operations?: HasuraOperationRegistry;
|
|
60
|
+
fetchImpl?: typeof fetch;
|
|
67
61
|
}
|
|
68
62
|
interface HasuraCustomOperation {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
63
|
+
/** Which native provider slot may execute this declaration. */
|
|
64
|
+
readonly kind: 'query' | 'mutation';
|
|
65
|
+
/** Static GraphQL document owned by the application/provider integration. */
|
|
66
|
+
readonly document: string;
|
|
67
|
+
/** Maps the declared operation input to the document's variables. */
|
|
68
|
+
readonly variables: (input: unknown) => Record<string, unknown> | undefined;
|
|
69
|
+
/** Maps the full GraphQL response to the declared operation output. */
|
|
70
|
+
readonly mapResult?: (data: unknown) => unknown;
|
|
71
|
+
/** Overrides the mapped resource role; undefined keeps that mapped role. */
|
|
72
|
+
readonly role?: string | null;
|
|
79
73
|
}
|
|
80
74
|
/**
|
|
81
75
|
* Registry entries are keyed by the resource binding first, then the declared
|
|
@@ -84,183 +78,251 @@ interface HasuraCustomOperation {
|
|
|
84
78
|
*/
|
|
85
79
|
type HasuraOperationRegistry = Readonly<Record<string, Readonly<Record<string, HasuraCustomOperation>>>>;
|
|
86
80
|
interface ResourceMapping {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
81
|
+
table: string;
|
|
82
|
+
selectFields: string[];
|
|
83
|
+
/**
|
|
84
|
+
* GraphQL scalar name for this table's primary key, interpolated into the
|
|
85
|
+
* `$id` variable declaration of every by-pk query/mutation. Default
|
|
86
|
+
* `'bigint'`.
|
|
87
|
+
*
|
|
88
|
+
* DISTILLATION NOTE (divergence 3 of 7 — feature, additive, l2-web-ui only).
|
|
89
|
+
* l2-web-ui added `'String'` for tables whose pk is a genuine string
|
|
90
|
+
* business key (a GS1 marking code), not a uuid/Int/bigint
|
|
91
|
+
* wire type: Hasura's by-pk root for such a table is literally
|
|
92
|
+
* `<table>_by_pk(<pk>: $id)` with `$id: String!`, which none of the other
|
|
93
|
+
* three names can express (playground's union stopped at
|
|
94
|
+
* `'bigint' | 'uuid' | 'Int'`). Purely a type-union widening — no runtime
|
|
95
|
+
* branch reads `pkType` beyond interpolating it — so l2-web-ui's wider union
|
|
96
|
+
* is taken wholesale rather than forked.
|
|
97
|
+
*/
|
|
98
|
+
pkType?: 'bigint' | 'uuid' | 'Int' | 'String';
|
|
99
|
+
/**
|
|
100
|
+
* Skip the aggregate on every `getList`. This is a COST decision, not a
|
|
101
|
+
* schema fact: the table has an `_aggregate` root but counting it on each
|
|
102
|
+
* page is too slow to pay for unprompted. (Measured in one of the apps this
|
|
103
|
+
* provider was distilled from: ~9.8M-row and ~28.9M-row tables, the latter
|
|
104
|
+
* taking ~55 s per count.) `total` is then omitted and the list renders the
|
|
105
|
+
* unknown-total shape.
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ NOT the same thing as {@link ResourceMapping.aggregate} `: false`, and
|
|
108
|
+
* NOT a proxy for it — most `noAggregate` tables CAN be counted on demand,
|
|
109
|
+
* which is exactly what `getCount` is for. Deriving one from the other gets
|
|
110
|
+
* the flagship resources backwards.
|
|
111
|
+
*/
|
|
112
|
+
noAggregate?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Which aggregate root field backs this table, for the on-demand `getCount`
|
|
115
|
+
* (and for `getList`'s own count when it runs):
|
|
116
|
+
* - absent — derive `${table}_aggregate` (the Hasura default; today's behaviour);
|
|
117
|
+
* - `false` — no aggregate root exists at all, under any name. `getCount`
|
|
118
|
+
* answers `{ total: null }` without a round trip and the UI drops the
|
|
119
|
+
* affordance for the session;
|
|
120
|
+
* - a string — an explicitly-named root, for tables whose custom root fields
|
|
121
|
+
* break the derivation.
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ Establish this by introspecting the live schema under the role the app
|
|
124
|
+
* actually runs as, not by reading `noAggregate` or a code comment. Both
|
|
125
|
+
* have been wrong here.
|
|
126
|
+
*/
|
|
127
|
+
aggregate?: false | string;
|
|
128
|
+
/**
|
|
129
|
+
* Root field for a single-row fetch by primary key. Defaults to the derived
|
|
130
|
+
* `${table}_by_pk`. Set `false` when the active role exposes only the
|
|
131
|
+
* collection root; `getOne` then queries it with pk `where` + `limit: 1`.
|
|
132
|
+
*
|
|
133
|
+
* ⚠️ Same class of fact as `aggregate`, and established the same way — by
|
|
134
|
+
* introspecting the live schema, not by reading a comment. In one of the
|
|
135
|
+
* source apps exactly one table of forty had a non-derivable by-pk root (a
|
|
136
|
+
* plural table whose root was singular), so the derived name 400'd and its
|
|
137
|
+
* Show page was a dead-end error reachable in two clicks from the sidebar.
|
|
138
|
+
*/
|
|
139
|
+
byPkRoot?: string | false;
|
|
140
|
+
/** Field to order by in getList. Defaults to 'id'. */
|
|
141
|
+
orderByField?: string;
|
|
142
|
+
/** Name of the primary-key field in the row (for synthetic id in list rows). Defaults to 'id'. */
|
|
143
|
+
pkField?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Hasura role for this resource. `null` (or `''`) means no role header (use
|
|
146
|
+
* the JWT's default role). Undefined falls back to
|
|
147
|
+
* {@link HasuraDataProviderConfig.defaultRole}.
|
|
148
|
+
*/
|
|
149
|
+
role?: string | null;
|
|
150
|
+
/**
|
|
151
|
+
* Columns typed `int64` in a Hasura ClickHouse source. Hasura serialises
|
|
152
|
+
* int64 as a JSON STRING and REJECTS a numeric filter value with
|
|
153
|
+
* «expected a string for type 'int64'» — so a `document_id` filter sent as a
|
|
154
|
+
* number matched nothing and the affected list tabs came back empty.
|
|
155
|
+
* Filter values for these fields are coerced to strings before the where
|
|
156
|
+
* clause is built. (Verified live: numeric/int literal → error, string → rows.)
|
|
157
|
+
*/
|
|
158
|
+
int64Fields?: string[];
|
|
159
|
+
/**
|
|
160
|
+
* Whitelist of scope keys this table actually has columns for. Only these
|
|
161
|
+
* keys from `getScope()` get injected into where clauses. Defaults to `[]`
|
|
162
|
+
* (no scope injection — useful for tables that lack the scope columns).
|
|
163
|
+
*
|
|
164
|
+
* ⚠️ SCOPE NARROWS THREE READ PATHS, NOT FIVE. `scopeToWhere` is reached
|
|
165
|
+
* only through {@link composeListWhere}, i.e. from `queries.list` (both
|
|
166
|
+
* branches), `queries.count` and `queries.many`. **`queries.one` composes
|
|
167
|
+
* `{pk _eq id} ∧ fixedFilter ∧ localeSlice` and carries NO scope**, so a
|
|
168
|
+
* record excluded from a scoped list is still readable by its id through
|
|
169
|
+
* this provider. `fixedFilter` — a slice that cannot depend on the session —
|
|
170
|
+
* does cover `one`; the session-dependent pair (`getScope` + `scopeFields`)
|
|
171
|
+
* does not. Treat a scoped projection as a LIST narrowing, and do not rely
|
|
172
|
+
* on it as an authorization boundary: that belongs in the backend's own row
|
|
173
|
+
* permissions, which is where a real Hasura deployment enforces it. Raised
|
|
174
|
+
* by the SP4 final review (M3); closing it is a change to the by-pk path
|
|
175
|
+
* with its own blast radius and is carried as a follow-up rather than made
|
|
176
|
+
* here.
|
|
177
|
+
*/
|
|
178
|
+
scopeFields?: string[];
|
|
179
|
+
/**
|
|
180
|
+
* JSONB-spill routing for tables whose forms submit a flat values record but
|
|
181
|
+
* whose real columns nest most of those keys inside JSONB blobs (e.g. a
|
|
182
|
+
* `documents` table where template head fields live inside `head`, a couple
|
|
183
|
+
* live inside `meta`, and only a handful — type/status/… — are real columns).
|
|
184
|
+
*
|
|
185
|
+
* When set, create/update partition the incoming flat `variables`:
|
|
186
|
+
* - keys that ARE real columns (present in `selectFields`) stay top-level;
|
|
187
|
+
* - keys listed in `metaKeys` nest under `metaColumn`;
|
|
188
|
+
* - every OTHER key nests under `column` (the head spill).
|
|
189
|
+
* getOne does the inverse — it spreads the fetched `column`/`metaColumn`
|
|
190
|
+
* objects back onto the flat record so edit forms / show pages read those
|
|
191
|
+
* keys as flat fields (the raw blobs are kept too).
|
|
192
|
+
*
|
|
193
|
+
* SCOPE: scalar / relation / enum head+meta fields only. Data-body arrays
|
|
194
|
+
* (children) live in a sibling table — see `dataBody`.
|
|
195
|
+
*/
|
|
196
|
+
jsonbSpill?: JsonbSpillConfig;
|
|
197
|
+
/**
|
|
198
|
+
* Data-body routing for resources whose heavy array payload lives in a
|
|
199
|
+
* SEPARATE 1:1 sibling table rather than a JSONB column on the row itself
|
|
200
|
+
* (e.g. a `documents.items[]` / `products[]` payload living in
|
|
201
|
+
* `documents_data.data`, keyed by `document_id`).
|
|
202
|
+
*
|
|
203
|
+
* When set, create/update peel the `keys` off the flat `variables` FIRST
|
|
204
|
+
* (before `jsonbSpill` runs, so they never leak into `head`), write the parent
|
|
205
|
+
* row, then upsert the sibling row (`insert_<table>_one … on_conflict`). getOne
|
|
206
|
+
* nested-selects the sibling via `relationship` and spreads its `column` blob
|
|
207
|
+
* back onto the flat record (so edit forms read `items` as a top-level field),
|
|
208
|
+
* dropping the raw relationship object so it can't round-trip into `head`.
|
|
209
|
+
* getList is left untouched — list columns are head/meta scalars, never the
|
|
210
|
+
* heavy data body.
|
|
211
|
+
*/
|
|
212
|
+
dataBody?: DataBodyConfig;
|
|
213
|
+
/**
|
|
214
|
+
* Fixed, always-on Hasura `where` fragment ANDed into every getList /
|
|
215
|
+
* getOne query for this resource. Powers "per-type" / projection resources:
|
|
216
|
+
* several resource names can map to the SAME table, each scoped to a
|
|
217
|
+
* different slice — e.g. `fixedFilter: { type: { _eq: 'work_order' } }` turns
|
|
218
|
+
* a `work_orders` resource into a work-orders-only view of a shared
|
|
219
|
+
* `documents` table. The fragment is a raw Hasura `<table>_bool_exp` object
|
|
220
|
+
* (same shape `filtersToWhere` emits), so nested / `_and` / `_or` clauses are
|
|
221
|
+
* all valid. Merged with `mergeWhere` (AND) after user filters + scope, so
|
|
222
|
+
* the user can still filter/search WITHIN the fixed slice.
|
|
223
|
+
*
|
|
224
|
+
* getOne: because `<table>_by_pk` takes no `where`, a resource with a
|
|
225
|
+
* `fixedFilter` fetches its single row via a `where`-based `limit: 1` query
|
|
226
|
+
* (pk `_eq` id AND fixedFilter) instead — so the row is only returned when it
|
|
227
|
+
* actually belongs to the slice (a work-order id won't resolve under
|
|
228
|
+
* `movements`). Resources without a `fixedFilter` keep the `_by_pk` path.
|
|
229
|
+
*/
|
|
230
|
+
fixedFilter?: Record<string, unknown>;
|
|
231
|
+
/**
|
|
232
|
+
* The column on this table that holds a row's LANGUAGE, for a backend that
|
|
233
|
+
* translates by storing one row per locale (the "sidecar translation table"
|
|
234
|
+
* shape: `catalog_product_translation(product_id, locale, name, …)`).
|
|
235
|
+
*
|
|
236
|
+
* When set, every read handler of this resource ANDs `{ <localeField>: {
|
|
237
|
+
* _eq: <descriptor.locale> } }` into its `where` — `queries.list` (both the
|
|
238
|
+
* offset and the cursor branch), `queries.count`, `queries.many` and the
|
|
239
|
+
* by-pk `queries.one` alike — so the caller gets exactly the active
|
|
240
|
+
* language's rows.
|
|
241
|
+
*
|
|
242
|
+
* ⚠️ EXACTLY FOUR HANDLERS, not "every read anywhere in this module", and
|
|
243
|
+
* the difference is worth stating because an earlier revision of this
|
|
244
|
+
* sentence was broader than the code. Two reads are deliberately outside it:
|
|
245
|
+
*
|
|
246
|
+
* - `readMutationRecord` — the re-read a mutation performs to return the
|
|
247
|
+
* row it just wrote. It addresses ONE row the caller has just named by
|
|
248
|
+
* primary key, and per the write rule below the caller supplied its
|
|
249
|
+
* `locale` themselves. Narrowing it by the UI's active language could
|
|
250
|
+
* only turn a successful write into a `null` result for a row the caller
|
|
251
|
+
* is holding in its hand.
|
|
252
|
+
* - `queries.many` with an EMPTY id set — see the note at its early
|
|
253
|
+
* return; it refuses eagerly (`localeWhere` is called before the empty
|
|
254
|
+
* read short-circuits) precisely so this list does not need a third
|
|
255
|
+
* exception.
|
|
256
|
+
* `descriptor.locale` is put there by `@burojs/core`'s
|
|
257
|
+
* `resolveDescriptorLocale`, which fills it ONLY for a resource declaring
|
|
258
|
+
* `data: { localeVarying: true }`.
|
|
259
|
+
*
|
|
260
|
+
* ⚠️ THE TWO DECLARATIONS MUST AGREE, and this provider REFUSES rather than
|
|
261
|
+
* guessing when they do not. A mapping with a `localeField` whose descriptor
|
|
262
|
+
* carries no locale means the resource forgot `localeVarying`, and the
|
|
263
|
+
* consequence is not cosmetic: the query returns EVERY language's rows —
|
|
264
|
+
* three rows per product, in mixed languages, with duplicate business keys —
|
|
265
|
+
* which is a plausible-looking wrong answer, the defect class this package
|
|
266
|
+
* refuses on four other paths (cursor orderability, unsupported operators,
|
|
267
|
+
* unmapped resources, absent aggregate roots). So the read throws
|
|
268
|
+
* {@link LOCALE_REQUIRED_CODE} naming both halves of the fix.
|
|
269
|
+
*
|
|
270
|
+
* ⚠️ WRITES ARE NOT TOUCHED. `create`/`update`/`delete` do not inject the
|
|
271
|
+
* locale: a mutation on a translation row must name its own `locale` in the
|
|
272
|
+
* submitted values (it is a real column), and a by-pk update addresses one
|
|
273
|
+
* row by its own primary key, which already identifies the language. Silently
|
|
274
|
+
* stamping the UI's active language onto a write would be a different and
|
|
275
|
+
* much worse guess than the one refused above.
|
|
276
|
+
*
|
|
277
|
+
* Why a column name here rather than a Hasura ARGUMENT: this provider selects
|
|
278
|
+
* FLAT COLUMNS (see `selectFields`), and `ARRAY_RELATION_ARGS` carries no
|
|
279
|
+
* nested `where`, so a translated value cannot be pulled through a nested
|
|
280
|
+
* relation selection on the parent row at all. Reading the translation table
|
|
281
|
+
* as its own root query, narrowed by locale, is the access path the schema
|
|
282
|
+
* shape actually supports.
|
|
283
|
+
*/
|
|
284
|
+
localeField?: string;
|
|
285
|
+
/**
|
|
286
|
+
* Opt this resource into real-time updates. The live provider then opens a
|
|
287
|
+
* cheap change-signal subscription for its list / single-record views and
|
|
288
|
+
* invalidates matching native runtime queries when the table changes.
|
|
289
|
+
*
|
|
290
|
+
* Off by default on purpose: every live resource holds an open server-side
|
|
291
|
+
* subscription, so it is a cost you opt into for data that actually moves
|
|
292
|
+
* (documents change constantly during processing; a country lookup does not).
|
|
293
|
+
*/
|
|
294
|
+
live?: boolean;
|
|
233
295
|
}
|
|
234
296
|
/** @see ResourceMapping.jsonbSpill */
|
|
235
297
|
interface JsonbSpillConfig {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
/** JSONB column that catches every non-base, non-meta key (e.g. `head`). */
|
|
299
|
+
column: string;
|
|
300
|
+
/** JSONB column that catches the `metaKeys` (e.g. `meta`). */
|
|
301
|
+
metaColumn: string;
|
|
302
|
+
/** Flat keys routed to `metaColumn` instead of the `column` spill. */
|
|
303
|
+
metaKeys: string[];
|
|
304
|
+
/**
|
|
305
|
+
* Flat form key → dotted NESTED path inside the head `column`. Lets a flat
|
|
306
|
+
* field (e.g. `vsd_number`) persist to a nested head location (e.g.
|
|
307
|
+
* `regulator_data.crpt.vsd_number`) without dotted field keys. getOne reads
|
|
308
|
+
* the nested value back out to the flat key.
|
|
309
|
+
*/
|
|
310
|
+
nestedKeys?: Record<string, string>;
|
|
249
311
|
}
|
|
250
312
|
/** @see ResourceMapping.dataBody */
|
|
251
313
|
interface DataBodyConfig {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
314
|
+
/** Sibling table holding the heavy data body, 1:1 with the parent (e.g. `documents_data`). */
|
|
315
|
+
table: string;
|
|
316
|
+
/** FK column on the sibling pointing back at the parent pk (e.g. `document_id`). */
|
|
317
|
+
fk: string;
|
|
318
|
+
/** JSONB column on the sibling that holds the whole data body (e.g. `data`). */
|
|
319
|
+
column: string;
|
|
320
|
+
/** Reverse object-relationship name on the PARENT row → sibling, for the getOne nested select (e.g. `document_data`). */
|
|
321
|
+
relationship: string;
|
|
322
|
+
/** `on_conflict` constraint enum for the upsert (e.g. `document_data_pkey`). */
|
|
323
|
+
constraint: string;
|
|
324
|
+
/** Flat value keys that belong to the data body — routed to the sibling `column` (e.g. `['items', 'products']`). */
|
|
325
|
+
keys: string[];
|
|
264
326
|
}
|
|
265
327
|
/**
|
|
266
328
|
* Split a flat `values` record for a data-body resource into the sibling-table
|
|
@@ -277,9 +339,9 @@ interface DataBodyConfig {
|
|
|
277
339
|
* Exported for unit testing; the provider calls it with `m.dataBody`.
|
|
278
340
|
*/
|
|
279
341
|
declare function partitionDataBody(values: Record<string, unknown>, cfg: DataBodyConfig): {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
342
|
+
dataBody: Record<string, unknown>;
|
|
343
|
+
rest: Record<string, unknown>;
|
|
344
|
+
hasDataBody: boolean;
|
|
283
345
|
};
|
|
284
346
|
/**
|
|
285
347
|
* Partition a flat `values` record for a jsonb-spill resource.
|
|
@@ -329,6 +391,49 @@ declare function flattenJsonbSpill(item: unknown, spill: JsonbSpillConfig): unkn
|
|
|
329
391
|
* stable symbol.
|
|
330
392
|
*/
|
|
331
393
|
declare const NOT_AUTHENTICATED_CODE = "not-authenticated";
|
|
394
|
+
/**
|
|
395
|
+
* The envelope codes with which Hasura reports a failure of work it ran OUTSIDE its own
|
|
396
|
+
* transaction — an Action's webhook or a remote schema — on a custom mutation. Such a side effect
|
|
397
|
+
* may have happened before the failure, so a custom mutation answered with one of them is
|
|
398
|
+
* `sent` (spec 4.7, P4-20). A table mutation keeps `protocol/graphql-error` for them: Hasura
|
|
399
|
+
* rolled its transaction back.
|
|
400
|
+
*
|
|
401
|
+
* Spellings confirmed against the Hasura GraphQL Engine v2 source, `instance ToJSON Code` in
|
|
402
|
+
* `server/lib/hasura-base/src/Hasura/Base/Error.hs` (hasura/graphql-engine@0a1dd53b):
|
|
403
|
+
* `Unexpected -> "unexpected"`, `RemoteSchemaError -> "remote-schema-error"`,
|
|
404
|
+
* `ParseFailed -> "parse-failed"`. Action failures are raised in
|
|
405
|
+
* `server/src-lib/Hasura/GraphQL/Execute/Action.hs`: an HTTP exception calling the webhook, a
|
|
406
|
+
* webhook 5xx, an unparsable webhook answer and a failed request or response transform are all
|
|
407
|
+
* `throw500…`, i.e. `unexpected`; a webhook that answered 2xx (its side effect done) with JSON
|
|
408
|
+
* that does not decode fails in `decodeValue` (`Action.hs:793`), which raises `ParseFailed`
|
|
409
|
+
* through `liftIResult` (`Error.hs:424`), i.e. `parse-failed`. Hasura has no
|
|
410
|
+
* `action-webhook-error` or `http-exception` code; a webhook's own 4xx answer carries the code
|
|
411
|
+
* the webhook chose (`ActionWebhookCode`), or `unexpected` when it chose none, and only that
|
|
412
|
+
* last case is caught here — a webhook-chosen code is the handler's own definite answer.
|
|
413
|
+
*
|
|
414
|
+
* Hasura gives no marker telling an Action-backed or remote-schema mutation apart from any
|
|
415
|
+
* other registered `operations` document, so every custom mutation is treated this way
|
|
416
|
+
* (fail-safe: a wrongly `sent` answer costs a "may have been applied" screen, never a duplicate).
|
|
417
|
+
*/
|
|
418
|
+
declare const HASURA_EXTERNAL_FAILURE_CODES: ReadonlySet<string>;
|
|
419
|
+
/**
|
|
420
|
+
* Refusal code for "this mapping declares a `localeField` but the descriptor
|
|
421
|
+
* carries no locale". Exported so an application can match a symbol rather
|
|
422
|
+
* than a string — same treatment as {@link FILTER_OPERATOR_UNSUPPORTED_CODE}.
|
|
423
|
+
*
|
|
424
|
+
* @see ResourceMapping.localeField for why this is a refusal and not a default.
|
|
425
|
+
*/
|
|
426
|
+
declare const LOCALE_REQUIRED_CODE = "locale-required";
|
|
427
|
+
/**
|
|
428
|
+
* The locale conjunct for a mapping that declares one, or `undefined` for a
|
|
429
|
+
* mapping that does not.
|
|
430
|
+
*
|
|
431
|
+
* The refusal is EAGER — it fires before any request leaves, so it does not
|
|
432
|
+
* depend on how many rows the table happens to hold. (Task 1 of this
|
|
433
|
+
* sub-project recorded the opposite mistake: an absence assertion that stayed
|
|
434
|
+
* green because a LAZY check downstream threw for its own reasons.)
|
|
435
|
+
*/
|
|
436
|
+
declare function localeWhere(m: ResourceMapping, locale: string | undefined): Record<string, unknown> | undefined;
|
|
332
437
|
/**
|
|
333
438
|
* The shared refusal code for "this provider cannot express that filter
|
|
334
439
|
* operator". `@burojs/pocketbase` throws the SAME `kind` and the SAME code, so
|
|
@@ -348,6 +453,13 @@ declare function scopeToWhere(scope: Record<string, unknown>, scopeFields: strin
|
|
|
348
453
|
declare function mergeWhere(a: Record<string, unknown> | undefined, b: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
349
454
|
/** Translate the native filter AST into a Hasura bool expression. */
|
|
350
455
|
declare function filtersToWhere(filter?: FilterExpression<ErasedResourceHandle>): Record<string, unknown> | undefined;
|
|
456
|
+
/**
|
|
457
|
+
* Exported for its own test: bigint coercion matches the WHOLE field string,
|
|
458
|
+
* so a nested predicate needs its full path declared in `int64Fields`.
|
|
459
|
+
* Matching the leaf would coerce a same-named column on any other table the
|
|
460
|
+
* path happens to traverse.
|
|
461
|
+
*/
|
|
462
|
+
declare function coerceInt64Filter(filter: FilterExpression<ErasedResourceHandle> | undefined, fields: readonly string[] | undefined): FilterExpression<ErasedResourceHandle> | undefined;
|
|
351
463
|
/**
|
|
352
464
|
* Resolve the Hasura aggregate root field for a mapping, or `null` when the
|
|
353
465
|
* registry declares that none exists. Registry-driven on purpose: which tables
|
|
@@ -363,7 +475,202 @@ declare function aggregateRoot(m: ResourceMapping): string | null;
|
|
|
363
475
|
*/
|
|
364
476
|
declare function byPkRoot(m: ResourceMapping): string | null;
|
|
365
477
|
declare function createHasuraDataProvider(config: HasuraDataProviderConfig): DataProvider;
|
|
366
|
-
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/cursor.d.ts
|
|
480
|
+
/**
|
|
481
|
+
* Keyset ("seek") cursor pagination for Hasura.
|
|
482
|
+
*
|
|
483
|
+
* ── What a cursor IS here, and why it is synthesised ──────────────────────
|
|
484
|
+
*
|
|
485
|
+
* Hasura issues no opaque cursor tokens of its own — there is no `endCursor`
|
|
486
|
+
* to echo back, no server-side scroll state, nothing. A Relay-style
|
|
487
|
+
* `pageInfo.endCursor` on a Hasura root is a Relay-connection feature of
|
|
488
|
+
* *other* GraphQL servers; the roots this provider talks to take
|
|
489
|
+
* `limit`/`offset`/`order_by`/`where` and nothing else.
|
|
490
|
+
*
|
|
491
|
+
* So the cursor is BUILT by this provider out of the only durable thing a
|
|
492
|
+
* page leaves behind: the ordering values of its last row. The next page is
|
|
493
|
+
* then "every row that sorts strictly after that row", expressed as an
|
|
494
|
+
* ordinary `where` — which is why this needs no backend feature at all and
|
|
495
|
+
* works against any Hasura table.
|
|
496
|
+
*
|
|
497
|
+
* It is nevertheless **opaque above the provider**. Nothing outside this
|
|
498
|
+
* package parses, constructs, or compares a cursor: callers receive
|
|
499
|
+
* `pageInfo.nextCursor` and hand it back as `pagination.after`. The encoding
|
|
500
|
+
* below (base64url of a small JSON envelope) exists to make that boundary
|
|
501
|
+
* obvious at a glance, not to hide anything — a decoded cursor is inert
|
|
502
|
+
* data, and the provider re-validates every field of it before it can shape
|
|
503
|
+
* a query.
|
|
504
|
+
*
|
|
505
|
+
* ── Why a total order is mandatory, not a nicety ──────────────────────────
|
|
506
|
+
*
|
|
507
|
+
* "The rows after this one" is only well defined if the ordering is TOTAL —
|
|
508
|
+
* i.e. no two rows compare equal. Order a product list by `price_cents` and
|
|
509
|
+
* seven rows share the value 1999: a page ending inside that run leaves the
|
|
510
|
+
* provider unable to say which of the seven it stopped at. `price_cents >
|
|
511
|
+
* 1999` SKIPS the rest of the run; `price_cents >= 1999` REPEATS the ones
|
|
512
|
+
* already shown. Both answers look perfectly plausible on screen, which is
|
|
513
|
+
* what makes the bug expensive.
|
|
514
|
+
*
|
|
515
|
+
* The fix is the standard one: append the primary key as a final
|
|
516
|
+
* tie-breaker, and compare rows lexicographically over the whole ordering
|
|
517
|
+
* (see {@link keysetWhere}). The pk is unique, so ordering columns + pk is
|
|
518
|
+
* always total.
|
|
519
|
+
*
|
|
520
|
+
* ── Why this module refuses instead of falling back ───────────────────────
|
|
521
|
+
*
|
|
522
|
+
* Every refusal below is a case where the provider cannot express the
|
|
523
|
+
* request faithfully. None of them degrades to offset pagination, and that
|
|
524
|
+
* is deliberate: converting "not expressible" into a plausible wrong answer
|
|
525
|
+
* is the single defect class this codebase has caught most often (a
|
|
526
|
+
* branchless `_or: []` matching every row; `OP_MAP[op] ?? '_eq'` turning an
|
|
527
|
+
* unknown operator into equality). A cursor list silently served as offset
|
|
528
|
+
* would duplicate and skip rows under concurrent writes and would never
|
|
529
|
+
* report that it had done so.
|
|
530
|
+
*/
|
|
531
|
+
/** One column of the effective ordering, after the tie-breaker has been applied. */
|
|
532
|
+
interface CursorOrderingColumn {
|
|
533
|
+
readonly field: string;
|
|
534
|
+
readonly direction: 'asc' | 'desc';
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Every refusal this module can raise, as a frozen record so a consumer
|
|
538
|
+
* matches a symbol rather than re-typing a string literal — the same reason
|
|
539
|
+
* `NOT_AUTHENTICATED_CODE` and `FILTER_OPERATOR_UNSUPPORTED_CODE` are
|
|
540
|
+
* exported.
|
|
541
|
+
*
|
|
542
|
+
* `cursor-pagination-unsupported`, the blanket refusal this package threw
|
|
543
|
+
* before cursors existed, is GONE — nothing throws it any more. A consumer
|
|
544
|
+
* that matched on it was matching "this provider has no cursors at all",
|
|
545
|
+
* which is no longer true; the honest replacements are these six, each
|
|
546
|
+
* naming a specific request the provider still cannot serve.
|
|
547
|
+
*/
|
|
548
|
+
declare const HASURA_CURSOR_CODES: Readonly<{
|
|
549
|
+
/** The effective ordering cannot be made total — see {@link assertCursorOrderable}. */
|
|
550
|
+
readonly unorderable: "cursor-unorderable";
|
|
551
|
+
/** `pagination.before` was supplied; only forward paging is implemented. */
|
|
552
|
+
readonly backward: "cursor-backward-unsupported";
|
|
553
|
+
/** `pagination.limit` is not a positive integer. */
|
|
554
|
+
readonly limit: "cursor-limit-invalid";
|
|
555
|
+
/** The `after` string is not a cursor this provider minted. */
|
|
556
|
+
readonly malformed: "cursor-malformed";
|
|
557
|
+
/** The cursor was minted under a different ordering than this request asks for. */
|
|
558
|
+
readonly mismatch: "cursor-ordering-mismatch";
|
|
559
|
+
/** The boundary row's ordering value is NULL, so no keyset predicate can address it. */
|
|
560
|
+
readonly boundaryNull: "cursor-boundary-null";
|
|
561
|
+
}>;
|
|
562
|
+
/**
|
|
563
|
+
* The ordering a cursor was minted under, as a single comparable string.
|
|
564
|
+
* Recorded inside the cursor and re-checked on the way back in, so a cursor
|
|
565
|
+
* taken from a list sorted by price cannot be replayed against the same list
|
|
566
|
+
* sorted by name: the keyset predicate would be built from the wrong columns
|
|
567
|
+
* and the resulting page would be arbitrary rather than wrong-looking.
|
|
568
|
+
*/
|
|
569
|
+
declare function orderingSignature(ordering: readonly CursorOrderingColumn[]): string;
|
|
570
|
+
/**
|
|
571
|
+
* The ordering this provider will actually emit for a cursor page: the
|
|
572
|
+
* caller's sort (or the mapping's default when there is none), with the
|
|
573
|
+
* primary key appended as the tie-breaker unless the caller already sorts by
|
|
574
|
+
* it.
|
|
575
|
+
*
|
|
576
|
+
* The tie-breaker inherits the direction of the last ordering column rather
|
|
577
|
+
* than being pinned to `asc`. Either choice yields a total order; inheriting
|
|
578
|
+
* keeps a `desc` list reading in one direction all the way down instead of
|
|
579
|
+
* flipping inside each run of equal values, which is what a reader would
|
|
580
|
+
* expect from "newest first, ties broken by newest id".
|
|
581
|
+
*/
|
|
582
|
+
declare function effectiveCursorOrdering(sort: readonly {
|
|
583
|
+
readonly field: string;
|
|
584
|
+
readonly direction: 'asc' | 'desc';
|
|
585
|
+
}[] | undefined, mapping: {
|
|
586
|
+
readonly pkField?: string;
|
|
587
|
+
readonly orderByField?: string;
|
|
588
|
+
}): readonly CursorOrderingColumn[];
|
|
589
|
+
/**
|
|
590
|
+
* REFUSAL 1 — the ordering cannot produce a total order this provider can
|
|
591
|
+
* both emit and read back.
|
|
592
|
+
*
|
|
593
|
+
* Two conditions, both checked EAGERLY (before any request leaves the
|
|
594
|
+
* client), because a refusal that depends on how many rows happen to be in
|
|
595
|
+
* the table is not a refusal — it is a coin flip. That lesson is recorded in
|
|
596
|
+
* `@burojs/mock-kit`: an unsupported operator checked lazily answered `[]`
|
|
597
|
+
* for an empty table and threw for a populated one.
|
|
598
|
+
*
|
|
599
|
+
* 1. An ordering column is not in `selectFields`. The cursor is made of the
|
|
600
|
+
* boundary row's ordering values, and a column the query does not select
|
|
601
|
+
* is not in the row that comes back. This is also how the pk tie-breaker
|
|
602
|
+
* is enforced: a mapping that does not select its own primary key cannot
|
|
603
|
+
* be cursor-paginated at all.
|
|
604
|
+
* 2. A sort entry carries `nulls: 'first' | 'last'`. Hasura expresses that
|
|
605
|
+
* as `asc_nulls_first`/`desc_nulls_last`, and a keyset predicate over a
|
|
606
|
+
* nullable column under those semantics needs `IS NULL` branches this
|
|
607
|
+
* provider does not build. The OFFSET path ignores `nulls` outright —
|
|
608
|
+
* which is a real (pre-existing) infidelity there, but harmless in the
|
|
609
|
+
* sense that offset paging stays self-consistent. Ignoring it here would
|
|
610
|
+
* not: pages would be cut at boundaries the emitted `order_by` does not
|
|
611
|
+
* actually produce, so rows would be skipped.
|
|
612
|
+
*/
|
|
613
|
+
declare function assertCursorOrderable(ordering: readonly CursorOrderingColumn[], sort: readonly {
|
|
614
|
+
readonly field: string;
|
|
615
|
+
readonly nulls?: 'first' | 'last';
|
|
616
|
+
}[] | undefined, mapping: {
|
|
617
|
+
readonly table: string;
|
|
618
|
+
readonly selectFields: readonly string[];
|
|
619
|
+
}): void;
|
|
620
|
+
/** Positive-integer page size. A cursor page has no other way to bound itself. */
|
|
621
|
+
declare function assertCursorLimit(limit: unknown, table: string): asserts limit is number;
|
|
622
|
+
/**
|
|
623
|
+
* Mints the cursor that addresses the row AFTER `row`.
|
|
624
|
+
*
|
|
625
|
+
* REFUSAL 2 — a NULL (or absent) ordering value on the boundary row. A
|
|
626
|
+
* keyset predicate compares with `_gt`/`_lt`, and in SQL every comparison
|
|
627
|
+
* against NULL is NULL, i.e. matches nothing: a cursor minted from a NULL
|
|
628
|
+
* boundary would return an empty next page and the list would look finished
|
|
629
|
+
* while rows remained. Refused rather than silently truncating the list.
|
|
630
|
+
*/
|
|
631
|
+
declare function encodeCursor(ordering: readonly CursorOrderingColumn[], row: Record<string, unknown>, table: string): string;
|
|
632
|
+
/**
|
|
633
|
+
* REFUSAL 3 / 4 — a cursor this provider did not mint (`malformed`), or one
|
|
634
|
+
* minted under a different ordering (`mismatch`).
|
|
635
|
+
*
|
|
636
|
+
* The mismatch check is the load-bearing half. Sorting a list re-issues the
|
|
637
|
+
* query, and a UI that kept the old `after` around would otherwise get a
|
|
638
|
+
* page built by comparing the NEW ordering's columns against the OLD
|
|
639
|
+
* ordering's values — a query that succeeds and returns rows that are simply
|
|
640
|
+
* not the ones asked for.
|
|
641
|
+
*/
|
|
642
|
+
declare function decodeCursor(raw: unknown, ordering: readonly CursorOrderingColumn[], table: string): readonly (string | number | boolean)[];
|
|
643
|
+
/**
|
|
644
|
+
* The keyset predicate: "every row that sorts strictly after the boundary",
|
|
645
|
+
* as the lexicographic expansion of a row comparison.
|
|
646
|
+
*
|
|
647
|
+
* For ordering `(a asc, b desc, id asc)` and boundary `(a0, b0, id0)` this
|
|
648
|
+
* emits
|
|
649
|
+
*
|
|
650
|
+
* ```
|
|
651
|
+
* _or: [ {a: {_gt: a0}},
|
|
652
|
+
* {_and: [{a: {_eq: a0}}, {b: {_lt: b0}}]},
|
|
653
|
+
* {_and: [{a: {_eq: a0}}, {b: {_eq: b0}}, {id: {_gt: id0}}]} ]
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
656
|
+
* Postgres's own `(a, b) > (a0, b0)` row comparison would be shorter, but
|
|
657
|
+
* Hasura's `bool_exp` has no row-comparison operator — this expansion is the
|
|
658
|
+
* only form expressible through it, and it is what every keyset
|
|
659
|
+
* implementation over Hasura ends up emitting.
|
|
660
|
+
*
|
|
661
|
+
* NOT written as `>=` on the leading column with a `_neq` correction, and
|
|
662
|
+
* not as a single `_gte`: both of those are the shapes that duplicate or
|
|
663
|
+
* skip rows across a run of equal values, which is the entire failure this
|
|
664
|
+
* function exists to avoid.
|
|
665
|
+
*
|
|
666
|
+
* `int64Fields` coercion is applied for the same reason
|
|
667
|
+
* `coerceInt64Filter` applies it to user filters: Hasura serialises an
|
|
668
|
+
* `int64` column as a JSON string and REJECTS a numeric filter value for it.
|
|
669
|
+
* A keyset predicate is an ordinary filter and gets no exemption.
|
|
670
|
+
*/
|
|
671
|
+
declare function keysetWhere(ordering: readonly CursorOrderingColumn[], boundary: readonly (string | number | boolean)[], int64Fields: readonly string[] | undefined): Record<string, unknown>;
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/endpoint.d.ts
|
|
367
674
|
/**
|
|
368
675
|
* Same-origin GraphQL path Hasura serves by default. The single source for
|
|
369
676
|
* both halves of this package: `createHasuraDataProvider`'s POST target and
|
|
@@ -390,48 +697,49 @@ declare function createHasuraDataProvider(config: HasuraDataProviderConfig): Dat
|
|
|
390
697
|
* `endpoint: `${prefix}/gql/v1/graphql``.
|
|
391
698
|
*/
|
|
392
699
|
declare const DEFAULT_HASURA_ENDPOINT = "/gql/v1/graphql";
|
|
393
|
-
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/live-provider.d.ts
|
|
394
702
|
interface SubscriptionPayload {
|
|
395
|
-
|
|
396
|
-
|
|
703
|
+
readonly query: string;
|
|
704
|
+
readonly variables?: Record<string, unknown>;
|
|
397
705
|
}
|
|
398
706
|
interface SubscriptionSink {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
707
|
+
next(value: unknown): void;
|
|
708
|
+
error(error: unknown): void;
|
|
709
|
+
complete(): void;
|
|
402
710
|
}
|
|
403
711
|
interface GraphqlWsClient {
|
|
404
|
-
|
|
712
|
+
subscribe(payload: SubscriptionPayload, sink: SubscriptionSink): () => void;
|
|
405
713
|
}
|
|
406
714
|
interface GraphqlWsClientOptions {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
715
|
+
readonly url: string;
|
|
716
|
+
readonly lazy: boolean;
|
|
717
|
+
readonly keepAlive: number;
|
|
718
|
+
readonly retryAttempts: number;
|
|
719
|
+
readonly shouldRetry: () => boolean;
|
|
720
|
+
readonly connectionParams: () => Promise<Record<string, unknown>>;
|
|
721
|
+
readonly on: {
|
|
722
|
+
connecting(): void;
|
|
723
|
+
connected(): void;
|
|
724
|
+
closed(): void;
|
|
725
|
+
};
|
|
418
726
|
}
|
|
419
727
|
interface HasuraLiveProviderConfig {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
728
|
+
readonly resources: Record<string, ResourceMapping>;
|
|
729
|
+
readonly getToken: () => Promise<string | null>;
|
|
730
|
+
/**
|
|
731
|
+
* The SAME GraphQL endpoint the data provider POSTs to — Hasura serves
|
|
732
|
+
* subscriptions on that exact path. Converted to `ws(s)://` by
|
|
733
|
+
* {@link hasuraWsUrl}. Defaults to {@link DEFAULT_HASURA_ENDPOINT}.
|
|
734
|
+
*
|
|
735
|
+
* `createHasuraDataProvider` forwards its own resolved endpoint here, so the
|
|
736
|
+
* socket and the POST target can never drift apart.
|
|
737
|
+
*/
|
|
738
|
+
readonly endpoint?: string;
|
|
739
|
+
/** `X-Hasura-Role` sent in the socket's connection params. `null`/omitted sends none. */
|
|
740
|
+
readonly role?: string | null;
|
|
741
|
+
/** Test seam; production uses graphql-ws directly. */
|
|
742
|
+
readonly clientFactory?: (options: GraphqlWsClientOptions) => GraphqlWsClient;
|
|
435
743
|
}
|
|
436
744
|
/**
|
|
437
745
|
* `https://host/path` → `wss://host/path` (Hasura serves subscriptions on the
|
|
@@ -462,5 +770,6 @@ declare function hasuraWsUrl(endpoint: string): string;
|
|
|
462
770
|
* single-sourced in the data provider.
|
|
463
771
|
*/
|
|
464
772
|
declare function createHasuraLiveProvider(config: HasuraLiveProviderConfig): DataLiveProvider;
|
|
465
|
-
|
|
466
|
-
export { DEFAULT_HASURA_ENDPOINT, type DataBodyConfig, FILTER_OPERATOR_UNSUPPORTED_CODE, type HasuraCustomOperation, type HasuraDataProviderConfig, type HasuraLiveProviderConfig, type HasuraOperationRegistry, type JsonbSpillConfig, NOT_AUTHENTICATED_CODE, type ResourceMapping, aggregateRoot, byPkRoot, createHasuraDataProvider, createHasuraLiveProvider, filtersToWhere, flattenJsonbSpill, hasuraWsUrl, mergeWhere, partitionDataBody, partitionJsonbSpill, scopeToWhere };
|
|
773
|
+
//#endregion
|
|
774
|
+
export { type CursorOrderingColumn, DEFAULT_HASURA_ENDPOINT, type DataBodyConfig, FILTER_OPERATOR_UNSUPPORTED_CODE, HASURA_CURSOR_CODES, HASURA_EXTERNAL_FAILURE_CODES, type HasuraCustomOperation, type HasuraDataProviderConfig, type HasuraLiveProviderConfig, type HasuraOperationRegistry, type JsonbSpillConfig, LOCALE_REQUIRED_CODE, NOT_AUTHENTICATED_CODE, type ResourceMapping, aggregateRoot, assertCursorLimit, assertCursorOrderable, byPkRoot, coerceInt64Filter, createHasuraDataProvider, createHasuraLiveProvider, decodeCursor, effectiveCursorOrdering, encodeCursor, filtersToWhere, flattenJsonbSpill, hasuraWsUrl, keysetWhere, localeWhere, mergeWhere, orderingSignature, partitionDataBody, partitionJsonbSpill, scopeToWhere };
|
|
775
|
+
//# sourceMappingURL=index.d.ts.map
|