@izumisy-tailor/tailor-data-viewer 0.2.14 → 0.2.16
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/README.md +2 -2
- package/package.json +1 -1
- package/src/component/collection/use-collection.test.ts +46 -25
- package/src/component/collection/use-collection.ts +62 -53
- package/src/component/collection/use-collection.typetest.ts +140 -21
- package/src/component/data-table/use-data-table.test.ts +8 -1
- package/src/component/data-table/use-data-table.ts +23 -4
- package/src/component/field-helpers.test.ts +19 -45
- package/src/component/field-helpers.ts +13 -22
- package/src/component/index.ts +4 -1
- package/src/component/pagination.tsx +7 -2
- package/src/component/types.ts +168 -43
package/README.md
CHANGED
|
@@ -215,7 +215,7 @@ export default defineConfig({
|
|
|
215
215
|
tailor-sdk generate
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
### `inferColumnHelper(
|
|
218
|
+
### `inferColumnHelper(tableMetadata)`
|
|
219
219
|
|
|
220
220
|
`field()` requires manually specifying `sort`/`filter` type configs and enum `options` for every column. `inferColumnHelper()` eliminates this boilerplate by automatically deriving these from the generated table metadata. Based on each field's type (string, number, date, enum, etc.), the appropriate `SortConfig` / `FilterConfig` is set automatically, and enum fields get their options populated from the schema.
|
|
221
221
|
|
|
@@ -223,7 +223,7 @@ tailor-sdk generate
|
|
|
223
223
|
import { inferColumnHelper, display } from "@izumisy-tailor/tailor-data-viewer/component";
|
|
224
224
|
import { tableMetadata } from "./generated/data-viewer-metadata.generated";
|
|
225
225
|
|
|
226
|
-
const { column, columns } = inferColumnHelper(tableMetadata
|
|
226
|
+
const { column, columns } = inferColumnHelper(tableMetadata.task);
|
|
227
227
|
|
|
228
228
|
const taskColumns = [
|
|
229
229
|
column("title"), // sort/filter auto-configured from metadata
|
package/package.json
CHANGED
|
@@ -243,7 +243,7 @@ describe("useCollection", () => {
|
|
|
243
243
|
// Pagination operations
|
|
244
244
|
// ---------------------------------------------------------------------------
|
|
245
245
|
describe("pagination operations", () => {
|
|
246
|
-
it("navigates to next page", () => {
|
|
246
|
+
it("navigates to next page (forward)", () => {
|
|
247
247
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
248
248
|
|
|
249
249
|
act(() => {
|
|
@@ -251,54 +251,75 @@ describe("useCollection", () => {
|
|
|
251
251
|
});
|
|
252
252
|
|
|
253
253
|
expect(result.current.cursor).toBe("cursor1");
|
|
254
|
-
expect(result.current.
|
|
254
|
+
expect(result.current.paginationDirection).toBe("forward");
|
|
255
255
|
expect(result.current.toQueryArgs().variables.after).toBe("cursor1");
|
|
256
|
+
expect(result.current.toQueryArgs().variables.first).toBe(20);
|
|
257
|
+
expect(result.current.toQueryArgs().variables.last).toBeUndefined();
|
|
258
|
+
expect(result.current.toQueryArgs().variables.before).toBeUndefined();
|
|
256
259
|
});
|
|
257
260
|
|
|
258
|
-
it("navigates
|
|
261
|
+
it("navigates to previous page (backward)", () => {
|
|
259
262
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
260
263
|
|
|
261
264
|
act(() => {
|
|
262
|
-
result.current.
|
|
263
|
-
});
|
|
264
|
-
act(() => {
|
|
265
|
-
result.current.nextPage("cursor2");
|
|
266
|
-
});
|
|
267
|
-
act(() => {
|
|
268
|
-
result.current.prevPage();
|
|
265
|
+
result.current.prevPage("cursor1");
|
|
269
266
|
});
|
|
270
267
|
|
|
271
268
|
expect(result.current.cursor).toBe("cursor1");
|
|
272
|
-
expect(result.current.
|
|
269
|
+
expect(result.current.paginationDirection).toBe("backward");
|
|
270
|
+
expect(result.current.toQueryArgs().variables.before).toBe("cursor1");
|
|
271
|
+
expect(result.current.toQueryArgs().variables.last).toBe(20);
|
|
272
|
+
expect(result.current.toQueryArgs().variables.first).toBeUndefined();
|
|
273
|
+
expect(result.current.toQueryArgs().variables.after).toBeUndefined();
|
|
273
274
|
});
|
|
274
275
|
|
|
275
|
-
it("
|
|
276
|
+
it("switches direction on nextPage after prevPage", () => {
|
|
276
277
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
277
278
|
|
|
278
279
|
act(() => {
|
|
279
|
-
result.current.
|
|
280
|
+
result.current.prevPage("cursorB");
|
|
280
281
|
});
|
|
282
|
+
expect(result.current.paginationDirection).toBe("backward");
|
|
283
|
+
|
|
281
284
|
act(() => {
|
|
282
|
-
result.current.
|
|
285
|
+
result.current.nextPage("cursorA");
|
|
283
286
|
});
|
|
284
|
-
|
|
285
|
-
expect(result.current.
|
|
286
|
-
expect(result.current.
|
|
287
|
+
expect(result.current.paginationDirection).toBe("forward");
|
|
288
|
+
expect(result.current.toQueryArgs().variables.after).toBe("cursorA");
|
|
289
|
+
expect(result.current.toQueryArgs().variables.first).toBe(20);
|
|
287
290
|
});
|
|
288
291
|
|
|
289
|
-
it("resets page", () => {
|
|
292
|
+
it("resets page to forward direction", () => {
|
|
290
293
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
291
294
|
|
|
292
295
|
act(() => {
|
|
293
|
-
result.current.
|
|
294
|
-
result.current.nextPage("cursor2");
|
|
296
|
+
result.current.prevPage("cursor1");
|
|
295
297
|
});
|
|
296
298
|
act(() => {
|
|
297
299
|
result.current.resetPage();
|
|
298
300
|
});
|
|
299
301
|
|
|
300
302
|
expect(result.current.cursor).toBeNull();
|
|
303
|
+
expect(result.current.paginationDirection).toBe("forward");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("tracks hasPrevPage and hasNextPage from setPageInfo", () => {
|
|
307
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
308
|
+
|
|
301
309
|
expect(result.current.hasPrevPage).toBe(false);
|
|
310
|
+
expect(result.current.hasNextPage).toBe(false);
|
|
311
|
+
|
|
312
|
+
act(() => {
|
|
313
|
+
result.current.setPageInfo({
|
|
314
|
+
hasNextPage: true,
|
|
315
|
+
endCursor: "end",
|
|
316
|
+
hasPreviousPage: true,
|
|
317
|
+
startCursor: "start",
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(result.current.hasPrevPage).toBe(true);
|
|
322
|
+
expect(result.current.hasNextPage).toBe(true);
|
|
302
323
|
});
|
|
303
324
|
});
|
|
304
325
|
|
|
@@ -346,6 +367,8 @@ describe("useCollection", () => {
|
|
|
346
367
|
expect("query" in variables).toBe(false);
|
|
347
368
|
expect("order" in variables).toBe(false);
|
|
348
369
|
expect("after" in variables).toBe(false);
|
|
370
|
+
expect("last" in variables).toBe(false);
|
|
371
|
+
expect("before" in variables).toBe(false);
|
|
349
372
|
});
|
|
350
373
|
|
|
351
374
|
it("includes query in toQueryArgs result", () => {
|
|
@@ -386,11 +409,10 @@ describe("useCollection", () => {
|
|
|
386
409
|
},
|
|
387
410
|
} as const satisfies TableMetadataMap;
|
|
388
411
|
|
|
389
|
-
it("works with
|
|
412
|
+
it("works with tableMetadata", () => {
|
|
390
413
|
const { result } = renderHook(() =>
|
|
391
414
|
useCollection({
|
|
392
|
-
|
|
393
|
-
tableName: "task",
|
|
415
|
+
tableMetadata: testMetadata.task,
|
|
394
416
|
query: FAKE_QUERY,
|
|
395
417
|
params: { pageSize: 10 },
|
|
396
418
|
}),
|
|
@@ -401,8 +423,7 @@ describe("useCollection", () => {
|
|
|
401
423
|
it("applies typed initialSort", () => {
|
|
402
424
|
const { result } = renderHook(() =>
|
|
403
425
|
useCollection({
|
|
404
|
-
|
|
405
|
-
tableName: "task",
|
|
426
|
+
tableMetadata: testMetadata.task,
|
|
406
427
|
query: FAKE_QUERY,
|
|
407
428
|
params: {
|
|
408
429
|
initialSort: [{ field: "dueDate", direction: "Desc" }],
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TableMetadata } from "../../generator/metadata-generator";
|
|
3
3
|
import type {
|
|
4
4
|
Filter,
|
|
5
5
|
FilterOperator,
|
|
6
|
-
|
|
6
|
+
TableMetadataFilter,
|
|
7
|
+
PageInfo,
|
|
7
8
|
QueryVariables,
|
|
8
9
|
SortState,
|
|
9
10
|
UseCollectionOptions,
|
|
@@ -11,7 +12,7 @@ import type {
|
|
|
11
12
|
ExtractQueryVariables,
|
|
12
13
|
ValidateCollectionQuery,
|
|
13
14
|
} from "../types";
|
|
14
|
-
import type {
|
|
15
|
+
import type { TableFieldName, TableOrderableFieldName } from "../types";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Resolves the variables type for `toQueryArgs()` return value.
|
|
@@ -46,8 +47,7 @@ type ResolveVariables<TQuery> =
|
|
|
46
47
|
* import { tableMetadata } from "./generated/data-viewer-metadata.generated";
|
|
47
48
|
*
|
|
48
49
|
* const collection = useCollection({
|
|
49
|
-
*
|
|
50
|
-
* tableName: "task",
|
|
50
|
+
* tableMetadata: tableMetadata.task,
|
|
51
51
|
* query: GET_TASKS,
|
|
52
52
|
* params: { pageSize: 20 },
|
|
53
53
|
* });
|
|
@@ -55,29 +55,27 @@ type ResolveVariables<TQuery> =
|
|
|
55
55
|
* ```
|
|
56
56
|
*/
|
|
57
57
|
export function useCollection<
|
|
58
|
-
const
|
|
59
|
-
TTableName extends string & keyof TMetadata,
|
|
58
|
+
const TTable extends TableMetadata,
|
|
60
59
|
TQuery,
|
|
61
60
|
>(
|
|
62
61
|
options: UseCollectionOptions<
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
TableFieldName<TTable>,
|
|
63
|
+
TableMetadataFilter<TTable>
|
|
65
64
|
> & {
|
|
66
|
-
|
|
67
|
-
tableName: TTableName;
|
|
65
|
+
tableMetadata: TTable;
|
|
68
66
|
query: ValidateCollectionQuery<
|
|
69
67
|
TQuery,
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
TableFieldName<TTable>,
|
|
69
|
+
TableOrderableFieldName<TTable>
|
|
72
70
|
>;
|
|
73
71
|
},
|
|
74
72
|
): UseCollectionReturn<
|
|
75
|
-
|
|
73
|
+
TableFieldName<TTable>,
|
|
76
74
|
{
|
|
77
75
|
query: TQuery;
|
|
78
76
|
variables: ResolveVariables<TQuery>;
|
|
79
77
|
},
|
|
80
|
-
|
|
78
|
+
TableMetadataFilter<TTable>
|
|
81
79
|
>;
|
|
82
80
|
|
|
83
81
|
/**
|
|
@@ -98,8 +96,7 @@ export function useCollection<
|
|
|
98
96
|
export function useCollection<TQuery>(
|
|
99
97
|
options: UseCollectionOptions & {
|
|
100
98
|
query: TQuery;
|
|
101
|
-
|
|
102
|
-
tableName?: never;
|
|
99
|
+
tableMetadata?: never;
|
|
103
100
|
},
|
|
104
101
|
): UseCollectionReturn<
|
|
105
102
|
string,
|
|
@@ -111,8 +108,7 @@ export function useCollection<TQuery>(
|
|
|
111
108
|
// -----------------------------------------------------------------------------
|
|
112
109
|
export function useCollection(
|
|
113
110
|
options: UseCollectionOptions & {
|
|
114
|
-
|
|
115
|
-
tableName?: string;
|
|
111
|
+
tableMetadata?: TableMetadata;
|
|
116
112
|
query: unknown;
|
|
117
113
|
},
|
|
118
114
|
): UseCollectionReturn<string, { query: unknown; variables: QueryVariables }> {
|
|
@@ -130,7 +126,15 @@ export function useCollection(
|
|
|
130
126
|
const [sortStates, setSortStates] = useState<SortState[]>(initialSort);
|
|
131
127
|
const [pageSize] = useState(initialPageSize);
|
|
132
128
|
const [cursor, setCursor] = useState<string | null>(null);
|
|
133
|
-
const [
|
|
129
|
+
const [paginationDirection, setPaginationDirection] = useState<
|
|
130
|
+
"forward" | "backward"
|
|
131
|
+
>("forward");
|
|
132
|
+
const [currentPageInfo, setCurrentPageInfo] = useState<PageInfo>({
|
|
133
|
+
hasNextPage: false,
|
|
134
|
+
endCursor: null,
|
|
135
|
+
hasPreviousPage: false,
|
|
136
|
+
startCursor: null,
|
|
137
|
+
});
|
|
134
138
|
|
|
135
139
|
// ---------------------------------------------------------------------------
|
|
136
140
|
// Filter operations
|
|
@@ -153,7 +157,7 @@ export function useCollection(
|
|
|
153
157
|
});
|
|
154
158
|
// Reset pagination when filters change
|
|
155
159
|
setCursor(null);
|
|
156
|
-
|
|
160
|
+
setPaginationDirection("forward");
|
|
157
161
|
},
|
|
158
162
|
[],
|
|
159
163
|
);
|
|
@@ -162,20 +166,20 @@ export function useCollection(
|
|
|
162
166
|
setFiltersState(newFilters);
|
|
163
167
|
// Reset pagination when filters change
|
|
164
168
|
setCursor(null);
|
|
165
|
-
|
|
169
|
+
setPaginationDirection("forward");
|
|
166
170
|
}, []);
|
|
167
171
|
|
|
168
172
|
const removeFilter = useCallback((field: string) => {
|
|
169
173
|
setFiltersState((prev) => prev.filter((f) => f.field !== field));
|
|
170
174
|
// Reset pagination when filters change
|
|
171
175
|
setCursor(null);
|
|
172
|
-
|
|
176
|
+
setPaginationDirection("forward");
|
|
173
177
|
}, []);
|
|
174
178
|
|
|
175
179
|
const clearFilters = useCallback(() => {
|
|
176
180
|
setFiltersState([]);
|
|
177
181
|
setCursor(null);
|
|
178
|
-
|
|
182
|
+
setPaginationDirection("forward");
|
|
179
183
|
}, []);
|
|
180
184
|
|
|
181
185
|
// ---------------------------------------------------------------------------
|
|
@@ -194,7 +198,7 @@ export function useCollection(
|
|
|
194
198
|
});
|
|
195
199
|
// Reset pagination when sort changes
|
|
196
200
|
setCursor(null);
|
|
197
|
-
|
|
201
|
+
setPaginationDirection("forward");
|
|
198
202
|
},
|
|
199
203
|
[],
|
|
200
204
|
);
|
|
@@ -202,45 +206,52 @@ export function useCollection(
|
|
|
202
206
|
const clearSort = useCallback(() => {
|
|
203
207
|
setSortStates([]);
|
|
204
208
|
setCursor(null);
|
|
205
|
-
|
|
209
|
+
setPaginationDirection("forward");
|
|
206
210
|
}, []);
|
|
207
211
|
|
|
208
212
|
// ---------------------------------------------------------------------------
|
|
209
213
|
// Pagination operations
|
|
210
214
|
// ---------------------------------------------------------------------------
|
|
211
|
-
const nextPage = useCallback(
|
|
212
|
-
(endCursor
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
},
|
|
216
|
-
[cursor],
|
|
217
|
-
);
|
|
215
|
+
const nextPage = useCallback((endCursor: string) => {
|
|
216
|
+
setCursor(endCursor);
|
|
217
|
+
setPaginationDirection("forward");
|
|
218
|
+
}, []);
|
|
218
219
|
|
|
219
|
-
const prevPage = useCallback(() => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const previousCursor = newHistory.pop();
|
|
223
|
-
setCursor(
|
|
224
|
-
previousCursor && previousCursor !== "" ? previousCursor : null,
|
|
225
|
-
);
|
|
226
|
-
return newHistory;
|
|
227
|
-
});
|
|
220
|
+
const prevPage = useCallback((startCursor: string) => {
|
|
221
|
+
setCursor(startCursor);
|
|
222
|
+
setPaginationDirection("backward");
|
|
228
223
|
}, []);
|
|
229
224
|
|
|
230
225
|
const resetPage = useCallback(() => {
|
|
231
226
|
setCursor(null);
|
|
232
|
-
|
|
227
|
+
setPaginationDirection("forward");
|
|
233
228
|
}, []);
|
|
234
229
|
|
|
235
|
-
const
|
|
230
|
+
const setPageInfo = useCallback((pageInfo: PageInfo) => {
|
|
231
|
+
setCurrentPageInfo(pageInfo);
|
|
232
|
+
}, []);
|
|
233
|
+
|
|
234
|
+
const hasPrevPage = currentPageInfo.hasPreviousPage;
|
|
235
|
+
const hasNextPage = currentPageInfo.hasNextPage;
|
|
236
236
|
|
|
237
237
|
// ---------------------------------------------------------------------------
|
|
238
238
|
// Build query variables (Tailor Platform format)
|
|
239
239
|
// ---------------------------------------------------------------------------
|
|
240
240
|
const variables = useMemo<QueryVariables>(() => {
|
|
241
|
-
const vars: QueryVariables = {
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
const vars: QueryVariables = {};
|
|
242
|
+
|
|
243
|
+
// Pagination direction determines first/after vs last/before
|
|
244
|
+
if (paginationDirection === "forward") {
|
|
245
|
+
vars.first = pageSize;
|
|
246
|
+
if (cursor) {
|
|
247
|
+
vars.after = cursor;
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
vars.last = pageSize;
|
|
251
|
+
if (cursor) {
|
|
252
|
+
vars.before = cursor;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
244
255
|
|
|
245
256
|
// Build query (filters)
|
|
246
257
|
if (filters.length > 0) {
|
|
@@ -259,13 +270,8 @@ export function useCollection(
|
|
|
259
270
|
}));
|
|
260
271
|
}
|
|
261
272
|
|
|
262
|
-
// Cursor
|
|
263
|
-
if (cursor) {
|
|
264
|
-
vars.after = cursor;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
273
|
return vars;
|
|
268
|
-
}, [filters, sortStates, pageSize, cursor]);
|
|
274
|
+
}, [filters, sortStates, pageSize, cursor, paginationDirection]);
|
|
269
275
|
|
|
270
276
|
// ---------------------------------------------------------------------------
|
|
271
277
|
// toQueryArgs
|
|
@@ -291,9 +297,12 @@ export function useCollection(
|
|
|
291
297
|
clearSort,
|
|
292
298
|
pageSize,
|
|
293
299
|
cursor,
|
|
300
|
+
paginationDirection,
|
|
294
301
|
nextPage,
|
|
295
302
|
prevPage,
|
|
296
303
|
resetPage,
|
|
297
304
|
hasPrevPage,
|
|
305
|
+
hasNextPage,
|
|
306
|
+
setPageInfo,
|
|
298
307
|
};
|
|
299
308
|
}
|
|
@@ -34,6 +34,8 @@ type FakeDoc<Variables> = {
|
|
|
34
34
|
type DocOk = FakeDoc<{
|
|
35
35
|
first?: number | null;
|
|
36
36
|
after?: string | null;
|
|
37
|
+
last?: number | null;
|
|
38
|
+
before?: string | null;
|
|
37
39
|
query?: { name?: unknown } | null;
|
|
38
40
|
order?: readonly { field?: "name" | null }[] | null;
|
|
39
41
|
}>;
|
|
@@ -44,12 +46,80 @@ export const assert1: Assert1 = true;
|
|
|
44
46
|
// ❌ Query missing $first should fail
|
|
45
47
|
type DocNoFirst = FakeDoc<{
|
|
46
48
|
after?: string | null;
|
|
49
|
+
last?: number | null;
|
|
50
|
+
before?: string | null;
|
|
47
51
|
query?: { name?: unknown } | null;
|
|
52
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
48
53
|
}>;
|
|
49
54
|
type Fail1 = ValidateCollectionQuery<DocNoFirst, "name">;
|
|
50
55
|
type AssertFail1 = HasCollectionQueryError<Fail1> extends true ? true : never;
|
|
51
56
|
export const assertFail1: AssertFail1 = true;
|
|
52
57
|
|
|
58
|
+
// ❌ Query missing $after should fail
|
|
59
|
+
type DocNoAfter = FakeDoc<{
|
|
60
|
+
first?: number | null;
|
|
61
|
+
last?: number | null;
|
|
62
|
+
before?: string | null;
|
|
63
|
+
query?: { name?: unknown } | null;
|
|
64
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
65
|
+
}>;
|
|
66
|
+
type FailNoAfter = ValidateCollectionQuery<DocNoAfter, "name">;
|
|
67
|
+
type AssertFailNoAfter =
|
|
68
|
+
HasCollectionQueryError<FailNoAfter> extends true ? true : never;
|
|
69
|
+
export const assertFailNoAfter: AssertFailNoAfter = true;
|
|
70
|
+
|
|
71
|
+
// ❌ Query missing $last should fail
|
|
72
|
+
type DocNoLast = FakeDoc<{
|
|
73
|
+
first?: number | null;
|
|
74
|
+
after?: string | null;
|
|
75
|
+
before?: string | null;
|
|
76
|
+
query?: { name?: unknown } | null;
|
|
77
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
78
|
+
}>;
|
|
79
|
+
type FailNoLast = ValidateCollectionQuery<DocNoLast, "name">;
|
|
80
|
+
type AssertFailNoLast =
|
|
81
|
+
HasCollectionQueryError<FailNoLast> extends true ? true : never;
|
|
82
|
+
export const assertFailNoLast: AssertFailNoLast = true;
|
|
83
|
+
|
|
84
|
+
// ❌ Query missing $before should fail
|
|
85
|
+
type DocNoBefore = FakeDoc<{
|
|
86
|
+
first?: number | null;
|
|
87
|
+
after?: string | null;
|
|
88
|
+
last?: number | null;
|
|
89
|
+
query?: { name?: unknown } | null;
|
|
90
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
91
|
+
}>;
|
|
92
|
+
type FailNoBefore = ValidateCollectionQuery<DocNoBefore, "name">;
|
|
93
|
+
type AssertFailNoBefore =
|
|
94
|
+
HasCollectionQueryError<FailNoBefore> extends true ? true : never;
|
|
95
|
+
export const assertFailNoBefore: AssertFailNoBefore = true;
|
|
96
|
+
|
|
97
|
+
// ❌ Query missing $query should fail
|
|
98
|
+
type DocNoQuery = FakeDoc<{
|
|
99
|
+
first?: number | null;
|
|
100
|
+
after?: string | null;
|
|
101
|
+
last?: number | null;
|
|
102
|
+
before?: string | null;
|
|
103
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
104
|
+
}>;
|
|
105
|
+
type FailNoQuery = ValidateCollectionQuery<DocNoQuery, "name">;
|
|
106
|
+
type AssertFailNoQuery =
|
|
107
|
+
HasCollectionQueryError<FailNoQuery> extends true ? true : never;
|
|
108
|
+
export const assertFailNoQuery: AssertFailNoQuery = true;
|
|
109
|
+
|
|
110
|
+
// ❌ Query missing $order should fail
|
|
111
|
+
type DocNoOrder = FakeDoc<{
|
|
112
|
+
first?: number | null;
|
|
113
|
+
after?: string | null;
|
|
114
|
+
last?: number | null;
|
|
115
|
+
before?: string | null;
|
|
116
|
+
query?: { name?: unknown } | null;
|
|
117
|
+
}>;
|
|
118
|
+
type FailNoOrder = ValidateCollectionQuery<DocNoOrder, "name">;
|
|
119
|
+
type AssertFailNoOrder =
|
|
120
|
+
HasCollectionQueryError<FailNoOrder> extends true ? true : never;
|
|
121
|
+
export const assertFailNoOrder: AssertFailNoOrder = true;
|
|
122
|
+
|
|
53
123
|
// =============================================================================
|
|
54
124
|
// 2. OrderInput field compatibility
|
|
55
125
|
// =============================================================================
|
|
@@ -57,6 +127,10 @@ export const assertFail1: AssertFail1 = true;
|
|
|
57
127
|
// ❌ Metadata has "name" | "email" but OrderInput only allows "name"
|
|
58
128
|
type DocNarrowOrder = FakeDoc<{
|
|
59
129
|
first?: number | null;
|
|
130
|
+
after?: string | null;
|
|
131
|
+
last?: number | null;
|
|
132
|
+
before?: string | null;
|
|
133
|
+
query?: { name?: unknown; email?: unknown } | null;
|
|
60
134
|
order?: readonly { field?: "name" | null }[] | null;
|
|
61
135
|
}>;
|
|
62
136
|
type Fail2 = ValidateCollectionQuery<DocNarrowOrder, "name" | "email">;
|
|
@@ -66,6 +140,10 @@ export const assertFail2: AssertFail2 = true;
|
|
|
66
140
|
// ✅ OrderInput allows all metadata fields
|
|
67
141
|
type DocWideOrder = FakeDoc<{
|
|
68
142
|
first?: number | null;
|
|
143
|
+
after?: string | null;
|
|
144
|
+
last?: number | null;
|
|
145
|
+
before?: string | null;
|
|
146
|
+
query?: { name?: unknown; email?: unknown } | null;
|
|
69
147
|
order?: readonly { field?: "name" | "email" | "phone" | null }[] | null;
|
|
70
148
|
}>;
|
|
71
149
|
type Pass2 = ValidateCollectionQuery<DocWideOrder, "name" | "email">;
|
|
@@ -79,6 +157,9 @@ export const assertPass2: AssertPass2 = true;
|
|
|
79
157
|
// ❌ Metadata has "name" | "phone" but QueryInput only has "name"
|
|
80
158
|
type DocNarrowQuery = FakeDoc<{
|
|
81
159
|
first?: number | null;
|
|
160
|
+
after?: string | null;
|
|
161
|
+
last?: number | null;
|
|
162
|
+
before?: string | null;
|
|
82
163
|
query?: { name?: unknown } | null;
|
|
83
164
|
order?: readonly { field?: "name" | "phone" | null }[] | null;
|
|
84
165
|
}>;
|
|
@@ -89,6 +170,9 @@ export const assertFail3: AssertFail3 = true;
|
|
|
89
170
|
// ✅ QueryInput has all metadata fields
|
|
90
171
|
type DocWideQuery = FakeDoc<{
|
|
91
172
|
first?: number | null;
|
|
173
|
+
after?: string | null;
|
|
174
|
+
last?: number | null;
|
|
175
|
+
before?: string | null;
|
|
92
176
|
query?: { name?: unknown; phone?: unknown; email?: unknown } | null;
|
|
93
177
|
order?: readonly { field?: "name" | "phone" | "email" | null }[] | null;
|
|
94
178
|
}>;
|
|
@@ -100,20 +184,33 @@ export const assertPass3: AssertPass3 = true;
|
|
|
100
184
|
// 4. Variable name typo detection
|
|
101
185
|
// =============================================================================
|
|
102
186
|
|
|
103
|
-
// Query uses $filter instead of $query (typo) —
|
|
104
|
-
//
|
|
105
|
-
// But if the order is also correct, this particular typo would not be caught
|
|
106
|
-
// at the useCollection level. It WILL be caught when spreading into useQuery()
|
|
107
|
-
// because urql checks that variables match the document.
|
|
187
|
+
// ❌ Query uses $filter instead of $query (typo) — now caught because $query
|
|
188
|
+
// is required.
|
|
108
189
|
type DocFilterTypo = FakeDoc<{
|
|
109
190
|
first?: number | null;
|
|
191
|
+
after?: string | null;
|
|
192
|
+
last?: number | null;
|
|
193
|
+
before?: string | null;
|
|
110
194
|
filter?: { name?: unknown } | null;
|
|
111
195
|
order?: readonly { field?: "name" | null }[] | null;
|
|
112
196
|
}>;
|
|
113
197
|
type Typo1 = ValidateCollectionQuery<DocFilterTypo, "name">;
|
|
114
|
-
type AssertTypo1 = HasCollectionQueryError<Typo1> extends true ?
|
|
198
|
+
type AssertTypo1 = HasCollectionQueryError<Typo1> extends true ? true : never;
|
|
115
199
|
export const assertTypo1: AssertTypo1 = true;
|
|
116
200
|
|
|
201
|
+
// ❌ Query uses $queryy instead of $query (typo) — now caught
|
|
202
|
+
type DocQueryTypo = FakeDoc<{
|
|
203
|
+
first?: number | null;
|
|
204
|
+
after?: string | null;
|
|
205
|
+
last?: number | null;
|
|
206
|
+
before?: string | null;
|
|
207
|
+
queryy?: { name?: unknown } | null;
|
|
208
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
209
|
+
}>;
|
|
210
|
+
type Typo2 = ValidateCollectionQuery<DocQueryTypo, "name">;
|
|
211
|
+
type AssertTypo2 = HasCollectionQueryError<Typo2> extends true ? true : never;
|
|
212
|
+
export const assertTypo2: AssertTypo2 = true;
|
|
213
|
+
|
|
117
214
|
// =============================================================================
|
|
118
215
|
// 5. No gql-tada (plain DocumentNode) — all checks skipped
|
|
119
216
|
// =============================================================================
|
|
@@ -124,13 +221,26 @@ type AssertPass5 = HasCollectionQueryError<Pass5> extends true ? never : true;
|
|
|
124
221
|
export const assertPass5: AssertPass5 = true;
|
|
125
222
|
|
|
126
223
|
// =============================================================================
|
|
127
|
-
// 6. No metadata (string field names) — field-level checks skipped
|
|
224
|
+
// 6. No metadata (string field names) — field-level checks skipped but
|
|
225
|
+
// variable existence is still required
|
|
128
226
|
// =============================================================================
|
|
129
227
|
|
|
130
228
|
type Pass6 = ValidateCollectionQuery<DocOk>;
|
|
131
229
|
type AssertPass6 = HasCollectionQueryError<Pass6> extends true ? never : true;
|
|
132
230
|
export const assertPass6: AssertPass6 = true;
|
|
133
231
|
|
|
232
|
+
// ❌ No metadata but missing $query — still fails
|
|
233
|
+
type DocNoMetaNoQuery = FakeDoc<{
|
|
234
|
+
first?: number | null;
|
|
235
|
+
after?: string | null;
|
|
236
|
+
last?: number | null;
|
|
237
|
+
before?: string | null;
|
|
238
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
239
|
+
}>;
|
|
240
|
+
type Fail6 = ValidateCollectionQuery<DocNoMetaNoQuery>;
|
|
241
|
+
type AssertFail6 = HasCollectionQueryError<Fail6> extends true ? true : never;
|
|
242
|
+
export const assertFail6: AssertFail6 = true;
|
|
243
|
+
|
|
134
244
|
// =============================================================================
|
|
135
245
|
// 7. Helper type tests
|
|
136
246
|
// =============================================================================
|
|
@@ -150,36 +260,45 @@ export const qk1: QIKeys1 = "name";
|
|
|
150
260
|
export const qk2: QIKeys1 = "email";
|
|
151
261
|
|
|
152
262
|
// =============================================================================
|
|
153
|
-
// 8.
|
|
263
|
+
// 8. Missing $query — now fails (previously was "Mixed: $order only")
|
|
154
264
|
// =============================================================================
|
|
155
265
|
|
|
156
|
-
//
|
|
266
|
+
// ❌ Missing $query variable
|
|
157
267
|
type DocOrderOnly = FakeDoc<{
|
|
158
268
|
first?: number | null;
|
|
269
|
+
after?: string | null;
|
|
270
|
+
last?: number | null;
|
|
271
|
+
before?: string | null;
|
|
159
272
|
order?: readonly { field?: "name" | "email" | null }[] | null;
|
|
160
273
|
}>;
|
|
161
|
-
type
|
|
162
|
-
type
|
|
163
|
-
export const
|
|
274
|
+
type Fail8 = ValidateCollectionQuery<DocOrderOnly, "name" | "email">;
|
|
275
|
+
type AssertFail8 = HasCollectionQueryError<Fail8> extends true ? true : never;
|
|
276
|
+
export const assertFail8: AssertFail8 = true;
|
|
164
277
|
|
|
165
278
|
// =============================================================================
|
|
166
|
-
// 9.
|
|
279
|
+
// 9. Missing $order — now fails (previously was "Mixed: $query only")
|
|
167
280
|
// =============================================================================
|
|
168
281
|
|
|
169
|
-
//
|
|
282
|
+
// ❌ Missing $order variable
|
|
170
283
|
type DocQueryOnly = FakeDoc<{
|
|
171
284
|
first?: number | null;
|
|
285
|
+
after?: string | null;
|
|
286
|
+
last?: number | null;
|
|
287
|
+
before?: string | null;
|
|
172
288
|
query?: { name?: unknown; email?: unknown } | null;
|
|
173
289
|
}>;
|
|
174
|
-
type
|
|
175
|
-
type
|
|
176
|
-
export const
|
|
290
|
+
type Fail9 = ValidateCollectionQuery<DocQueryOnly, "name" | "email">;
|
|
291
|
+
type AssertFail9 = HasCollectionQueryError<Fail9> extends true ? true : never;
|
|
292
|
+
export const assertFail9: AssertFail9 = true;
|
|
177
293
|
|
|
178
|
-
// ❌
|
|
294
|
+
// ❌ Missing $order, and query keys don't cover metadata fields
|
|
179
295
|
type DocQueryOnlyMissing = FakeDoc<{
|
|
180
296
|
first?: number | null;
|
|
297
|
+
after?: string | null;
|
|
298
|
+
last?: number | null;
|
|
299
|
+
before?: string | null;
|
|
181
300
|
query?: { name?: unknown } | null;
|
|
182
301
|
}>;
|
|
183
|
-
type
|
|
184
|
-
type
|
|
185
|
-
export const
|
|
302
|
+
type Fail9b = ValidateCollectionQuery<DocQueryOnlyMissing, "name" | "email">;
|
|
303
|
+
type AssertFail9b = HasCollectionQueryError<Fail9b> extends true ? true : never;
|
|
304
|
+
export const assertFail9b: AssertFail9b = true;
|