@izumisy-tailor/tailor-data-viewer 0.2.6 → 0.2.7

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.6",
4
+ "version": "0.2.7",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -44,7 +44,6 @@ describe("useCollectionParams", () => {
44
44
  initialFilters: [
45
45
  {
46
46
  field: "status",
47
- fieldType: "enum",
48
47
  operator: "eq",
49
48
  value: "ACTIVE",
50
49
  },
@@ -101,13 +100,11 @@ describe("useCollectionParams", () => {
101
100
  result.current.setFilters([
102
101
  {
103
102
  field: "status",
104
- fieldType: "enum",
105
103
  operator: "eq",
106
104
  value: "ACTIVE",
107
105
  },
108
106
  {
109
107
  field: "amount",
110
- fieldType: "number",
111
108
  operator: "gte",
112
109
  value: 1000,
113
110
  },
@@ -308,7 +305,6 @@ describe("useCollectionParams", () => {
308
305
  initialFilters: [
309
306
  {
310
307
  field: "status",
311
- fieldType: "enum",
312
308
  operator: "eq",
313
309
  value: "ACTIVE",
314
310
  },
@@ -374,81 +370,6 @@ describe("useCollectionParams", () => {
374
370
  expect(result.current.variables).toEqual({ first: 10 });
375
371
  });
376
372
 
377
- it("auto-detects fieldType from metadata for string field", () => {
378
- const { result } = renderHook(() =>
379
- useCollectionParams({
380
- metadata: testMetadata,
381
- tableName: "task",
382
- }),
383
- );
384
-
385
- act(() => {
386
- result.current.addFilter("title", "foo");
387
- });
388
-
389
- expect(result.current.filters[0].fieldType).toBe("string");
390
- });
391
-
392
- it("auto-detects fieldType from metadata for enum field", () => {
393
- const { result } = renderHook(() =>
394
- useCollectionParams({
395
- metadata: testMetadata,
396
- tableName: "task",
397
- }),
398
- );
399
-
400
- act(() => {
401
- result.current.addFilter("status", "todo");
402
- });
403
-
404
- expect(result.current.filters[0].fieldType).toBe("enum");
405
- });
406
-
407
- it("auto-detects fieldType from metadata for date field", () => {
408
- const { result } = renderHook(() =>
409
- useCollectionParams({
410
- metadata: testMetadata,
411
- tableName: "task",
412
- }),
413
- );
414
-
415
- act(() => {
416
- result.current.addFilter("dueDate", "2026-01-01");
417
- });
418
-
419
- expect(result.current.filters[0].fieldType).toBe("date");
420
- });
421
-
422
- it("auto-detects fieldType from metadata for uuid field", () => {
423
- const { result } = renderHook(() =>
424
- useCollectionParams({
425
- metadata: testMetadata,
426
- tableName: "task",
427
- }),
428
- );
429
-
430
- act(() => {
431
- result.current.addFilter("id", "some-uuid");
432
- });
433
-
434
- expect(result.current.filters[0].fieldType).toBe("uuid");
435
- });
436
-
437
- it("auto-detects fieldType from metadata for number field", () => {
438
- const { result } = renderHook(() =>
439
- useCollectionParams({
440
- metadata: testMetadata,
441
- tableName: "task",
442
- }),
443
- );
444
-
445
- act(() => {
446
- result.current.addFilter("count", 42);
447
- });
448
-
449
- expect(result.current.filters[0].fieldType).toBe("number");
450
- });
451
-
452
373
  it("applies typed initialSort", () => {
453
374
  const { result } = renderHook(() =>
454
375
  useCollectionParams({
@@ -1,18 +1,15 @@
1
- import { useCallback, useMemo, useRef, useState } from "react";
2
- import type {
3
- FieldType,
4
- TableMetadataMap,
5
- } from "../../generator/metadata-generator";
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import type { TableMetadataMap } from "../../generator/metadata-generator";
6
3
  import type {
7
4
  Filter,
8
5
  FilterOperator,
6
+ MetadataFilter,
9
7
  QueryVariables,
10
8
  SortState,
11
9
  UseCollectionParamsOptions,
12
10
  UseCollectionParamsReturn,
13
11
  ExtractQueryVariables,
14
12
  } from "../types";
15
- import { fieldTypeToFilterConfig } from "../types";
16
13
  import type { FieldName } from "../types";
17
14
 
18
15
  // -----------------------------------------------------------------------------
@@ -52,7 +49,10 @@ export function useCollectionParams<
52
49
  TTableName extends string & keyof TMetadata,
53
50
  TQuery = never,
54
51
  >(
55
- options: UseCollectionParamsOptions<FieldName<TMetadata, TTableName>> & {
52
+ options: UseCollectionParamsOptions<
53
+ FieldName<TMetadata, TTableName>,
54
+ MetadataFilter<TMetadata, TTableName>
55
+ > & {
56
56
  metadata: TMetadata;
57
57
  tableName: TTableName;
58
58
  query?: TQuery;
@@ -106,15 +106,8 @@ export function useCollectionParams(
106
106
  initialFilters = [],
107
107
  initialSort = [],
108
108
  pageSize: initialPageSize = 20,
109
- metadata,
110
- tableName,
111
109
  } = options;
112
110
 
113
- // Keep a ref to the resolved fields list so callbacks can look up fieldType.
114
- const fieldsRef = useRef(
115
- metadata && tableName ? metadata[tableName]?.fields : undefined,
116
- );
117
-
118
111
  // ---------------------------------------------------------------------------
119
112
  // State
120
113
  // ---------------------------------------------------------------------------
@@ -131,17 +124,8 @@ export function useCollectionParams(
131
124
  (field: string, value: unknown, operator: FilterOperator = "eq") => {
132
125
  setFiltersState((prev) => {
133
126
  const existing = prev.findIndex((f) => f.field === field);
134
- // Auto-detect fieldType from metadata when available
135
- const fieldMeta = fieldsRef.current?.find((f) => f.name === field);
136
- const detectedType: Filter["fieldType"] = fieldMeta
137
- ? (fieldTypeToFilterConfig(
138
- fieldMeta.type as FieldType,
139
- fieldMeta.enumValues as readonly string[] | undefined,
140
- )?.type ?? "string")
141
- : "string";
142
127
  const newFilter: Filter = {
143
128
  field,
144
- fieldType: detectedType,
145
129
  operator,
146
130
  value,
147
131
  };
@@ -5,6 +5,7 @@ export type {
5
5
  SelectOption,
6
6
  FilterOperator,
7
7
  Filter,
8
+ MetadataFilter,
8
9
  SortState,
9
10
  PageInfo,
10
11
  QueryVariables,
@@ -116,7 +116,9 @@ export function SearchFilterForm<TRow extends Record<string, unknown>>({
116
116
  // Reset operator when field changes
117
117
  useEffect(() => {
118
118
  if (selectedFilterConfig) {
119
- const ops = OPERATORS_BY_FILTER_TYPE[selectedFilterConfig.type];
119
+ const ops = OPERATORS_BY_FILTER_TYPE[
120
+ selectedFilterConfig.type
121
+ ] as readonly FilterOperator[];
120
122
  if (ops.length > 0 && !ops.includes(selectedOperator)) {
121
123
  setSelectedOperator(ops[0]);
122
124
  }
@@ -39,39 +39,60 @@ export type FilterConfig =
39
39
  | { type: "uuid" };
40
40
 
41
41
  // =============================================================================
42
- // Filter Operators
42
+ // Filter Operators (Single Source of Truth)
43
43
  // =============================================================================
44
44
 
45
- /**
46
- * Available filter operators (Tailor Platform compliant).
47
- */
48
- export type FilterOperator =
49
- | "eq"
50
- | "ne"
51
- | "contains"
52
- | "startsWith"
53
- | "endsWith"
54
- | "gt"
55
- | "gte"
56
- | "lt"
57
- | "lte"
58
- | "between"
59
- | "in"
60
- | "notIn";
61
-
62
45
  /**
63
46
  * Operators available per filter type (Tailor Platform schema).
47
+ *
48
+ * This `as const satisfies` definition is the **single source of truth**.
49
+ * Both the `FilterOperator` union and the `OperatorForFilterType` mapping
50
+ * are derived from it, eliminating duplication.
64
51
  */
65
- export const OPERATORS_BY_FILTER_TYPE: Record<
66
- FilterConfig["type"],
67
- FilterOperator[]
68
- > = {
52
+ export const OPERATORS_BY_FILTER_TYPE = {
69
53
  string: ["eq", "ne", "contains", "startsWith", "endsWith"],
70
54
  number: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
71
55
  date: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
72
56
  enum: ["eq", "ne", "in", "notIn"],
73
57
  boolean: ["eq", "ne"],
74
58
  uuid: ["eq", "ne", "in", "notIn"],
59
+ } as const satisfies Record<FilterConfig["type"], readonly string[]>;
60
+
61
+ /**
62
+ * Maps each filter type to the union of operators it supports.
63
+ * Derived from `OPERATORS_BY_FILTER_TYPE`.
64
+ */
65
+ export type OperatorForFilterType = {
66
+ [T in FilterConfig["type"]]: (typeof OPERATORS_BY_FILTER_TYPE)[T][number];
67
+ };
68
+
69
+ /**
70
+ * Union of all available filter operators.
71
+ * Derived from `OPERATORS_BY_FILTER_TYPE`.
72
+ */
73
+ export type FilterOperator = OperatorForFilterType[FilterConfig["type"]];
74
+
75
+ // =============================================================================
76
+ // Filter Type → FieldType Mapping (type-level)
77
+ // =============================================================================
78
+
79
+ /**
80
+ * Type-level mapping from `FieldType` (metadata) to `FilterConfig["type"]`.
81
+ * Mirrors the runtime `fieldTypeToFilterConfig` function.
82
+ * Types that don't support filtering map to `never`.
83
+ */
84
+ export type FieldTypeToFilterConfigType = {
85
+ string: "string";
86
+ number: "number";
87
+ boolean: "boolean";
88
+ uuid: "uuid";
89
+ datetime: "date";
90
+ date: "date";
91
+ time: "date";
92
+ enum: "enum";
93
+ array: never;
94
+ nested: never;
95
+ file: never;
75
96
  };
76
97
 
77
98
  // =============================================================================
@@ -80,14 +101,53 @@ export const OPERATORS_BY_FILTER_TYPE: Record<
80
101
 
81
102
  /**
82
103
  * Active filter state.
104
+ *
105
+ * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
83
106
  */
84
- export interface Filter {
85
- field: string;
86
- fieldType: FilterConfig["type"];
107
+ export interface Filter<TFieldName extends string = string> {
108
+ field: TFieldName;
87
109
  operator: FilterOperator;
88
110
  value: unknown;
89
111
  }
90
112
 
113
+ /**
114
+ * Metadata-aware filter type.
115
+ *
116
+ * Given table metadata, produces a discriminated union over each field
117
+ * where the `operator` is narrowed to only those valid for that field's type.
118
+ * Fields whose `FieldType` doesn't support filtering (array, nested, file) are excluded.
119
+ *
120
+ * @typeParam TMetadata - The `tableMetadata` object type (as const).
121
+ * @typeParam TTableName - The table name key in `TMetadata`.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * // If table "task" has fields: name (string), priority (number), status (enum)
126
+ * type F = MetadataFilter<typeof tableMetadata, "task">;
127
+ * // | { field: "name"; operator: "eq" | "ne" | "contains" | ...; value: unknown }
128
+ * // | { field: "priority"; operator: "eq" | "ne" | "gt" | ...; value: unknown }
129
+ * // | { field: "status"; operator: "eq" | "ne" | "in" | "notIn"; value: unknown }
130
+ * ```
131
+ */
132
+ export type MetadataFilter<
133
+ TMetadata extends TableMetadataMap,
134
+ TTableName extends string & keyof TMetadata,
135
+ > = TMetadata[TTableName]["fields"][number] extends infer F
136
+ ? F extends { readonly name: infer N; readonly type: infer T }
137
+ ? N extends string
138
+ ? T extends keyof FieldTypeToFilterConfigType
139
+ ? FieldTypeToFilterConfigType[T] extends never
140
+ ? never
141
+ : {
142
+ field: N;
143
+ operator: OperatorForFilterType[FieldTypeToFilterConfigType[T]];
144
+ value: unknown;
145
+ }
146
+ : never
147
+ : never
148
+ : never
149
+ : never;
150
+
91
151
  /**
92
152
  * Active sort state for a single field.
93
153
  */
@@ -270,9 +330,10 @@ export type ColumnDefinition<TRow extends Record<string, unknown>> =
270
330
  */
271
331
  export interface UseCollectionParamsOptions<
272
332
  TFieldName extends string = string,
333
+ TFilter extends Filter<TFieldName> = Filter<TFieldName>,
273
334
  > {
274
335
  /** Initial filters to apply */
275
- initialFilters?: Filter[];
336
+ initialFilters?: TFilter[];
276
337
  /** Initial sort states */
277
338
  initialSort?: { field: TFieldName; direction: "Asc" | "Desc" }[];
278
339
  /** Number of items per page (default: 20) */