@izumisy-tailor/tailor-data-viewer 0.2.33 → 0.3.0

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.
@@ -53,12 +53,23 @@ export type FilterConfig =
53
53
  * are derived from it, eliminating duplication.
54
54
  */
55
55
  export const OPERATORS_BY_FILTER_TYPE = {
56
- string: ["eq", "ne", "contains", "startsWith", "endsWith"],
57
- number: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
58
- date: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
59
- enum: ["eq", "ne", "in", "notIn"],
56
+ string: [
57
+ "eq",
58
+ "ne",
59
+ "contains",
60
+ "notContains",
61
+ "hasPrefix",
62
+ "hasSuffix",
63
+ "notHasPrefix",
64
+ "notHasSuffix",
65
+ "in",
66
+ "nin",
67
+ ],
68
+ number: ["eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "nin"],
69
+ date: ["eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "nin"],
70
+ enum: ["eq", "ne", "in", "nin"],
60
71
  boolean: ["eq", "ne"],
61
- uuid: ["eq", "ne", "in", "notIn"],
72
+ uuid: ["eq", "ne", "in", "nin"],
62
73
  } as const satisfies Record<FilterConfig["type"], readonly string[]>;
63
74
 
64
75
  /**
@@ -144,7 +155,7 @@ export interface Filter<TFieldName extends string = string> {
144
155
  * type F = MetadataFilter<typeof tableMetadata, "task">;
145
156
  * // | { field: "name"; operator: "eq" | "ne" | "contains" | ...; value: unknown }
146
157
  * // | { field: "priority"; operator: "eq" | "ne" | "gt" | ...; value: unknown }
147
- * // | { field: "status"; operator: "eq" | "ne" | "in" | "notIn"; value: unknown }
158
+ * // | { field: "status"; operator: "eq" | "ne" | "in" | "nin"; value: unknown }
148
159
  * ```
149
160
  */
150
161
  /**
@@ -208,29 +219,13 @@ export interface PageInfo {
208
219
  * GraphQL query variables in Tailor Platform format.
209
220
  *
210
221
  * Field-level type safety for `query` and `order` is not enforced here.
211
- * Instead, `ValidateCollectionQuery` performs compile-time checks
212
- * (e.g. `CheckQueryInput` for filter field names, `CheckOrderField`
213
- * for orderable fields) when `useCollection` is called with metadata.
222
+ * When `useCollectionVariables` is called with metadata, `BuildQueryVariables`
223
+ * produces precise per-field filter types at compile time.
214
224
  */
215
225
  export interface QueryVariables {
216
- /**
217
- * Filter object built at runtime by `useCollection`.
218
- *
219
- * Typed as `Record<string, unknown>` because the concrete shape depends on
220
- * which filters the user applies at runtime (e.g. `{ name: { eq: "foo" } }`).
221
- *
222
- * Compile-time safety for field names is enforced separately by
223
- * `ValidateCollectionQuery` (specifically `CheckQueryInput`), which verifies
224
- * that metadata field names are a subset of the GraphQL `QueryInput` type's keys.
225
- */
226
+ /** Filter object built at runtime by `useCollectionVariables`. */
226
227
  query?: Record<string, unknown>;
227
- /**
228
- * Sort order built at runtime by `useCollection`.
229
- *
230
- * `field` is typed as `string` here. Compile-time validation that
231
- * field names are assignable to the GraphQL `OrderInput` enum is
232
- * performed by `ValidateCollectionQuery` (specifically `CheckOrderField`).
233
- */
228
+ /** Sort order built at runtime by `useCollectionVariables`. */
234
229
  order?: { field: string; direction: "Asc" | "Desc" }[];
235
230
  /** Forward pagination: number of items to fetch */
236
231
  first?: number | null;
@@ -242,6 +237,111 @@ export interface QueryVariables {
242
237
  before?: string | null;
243
238
  }
244
239
 
240
+ // =============================================================================
241
+ // Collection Variables (split into explicit sub-properties)
242
+ // =============================================================================
243
+
244
+ /**
245
+ * Pagination variables for cursor-based pagination (Tailor Platform format).
246
+ */
247
+ export interface PaginationVariables {
248
+ /** Forward pagination: number of items to fetch */
249
+ first?: number;
250
+ /** Forward pagination: cursor to start after */
251
+ after?: string | null;
252
+ /** Backward pagination: number of items to fetch from the end */
253
+ last?: number;
254
+ /** Backward pagination: cursor to fetch before */
255
+ before?: string | null;
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Metadata-aware filter variable types
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Maps `FieldType` (from metadata) to the TypeScript value type
264
+ * used in filter variable construction.
265
+ */
266
+ type FieldTypeToTSType = {
267
+ string: string;
268
+ number: number;
269
+ boolean: boolean;
270
+ uuid: string;
271
+ datetime: string;
272
+ date: string;
273
+ time: string;
274
+ enum: string;
275
+ };
276
+
277
+ /**
278
+ * Resolves the value type for a filter operator.
279
+ * `in`/`nin` operators accept arrays, `between` accepts `{ min, max }`,
280
+ * all other operators accept a scalar value.
281
+ */
282
+ type OperatorValueType<TOp extends string, TValue> = TOp extends "in" | "nin"
283
+ ? TValue[]
284
+ : TOp extends "between"
285
+ ? { min: TValue; max: TValue }
286
+ : TValue;
287
+
288
+ /**
289
+ * Builds a filter input type for a specific filter config type and value type.
290
+ * Each operator key is optional, with the appropriate value type.
291
+ */
292
+ type FilterInputForFieldType<
293
+ TFilterConfigType extends FilterConfig["type"],
294
+ TValue,
295
+ > = {
296
+ [Op in OperatorForFilterType[TFilterConfigType]]?: OperatorValueType<
297
+ Op,
298
+ TValue
299
+ >;
300
+ };
301
+
302
+ /**
303
+ * Builds a Tailor Platform QueryInput-compatible type from table metadata.
304
+ *
305
+ * Each filterable field becomes an optional key with its filter input type.
306
+ * Non-filterable fields (array, nested, file) are excluded.
307
+ *
308
+ * The resulting type is structurally assignable to the corresponding
309
+ * GraphQL QueryInput type generated by gql-tada.
310
+ */
311
+ export type BuildQueryVariables<TTable extends TableMetadata> = {
312
+ [F in TTable["fields"][number] as F extends {
313
+ readonly name: infer N extends string;
314
+ readonly type: infer T;
315
+ }
316
+ ? T extends keyof FieldTypeToFilterConfigType
317
+ ? FieldTypeToFilterConfigType[T] extends never
318
+ ? never
319
+ : N
320
+ : never
321
+ : never]?: F extends {
322
+ readonly type: infer T extends keyof FieldTypeToFilterConfigType &
323
+ keyof FieldTypeToTSType;
324
+ }
325
+ ? FieldTypeToFilterConfigType[T] extends infer FCT extends
326
+ FilterConfig["type"]
327
+ ? FilterInputForFieldType<FCT, FieldTypeToTSType[T]>
328
+ : never
329
+ : never;
330
+ };
331
+
332
+ /**
333
+ * Collection variables split into explicit sub-properties.
334
+ * Each sub-property maps to a specific part of the GraphQL query variables.
335
+ */
336
+ export interface CollectionVariables {
337
+ /** Filter variables (Tailor Platform QueryInput format) */
338
+ query: Record<string, Record<string, unknown>> | undefined;
339
+ /** Sort order variables (Tailor Platform OrderInput format) */
340
+ order: { field: string; direction: "Asc" | "Desc" }[] | undefined;
341
+ /** Cursor-based pagination variables */
342
+ pagination: PaginationVariables;
343
+ }
344
+
245
345
  // =============================================================================
246
346
  // Collection Result (Tailor Platform standard)
247
347
  // =============================================================================
@@ -325,11 +425,11 @@ export type ColumnDefinition<TRow extends Record<string, unknown>> =
325
425
  Column<TRow>;
326
426
 
327
427
  // =============================================================================
328
- // useCollection Types
428
+ // useCollectionVariables Types
329
429
  // =============================================================================
330
430
 
331
431
  /**
332
- * Options for `useCollection` hook.
432
+ * Options for `useCollectionVariables` hook.
333
433
  *
334
434
  * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
335
435
  */
@@ -349,7 +449,7 @@ export interface UseCollectionOptions<
349
449
  }
350
450
 
351
451
  /**
352
- * Return type of `useCollection` hook.
452
+ * Return type of `useCollectionVariables` hook.
353
453
  *
354
454
  * Methods that accept a field name are typed with `TFieldName` so that
355
455
  * auto-completion works when a concrete union is supplied.
@@ -361,25 +461,27 @@ export interface UseCollectionOptions<
361
461
  * can be safely destructured without triggering `unbound-method` lint rules.
362
462
  *
363
463
  * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
364
- * @typeParam TQueryArgs - Type returned by `toQueryArgs()`. Contains both
365
- * `query` and `variables`.
464
+ * @typeParam TVariables - Type of `variables` property.
366
465
  */
367
466
  export interface UseCollectionReturn<
368
467
  TFieldName extends string = string,
369
- TQueryArgs = { query: unknown; variables: QueryVariables },
468
+ TVariables = CollectionVariables,
370
469
  TFilter = Filter<TFieldName>,
371
470
  > {
372
471
  /**
373
- * Returns query arguments (`{ query, variables }`) that can be spread
374
- * directly into `useQuery()`.
472
+ * Collection variables split into explicit sub-properties
473
+ * for direct mapping to GraphQL query variables.
375
474
  *
376
475
  * @example
377
476
  * ```tsx
378
- * const collection = useCollection({ query: GET_ORDERS, params: { pageSize: 20 } });
379
- * const [result] = useQuery({ ...collection.toQueryArgs() });
477
+ * const { query, order, pagination } = collection.variables;
478
+ * const [result] = useQuery({
479
+ * query: GET_TASKS,
480
+ * variables: { ...pagination, query, order },
481
+ * });
380
482
  * ```
381
483
  */
382
- toQueryArgs: () => TQueryArgs;
484
+ variables: TVariables;
383
485
 
384
486
  // Filter operations
385
487
  /** Current active filters */
@@ -647,242 +749,6 @@ export type OrderableFieldName<
647
749
  TTableName extends keyof TMetadata,
648
750
  > = TableOrderableFieldName<TMetadata[TTableName]>;
649
751
 
650
- /**
651
- * Extract the `order[].field` union type from a GraphQL variables type.
652
- *
653
- * For gql-tada's `VariablesOf<typeof QUERY>`, this extracts the allowed
654
- * field names from the `order` parameter.
655
- *
656
- * @example
657
- * ```ts
658
- * type Fields = ExtractOrderField<VariablesOf<typeof GET_ORDERS>>;
659
- * // → "name" | "amount" | "status" | "createdAt"
660
- * ```
661
- */
662
- export type ExtractOrderField<T> = T extends {
663
- order?: readonly (infer O | null | undefined)[] | null | undefined;
664
- }
665
- ? O extends { field?: infer F | null }
666
- ? NonNullable<F> & string
667
- : string
668
- : string;
669
-
670
- /**
671
- * Extract the variables type from a gql.tada `DocumentDecoration`.
672
- *
673
- * gql.tada stores variables via `__apiType?: (variables: V) => R`.
674
- *
675
- * @example
676
- * ```ts
677
- * const GET_ORDERS = graphql(`query Orders($first: Int, ...) { ... }`);
678
- * type Vars = ExtractQueryVariables<typeof GET_ORDERS>;
679
- * // → { first?: number | null; order?: OrderInput[] | null; ... }
680
- * ```
681
- */
682
- export type ExtractQueryVariables<T> = T extends {
683
- __apiType?: (variables: infer V) => unknown;
684
- }
685
- ? NonNullable<V>
686
- : never;
687
-
688
- // =============================================================================
689
- // Collection Query Validation (compile-time safety)
690
- // =============================================================================
691
-
692
- /**
693
- * Extract the key names of the `query` (filter) input type from GraphQL variables.
694
- *
695
- * For gql-tada's `VariablesOf<typeof QUERY>`, this extracts the allowed
696
- * field names from the `query` (QueryInput) parameter.
697
- *
698
- * @example
699
- * ```ts
700
- * // query: BuyerContactQueryInput has keys: name, email, phone, ...
701
- * type Keys = ExtractQueryInputKeys<typeof GET_BUYER_CONTACTS>;
702
- * // → "name" | "email" | "phone" | ...
703
- * ```
704
- */
705
- export type ExtractQueryInputKeys<T> =
706
- ExtractQueryVariables<T> extends {
707
- query?: infer Q | null;
708
- }
709
- ? keyof NonNullable<Q> & string
710
- : string;
711
-
712
- // ---------------------------------------------------------------------------
713
- // Individual collection query validation rules
714
- // ---------------------------------------------------------------------------
715
- // Each rule resolves to `Pass` when the check passes (or is not applicable),
716
- // or `{ __xxxError: "…" }` when it fails. The rules are independent, so
717
- // adding a new one is just defining a new `Check*` type and appending it to
718
- // the intersection inside `ValidateCollectionQuery`.
719
- // ---------------------------------------------------------------------------
720
-
721
- /**
722
- * Identity type for intersection: `T & Pass` ≡ `T`.
723
- * Used as the "pass" branch of each validation rule.
724
- */
725
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
726
- type Pass = {};
727
-
728
- /**
729
- * Rule 1 — The query must declare `$first` as a variable.
730
- */
731
- type CheckFirstVariable<TQuery> =
732
- "first" extends keyof ExtractQueryVariables<TQuery>
733
- ? Pass
734
- : {
735
- __firstVariableError: `Query must declare a $first variable (e.g. $first: Int).`;
736
- };
737
-
738
- /**
739
- * Rule 2 — The query must declare `$after` as a variable.
740
- */
741
- type CheckAfterVariable<TQuery> =
742
- "after" extends keyof ExtractQueryVariables<TQuery>
743
- ? Pass
744
- : {
745
- __afterVariableError: `Query must declare an $after variable (e.g. $after: String).`;
746
- };
747
-
748
- /**
749
- * Rule 3 — The query must declare `$last` as a variable.
750
- */
751
- type CheckLastVariable<TQuery> =
752
- "last" extends keyof ExtractQueryVariables<TQuery>
753
- ? Pass
754
- : {
755
- __lastVariableError: `Query must declare a $last variable (e.g. $last: Int).`;
756
- };
757
-
758
- /**
759
- * Rule 4 — The query must declare `$before` as a variable.
760
- */
761
- type CheckBeforeVariable<TQuery> =
762
- "before" extends keyof ExtractQueryVariables<TQuery>
763
- ? Pass
764
- : {
765
- __beforeVariableError: `Query must declare a $before variable (e.g. $before: String).`;
766
- };
767
-
768
- /**
769
- * Rule 5 — The query must declare `$query` as a variable.
770
- */
771
- type CheckQueryVariable<TQuery> =
772
- "query" extends keyof ExtractQueryVariables<TQuery>
773
- ? Pass
774
- : {
775
- __queryVariableError: `Query must declare a $query variable (e.g. $query: XxxQueryInput!).`;
776
- };
777
-
778
- /**
779
- * Rule 6 — The query must declare `$order` as a variable.
780
- */
781
- type CheckOrderVariable<TQuery> =
782
- "order" extends keyof ExtractQueryVariables<TQuery>
783
- ? Pass
784
- : {
785
- __orderVariableError: `Query must declare an $order variable (e.g. $order: [XxxOrderInput]).`;
786
- };
787
-
788
- /**
789
- * Rule 7 — When metadata is provided, metadata field names must be
790
- * assignable to the `field` enum inside the `OrderInput` type.
791
- */
792
- type CheckOrderField<
793
- TQuery,
794
- TFieldName extends string,
795
- > = "order" extends keyof ExtractQueryVariables<TQuery>
796
- ? [TFieldName] extends [ExtractOrderField<ExtractQueryVariables<TQuery>>]
797
- ? Pass
798
- : {
799
- __orderFieldError: `Some metadata field names are not assignable to OrderInput field enum. Check that the order variable's field type includes all metadata fields.`;
800
- }
801
- : Pass;
802
-
803
- /**
804
- * Rule 8 — When metadata is provided, metadata field names must be a
805
- * subset of the `QueryInput` type's keys.
806
- */
807
- type CheckQueryInput<
808
- TQuery,
809
- TFieldName extends string,
810
- > = "query" extends keyof ExtractQueryVariables<TQuery>
811
- ? [Exclude<TFieldName, ExtractQueryInputKeys<TQuery>>] extends [never]
812
- ? Pass
813
- : {
814
- __queryInputError: `Some metadata field names are not present in QueryInput keys. Missing: ${Exclude<TFieldName, ExtractQueryInputKeys<TQuery>> & string}`;
815
- }
816
- : Pass;
817
-
818
- /**
819
- * Validate that a query document is compatible with `useCollection`.
820
- *
821
- * When gql-tada type information is available (i.e. `ExtractQueryVariables`
822
- * does not resolve to `never`), this type performs compile-time checks by
823
- * intersecting independent rule types:
824
- *
825
- * 1. **`CheckFirstVariable`** — `$first` must exist.
826
- * 2. **`CheckAfterVariable`** — `$after` must exist.
827
- * 3. **`CheckLastVariable`** — `$last` must exist.
828
- * 4. **`CheckBeforeVariable`** — `$before` must exist.
829
- * 5. **`CheckQueryVariable`** — `$query` must exist.
830
- * 6. **`CheckOrderVariable`** — `$order` must exist.
831
- * 7. **`CheckOrderField`** — OrderInput field compatibility (metadata-aware).
832
- * 8. **`CheckQueryInput`** — QueryInput key compatibility (metadata-aware).
833
- *
834
- * Each rule resolves to `{}` on pass or `{ __xxxError: "…" }` on fail.
835
- * A failing rule adds a phantom error property that makes `TQuery`
836
- * incompatible with the actual value, producing a compile error.
837
- *
838
- * When gql-tada is not used (plain `DocumentNode`), all checks are skipped.
839
- *
840
- * @typeParam TQuery - The query document type (e.g. `typeof GET_ORDERS`).
841
- * @typeParam TFieldName - Union of metadata field names (default: `string`,
842
- * which disables metadata-aware checks).
843
- */
844
- export type ValidateCollectionQuery<
845
- TQuery,
846
- TFieldName extends string = string,
847
- TOrderableFieldName extends string = TFieldName,
848
- > =
849
- ExtractQueryVariables<TQuery> extends never
850
- ? TQuery // No gql-tada type info → skip validation
851
- : string extends TFieldName
852
- ? TQuery &
853
- CheckFirstVariable<TQuery> &
854
- CheckAfterVariable<TQuery> &
855
- CheckLastVariable<TQuery> &
856
- CheckBeforeVariable<TQuery> &
857
- CheckQueryVariable<TQuery> &
858
- CheckOrderVariable<TQuery>
859
- : TQuery &
860
- CheckFirstVariable<TQuery> &
861
- CheckAfterVariable<TQuery> &
862
- CheckLastVariable<TQuery> &
863
- CheckBeforeVariable<TQuery> &
864
- CheckQueryVariable<TQuery> &
865
- CheckOrderVariable<TQuery> &
866
- CheckOrderField<TQuery, TOrderableFieldName> &
867
- CheckQueryInput<TQuery, TFieldName>;
868
-
869
- /**
870
- * Helper to check whether a `ValidateCollectionQuery` result contains any
871
- * validation error. Use this in type-level assertions instead of checking
872
- * individual error keys.
873
- */
874
- export type HasCollectionQueryError<T> = T extends
875
- | { __firstVariableError: string }
876
- | { __afterVariableError: string }
877
- | { __lastVariableError: string }
878
- | { __beforeVariableError: string }
879
- | { __queryVariableError: string }
880
- | { __orderVariableError: string }
881
- | { __orderFieldError: string }
882
- | { __queryInputError: string }
883
- ? true
884
- : false;
885
-
886
752
  /**
887
753
  * Find table names in metadata whose fields are a superset of `TFieldName`.
888
754
  *
@@ -977,15 +843,18 @@ export const DEFAULT_OPERATOR_LABELS: Record<FilterOperator, string> = {
977
843
  eq: "=",
978
844
  ne: "≠",
979
845
  contains: "contains",
980
- startsWith: "starts with",
981
- endsWith: "ends with",
846
+ notContains: "not contains",
847
+ hasPrefix: "starts with",
848
+ hasSuffix: "ends with",
849
+ notHasPrefix: "not starts with",
850
+ notHasSuffix: "not ends with",
982
851
  gt: ">",
983
852
  gte: "≥",
984
853
  lt: "<",
985
854
  lte: "≤",
986
855
  between: "between",
987
856
  in: "in",
988
- notIn: "not in",
857
+ nin: "not in",
989
858
  };
990
859
 
991
860
  /**
@@ -3,7 +3,7 @@ import { vi } from "vitest";
3
3
  import type { Column, UseCollectionReturn } from "../component/types";
4
4
  import { DataTableContext } from "../component/data-table/data-table-context";
5
5
  import type { DataTableContextValue } from "../component/data-table/data-table-context";
6
- import { CollectionProvider } from "../component/collection/collection-provider";
6
+ import { CollectionVariablesProvider } from "../component/collection/collection-provider";
7
7
 
8
8
  // =============================================================================
9
9
  // Mock factory: DataTableContext
@@ -43,14 +43,18 @@ export function createMockDataTableContext<T extends Record<string, unknown>>(
43
43
  }
44
44
 
45
45
  // =============================================================================
46
- // Mock factory: CollectionContext
46
+ // Mock factory: CollectionVariablesContext
47
47
  // =============================================================================
48
48
 
49
49
  export function createMockCollectionContext(
50
50
  overrides?: Partial<UseCollectionReturn<string, unknown>>,
51
51
  ): UseCollectionReturn<string, unknown> {
52
52
  return {
53
- toQueryArgs: vi.fn(() => ({ query: null, variables: {} })),
53
+ variables: {
54
+ query: undefined,
55
+ order: undefined,
56
+ pagination: { first: 20 },
57
+ },
54
58
  filters: [],
55
59
  addFilter: vi.fn(),
56
60
  setFilters: vi.fn(),
@@ -118,7 +122,7 @@ export function createTestProviders<
118
122
  collection?: Partial<UseCollectionReturn<string, unknown>>;
119
123
  }) {
120
124
  return (
121
- <CollectionProvider
125
+ <CollectionVariablesProvider
122
126
  value={createMockCollectionContext({
123
127
  ...defaults.collectionDefaults,
124
128
  ...collection,
@@ -132,7 +136,7 @@ export function createTestProviders<
132
136
  >
133
137
  {children}
134
138
  </DataTableContext.Provider>
135
- </CollectionProvider>
139
+ </CollectionVariablesProvider>
136
140
  );
137
141
  };
138
142
  }