tsense 0.2.0-next.2 → 0.2.0-next.3

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.
@@ -60,20 +60,25 @@ function filterValueToRow(field, filterValue) {
60
60
  value: filterValue,
61
61
  };
62
62
  }
63
- const ops = filterValue;
64
- const keys = Object.keys(ops);
63
+ const operators = filterValue;
64
+ const keys = Object.keys(operators);
65
65
  if (keys.includes("gte") && keys.includes("lte")) {
66
66
  return {
67
67
  id: ++nextRowId,
68
68
  field,
69
69
  condition: "between",
70
- value: [ops.gte, ops.lte],
70
+ value: [operators.gte, operators.lte],
71
71
  };
72
72
  }
73
- if (keys[0]) {
74
- return { id: ++nextRowId, field, condition: keys[0], value: ops[keys[0]] };
73
+ if (!keys[0]) {
74
+ return null;
75
75
  }
76
- return null;
76
+ return {
77
+ id: ++nextRowId,
78
+ field,
79
+ condition: keys[0],
80
+ value: operators[keys[0]],
81
+ };
77
82
  }
78
83
  export function applyPreset(state, descriptor, field, name) {
79
84
  const column = descriptor.columns.find((c) => c.key === field);
@@ -105,8 +110,9 @@ export function columnFor(descriptor, field) {
105
110
  return column;
106
111
  }
107
112
  function isRowComplete(row) {
108
- if (!row.field || !row.condition || row.value == null)
113
+ if (!row.field || !row.condition || row.value == null) {
109
114
  return false;
115
+ }
110
116
  if (row.condition === "between") {
111
117
  return (Array.isArray(row.value) && row.value[0] != null && row.value[1] != null);
112
118
  }
@@ -118,8 +124,9 @@ function isRowComplete(row) {
118
124
  export function buildResult(state) {
119
125
  const result = {};
120
126
  for (const row of state.rows) {
121
- if (!isRowComplete(row))
127
+ if (!isRowComplete(row)) {
122
128
  continue;
129
+ }
123
130
  if (row.condition === "equals" || row.condition === "is_in") {
124
131
  result[row.field] = row.value;
125
132
  continue;
@@ -133,8 +140,9 @@ export function buildResult(state) {
133
140
  continue;
134
141
  }
135
142
  const operator = conditionToOperator[row.condition];
136
- if (!operator)
143
+ if (!operator) {
137
144
  continue;
145
+ }
138
146
  const existing = result[row.field];
139
147
  if (typeof existing === "object" &&
140
148
  existing !== null &&
@@ -17,6 +17,11 @@ function coerceValue(raw, columnType) {
17
17
  }
18
18
  return raw;
19
19
  }
20
+ function appendValues(params, key, values) {
21
+ for (const value of values) {
22
+ params.append(key, serializeValue(value));
23
+ }
24
+ }
20
25
  export function serializeFilter(filter, descriptor) {
21
26
  const params = new URLSearchParams();
22
27
  const columnMap = new Map(descriptor.columns.map((c) => [c.key, c]));
@@ -26,12 +31,7 @@ export function serializeFilter(filter, descriptor) {
26
31
  continue;
27
32
  }
28
33
  if (Array.isArray(value)) {
29
- if (value.length === 1) {
30
- params.set(key, serializeValue(value[0]));
31
- }
32
- else if (value.length > 1) {
33
- params.set(key, value.map((v) => serializeValue(v)).join(","));
34
- }
34
+ appendValues(params, key, value);
35
35
  continue;
36
36
  }
37
37
  if (typeof value === "object" && !(value instanceof Date)) {
@@ -40,7 +40,7 @@ export function serializeFilter(filter, descriptor) {
40
40
  continue;
41
41
  }
42
42
  if (Array.isArray(opValue)) {
43
- params.set(`${key}.${op}`, opValue.map((v) => serializeValue(v)).join(","));
43
+ appendValues(params, `${key}.${op}`, opValue);
44
44
  }
45
45
  else {
46
46
  params.set(`${key}.${op}`, serializeValue(opValue));
@@ -55,7 +55,13 @@ export function serializeFilter(filter, descriptor) {
55
55
  export function deserializeFilter(params, descriptor) {
56
56
  const result = {};
57
57
  const columnMap = new Map(descriptor.columns.map((c) => [c.key, c]));
58
- for (const [paramKey, rawValue] of params.entries()) {
58
+ const visited = new Set();
59
+ for (const paramKey of params.keys()) {
60
+ if (visited.has(paramKey)) {
61
+ continue;
62
+ }
63
+ visited.add(paramKey);
64
+ const rawValues = params.getAll(paramKey);
59
65
  const dotIndex = paramKey.indexOf(".");
60
66
  if (dotIndex !== -1) {
61
67
  const field = paramKey.slice(0, dotIndex);
@@ -66,12 +72,10 @@ export function deserializeFilter(params, descriptor) {
66
72
  }
67
73
  const existing = (result[field] ?? {});
68
74
  if (ARRAY_OPERATORS.has(operator)) {
69
- existing[operator] = rawValue
70
- .split(",")
71
- .map((v) => coerceValue(v, column.type));
75
+ existing[operator] = rawValues.map((value) => coerceValue(value, column.type));
72
76
  }
73
77
  else {
74
- existing[operator] = coerceValue(rawValue, column.type);
78
+ existing[operator] = coerceValue(rawValues[0], column.type);
75
79
  }
76
80
  result[field] = existing;
77
81
  continue;
@@ -80,13 +84,11 @@ export function deserializeFilter(params, descriptor) {
80
84
  if (!column) {
81
85
  continue;
82
86
  }
83
- if (rawValue.includes(",")) {
84
- result[paramKey] = rawValue
85
- .split(",")
86
- .map((v) => coerceValue(v, column.type));
87
+ if (rawValues.length > 1) {
88
+ result[paramKey] = rawValues.map((value) => coerceValue(value, column.type));
87
89
  }
88
90
  else {
89
- result[paramKey] = coerceValue(rawValue, column.type);
91
+ result[paramKey] = coerceValue(rawValues[0], column.type);
90
92
  }
91
93
  }
92
94
  return result;
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ 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, WithNull, } from "./types.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";
@@ -0,0 +1,7 @@
1
+ import type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, PresetButtonSlotProps, RemoveButtonSlotProps, ValueInputSlotProps } from "./filter-builder-types.js";
2
+ export declare function defaultFieldSelect({ columns, value, onChange }: FieldSelectSlotProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare function defaultConditionSelect({ conditions, value, onChange, disabled }: ConditionSelectSlotProps): import("react/jsx-runtime").JSX.Element;
4
+ export declare function defaultValueInput({ column, condition, value, onChange }: ValueInputSlotProps): import("react/jsx-runtime").JSX.Element;
5
+ export declare function defaultRemoveButton({ onClick }: RemoveButtonSlotProps): import("react/jsx-runtime").JSX.Element;
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;
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ function formatDateForInput(date) {
3
+ if (!(date instanceof Date)) {
4
+ return "";
5
+ }
6
+ return date.toISOString().slice(0, 10);
7
+ }
8
+ function parseDateInput(value) {
9
+ if (!value) {
10
+ return;
11
+ }
12
+ return new Date(value + "T00:00:00Z");
13
+ }
14
+ function parseNumberInput(value) {
15
+ if (!value) {
16
+ return;
17
+ }
18
+ return Number(value);
19
+ }
20
+ function parseBooleanInput(value) {
21
+ if (!value) {
22
+ return;
23
+ }
24
+ return value === "true";
25
+ }
26
+ function asTuple(value) {
27
+ if (Array.isArray(value)) {
28
+ return [value[0], value[1]];
29
+ }
30
+ return [undefined, undefined];
31
+ }
32
+ function renderSelectOptions(options, defaultValue) {
33
+ const allOptions = defaultValue
34
+ ? [{ key: "", label: defaultValue }, ...options]
35
+ : options;
36
+ return allOptions.map((option) => (_jsx("option", { value: option.key, children: option.label }, option.key)));
37
+ }
38
+ export function defaultFieldSelect({ columns, value, onChange, }) {
39
+ return (_jsx("select", { className: "rounded border px-2 py-1", value: value ?? "", onChange: (event) => onChange(event.target.value), children: renderSelectOptions(columns, "Column") }));
40
+ }
41
+ export function defaultConditionSelect({ conditions, value, onChange, disabled, }) {
42
+ return (_jsx("select", { className: "rounded border px-2 py-1", value: value ?? "", onChange: (event) => onChange(event.target.value), disabled: disabled, children: renderSelectOptions(conditions, "Condition") }));
43
+ }
44
+ export function defaultValueInput({ column, condition, value, onChange, }) {
45
+ if (condition === "between" && column.type === "date") {
46
+ const tuple = asTuple(value);
47
+ return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(tuple[0]), onChange: (event) => onChange([parseDateInput(event.target.value), tuple[1]]) }), _jsx("span", { className: "text-sm text-gray-500", children: "and" }), _jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(tuple[1]), onChange: (event) => onChange([tuple[0], parseDateInput(event.target.value)]) })] }));
48
+ }
49
+ if (condition === "between") {
50
+ const tuple = asTuple(value);
51
+ return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: "w-24 rounded border px-2 py-1", type: "number", value: String(tuple[0] ?? ""), onChange: (event) => onChange([parseNumberInput(event.target.value), tuple[1]]) }), _jsx("span", { className: "text-sm text-gray-500", children: "and" }), _jsx("input", { className: "w-24 rounded border px-2 py-1", type: "number", value: String(tuple[1] ?? ""), onChange: (event) => onChange([tuple[0], parseNumberInput(event.target.value)]) })] }));
52
+ }
53
+ if (column.values) {
54
+ const selected = Array.isArray(value) ? value : [];
55
+ return (_jsx("div", { className: "flex flex-wrap gap-2", children: column.values.map((option) => {
56
+ const checked = selected.includes(option.value);
57
+ return (_jsxs("label", { className: "flex cursor-pointer select-none items-center gap-1 text-sm", children: [_jsx("input", { type: "checkbox", checked: checked, onChange: () => onChange(checked
58
+ ? selected.filter((item) => item !== option.value)
59
+ : [...selected, option.value]) }), option.label] }, option.value));
60
+ }) }));
61
+ }
62
+ if (column.type === "date") {
63
+ return (_jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(value), onChange: (event) => onChange(parseDateInput(event.target.value)) }));
64
+ }
65
+ if (column.type === "number") {
66
+ return (_jsx("input", { className: "rounded border px-2 py-1", type: "number", value: String(value ?? ""), onChange: (event) => onChange(parseNumberInput(event.target.value)) }));
67
+ }
68
+ if (column.type === "boolean") {
69
+ const options = [
70
+ { key: "", label: "Value" },
71
+ { key: "true", label: "true" },
72
+ { key: "false", label: "false" },
73
+ ];
74
+ return (_jsx("select", { className: "rounded border px-2 py-1", value: value == null ? "" : String(value), onChange: (event) => onChange(parseBooleanInput(event.target.value)), children: renderSelectOptions(options) }));
75
+ }
76
+ return (_jsx("input", { className: "rounded border px-2 py-1", type: "text", value: typeof value === "string" ? value : "", onChange: (event) => onChange(event.target.value) }));
77
+ }
78
+ export function defaultRemoveButton({ onClick }) {
79
+ return (_jsx("button", { type: "button", className: "text-red-500 hover:text-red-700", onClick: onClick, children: "\u00D7" }));
80
+ }
81
+ export function defaultAddButton({ onClick }) {
82
+ return (_jsx("button", { type: "button", className: "text-blue-500 hover:text-blue-700", onClick: onClick, children: "+ Add filter" }));
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
+ }
@@ -0,0 +1,58 @@
1
+ import type { ReactNode } from "react";
2
+ import type { FilterValue } from "../../filters/filter-state.js";
3
+ import type { FilterDescriptor } from "../../filters/index.js";
4
+ import type { FilterFor } from "../../types.js";
5
+ export type FieldSelectSlotProps = {
6
+ columns: FilterDescriptor["columns"];
7
+ value: string | undefined;
8
+ onChange: (field: string) => void;
9
+ };
10
+ export type ConditionSelectSlotProps = {
11
+ conditions: FilterDescriptor["columns"][number]["conditions"];
12
+ value: string | undefined;
13
+ onChange: (condition: string) => void;
14
+ disabled: boolean;
15
+ };
16
+ export type ValueInputSlotProps = {
17
+ column: FilterDescriptor["columns"][number];
18
+ condition: string;
19
+ value: FilterValue;
20
+ onChange: (value: FilterValue) => void;
21
+ };
22
+ export type RemoveButtonSlotProps = {
23
+ onClick: () => void;
24
+ };
25
+ export type AddButtonSlotProps = {
26
+ onClick: () => void;
27
+ };
28
+ export type PresetButtonSlotProps = {
29
+ preset: {
30
+ field: string;
31
+ name: string;
32
+ };
33
+ onClick: () => void;
34
+ };
35
+ export type RowSlotProps = {
36
+ index: number;
37
+ fieldSelect: ReactNode;
38
+ conditionSelect: ReactNode;
39
+ valueInput: ReactNode | null;
40
+ removeButton: ReactNode;
41
+ };
42
+ export type RootSlotProps = {
43
+ rows: ReactNode;
44
+ addButton: ReactNode;
45
+ presets: ReactNode | null;
46
+ };
47
+ export type FilterBuilderProps<T> = {
48
+ descriptor: FilterDescriptor<T>;
49
+ onChange?: (filter: FilterFor<T>) => void;
50
+ renderRoot?: (props: RootSlotProps) => ReactNode;
51
+ renderRow?: (props: RowSlotProps) => ReactNode;
52
+ renderFieldSelect?: (props: FieldSelectSlotProps) => ReactNode;
53
+ renderConditionSelect?: (props: ConditionSelectSlotProps) => ReactNode;
54
+ renderValueInput?: (props: ValueInputSlotProps) => ReactNode;
55
+ renderAddButton?: (props: AddButtonSlotProps) => ReactNode;
56
+ renderRemoveButton?: (props: RemoveButtonSlotProps) => ReactNode;
57
+ renderPresetButton?: (props: PresetButtonSlotProps) => ReactNode;
58
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { type ReactNode } from "react";
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;
@@ -0,0 +1,40 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useEffect, useRef } from "react";
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, }) {
6
+ const filters = useFilterBuilder(descriptor);
7
+ const onChangeRef = useRef(onChange);
8
+ onChangeRef.current = onChange;
9
+ useEffect(() => {
10
+ onChangeRef.current?.(filters.result);
11
+ }, [filters.result]);
12
+ const FieldSelect = renderFieldSelect ?? defaultFieldSelect;
13
+ const ConditionSelect = renderConditionSelect ?? defaultConditionSelect;
14
+ const ValueInput = renderValueInput ?? defaultValueInput;
15
+ const RemoveButton = renderRemoveButton ?? defaultRemoveButton;
16
+ const AddButton = renderAddButton ?? defaultAddButton;
17
+ const PresetButton = renderPresetButton ?? defaultPresetButton;
18
+ const rows = filters.rows.map((row, index) => {
19
+ const fieldSelect = (_jsx(FieldSelect, { columns: filters.columns, value: row.field, onChange: (field) => filters.setField(index, field) }));
20
+ const conditionSelect = (_jsx(ConditionSelect, { conditions: row.field ? filters.conditionsFor(row.field) : [], value: row.condition, onChange: (condition) => filters.setCondition(index, condition), disabled: !row.field }));
21
+ const valueInput = row.field && row.condition ? (_jsx(ValueInput, { column: filters.columnFor(row.field), condition: row.condition, value: row.value, onChange: (value) => filters.setValue(index, value) })) : null;
22
+ const removeButton = _jsx(RemoveButton, { onClick: () => filters.remove(index) });
23
+ if (renderRow) {
24
+ return (_jsx(Fragment, { children: renderRow({
25
+ index,
26
+ fieldSelect,
27
+ conditionSelect,
28
+ valueInput,
29
+ removeButton,
30
+ }) }, row.id));
31
+ }
32
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [fieldSelect, conditionSelect, valueInput, removeButton] }, row.id));
33
+ });
34
+ 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
+ if (renderRoot) {
37
+ return _jsx(_Fragment, { children: renderRoot({ rows: _jsx(_Fragment, { children: rows }), addButton, presets }) });
38
+ }
39
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [rows, addButton, presets] }));
40
+ }
@@ -1,4 +1,4 @@
1
- export { FilterBuilder } from "./filter-builder.js";
2
- export type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, PresetButtonSlotProps, RemoveButtonSlotProps, RootSlotProps, RowSlotProps, ValueInputSlotProps, } from "./filter-builder.js";
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";
3
3
  export type { FilterValue } from "../filters/filter-state.js";
4
4
  export { useFilterBuilder } from "./use-filter-builder.js";
@@ -1,2 +1,2 @@
1
- export { FilterBuilder } from "./filter-builder.js";
1
+ export { FilterBuilder } from "./filter-builder/filter-builder.js";
2
2
  export { useFilterBuilder } from "./use-filter-builder.js";
package/dist/tsense.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Type } from "arktype";
2
2
  import redaxios from "redaxios";
3
- import type { DeleteResult, FieldSchema, FilterFor, ProjectSearch, ScopedCollection, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchResult, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, WithNull } from "./types.js";
3
+ import type { DeleteResult, FieldSchema, FilterFor, ProjectSearch, ScopedCollection, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchResult, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult } from "./types.js";
4
4
  declare const redaxiosInstance: {
5
5
  <T>(urlOrConfig: string | redaxios.Options, config?: redaxios.Options | undefined, _method?: any, data?: any, _undefined?: undefined): Promise<redaxios.Response<T>>;
6
6
  request: (<T_1 = any>(config?: redaxios.Options | undefined) => Promise<redaxios.Response<T_1>>) | (<T_2 = any>(url: string, config?: redaxios.Options | undefined) => Promise<redaxios.Response<T_2>>);
@@ -76,19 +76,27 @@ export declare class TSense<T extends Type> {
76
76
  syncSchema(): Promise<void>;
77
77
  private buildObjectFilter;
78
78
  private buildFilter;
79
+ private validateFilterFields;
79
80
  private validateFields;
81
+ private buildFilterExpression;
82
+ private combineFilterExpressions;
80
83
  private buildSort;
81
84
  create(): Promise<this>;
82
85
  drop(): Promise<void>;
83
86
  get(id: string): Promise<T["infer"] | null>;
84
87
  delete(id: string): Promise<boolean>;
88
+ private deleteManyWithFilterBy;
85
89
  deleteMany(filter: FilterFor<T["infer"]>): Promise<DeleteResult>;
86
90
  update(id: string, data: Partial<T["infer"]>): Promise<T["infer"]>;
91
+ private updateManyWithFilterBy;
87
92
  updateMany(filter: FilterFor<T["infer"]>, data: Partial<T["infer"]>): Promise<UpdateResult>;
93
+ private searchWithFilterBy;
88
94
  search<const O extends SearchOptions<T["infer"]> = SearchOptionsPlain<T["infer"]>>(options: O): Promise<SearchResult<ProjectSearch<T["infer"], O>>>;
95
+ private searchListWithFilterBy;
89
96
  searchList(options: SearchListOptions<T["infer"]>): Promise<SearchListResult<T["infer"]>>;
97
+ private countWithFilterBy;
90
98
  count(filter?: FilterFor<T["infer"]>): Promise<number>;
91
- upsert(docs: WithNull<T["infer"]> | WithNull<T["infer"]>[]): Promise<UpsertResult[]>;
99
+ upsert(docs: T["infer"] | T["infer"][]): Promise<UpsertResult[]>;
92
100
  syncData(options?: SyncOptions): Promise<SyncResult>;
93
101
  private purgeOrphans;
94
102
  exportIds(): Promise<string[]>;
package/dist/tsense.js CHANGED
@@ -209,8 +209,14 @@ export class TSense {
209
209
  const orParts = [];
210
210
  for (const condition of rawValue) {
211
211
  const inner = this.buildFilter(condition);
212
+ if (!inner.length) {
213
+ continue;
214
+ }
212
215
  orParts.push(`(${inner.join("&&")})`);
213
216
  }
217
+ if (!orParts.length) {
218
+ continue;
219
+ }
214
220
  result.push(`(${orParts.join("||")})`);
215
221
  continue;
216
222
  }
@@ -232,6 +238,28 @@ export class TSense {
232
238
  }
233
239
  return result;
234
240
  }
241
+ validateFilterFields(filter) {
242
+ if (!filter) {
243
+ return;
244
+ }
245
+ const fields = [];
246
+ for (const [key, value] of Object.entries(filter)) {
247
+ if (value == null) {
248
+ continue;
249
+ }
250
+ if (key === "OR") {
251
+ for (const condition of value) {
252
+ this.validateFilterFields(condition);
253
+ }
254
+ continue;
255
+ }
256
+ fields.push(key);
257
+ }
258
+ if (!fields.length) {
259
+ return;
260
+ }
261
+ this.validateFields(fields);
262
+ }
235
263
  validateFields(fields) {
236
264
  const valid = new Set(this.fields.map((f) => f.name));
237
265
  for (const field of fields) {
@@ -240,6 +268,20 @@ export class TSense {
240
268
  }
241
269
  }
242
270
  }
271
+ buildFilterExpression(filter) {
272
+ this.validateFilterFields(filter);
273
+ const parts = this.buildFilter(filter);
274
+ if (!parts.length) {
275
+ return;
276
+ }
277
+ return `(${parts.join("&&")})`;
278
+ }
279
+ combineFilterExpressions(...filters) {
280
+ return filters
281
+ .map((filter) => this.buildFilterExpression(filter))
282
+ .filter((filter) => filter != null)
283
+ .join("&&");
284
+ }
243
285
  buildSort(options) {
244
286
  if (!options.sortBy)
245
287
  return;
@@ -297,9 +339,8 @@ export class TSense {
297
339
  });
298
340
  return data != null;
299
341
  }
300
- async deleteMany(filter) {
342
+ async deleteManyWithFilterBy(filterBy) {
301
343
  await this.ensureSynced();
302
- const filterBy = this.buildFilter(filter).join("&&");
303
344
  if (!filterBy) {
304
345
  throw new Error("FILTER_REQUIRED");
305
346
  }
@@ -310,6 +351,9 @@ export class TSense {
310
351
  });
311
352
  return { deleted: data.num_deleted };
312
353
  }
354
+ async deleteMany(filter) {
355
+ return await this.deleteManyWithFilterBy(this.combineFilterExpressions(filter));
356
+ }
313
357
  async update(id, data) {
314
358
  await this.ensureSynced();
315
359
  const serialized = this.serializeDoc(data);
@@ -320,9 +364,8 @@ export class TSense {
320
364
  });
321
365
  return this.deserializeDoc(updated);
322
366
  }
323
- async updateMany(filter, data) {
367
+ async updateManyWithFilterBy(filterBy, data) {
324
368
  await this.ensureSynced();
325
- const filterBy = this.buildFilter(filter).join("&&");
326
369
  if (!filterBy) {
327
370
  throw new Error("FILTER_REQUIRED");
328
371
  }
@@ -335,7 +378,10 @@ export class TSense {
335
378
  });
336
379
  return { updated: result.num_updated };
337
380
  }
338
- async search(options) {
381
+ async updateMany(filter, data) {
382
+ return await this.updateManyWithFilterBy(this.combineFilterExpressions(filter), data);
383
+ }
384
+ async searchWithFilterBy(options, filterBy) {
339
385
  await this.ensureSynced();
340
386
  const queryByFields = options.queryBy ?? [
341
387
  this.options.defaultSearchField,
@@ -357,7 +403,6 @@ export class TSense {
357
403
  const sortBy = this.buildSort(options);
358
404
  if (sortBy)
359
405
  params.sort_by = sortBy;
360
- const filterBy = this.buildFilter(options.filter).join("&&");
361
406
  if (filterBy)
362
407
  params.filter_by = filterBy;
363
408
  if (options.page != null)
@@ -423,17 +468,19 @@ export class TSense {
423
468
  scores,
424
469
  };
425
470
  }
426
- async searchList(options) {
471
+ async search(options) {
472
+ return await this.searchWithFilterBy(options, this.combineFilterExpressions(options.filter));
473
+ }
474
+ async searchListWithFilterBy(options, filterBy) {
427
475
  const page = options.cursor ? Number(options.cursor) : 1;
428
476
  const limit = Math.min(options.limit ?? 20, 100);
429
- const result = await this.search({
477
+ const result = await this.searchWithFilterBy({
430
478
  query: options.query,
431
479
  queryBy: options.queryBy,
432
- filter: options.filter,
433
480
  sortBy: [options.sortBy],
434
481
  page,
435
482
  limit,
436
- });
483
+ }, filterBy);
437
484
  const hasMore = page * limit < result.count;
438
485
  return {
439
486
  data: result.data,
@@ -441,9 +488,11 @@ export class TSense {
441
488
  total: result.count,
442
489
  };
443
490
  }
444
- async count(filter) {
491
+ async searchList(options) {
492
+ return await this.searchListWithFilterBy(options, this.combineFilterExpressions(options.filter));
493
+ }
494
+ async countWithFilterBy(filterBy) {
445
495
  await this.ensureSynced();
446
- const filterBy = this.buildFilter(filter).join("&&");
447
496
  if (!filterBy) {
448
497
  const { data } = await this.axios({
449
498
  method: "GET",
@@ -464,6 +513,9 @@ export class TSense {
464
513
  });
465
514
  return data.found;
466
515
  }
516
+ async count(filter) {
517
+ return await this.countWithFilterBy(this.combineFilterExpressions(filter));
518
+ }
467
519
  async upsert(docs) {
468
520
  await this.ensureSynced();
469
521
  const items = Array.isArray(docs) ? docs : [docs];
@@ -543,17 +595,11 @@ export class TSense {
543
595
  }
544
596
  scoped(baseFilter) {
545
597
  return {
546
- search: (options) => this.search({
547
- ...options,
548
- filter: { ...options.filter, ...baseFilter },
549
- }),
550
- searchList: (options) => this.searchList({
551
- ...options,
552
- filter: { ...options.filter, ...baseFilter },
553
- }),
554
- count: (filter) => this.count({ ...filter, ...baseFilter }),
555
- deleteMany: (filter) => this.deleteMany({ ...filter, ...baseFilter }),
556
- updateMany: (filter, data) => this.updateMany({ ...filter, ...baseFilter }, data),
598
+ search: (options) => this.searchWithFilterBy(options, this.combineFilterExpressions(baseFilter, options.filter)),
599
+ searchList: (options) => this.searchListWithFilterBy(options, this.combineFilterExpressions(baseFilter, options.filter)),
600
+ count: (filter) => this.countWithFilterBy(this.combineFilterExpressions(baseFilter, filter)),
601
+ deleteMany: (filter) => this.deleteManyWithFilterBy(this.combineFilterExpressions(baseFilter, filter)),
602
+ updateMany: (filter, data) => this.updateManyWithFilterBy(this.combineFilterExpressions(baseFilter, filter), data),
557
603
  };
558
604
  }
559
605
  }
package/dist/types.d.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  import type { Type } from "arktype";
2
2
  import type { FieldTransformer } from "./transformers/types.js";
3
3
  type BaseIfArray<T> = T extends (infer Q)[] ? Q : T;
4
- export type WithNull<T> = {
5
- [K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
6
- };
7
4
  export type FieldSchema = {
8
5
  name: string;
9
6
  type: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsense",
3
- "version": "0.2.0-next.2",
3
+ "version": "0.2.0-next.3",
4
4
  "private": false,
5
5
  "description": "Opinionated, fully typed typesense client",
6
6
  "keywords": [