@cratestack/refine 0.7.14

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stephane Segning
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,160 @@
1
+ # @cratestack/refine
2
+
3
+ A [refine.dev](https://refine.dev) `DataProvider` over CrateStack's generated TypeScript **REST**
4
+ client. Wires refine's fixed `getList`/`getOne`/`getMany`/`create`/`update`/`deleteOne` surface to
5
+ a generated model class (`client.widgets`, `client.ledgers`, …), mapping refine's filter operators,
6
+ pagination, primary keys, and `@version` optimistic-locking conflicts onto the server's real
7
+ contract — the same one [cratestack-studio](https://cratestack.dev) deliberately bypasses.
8
+ `cratestack-studio` talks to `[target.db]` directly and skips `@@allow`; a refine app built on this
9
+ package goes through the generated API and inherits policy, validation, `@version` concurrency, and
10
+ audit. That's the reason this package exists (cratestack#571): refine is the safe end-user admin
11
+ surface, Studio is the sysadmin one.
12
+
13
+ This package covers REST-transport schemas only (`generate-typescript`'s default transport, no
14
+ `transport rpc`) — see [Scope](#scope) below.
15
+
16
+ ## Why a runtime package, not a generator
17
+
18
+ The open question cratestack#571 asked to settle first: how much of this genuinely has to be
19
+ generated per schema? Answer: none of it, given a small hand-written manifest.
20
+
21
+ A `DataProvider` needs three schema facts per resource — its primary-key field name, whether its
22
+ model declares `@@paged`, and whether/where it declares `@version` — plus the resource's own
23
+ generated API object. None of those four things has a runtime discovery path: the generated client
24
+ carries no `$meta` object, no introspection endpoint, nothing beyond what its TypeScript *types*
25
+ already encode at compile time (confirmed by reading the real generated `client.ts`/`models.ts`
26
+ output this package's own tests drive — see [tests/](./tests)). Whether or not `@cratestack/refine`
27
+ ships a code generator, an app still needs to write down those four things per resource somewhere.
28
+ A generator that emits a five-line object literal isn't materially cheaper to maintain than writing
29
+ that object literal directly against the schema the developer already wrote — it's a second
30
+ artifact to keep in sync instead of one. So this package is exactly what `createCratestackDataProvider`
31
+ below is: a hand-written runtime function over a hand-written manifest, no `cratestack generate-*`
32
+ subcommand, no build step beyond `tsc`.
33
+
34
+ ## Usage
35
+
36
+ ```ts
37
+ import { createCratestackDataProvider } from "@cratestack/refine";
38
+ import { ExampleApiClientClient } from "./generated/client"; // your project's generated REST client
39
+
40
+ const client = new ExampleApiClientClient("https://api.example.com", { basePath: "/api" });
41
+
42
+ const dataProvider = createCratestackDataProvider({
43
+ widgets: { api: client.widgets, primaryKey: "id", paged: false },
44
+ ledgers: { api: client.ledgers, primaryKey: "id", paged: true, versionField: "version" },
45
+ products: { api: client.products, primaryKey: "sku", paged: false },
46
+ });
47
+ ```
48
+
49
+ Pass this straight to refine's `<Refine dataProvider={dataProvider} .../>`.
50
+
51
+ ## Filters
52
+
53
+ refine's `CrudFilters` (`{ field, operator, value }`) map onto the generated list route's
54
+ `field__operator=value` query convention — the same operator set the generated client's shared
55
+ filter types (`EqualityFilter`/`ComparableFilter`/`StringFilter`) expose:
56
+
57
+ | refine operator | cratestack query key |
58
+ | --- | --- |
59
+ | `eq` | `field` (no suffix) |
60
+ | `ne` | `field__ne` |
61
+ | `in` | `field__in` (comma-separated) |
62
+ | `lt` / `lte` / `gt` / `gte` | `field__lt` / `__lte` / `__gt` / `__gte` |
63
+ | `contains` | `field__contains` |
64
+ | `startswith` | `field__startsWith` |
65
+ | `null` | `field__isNull=true` |
66
+ | `nnull` | `field__isNull=false` |
67
+
68
+ **Every other refine operator throws** — `endswith`, `between`, `nin`, `containss`, refine's
69
+ `or`/`and` filter groups, and anything else not in the table above. Dropping an unsupported filter
70
+ silently would show the wrong data as if it had been filtered; an error is strictly better. Note
71
+ also that `eq`/`ne`/`in`/comparison operators only work against **required** (non-nullable) fields
72
+ — that's a server-side codegen gate
73
+ (`crates/cratestack-macros/src/axum/filter_arms.rs::generate_query_filter_arm`), not something this
74
+ package can work around.
75
+
76
+ ## Pagination
77
+
78
+ refine's `{ current, pageSize }` maps to `limit`/`offset` directly. **`totalCount` is only ever
79
+ emitted for a `@@paged` model's list route.** Set `paged: false` in a resource's config honestly —
80
+ a non-`@@paged` resource's `getList` still returns real data, but `total` degrades to
81
+ "how many rows this one response returned" rather than a true count. Either add `@@paged` to the
82
+ model, or configure refine's `pagination: { mode: "off" }` for that resource and treat the result as
83
+ one (capped) page.
84
+
85
+ ## Primary keys
86
+
87
+ refine assumes every record has `id: BaseKey`. cratestack's `@id` can be on any field
88
+ (`primaryKey` in the resource config above). Every returned record gets a synthetic `id` field
89
+ alongside its real primary key so refine's row-selection machinery has something to key off;
90
+ writes (`get`/`update`/`deleteOne`) receive that same value back and pass it straight through as
91
+ the real primary key — no translation needed, since the *value space* is identical. The one place
92
+ this bites: a `<Create>` form's fields must use the schema's real primary-key field name (e.g.
93
+ `sku`), not `id` — a create payload has no record yet to synthesize an `id` from.
94
+
95
+ ## Optimistic locking (`@version` / `If-Match`)
96
+
97
+ **The single most important correctness point in this package.** A `@version` model requires
98
+ `If-Match` on both update *and* delete (cratestack#493/#519/#538) — missing or stale `If-Match`
99
+ returns `412 Precondition Failed` and leaves the row untouched. refine's `update`/`deleteOne` hooks
100
+ fetch the record before editing it, so the version is known by the time a mutation fires; this
101
+ package remembers it (keyed per `createCratestackDataProvider` call, not module-global) from every
102
+ read/write that returns a fresh record, and sends it automatically. A `412` is surfaced as a
103
+ distinguishable conflict (`statusCode: 412`, a human-readable message) rather than a generic
104
+ failure — check `error.statusCode === 412` in an `onError` handler to show a "someone else changed
105
+ this" message instead of a generic one.
106
+
107
+ Pass `meta: { ifMatch: <version> }` to `update`/`deleteOne` to override the cache explicitly (e.g.
108
+ optimistic-lock-aware bulk workflows). Calling `update`/`deleteOne` on a `@version` resource with no
109
+ known version and no override throws rather than silently omitting `If-Match` — omitting it doesn't
110
+ fail on the wire the way a *stale* value does, so this package refuses to make that mistake for you.
111
+
112
+ ## `createMany` / `updateMany` / `deleteMany`
113
+
114
+ Implemented, not declined — but as N sequential single-record round trips (`Promise.all` over
115
+ `create`/`update`/`deleteOne`), not a real batch. The generated REST client exposes no
116
+ `updateMany`/`deleteMany` wrapper around the server's actual `update_many`/`delete_many`, and
117
+ `/rpc/batch` is an RPC-transport-only endpoint. So these three methods work, but without atomicity
118
+ and at the cost of N requests instead of one.
119
+
120
+ ## Procedures (`custom`)
121
+
122
+ A cratestack `procedure` has no other home in a `DataProvider`. Pass a `procedures` map to
123
+ `createCratestackDataProvider`'s second argument and call it through refine's `custom`:
124
+
125
+ ```ts
126
+ const dataProvider = createCratestackDataProvider(resources, {
127
+ procedures: { publishPost: (args) => client.procedures.publishPost(args as never) },
128
+ });
129
+
130
+ await dataProvider.custom!({
131
+ url: "",
132
+ method: "post",
133
+ meta: { procedure: "publishPost" },
134
+ payload: { args: { postId: 1 } },
135
+ });
136
+ ```
137
+
138
+ ## Scope
139
+
140
+ **Out of scope, tracked as follow-ups by cratestack#571 itself:**
141
+
142
+ - **`liveProvider`** — the generated TypeScript client has no SSE consumer today (no `EventSource`
143
+ anywhere in `crates/cratestack-client-typescript/templates/`), even though the server has a real
144
+ subscribe endpoint. Needs a TS SSE client first.
145
+ - **`authProvider`** — a separate, orthogonal concern from data access.
146
+ - **RPC-transport schemas** (`transport rpc`) — this package's filter/pagination query-string
147
+ convention is REST-specific. An RPC dataProvider needs its own mapping layer.
148
+
149
+ **A known, pre-existing gap this package inherits rather than causes:** route suppression
150
+ (cratestack#514) isn't implemented, so a policy-denied `create`/`update`/`delete` still generates a
151
+ working-looking client method — `config.api.create` being present only proves the model declares
152
+ *some* `@@allow("create", ...)`, not that the current caller satisfies it. A refine `<Create>`
153
+ button wired to such a resource renders and then `403`s on submit for a caller who can't actually
154
+ create one. Until #514 ships, gate that in refine's own `resources` config
155
+ (`create: false`/`edit: false`/`delete: false`) per caller role, not on this package's method
156
+ presence.
157
+
158
+ See `guides/refine-integration.md` in
159
+ [cratestack-docs](https://github.com/cratestack/cratestack-docs) for the hand-wired version of
160
+ everything this package automates, including the full reasoning behind each design choice above.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@cratestack/refine",
3
+ "version": "0.7.14",
4
+ "description": "A refine.dev DataProvider over CrateStack's generated TypeScript REST client — the safe end-user admin-UI surface that goes through the generated API (policy, validation, @version concurrency, audit), unlike cratestack-studio's direct-to-database sysadmin access.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/cratestack/cratestack.git",
9
+ "directory": "packages/cratestack-refine"
10
+ },
11
+ "homepage": "https://cratestack.dev",
12
+ "bugs": "https://github.com/cratestack/cratestack/issues",
13
+ "keywords": [
14
+ "cratestack",
15
+ "cstack",
16
+ "refine",
17
+ "refinedev",
18
+ "dataProvider",
19
+ "admin-panel"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "test": "vitest run",
39
+ "lint": "biome check ."
40
+ },
41
+ "peerDependencies": {
42
+ "@refinedev/core": "^5.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@refinedev/core": "^5.0.0",
46
+ "decimal.js": "^10.6.0",
47
+ "typescript": "^7.0.2",
48
+ "vitest": "^4.1.10"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ }