rastack 0.0.51 → 0.0.52
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/CHANGELOG.md +2 -0
- package/auth/context.ts +73 -0
- package/auth/core.ts +293 -0
- package/auth/index.ts +23 -0
- package/components/auto-form/AutoForm.tsx +32 -12
- package/components/auto-form/use-auto-form.ts +75 -6
- package/components/data-table/DataTable.tsx +72 -45
- package/components/data-table/use-data-table.ts +57 -3
- package/components/index.ts +2 -0
- package/db.ts +9 -0
- package/dist/admin.js +13 -13
- package/dist/compile/analyze.js +7 -2
- package/dist/compile/entities.js +163 -16
- package/dist/compile/model.d.ts +7 -3
- package/dist/compile/openapi.js +13 -3
- package/dist/compile/program.d.ts +2 -1
- package/dist/compile/program.js +7 -2
- package/dist/define/auth.d.ts +27 -0
- package/dist/define/auth.js +42 -0
- package/dist/define/db.d.ts +24 -1
- package/dist/define/db.js +1 -1
- package/dist/define/index.d.ts +19 -2
- package/dist/define/manifest.js +10 -1
- package/dist/define/markers.d.ts +80 -0
- package/dist/define/markers.js +37 -0
- package/dist/define/types.d.ts +10 -0
- package/dist/define/types.js +26 -0
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/provider/index.ts +1 -5
- package/provider/provider.tsx +42 -11
- package/provider/types.ts +14 -2
- package/provider/warehouse.ts +4 -3
- package/provider/wasm.ts +3 -1
- package/runtime.ts +1 -0
- package/src/compile/analyze.ts +15 -3
- package/src/compile/entities.ts +193 -19
- package/src/compile/model.ts +8 -9
- package/src/compile/openapi.ts +11 -3
- package/src/compile/program.ts +10 -3
- package/src/define/auth.ts +28 -0
- package/src/define/db.ts +26 -4
- package/src/define/index.ts +20 -2
- package/src/define/manifest.ts +10 -1
- package/src/define/markers.ts +83 -0
- package/src/define/types.ts +11 -0
- package/test/auth.spec.ts +207 -0
- package/test/compile.spec.ts +18 -7
- package/test/components.spec.ts +116 -0
- package/test/entities.spec.ts +123 -6
- package/types.ts +7 -2
- package/wasm/rastack_wasm.js +1 -1
- package/wasm/rastack_wasm_bg.wasm +0 -0
|
@@ -38,6 +38,8 @@ export interface DataTableProps<
|
|
|
38
38
|
searchPlaceholder?: string;
|
|
39
39
|
/** Rendered when there are no rows. */
|
|
40
40
|
emptyMessage?: React.ReactNode;
|
|
41
|
+
/** Rendered when the identity may not list this resource (`canRead` false). */
|
|
42
|
+
unauthorizedMessage?: React.ReactNode;
|
|
41
43
|
/** Stable React key per row. Defaults to `row.id` then the index. */
|
|
42
44
|
rowKey?: (row: TRow, index: number) => React.Key;
|
|
43
45
|
/** Total-control escape hatch: receives the headless model, you render everything. */
|
|
@@ -65,10 +67,14 @@ export function DataTable<TRow extends Record<string, unknown>>(
|
|
|
65
67
|
query,
|
|
66
68
|
columns: columnsOption,
|
|
67
69
|
sortable,
|
|
70
|
+
auth,
|
|
71
|
+
policy,
|
|
72
|
+
resource,
|
|
68
73
|
onRowClick,
|
|
69
74
|
searchable = true,
|
|
70
75
|
searchPlaceholder = "Search…",
|
|
71
76
|
emptyMessage = "No records.",
|
|
77
|
+
unauthorizedMessage = "Sign in to view these records.",
|
|
72
78
|
rowKey,
|
|
73
79
|
children,
|
|
74
80
|
style,
|
|
@@ -76,12 +82,27 @@ export function DataTable<TRow extends Record<string, unknown>>(
|
|
|
76
82
|
} = props;
|
|
77
83
|
|
|
78
84
|
useDataTableCss();
|
|
79
|
-
const model = useDataTable<TRow>(query, {
|
|
85
|
+
const model = useDataTable<TRow>(query, {
|
|
86
|
+
columns: columnsOption,
|
|
87
|
+
sortable,
|
|
88
|
+
auth,
|
|
89
|
+
policy,
|
|
90
|
+
resource,
|
|
91
|
+
});
|
|
80
92
|
|
|
81
93
|
if (children) return <>{children(model)}</>;
|
|
82
94
|
|
|
83
|
-
const {
|
|
84
|
-
|
|
95
|
+
const {
|
|
96
|
+
columns,
|
|
97
|
+
rows,
|
|
98
|
+
isLoading,
|
|
99
|
+
isEmpty,
|
|
100
|
+
error,
|
|
101
|
+
canRead,
|
|
102
|
+
search,
|
|
103
|
+
sort,
|
|
104
|
+
pagination,
|
|
105
|
+
} = model;
|
|
85
106
|
const colSpan = Math.max(columns.length, 1);
|
|
86
107
|
|
|
87
108
|
const keyOf = (row: TRow, index: number): React.Key => {
|
|
@@ -115,7 +136,7 @@ export function DataTable<TRow extends Record<string, unknown>>(
|
|
|
115
136
|
...style,
|
|
116
137
|
}}
|
|
117
138
|
>
|
|
118
|
-
{search && searchable ? (
|
|
139
|
+
{search && searchable && canRead ? (
|
|
119
140
|
<div style={{ maxWidth: 320 }}>
|
|
120
141
|
<Input
|
|
121
142
|
size="sm"
|
|
@@ -157,55 +178,61 @@ export function DataTable<TRow extends Record<string, unknown>>(
|
|
|
157
178
|
</Tr>
|
|
158
179
|
</THead>
|
|
159
180
|
<TBody>
|
|
160
|
-
{
|
|
161
|
-
?
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
: "Something went wrong."}
|
|
166
|
-
</span>,
|
|
167
|
-
)
|
|
168
|
-
: isLoading && rows.length === 0
|
|
181
|
+
{!canRead
|
|
182
|
+
? // The server would answer 401/403 — say so instead of showing
|
|
183
|
+
// a misleading empty/error state.
|
|
184
|
+
messageRow(unauthorizedMessage)
|
|
185
|
+
: error
|
|
169
186
|
? messageRow(
|
|
170
|
-
<span
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
gap: tokens.space.sm,
|
|
175
|
-
}}
|
|
176
|
-
>
|
|
177
|
-
<Spinner /> Loading…
|
|
187
|
+
<span style={{ color: tokens.color.danger }}>
|
|
188
|
+
{error instanceof Error
|
|
189
|
+
? error.message
|
|
190
|
+
: "Something went wrong."}
|
|
178
191
|
</span>,
|
|
179
192
|
)
|
|
180
|
-
:
|
|
181
|
-
? messageRow(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
193
|
+
: isLoading && rows.length === 0
|
|
194
|
+
? messageRow(
|
|
195
|
+
<span
|
|
196
|
+
style={{
|
|
197
|
+
display: "inline-flex",
|
|
198
|
+
alignItems: "center",
|
|
199
|
+
gap: tokens.space.sm,
|
|
200
|
+
}}
|
|
189
201
|
>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
<Spinner /> Loading…
|
|
203
|
+
</span>,
|
|
204
|
+
)
|
|
205
|
+
: isEmpty
|
|
206
|
+
? messageRow(emptyMessage)
|
|
207
|
+
: rows.map((row, index) => (
|
|
208
|
+
<Tr
|
|
209
|
+
key={keyOf(row, index)}
|
|
210
|
+
data-clickable={onRowClick ? "true" : undefined}
|
|
211
|
+
onClick={
|
|
212
|
+
onRowClick
|
|
213
|
+
? () => onRowClick(row, index)
|
|
214
|
+
: undefined
|
|
215
|
+
}
|
|
216
|
+
>
|
|
217
|
+
{columns.map((col) => {
|
|
218
|
+
const value = (row as Record<string, unknown>)[
|
|
219
|
+
col.key
|
|
220
|
+
];
|
|
221
|
+
return (
|
|
222
|
+
<Td key={col.key} align={col.align}>
|
|
223
|
+
{col.render
|
|
224
|
+
? col.render(row, value)
|
|
225
|
+
: formatCell(value)}
|
|
226
|
+
</Td>
|
|
227
|
+
);
|
|
228
|
+
})}
|
|
229
|
+
</Tr>
|
|
230
|
+
))}
|
|
204
231
|
</TBody>
|
|
205
232
|
</Table>
|
|
206
233
|
</div>
|
|
207
234
|
|
|
208
|
-
{pagination ? (
|
|
235
|
+
{pagination && canRead ? (
|
|
209
236
|
<div
|
|
210
237
|
style={{
|
|
211
238
|
display: "flex",
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
import { useMemo } from "react";
|
|
15
15
|
import type { TableQuery } from "../types";
|
|
16
|
+
import {
|
|
17
|
+
can,
|
|
18
|
+
canActOnRow,
|
|
19
|
+
useRastackAuth,
|
|
20
|
+
useResourcePolicy,
|
|
21
|
+
type AuthAction,
|
|
22
|
+
type RastackAuth,
|
|
23
|
+
type ResourcePolicy,
|
|
24
|
+
type ResourceRef,
|
|
25
|
+
} from "../../auth";
|
|
16
26
|
import {
|
|
17
27
|
resolveColumns,
|
|
18
28
|
type DataTableColumn,
|
|
@@ -49,6 +59,21 @@ export interface DataTableModel<TRow = Record<string, unknown>> {
|
|
|
49
59
|
isLoading: boolean;
|
|
50
60
|
isEmpty: boolean;
|
|
51
61
|
error: unknown;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the identity may list this resource at all — `false` means the
|
|
64
|
+
* server would answer 401/403, so `<DataTable>` renders a sign-in notice
|
|
65
|
+
* instead of firing doomed affordances. Always `true` when no auth/policy
|
|
66
|
+
* is wired up.
|
|
67
|
+
*/
|
|
68
|
+
canRead: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The permission gate for any action on this resource — build your actions
|
|
71
|
+
* column with it (`model.can("delete")` hides a delete button the server
|
|
72
|
+
* would reject).
|
|
73
|
+
*/
|
|
74
|
+
can: (action: AuthAction) => boolean;
|
|
75
|
+
/** Row-level check: {@link can} *and* ownership of the specific row. */
|
|
76
|
+
canActOnRow: (action: AuthAction, row: TRow) => boolean;
|
|
52
77
|
/** Present only when the hook exposes searching. */
|
|
53
78
|
search?: DataTableSearch;
|
|
54
79
|
/** Present only when the hook exposes sorting. */
|
|
@@ -62,6 +87,19 @@ export interface DataTableOptions<TRow = Record<string, unknown>> {
|
|
|
62
87
|
columns?: DataTableColumnInput<TRow>[];
|
|
63
88
|
/** Force sortable headers on/off. Defaults to on whenever the hook supports sorting. */
|
|
64
89
|
sortable?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The identity to gate against. Defaults to the ambient identity from
|
|
92
|
+
* `<RAStackProvider auth={…}>` / `<RastackAuthProvider>`; pass `null` to
|
|
93
|
+
* opt out of gating for this table.
|
|
94
|
+
*/
|
|
95
|
+
auth?: RastackAuth | null;
|
|
96
|
+
/** The resource's compiled `permission` + `access` policy. */
|
|
97
|
+
policy?: ResourcePolicy;
|
|
98
|
+
/**
|
|
99
|
+
* `"app.model"` — looks `policy` up from the provider's manifest when one
|
|
100
|
+
* is available (always the case in `local`/`s3` mode).
|
|
101
|
+
*/
|
|
102
|
+
resource?: ResourceRef;
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
/** Pure model assembly — see {@link useDataTable}. */
|
|
@@ -85,12 +123,18 @@ export function buildDataTableModel<TRow extends Record<string, unknown>>(
|
|
|
85
123
|
const error =
|
|
86
124
|
query.error ?? (query.isError ? new Error("Request failed") : undefined);
|
|
87
125
|
|
|
126
|
+
const auth = options.auth ?? undefined;
|
|
127
|
+
const policy = options.policy;
|
|
128
|
+
|
|
88
129
|
const model: DataTableModel<TRow> = {
|
|
89
130
|
columns,
|
|
90
131
|
rows,
|
|
91
132
|
isLoading,
|
|
92
133
|
isEmpty,
|
|
93
134
|
error,
|
|
135
|
+
canRead: can(auth, policy, "list"),
|
|
136
|
+
can: (action) => can(auth, policy, action),
|
|
137
|
+
canActOnRow: (action, row) => canActOnRow(auth, policy, action, row),
|
|
94
138
|
};
|
|
95
139
|
|
|
96
140
|
if (query.searching) {
|
|
@@ -128,14 +172,24 @@ export function buildDataTableModel<TRow extends Record<string, unknown>>(
|
|
|
128
172
|
return model;
|
|
129
173
|
}
|
|
130
174
|
|
|
131
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* React hook wrapper around {@link buildDataTableModel} (memoized on the query
|
|
177
|
+
* + options). Auth handling is built in: the identity defaults to the ambient
|
|
178
|
+
* one from `<RAStackProvider auth={…}>`, and `resource: "app.model"` resolves
|
|
179
|
+
* the policy from the provider's manifest.
|
|
180
|
+
*/
|
|
132
181
|
export function useDataTable<TRow extends Record<string, unknown>>(
|
|
133
182
|
query: TableQuery<TRow>,
|
|
134
183
|
options: DataTableOptions<TRow> = {},
|
|
135
184
|
): DataTableModel<TRow> {
|
|
185
|
+
const ambientAuth = useRastackAuth();
|
|
186
|
+
const ambientPolicy = useResourcePolicy(options.resource);
|
|
187
|
+
const auth = options.auth !== undefined ? options.auth : ambientAuth;
|
|
188
|
+
const policy = options.policy ?? ambientPolicy;
|
|
136
189
|
return useMemo(
|
|
137
|
-
() => buildDataTableModel(query, options),
|
|
190
|
+
() => buildDataTableModel(query, { ...options, auth, policy }),
|
|
138
191
|
// The query object identity changes as react-query state advances.
|
|
139
|
-
|
|
192
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
193
|
+
[query, options.columns, options.sortable, auth, policy],
|
|
140
194
|
);
|
|
141
195
|
}
|
package/components/index.ts
CHANGED
package/db.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rastack/db` — type-level column configuration for class/interface
|
|
3
|
+
* entities: the `DbConfig` brands (`Unique`, `MaxLength<N>`, `PrimaryKey`,
|
|
4
|
+
* `Default<V>`, `Int`, `UUID`, `DateTimeString`) plus the access-control
|
|
5
|
+
* field brands (`Owner`, `Group`), which are also exported from
|
|
6
|
+
* `rastack/auth` alongside the entity-level markers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from "./src/define/db";
|