@burojs/hasura 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maksim Kuznetsov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # @burojs/hasura
2
+
3
+ A [Hasura](https://hasura.io/) GraphQL data provider and live (subscription)
4
+ provider for [buro](https://github.com/buro/buro).
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pnpm add @burojs/hasura @burojs/core
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```ts
15
+ import { createHasuraDataProvider } from '@burojs/hasura';
16
+
17
+ const dataProvider = createHasuraDataProvider({
18
+ // Where to POST. Same-origin path, path-prefixed path, or absolute URL.
19
+ // Defaults to '/gql/v1/graphql'.
20
+ endpoint: '/gql/v1/graphql',
21
+ getToken: async () => session.jwt,
22
+ // X-Hasura-Role for resources that don't declare their own. Omit for none.
23
+ defaultRole: 'company-admin',
24
+ resources: {
25
+ orders: {
26
+ table: 'orders',
27
+ selectFields: ['id', 'code', 'status', 'created_at'],
28
+ pkType: 'uuid',
29
+ live: true,
30
+ },
31
+ },
32
+ });
33
+ ```
34
+
35
+ ### `createHasuraDataProvider(config)`
36
+
37
+ A `DataProvider` over Hasura's GraphQL API: `list` / `one` / `many` / `count`
38
+ queries, `createOne` / `updateOne` / `deleteOne` mutations, an opt-in
39
+ custom-operation registry, and a bundled live provider. Offset pagination only
40
+ — cursor pagination is refused with a `capability` error.
41
+
42
+ Everything the provider does is driven by the per-resource `ResourceMapping`
43
+ registry rather than by branching on resource names:
44
+
45
+ | Mapping field | What it declares |
46
+ |---|---|
47
+ | `table`, `selectFields` | the Hasura table and the columns to select |
48
+ | `pkField`, `pkType` | primary-key column name and its GraphQL scalar (`bigint` \| `uuid` \| `Int` \| `String`) |
49
+ | `byPkRoot` | the `<table>_by_pk` root name, or `false` when the role exposes only the collection root |
50
+ | `aggregate`, `noAggregate` | whether a count root exists, and whether to pay for a count on every page |
51
+ | `role` | per-resource `X-Hasura-Role` (`null` suppresses the header) |
52
+ | `scopeFields` | which keys from `getScope()` this table actually has columns for |
53
+ | `fixedFilter` | an always-on `where` fragment — several resource names over one table, each a different slice |
54
+ | `int64Fields` | columns Hasura serialises as JSON strings (ClickHouse `int64`), whose filter values must be stringified |
55
+ | `jsonbSpill` | route a flat form record into/out of JSONB `head` / `meta` columns |
56
+ | `dataBody` | route a heavy array payload into/out of a 1:1 sibling table |
57
+ | `live` | opt this resource into subscription-driven invalidation |
58
+
59
+ #### `recoverAuth` fires on HTTP 401 — which is not how Hasura rejects a credential
60
+
61
+ `config.recoverAuth` is consulted when a request comes back with HTTP **401**,
62
+ and only then. The one capture in this repository of a real Hasura rejection —
63
+ `packages/mock-kit/fixtures/hasura/error-unauthorized.json`, taken from a live
64
+ backend with a deliberately malformed bearer token — came back **HTTP 200 with
65
+ an `invalid-jwt` errors envelope**. Replayed through this provider that
66
+ surfaces as `{ kind: 'protocol', code: 'graphql-error' }`, `recoverAuth` is
67
+ never called, and app logic keyed on `kind: 'auth'` never fires either.
68
+
69
+ So `recoverAuth` is useful for a deployment that really answers 401 (a proxy or
70
+ gateway in front of Hasura); against Hasura's own rejection shape it is inert.
71
+ The behaviour is pinned by `tests/fixture-fidelity.test.ts`'s
72
+ `error-unauthorized` case, and the package's existing 401-recovery tests use a
73
+ fabricated 401 — they are not evidence about Hasura. Keying recovery on the
74
+ error envelope is a product decision this package has not taken.
75
+
76
+ Optimistic concurrency is available per call: pass
77
+ `providerOptions.ifUpdatedAt` on an update and the mutation switches to a
78
+ `where`-guarded form, rejecting with `{ kind: 'conflict', code:
79
+ 'stale-record' }` when the row has moved on. Omit it (or pass `null`) and
80
+ nothing changes.
81
+
82
+ ### `createHasuraLiveProvider(config)`
83
+
84
+ The subscription half, already wired in by `createHasuraDataProvider`. Exported
85
+ separately for callers that want it on its own. The socket carries only a
86
+ change SIGNAL — rows always come back through the regular descriptor query, so
87
+ mapping and pagination stay single-sourced in the data provider.
88
+ `hasuraWsUrl(endpoint)` (also exported) is the http(s) → ws(s) conversion. Two
89
+ things a caller has to know about it:
90
+
91
+ - Its argument is the **full endpoint**, the same string you pass
92
+ `createHasuraDataProvider` — not a path prefix. It appends nothing. (The two
93
+ applications this package was distilled from had a same-named,
94
+ same-signature function that took a PREFIX and appended `/gql/v1/graphql`
95
+ itself; copying one of those call sites here yields a socket URL that
96
+ addresses nothing, and a socket that never connects does not throw. Pinned by
97
+ `tests/live-provider.test.ts`.)
98
+ - The conversion needs a `window`. With none (SSR, or a non-browser import) the
99
+ endpoint is returned **unchanged** — an absolute `https://…` endpoint comes
100
+ back as `https://…`, not `wss://…`. A relative endpoint has no host to
101
+ resolve against there, so this is a real boundary, not just a scheme bug.
102
+
103
+ ## Provenance
104
+
105
+ This package was distilled from two independently-evolved copies of the same
106
+ provider that lived in two applications. The copies had drifted in exactly
107
+ seven places; each resolution is documented in a comment next to the code it
108
+ affects, saying which side it came from and whether it was a feature, a bug
109
+ fix, or drift. Two defects that neither copy had fixed at the time were closed
110
+ here, each with a test that reddens without the fix; both were subsequently
111
+ backported into both applications, so the in-code notes describe the state this
112
+ package was distilled FROM, not the state of the repository today.
113
+
114
+ Neither application is migrated onto this package and neither has been removed
115
+ yet, so at present the repository holds three copies of this provider rather
116
+ than one. That is deliberate — the applications are leaving, so migrating them
117
+ would be wasted work — but the de-duplication this package exists for is not
118
+ complete until they go.
119
+
120
+ ## License
121
+
122
+ MIT