tsense 0.2.0-next.4 → 0.2.0-next.6

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.
@@ -1,5 +1,6 @@
1
+ import type { RelativeDate } from "../types.js";
1
2
  import type { FilterDescriptor } from "./index.js";
2
- export type FilterValue = string | string[] | number | boolean | Date | [FilterValue | undefined, FilterValue | undefined] | undefined;
3
+ export type FilterValue = string | string[] | number | boolean | Date | RelativeDate | [FilterValue | undefined, FilterValue | undefined] | undefined;
3
4
  export type FilterRow = {
4
5
  id: number;
5
6
  field?: string;
@@ -8,6 +9,7 @@ export type FilterRow = {
8
9
  };
9
10
  export type FilterState = {
10
11
  rows: FilterRow[];
12
+ manualRowIds: Set<number>;
11
13
  };
12
14
  export declare function createInitialState(): FilterState;
13
15
  export declare function addRow(state: FilterState): FilterState;
@@ -17,7 +19,7 @@ export declare function setRowField(state: FilterState, index: number, field: st
17
19
  export declare function setRowCondition(state: FilterState, index: number, condition: string): FilterState;
18
20
  export declare function setRowValue(state: FilterState, index: number, value: FilterValue): FilterState;
19
21
  export declare function clearState(): FilterState;
20
- export declare function applyPreset<T>(state: FilterState, descriptor: FilterDescriptor<T>, field: string, name: string): FilterState;
22
+ export declare function hasManualRows(state: FilterState): boolean;
21
23
  export declare function conditionsFor<T>(descriptor: FilterDescriptor<T>, field: string): FilterDescriptor<T>["columns"][number]["conditions"];
22
24
  export declare function columnFor<T>(descriptor: FilterDescriptor<T>, field: string): FilterDescriptor<T>["columns"][number];
23
25
  export declare function buildResult(state: FilterState): Record<string, unknown>;
@@ -11,94 +11,59 @@ const conditionToOperator = {
11
11
  };
12
12
  let nextRowId = 0;
13
13
  export function createInitialState() {
14
- return { rows: [] };
14
+ return { rows: [], manualRowIds: new Set() };
15
15
  }
16
16
  export function addRow(state) {
17
- return { rows: [...state.rows, { id: ++nextRowId }] };
17
+ const id = ++nextRowId;
18
+ return {
19
+ rows: [...state.rows, { id }],
20
+ manualRowIds: new Set([...state.manualRowIds, id]),
21
+ };
18
22
  }
19
23
  export function addRowWithField(state, field) {
20
- return { rows: [...state.rows, { id: ++nextRowId, field }] };
24
+ const id = ++nextRowId;
25
+ return {
26
+ rows: [...state.rows, { id, field }],
27
+ manualRowIds: new Set([...state.manualRowIds, id]),
28
+ };
21
29
  }
22
30
  export function removeRow(state, index) {
23
- return { rows: state.rows.filter((_, i) => i !== index) };
31
+ const removed = state.rows[index];
32
+ const manualRowIds = new Set(state.manualRowIds);
33
+ if (removed) {
34
+ manualRowIds.delete(removed.id);
35
+ }
36
+ return {
37
+ rows: state.rows.filter((_, i) => i !== index),
38
+ manualRowIds,
39
+ };
24
40
  }
25
41
  export function setRowField(state, index, field) {
26
42
  return {
27
43
  rows: state.rows.map((row, i) => i === index ? { id: row.id, field } : row),
44
+ manualRowIds: state.manualRowIds,
28
45
  };
29
46
  }
30
47
  export function setRowCondition(state, index, condition) {
31
48
  return {
32
49
  rows: state.rows.map((row, i) => i === index ? { ...row, condition, value: undefined } : row),
50
+ manualRowIds: state.manualRowIds,
33
51
  };
34
52
  }
35
53
  export function setRowValue(state, index, value) {
36
54
  return {
37
55
  rows: state.rows.map((row, i) => (i === index ? { ...row, value } : row)),
56
+ manualRowIds: state.manualRowIds,
38
57
  };
39
58
  }
40
59
  export function clearState() {
41
- return { rows: [] };
42
- }
43
- function filterValueToRow(field, filterValue) {
44
- if (filterValue == null) {
45
- return null;
46
- }
47
- if (Array.isArray(filterValue)) {
48
- return {
49
- id: ++nextRowId,
50
- field,
51
- condition: "is_in",
52
- value: filterValue,
53
- };
54
- }
55
- if (typeof filterValue !== "object" || filterValue instanceof Date) {
56
- return {
57
- id: ++nextRowId,
58
- field,
59
- condition: "equals",
60
- value: filterValue,
61
- };
62
- }
63
- const operators = filterValue;
64
- const keys = Object.keys(operators);
65
- if (keys.includes("gte") && keys.includes("lte")) {
66
- return {
67
- id: ++nextRowId,
68
- field,
69
- condition: "between",
70
- value: [operators.gte, operators.lte],
71
- };
72
- }
73
- if (!keys[0]) {
74
- return null;
75
- }
76
- return {
77
- id: ++nextRowId,
78
- field,
79
- condition: keys[0],
80
- value: operators[keys[0]],
81
- };
60
+ return { rows: [], manualRowIds: new Set() };
82
61
  }
83
- export function applyPreset(state, descriptor, field, name) {
84
- const column = descriptor.columns.find((c) => c.key === field);
85
- const preset = column?.presets?.find((p) => p.name === name);
86
- if (!preset) {
87
- return state;
88
- }
89
- const presetRows = [];
90
- for (const [key, value] of Object.entries(preset.filter)) {
91
- const row = filterValueToRow(key, value);
92
- if (row) {
93
- presetRows.push(row);
94
- }
95
- }
96
- if (!presetRows.length) {
97
- return state;
98
- }
99
- const presetFields = new Set(presetRows.map((r) => r.field));
100
- const preserved = state.rows.filter((r) => !r.field || !presetFields.has(r.field));
101
- return { rows: [...preserved, ...presetRows] };
62
+ export function hasManualRows(state) {
63
+ return state.rows.some((r) => state.manualRowIds.has(r.id) &&
64
+ r.field != null &&
65
+ r.condition != null &&
66
+ r.value != null);
102
67
  }
103
68
  export function conditionsFor(descriptor, field) {
104
69
  const column = descriptor.columns.find((c) => c.key === field);
@@ -1,3 +1,4 @@
1
+ export { isRelativeDate, resolveRelativeDate } from "./relative-dates.js";
1
2
  export { deserializeFilter, serializeFilter } from "./url.js";
2
3
  import type { Type } from "arktype";
3
4
  import type { TSense } from "../tsense.js";
@@ -16,16 +17,11 @@ export type FilterDescriptor<T = Record<string, unknown>> = {
16
17
  value: string;
17
18
  label: string;
18
19
  }[];
19
- presets?: {
20
- name: string;
21
- filter: Record<string, unknown>;
22
- }[];
23
20
  }[];
24
21
  };
25
- type FilterBuilderFieldConfig<T> = {
22
+ type FilterBuilderFieldConfig = {
26
23
  label: string;
27
24
  labels?: Record<string, string>;
28
- presets?: Record<string, FilterFor<T> | (() => FilterFor<T>)>;
29
25
  };
30
26
  type FilterBuilderReturn<T> = {
31
27
  describe(): FilterDescriptor<T>;
@@ -36,5 +32,5 @@ type FilterBuilderOptions = {
36
32
  conditionLabels?: Partial<Record<ColumnType | "enum", Partial<Record<string, string>>>>;
37
33
  };
38
34
  export declare function createFilterBuilder<T extends Type>(collection: TSense<T>, config: {
39
- [K in keyof T["infer"]]?: FilterBuilderFieldConfig<T["infer"]>;
35
+ [K in keyof T["infer"]]?: FilterBuilderFieldConfig;
40
36
  }, options?: FilterBuilderOptions): FilterBuilderReturn<T["infer"]>;
@@ -1,3 +1,4 @@
1
+ export { isRelativeDate, resolveRelativeDate } from "./relative-dates.js";
1
2
  export { deserializeFilter, serializeFilter } from "./url.js";
2
3
  import { type } from "arktype";
3
4
  const tsenseTypeMap = {
@@ -58,23 +59,26 @@ export function createFilterBuilder(collection, config, options) {
58
59
  "not?": "string",
59
60
  "notIn?": "string[]",
60
61
  });
61
- const dateInput = "number | string | Date";
62
+ const relativeDateUnit = "'day' | 'week' | 'month'";
63
+ const relativeDate = type
64
+ .raw({ startOf: relativeDateUnit })
65
+ .or(type.raw({ endOf: relativeDateUnit }));
66
+ const concreteDateInput = type.raw("number | string | Date");
67
+ const dateInput = concreteDateInput.or(relativeDate);
68
+ const dateArrayInput = dateInput.array();
62
69
  const dateOps = type.raw({
63
70
  "not?": dateInput,
64
71
  "gt?": dateInput,
65
72
  "gte?": dateInput,
66
73
  "lt?": dateInput,
67
74
  "lte?": dateInput,
68
- "notIn?": `(${dateInput})[]`,
75
+ "notIn?": dateArrayInput,
69
76
  });
70
77
  const fieldSchemas = {
71
78
  number: type.raw("number").or(type.raw("number[]")).or(numberOps),
72
79
  string: type.raw("string").or(type.raw("string[]")).or(stringOps),
73
80
  boolean: type.raw("boolean"),
74
- date: type
75
- .raw(dateInput)
76
- .or(type.raw(`(${dateInput})[]`))
77
- .or(dateOps),
81
+ date: dateInput.or(dateArrayInput).or(dateOps),
78
82
  };
79
83
  const descriptor = this.describe();
80
84
  const filterDef = {};
@@ -124,14 +128,6 @@ export function createFilterBuilder(collection, config, options) {
124
128
  }));
125
129
  column.conditions = withLabels(enumConditions, "enum");
126
130
  }
127
- if (fieldConfig.presets) {
128
- column.presets = Object.entries(fieldConfig.presets).map(([name, filterOrFn]) => ({
129
- name,
130
- filter: (typeof filterOrFn === "function"
131
- ? filterOrFn()
132
- : filterOrFn),
133
- }));
134
- }
135
131
  columns.push(column);
136
132
  }
137
133
  return { infer: undefined, columns };
@@ -0,0 +1,3 @@
1
+ import type { RelativeDate } from "../types.js";
2
+ export declare function isRelativeDate(value: unknown): value is RelativeDate;
3
+ export declare function resolveRelativeDate(expr: RelativeDate, tz: string, now?: Date): Date;
@@ -0,0 +1,20 @@
1
+ import dayjs from "dayjs";
2
+ import timezone from "dayjs/plugin/timezone.js";
3
+ import utc from "dayjs/plugin/utc.js";
4
+ dayjs.extend(utc);
5
+ dayjs.extend(timezone);
6
+ export function isRelativeDate(value) {
7
+ if (typeof value !== "object" || value === null || value instanceof Date) {
8
+ return false;
9
+ }
10
+ const obj = value;
11
+ return (("startOf" in obj && typeof obj.startOf === "string") ||
12
+ ("endOf" in obj && typeof obj.endOf === "string"));
13
+ }
14
+ export function resolveRelativeDate(expr, tz, now) {
15
+ const base = now ? dayjs(now).tz(tz) : dayjs().tz(tz);
16
+ if ("startOf" in expr) {
17
+ return base.startOf(expr.startOf).toDate();
18
+ }
19
+ return base.endOf(expr.endOf).toDate();
20
+ }
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@ export { DateTransformer } from "./transformers/date.js";
4
4
  export { defaultTransformers } from "./transformers/defaults.js";
5
5
  export type { FieldTransformer } from "./transformers/types.js";
6
6
  export { TSense } from "./tsense.js";
7
- export type { ConnectionConfig, DeleteResult, FilterFor, HighlightOptions, NumberFilter, ProjectSearch, SearchListOptions, SearchListResult, SearchInput, SearchOptions, ScopedCollection, SearchOptionsPlain, SearchOptionsWithOmit, SearchOptionsWithPick, SearchResult, StringFilter, SyncConfig, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, } from "./types.js";
7
+ export { isRelativeDate, resolveRelativeDate, } from "./filters/relative-dates.js";
8
+ export type { ConnectionConfig, DeleteResult, FilterFor, HighlightOptions, NumberFilter, ProjectSearch, RelativeDate, RelativeDateUnit, SearchListOptions, SearchListResult, SearchInput, SearchOptions, ScopedCollection, SearchOptionsPlain, SearchOptionsWithOmit, SearchOptionsWithPick, SearchResult, StringFilter, SyncConfig, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, } from "./types.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { rank } from "./rank.js";
2
2
  export { DateTransformer } from "./transformers/date.js";
3
3
  export { defaultTransformers } from "./transformers/defaults.js";
4
4
  export { TSense } from "./tsense.js";
5
+ export { isRelativeDate, resolveRelativeDate, } from "./filters/relative-dates.js";
@@ -1,7 +1,6 @@
1
- import type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, PresetButtonSlotProps, RemoveButtonSlotProps, ValueInputSlotProps } from "./filter-builder-types.js";
1
+ import type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, RemoveButtonSlotProps, ValueInputSlotProps } from "./filter-builder-types.js";
2
2
  export declare function defaultFieldSelect({ columns, value, onChange }: FieldSelectSlotProps): import("react/jsx-runtime").JSX.Element;
3
3
  export declare function defaultConditionSelect({ conditions, value, onChange, disabled }: ConditionSelectSlotProps): import("react/jsx-runtime").JSX.Element;
4
4
  export declare function defaultValueInput({ column, condition, value, onChange }: ValueInputSlotProps): import("react/jsx-runtime").JSX.Element;
5
5
  export declare function defaultRemoveButton({ onClick }: RemoveButtonSlotProps): import("react/jsx-runtime").JSX.Element;
6
6
  export declare function defaultAddButton({ onClick }: AddButtonSlotProps): import("react/jsx-runtime").JSX.Element;
7
- export declare function defaultPresetButton({ preset, onClick }: PresetButtonSlotProps): import("react/jsx-runtime").JSX.Element;
@@ -81,6 +81,3 @@ export function defaultRemoveButton({ onClick }) {
81
81
  export function defaultAddButton({ onClick }) {
82
82
  return (_jsx("button", { type: "button", className: "text-blue-500 hover:text-blue-700", onClick: onClick, children: "+ Add filter" }));
83
83
  }
84
- export function defaultPresetButton({ preset, onClick, }) {
85
- return (_jsx("button", { type: "button", className: "rounded bg-gray-100 px-2 py-1 text-sm hover:bg-gray-200", onClick: onClick, children: preset.name }));
86
- }
@@ -25,13 +25,6 @@ export type RemoveButtonSlotProps = {
25
25
  export type AddButtonSlotProps = {
26
26
  onClick: () => void;
27
27
  };
28
- export type PresetButtonSlotProps = {
29
- preset: {
30
- field: string;
31
- name: string;
32
- };
33
- onClick: () => void;
34
- };
35
28
  export type RowSlotProps = {
36
29
  index: number;
37
30
  fieldSelect: ReactNode;
@@ -42,7 +35,6 @@ export type RowSlotProps = {
42
35
  export type RootSlotProps = {
43
36
  rows: ReactNode;
44
37
  addButton: ReactNode;
45
- presets: ReactNode | null;
46
38
  };
47
39
  export type FilterBuilderProps<T> = {
48
40
  descriptor: FilterDescriptor<T>;
@@ -54,5 +46,4 @@ export type FilterBuilderProps<T> = {
54
46
  renderValueInput?: (props: ValueInputSlotProps) => ReactNode;
55
47
  renderAddButton?: (props: AddButtonSlotProps) => ReactNode;
56
48
  renderRemoveButton?: (props: RemoveButtonSlotProps) => ReactNode;
57
- renderPresetButton?: (props: PresetButtonSlotProps) => ReactNode;
58
49
  };
@@ -1,3 +1,3 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { FilterBuilderProps } from "./filter-builder-types.js";
3
- export declare function FilterBuilder<T>({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton, renderPresetButton }: FilterBuilderProps<T>): ReactNode;
3
+ export declare function FilterBuilder<T>({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton }: FilterBuilderProps<T>): ReactNode;
@@ -1,8 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Fragment, useEffect, useRef } from "react";
3
3
  import { useFilterBuilder } from "../use-filter-builder.js";
4
- import { defaultAddButton, defaultConditionSelect, defaultFieldSelect, defaultPresetButton, defaultRemoveButton, defaultValueInput, } from "./filter-builder-defaults.js";
5
- export function FilterBuilder({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton, renderPresetButton, }) {
4
+ import { defaultAddButton, defaultConditionSelect, defaultFieldSelect, defaultRemoveButton, defaultValueInput, } from "./filter-builder-defaults.js";
5
+ export function FilterBuilder({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton, }) {
6
6
  const filters = useFilterBuilder(descriptor);
7
7
  const onChangeRef = useRef(onChange);
8
8
  onChangeRef.current = onChange;
@@ -14,7 +14,6 @@ export function FilterBuilder({ descriptor, onChange, renderRoot, renderRow, ren
14
14
  const ValueInput = renderValueInput ?? defaultValueInput;
15
15
  const RemoveButton = renderRemoveButton ?? defaultRemoveButton;
16
16
  const AddButton = renderAddButton ?? defaultAddButton;
17
- const PresetButton = renderPresetButton ?? defaultPresetButton;
18
17
  const rows = filters.rows.map((row, index) => {
19
18
  const fieldSelect = (_jsx(FieldSelect, { columns: filters.columns, value: row.field, onChange: (field) => filters.setField(index, field) }));
20
19
  const conditionSelect = (_jsx(ConditionSelect, { conditions: row.field ? filters.conditionsFor(row.field) : [], value: row.condition, onChange: (condition) => filters.setCondition(index, condition), disabled: !row.field }));
@@ -32,9 +31,8 @@ export function FilterBuilder({ descriptor, onChange, renderRoot, renderRow, ren
32
31
  return (_jsxs("div", { className: "flex items-center gap-2", children: [fieldSelect, conditionSelect, valueInput, removeButton] }, row.id));
33
32
  });
34
33
  const addButton = _jsx(AddButton, { onClick: filters.add });
35
- const presets = filters.presets.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1 border-t pt-2", children: filters.presets.map((preset) => (_jsx(Fragment, { children: _jsx(PresetButton, { preset: preset, onClick: () => filters.applyPreset(preset.field, preset.name) }) }, `${preset.field}-${preset.name}`))) })) : null;
36
34
  if (renderRoot) {
37
- return _jsx(_Fragment, { children: renderRoot({ rows: _jsx(_Fragment, { children: rows }), addButton, presets }) });
35
+ return _jsx(_Fragment, { children: renderRoot({ rows: _jsx(_Fragment, { children: rows }), addButton }) });
38
36
  }
39
- return (_jsxs("div", { className: "flex flex-col gap-2", children: [rows, addButton, presets] }));
37
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [rows, addButton] }));
40
38
  }
@@ -1,4 +1,4 @@
1
1
  export { FilterBuilder } from "./filter-builder/filter-builder.js";
2
- export type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, PresetButtonSlotProps, RemoveButtonSlotProps, RootSlotProps, RowSlotProps, ValueInputSlotProps, } from "./filter-builder/filter-builder-types.js";
2
+ export type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, RemoveButtonSlotProps, RootSlotProps, RowSlotProps, ValueInputSlotProps, } from "./filter-builder/filter-builder-types.js";
3
3
  export type { FilterValue } from "../filters/filter-state.js";
4
4
  export { useFilterBuilder } from "./use-filter-builder.js";
@@ -1,12 +1,9 @@
1
1
  import { type FilterRow, type FilterValue } from "../filters/filter-state.js";
2
2
  import type { FilterDescriptor } from "../filters/index.js";
3
- type Preset = {
4
- field: string;
5
- name: string;
6
- };
7
3
  type UseFilterBuilderReturn = {
8
4
  columns: FilterDescriptor["columns"];
9
5
  rows: FilterRow[];
6
+ hasManualRows: boolean;
10
7
  add: () => void;
11
8
  addWithField: (field: string) => void;
12
9
  remove: (index: number) => void;
@@ -16,8 +13,6 @@ type UseFilterBuilderReturn = {
16
13
  clear: () => void;
17
14
  conditionsFor: (field: string) => FilterDescriptor["columns"][number]["conditions"];
18
15
  columnFor: (field: string) => FilterDescriptor["columns"][number];
19
- presets: Preset[];
20
- applyPreset: (field: string, name: string) => void;
21
16
  result: Record<string, unknown>;
22
17
  };
23
18
  export declare function useFilterBuilder<T>(descriptor: FilterDescriptor<T>): UseFilterBuilderReturn;
@@ -1,12 +1,12 @@
1
1
  import { useMemo, useState } from "react";
2
- import { addRow, addRowWithField, applyPreset as applyPresetState, buildResult, clearState, columnFor, conditionsFor, createInitialState, removeRow, setRowCondition, setRowField, setRowValue, } from "../filters/filter-state.js";
2
+ import { addRow, addRowWithField, buildResult, clearState, columnFor, conditionsFor, createInitialState, hasManualRows as hasManualRowsState, removeRow, setRowCondition, setRowField, setRowValue, } from "../filters/filter-state.js";
3
3
  export function useFilterBuilder(descriptor) {
4
4
  const [state, setState] = useState(createInitialState);
5
- const presets = useMemo(() => descriptor.columns.flatMap((col) => (col.presets ?? []).map((p) => ({ field: col.key, name: p.name }))), [descriptor]);
6
5
  const result = useMemo(() => buildResult(state), [state]);
7
6
  return {
8
7
  columns: descriptor.columns,
9
8
  rows: state.rows,
9
+ hasManualRows: hasManualRowsState(state),
10
10
  add: () => setState(addRow),
11
11
  addWithField: (field) => setState((s) => addRowWithField(s, field)),
12
12
  remove: (index) => setState((s) => removeRow(s, index)),
@@ -16,8 +16,6 @@ export function useFilterBuilder(descriptor) {
16
16
  clear: () => setState(clearState),
17
17
  conditionsFor: (field) => conditionsFor(descriptor, field),
18
18
  columnFor: (field) => columnFor(descriptor, field),
19
- presets,
20
- applyPreset: (field, name) => setState((s) => applyPresetState(s, descriptor, field, name)),
21
19
  result,
22
20
  };
23
21
  }
package/dist/tsense.d.ts CHANGED
@@ -78,6 +78,8 @@ export declare class TSense<T extends Type> {
78
78
  private buildFilter;
79
79
  private validateFilterFields;
80
80
  private validateFields;
81
+ private resolveFilterValue;
82
+ private resolveFilterDates;
81
83
  private buildFilterExpression;
82
84
  private combineFilterExpressions;
83
85
  private buildSort;
package/dist/tsense.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import redaxios from "redaxios";
2
+ import { isRelativeDate, resolveRelativeDate, } from "./filters/relative-dates.js";
2
3
  import { TSenseMigrator } from "./migrator.js";
3
4
  import { defaultTransformers } from "./transformers/defaults.js";
4
5
  function chunkArray(arr, size) {
@@ -268,9 +269,45 @@ export class TSense {
268
269
  }
269
270
  }
270
271
  }
272
+ resolveFilterValue(value) {
273
+ if (isRelativeDate(value)) {
274
+ return resolveRelativeDate(value, this.options.timezone);
275
+ }
276
+ if (Array.isArray(value)) {
277
+ return value.map((v) => isRelativeDate(v) ? resolveRelativeDate(v, this.options.timezone) : v);
278
+ }
279
+ if (typeof value === "object" &&
280
+ value !== null &&
281
+ !(value instanceof Date)) {
282
+ const result = {};
283
+ for (const [k, v] of Object.entries(value)) {
284
+ result[k] = this.resolveFilterValue(v);
285
+ }
286
+ return result;
287
+ }
288
+ return value;
289
+ }
290
+ resolveFilterDates(filter) {
291
+ if (!filter || !this.options.timezone) {
292
+ return filter;
293
+ }
294
+ const result = {};
295
+ for (const [key, value] of Object.entries(filter)) {
296
+ if (value == null) {
297
+ continue;
298
+ }
299
+ if (key === "OR") {
300
+ result.OR = value.map((f) => this.resolveFilterDates(f));
301
+ continue;
302
+ }
303
+ result[key] = this.resolveFilterValue(value);
304
+ }
305
+ return result;
306
+ }
271
307
  buildFilterExpression(filter) {
272
- this.validateFilterFields(filter);
273
- const parts = this.buildFilter(filter);
308
+ const resolved = this.resolveFilterDates(filter);
309
+ this.validateFilterFields(resolved);
310
+ const parts = this.buildFilter(resolved);
274
311
  if (!parts.length) {
275
312
  return;
276
313
  }
package/dist/types.d.ts CHANGED
@@ -45,9 +45,17 @@ export type TsenseOptions<T extends Type> = {
45
45
  batchSize?: number;
46
46
  validateOnUpsert?: boolean;
47
47
  autoSyncSchema?: boolean;
48
+ timezone?: string;
48
49
  transformers?: FieldTransformer[];
49
50
  dataSync?: SyncConfig<T["infer"]>;
50
51
  };
52
+ export type RelativeDateUnit = "day" | "week" | "month";
53
+ export type RelativeDate = {
54
+ startOf: RelativeDateUnit;
55
+ } | {
56
+ endOf: RelativeDateUnit;
57
+ };
58
+ type DateValue = Date | RelativeDate;
51
59
  export type StringFilter = {
52
60
  not?: string;
53
61
  notIn?: string[];
@@ -61,14 +69,14 @@ export type NumberFilter = {
61
69
  lte?: number;
62
70
  };
63
71
  type DateFilter = {
64
- not?: Date;
65
- notIn?: Date[];
66
- gt?: Date;
67
- gte?: Date;
68
- lt?: Date;
69
- lte?: Date;
70
- };
71
- type FilterValueFor<T> = [T] extends [boolean] ? boolean : [T] extends [Date] ? Date | Date[] | DateFilter : [T] extends [number] ? number | number[] | NumberFilter : [T] extends [string] ? T | T[] | StringFilter : never;
72
+ not?: DateValue;
73
+ notIn?: DateValue[];
74
+ gt?: DateValue;
75
+ gte?: DateValue;
76
+ lt?: DateValue;
77
+ lte?: DateValue;
78
+ };
79
+ type FilterValueFor<T> = [T] extends [boolean] ? boolean : [T] extends [Date] ? DateValue | DateValue[] | DateFilter : [T] extends [number] ? number | number[] | NumberFilter : [T] extends [string] ? T | T[] | StringFilter : never;
72
80
  type SingleFilter<T> = Partial<{
73
81
  [K in keyof T]: FilterValueFor<NonNullable<BaseIfArray<T[K]>>>;
74
82
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsense",
3
- "version": "0.2.0-next.4",
3
+ "version": "0.2.0-next.6",
4
4
  "private": false,
5
5
  "description": "Opinionated, fully typed typesense client",
6
6
  "keywords": [
@@ -43,6 +43,7 @@
43
43
  "prepublishOnly": "bun run ci"
44
44
  },
45
45
  "dependencies": {
46
+ "dayjs": "^1.11.20",
46
47
  "redaxios": "^0.5.1"
47
48
  },
48
49
  "devDependencies": {