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.
Files changed (54) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/auth/context.ts +73 -0
  3. package/auth/core.ts +293 -0
  4. package/auth/index.ts +23 -0
  5. package/components/auto-form/AutoForm.tsx +32 -12
  6. package/components/auto-form/use-auto-form.ts +75 -6
  7. package/components/data-table/DataTable.tsx +72 -45
  8. package/components/data-table/use-data-table.ts +57 -3
  9. package/components/index.ts +2 -0
  10. package/db.ts +9 -0
  11. package/dist/admin.js +13 -13
  12. package/dist/compile/analyze.js +7 -2
  13. package/dist/compile/entities.js +163 -16
  14. package/dist/compile/model.d.ts +7 -3
  15. package/dist/compile/openapi.js +13 -3
  16. package/dist/compile/program.d.ts +2 -1
  17. package/dist/compile/program.js +7 -2
  18. package/dist/define/auth.d.ts +27 -0
  19. package/dist/define/auth.js +42 -0
  20. package/dist/define/db.d.ts +24 -1
  21. package/dist/define/db.js +1 -1
  22. package/dist/define/index.d.ts +19 -2
  23. package/dist/define/manifest.js +10 -1
  24. package/dist/define/markers.d.ts +80 -0
  25. package/dist/define/markers.js +37 -0
  26. package/dist/define/types.d.ts +10 -0
  27. package/dist/define/types.js +26 -0
  28. package/dist/wasm/rastack_wasm.js +1 -1
  29. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  30. package/package.json +1 -1
  31. package/provider/index.ts +1 -5
  32. package/provider/provider.tsx +42 -11
  33. package/provider/types.ts +14 -2
  34. package/provider/warehouse.ts +4 -3
  35. package/provider/wasm.ts +3 -1
  36. package/runtime.ts +1 -0
  37. package/src/compile/analyze.ts +15 -3
  38. package/src/compile/entities.ts +193 -19
  39. package/src/compile/model.ts +8 -9
  40. package/src/compile/openapi.ts +11 -3
  41. package/src/compile/program.ts +10 -3
  42. package/src/define/auth.ts +28 -0
  43. package/src/define/db.ts +26 -4
  44. package/src/define/index.ts +20 -2
  45. package/src/define/manifest.ts +10 -1
  46. package/src/define/markers.ts +83 -0
  47. package/src/define/types.ts +11 -0
  48. package/test/auth.spec.ts +207 -0
  49. package/test/compile.spec.ts +18 -7
  50. package/test/components.spec.ts +116 -0
  51. package/test/entities.spec.ts +123 -6
  52. package/types.ts +7 -2
  53. package/wasm/rastack_wasm.js +1 -1
  54. 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, { columns: columnsOption, sortable });
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 { columns, rows, isLoading, isEmpty, error, search, sort, pagination } =
84
- model;
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
- {error
161
- ? messageRow(
162
- <span style={{ color: tokens.color.danger }}>
163
- {error instanceof Error
164
- ? error.message
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
- style={{
172
- display: "inline-flex",
173
- alignItems: "center",
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
- : isEmpty
181
- ? messageRow(emptyMessage)
182
- : rows.map((row, index) => (
183
- <Tr
184
- key={keyOf(row, index)}
185
- data-clickable={onRowClick ? "true" : undefined}
186
- onClick={
187
- onRowClick ? () => onRowClick(row, index) : undefined
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
- {columns.map((col) => {
191
- const value = (row as Record<string, unknown>)[
192
- col.key
193
- ];
194
- return (
195
- <Td key={col.key} align={col.align}>
196
- {col.render
197
- ? col.render(row, value)
198
- : formatCell(value)}
199
- </Td>
200
- );
201
- })}
202
- </Tr>
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
- /** React hook wrapper around {@link buildDataTableModel} (memoized on the query + options). */
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
- [query, options.columns, options.sortable],
192
+ // eslint-disable-next-line react-hooks/exhaustive-deps
193
+ [query, options.columns, options.sortable, auth, policy],
140
194
  );
141
195
  }
@@ -66,6 +66,8 @@ export {
66
66
  buildAutoFormModel,
67
67
  type AutoFormModel,
68
68
  type AutoFormFieldModel,
69
+ type AutoFormAuthOptions,
70
+ type AutoFormModelOptions,
69
71
  } from "./auto-form/use-auto-form";
70
72
  export {
71
73
  inputKindForFieldType,
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";