@izumisy-tailor/tailor-data-viewer 0.3.0 → 0.3.2

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.
@@ -64,7 +64,6 @@ interface TableMetadata {
64
64
  readonly name: string;
65
65
  readonly pluralForm: string;
66
66
  readonly description?: string;
67
- readonly readAllowedRoles: readonly string[];
68
67
  readonly fields: readonly FieldMetadata[];
69
68
  /** Relations (manyToOne, oneToOne, and oneToMany) */
70
69
  readonly relations?: readonly RelationMetadata[];
@@ -89,7 +88,6 @@ interface ProcessedTable {
89
88
  pluralForm: string;
90
89
  originalName: string;
91
90
  description?: string;
92
- readAllowedRoles: string[];
93
91
  fields: FieldMetadata[];
94
92
  relations: RelationMetadata[];
95
93
  }
@@ -29,26 +29,6 @@ function toCamelCase(str) {
29
29
  return str.charAt(0).toLowerCase() + str.slice(1);
30
30
  }
31
31
  /**
32
- * Extract allowed roles from gql permission policies
33
- * Only extracts roles from 'read' action policies with 'allow' permit
34
- */
35
- function extractReadAllowedRoles(gqlPermission) {
36
- if (!gqlPermission) return [];
37
- const roles = /* @__PURE__ */ new Set();
38
- for (const policy of gqlPermission) {
39
- if (policy.permit !== "allow") continue;
40
- const actions = policy.actions;
41
- if (!actions.includes("all") && !actions.includes("read")) continue;
42
- for (const condition of policy.conditions) {
43
- if (!Array.isArray(condition) || condition.length < 3) continue;
44
- const [left, operator, right] = condition;
45
- if (typeof left === "string" && operator === "in" && typeof right === "object" && right !== null && "user" in right && right.user === "roles") roles.add(left);
46
- if (typeof right === "string" && operator === "in" && typeof left === "object" && left !== null && "user" in left && left.user === "roles") roles.add(right);
47
- }
48
- }
49
- return Array.from(roles);
50
- }
51
- /**
52
32
  * Creates a custom generator that extracts table metadata for Data View
53
33
  */
54
34
  function dataViewerMetadataGenerator(options = {}) {
@@ -100,13 +80,11 @@ function dataViewerMetadataGenerator(options = {}) {
100
80
  };
101
81
  fields.push(fieldMetadata);
102
82
  }
103
- const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
104
83
  return {
105
84
  name: toCamelCase(type.name),
106
85
  pluralForm: toCamelCase(type.pluralForm),
107
86
  originalName: type.name,
108
87
  description: type.description,
109
- readAllowedRoles,
110
88
  fields,
111
89
  relations
112
90
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -1,69 +1,66 @@
1
1
  import { createContext, useContext, type ReactNode } from "react";
2
- import type { UseCollectionReturn } from "../types";
2
+ import type { CollectionControl } from "../types";
3
3
 
4
- const CollectionVariablesContext = createContext<UseCollectionReturn<
5
- string,
6
- unknown
7
- > | null>(null);
4
+ const CollectionControlContext = createContext<CollectionControl | null>(null);
8
5
 
9
6
  /**
10
- * Provider that shares collection query parameters via React Context.
7
+ * Provider that shares collection control state via React Context.
11
8
  *
12
9
  * @example
13
10
  * ```tsx
14
- * const { variables, ...collection } = useCollectionVariables({ params: { pageSize: 20 } });
11
+ * const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
15
12
  *
16
- * <CollectionVariablesProvider value={collection}>
13
+ * <CollectionControlProvider value={control}>
17
14
  * <FilterPanel />
18
15
  * <DataTable.Root>...</DataTable.Root>
19
16
  * <Pagination />
20
- * </CollectionVariablesProvider>
17
+ * </CollectionControlProvider>
21
18
  * ```
22
19
  */
23
- export function CollectionVariablesProvider({
20
+ export function CollectionControlProvider({
24
21
  value,
25
22
  children,
26
23
  }: {
27
- value: UseCollectionReturn<string, unknown>;
24
+ value: CollectionControl;
28
25
  children: ReactNode;
29
26
  }) {
30
27
  return (
31
- <CollectionVariablesContext.Provider value={value}>
28
+ <CollectionControlContext.Provider value={value}>
32
29
  {children}
33
- </CollectionVariablesContext.Provider>
30
+ </CollectionControlContext.Provider>
34
31
  );
35
32
  }
36
33
 
37
34
  /**
38
- * Hook to access collection state from the nearest `CollectionVariablesProvider`.
35
+ * Hook to access collection control from the nearest `CollectionControlProvider`.
39
36
  *
40
- * Returns the same interface as `useCollectionVariables()`. Pass a `TFieldName`
37
+ * Returns the `CollectionControl` interface. Pass a `TFieldName`
41
38
  * type parameter to narrow method arguments like `addFilter` / `setSort`.
42
39
  *
43
40
  * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
44
41
  *
45
- * @throws Error if used outside of `CollectionVariablesProvider`.
42
+ * @throws Error if used outside of `CollectionControlProvider`.
46
43
  *
47
44
  * @example
48
45
  * ```tsx
49
46
  * function StatusFilter() {
50
- * const { filters, addFilter, removeFilter } = useCollectionVariablesContext();
47
+ * const { filters, addFilter, removeFilter } = useCollectionControl();
51
48
  * // ...
52
49
  * }
53
50
  *
54
51
  * // With typed field names:
55
52
  * type TaskField = FieldName<typeof tableMetadata, "task">;
56
- * const { addFilter } = useCollectionVariablesContext<TaskField>();
53
+ * const { addFilter } = useCollectionControl<TaskField>();
57
54
  * ```
58
55
  */
59
- export function useCollectionVariablesContext<
56
+ export function useCollectionControl<
60
57
  TFieldName extends string = string,
61
- >(): UseCollectionReturn<TFieldName> {
62
- const ctx = useContext(CollectionVariablesContext);
58
+ >(): CollectionControl<TFieldName> {
59
+ const ctx = useContext(CollectionControlContext);
63
60
  if (!ctx) {
64
61
  throw new Error(
65
- "useCollectionVariablesContext must be used within <CollectionVariablesProvider>",
62
+ "useCollectionControl must be used within <CollectionControlProvider>",
66
63
  );
67
64
  }
68
- return ctx as UseCollectionReturn<TFieldName>;
65
+ return ctx as CollectionControl<TFieldName>;
69
66
  }
@@ -13,10 +13,10 @@ describe("useCollectionVariables", () => {
13
13
  expect(result.current.variables.pagination).toEqual({ first: 20 });
14
14
  expect(result.current.variables.query).toBeUndefined();
15
15
  expect(result.current.variables.order).toBeUndefined();
16
- expect(result.current.filters).toEqual([]);
17
- expect(result.current.sortStates).toEqual([]);
18
- expect(result.current.cursor).toBeNull();
19
- expect(result.current.hasPrevPage).toBe(false);
16
+ expect(result.current.control.filters).toEqual([]);
17
+ expect(result.current.control.sortStates).toEqual([]);
18
+ expect(result.current.control.cursor).toBeNull();
19
+ expect(result.current.control.hasPrevPage).toBe(false);
20
20
  });
21
21
 
22
22
  it("uses custom pageSize", () => {
@@ -34,7 +34,7 @@ describe("useCollectionVariables", () => {
34
34
  },
35
35
  }),
36
36
  );
37
- expect(result.current.sortStates).toEqual([
37
+ expect(result.current.control.sortStates).toEqual([
38
38
  { field: "createdAt", direction: "Desc" },
39
39
  ]);
40
40
  expect(result.current.variables.order).toEqual([
@@ -56,7 +56,7 @@ describe("useCollectionVariables", () => {
56
56
  },
57
57
  }),
58
58
  );
59
- expect(result.current.filters).toHaveLength(1);
59
+ expect(result.current.control.filters).toHaveLength(1);
60
60
  expect(result.current.variables.query).toEqual({
61
61
  status: { eq: "ACTIVE" },
62
62
  });
@@ -71,11 +71,11 @@ describe("useCollectionVariables", () => {
71
71
  const { result } = renderHook(() => useCollectionVariables({}));
72
72
 
73
73
  act(() => {
74
- result.current.addFilter("status", "eq", "ACTIVE");
74
+ result.current.control.addFilter("status", "eq", "ACTIVE");
75
75
  });
76
76
 
77
- expect(result.current.filters).toHaveLength(1);
78
- expect(result.current.filters[0]).toMatchObject({
77
+ expect(result.current.control.filters).toHaveLength(1);
78
+ expect(result.current.control.filters[0]).toMatchObject({
79
79
  field: "status",
80
80
  operator: "eq",
81
81
  value: "ACTIVE",
@@ -89,21 +89,21 @@ describe("useCollectionVariables", () => {
89
89
  const { result } = renderHook(() => useCollectionVariables({}));
90
90
 
91
91
  act(() => {
92
- result.current.addFilter("status", "eq", "ACTIVE");
92
+ result.current.control.addFilter("status", "eq", "ACTIVE");
93
93
  });
94
94
  act(() => {
95
- result.current.addFilter("status", "eq", "INACTIVE");
95
+ result.current.control.addFilter("status", "eq", "INACTIVE");
96
96
  });
97
97
 
98
- expect(result.current.filters).toHaveLength(1);
99
- expect(result.current.filters[0].value).toBe("INACTIVE");
98
+ expect(result.current.control.filters).toHaveLength(1);
99
+ expect(result.current.control.filters[0].value).toBe("INACTIVE");
100
100
  });
101
101
 
102
102
  it("sets filters in bulk", () => {
103
103
  const { result } = renderHook(() => useCollectionVariables({}));
104
104
 
105
105
  act(() => {
106
- result.current.setFilters([
106
+ result.current.control.setFilters([
107
107
  {
108
108
  field: "status",
109
109
  operator: "eq",
@@ -117,7 +117,7 @@ describe("useCollectionVariables", () => {
117
117
  ]);
118
118
  });
119
119
 
120
- expect(result.current.filters).toHaveLength(2);
120
+ expect(result.current.control.filters).toHaveLength(2);
121
121
  expect(result.current.variables.query).toEqual({
122
122
  status: { eq: "ACTIVE" },
123
123
  amount: { gte: 1000 },
@@ -128,29 +128,29 @@ describe("useCollectionVariables", () => {
128
128
  const { result } = renderHook(() => useCollectionVariables({}));
129
129
 
130
130
  act(() => {
131
- result.current.addFilter("status", "eq", "ACTIVE");
132
- result.current.addFilter("amount", "gte", 1000);
131
+ result.current.control.addFilter("status", "eq", "ACTIVE");
132
+ result.current.control.addFilter("amount", "gte", 1000);
133
133
  });
134
134
  act(() => {
135
- result.current.removeFilter("status");
135
+ result.current.control.removeFilter("status");
136
136
  });
137
137
 
138
- expect(result.current.filters).toHaveLength(1);
139
- expect(result.current.filters[0].field).toBe("amount");
138
+ expect(result.current.control.filters).toHaveLength(1);
139
+ expect(result.current.control.filters[0].field).toBe("amount");
140
140
  });
141
141
 
142
142
  it("clears all filters", () => {
143
143
  const { result } = renderHook(() => useCollectionVariables({}));
144
144
 
145
145
  act(() => {
146
- result.current.addFilter("status", "eq", "ACTIVE");
147
- result.current.addFilter("amount", "gte", 1000);
146
+ result.current.control.addFilter("status", "eq", "ACTIVE");
147
+ result.current.control.addFilter("amount", "gte", 1000);
148
148
  });
149
149
  act(() => {
150
- result.current.clearFilters();
150
+ result.current.control.clearFilters();
151
151
  });
152
152
 
153
- expect(result.current.filters).toHaveLength(0);
153
+ expect(result.current.control.filters).toHaveLength(0);
154
154
  expect(result.current.variables.query).toBeUndefined();
155
155
  });
156
156
 
@@ -159,16 +159,16 @@ describe("useCollectionVariables", () => {
159
159
 
160
160
  // Navigate to next page
161
161
  act(() => {
162
- result.current.nextPage("cursor1");
162
+ result.current.control.nextPage("cursor1");
163
163
  });
164
- expect(result.current.cursor).toBe("cursor1");
164
+ expect(result.current.control.cursor).toBe("cursor1");
165
165
 
166
166
  // Adding filter should reset pagination
167
167
  act(() => {
168
- result.current.addFilter("status", "eq", "ACTIVE");
168
+ result.current.control.addFilter("status", "eq", "ACTIVE");
169
169
  });
170
- expect(result.current.cursor).toBeNull();
171
- expect(result.current.hasPrevPage).toBe(false);
170
+ expect(result.current.control.cursor).toBeNull();
171
+ expect(result.current.control.hasPrevPage).toBe(false);
172
172
  });
173
173
  });
174
174
 
@@ -180,10 +180,10 @@ describe("useCollectionVariables", () => {
180
180
  const { result } = renderHook(() => useCollectionVariables({}));
181
181
 
182
182
  act(() => {
183
- result.current.setSort("createdAt", "Desc");
183
+ result.current.control.setSort("createdAt", "Desc");
184
184
  });
185
185
 
186
- expect(result.current.sortStates).toEqual([
186
+ expect(result.current.control.sortStates).toEqual([
187
187
  { field: "createdAt", direction: "Desc" },
188
188
  ]);
189
189
  expect(result.current.variables.order).toEqual([
@@ -195,13 +195,13 @@ describe("useCollectionVariables", () => {
195
195
  const { result } = renderHook(() => useCollectionVariables({}));
196
196
 
197
197
  act(() => {
198
- result.current.setSort("createdAt", "Desc");
198
+ result.current.control.setSort("createdAt", "Desc");
199
199
  });
200
200
  act(() => {
201
- result.current.setSort("name", "Asc");
201
+ result.current.control.setSort("name", "Asc");
202
202
  });
203
203
 
204
- expect(result.current.sortStates).toEqual([
204
+ expect(result.current.control.sortStates).toEqual([
205
205
  { field: "createdAt", direction: "Desc" },
206
206
  { field: "name", direction: "Asc" },
207
207
  ]);
@@ -211,16 +211,16 @@ describe("useCollectionVariables", () => {
211
211
  const { result } = renderHook(() => useCollectionVariables({}));
212
212
 
213
213
  act(() => {
214
- result.current.setSort("createdAt", "Desc");
214
+ result.current.control.setSort("createdAt", "Desc");
215
215
  });
216
216
  act(() => {
217
- result.current.setSort("name", "Asc");
217
+ result.current.control.setSort("name", "Asc");
218
218
  });
219
219
  act(() => {
220
- result.current.setSort("createdAt", "Asc");
220
+ result.current.control.setSort("createdAt", "Asc");
221
221
  });
222
222
 
223
- expect(result.current.sortStates).toEqual([
223
+ expect(result.current.control.sortStates).toEqual([
224
224
  { field: "name", direction: "Asc" },
225
225
  { field: "createdAt", direction: "Asc" },
226
226
  ]);
@@ -230,16 +230,16 @@ describe("useCollectionVariables", () => {
230
230
  const { result } = renderHook(() => useCollectionVariables({}));
231
231
 
232
232
  act(() => {
233
- result.current.setSort("createdAt", "Desc");
233
+ result.current.control.setSort("createdAt", "Desc");
234
234
  });
235
235
  act(() => {
236
- result.current.setSort("name", "Asc");
236
+ result.current.control.setSort("name", "Asc");
237
237
  });
238
238
  act(() => {
239
- result.current.setSort("createdAt");
239
+ result.current.control.setSort("createdAt");
240
240
  });
241
241
 
242
- expect(result.current.sortStates).toEqual([
242
+ expect(result.current.control.sortStates).toEqual([
243
243
  { field: "name", direction: "Asc" },
244
244
  ]);
245
245
  });
@@ -248,13 +248,13 @@ describe("useCollectionVariables", () => {
248
248
  const { result } = renderHook(() => useCollectionVariables({}));
249
249
 
250
250
  act(() => {
251
- result.current.setSort("createdAt", "Desc");
251
+ result.current.control.setSort("createdAt", "Desc");
252
252
  });
253
253
  act(() => {
254
- result.current.clearSort();
254
+ result.current.control.clearSort();
255
255
  });
256
256
 
257
- expect(result.current.sortStates).toEqual([]);
257
+ expect(result.current.control.sortStates).toEqual([]);
258
258
  expect(result.current.variables.order).toBeUndefined();
259
259
  });
260
260
  });
@@ -267,11 +267,11 @@ describe("useCollectionVariables", () => {
267
267
  const { result } = renderHook(() => useCollectionVariables({}));
268
268
 
269
269
  act(() => {
270
- result.current.nextPage("cursor1");
270
+ result.current.control.nextPage("cursor1");
271
271
  });
272
272
 
273
- expect(result.current.cursor).toBe("cursor1");
274
- expect(result.current.paginationDirection).toBe("forward");
273
+ expect(result.current.control.cursor).toBe("cursor1");
274
+ expect(result.current.control.paginationDirection).toBe("forward");
275
275
  expect(result.current.variables.pagination.after).toBe("cursor1");
276
276
  expect(result.current.variables.pagination.first).toBe(20);
277
277
  expect(result.current.variables.pagination.last).toBeUndefined();
@@ -282,11 +282,11 @@ describe("useCollectionVariables", () => {
282
282
  const { result } = renderHook(() => useCollectionVariables({}));
283
283
 
284
284
  act(() => {
285
- result.current.prevPage("cursor1");
285
+ result.current.control.prevPage("cursor1");
286
286
  });
287
287
 
288
- expect(result.current.cursor).toBe("cursor1");
289
- expect(result.current.paginationDirection).toBe("backward");
288
+ expect(result.current.control.cursor).toBe("cursor1");
289
+ expect(result.current.control.paginationDirection).toBe("backward");
290
290
  expect(result.current.variables.pagination.before).toBe("cursor1");
291
291
  expect(result.current.variables.pagination.last).toBe(20);
292
292
  expect(result.current.variables.pagination.first).toBeUndefined();
@@ -297,14 +297,14 @@ describe("useCollectionVariables", () => {
297
297
  const { result } = renderHook(() => useCollectionVariables({}));
298
298
 
299
299
  act(() => {
300
- result.current.prevPage("cursorB");
300
+ result.current.control.prevPage("cursorB");
301
301
  });
302
- expect(result.current.paginationDirection).toBe("backward");
302
+ expect(result.current.control.paginationDirection).toBe("backward");
303
303
 
304
304
  act(() => {
305
- result.current.nextPage("cursorA");
305
+ result.current.control.nextPage("cursorA");
306
306
  });
307
- expect(result.current.paginationDirection).toBe("forward");
307
+ expect(result.current.control.paginationDirection).toBe("forward");
308
308
  expect(result.current.variables.pagination.after).toBe("cursorA");
309
309
  expect(result.current.variables.pagination.first).toBe(20);
310
310
  });
@@ -313,33 +313,33 @@ describe("useCollectionVariables", () => {
313
313
  const { result } = renderHook(() => useCollectionVariables({}));
314
314
 
315
315
  act(() => {
316
- result.current.prevPage("cursor1");
316
+ result.current.control.prevPage("cursor1");
317
317
  });
318
318
  act(() => {
319
- result.current.resetPage();
319
+ result.current.control.resetPage();
320
320
  });
321
321
 
322
- expect(result.current.cursor).toBeNull();
323
- expect(result.current.paginationDirection).toBe("forward");
322
+ expect(result.current.control.cursor).toBeNull();
323
+ expect(result.current.control.paginationDirection).toBe("forward");
324
324
  });
325
325
 
326
326
  it("tracks hasPrevPage from currentPage and hasNextPage from setPageInfo", () => {
327
327
  const { result } = renderHook(() => useCollectionVariables({}));
328
328
 
329
329
  // Initially on page 1: no prev, no next
330
- expect(result.current.hasPrevPage).toBe(false);
331
- expect(result.current.hasNextPage).toBe(false);
330
+ expect(result.current.control.hasPrevPage).toBe(false);
331
+ expect(result.current.control.hasNextPage).toBe(false);
332
332
 
333
333
  // After navigating to next page, hasPrevPage becomes true
334
334
  act(() => {
335
- result.current.nextPage("cursor-end");
335
+ result.current.control.nextPage("cursor-end");
336
336
  });
337
337
 
338
- expect(result.current.hasPrevPage).toBe(true);
338
+ expect(result.current.control.hasPrevPage).toBe(true);
339
339
 
340
340
  // hasNextPage comes from setPageInfo when totalPages is null
341
341
  act(() => {
342
- result.current.setPageInfo({
342
+ result.current.control.setPageInfo({
343
343
  hasNextPage: true,
344
344
  endCursor: "end",
345
345
  hasPreviousPage: false,
@@ -347,7 +347,7 @@ describe("useCollectionVariables", () => {
347
347
  });
348
348
  });
349
349
 
350
- expect(result.current.hasNextPage).toBe(true);
350
+ expect(result.current.control.hasNextPage).toBe(true);
351
351
  });
352
352
  });
353
353
 
@@ -373,7 +373,7 @@ describe("useCollectionVariables", () => {
373
373
  );
374
374
 
375
375
  act(() => {
376
- result.current.nextPage("abc123");
376
+ result.current.control.nextPage("abc123");
377
377
  });
378
378
 
379
379
  expect(result.current.variables).toEqual({
@@ -414,7 +414,6 @@ describe("useCollectionVariables", () => {
414
414
  task: {
415
415
  name: "task",
416
416
  pluralForm: "tasks",
417
- readAllowedRoles: [],
418
417
  fields: [
419
418
  { name: "id", type: "uuid", required: true },
420
419
  { name: "title", type: "string", required: true },
@@ -450,7 +449,7 @@ describe("useCollectionVariables", () => {
450
449
  }),
451
450
  );
452
451
 
453
- expect(result.current.sortStates).toEqual([
452
+ expect(result.current.control.sortStates).toEqual([
454
453
  { field: "dueDate", direction: "Desc" },
455
454
  ]);
456
455
  });
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from "react";
2
2
  import type { TableMetadata } from "../../generator/metadata-generator";
3
3
  import type {
4
4
  BuildQueryVariables,
5
+ CollectionControl,
5
6
  CollectionVariables,
6
7
  Filter,
7
8
  FilterOperator,
@@ -298,30 +299,32 @@ export function useCollectionVariables(
298
299
  // ---------------------------------------------------------------------------
299
300
  return {
300
301
  variables,
301
- filters,
302
- // Cast needed: implementation accepts FilterOperator (widest),
303
- // but overload signatures narrow via OperatorForField<TFilter, F>.
304
- addFilter: addFilter as UseCollectionReturn["addFilter"],
305
- setFilters,
306
- removeFilter,
307
- clearFilters,
308
- sortStates,
309
- setSort,
310
- clearSort,
311
- pageSize,
312
- setPageSize,
313
- cursor,
314
- paginationDirection,
315
- nextPage,
316
- prevPage,
317
- resetPage,
318
- hasPrevPage,
319
- hasNextPage,
320
- setPageInfo,
321
- currentPage,
322
- totalPages,
323
- goToFirstPage,
324
- goToLastPage,
325
- setTotal,
302
+ control: {
303
+ filters,
304
+ // Cast needed: implementation accepts FilterOperator (widest),
305
+ // but overload signatures narrow via OperatorForField<TFilter, F>.
306
+ addFilter: addFilter as CollectionControl["addFilter"],
307
+ setFilters,
308
+ removeFilter,
309
+ clearFilters,
310
+ sortStates,
311
+ setSort,
312
+ clearSort,
313
+ pageSize,
314
+ setPageSize,
315
+ cursor,
316
+ paginationDirection,
317
+ nextPage,
318
+ prevPage,
319
+ resetPage,
320
+ hasPrevPage,
321
+ hasNextPage,
322
+ setPageInfo,
323
+ currentPage,
324
+ totalPages,
325
+ goToFirstPage,
326
+ goToLastPage,
327
+ setTotal,
328
+ },
326
329
  };
327
330
  }
@@ -22,7 +22,6 @@ import type { TableMetadata } from "../../generator/metadata-generator";
22
22
  type TestTable = {
23
23
  readonly name: "test";
24
24
  readonly pluralForm: "tests";
25
- readonly readAllowedRoles: readonly [];
26
25
  readonly fields: readonly [
27
26
  { readonly name: "id"; readonly type: "uuid"; readonly required: true },
28
27
  {
@@ -82,9 +81,13 @@ type AssertNumberField = TestQuery extends {
82
81
  : never;
83
82
  export const assertNumberField: AssertNumberField = true;
84
83
 
85
- // ✅ Enum fields produce in/nin with string arrays
84
+ // ✅ Enum fields produce literal union types from enumValues
86
85
  type AssertEnumField = TestQuery extends {
87
- status?: { eq?: string; in?: string[]; nin?: string[] };
86
+ status?: {
87
+ eq?: "active" | "inactive";
88
+ in?: ("active" | "inactive")[];
89
+ nin?: ("active" | "inactive")[];
90
+ };
88
91
  }
89
92
  ? true
90
93
  : never;
@@ -8,7 +8,7 @@ import {
8
8
  type ReactNode,
9
9
  } from "react";
10
10
  import { cn } from "../lib/utils";
11
- import { CollectionVariablesProvider } from "../collection/collection-provider";
11
+ import { CollectionControlProvider } from "../collection/collection-provider";
12
12
  import { Table } from "../table";
13
13
  import type { RowAction, SortConfig, UseDataTableReturn } from "../types";
14
14
  import {
@@ -102,7 +102,7 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
102
102
  locale: value.locale,
103
103
  };
104
104
 
105
- const collectionValue = value.collection ?? null;
105
+ const controlValue = value.control ?? null;
106
106
 
107
107
  const inner = (
108
108
  <DataTableContext.Provider value={dataTableValue}>
@@ -110,12 +110,12 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
110
110
  </DataTableContext.Provider>
111
111
  );
112
112
 
113
- // Wrap with CollectionVariablesContext when collection is available
114
- if (collectionValue) {
113
+ // Wrap with CollectionControlProvider when control is available
114
+ if (controlValue) {
115
115
  return (
116
- <CollectionVariablesProvider value={collectionValue}>
116
+ <CollectionControlProvider value={controlValue}>
117
117
  {inner}
118
- </CollectionVariablesProvider>
118
+ </CollectionControlProvider>
119
119
  );
120
120
  }
121
121
 
@@ -1,5 +1,5 @@
1
1
  import { useDataTableContext } from "./data-table-context";
2
- import { useCollectionVariablesContext } from "../collection/collection-provider";
2
+ import { useCollectionControl } from "../collection/collection-provider";
3
3
  import { getLabels } from "./i18n";
4
4
 
5
5
  // =============================================================================
@@ -139,7 +139,7 @@ export function Pagination({ labels, pageSizeOptions }: PaginationProps = {}) {
139
139
  goToLastPage,
140
140
  pageSize,
141
141
  setPageSize,
142
- } = useCollectionVariablesContext();
142
+ } = useCollectionControl();
143
143
 
144
144
  const pl = getLabels(locale).pagination;
145
145
  const firstLabel = labels?.first ?? pl.first;
@@ -14,7 +14,7 @@ import type {
14
14
  } from "../types";
15
15
  import { OPERATORS_BY_FILTER_TYPE } from "../types";
16
16
  import { useDataTableContext } from "./data-table-context";
17
- import { useCollectionVariablesContext } from "../collection/collection-provider";
17
+ import { useCollectionControl } from "../collection/collection-provider";
18
18
  import { getLabels } from "./i18n";
19
19
 
20
20
  /**
@@ -46,7 +46,7 @@ export function SearchFilterForm({
46
46
  } = {}) {
47
47
  const { columns, locale } = useDataTableContext();
48
48
  const { filters, addFilter, removeFilter, clearFilters } =
49
- useCollectionVariablesContext();
49
+ useCollectionControl();
50
50
  const sf = getLabels(locale).searchFilter;
51
51
  const filterableColumns = columns.filter((col) => !!col.filter);
52
52
 
@@ -234,7 +234,7 @@ describe("useDataTable", () => {
234
234
  // Sort state
235
235
  // ---------------------------------------------------------------------------
236
236
  describe("sort state", () => {
237
- it("sortStates is empty when no collection is provided", () => {
237
+ it("sortStates is empty when no control is provided", () => {
238
238
  const { result } = renderHook(() =>
239
239
  useDataTable<TestRow>({ columns: testColumns, data: testData }),
240
240
  );
@@ -242,7 +242,7 @@ describe("useDataTable", () => {
242
242
  expect(result.current.sortStates).toEqual([]);
243
243
  });
244
244
 
245
- it("onSort is undefined when no collection is provided", () => {
245
+ it("onSort is undefined when no control is provided", () => {
246
246
  const { result } = renderHook(() =>
247
247
  useDataTable<TestRow>({ columns: testColumns, data: testData }),
248
248
  );
@@ -13,7 +13,7 @@ import type {
13
13
  *
14
14
  * @example
15
15
  * ```tsx
16
- * const { variables, ...collection } = useCollectionVariables({ params: { pageSize: 20 } });
16
+ * const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
17
17
  * const { query, order, pagination } = variables;
18
18
  * const [result] = useQuery({
19
19
  * query: GET_ORDERS,
@@ -24,7 +24,7 @@ import type {
24
24
  * columns,
25
25
  * data: result.data?.orders,
26
26
  * loading: result.fetching,
27
- * collection,
27
+ * control,
28
28
  * });
29
29
  *
30
30
  * <DataTable.Provider value={table}>
@@ -43,7 +43,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
43
43
  data,
44
44
  loading = false,
45
45
  error = null,
46
- collection,
46
+ control,
47
47
  onClickRow,
48
48
  rowActions,
49
49
  locale = "en",
@@ -76,20 +76,15 @@ export function useDataTable<TRow extends Record<string, unknown>>(
76
76
  );
77
77
  }, [data]);
78
78
 
79
- // Sync pageInfo to collection so hasPrevPage/hasNextPage are up-to-date
79
+ // Sync pageInfo to control so hasPrevPage/hasNextPage are up-to-date
80
80
  useEffect(() => {
81
81
  if (data?.pageInfo) {
82
- collection?.setPageInfo(data.pageInfo);
82
+ control?.setPageInfo(data.pageInfo);
83
83
  }
84
84
  if (data?.total != null) {
85
- collection?.setTotal(data.total);
85
+ control?.setTotal(data.total);
86
86
  }
87
- }, [
88
- data?.pageInfo,
89
- data?.total,
90
- collection?.setPageInfo,
91
- collection?.setTotal,
92
- ]);
87
+ }, [data?.pageInfo, data?.total, control?.setPageInfo, control?.setTotal]);
93
88
 
94
89
  // ---------------------------------------------------------------------------
95
90
  // Column visibility management
@@ -131,24 +126,24 @@ export function useDataTable<TRow extends Record<string, unknown>>(
131
126
  );
132
127
 
133
128
  // ---------------------------------------------------------------------------
134
- // Pagination (delegated from collection)
129
+ // Pagination (delegated from control)
135
130
  // ---------------------------------------------------------------------------
136
131
  const nextPage = useCallback(
137
132
  (endCursor: string) => {
138
- collection?.nextPage(endCursor);
133
+ control?.nextPage(endCursor);
139
134
  },
140
- [collection],
135
+ [control],
141
136
  );
142
137
 
143
138
  const prevPage = useCallback(
144
139
  (startCursor: string) => {
145
- collection?.prevPage(startCursor);
140
+ control?.prevPage(startCursor);
146
141
  },
147
- [collection],
142
+ [control],
148
143
  );
149
144
 
150
- const hasPrevPage = collection?.hasPrevPage ?? false;
151
- const hasNextPage = collection?.hasNextPage ?? false;
145
+ const hasPrevPage = control?.hasPrevPage ?? false;
146
+ const hasNextPage = control?.hasNextPage ?? false;
152
147
 
153
148
  // ---------------------------------------------------------------------------
154
149
  // Row Operations (Optimistic Updates)
@@ -209,19 +204,19 @@ export function useDataTable<TRow extends Record<string, unknown>>(
209
204
  );
210
205
 
211
206
  // ---------------------------------------------------------------------------
212
- // Sort (delegated from collection)
207
+ // Sort (delegated from control)
213
208
  // ---------------------------------------------------------------------------
214
209
  const sortStates = useMemo<SortState[]>(() => {
215
- return collection?.sortStates ?? [];
216
- }, [collection?.sortStates]);
210
+ return control?.sortStates ?? [];
211
+ }, [control?.sortStates]);
217
212
 
218
213
  const onSort = useMemo<
219
214
  ((field: string, direction?: "Asc" | "Desc") => void) | undefined
220
215
  >(() => {
221
- if (!collection) return undefined;
216
+ if (!control) return undefined;
222
217
  return (field: string, direction?: "Asc" | "Desc") =>
223
- collection.setSort(field, direction);
224
- }, [collection]);
218
+ control.setSort(field, direction);
219
+ }, [control]);
225
220
 
226
221
  // ---------------------------------------------------------------------------
227
222
  // Return
@@ -254,8 +249,8 @@ export function useDataTable<TRow extends Record<string, unknown>>(
254
249
  deleteRow,
255
250
  insertRow,
256
251
 
257
- // Collection (passthrough for DataTable.Provider)
258
- collection,
252
+ // Control (passthrough for DataTable.Provider)
253
+ control,
259
254
 
260
255
  // Row interaction (passthrough for DataTable.Provider)
261
256
  onClickRow,
@@ -75,7 +75,6 @@ describe("inferColumns()", () => {
75
75
  const metadata = {
76
76
  name: "task",
77
77
  pluralForm: "tasks",
78
- readAllowedRoles: [],
79
78
  fields: [
80
79
  { name: "id", type: "uuid", required: true },
81
80
  { name: "title", type: "string", required: true },
@@ -204,7 +203,6 @@ describe("inferColumns() with metadata", () => {
204
203
  task: {
205
204
  name: "task",
206
205
  pluralForm: "tasks",
207
- readAllowedRoles: [],
208
206
  fields: [
209
207
  { name: "id", type: "uuid", required: true },
210
208
  { name: "title", type: "string", required: true },
@@ -370,7 +368,6 @@ describe("createColumnHelper()", () => {
370
368
  const metadata = {
371
369
  name: "order",
372
370
  pluralForm: "orders",
373
- readAllowedRoles: [],
374
371
  fields: [
375
372
  { name: "id", type: "uuid", required: true },
376
373
  { name: "name", type: "string", required: true },
@@ -390,7 +387,6 @@ describe("createColumnHelper()", () => {
390
387
  const metadata = {
391
388
  name: "order",
392
389
  pluralForm: "orders",
393
- readAllowedRoles: [],
394
390
  fields: [
395
391
  { name: "id", type: "uuid", required: true },
396
392
  { name: "name", type: "string", required: true },
@@ -18,6 +18,7 @@ export type {
18
18
  ColumnDefinition,
19
19
  UseCollectionOptions,
20
20
  UseCollectionReturn,
21
+ CollectionControl,
21
22
  UseDataTableOptions,
22
23
  UseDataTableReturn,
23
24
  RowAction,
@@ -44,8 +45,8 @@ export {
44
45
  // Collection
45
46
  export { useCollectionVariables } from "./collection/use-collection";
46
47
  export {
47
- CollectionVariablesProvider,
48
- useCollectionVariablesContext,
48
+ CollectionControlProvider,
49
+ useCollectionControl,
49
50
  } from "./collection/collection-provider";
50
51
 
51
52
  // Table (static)
@@ -274,6 +274,23 @@ type FieldTypeToTSType = {
274
274
  enum: string;
275
275
  };
276
276
 
277
+ /**
278
+ * Resolves the value type for a field, narrowing enum fields to their
279
+ * literal union when `enumValues` is present in the metadata.
280
+ *
281
+ * - enum field with `enumValues: readonly ["OPEN", "CLOSED"]` → `"OPEN" | "CLOSED"`
282
+ * - enum field without `enumValues` → `string` (fallback)
283
+ * - all other field types → `FieldTypeToTSType[T]` (unchanged)
284
+ */
285
+ type ResolveFieldValueType<
286
+ F,
287
+ T extends keyof FieldTypeToTSType,
288
+ > = T extends "enum"
289
+ ? F extends { readonly enumValues: readonly (infer V extends string)[] }
290
+ ? V
291
+ : string
292
+ : FieldTypeToTSType[T];
293
+
277
294
  /**
278
295
  * Resolves the value type for a filter operator.
279
296
  * `in`/`nin` operators accept arrays, `between` accepts `{ min, max }`,
@@ -324,7 +341,7 @@ export type BuildQueryVariables<TTable extends TableMetadata> = {
324
341
  }
325
342
  ? FieldTypeToFilterConfigType[T] extends infer FCT extends
326
343
  FilterConfig["type"]
327
- ? FilterInputForFieldType<FCT, FieldTypeToTSType[T]>
344
+ ? FilterInputForFieldType<FCT, ResolveFieldValueType<F, T>>
328
345
  : never
329
346
  : never;
330
347
  };
@@ -449,40 +466,25 @@ export interface UseCollectionOptions<
449
466
  }
450
467
 
451
468
  /**
452
- * Return type of `useCollectionVariables` hook.
469
+ * Collection control interface for UI components to interact with
470
+ * filter, sort, and pagination state.
453
471
  *
454
472
  * Methods that accept a field name are typed with `TFieldName` so that
455
473
  * auto-completion works when a concrete union is supplied.
456
474
  *
457
475
  * **Note:** Methods that accept `TFieldName` use *method syntax* intentionally
458
- * so that `UseCollectionReturn<"a" | "b">` remains assignable to
459
- * `UseCollectionReturn<string>` (bivariant method check).
476
+ * so that `CollectionControl<"a" | "b">` remains assignable to
477
+ * `CollectionControl<string>` (bivariant method check).
460
478
  * Methods that don't depend on `TFieldName` use property syntax so they
461
479
  * can be safely destructured without triggering `unbound-method` lint rules.
462
480
  *
463
481
  * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
464
- * @typeParam TVariables - Type of `variables` property.
482
+ * @typeParam TFilter - Filter type (default: `Filter<TFieldName>`).
465
483
  */
466
- export interface UseCollectionReturn<
484
+ export interface CollectionControl<
467
485
  TFieldName extends string = string,
468
- TVariables = CollectionVariables,
469
486
  TFilter = Filter<TFieldName>,
470
487
  > {
471
- /**
472
- * Collection variables split into explicit sub-properties
473
- * for direct mapping to GraphQL query variables.
474
- *
475
- * @example
476
- * ```tsx
477
- * const { query, order, pagination } = collection.variables;
478
- * const [result] = useQuery({
479
- * query: GET_TASKS,
480
- * variables: { ...pagination, query, order },
481
- * });
482
- * ```
483
- */
484
- variables: TVariables;
485
-
486
488
  // Filter operations
487
489
  /** Current active filters */
488
490
  filters: Filter[];
@@ -540,6 +542,49 @@ export interface UseCollectionReturn<
540
542
  setTotal: (total: number) => void;
541
543
  }
542
544
 
545
+ /**
546
+ * Return type of `useCollectionVariables` hook.
547
+ *
548
+ * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
549
+ * @typeParam TVariables - Type of `variables` property.
550
+ * @typeParam TFilter - Filter type (default: `Filter<TFieldName>`).
551
+ */
552
+ export interface UseCollectionReturn<
553
+ TFieldName extends string = string,
554
+ TVariables = CollectionVariables,
555
+ TFilter = Filter<TFieldName>,
556
+ > {
557
+ /**
558
+ * Collection variables split into explicit sub-properties
559
+ * for direct mapping to GraphQL query variables.
560
+ *
561
+ * @example
562
+ * ```tsx
563
+ * const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
564
+ * const { query, order, pagination } = variables;
565
+ * const [result] = useQuery({
566
+ * query: GET_TASKS,
567
+ * variables: { ...pagination, query, order },
568
+ * });
569
+ * ```
570
+ */
571
+ variables: TVariables;
572
+
573
+ /**
574
+ * Collection control interface for UI components.
575
+ *
576
+ * Pass this to `useDataTable({ control })` to connect filter, sort,
577
+ * and pagination state to the data table.
578
+ *
579
+ * @example
580
+ * ```tsx
581
+ * const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
582
+ * const table = useDataTable({ columns, data: result.data?.orders, control });
583
+ * ```
584
+ */
585
+ control: CollectionControl<TFieldName, TFilter>;
586
+ }
587
+
543
588
  // =============================================================================
544
589
  // useDataTable Types
545
590
  // =============================================================================
@@ -556,8 +601,8 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
556
601
  loading?: boolean;
557
602
  /** Error */
558
603
  error?: Error | null;
559
- /** Collection state for sort/pagination integration */
560
- collection?: UseCollectionReturn<string, unknown>;
604
+ /** Collection control for sort/filter/pagination integration */
605
+ control?: CollectionControl;
561
606
  /** Handler called when a row is clicked */
562
607
  onClickRow?: (row: TRow) => void;
563
608
  /** Row action definitions for the actions column */
@@ -663,9 +708,9 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
663
708
  /** Optimistically insert a row */
664
709
  insertRow: (row: TRow) => { rollback: () => void };
665
710
 
666
- // Collection (passthrough for DataTable.Provider)
667
- /** Collection state passed through from options */
668
- collection: UseCollectionReturn<string, unknown> | undefined;
711
+ // Control (passthrough for DataTable.Provider)
712
+ /** Collection control passed through from options */
713
+ control: CollectionControl | undefined;
669
714
 
670
715
  // Row interaction (passthrough for DataTable.Provider)
671
716
  /** Handler called when a row is clicked */
@@ -86,7 +86,6 @@ export interface TableMetadata {
86
86
  readonly name: string;
87
87
  readonly pluralForm: string;
88
88
  readonly description?: string;
89
- readonly readAllowedRoles: readonly string[];
90
89
  readonly fields: readonly FieldMetadata[];
91
90
  /** Relations (manyToOne, oneToOne, and oneToMany) */
92
91
  readonly relations?: readonly RelationMetadata[];
@@ -112,7 +111,6 @@ interface ProcessedTable {
112
111
  pluralForm: string;
113
112
  originalName: string; // PascalCase name for relation lookup
114
113
  description?: string;
115
- readAllowedRoles: string[];
116
114
  fields: FieldMetadata[];
117
115
  relations: RelationMetadata[];
118
116
  }
@@ -154,63 +152,6 @@ function toCamelCase(str: string): string {
154
152
  return str.charAt(0).toLowerCase() + str.slice(1);
155
153
  }
156
154
 
157
- /**
158
- * Extract allowed roles from gql permission policies
159
- * Only extracts roles from 'read' action policies with 'allow' permit
160
- */
161
- function extractReadAllowedRoles(
162
- gqlPermission?: readonly {
163
- conditions: readonly unknown[];
164
- actions: readonly ["all"] | readonly string[];
165
- permit: "allow" | "deny";
166
- description?: string;
167
- }[],
168
- ): string[] {
169
- if (!gqlPermission) return [];
170
-
171
- const roles = new Set<string>();
172
-
173
- for (const policy of gqlPermission) {
174
- // Only process 'allow' policies that include 'read' action
175
- if (policy.permit !== "allow") continue;
176
- const actions = policy.actions as readonly string[];
177
- if (!actions.includes("all") && !actions.includes("read")) continue;
178
-
179
- // Extract roles from conditions
180
- for (const condition of policy.conditions) {
181
- if (!Array.isArray(condition) || condition.length < 3) continue;
182
-
183
- const [left, operator, right] = condition;
184
-
185
- // Check for pattern: ["ROLE_NAME", "in", { user: "roles" }]
186
- if (
187
- typeof left === "string" &&
188
- operator === "in" &&
189
- typeof right === "object" &&
190
- right !== null &&
191
- "user" in right &&
192
- (right as { user: string }).user === "roles"
193
- ) {
194
- roles.add(left);
195
- }
196
-
197
- // Check for pattern: [{ user: "roles" }, "in", "ROLE_NAME"] (reversed)
198
- if (
199
- typeof right === "string" &&
200
- operator === "in" &&
201
- typeof left === "object" &&
202
- left !== null &&
203
- "user" in left &&
204
- (left as { user: string }).user === "roles"
205
- ) {
206
- roles.add(right);
207
- }
208
- }
209
- }
210
-
211
- return Array.from(roles);
212
- }
213
-
214
155
  /**
215
156
  * Parsed field type from TailorDB
216
157
  */
@@ -402,15 +343,11 @@ export function dataViewerMetadataGenerator(
402
343
  fields.push(fieldMetadata);
403
344
  }
404
345
 
405
- // Extract read allowed roles from gql permission
406
- const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
407
-
408
346
  return {
409
347
  name: toCamelCase(type.name),
410
348
  pluralForm: toCamelCase(type.pluralForm),
411
349
  originalName: type.name,
412
350
  description: type.description,
413
- readAllowedRoles,
414
351
  fields,
415
352
  relations,
416
353
  };
@@ -1,9 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { vi } from "vitest";
3
- import type { Column, UseCollectionReturn } from "../component/types";
3
+ import type { Column, CollectionControl } from "../component/types";
4
4
  import { DataTableContext } from "../component/data-table/data-table-context";
5
5
  import type { DataTableContextValue } from "../component/data-table/data-table-context";
6
- import { CollectionVariablesProvider } from "../component/collection/collection-provider";
6
+ import { CollectionControlProvider } from "../component/collection/collection-provider";
7
7
 
8
8
  // =============================================================================
9
9
  // Mock factory: DataTableContext
@@ -47,14 +47,9 @@ export function createMockDataTableContext<T extends Record<string, unknown>>(
47
47
  // =============================================================================
48
48
 
49
49
  export function createMockCollectionContext(
50
- overrides?: Partial<UseCollectionReturn<string, unknown>>,
51
- ): UseCollectionReturn<string, unknown> {
50
+ overrides?: Partial<CollectionControl>,
51
+ ): CollectionControl {
52
52
  return {
53
- variables: {
54
- query: undefined,
55
- order: undefined,
56
- pagination: { first: 20 },
57
- },
58
53
  filters: [],
59
54
  addFilter: vi.fn(),
60
55
  setFilters: vi.fn(),
@@ -110,7 +105,7 @@ export function createTestProviders<
110
105
  columns: Column<T>[];
111
106
  rows: T[];
112
107
  dataTableDefaults?: Partial<DataTableContextValue<T>>;
113
- collectionDefaults?: Partial<UseCollectionReturn<string, unknown>>;
108
+ collectionDefaults?: Partial<CollectionControl>;
114
109
  }) {
115
110
  return function TestProviders({
116
111
  children,
@@ -119,10 +114,10 @@ export function createTestProviders<
119
114
  }: {
120
115
  children: ReactNode;
121
116
  dataTable?: Partial<DataTableContextValue<T>>;
122
- collection?: Partial<UseCollectionReturn<string, unknown>>;
117
+ collection?: Partial<CollectionControl>;
123
118
  }) {
124
119
  return (
125
- <CollectionVariablesProvider
120
+ <CollectionControlProvider
126
121
  value={createMockCollectionContext({
127
122
  ...defaults.collectionDefaults,
128
123
  ...collection,
@@ -136,7 +131,7 @@ export function createTestProviders<
136
131
  >
137
132
  {children}
138
133
  </DataTableContext.Provider>
139
- </CollectionVariablesProvider>
134
+ </CollectionControlProvider>
140
135
  );
141
136
  };
142
137
  }