@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.2.10",
4
+ "version": "0.2.11",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -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;
@@ -30,6 +30,9 @@ export type {
30
30
  FieldName,
31
31
  ExtractOrderField,
32
32
  ExtractQueryVariables,
33
+ ExtractQueryInputKeys,
34
+ ValidateCollectionQuery,
35
+ HasCollectionQueryError,
33
36
  MatchingTableName,
34
37
  MetadataFieldOptions,
35
38
  MetadataFieldsOptions,
@@ -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
  *