@izumisy-tailor/tailor-data-viewer 0.2.10 → 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
CHANGED
|
@@ -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>,
|
|
@@ -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
package/src/component/types.ts
CHANGED
|
@@ -620,6 +620,134 @@ export type ExtractQueryVariables<T> = T extends {
|
|
|
620
620
|
? NonNullable<V>
|
|
621
621
|
: never;
|
|
622
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
|
+
|
|
623
751
|
/**
|
|
624
752
|
* Find table names in metadata whose fields are a superset of `TFieldName`.
|
|
625
753
|
*
|