@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/dist/index.d.cts CHANGED
@@ -1,81 +1,75 @@
1
- import { DataOperationContext, DataProvider, FilterExpression, ErasedResourceHandle, DataLiveProvider } from '@burojs/core';
2
-
1
+ import { DataLiveProvider, DataOperationContext, DataProvider, ErasedResourceHandle, FilterExpression } from "@burojs/core";
2
+ //#region src/data-provider.d.ts
3
3
  interface HasuraDataProviderConfig {
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
- * ⚠️ KNOWN GAP, stated here because this is where a consumer reads: HTTP 401
32
- * is NOT how the Hasura deployment this provider was distilled from rejects
33
- * a credential. The one capture in this repository of a real rejection
34
- * (`packages/mock-kit/fixtures/hasura/error-unauthorized.json`, taken from
35
- * the live backend with a deliberately malformed bearer token) came back
36
- * **HTTP 200** with an `invalid-jwt` errors envelope. Replayed through this
37
- * provider it surfaces as `{ kind: 'protocol', code: 'graphql-error' }` and
38
- * `recoverAuth` is never consulted — no refresh, no retry, and app logic
39
- * keyed on `kind: 'auth'` never fires either. Pinned by
40
- * `tests/fixture-fidelity.test.ts`'s `error-unauthorized` case.
41
- *
42
- * So this callback fires only for a deployment that really answers 401 (a
43
- * proxy or gateway in front of Hasura, typically). Keying recovery on the
44
- * error envelope instead is a product decision, deliberately not taken here;
45
- * the existing 401 tests use a FABRICATED 401 and must not be read as proof
46
- * that recovery works against Hasura itself.
47
- */
48
- recoverAuth?: () => Promise<boolean>;
49
- /** Map our resource name → table + field selection. */
50
- resources: Record<string, ResourceMapping>;
51
- /**
52
- * Optional global scope read on every getList. Returns key → value pairs
53
- * that get merged into the where clause as `{ <key>: { _eq: <value> } }`,
54
- * but only for resources that whitelist the key in `scopeFields`.
55
- *
56
- * Example: `() => ({ pg: 'radio', location_id: 5 })` — applied to documents
57
- * (which have both fields) but not products (which only have pg).
58
- */
59
- getScope?: (context: DataOperationContext) => Record<string, unknown>;
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
- /** Which native provider slot may execute this declaration. */
70
- readonly kind: 'query' | 'mutation';
71
- /** Static GraphQL document owned by the application/provider integration. */
72
- readonly document: string;
73
- /** Maps the declared operation input to the document's variables. */
74
- readonly variables: (input: unknown) => Record<string, unknown> | undefined;
75
- /** Maps the full GraphQL response to the declared operation output. */
76
- readonly mapResult?: (data: unknown) => unknown;
77
- /** Overrides the mapped resource role; undefined keeps that mapped role. */
78
- readonly role?: string | null;
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
- table: string;
88
- selectFields: string[];
89
- /**
90
- * GraphQL scalar name for this table's primary key, interpolated into the
91
- * `$id` variable declaration of every by-pk query/mutation. Default
92
- * `'bigint'`.
93
- *
94
- * DISTILLATION NOTE (divergence 3 of 7 — feature, additive, l2-web-ui only).
95
- * l2-web-ui added `'String'` for tables whose pk is a genuine string
96
- * business key (a GS1 marking code), not a uuid/Int/bigint
97
- * wire type: Hasura's by-pk root for such a table is literally
98
- * `<table>_by_pk(<pk>: $id)` with `$id: String!`, which none of the other
99
- * three names can express (playground's union stopped at
100
- * `'bigint' | 'uuid' | 'Int'`). Purely a type-union widening — no runtime
101
- * branch reads `pkType` beyond interpolating it — so l2-web-ui's wider union
102
- * is taken wholesale rather than forked.
103
- */
104
- pkType?: 'bigint' | 'uuid' | 'Int' | 'String';
105
- /**
106
- * Skip the aggregate on every `getList`. This is a COST decision, not a
107
- * schema fact: the table has an `_aggregate` root but counting it on each
108
- * page is too slow to pay for unprompted. (Measured in one of the apps this
109
- * provider was distilled from: ~9.8M-row and ~28.9M-row tables, the latter
110
- * taking ~55 s per count.) `total` is then omitted and the list renders the
111
- * unknown-total shape.
112
- *
113
- * ⚠️ NOT the same thing as {@link ResourceMapping.aggregate} `: false`, and
114
- * NOT a proxy for it — most `noAggregate` tables CAN be counted on demand,
115
- * which is exactly what `getCount` is for. Deriving one from the other gets
116
- * the flagship resources backwards.
117
- */
118
- noAggregate?: boolean;
119
- /**
120
- * Which aggregate root field backs this table, for the on-demand `getCount`
121
- * (and for `getList`'s own count when it runs):
122
- * - absent — derive `${table}_aggregate` (the Hasura default; today's behaviour);
123
- * - `false` — no aggregate root exists at all, under any name. `getCount`
124
- * answers `{ total: null }` without a round trip and the UI drops the
125
- * affordance for the session;
126
- * - a string — an explicitly-named root, for tables whose custom root fields
127
- * break the derivation.
128
- *
129
- * ⚠️ Establish this by introspecting the live schema under the role the app
130
- * actually runs as, not by reading `noAggregate` or a code comment. Both
131
- * have been wrong here.
132
- */
133
- aggregate?: false | string;
134
- /**
135
- * Root field for a single-row fetch by primary key. Defaults to the derived
136
- * `${table}_by_pk`. Set `false` when the active role exposes only the
137
- * collection root; `getOne` then queries it with pk `where` + `limit: 1`.
138
- *
139
- * ⚠️ Same class of fact as `aggregate`, and established the same way — by
140
- * introspecting the live schema, not by reading a comment. In one of the
141
- * source apps exactly one table of forty had a non-derivable by-pk root (a
142
- * plural table whose root was singular), so the derived name 400'd and its
143
- * Show page was a dead-end error reachable in two clicks from the sidebar.
144
- */
145
- byPkRoot?: string | false;
146
- /** Field to order by in getList. Defaults to 'id'. */
147
- orderByField?: string;
148
- /** Name of the primary-key field in the row (for synthetic id in list rows). Defaults to 'id'. */
149
- pkField?: string;
150
- /**
151
- * Hasura role for this resource. `null` (or `''`) means no role header (use
152
- * the JWT's default role). Undefined falls back to
153
- * {@link HasuraDataProviderConfig.defaultRole}.
154
- */
155
- role?: string | null;
156
- /**
157
- * Columns typed `int64` in a Hasura ClickHouse source. Hasura serialises
158
- * int64 as a JSON STRING and REJECTS a numeric filter value with
159
- * «expected a string for type 'int64'» — so a `document_id` filter sent as a
160
- * number matched nothing and the affected list tabs came back empty.
161
- * Filter values for these fields are coerced to strings before the where
162
- * clause is built. (Verified live: numeric/int literal → error, string → rows.)
163
- */
164
- int64Fields?: string[];
165
- /**
166
- * Whitelist of scope keys this table actually has columns for. Only these
167
- * keys from `getScope()` get injected into where clauses. Defaults to `[]`
168
- * (no scope injection — useful for tables that lack the scope columns).
169
- */
170
- scopeFields?: string[];
171
- /**
172
- * JSONB-spill routing for tables whose forms submit a flat values record but
173
- * whose real columns nest most of those keys inside JSONB blobs (e.g. a
174
- * `documents` table where template head fields live inside `head`, a couple
175
- * live inside `meta`, and only a handful — type/status/… — are real columns).
176
- *
177
- * When set, create/update partition the incoming flat `variables`:
178
- * - keys that ARE real columns (present in `selectFields`) stay top-level;
179
- * - keys listed in `metaKeys` nest under `metaColumn`;
180
- * - every OTHER key nests under `column` (the head spill).
181
- * getOne does the inverse — it spreads the fetched `column`/`metaColumn`
182
- * objects back onto the flat record so edit forms / show pages read those
183
- * keys as flat fields (the raw blobs are kept too).
184
- *
185
- * SCOPE: scalar / relation / enum head+meta fields only. Data-body arrays
186
- * (children) live in a sibling table — see `dataBody`.
187
- */
188
- jsonbSpill?: JsonbSpillConfig;
189
- /**
190
- * Data-body routing for resources whose heavy array payload lives in a
191
- * SEPARATE 1:1 sibling table rather than a JSONB column on the row itself
192
- * (e.g. a `documents.items[]` / `products[]` payload living in
193
- * `documents_data.data`, keyed by `document_id`).
194
- *
195
- * When set, create/update peel the `keys` off the flat `variables` FIRST
196
- * (before `jsonbSpill` runs, so they never leak into `head`), write the parent
197
- * row, then upsert the sibling row (`insert_<table>_one … on_conflict`). getOne
198
- * nested-selects the sibling via `relationship` and spreads its `column` blob
199
- * back onto the flat record (so edit forms read `items` as a top-level field),
200
- * dropping the raw relationship object so it can't round-trip into `head`.
201
- * getList is left untouched — list columns are head/meta scalars, never the
202
- * heavy data body.
203
- */
204
- dataBody?: DataBodyConfig;
205
- /**
206
- * Fixed, always-on Hasura `where` fragment ANDed into every getList /
207
- * getOne query for this resource. Powers "per-type" / projection resources:
208
- * several resource names can map to the SAME table, each scoped to a
209
- * different slice — e.g. `fixedFilter: { type: { _eq: 'work_order' } }` turns
210
- * a `work_orders` resource into a work-orders-only view of a shared
211
- * `documents` table. The fragment is a raw Hasura `<table>_bool_exp` object
212
- * (same shape `filtersToWhere` emits), so nested / `_and` / `_or` clauses are
213
- * all valid. Merged with `mergeWhere` (AND) after user filters + scope, so
214
- * the user can still filter/search WITHIN the fixed slice.
215
- *
216
- * getOne: because `<table>_by_pk` takes no `where`, a resource with a
217
- * `fixedFilter` fetches its single row via a `where`-based `limit: 1` query
218
- * (pk `_eq` id AND fixedFilter) instead — so the row is only returned when it
219
- * actually belongs to the slice (a work-order id won't resolve under
220
- * `movements`). Resources without a `fixedFilter` keep the `_by_pk` path.
221
- */
222
- fixedFilter?: Record<string, unknown>;
223
- /**
224
- * Opt this resource into real-time updates. The live provider then opens a
225
- * cheap change-signal subscription for its list / single-record views and
226
- * invalidates matching native runtime queries when the table changes.
227
- *
228
- * Off by default on purpose: every live resource holds an open server-side
229
- * subscription, so it is a cost you opt into for data that actually moves
230
- * (documents change constantly during processing; a country lookup does not).
231
- */
232
- live?: boolean;
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
- /** JSONB column that catches every non-base, non-meta key (e.g. `head`). */
237
- column: string;
238
- /** JSONB column that catches the `metaKeys` (e.g. `meta`). */
239
- metaColumn: string;
240
- /** Flat keys routed to `metaColumn` instead of the `column` spill. */
241
- metaKeys: string[];
242
- /**
243
- * Flat form key → dotted NESTED path inside the head `column`. Lets a flat
244
- * field (e.g. `vsd_number`) persist to a nested head location (e.g.
245
- * `regulator_data.crpt.vsd_number`) without dotted field keys. getOne reads
246
- * the nested value back out to the flat key.
247
- */
248
- nestedKeys?: Record<string, string>;
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
- /** Sibling table holding the heavy data body, 1:1 with the parent (e.g. `documents_data`). */
253
- table: string;
254
- /** FK column on the sibling pointing back at the parent pk (e.g. `document_id`). */
255
- fk: string;
256
- /** JSONB column on the sibling that holds the whole data body (e.g. `data`). */
257
- column: string;
258
- /** Reverse object-relationship name on the PARENT row → sibling, for the getOne nested select (e.g. `document_data`). */
259
- relationship: string;
260
- /** `on_conflict` constraint enum for the upsert (e.g. `document_data_pkey`). */
261
- constraint: string;
262
- /** Flat value keys that belong to the data body — routed to the sibling `column` (e.g. `['items', 'products']`). */
263
- keys: string[];
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
- dataBody: Record<string, unknown>;
281
- rest: Record<string, unknown>;
282
- hasDataBody: boolean;
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
- readonly query: string;
396
- readonly variables?: Record<string, unknown>;
703
+ readonly query: string;
704
+ readonly variables?: Record<string, unknown>;
397
705
  }
398
706
  interface SubscriptionSink {
399
- next(value: unknown): void;
400
- error(error: unknown): void;
401
- complete(): void;
707
+ next(value: unknown): void;
708
+ error(error: unknown): void;
709
+ complete(): void;
402
710
  }
403
711
  interface GraphqlWsClient {
404
- subscribe(payload: SubscriptionPayload, sink: SubscriptionSink): () => void;
712
+ subscribe(payload: SubscriptionPayload, sink: SubscriptionSink): () => void;
405
713
  }
406
714
  interface GraphqlWsClientOptions {
407
- readonly url: string;
408
- readonly lazy: boolean;
409
- readonly keepAlive: number;
410
- readonly retryAttempts: number;
411
- readonly shouldRetry: () => boolean;
412
- readonly connectionParams: () => Promise<Record<string, unknown>>;
413
- readonly on: {
414
- connecting(): void;
415
- connected(): void;
416
- closed(): void;
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
- readonly resources: Record<string, ResourceMapping>;
421
- readonly getToken: () => Promise<string | null>;
422
- /**
423
- * The SAME GraphQL endpoint the data provider POSTs to — Hasura serves
424
- * subscriptions on that exact path. Converted to `ws(s)://` by
425
- * {@link hasuraWsUrl}. Defaults to {@link DEFAULT_HASURA_ENDPOINT}.
426
- *
427
- * `createHasuraDataProvider` forwards its own resolved endpoint here, so the
428
- * socket and the POST target can never drift apart.
429
- */
430
- readonly endpoint?: string;
431
- /** `X-Hasura-Role` sent in the socket's connection params. `null`/omitted sends none. */
432
- readonly role?: string | null;
433
- /** Test seam; production uses graphql-ws directly. */
434
- readonly clientFactory?: (options: GraphqlWsClientOptions) => GraphqlWsClient;
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.cts.map