@izumisy-tailor/tailor-data-viewer 0.2.10 → 0.2.12

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.12",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -9,8 +9,9 @@ import type {
9
9
  UseCollectionOptions,
10
10
  UseCollectionReturn,
11
11
  ExtractQueryVariables,
12
+ ValidateCollectionQuery,
12
13
  } from "../types";
13
- import type { FieldName } from "../types";
14
+ import type { FieldName, OrderableFieldName } from "../types";
14
15
 
15
16
  /**
16
17
  * Resolves query variables type: uses `ExtractQueryVariables<TQuery>` when
@@ -57,7 +58,11 @@ export function useCollection<
57
58
  > & {
58
59
  metadata: TMetadata;
59
60
  tableName: TTableName;
60
- query: TQuery;
61
+ query: ValidateCollectionQuery<
62
+ TQuery,
63
+ FieldName<TMetadata, TTableName>,
64
+ OrderableFieldName<TMetadata, TTableName>
65
+ >;
61
66
  },
62
67
  ): UseCollectionReturn<
63
68
  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;
@@ -28,8 +28,12 @@ export type {
28
28
  DataTableRowProps,
29
29
  DataTableCellProps,
30
30
  FieldName,
31
+ OrderableFieldName,
31
32
  ExtractOrderField,
32
33
  ExtractQueryVariables,
34
+ ExtractQueryInputKeys,
35
+ ValidateCollectionQuery,
36
+ HasCollectionQueryError,
33
37
  MatchingTableName,
34
38
  MetadataFieldOptions,
35
39
  MetadataFieldsOptions,
@@ -582,6 +582,41 @@ export type FieldName<
582
582
  : never
583
583
  : never;
584
584
 
585
+ /**
586
+ * Field types that support ordering.
587
+ * Mirrors the runtime `fieldTypeToSortConfig` logic — types that return
588
+ * a `SortConfig` (not `undefined`) are considered orderable.
589
+ */
590
+ type OrderableFieldType =
591
+ | "string"
592
+ | "number"
593
+ | "boolean"
594
+ | "datetime"
595
+ | "date"
596
+ | "time"
597
+ | "enum";
598
+
599
+ /**
600
+ * Extract only orderable field names from table metadata.
601
+ *
602
+ * Fields whose `type` is not in `OrderableFieldType` (e.g. `uuid`, `array`,
603
+ * `nested`, `file`) are excluded. This allows `CheckOrderField` to compare
604
+ * only the fields that Tailor Platform actually exposes in the
605
+ * `OrderFieldEnum`, avoiding false positives for fields like `id`.
606
+ */
607
+ export type OrderableFieldName<
608
+ TMetadata extends TableMetadataMap,
609
+ TTableName extends keyof TMetadata,
610
+ > =
611
+ Extract<
612
+ TMetadata[TTableName]["fields"][number],
613
+ { readonly type: OrderableFieldType }
614
+ > extends { readonly name: infer N }
615
+ ? N extends string
616
+ ? N
617
+ : never
618
+ : never;
619
+
585
620
  /**
586
621
  * Extract the `order[].field` union type from a GraphQL variables type.
587
622
  *
@@ -620,6 +655,135 @@ export type ExtractQueryVariables<T> = T extends {
620
655
  ? NonNullable<V>
621
656
  : never;
622
657
 
658
+ // =============================================================================
659
+ // Collection Query Validation (compile-time safety)
660
+ // =============================================================================
661
+
662
+ /**
663
+ * Extract the key names of the `query` (filter) input type from GraphQL variables.
664
+ *
665
+ * For gql-tada's `VariablesOf<typeof QUERY>`, this extracts the allowed
666
+ * field names from the `query` (QueryInput) parameter.
667
+ *
668
+ * @example
669
+ * ```ts
670
+ * // query: BuyerContactQueryInput has keys: name, email, phone, ...
671
+ * type Keys = ExtractQueryInputKeys<typeof GET_BUYER_CONTACTS>;
672
+ * // → "name" | "email" | "phone" | ...
673
+ * ```
674
+ */
675
+ export type ExtractQueryInputKeys<T> =
676
+ ExtractQueryVariables<T> extends {
677
+ query?: infer Q | null;
678
+ }
679
+ ? keyof NonNullable<Q> & string
680
+ : string;
681
+
682
+ // ---------------------------------------------------------------------------
683
+ // Individual collection query validation rules
684
+ // ---------------------------------------------------------------------------
685
+ // Each rule resolves to `Pass` when the check passes (or is not applicable),
686
+ // or `{ __xxxError: "…" }` when it fails. The rules are independent, so
687
+ // adding a new one is just defining a new `Check*` type and appending it to
688
+ // the intersection inside `ValidateCollectionQuery`.
689
+ // ---------------------------------------------------------------------------
690
+
691
+ /**
692
+ * Identity type for intersection: `T & Pass` ≡ `T`.
693
+ * Used as the "pass" branch of each validation rule.
694
+ */
695
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
696
+ type Pass = {};
697
+
698
+ /**
699
+ * Rule 1 — The query must declare `$first` as a variable.
700
+ */
701
+ type CheckFirstVariable<TQuery> =
702
+ "first" extends keyof ExtractQueryVariables<TQuery>
703
+ ? Pass
704
+ : {
705
+ __firstVariableError: `Query must declare a $first variable (e.g. $first: Int).`;
706
+ };
707
+
708
+ /**
709
+ * Rule 2 — When both metadata and `$order` variable exist, metadata field
710
+ * names must be assignable to the `field` enum inside the `OrderInput` type.
711
+ * Skipped when `TFieldName` is the generic `string` (no metadata).
712
+ */
713
+ type CheckOrderField<
714
+ TQuery,
715
+ TFieldName extends string,
716
+ > = "order" extends keyof ExtractQueryVariables<TQuery>
717
+ ? [TFieldName] extends [ExtractOrderField<ExtractQueryVariables<TQuery>>]
718
+ ? Pass
719
+ : {
720
+ __orderFieldError: `Some metadata field names are not assignable to OrderInput field enum. Check that the order variable's field type includes all metadata fields.`;
721
+ }
722
+ : Pass;
723
+
724
+ /**
725
+ * Rule 3 — When both metadata and `$query` variable exist, metadata field
726
+ * names must be a subset of the `QueryInput` type's keys.
727
+ * Skipped when `TFieldName` is the generic `string` (no metadata).
728
+ */
729
+ type CheckQueryInput<
730
+ TQuery,
731
+ TFieldName extends string,
732
+ > = "query" extends keyof ExtractQueryVariables<TQuery>
733
+ ? [Exclude<TFieldName, ExtractQueryInputKeys<TQuery>>] extends [never]
734
+ ? Pass
735
+ : {
736
+ __queryInputError: `Some metadata field names are not present in QueryInput keys. Missing: ${Exclude<TFieldName, ExtractQueryInputKeys<TQuery>> & string}`;
737
+ }
738
+ : Pass;
739
+
740
+ /**
741
+ * Validate that a query document is compatible with `useCollection`.
742
+ *
743
+ * When gql-tada type information is available (i.e. `ExtractQueryVariables`
744
+ * does not resolve to `never`), this type performs compile-time checks by
745
+ * intersecting independent rule types:
746
+ *
747
+ * 1. **`CheckFirstVariable`** — `$first` must exist.
748
+ * 2. **`CheckOrderField`** — OrderInput field compatibility (metadata-aware).
749
+ * 3. **`CheckQueryInput`** — QueryInput key compatibility (metadata-aware).
750
+ *
751
+ * Each rule resolves to `{}` on pass or `{ __xxxError: "…" }` on fail.
752
+ * A failing rule adds a phantom error property that makes `TQuery`
753
+ * incompatible with the actual value, producing a compile error.
754
+ *
755
+ * When gql-tada is not used (plain `DocumentNode`), all checks are skipped.
756
+ *
757
+ * @typeParam TQuery - The query document type (e.g. `typeof GET_ORDERS`).
758
+ * @typeParam TFieldName - Union of metadata field names (default: `string`,
759
+ * which disables metadata-aware checks).
760
+ */
761
+ export type ValidateCollectionQuery<
762
+ TQuery,
763
+ TFieldName extends string = string,
764
+ TOrderableFieldName extends string = TFieldName,
765
+ > =
766
+ ExtractQueryVariables<TQuery> extends never
767
+ ? TQuery // No gql-tada type info → skip validation
768
+ : string extends TFieldName
769
+ ? TQuery & CheckFirstVariable<TQuery> // No metadata → only check $first
770
+ : TQuery &
771
+ CheckFirstVariable<TQuery> &
772
+ CheckOrderField<TQuery, TOrderableFieldName> &
773
+ CheckQueryInput<TQuery, TFieldName>;
774
+
775
+ /**
776
+ * Helper to check whether a `ValidateCollectionQuery` result contains any
777
+ * validation error. Use this in type-level assertions instead of checking
778
+ * individual error keys.
779
+ */
780
+ export type HasCollectionQueryError<T> = T extends
781
+ | { __firstVariableError: string }
782
+ | { __orderFieldError: string }
783
+ | { __queryInputError: string }
784
+ ? true
785
+ : false;
786
+
623
787
  /**
624
788
  * Find table names in metadata whose fields are a superset of `TFieldName`.
625
789
  *