@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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- 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
|
+
}
|