@izumisy-tailor/tailor-data-viewer 0.2.9 → 0.2.11
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/package.json +1 -1
- package/src/component/collection/use-collection.test.ts +8 -8
- package/src/component/collection/use-collection.ts +8 -4
- package/src/component/collection/use-collection.typetest.ts +185 -0
- package/src/component/index.ts +3 -0
- package/src/component/search-filter-form.tsx +1 -1
- package/src/component/types.ts +150 -2
package/package.json
CHANGED
|
@@ -73,7 +73,7 @@ describe("useCollection", () => {
|
|
|
73
73
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
74
74
|
|
|
75
75
|
act(() => {
|
|
76
|
-
result.current.addFilter("status", "
|
|
76
|
+
result.current.addFilter("status", "eq", "ACTIVE");
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
expect(result.current.filters).toHaveLength(1);
|
|
@@ -91,10 +91,10 @@ describe("useCollection", () => {
|
|
|
91
91
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
92
92
|
|
|
93
93
|
act(() => {
|
|
94
|
-
result.current.addFilter("status", "
|
|
94
|
+
result.current.addFilter("status", "eq", "ACTIVE");
|
|
95
95
|
});
|
|
96
96
|
act(() => {
|
|
97
|
-
result.current.addFilter("status", "
|
|
97
|
+
result.current.addFilter("status", "eq", "INACTIVE");
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
expect(result.current.filters).toHaveLength(1);
|
|
@@ -130,8 +130,8 @@ describe("useCollection", () => {
|
|
|
130
130
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
131
131
|
|
|
132
132
|
act(() => {
|
|
133
|
-
result.current.addFilter("status", "
|
|
134
|
-
result.current.addFilter("amount",
|
|
133
|
+
result.current.addFilter("status", "eq", "ACTIVE");
|
|
134
|
+
result.current.addFilter("amount", "gte", 1000);
|
|
135
135
|
});
|
|
136
136
|
act(() => {
|
|
137
137
|
result.current.removeFilter("status");
|
|
@@ -145,8 +145,8 @@ describe("useCollection", () => {
|
|
|
145
145
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
146
146
|
|
|
147
147
|
act(() => {
|
|
148
|
-
result.current.addFilter("status", "
|
|
149
|
-
result.current.addFilter("amount",
|
|
148
|
+
result.current.addFilter("status", "eq", "ACTIVE");
|
|
149
|
+
result.current.addFilter("amount", "gte", 1000);
|
|
150
150
|
});
|
|
151
151
|
act(() => {
|
|
152
152
|
result.current.clearFilters();
|
|
@@ -167,7 +167,7 @@ describe("useCollection", () => {
|
|
|
167
167
|
|
|
168
168
|
// Adding filter should reset pagination
|
|
169
169
|
act(() => {
|
|
170
|
-
result.current.addFilter("status", "
|
|
170
|
+
result.current.addFilter("status", "eq", "ACTIVE");
|
|
171
171
|
});
|
|
172
172
|
expect(result.current.cursor).toBeNull();
|
|
173
173
|
expect(result.current.hasPrevPage).toBe(false);
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
UseCollectionOptions,
|
|
10
10
|
UseCollectionReturn,
|
|
11
11
|
ExtractQueryVariables,
|
|
12
|
+
ValidateCollectionQuery,
|
|
12
13
|
} from "../types";
|
|
13
14
|
import type { FieldName } from "../types";
|
|
14
15
|
|
|
@@ -57,7 +58,7 @@ export function useCollection<
|
|
|
57
58
|
> & {
|
|
58
59
|
metadata: TMetadata;
|
|
59
60
|
tableName: TTableName;
|
|
60
|
-
query: TQuery
|
|
61
|
+
query: ValidateCollectionQuery<TQuery, FieldName<TMetadata, TTableName>>;
|
|
61
62
|
},
|
|
62
63
|
): UseCollectionReturn<
|
|
63
64
|
FieldName<TMetadata, TTableName>,
|
|
@@ -65,7 +66,8 @@ export function useCollection<
|
|
|
65
66
|
{
|
|
66
67
|
query: TQuery;
|
|
67
68
|
variables: ResolveVariables<TQuery, FieldName<TMetadata, TTableName>>;
|
|
68
|
-
}
|
|
69
|
+
},
|
|
70
|
+
MetadataFilter<TMetadata, TTableName>
|
|
69
71
|
>;
|
|
70
72
|
|
|
71
73
|
/**
|
|
@@ -129,7 +131,7 @@ export function useCollection(
|
|
|
129
131
|
// Filter operations
|
|
130
132
|
// ---------------------------------------------------------------------------
|
|
131
133
|
const addFilter = useCallback(
|
|
132
|
-
(field: string,
|
|
134
|
+
(field: string, operator: FilterOperator, value: unknown) => {
|
|
133
135
|
setFiltersState((prev) => {
|
|
134
136
|
const existing = prev.findIndex((f) => f.field === field);
|
|
135
137
|
const newFilter: Filter = {
|
|
@@ -273,7 +275,9 @@ export function useCollection(
|
|
|
273
275
|
return {
|
|
274
276
|
toQueryArgs,
|
|
275
277
|
filters,
|
|
276
|
-
|
|
278
|
+
// Cast needed: implementation accepts FilterOperator (widest),
|
|
279
|
+
// but overload signatures narrow via OperatorForField<TFilter, F>.
|
|
280
|
+
addFilter: addFilter as UseCollectionReturn["addFilter"],
|
|
277
281
|
setFilters,
|
|
278
282
|
removeFilter,
|
|
279
283
|
clearFilters,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time type tests for `ValidateCollectionQuery`.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that `useCollection` produces type errors when the
|
|
5
|
+
* query document's variables are incompatible with the collection's
|
|
6
|
+
* metadata fields.
|
|
7
|
+
*
|
|
8
|
+
* To run: `npx tsc --noEmit` — any assertion that evaluates to `never`
|
|
9
|
+
* will cause a compile error, ensuring the test cases stay enforced.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ValidateCollectionQuery,
|
|
14
|
+
HasCollectionQueryError,
|
|
15
|
+
ExtractQueryInputKeys,
|
|
16
|
+
ExtractOrderField,
|
|
17
|
+
} from "../types";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Helpers – minimal branded document types mimicking gql-tada output
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/** Helper to build a fake TypedDocumentNode with variable types */
|
|
24
|
+
type FakeDoc<Variables> = {
|
|
25
|
+
__apiType?: (variables: Variables) => unknown;
|
|
26
|
+
kind: "Document";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// 1. Variable key checks
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
// ✅ Query with standard Tailor variables should pass
|
|
34
|
+
type DocOk = FakeDoc<{
|
|
35
|
+
first?: number | null;
|
|
36
|
+
after?: string | null;
|
|
37
|
+
query?: { name?: unknown } | null;
|
|
38
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
39
|
+
}>;
|
|
40
|
+
type Pass1 = ValidateCollectionQuery<DocOk, "name">;
|
|
41
|
+
type Assert1 = HasCollectionQueryError<Pass1> extends true ? never : true;
|
|
42
|
+
export const assert1: Assert1 = true;
|
|
43
|
+
|
|
44
|
+
// ❌ Query missing $first should fail
|
|
45
|
+
type DocNoFirst = FakeDoc<{
|
|
46
|
+
after?: string | null;
|
|
47
|
+
query?: { name?: unknown } | null;
|
|
48
|
+
}>;
|
|
49
|
+
type Fail1 = ValidateCollectionQuery<DocNoFirst, "name">;
|
|
50
|
+
type AssertFail1 = HasCollectionQueryError<Fail1> extends true ? true : never;
|
|
51
|
+
export const assertFail1: AssertFail1 = true;
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// 2. OrderInput field compatibility
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
// ❌ Metadata has "name" | "email" but OrderInput only allows "name"
|
|
58
|
+
type DocNarrowOrder = FakeDoc<{
|
|
59
|
+
first?: number | null;
|
|
60
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
61
|
+
}>;
|
|
62
|
+
type Fail2 = ValidateCollectionQuery<DocNarrowOrder, "name" | "email">;
|
|
63
|
+
type AssertFail2 = HasCollectionQueryError<Fail2> extends true ? true : never;
|
|
64
|
+
export const assertFail2: AssertFail2 = true;
|
|
65
|
+
|
|
66
|
+
// ✅ OrderInput allows all metadata fields
|
|
67
|
+
type DocWideOrder = FakeDoc<{
|
|
68
|
+
first?: number | null;
|
|
69
|
+
order?: readonly { field?: "name" | "email" | "phone" | null }[] | null;
|
|
70
|
+
}>;
|
|
71
|
+
type Pass2 = ValidateCollectionQuery<DocWideOrder, "name" | "email">;
|
|
72
|
+
type AssertPass2 = HasCollectionQueryError<Pass2> extends true ? never : true;
|
|
73
|
+
export const assertPass2: AssertPass2 = true;
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// 3. QueryInput key compatibility
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
// ❌ Metadata has "name" | "phone" but QueryInput only has "name"
|
|
80
|
+
type DocNarrowQuery = FakeDoc<{
|
|
81
|
+
first?: number | null;
|
|
82
|
+
query?: { name?: unknown } | null;
|
|
83
|
+
order?: readonly { field?: "name" | "phone" | null }[] | null;
|
|
84
|
+
}>;
|
|
85
|
+
type Fail3 = ValidateCollectionQuery<DocNarrowQuery, "name" | "phone">;
|
|
86
|
+
type AssertFail3 = HasCollectionQueryError<Fail3> extends true ? true : never;
|
|
87
|
+
export const assertFail3: AssertFail3 = true;
|
|
88
|
+
|
|
89
|
+
// ✅ QueryInput has all metadata fields
|
|
90
|
+
type DocWideQuery = FakeDoc<{
|
|
91
|
+
first?: number | null;
|
|
92
|
+
query?: { name?: unknown; phone?: unknown; email?: unknown } | null;
|
|
93
|
+
order?: readonly { field?: "name" | "phone" | "email" | null }[] | null;
|
|
94
|
+
}>;
|
|
95
|
+
type Pass3 = ValidateCollectionQuery<DocWideQuery, "name" | "phone">;
|
|
96
|
+
type AssertPass3 = HasCollectionQueryError<Pass3> extends true ? never : true;
|
|
97
|
+
export const assertPass3: AssertPass3 = true;
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// 4. Variable name typo detection
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
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.
|
|
108
|
+
type DocFilterTypo = FakeDoc<{
|
|
109
|
+
first?: number | null;
|
|
110
|
+
filter?: { name?: unknown } | null;
|
|
111
|
+
order?: readonly { field?: "name" | null }[] | null;
|
|
112
|
+
}>;
|
|
113
|
+
type Typo1 = ValidateCollectionQuery<DocFilterTypo, "name">;
|
|
114
|
+
type AssertTypo1 = HasCollectionQueryError<Typo1> extends true ? never : true;
|
|
115
|
+
export const assertTypo1: AssertTypo1 = true;
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// 5. No gql-tada (plain DocumentNode) — all checks skipped
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
type PlainDoc = { kind: "Document" };
|
|
122
|
+
type Pass5 = ValidateCollectionQuery<PlainDoc, "name">;
|
|
123
|
+
type AssertPass5 = HasCollectionQueryError<Pass5> extends true ? never : true;
|
|
124
|
+
export const assertPass5: AssertPass5 = true;
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// 6. No metadata (string field names) — field-level checks skipped
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
type Pass6 = ValidateCollectionQuery<DocOk>;
|
|
131
|
+
type AssertPass6 = HasCollectionQueryError<Pass6> extends true ? never : true;
|
|
132
|
+
export const assertPass6: AssertPass6 = true;
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// 7. Helper type tests
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
// ExtractOrderField
|
|
139
|
+
type OrderField1 = ExtractOrderField<{
|
|
140
|
+
order?: readonly { field?: "a" | "b" | null }[] | null;
|
|
141
|
+
}>;
|
|
142
|
+
export const of1: OrderField1 = "a";
|
|
143
|
+
export const of2: OrderField1 = "b";
|
|
144
|
+
|
|
145
|
+
// ExtractQueryInputKeys
|
|
146
|
+
type QIKeys1 = ExtractQueryInputKeys<
|
|
147
|
+
FakeDoc<{ query?: { name?: unknown; email?: unknown } | null }>
|
|
148
|
+
>;
|
|
149
|
+
export const qk1: QIKeys1 = "name";
|
|
150
|
+
export const qk2: QIKeys1 = "email";
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// 8. Mixed: $order present but no $query — only OrderInput checked
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
// ✅ No $query, order fields match
|
|
157
|
+
type DocOrderOnly = FakeDoc<{
|
|
158
|
+
first?: number | null;
|
|
159
|
+
order?: readonly { field?: "name" | "email" | null }[] | null;
|
|
160
|
+
}>;
|
|
161
|
+
type Pass8 = ValidateCollectionQuery<DocOrderOnly, "name" | "email">;
|
|
162
|
+
type AssertPass8 = HasCollectionQueryError<Pass8> extends true ? never : true;
|
|
163
|
+
export const assertPass8: AssertPass8 = true;
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// 9. Mixed: $query present but no $order — only QueryInput checked
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
// ✅ No $order, query keys match
|
|
170
|
+
type DocQueryOnly = FakeDoc<{
|
|
171
|
+
first?: number | null;
|
|
172
|
+
query?: { name?: unknown; email?: unknown } | null;
|
|
173
|
+
}>;
|
|
174
|
+
type Pass9 = ValidateCollectionQuery<DocQueryOnly, "name" | "email">;
|
|
175
|
+
type AssertPass9 = HasCollectionQueryError<Pass9> extends true ? never : true;
|
|
176
|
+
export const assertPass9: AssertPass9 = true;
|
|
177
|
+
|
|
178
|
+
// ❌ No $order, but query keys don't cover metadata fields
|
|
179
|
+
type DocQueryOnlyMissing = FakeDoc<{
|
|
180
|
+
first?: number | null;
|
|
181
|
+
query?: { name?: unknown } | null;
|
|
182
|
+
}>;
|
|
183
|
+
type Fail9 = ValidateCollectionQuery<DocQueryOnlyMissing, "name" | "email">;
|
|
184
|
+
type AssertFail9 = HasCollectionQueryError<Fail9> extends true ? true : never;
|
|
185
|
+
export const assertFail9: AssertFail9 = true;
|
package/src/component/index.ts
CHANGED
|
@@ -133,7 +133,7 @@ export function SearchFilterForm<TRow extends Record<string, unknown>>({
|
|
|
133
133
|
|
|
134
134
|
if (!isBooleanType && !filterValue.trim()) return;
|
|
135
135
|
|
|
136
|
-
addFilter(selectedField,
|
|
136
|
+
addFilter(selectedField, selectedOperator, value);
|
|
137
137
|
setSelectedField("");
|
|
138
138
|
setSelectedOperator("eq");
|
|
139
139
|
setFilterValue("");
|
package/src/component/types.ts
CHANGED
|
@@ -72,6 +72,21 @@ export type OperatorForFilterType = {
|
|
|
72
72
|
*/
|
|
73
73
|
export type FilterOperator = OperatorForFilterType[FilterConfig["type"]];
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the operator union for a specific field within a filter type.
|
|
77
|
+
*
|
|
78
|
+
* When `TFilter` is a metadata-aware discriminated union (e.g. `MetadataFilter`),
|
|
79
|
+
* this narrows operators to only those valid for the given field's type.
|
|
80
|
+
* When the field cannot be resolved (e.g. non-metadata usage), falls back to `FilterOperator`.
|
|
81
|
+
*/
|
|
82
|
+
export type OperatorForField<TFilter, F extends string> = [
|
|
83
|
+
Extract<TFilter, { field: F }>,
|
|
84
|
+
] extends [never]
|
|
85
|
+
? FilterOperator
|
|
86
|
+
: Extract<TFilter, { field: F }> extends { operator: infer O }
|
|
87
|
+
? O
|
|
88
|
+
: FilterOperator;
|
|
89
|
+
|
|
75
90
|
// =============================================================================
|
|
76
91
|
// Filter Type → FieldType Mapping (type-level)
|
|
77
92
|
// =============================================================================
|
|
@@ -364,6 +379,7 @@ export interface UseCollectionReturn<
|
|
|
364
379
|
TFieldName extends string = string,
|
|
365
380
|
TVariables = QueryVariables<TFieldName>,
|
|
366
381
|
TQueryArgs = { query: unknown; variables: TVariables },
|
|
382
|
+
TFilter = Filter<TFieldName>,
|
|
367
383
|
> {
|
|
368
384
|
/**
|
|
369
385
|
* Returns query arguments (`{ query, variables }`) that can be spread
|
|
@@ -381,7 +397,11 @@ export interface UseCollectionReturn<
|
|
|
381
397
|
/** Current active filters */
|
|
382
398
|
filters: Filter[];
|
|
383
399
|
/** Add or update a filter for a field */
|
|
384
|
-
addFilter
|
|
400
|
+
addFilter<F extends TFieldName>(
|
|
401
|
+
field: F,
|
|
402
|
+
operator: OperatorForField<TFilter, F>,
|
|
403
|
+
value: unknown,
|
|
404
|
+
): void;
|
|
385
405
|
/** Replace all filters at once */
|
|
386
406
|
setFilters(filters: Filter[]): void;
|
|
387
407
|
/** Remove filter for a specific field */
|
|
@@ -600,6 +620,134 @@ export type ExtractQueryVariables<T> = T extends {
|
|
|
600
620
|
? NonNullable<V>
|
|
601
621
|
: never;
|
|
602
622
|
|
|
623
|
+
// =============================================================================
|
|
624
|
+
// Collection Query Validation (compile-time safety)
|
|
625
|
+
// =============================================================================
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Extract the key names of the `query` (filter) input type from GraphQL variables.
|
|
629
|
+
*
|
|
630
|
+
* For gql-tada's `VariablesOf<typeof QUERY>`, this extracts the allowed
|
|
631
|
+
* field names from the `query` (QueryInput) parameter.
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* ```ts
|
|
635
|
+
* // query: BuyerContactQueryInput has keys: name, email, phone, ...
|
|
636
|
+
* type Keys = ExtractQueryInputKeys<typeof GET_BUYER_CONTACTS>;
|
|
637
|
+
* // → "name" | "email" | "phone" | ...
|
|
638
|
+
* ```
|
|
639
|
+
*/
|
|
640
|
+
export type ExtractQueryInputKeys<T> =
|
|
641
|
+
ExtractQueryVariables<T> extends {
|
|
642
|
+
query?: infer Q | null;
|
|
643
|
+
}
|
|
644
|
+
? keyof NonNullable<Q> & string
|
|
645
|
+
: string;
|
|
646
|
+
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
// Individual collection query validation rules
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Each rule resolves to `Pass` when the check passes (or is not applicable),
|
|
651
|
+
// or `{ __xxxError: "…" }` when it fails. The rules are independent, so
|
|
652
|
+
// adding a new one is just defining a new `Check*` type and appending it to
|
|
653
|
+
// the intersection inside `ValidateCollectionQuery`.
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Identity type for intersection: `T & Pass` ≡ `T`.
|
|
658
|
+
* Used as the "pass" branch of each validation rule.
|
|
659
|
+
*/
|
|
660
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
661
|
+
type Pass = {};
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Rule 1 — The query must declare `$first` as a variable.
|
|
665
|
+
*/
|
|
666
|
+
type CheckFirstVariable<TQuery> =
|
|
667
|
+
"first" extends keyof ExtractQueryVariables<TQuery>
|
|
668
|
+
? Pass
|
|
669
|
+
: {
|
|
670
|
+
__firstVariableError: `Query must declare a $first variable (e.g. $first: Int).`;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Rule 2 — When both metadata and `$order` variable exist, metadata field
|
|
675
|
+
* names must be assignable to the `field` enum inside the `OrderInput` type.
|
|
676
|
+
* Skipped when `TFieldName` is the generic `string` (no metadata).
|
|
677
|
+
*/
|
|
678
|
+
type CheckOrderField<
|
|
679
|
+
TQuery,
|
|
680
|
+
TFieldName extends string,
|
|
681
|
+
> = "order" extends keyof ExtractQueryVariables<TQuery>
|
|
682
|
+
? [TFieldName] extends [ExtractOrderField<ExtractQueryVariables<TQuery>>]
|
|
683
|
+
? Pass
|
|
684
|
+
: {
|
|
685
|
+
__orderFieldError: `Some metadata field names are not assignable to OrderInput field enum. Check that the order variable's field type includes all metadata fields.`;
|
|
686
|
+
}
|
|
687
|
+
: Pass;
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Rule 3 — When both metadata and `$query` variable exist, metadata field
|
|
691
|
+
* names must be a subset of the `QueryInput` type's keys.
|
|
692
|
+
* Skipped when `TFieldName` is the generic `string` (no metadata).
|
|
693
|
+
*/
|
|
694
|
+
type CheckQueryInput<
|
|
695
|
+
TQuery,
|
|
696
|
+
TFieldName extends string,
|
|
697
|
+
> = "query" extends keyof ExtractQueryVariables<TQuery>
|
|
698
|
+
? [Exclude<TFieldName, ExtractQueryInputKeys<TQuery>>] extends [never]
|
|
699
|
+
? Pass
|
|
700
|
+
: {
|
|
701
|
+
__queryInputError: `Some metadata field names are not present in QueryInput keys. Missing: ${Exclude<TFieldName, ExtractQueryInputKeys<TQuery>> & string}`;
|
|
702
|
+
}
|
|
703
|
+
: Pass;
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Validate that a query document is compatible with `useCollection`.
|
|
707
|
+
*
|
|
708
|
+
* When gql-tada type information is available (i.e. `ExtractQueryVariables`
|
|
709
|
+
* does not resolve to `never`), this type performs compile-time checks by
|
|
710
|
+
* intersecting independent rule types:
|
|
711
|
+
*
|
|
712
|
+
* 1. **`CheckFirstVariable`** — `$first` must exist.
|
|
713
|
+
* 2. **`CheckOrderField`** — OrderInput field compatibility (metadata-aware).
|
|
714
|
+
* 3. **`CheckQueryInput`** — QueryInput key compatibility (metadata-aware).
|
|
715
|
+
*
|
|
716
|
+
* Each rule resolves to `{}` on pass or `{ __xxxError: "…" }` on fail.
|
|
717
|
+
* A failing rule adds a phantom error property that makes `TQuery`
|
|
718
|
+
* incompatible with the actual value, producing a compile error.
|
|
719
|
+
*
|
|
720
|
+
* When gql-tada is not used (plain `DocumentNode`), all checks are skipped.
|
|
721
|
+
*
|
|
722
|
+
* @typeParam TQuery - The query document type (e.g. `typeof GET_ORDERS`).
|
|
723
|
+
* @typeParam TFieldName - Union of metadata field names (default: `string`,
|
|
724
|
+
* which disables metadata-aware checks).
|
|
725
|
+
*/
|
|
726
|
+
export type ValidateCollectionQuery<
|
|
727
|
+
TQuery,
|
|
728
|
+
TFieldName extends string = string,
|
|
729
|
+
> =
|
|
730
|
+
ExtractQueryVariables<TQuery> extends never
|
|
731
|
+
? TQuery // No gql-tada type info → skip validation
|
|
732
|
+
: string extends TFieldName
|
|
733
|
+
? TQuery & CheckFirstVariable<TQuery> // No metadata → only check $first
|
|
734
|
+
: TQuery &
|
|
735
|
+
CheckFirstVariable<TQuery> &
|
|
736
|
+
CheckOrderField<TQuery, TFieldName> &
|
|
737
|
+
CheckQueryInput<TQuery, TFieldName>;
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Helper to check whether a `ValidateCollectionQuery` result contains any
|
|
741
|
+
* validation error. Use this in type-level assertions instead of checking
|
|
742
|
+
* individual error keys.
|
|
743
|
+
*/
|
|
744
|
+
export type HasCollectionQueryError<T> = T extends
|
|
745
|
+
| { __firstVariableError: string }
|
|
746
|
+
| { __orderFieldError: string }
|
|
747
|
+
| { __queryInputError: string }
|
|
748
|
+
? true
|
|
749
|
+
: false;
|
|
750
|
+
|
|
603
751
|
/**
|
|
604
752
|
* Find table names in metadata whose fields are a superset of `TFieldName`.
|
|
605
753
|
*
|
|
@@ -762,7 +910,7 @@ export interface SearchFilterFormProps<TRow extends Record<string, unknown>> {
|
|
|
762
910
|
/** Current active filters */
|
|
763
911
|
filters: Filter[];
|
|
764
912
|
/** Add or update a filter */
|
|
765
|
-
addFilter: (field: string,
|
|
913
|
+
addFilter: (field: string, operator: FilterOperator, value: unknown) => void;
|
|
766
914
|
/** Remove a filter */
|
|
767
915
|
removeFilter: (field: string) => void;
|
|
768
916
|
/** Clear all filters */
|