tsense 0.2.0-next.3 → 0.2.0-next.5

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.
@@ -86,17 +86,19 @@ export function applyPreset(state, descriptor, field, name) {
86
86
  if (!preset) {
87
87
  return state;
88
88
  }
89
- const rows = [];
89
+ const presetRows = [];
90
90
  for (const [key, value] of Object.entries(preset.filter)) {
91
91
  const row = filterValueToRow(key, value);
92
92
  if (row) {
93
- rows.push(row);
93
+ presetRows.push(row);
94
94
  }
95
95
  }
96
- if (!rows.length) {
96
+ if (!presetRows.length) {
97
97
  return state;
98
98
  }
99
- return { rows };
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] };
100
102
  }
101
103
  export function conditionsFor(descriptor, field) {
102
104
  const column = descriptor.columns.find((c) => c.key === field);
@@ -1,13 +1,14 @@
1
- export { deserializeFilter, serializeFilter } from "./url.js";
2
- import type { Type } from "arktype";
3
- import type { TSense } from "../tsense.js";
4
- import type { FilterFor, SearchInput } from "../types.js";
1
+ export { isRelativeDate, resolveRelativeDate } from './relative-dates.js';
2
+ export { deserializeFilter, serializeFilter } from './url.js';
3
+ import type { Type } from 'arktype';
4
+ import type { TSense } from '../tsense.js';
5
+ import type { FilterFor, SearchInput } from '../types.js';
5
6
  export type FilterDescriptor<T = Record<string, unknown>> = {
6
7
  infer: FilterFor<T>;
7
8
  columns: {
8
9
  key: keyof T & string;
9
10
  label: string;
10
- type: "string" | "number" | "boolean" | "date";
11
+ type: 'string' | 'number' | 'boolean' | 'date';
11
12
  conditions: {
12
13
  key: string;
13
14
  label: string;
@@ -31,10 +32,10 @@ type FilterBuilderReturn<T> = {
31
32
  describe(): FilterDescriptor<T>;
32
33
  schema(): Type<SearchInput<T>>;
33
34
  };
34
- type ColumnType = FilterDescriptor["columns"][number]["type"];
35
+ type ColumnType = FilterDescriptor['columns'][number]['type'];
35
36
  type FilterBuilderOptions = {
36
- conditionLabels?: Partial<Record<ColumnType | "enum", Partial<Record<string, string>>>>;
37
+ conditionLabels?: Partial<Record<ColumnType | 'enum', Partial<Record<string, string>>>>;
37
38
  };
38
39
  export declare function createFilterBuilder<T extends Type>(collection: TSense<T>, config: {
39
- [K in keyof T["infer"]]?: FilterBuilderFieldConfig<T["infer"]>;
40
- }, options?: FilterBuilderOptions): FilterBuilderReturn<T["infer"]>;
40
+ [K in keyof T['infer']]?: FilterBuilderFieldConfig<T['infer']>;
41
+ }, options?: FilterBuilderOptions): FilterBuilderReturn<T['infer']>;
@@ -1,45 +1,46 @@
1
- export { deserializeFilter, serializeFilter } from "./url.js";
2
- import { type } from "arktype";
1
+ export { isRelativeDate, resolveRelativeDate } from './relative-dates.js';
2
+ export { deserializeFilter, serializeFilter } from './url.js';
3
+ import { type } from 'arktype';
3
4
  const tsenseTypeMap = {
4
- string: "string",
5
- "string*": "string",
6
- "string[]": "string",
7
- int32: "number",
8
- int64: "number",
9
- float: "number",
10
- "int32[]": "number",
11
- "int64[]": "number",
12
- "float[]": "number",
13
- bool: "boolean",
14
- "bool[]": "boolean",
15
- auto: "string",
16
- image: "string",
5
+ string: 'string',
6
+ 'string*': 'string',
7
+ 'string[]': 'string',
8
+ int32: 'number',
9
+ int64: 'number',
10
+ float: 'number',
11
+ 'int32[]': 'number',
12
+ 'int64[]': 'number',
13
+ 'float[]': 'number',
14
+ bool: 'boolean',
15
+ 'bool[]': 'boolean',
16
+ auto: 'string',
17
+ image: 'string',
17
18
  };
18
19
  const enumConditions = [
19
- { key: "is_in", label: "is in" },
20
- { key: "is_not_in", label: "is not in" },
20
+ { key: 'is_in', label: 'is in' },
21
+ { key: 'is_not_in', label: 'is not in' },
21
22
  ];
22
23
  const conditionsByType = {
23
24
  string: [
24
- { key: "equals", label: "equals" },
25
- { key: "not_equals", label: "not equals" },
25
+ { key: 'equals', label: 'equals' },
26
+ { key: 'not_equals', label: 'not equals' },
26
27
  ],
27
28
  number: [
28
- { key: "equals", label: "equals" },
29
- { key: "not_equals", label: "not equals" },
30
- { key: "gt", label: "greater than" },
31
- { key: "gte", label: "greater than or equal" },
32
- { key: "lt", label: "less than" },
33
- { key: "lte", label: "less than or equal" },
34
- { key: "between", label: "between" },
29
+ { key: 'equals', label: 'equals' },
30
+ { key: 'not_equals', label: 'not equals' },
31
+ { key: 'gt', label: 'greater than' },
32
+ { key: 'gte', label: 'greater than or equal' },
33
+ { key: 'lt', label: 'less than' },
34
+ { key: 'lte', label: 'less than or equal' },
35
+ { key: 'between', label: 'between' },
35
36
  ],
36
- boolean: [{ key: "equals", label: "equals" }],
37
+ boolean: [{ key: 'equals', label: 'equals' }],
37
38
  date: [
38
- { key: "equals", label: "equals" },
39
- { key: "not_equals", label: "not equals" },
40
- { key: "gt", label: "after" },
41
- { key: "lt", label: "before" },
42
- { key: "between", label: "between" },
39
+ { key: 'equals', label: 'equals' },
40
+ { key: 'not_equals', label: 'not equals' },
41
+ { key: 'gt', label: 'after' },
42
+ { key: 'lt', label: 'before' },
43
+ { key: 'between', label: 'between' },
43
44
  ],
44
45
  };
45
46
  export function createFilterBuilder(collection, config, options) {
@@ -47,34 +48,37 @@ export function createFilterBuilder(collection, config, options) {
47
48
  return {
48
49
  schema() {
49
50
  const numberOps = type.raw({
50
- "not?": "number",
51
- "gt?": "number",
52
- "gte?": "number",
53
- "lt?": "number",
54
- "lte?": "number",
55
- "notIn?": "number[]",
51
+ 'not?': 'number',
52
+ 'gt?': 'number',
53
+ 'gte?': 'number',
54
+ 'lt?': 'number',
55
+ 'lte?': 'number',
56
+ 'notIn?': 'number[]',
56
57
  });
57
58
  const stringOps = type.raw({
58
- "not?": "string",
59
- "notIn?": "string[]",
59
+ 'not?': 'string',
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
- "not?": dateInput,
64
- "gt?": dateInput,
65
- "gte?": dateInput,
66
- "lt?": dateInput,
67
- "lte?": dateInput,
68
- "notIn?": `(${dateInput})[]`,
70
+ 'not?': dateInput,
71
+ 'gt?': dateInput,
72
+ 'gte?': dateInput,
73
+ 'lt?': dateInput,
74
+ 'lte?': dateInput,
75
+ 'notIn?': dateArrayInput,
69
76
  });
70
77
  const fieldSchemas = {
71
- number: type.raw("number").or(type.raw("number[]")).or(numberOps),
72
- string: type.raw("string").or(type.raw("string[]")).or(stringOps),
73
- boolean: type.raw("boolean"),
74
- date: type
75
- .raw(dateInput)
76
- .or(type.raw(`(${dateInput})[]`))
77
- .or(dateOps),
78
+ number: type.raw('number').or(type.raw('number[]')).or(numberOps),
79
+ string: type.raw('string').or(type.raw('string[]')).or(stringOps),
80
+ boolean: type.raw('boolean'),
81
+ date: dateInput.or(dateArrayInput).or(dateOps),
78
82
  };
79
83
  const descriptor = this.describe();
80
84
  const filterDef = {};
@@ -83,10 +87,10 @@ export function createFilterBuilder(collection, config, options) {
83
87
  }
84
88
  return type
85
89
  .raw({
86
- "query?": "string",
87
- "filter?": type.raw(filterDef),
88
- "page?": "number",
89
- "limit?": "number",
90
+ 'query?': 'string',
91
+ 'filter?': type.raw(filterDef),
92
+ 'page?': 'number',
93
+ 'limit?': 'number',
90
94
  })
91
95
  .as();
92
96
  },
@@ -106,8 +110,8 @@ export function createFilterBuilder(collection, config, options) {
106
110
  const fieldConfig = config[field.name];
107
111
  if (!fieldConfig)
108
112
  continue;
109
- const columnType = field.sourceExpression === "Date"
110
- ? "date"
113
+ const columnType = field.sourceExpression === 'Date'
114
+ ? 'date'
111
115
  : tsenseTypeMap[field.type];
112
116
  if (!columnType)
113
117
  continue;
@@ -122,12 +126,12 @@ export function createFilterBuilder(collection, config, options) {
122
126
  value: v,
123
127
  label: fieldConfig.labels?.[v] ?? v,
124
128
  }));
125
- column.conditions = withLabels(enumConditions, "enum");
129
+ column.conditions = withLabels(enumConditions, 'enum');
126
130
  }
127
131
  if (fieldConfig.presets) {
128
132
  column.presets = Object.entries(fieldConfig.presets).map(([name, filterOrFn]) => ({
129
133
  name,
130
- filter: (typeof filterOrFn === "function"
134
+ filter: (typeof filterOrFn === 'function'
131
135
  ? filterOrFn()
132
136
  : filterOrFn),
133
137
  }));
@@ -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
@@ -1,7 +1,8 @@
1
- export type { TsenseFieldMeta, TsenseFieldType } from "./env.js";
2
- export { rank } from "./rank.js";
3
- export { DateTransformer } from "./transformers/date.js";
4
- export { defaultTransformers } from "./transformers/defaults.js";
5
- export type { FieldTransformer } from "./transformers/types.js";
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";
1
+ export type { TsenseFieldMeta, TsenseFieldType } from './env.js';
2
+ export { rank } from './rank.js';
3
+ export { DateTransformer } from './transformers/date.js';
4
+ export { defaultTransformers } from './transformers/defaults.js';
5
+ export type { FieldTransformer } from './transformers/types.js';
6
+ export { TSense } from './tsense.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
@@ -1,4 +1,5 @@
1
- export { rank } from "./rank.js";
2
- export { DateTransformer } from "./transformers/date.js";
3
- export { defaultTransformers } from "./transformers/defaults.js";
4
- export { TSense } from "./tsense.js";
1
+ export { rank } from './rank.js';
2
+ export { DateTransformer } from './transformers/date.js';
3
+ export { defaultTransformers } from './transformers/defaults.js';
4
+ export { TSense } from './tsense.js';
5
+ export { isRelativeDate, resolveRelativeDate, } from './filters/relative-dates.js';
package/dist/tsense.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { Type } from "arktype";
2
- import redaxios from "redaxios";
3
- import type { DeleteResult, FieldSchema, FilterFor, ProjectSearch, ScopedCollection, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchResult, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult } from "./types.js";
1
+ import type { Type } from 'arktype';
2
+ import redaxios from 'redaxios';
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>>);
@@ -64,7 +64,7 @@ export declare class TSense<T extends Type> {
64
64
  private synced;
65
65
  private fieldTransformers;
66
66
  private dataSyncConfig?;
67
- infer: T["infer"];
67
+ infer: T['infer'];
68
68
  constructor(options: TsenseOptions<T>);
69
69
  private getBaseType;
70
70
  private inferType;
@@ -78,28 +78,30 @@ 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;
84
86
  create(): Promise<this>;
85
87
  drop(): Promise<void>;
86
- get(id: string): Promise<T["infer"] | null>;
88
+ get(id: string): Promise<T['infer'] | null>;
87
89
  delete(id: string): Promise<boolean>;
88
90
  private deleteManyWithFilterBy;
89
- deleteMany(filter: FilterFor<T["infer"]>): Promise<DeleteResult>;
90
- update(id: string, data: Partial<T["infer"]>): Promise<T["infer"]>;
91
+ deleteMany(filter: FilterFor<T['infer']>): Promise<DeleteResult>;
92
+ update(id: string, data: Partial<T['infer']>): Promise<T['infer']>;
91
93
  private updateManyWithFilterBy;
92
- updateMany(filter: FilterFor<T["infer"]>, data: Partial<T["infer"]>): Promise<UpdateResult>;
94
+ updateMany(filter: FilterFor<T['infer']>, data: Partial<T['infer']>): Promise<UpdateResult>;
93
95
  private searchWithFilterBy;
94
- search<const O extends SearchOptions<T["infer"]> = SearchOptionsPlain<T["infer"]>>(options: O): Promise<SearchResult<ProjectSearch<T["infer"], O>>>;
96
+ search<const O extends SearchOptions<T['infer']> = SearchOptionsPlain<T['infer']>>(options: O): Promise<SearchResult<ProjectSearch<T['infer'], O>>>;
95
97
  private searchListWithFilterBy;
96
- searchList(options: SearchListOptions<T["infer"]>): Promise<SearchListResult<T["infer"]>>;
98
+ searchList(options: SearchListOptions<T['infer']>): Promise<SearchListResult<T['infer']>>;
97
99
  private countWithFilterBy;
98
- count(filter?: FilterFor<T["infer"]>): Promise<number>;
99
- upsert(docs: T["infer"] | T["infer"][]): Promise<UpsertResult[]>;
100
+ count(filter?: FilterFor<T['infer']>): Promise<number>;
101
+ upsert(docs: T['infer'] | T['infer'][]): Promise<UpsertResult[]>;
100
102
  syncData(options?: SyncOptions): Promise<SyncResult>;
101
103
  private purgeOrphans;
102
104
  exportIds(): Promise<string[]>;
103
- scoped(baseFilter: FilterFor<T["infer"]>): ScopedCollection<T["infer"]>;
105
+ scoped(baseFilter: FilterFor<T['infer']>): ScopedCollection<T['infer']>;
104
106
  }
105
107
  export {};
package/dist/tsense.js CHANGED
@@ -1,6 +1,7 @@
1
- import redaxios from "redaxios";
2
- import { TSenseMigrator } from "./migrator.js";
3
- import { defaultTransformers } from "./transformers/defaults.js";
1
+ import redaxios from 'redaxios';
2
+ import { isRelativeDate, resolveRelativeDate, } from './filters/relative-dates.js';
3
+ import { TSenseMigrator } from './migrator.js';
4
+ import { defaultTransformers } from './transformers/defaults.js';
4
5
  function chunkArray(arr, size) {
5
6
  const chunks = [];
6
7
  for (let i = 0; i < arr.length; i += size) {
@@ -10,8 +11,8 @@ function chunkArray(arr, size) {
10
11
  }
11
12
  const redaxiosInstance = redaxios.default ?? redaxios;
12
13
  function escapeFilterValue(value) {
13
- if (typeof value === "string") {
14
- return `\`${value.replaceAll("`", "")}\``;
14
+ if (typeof value === 'string') {
15
+ return `\`${value.replaceAll('`', '')}\``;
15
16
  }
16
17
  if (Array.isArray(value)) {
17
18
  return value.map(escapeFilterValue);
@@ -24,17 +25,17 @@ const filterOperators = {
24
25
  gte: (k, v) => `${k}:>=${v}`,
25
26
  lt: (k, v) => `${k}:<${v}`,
26
27
  lte: (k, v) => `${k}:<=${v}`,
27
- notIn: (k, v) => `${k}:!=[${v.join(",")}]`,
28
+ notIn: (k, v) => `${k}:!=[${v.join(',')}]`,
28
29
  };
29
30
  const arkToTsense = {
30
- string: "string",
31
- number: "float",
32
- "number.integer": "int64",
33
- "number % 1": "int64",
34
- boolean: "bool",
35
- "string[]": "string[]",
36
- "number[]": "float[]",
37
- "boolean[]": "bool[]",
31
+ string: 'string',
32
+ number: 'float',
33
+ 'number.integer': 'int64',
34
+ 'number % 1': 'int64',
35
+ boolean: 'bool',
36
+ 'string[]': 'string[]',
37
+ 'number[]': 'float[]',
38
+ 'boolean[]': 'bool[]',
38
39
  };
39
40
  export class TSense {
40
41
  options;
@@ -48,27 +49,27 @@ export class TSense {
48
49
  this.options = options;
49
50
  this.axios = redaxiosInstance.create({
50
51
  baseURL: `${options.connection.protocol}://${options.connection.host}:${options.connection.port}`,
51
- headers: { "X-TYPESENSE-API-KEY": options.connection.apiKey },
52
+ headers: { 'X-TYPESENSE-API-KEY': options.connection.apiKey },
52
53
  });
53
54
  this.fields = this.extractFields(options.transformers ?? defaultTransformers);
54
55
  this.dataSyncConfig = options.dataSync;
55
56
  }
56
57
  getBaseType(expression, domain) {
57
- if (domain && domain !== "undefined")
58
+ if (domain && domain !== 'undefined')
58
59
  return domain;
59
- return expression.replace(/ \| undefined$/, "");
60
+ return expression.replace(/ \| undefined$/, '');
60
61
  }
61
62
  inferType(arkType) {
62
63
  const direct = arkToTsense[arkType];
63
64
  if (direct)
64
65
  return direct;
65
- if (arkType.includes("[]"))
66
- return "object[]";
66
+ if (arkType.includes('[]'))
67
+ return 'object[]';
67
68
  if (arkType.includes("'") || arkType.includes('"'))
68
- return "string";
69
- if (arkType.includes("{") || arkType.includes("|"))
70
- return "object";
71
- return "string";
69
+ return 'string';
70
+ if (arkType.includes('{') || arkType.includes('|'))
71
+ return 'object';
72
+ return 'string';
72
73
  }
73
74
  serializeDoc(doc) {
74
75
  const result = { ...doc };
@@ -99,7 +100,7 @@ export class TSense {
99
100
  if (Array.isArray(value)) {
100
101
  return value.map((v) => transformer.serialize(v));
101
102
  }
102
- if (typeof value === "object" &&
103
+ if (typeof value === 'object' &&
103
104
  value !== null &&
104
105
  Object.getPrototypeOf(value) === Object.prototype) {
105
106
  const result = {};
@@ -126,7 +127,7 @@ export class TSense {
126
127
  const branches = prop.value.branches ?? [];
127
128
  const enumValues = [];
128
129
  for (const branch of branches) {
129
- if (typeof branch.unit === "string") {
130
+ if (typeof branch.unit === 'string') {
130
131
  enumValues.push(branch.unit);
131
132
  }
132
133
  }
@@ -142,7 +143,7 @@ export class TSense {
142
143
  name: prop.key,
143
144
  type: transformer.storageType,
144
145
  sourceExpression: expression,
145
- optional: prop.kind === "optional",
146
+ optional: prop.kind === 'optional',
146
147
  facet: meta?.facet,
147
148
  sort: meta?.sort,
148
149
  index: meta?.index,
@@ -155,7 +156,7 @@ export class TSense {
155
156
  name: prop.key,
156
157
  type,
157
158
  sourceExpression: expression,
158
- optional: prop.kind === "optional",
159
+ optional: prop.kind === 'optional',
159
160
  facet: meta?.facet,
160
161
  sort: meta?.sort,
161
162
  index: meta?.index,
@@ -179,7 +180,7 @@ export class TSense {
179
180
  const escapedLte = escapeFilterValue(value.lte);
180
181
  const parts = [`${key}:[${escaped}..${escapedLte}]`];
181
182
  for (const [op, opValue] of Object.entries(value)) {
182
- if (op === "gte" || op === "lte" || opValue == null)
183
+ if (op === 'gte' || op === 'lte' || opValue == null)
183
184
  continue;
184
185
  const builder = filterOperators[op];
185
186
  if (builder) {
@@ -205,34 +206,34 @@ export class TSense {
205
206
  const [key, rawValue] = entry;
206
207
  if (rawValue == null)
207
208
  continue;
208
- if (key === "OR") {
209
+ if (key === 'OR') {
209
210
  const orParts = [];
210
211
  for (const condition of rawValue) {
211
212
  const inner = this.buildFilter(condition);
212
213
  if (!inner.length) {
213
214
  continue;
214
215
  }
215
- orParts.push(`(${inner.join("&&")})`);
216
+ orParts.push(`(${inner.join('&&')})`);
216
217
  }
217
218
  if (!orParts.length) {
218
219
  continue;
219
220
  }
220
- result.push(`(${orParts.join("||")})`);
221
+ result.push(`(${orParts.join('||')})`);
221
222
  continue;
222
223
  }
223
224
  const value = this.serializeFilterValue(key, rawValue);
224
225
  const escaped = escapeFilterValue(value);
225
- if (typeof escaped === "string" ||
226
- typeof escaped === "number" ||
227
- typeof escaped === "boolean") {
226
+ if (typeof escaped === 'string' ||
227
+ typeof escaped === 'number' ||
228
+ typeof escaped === 'boolean') {
228
229
  result.push(`${key}:=${escaped}`);
229
230
  continue;
230
231
  }
231
232
  if (Array.isArray(escaped)) {
232
- result.push(`${key}:[${escaped.join(",")}]`);
233
+ result.push(`${key}:[${escaped.join(',')}]`);
233
234
  continue;
234
235
  }
235
- if (typeof value === "object" && value !== null) {
236
+ if (typeof value === 'object' && value !== null) {
236
237
  result.push(...this.buildObjectFilter(key, value));
237
238
  }
238
239
  }
@@ -247,7 +248,7 @@ export class TSense {
247
248
  if (value == null) {
248
249
  continue;
249
250
  }
250
- if (key === "OR") {
251
+ if (key === 'OR') {
251
252
  for (const condition of value) {
252
253
  this.validateFilterFields(condition);
253
254
  }
@@ -263,43 +264,79 @@ export class TSense {
263
264
  validateFields(fields) {
264
265
  const valid = new Set(this.fields.map((f) => f.name));
265
266
  for (const field of fields) {
266
- if (field !== "score" && !valid.has(field)) {
267
+ if (field !== 'score' && !valid.has(field)) {
267
268
  throw new Error(`INVALID_FIELD: ${field}`);
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
  }
277
- return `(${parts.join("&&")})`;
314
+ return `(${parts.join('&&')})`;
278
315
  }
279
316
  combineFilterExpressions(...filters) {
280
317
  return filters
281
318
  .map((filter) => this.buildFilterExpression(filter))
282
319
  .filter((filter) => filter != null)
283
- .join("&&");
320
+ .join('&&');
284
321
  }
285
322
  buildSort(options) {
286
323
  if (!options.sortBy)
287
324
  return;
288
325
  const result = [];
289
326
  for (const item of options.sortBy) {
290
- const [field, direction] = item.split(":");
291
- if (field === "undefined")
327
+ const [field, direction] = item.split(':');
328
+ if (field === 'undefined')
292
329
  continue;
293
- const realField = field === "score" ? "_text_match" : field;
330
+ const realField = field === 'score' ? '_text_match' : field;
294
331
  result.push(`${realField}:${direction}`);
295
332
  }
296
- return result.join(",");
333
+ return result.join(',');
297
334
  }
298
335
  async create() {
299
- const enableNested = this.fields.some((f) => f.type === "object" || f.type === "object[]");
336
+ const enableNested = this.fields.some((f) => f.type === 'object' || f.type === 'object[]');
300
337
  await this.axios({
301
- method: "POST",
302
- url: "/collections",
338
+ method: 'POST',
339
+ url: '/collections',
303
340
  data: {
304
341
  name: this.options.name,
305
342
  fields: this.fields,
@@ -311,14 +348,14 @@ export class TSense {
311
348
  }
312
349
  async drop() {
313
350
  await this.axios({
314
- method: "DELETE",
351
+ method: 'DELETE',
315
352
  url: `/collections/${this.options.name}`,
316
353
  });
317
354
  }
318
355
  async get(id) {
319
356
  await this.ensureSynced();
320
357
  const { data } = await this.axios({
321
- method: "GET",
358
+ method: 'GET',
322
359
  url: `/collections/${this.options.name}/documents/${id}`,
323
360
  }).catch((e) => {
324
361
  if (e.status === 404)
@@ -330,7 +367,7 @@ export class TSense {
330
367
  async delete(id) {
331
368
  await this.ensureSynced();
332
369
  const { data } = await this.axios({
333
- method: "DELETE",
370
+ method: 'DELETE',
334
371
  url: `/collections/${this.options.name}/documents/${id}`,
335
372
  }).catch((e) => {
336
373
  if (e.status === 404)
@@ -342,10 +379,10 @@ export class TSense {
342
379
  async deleteManyWithFilterBy(filterBy) {
343
380
  await this.ensureSynced();
344
381
  if (!filterBy) {
345
- throw new Error("FILTER_REQUIRED");
382
+ throw new Error('FILTER_REQUIRED');
346
383
  }
347
384
  const { data } = await this.axios({
348
- method: "DELETE",
385
+ method: 'DELETE',
349
386
  url: `/collections/${this.options.name}/documents`,
350
387
  params: { filter_by: filterBy },
351
388
  });
@@ -358,7 +395,7 @@ export class TSense {
358
395
  await this.ensureSynced();
359
396
  const serialized = this.serializeDoc(data);
360
397
  const { data: updated } = await this.axios({
361
- method: "PATCH",
398
+ method: 'PATCH',
362
399
  url: `/collections/${this.options.name}/documents/${id}`,
363
400
  data: serialized,
364
401
  });
@@ -367,11 +404,11 @@ export class TSense {
367
404
  async updateManyWithFilterBy(filterBy, data) {
368
405
  await this.ensureSynced();
369
406
  if (!filterBy) {
370
- throw new Error("FILTER_REQUIRED");
407
+ throw new Error('FILTER_REQUIRED');
371
408
  }
372
409
  const serialized = this.serializeDoc(data);
373
410
  const { data: result } = await this.axios({
374
- method: "PATCH",
411
+ method: 'PATCH',
375
412
  url: `/collections/${this.options.name}/documents`,
376
413
  params: { filter_by: filterBy },
377
414
  data: serialized,
@@ -389,15 +426,15 @@ export class TSense {
389
426
  this.validateFields(queryByFields);
390
427
  if (options.sortBy) {
391
428
  this.validateFields(options.sortBy
392
- .map((s) => s.split(":")[0])
393
- .filter((f) => f !== "undefined"));
429
+ .map((s) => s.split(':')[0])
430
+ .filter((f) => f !== 'undefined'));
394
431
  }
395
432
  if (options.facetBy) {
396
433
  this.validateFields(options.facetBy);
397
434
  }
398
- const queryBy = queryByFields.join(",");
435
+ const queryBy = queryByFields.join(',');
399
436
  const params = {
400
- q: options.query ?? "*",
437
+ q: options.query ?? '*',
401
438
  query_by: queryBy,
402
439
  };
403
440
  const sortBy = this.buildSort(options);
@@ -409,20 +446,20 @@ export class TSense {
409
446
  params.page = options.page;
410
447
  if (options.limit != null)
411
448
  params.per_page = options.limit;
412
- const facetBy = options.facetBy?.join(",");
449
+ const facetBy = options.facetBy?.join(',');
413
450
  if (facetBy)
414
451
  params.facet_by = facetBy;
415
- if ("pick" in options && options.pick) {
416
- params.include_fields = options.pick.join(",");
452
+ if ('pick' in options && options.pick) {
453
+ params.include_fields = options.pick.join(',');
417
454
  }
418
- if ("omit" in options && options.omit) {
419
- params.exclude_fields = options.omit.join(",");
455
+ if ('omit' in options && options.omit) {
456
+ params.exclude_fields = options.omit.join(',');
420
457
  }
421
458
  const highlight = options.highlight;
422
- const highlightOpts = typeof highlight === "object" ? highlight : undefined;
459
+ const highlightOpts = typeof highlight === 'object' ? highlight : undefined;
423
460
  if (highlightOpts) {
424
461
  if (highlightOpts.fields) {
425
- params.highlight_fields = highlightOpts.fields.join(",");
462
+ params.highlight_fields = highlightOpts.fields.join(',');
426
463
  }
427
464
  if (highlightOpts.startTag) {
428
465
  params.highlight_start_tag = highlightOpts.startTag;
@@ -432,7 +469,7 @@ export class TSense {
432
469
  }
433
470
  }
434
471
  const { data: res } = await this.axios({
435
- method: "GET",
472
+ method: 'GET',
436
473
  url: `/collections/${this.options.name}/documents/search`,
437
474
  params,
438
475
  });
@@ -495,19 +532,19 @@ export class TSense {
495
532
  await this.ensureSynced();
496
533
  if (!filterBy) {
497
534
  const { data } = await this.axios({
498
- method: "GET",
535
+ method: 'GET',
499
536
  url: `/collections/${this.options.name}`,
500
537
  });
501
538
  return data.num_documents;
502
539
  }
503
540
  const params = {
504
- q: "*",
541
+ q: '*',
505
542
  query_by: this.options.defaultSearchField,
506
543
  per_page: 0,
507
544
  filter_by: filterBy,
508
545
  };
509
546
  const { data } = await this.axios({
510
- method: "GET",
547
+ method: 'GET',
511
548
  url: `/collections/${this.options.name}/documents/search`,
512
549
  params,
513
550
  });
@@ -528,26 +565,26 @@ export class TSense {
528
565
  }
529
566
  const payload = items
530
567
  .map((item) => JSON.stringify(this.serializeDoc(item)))
531
- .join("\n");
532
- const params = { action: "upsert" };
568
+ .join('\n');
569
+ const params = { action: 'upsert' };
533
570
  if (this.options.batchSize) {
534
571
  params.batch_size = this.options.batchSize;
535
572
  }
536
573
  const { data } = await this.axios({
537
- method: "POST",
574
+ method: 'POST',
538
575
  url: `/collections/${this.options.name}/documents/import`,
539
- headers: { "Content-Type": "text/plain" },
576
+ headers: { 'Content-Type': 'text/plain' },
540
577
  params,
541
578
  data: payload,
542
579
  });
543
- if (typeof data === "string") {
544
- return data.split("\n").map((v) => JSON.parse(v));
580
+ if (typeof data === 'string') {
581
+ return data.split('\n').map((v) => JSON.parse(v));
545
582
  }
546
583
  return [data];
547
584
  }
548
585
  async syncData(options) {
549
586
  if (!this.dataSyncConfig) {
550
- throw new Error("DATA_SYNC_NOT_CONFIGURED");
587
+ throw new Error('DATA_SYNC_NOT_CONFIGURED');
551
588
  }
552
589
  const chunkSize = options?.chunkSize ?? this.dataSyncConfig.chunkSize ?? 500;
553
590
  const ids = options?.ids ?? (await this.dataSyncConfig.getAllIds());
@@ -584,12 +621,12 @@ export class TSense {
584
621
  }
585
622
  async exportIds() {
586
623
  const { data } = await this.axios({
587
- method: "GET",
624
+ method: 'GET',
588
625
  url: `/collections/${this.options.name}/documents/export`,
589
- params: { include_fields: "id" },
626
+ params: { include_fields: 'id' },
590
627
  });
591
628
  return data
592
- .split("\n")
629
+ .split('\n')
593
630
  .filter((line) => line.length)
594
631
  .map((line) => JSON.parse(line).id);
595
632
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Type } from "arktype";
2
- import type { FieldTransformer } from "./transformers/types.js";
1
+ import type { Type } from 'arktype';
2
+ import type { FieldTransformer } from './transformers/types.js';
3
3
  type BaseIfArray<T> = T extends (infer Q)[] ? Q : T;
4
4
  export type FieldSchema = {
5
5
  name: string;
@@ -32,7 +32,7 @@ export type SearchApiResponse<T> = {
32
32
  export type ConnectionConfig = {
33
33
  host: string;
34
34
  port: number;
35
- protocol: "http" | "https";
35
+ protocol: 'http' | 'https';
36
36
  apiKey: string;
37
37
  timeout?: number;
38
38
  };
@@ -40,14 +40,22 @@ export type TsenseOptions<T extends Type> = {
40
40
  name: string;
41
41
  schema: T;
42
42
  connection: ConnectionConfig;
43
- defaultSearchField?: keyof T["infer"];
44
- defaultSortingField?: keyof T["infer"];
43
+ defaultSearchField?: keyof T['infer'];
44
+ defaultSortingField?: keyof T['infer'];
45
45
  batchSize?: number;
46
46
  validateOnUpsert?: boolean;
47
47
  autoSyncSchema?: boolean;
48
+ timezone?: string;
48
49
  transformers?: FieldTransformer[];
49
- dataSync?: SyncConfig<T["infer"]>;
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
  }>;
@@ -80,12 +88,12 @@ export type HighlightOptions<T> = {
80
88
  startTag?: string;
81
89
  endTag?: string;
82
90
  };
83
- type SortableField<T> = Extract<keyof T, string> | "score";
91
+ type SortableField<T> = Extract<keyof T, string> | 'score';
84
92
  export type BaseSearchOptions<T> = {
85
93
  query?: string;
86
94
  queryBy?: (keyof T)[];
87
95
  filter?: FilterFor<T>;
88
- sortBy?: `${SortableField<T>}:${"asc" | "desc"}`[];
96
+ sortBy?: `${SortableField<T>}:${'asc' | 'desc'}`[];
89
97
  facetBy?: (keyof T)[];
90
98
  page?: number;
91
99
  limit?: number;
@@ -136,7 +144,7 @@ export type SearchListOptions<T> = {
136
144
  query?: string;
137
145
  queryBy?: (keyof T)[];
138
146
  filter?: FilterFor<T>;
139
- sortBy: `${Extract<keyof T, string>}:${"asc" | "desc"}`;
147
+ sortBy: `${Extract<keyof T, string>}:${'asc' | 'desc'}`;
140
148
  limit?: number;
141
149
  cursor?: string;
142
150
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsense",
3
- "version": "0.2.0-next.3",
3
+ "version": "0.2.0-next.5",
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": {