@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 CHANGED
@@ -215,7 +215,7 @@ export default defineConfig({
215
215
  tailor-sdk generate
216
216
  ```
217
217
 
218
- ### `inferColumnHelper(metadata, tableName)`
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, "task");
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.2.14",
4
+ "version": "0.2.16",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -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.hasPrevPage).toBe(true);
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 back to previous page", () => {
261
+ it("navigates to previous page (backward)", () => {
259
262
  const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
260
263
 
261
264
  act(() => {
262
- result.current.nextPage("cursor1");
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.hasPrevPage).toBe(true);
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("navigates back to first page", () => {
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.nextPage("cursor1");
280
+ result.current.prevPage("cursorB");
280
281
  });
282
+ expect(result.current.paginationDirection).toBe("backward");
283
+
281
284
  act(() => {
282
- result.current.prevPage();
285
+ result.current.nextPage("cursorA");
283
286
  });
284
-
285
- expect(result.current.cursor).toBeNull();
286
- expect(result.current.hasPrevPage).toBe(false);
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.nextPage("cursor1");
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 metadata and tableName", () => {
412
+ it("works with tableMetadata", () => {
390
413
  const { result } = renderHook(() =>
391
414
  useCollection({
392
- metadata: testMetadata,
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
- metadata: testMetadata,
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 { TableMetadataMap } from "../../generator/metadata-generator";
2
+ import type { TableMetadata } from "../../generator/metadata-generator";
3
3
  import type {
4
4
  Filter,
5
5
  FilterOperator,
6
- MetadataFilter,
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 { FieldName, OrderableFieldName } from "../types";
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
- * metadata: tableMetadata,
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 TMetadata extends TableMetadataMap,
59
- TTableName extends string & keyof TMetadata,
58
+ const TTable extends TableMetadata,
60
59
  TQuery,
61
60
  >(
62
61
  options: UseCollectionOptions<
63
- FieldName<TMetadata, TTableName>,
64
- MetadataFilter<TMetadata, TTableName>
62
+ TableFieldName<TTable>,
63
+ TableMetadataFilter<TTable>
65
64
  > & {
66
- metadata: TMetadata;
67
- tableName: TTableName;
65
+ tableMetadata: TTable;
68
66
  query: ValidateCollectionQuery<
69
67
  TQuery,
70
- FieldName<TMetadata, TTableName>,
71
- OrderableFieldName<TMetadata, TTableName>
68
+ TableFieldName<TTable>,
69
+ TableOrderableFieldName<TTable>
72
70
  >;
73
71
  },
74
72
  ): UseCollectionReturn<
75
- FieldName<TMetadata, TTableName>,
73
+ TableFieldName<TTable>,
76
74
  {
77
75
  query: TQuery;
78
76
  variables: ResolveVariables<TQuery>;
79
77
  },
80
- MetadataFilter<TMetadata, TTableName>
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
- metadata?: never;
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
- metadata?: TableMetadataMap;
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 [cursorHistory, setCursorHistory] = useState<string[]>([]);
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
- setCursorHistory([]);
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
- setCursorHistory([]);
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
- setCursorHistory([]);
176
+ setPaginationDirection("forward");
173
177
  }, []);
174
178
 
175
179
  const clearFilters = useCallback(() => {
176
180
  setFiltersState([]);
177
181
  setCursor(null);
178
- setCursorHistory([]);
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
- setCursorHistory([]);
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
- setCursorHistory([]);
209
+ setPaginationDirection("forward");
206
210
  }, []);
207
211
 
208
212
  // ---------------------------------------------------------------------------
209
213
  // Pagination operations
210
214
  // ---------------------------------------------------------------------------
211
- const nextPage = useCallback(
212
- (endCursor: string) => {
213
- setCursorHistory((prev) => [...prev, cursor ?? ""]);
214
- setCursor(endCursor);
215
- },
216
- [cursor],
217
- );
215
+ const nextPage = useCallback((endCursor: string) => {
216
+ setCursor(endCursor);
217
+ setPaginationDirection("forward");
218
+ }, []);
218
219
 
219
- const prevPage = useCallback(() => {
220
- setCursorHistory((prev) => {
221
- const newHistory = [...prev];
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
- setCursorHistory([]);
227
+ setPaginationDirection("forward");
233
228
  }, []);
234
229
 
235
- const hasPrevPage = cursorHistory.length > 0;
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
- first: pageSize,
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) — no $query key in variables.
104
- // With metadata, there is no "query" key so QueryInput check is skipped.
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 ? never : 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. Mixed: $order present but no $query only OrderInput checked
263
+ // 8. Missing $query now fails (previously was "Mixed: $order only")
154
264
  // =============================================================================
155
265
 
156
- // No $query, order fields match
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 Pass8 = ValidateCollectionQuery<DocOrderOnly, "name" | "email">;
162
- type AssertPass8 = HasCollectionQueryError<Pass8> extends true ? never : true;
163
- export const assertPass8: AssertPass8 = true;
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. Mixed: $query present but no $order only QueryInput checked
279
+ // 9. Missing $order now fails (previously was "Mixed: $query only")
167
280
  // =============================================================================
168
281
 
169
- // No $order, query keys match
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 Pass9 = ValidateCollectionQuery<DocQueryOnly, "name" | "email">;
175
- type AssertPass9 = HasCollectionQueryError<Pass9> extends true ? never : true;
176
- export const assertPass9: AssertPass9 = true;
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
- // ❌ No $order, but query keys don't cover metadata fields
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 Fail9 = ValidateCollectionQuery<DocQueryOnlyMissing, "name" | "email">;
184
- type AssertFail9 = HasCollectionQueryError<Fail9> extends true ? true : never;
185
- export const assertFail9: AssertFail9 = true;
302
+ type Fail9b = ValidateCollectionQuery<DocQueryOnlyMissing, "name" | "email">;
303
+ type AssertFail9b = HasCollectionQueryError<Fail9b> extends true ? true : never;
304
+ export const assertFail9b: AssertFail9b = true;