tsense 0.0.4 → 0.0.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.
package/README.md CHANGED
@@ -1,125 +1,151 @@
1
- ## TSense (WIP)
1
+ # TSense
2
2
 
3
- Opinionated, fully-typed typesense client
3
+ Opinionated, fully-typed Typesense client powered by [Arktype](https://arktype.io/)
4
4
 
5
- # TO-DO
6
- - [ ] Remove base typesense dependency
7
- - [ ] Better filter support (includes, exact, etc...)
8
- - [ ] Documentation
9
- - [ ] Improve tests
10
- - [ ] Facet
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add tsense arktype
9
+ ```
10
+
11
+ ## Example
11
12
 
12
- # Example
13
13
  ```typescript
14
- import { Client } from "typesense";
14
+ import { type } from "arktype";
15
15
  import { TSense } from "tsense";
16
16
 
17
- export const client = new Client({
18
- nodes: [
19
- {
20
- host: "127.0.0.1",
21
- port: 8108,
22
- protocol: "http",
23
- },
24
- ],
25
- apiKey: "123",
26
- connectionTimeoutSeconds: 2,
17
+ const UsersCollection = new TSense({
18
+ name: "users",
19
+ schema: type({
20
+ "id?": "string",
21
+ email: "string",
22
+ age: type("number.integer").configure({ type: "int32", sort: true }),
23
+ "company?": type.enumerated("netflix", "google").configure({ facet: true }),
24
+ "phone?": "string",
25
+ name: type("string").configure({ sort: true }),
26
+ "work_history?": type({
27
+ company: "string",
28
+ date: "string",
29
+ })
30
+ .array()
31
+ .configure({ type: "object[]", index: false }),
32
+ }),
33
+ connection: {
34
+ host: "127.0.0.1",
35
+ port: 8108,
36
+ protocol: "http",
37
+ apiKey: "123",
38
+ },
39
+ defaultSearchField: "name",
40
+ validateOnUpsert: true,
27
41
  });
28
42
 
29
- export const UsersCollection = new TSense("users", {
30
- client,
31
- fields: {
32
- // specify the typesense type directly as a string
33
- email: "string",
34
- age: "int32",
35
- // suffix it with a "?" to mark as optional
36
- phone: "string?",
37
- name: {
38
- type: "string",
39
- sort: true,
40
- },
41
- company: {
42
- type: "string",
43
- override: {} as "netflix" | "google",
44
- facet: true,
45
- optional: true,
46
- },
47
- work_history: {
48
- // object and object[] auto-infers enable_nested_fields
49
- type: "object[]",
50
- index: false,
51
- optional: true,
52
- override: {} as {
53
- company: string;
54
- date: string;
55
- }[],
56
- },
57
- },
58
- default_search_field: "name",
43
+ await UsersCollection.create();
44
+
45
+ await UsersCollection.upsert([
46
+ { id: "1", email: "john@example.com", age: 30, name: "John Doe", company: "netflix" },
47
+ { id: "2", email: "jane@example.com", age: 25, name: "Jane Smith", company: "google" },
48
+ ]);
49
+
50
+ const results = await UsersCollection.search({
51
+ query: "john",
52
+ queryBy: ["name", "email"],
53
+ sortBy: ["age:desc", "name:asc"],
54
+ filter: {
55
+ age: { min: 20 },
56
+ OR: [{ company: "google" }, { company: "netflix" }],
57
+ },
59
58
  });
60
59
 
61
- // infer the collection type (undefined at runtime)
62
- typeof UsersCollection.infer;
63
- /*
64
- {
65
- id?: string | undefined;
66
- phone?: string | null | undefined;
67
- work_history?: {
68
- company: string;
69
- date: string;
70
- }[] | null | undefined;
71
- email: string;
72
- age: number;
73
- name: string;
74
- }
75
- */
76
-
77
- const results = await UsersCollection.searchDocuments({
78
- search: "john",
79
- search_keys: ["name"],
80
- // can sort multiple fields
81
- order_by: ["age desc", "name asc"],
82
- // compiles into
83
- // age:>=20&&((email:=@google.com)||(email:=@netflix.com))
84
- filter: {
85
- // min and max range on numbers
86
- age: {
87
- min: 20,
88
- },
89
- // OR syntax similar to prisma
90
- OR: [
91
- {
92
- email: "@google.com",
93
- },
94
- {
95
- email: "@netflix.com",
96
- },
97
- ],
98
- },
60
+ const faceted = await UsersCollection.search({
61
+ query: "john",
62
+ facetBy: ["company"],
99
63
  });
100
64
 
101
- /*
102
- typed as
103
- count: number;
104
- data: {
105
- id?: string | undefined;
106
- phone?: string | null | undefined;
107
- ...
108
- }[];
109
- facet: {
110
- netflix: number;
111
- google: number;
112
- // enabled by enable_facet_total
113
- total: number;
114
- };
115
- */
116
- const faceted = await UsersCollection.searchDocuments(
117
- {
118
- search: "john",
119
- },
120
- {
121
- facet_by: "company",
122
- enable_facet_total: true,
123
- },
124
- );
125
- ```
65
+ const highlighted = await UsersCollection.search({
66
+ query: "john",
67
+ highlight: true,
68
+ });
69
+
70
+ await UsersCollection.drop();
71
+ ```
72
+
73
+ ## API Reference
74
+
75
+ ### Constructor
76
+
77
+ ```typescript
78
+ new TSense({
79
+ name: string,
80
+ schema: Type,
81
+ connection: ConnectionConfig,
82
+ defaultSearchField?: keyof T,
83
+ defaultSortingField?: keyof T,
84
+ batchSize?: number,
85
+ validateOnUpsert?: boolean,
86
+ })
87
+ ```
88
+
89
+ ### ConnectionConfig
90
+
91
+ | Option | Type | Description |
92
+ | ---------- | --------------------- | --------------------- |
93
+ | `host` | `string` | Typesense server host |
94
+ | `port` | `number` | Typesense server port |
95
+ | `protocol` | `"http"` \| `"https"` | Connection protocol |
96
+ | `apiKey` | `string` | Typesense API key |
97
+
98
+ ### Schema Configuration
99
+
100
+ Use `.configure()` to set Typesense field options:
101
+
102
+ ```typescript
103
+ type("string").configure({
104
+ type: "string",
105
+ facet: true,
106
+ sort: true,
107
+ index: true,
108
+ });
109
+ ```
110
+
111
+ ### Collection Methods
112
+
113
+ | Method | Description |
114
+ | ------ | ----------- |
115
+ | `create()` | Creates the collection in Typesense |
116
+ | `drop()` | Deletes the collection |
117
+ | `get(id)` | Retrieves a document by ID |
118
+ | `delete(id)` | Deletes a document by ID |
119
+ | `deleteMany(filter)` | Deletes documents matching filter |
120
+ | `update(id, data)` | Updates a document by ID |
121
+ | `updateMany(filter, data)` | Updates documents matching filter |
122
+ | `upsert(docs)` | Inserts or updates documents |
123
+ | `search(options)` | Searches the collection |
124
+
125
+ ### Search Options
126
+
127
+ | Option | Type | Description |
128
+ | ----------- | --------------------------------- | ------------------------ |
129
+ | `query` | `string` | Text search query |
130
+ | `queryBy` | `(keyof T)[]` | Fields to search in |
131
+ | `filter` | `FilterFor<T>` | Filter conditions |
132
+ | `sortBy` | `"field:asc\|desc"[]` | Sort order |
133
+ | `facetBy` | `(keyof T)[]` | Fields to facet by |
134
+ | `page` | `number` | Page number |
135
+ | `limit` | `number` | Results per page |
136
+ | `pick` | `(keyof T)[]` | Only return these fields |
137
+ | `omit` | `(keyof T)[]` | Exclude these fields |
138
+ | `highlight` | `boolean \| HighlightOptions<T>` | Enable highlighting |
139
+
140
+ Note: `pick` and `omit` are mutually exclusive.
141
+
142
+ ### Filter Syntax
143
+
144
+ ```typescript
145
+ filter: { name: "John" } // Exact match
146
+ filter: { age: 30 } // Numeric match
147
+ filter: { age: [25, 30, 35] } // IN
148
+ filter: { age: { min: 20, max: 40 } } // Range
149
+ filter: { name: { not: "John" } } // Not equal
150
+ filter: { OR: [{ age: 25 }, { age: 30 }] } // OR conditions
151
+ ```
package/dist/env.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type TsenseFieldType = "string" | "int32" | "int64" | "float" | "bool" | "image" | "string[]" | "int32[]" | "int64[]" | "float[]" | "bool[]" | "geopoint" | "geopoint[]" | "object" | "object[]" | "auto" | "string*";
2
+ export type TsenseFieldMeta = {
3
+ type?: TsenseFieldType;
4
+ facet?: boolean;
5
+ sort?: boolean;
6
+ index?: boolean;
7
+ };
8
+ declare global {
9
+ interface ArkEnv {
10
+ meta(): TsenseFieldMeta;
11
+ }
12
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
+ export type { TsenseFieldMeta, TsenseFieldType } from "./env.js";
1
2
  export { TSense } from "./tsense.js";
3
+ export type { ConnectionConfig, DeleteResult, FilterFor, HighlightOptions, SearchOptions, SearchResult, TsenseOptions, UpdateResult, UpsertResult, } from "./types.js";
package/dist/tsense.d.ts CHANGED
@@ -1,34 +1,24 @@
1
- import type { Client } from "typesense";
2
- import type { CustomCollectionField, Filters, InferCollectionTypes } from "./types/core.js";
3
- import type { Simplify } from "./types/helpers.js";
4
- export declare class TSense<Options extends CustomCollectionField, Fields extends Record<string, Options>, Inferred = InferCollectionTypes<Fields>> {
5
- private name;
6
- private data;
7
- infer: Inferred;
8
- constructor(name: string, data: {
9
- fields: Fields;
10
- client: Client;
11
- default_search_field?: NoInfer<keyof Inferred>;
12
- default_sorting_field?: NoInfer<keyof Inferred>;
13
- batch_size?: number;
14
- enable_nested_fields?: boolean;
15
- });
16
- private checkNested;
1
+ import type { Type } from "arktype";
2
+ import type { DeleteResult, FilterFor, SearchOptions, SearchResult, TsenseOptions, UpdateResult, UpsertResult } from "./types.js";
3
+ export declare class TSense<T extends Type> {
4
+ private options;
5
+ private fields;
6
+ private enableNested;
7
+ private baseURL;
8
+ private headers;
9
+ constructor(options: TsenseOptions<T>);
10
+ private inferType;
11
+ private extractFields;
17
12
  private buildObjectFilter;
18
- private buildSort;
19
13
  private buildFilter;
20
- private maybeArray;
21
- delete(): Promise<void>;
14
+ private buildSort;
22
15
  create(): Promise<this>;
23
- searchDocuments<FacetBy extends keyof Inferred, EnableFacetTotal extends boolean | undefined = undefined>(data: Filters<Inferred>, facet?: {
24
- facet_by?: FacetBy;
25
- enable_facet_total?: EnableFacetTotal;
26
- }): Promise<{
27
- count: number;
28
- data: Inferred[];
29
- facet: FacetBy extends keyof Inferred ? Simplify<Record<NonNullable<Inferred[FacetBy]> extends string ? NonNullable<Inferred[FacetBy]> : never, number> & (EnableFacetTotal extends true ? {
30
- total: number;
31
- } : {})> : never;
32
- }>;
33
- upsertDocuments(items: Inferred | Inferred[]): Promise<any[] | undefined>;
16
+ drop(): Promise<void>;
17
+ get(id: string): Promise<T["infer"] | null>;
18
+ delete(id: string): Promise<boolean>;
19
+ deleteMany(filter: FilterFor<T["infer"]>): Promise<DeleteResult>;
20
+ update(id: string, data: Partial<T["infer"]>): Promise<T["infer"]>;
21
+ updateMany(filter: FilterFor<T["infer"]>, data: Partial<T["infer"]>): Promise<UpdateResult>;
22
+ search(options: SearchOptions<T["infer"]>): Promise<SearchResult<T["infer"]>>;
23
+ upsert(docs: T["infer"] | T["infer"][]): Promise<UpsertResult[]>;
34
24
  }
package/dist/tsense.js CHANGED
@@ -7,32 +7,73 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ var _a;
11
+ import redaxios from "redaxios";
12
+ const axios = (_a = redaxios.default) !== null && _a !== void 0 ? _a : redaxios;
10
13
  const requiresNested = ["object", "object[]"];
14
+ const arkToTsense = {
15
+ string: "string",
16
+ number: "float",
17
+ "number.integer": "int64",
18
+ boolean: "bool",
19
+ "string[]": "string[]",
20
+ "number[]": "float[]",
21
+ "boolean[]": "bool[]",
22
+ };
11
23
  export class TSense {
12
- constructor(name, data) {
13
- this.name = name;
14
- this.data = data;
15
- this.infer = undefined;
24
+ constructor(options) {
25
+ this.options = options;
26
+ this.fields = [];
27
+ this.enableNested = false;
28
+ const { connection } = options;
29
+ this.baseURL = `${connection.protocol}://${connection.host}:${connection.port}`;
30
+ this.headers = { "X-TYPESENSE-API-KEY": connection.apiKey };
31
+ this.extractFields();
16
32
  }
17
- checkNested(fields) {
18
- if (this.data.enable_nested_fields) {
19
- return;
20
- }
21
- for (const field of fields) {
22
- requiresNested.includes(field.type);
23
- this.data.enable_nested_fields = true;
24
- return;
33
+ inferType(arkType) {
34
+ const direct = arkToTsense[arkType];
35
+ if (direct)
36
+ return direct;
37
+ if (arkType.includes("[]"))
38
+ return "object[]";
39
+ if (arkType.includes("{") || arkType.includes("|"))
40
+ return "object";
41
+ if (arkType.startsWith("'") || arkType.includes("'"))
42
+ return "string";
43
+ return "string";
44
+ }
45
+ extractFields() {
46
+ var _a;
47
+ const internal = this.options.schema;
48
+ for (const prop of internal.structure.props) {
49
+ const meta = prop.value.meta;
50
+ const expression = String(prop.value.expression);
51
+ const domain = prop.value.domain;
52
+ const tsType = (_a = meta === null || meta === void 0 ? void 0 : meta.type) !== null && _a !== void 0 ? _a : this.inferType(domain !== null && domain !== void 0 ? domain : expression);
53
+ if (requiresNested.includes(tsType)) {
54
+ this.enableNested = true;
55
+ }
56
+ this.fields.push({
57
+ name: prop.key,
58
+ type: tsType,
59
+ optional: prop.kind === "optional",
60
+ facet: meta === null || meta === void 0 ? void 0 : meta.facet,
61
+ sort: meta === null || meta === void 0 ? void 0 : meta.sort,
62
+ index: meta === null || meta === void 0 ? void 0 : meta.index,
63
+ });
25
64
  }
26
65
  }
27
66
  buildObjectFilter(key, value) {
67
+ var _a, _b;
28
68
  if (Array.isArray(value)) {
29
69
  return `(${key}:[${value.join(",")}])`;
30
70
  }
31
- if ("not" in value) {
32
- return `${key}:!=${value.not}`;
71
+ const v = value;
72
+ if ("not" in v) {
73
+ return `${key}:!=${v.not}`;
33
74
  }
34
- const min = value.min != null ? value.min : undefined;
35
- const max = value.max != null ? value.max : undefined;
75
+ const min = (_a = v.min) !== null && _a !== void 0 ? _a : undefined;
76
+ const max = (_b = v.max) !== null && _b !== void 0 ? _b : undefined;
36
77
  if (min != null && max != null) {
37
78
  return `${key}:[${min}..${max}]`;
38
79
  }
@@ -43,173 +84,263 @@ export class TSense {
43
84
  return `${key}:>=${min}`;
44
85
  }
45
86
  }
46
- buildSort(data) {
47
- var _a;
48
- if (!data.order_by) {
49
- return;
50
- }
51
- const order = [];
52
- for (const key of this.maybeArray(data.order_by)) {
53
- const splitted = key.split(" ");
54
- let direction = splitted.at(-1);
55
- if (direction !== "asc" && direction !== "desc") {
56
- direction = (_a = data.direction) !== null && _a !== void 0 ? _a : "desc";
57
- }
58
- // safeguard
59
- if (splitted[0] === "undefined") {
60
- continue;
61
- }
62
- if (splitted[0] === "score") {
63
- splitted[0] = "_text_match";
64
- }
65
- order.push(`${splitted[0]}:${direction}`);
66
- }
67
- return order.join(",");
68
- }
69
- buildFilter(data) {
70
- var _a;
71
- const filter = [];
72
- for (const entry of Object.entries((_a = data.filter) !== null && _a !== void 0 ? _a : {})) {
87
+ buildFilter(filter) {
88
+ const result = [];
89
+ for (const entry of Object.entries(filter !== null && filter !== void 0 ? filter : {})) {
73
90
  const [key, value] = entry;
74
91
  if (value == null)
75
92
  continue;
76
93
  if (key === "OR") {
77
94
  const orFilter = [];
78
95
  for (const condition of value) {
79
- const filter = this.buildFilter({ filter: condition });
80
- orFilter.push(`(${filter.join("||")})`);
96
+ const inner = this.buildFilter(condition);
97
+ orFilter.push(`(${inner.join("||")})`);
81
98
  }
82
- filter.push(`(${orFilter.join("||")})`);
99
+ result.push(`(${orFilter.join("||")})`);
83
100
  continue;
84
101
  }
85
102
  switch (typeof value) {
86
103
  case "string":
87
104
  case "number":
88
105
  case "boolean":
89
- filter.push(`${key}:=${value}`);
106
+ result.push(`${key}:=${value}`);
90
107
  break;
91
108
  case "object": {
92
109
  const built = this.buildObjectFilter(key, value);
93
- if (built) {
94
- filter.push(built);
95
- }
110
+ if (built)
111
+ result.push(built);
96
112
  break;
97
113
  }
98
- default: {
114
+ default:
99
115
  break;
100
- }
101
116
  }
102
117
  }
103
- return filter;
118
+ return result;
104
119
  }
105
- maybeArray(d) {
106
- if (Array.isArray(d)) {
107
- return d;
120
+ buildSort(options) {
121
+ if (!options.sortBy)
122
+ return;
123
+ const result = [];
124
+ for (const item of options.sortBy) {
125
+ const [field, direction] = item.split(":");
126
+ if (field === "undefined")
127
+ continue;
128
+ const realField = field === "score" ? "_text_match" : field;
129
+ result.push(`${realField}:${direction}`);
108
130
  }
109
- return [d];
131
+ return result.join(",");
110
132
  }
111
- delete() {
133
+ create() {
112
134
  return __awaiter(this, void 0, void 0, function* () {
113
- yield this.data.client.collections(this.name).delete();
135
+ yield axios({
136
+ method: "POST",
137
+ baseURL: this.baseURL,
138
+ url: "/collections",
139
+ headers: this.headers,
140
+ data: {
141
+ name: this.options.name,
142
+ fields: this.fields,
143
+ default_sorting_field: this.options.defaultSortingField,
144
+ enable_nested_fields: this.enableNested,
145
+ },
146
+ });
147
+ return this;
114
148
  });
115
149
  }
116
- create() {
150
+ drop() {
117
151
  return __awaiter(this, void 0, void 0, function* () {
118
- const fields = [];
119
- for (const [name, field] of Object.entries(this.data.fields)) {
120
- const isSimpleField = typeof field === "string";
121
- if (isSimpleField) {
122
- const isOptional = field[field.length - 1] === "?";
123
- const realType = isOptional ? field.slice(0, -1) : field;
124
- fields.push({
125
- name,
126
- type: realType,
127
- optional: isOptional,
128
- });
129
- continue;
130
- }
131
- fields.push({
132
- name,
133
- type: field.type,
134
- optional: field.optional,
135
- facet: field.facet,
136
- index: field.index,
137
- sort: field.sort,
138
- });
152
+ yield axios({
153
+ method: "DELETE",
154
+ baseURL: this.baseURL,
155
+ url: `/collections/${this.options.name}`,
156
+ headers: this.headers,
157
+ });
158
+ });
159
+ }
160
+ get(id) {
161
+ return __awaiter(this, void 0, void 0, function* () {
162
+ const { data } = yield axios({
163
+ method: "GET",
164
+ baseURL: this.baseURL,
165
+ url: `/collections/${this.options.name}/documents/${id}`,
166
+ headers: this.headers,
167
+ }).catch((e) => {
168
+ if (e.status === 404)
169
+ return { data: null };
170
+ throw e;
171
+ });
172
+ return data;
173
+ });
174
+ }
175
+ delete(id) {
176
+ return __awaiter(this, void 0, void 0, function* () {
177
+ const { data } = yield axios({
178
+ method: "DELETE",
179
+ baseURL: this.baseURL,
180
+ url: `/collections/${this.options.name}/documents/${id}`,
181
+ headers: this.headers,
182
+ }).catch((e) => {
183
+ if (e.status === 404)
184
+ return { data: null };
185
+ throw e;
186
+ });
187
+ return data != null;
188
+ });
189
+ }
190
+ deleteMany(filter) {
191
+ return __awaiter(this, void 0, void 0, function* () {
192
+ const filterBy = this.buildFilter(filter).join("&&");
193
+ if (!filterBy) {
194
+ throw new Error("FILTER_REQUIRED");
139
195
  }
140
- this.checkNested(fields);
141
- yield this.data.client.collections().create({
142
- name: this.name,
143
- fields,
144
- default_sorting_field: this.data.default_sorting_field,
145
- enable_nested_fields: this.data.enable_nested_fields,
196
+ const { data } = yield axios({
197
+ method: "DELETE",
198
+ baseURL: this.baseURL,
199
+ url: `/collections/${this.options.name}/documents`,
200
+ headers: this.headers,
201
+ params: { filter_by: filterBy },
146
202
  });
147
- return this;
203
+ return { deleted: data.num_deleted };
148
204
  });
149
205
  }
150
- searchDocuments(data, facet) {
206
+ update(id, data) {
151
207
  return __awaiter(this, void 0, void 0, function* () {
152
- var _a, _b, _c, _d, _e;
153
- const res = yield this.data.client
154
- .collections(this.name)
155
- .documents()
156
- .search({
157
- q: (_a = data.search) !== null && _a !== void 0 ? _a : "",
158
- query_by: (_b = data.search_keys) !== null && _b !== void 0 ? _b : [this.data.default_search_field],
159
- sort_by: this.buildSort(data),
160
- filter_by: this.buildFilter(data).join("&&"),
161
- page: data.page,
162
- limit: data.limit,
163
- facet_by: facet === null || facet === void 0 ? void 0 : facet.facet_by,
208
+ const { data: updated } = yield axios({
209
+ method: "PATCH",
210
+ baseURL: this.baseURL,
211
+ url: `/collections/${this.options.name}/documents/${id}`,
212
+ headers: this.headers,
213
+ data,
164
214
  });
165
- const facet_result = (facet === null || facet === void 0 ? void 0 : facet.facet_by)
166
- ? facet.enable_facet_total
167
- ? { total: 0 }
168
- : {}
169
- : undefined;
170
- if ((_d = (_c = res.facet_counts) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.counts) {
171
- for (const iter of res.facet_counts[0].counts) {
172
- facet_result[iter.value] = iter.count;
173
- if (facet === null || facet === void 0 ? void 0 : facet.enable_facet_total)
174
- facet_result.total += iter.count;
215
+ return updated;
216
+ });
217
+ }
218
+ updateMany(filter, data) {
219
+ return __awaiter(this, void 0, void 0, function* () {
220
+ const filterBy = this.buildFilter(filter).join("&&");
221
+ if (!filterBy) {
222
+ throw new Error("FILTER_REQUIRED");
223
+ }
224
+ const { data: result } = yield axios({
225
+ method: "PATCH",
226
+ baseURL: this.baseURL,
227
+ url: `/collections/${this.options.name}/documents`,
228
+ headers: this.headers,
229
+ params: { filter_by: filterBy },
230
+ data,
231
+ });
232
+ return { updated: result.num_updated };
233
+ });
234
+ }
235
+ search(options) {
236
+ return __awaiter(this, void 0, void 0, function* () {
237
+ var _a, _b, _c, _d, _e, _f, _g;
238
+ const params = {
239
+ q: (_a = options.query) !== null && _a !== void 0 ? _a : "",
240
+ query_by: ((_b = options.queryBy) !== null && _b !== void 0 ? _b : [
241
+ this.options.defaultSearchField,
242
+ ]).join(","),
243
+ };
244
+ const sortBy = this.buildSort(options);
245
+ if (sortBy)
246
+ params.sort_by = sortBy;
247
+ const filterBy = this.buildFilter(options.filter).join("&&");
248
+ if (filterBy)
249
+ params.filter_by = filterBy;
250
+ if (options.page != null)
251
+ params.page = options.page;
252
+ if (options.limit != null)
253
+ params.per_page = options.limit;
254
+ const facetBy = (_c = options.facetBy) === null || _c === void 0 ? void 0 : _c.join(",");
255
+ if (facetBy)
256
+ params.facet_by = facetBy;
257
+ if ("pick" in options && options.pick) {
258
+ params.include_fields = options.pick.join(",");
259
+ }
260
+ if ("omit" in options && options.omit) {
261
+ params.exclude_fields = options.omit.join(",");
262
+ }
263
+ const highlight = options.highlight;
264
+ const highlightEnabled = !!highlight;
265
+ let highlightOpts;
266
+ if (typeof highlight === "object") {
267
+ highlightOpts = highlight;
268
+ if (highlightOpts.fields) {
269
+ params.highlight_fields = highlightOpts.fields.join(",");
270
+ }
271
+ if (highlightOpts.startTag) {
272
+ params.highlight_start_tag = highlightOpts.startTag;
273
+ }
274
+ if (highlightOpts.endTag) {
275
+ params.highlight_end_tag = highlightOpts.endTag;
175
276
  }
176
277
  }
177
- const result = [];
178
- for (const hit of (_e = res.hits) !== null && _e !== void 0 ? _e : []) {
179
- if (data.highlight) {
180
- for (const [key, value] of Object.entries(hit.highlight)) {
181
- if (value) {
182
- // @ts-expect-error
183
- hit.document[key] = value.snippet;
184
- }
278
+ const { data: res } = yield axios({
279
+ method: "GET",
280
+ baseURL: this.baseURL,
281
+ url: `/collections/${this.options.name}/documents/search`,
282
+ headers: this.headers,
283
+ params,
284
+ });
285
+ const data = [];
286
+ const scores = [];
287
+ for (const hit of (_d = res.hits) !== null && _d !== void 0 ? _d : []) {
288
+ if (highlightEnabled) {
289
+ const fieldsToHighlight = highlightOpts === null || highlightOpts === void 0 ? void 0 : highlightOpts.fields;
290
+ for (const [key, value] of Object.entries((_e = hit.highlight) !== null && _e !== void 0 ? _e : {})) {
291
+ if (!(value === null || value === void 0 ? void 0 : value.snippet))
292
+ continue;
293
+ if (fieldsToHighlight && !fieldsToHighlight.includes(key))
294
+ continue;
295
+ hit.document[key] = value.snippet;
185
296
  }
186
297
  }
187
- result.push(hit.document);
298
+ data.push(hit.document);
299
+ scores.push((_f = hit.text_match) !== null && _f !== void 0 ? _f : 0);
300
+ }
301
+ const facets = {};
302
+ for (const facetCount of (_g = res.facet_counts) !== null && _g !== void 0 ? _g : []) {
303
+ const fieldName = facetCount.field_name;
304
+ facets[fieldName] = {};
305
+ for (const item of facetCount.counts) {
306
+ facets[fieldName][item.value] = item.count;
307
+ }
188
308
  }
189
309
  return {
190
- data: result,
191
310
  count: res.found,
192
- facet: facet_result,
311
+ data,
312
+ facets,
313
+ scores,
193
314
  };
194
315
  });
195
316
  }
196
- upsertDocuments(items) {
317
+ upsert(docs) {
197
318
  return __awaiter(this, void 0, void 0, function* () {
198
- const parsed = [];
199
- for (const item of this.maybeArray(items)) {
200
- parsed.push(JSON.stringify(item));
319
+ const items = Array.isArray(docs) ? docs : [docs];
320
+ if (!items.length)
321
+ return [];
322
+ if (this.options.validateOnUpsert) {
323
+ for (const item of items) {
324
+ this.options.schema.assert(item);
325
+ }
201
326
  }
202
- if (!parsed.length) {
203
- return;
327
+ const payload = items.map((item) => JSON.stringify(item)).join("\n");
328
+ const params = { action: "upsert" };
329
+ if (this.options.batchSize) {
330
+ params.batch_size = this.options.batchSize;
204
331
  }
205
- const res = yield this.data.client
206
- .collections(this.name)
207
- .documents()
208
- .import(parsed.join("\n"), {
209
- action: "upsert",
210
- batch_size: this.data.batch_size,
332
+ const { data } = yield axios({
333
+ method: "POST",
334
+ baseURL: this.baseURL,
335
+ url: `/collections/${this.options.name}/documents/import`,
336
+ headers: Object.assign(Object.assign({}, this.headers), { "Content-Type": "text/plain" }),
337
+ params,
338
+ data: payload,
211
339
  });
212
- return res.split("\n").map((v) => JSON.parse(v));
340
+ if (typeof data === "string") {
341
+ return data.split("\n").map((v) => JSON.parse(v));
342
+ }
343
+ return [data];
213
344
  });
214
345
  }
215
346
  }
@@ -0,0 +1,99 @@
1
+ import type { Type } from "arktype";
2
+ type BaseIfArray<T> = T extends (infer Q)[] ? Q : T;
3
+ export type FieldSchema = {
4
+ name: string;
5
+ type: string;
6
+ facet?: boolean;
7
+ sort?: boolean;
8
+ index?: boolean;
9
+ optional?: boolean;
10
+ };
11
+ export type SearchHit<T> = {
12
+ document: T;
13
+ highlight?: Record<string, {
14
+ snippet?: string;
15
+ }>;
16
+ text_match?: number;
17
+ };
18
+ export type SearchApiResponse<T> = {
19
+ found: number;
20
+ hits?: SearchHit<T>[];
21
+ facet_counts?: {
22
+ field_name: string;
23
+ counts: {
24
+ value: string;
25
+ count: number;
26
+ }[];
27
+ }[];
28
+ };
29
+ export type ConnectionConfig = {
30
+ host: string;
31
+ port: number;
32
+ protocol: "http" | "https";
33
+ apiKey: string;
34
+ timeout?: number;
35
+ };
36
+ export type TsenseOptions<T extends Type> = {
37
+ name: string;
38
+ schema: T;
39
+ connection: ConnectionConfig;
40
+ defaultSearchField?: keyof T["infer"];
41
+ defaultSortingField?: keyof T["infer"];
42
+ batchSize?: number;
43
+ validateOnUpsert?: boolean;
44
+ };
45
+ type SingleFilter<T> = Partial<{
46
+ [K in keyof T]: BaseIfArray<T[K]> | NonNullable<BaseIfArray<T[K]>>[] | {
47
+ not?: BaseIfArray<T[K]>;
48
+ } | (NonNullable<T[K]> extends number ? {
49
+ min?: number;
50
+ max?: number;
51
+ } : never);
52
+ }>;
53
+ export type FilterFor<T> = SingleFilter<T> & {
54
+ OR?: FilterFor<T>[];
55
+ };
56
+ export type HighlightOptions<T> = {
57
+ fields?: (keyof T)[];
58
+ startTag?: string;
59
+ endTag?: string;
60
+ };
61
+ type SortableField<T> = Extract<keyof T, string> | "score";
62
+ type BaseSearchOptions<T> = {
63
+ query?: string;
64
+ queryBy?: (keyof T)[];
65
+ filter?: FilterFor<T>;
66
+ sortBy?: `${SortableField<T>}:${"asc" | "desc"}`[];
67
+ facetBy?: (keyof T)[];
68
+ page?: number;
69
+ limit?: number;
70
+ highlight?: boolean | HighlightOptions<T>;
71
+ };
72
+ export type SearchOptions<T> = BaseSearchOptions<T> & ({
73
+ pick?: (keyof T)[];
74
+ omit?: never;
75
+ } | {
76
+ omit?: (keyof T)[];
77
+ pick?: never;
78
+ } | {
79
+ pick?: never;
80
+ omit?: never;
81
+ });
82
+ export type SearchResult<T> = {
83
+ count: number;
84
+ data: T[];
85
+ facets: Record<string, Record<string, number>>;
86
+ scores: number[];
87
+ };
88
+ export type DeleteResult = {
89
+ deleted: number;
90
+ };
91
+ export type UpdateResult = {
92
+ updated: number;
93
+ };
94
+ export type UpsertResult = {
95
+ success: boolean;
96
+ error?: string;
97
+ document?: unknown;
98
+ };
99
+ export {};
package/package.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "name": "tsense",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
+ "private": false,
5
+ "description": "Opinionated, fully typed typesense client",
6
+ "keywords": [
7
+ "typesense"
8
+ ],
9
+ "homepage": "https://github.com/lobomfz/tsense",
10
+ "license": "MIT",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": false,
4
16
  "main": "dist/index.js",
5
17
  "types": "dist/index.d.ts",
6
18
  "exports": {
@@ -9,33 +21,9 @@
9
21
  "import": "./dist/index.js"
10
22
  }
11
23
  },
12
- "files": [
13
- "dist"
14
- ],
15
- "sideEffects": false,
16
- "description": "Opinionated, fully typed typesense client",
17
- "license": "MIT",
18
- "keywords": [
19
- "typesense"
20
- ],
21
24
  "publishConfig": {
22
25
  "access": "public"
23
26
  },
24
- "homepage": "https://github.com/lobomfz/tsense",
25
- "type": "module",
26
- "private": false,
27
- "devDependencies": {
28
- "@arethetypeswrong/cli": "^0.18.2",
29
- "@biomejs/biome": "2.1.4",
30
- "@changesets/cli": "^2.29.7",
31
- "@types/bun": "latest"
32
- },
33
- "peerDependencies": {
34
- "typescript": "^5"
35
- },
36
- "dependencies": {
37
- "typesense": "^2.1.0"
38
- },
39
27
  "scripts": {
40
28
  "build": "tsc",
41
29
  "ci": "bun run build && bun run format && bun run check-exports",
@@ -44,5 +32,19 @@
44
32
  "check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
45
33
  "local-release": "changeset version && changeset publish",
46
34
  "prepublishOnly": "bun run ci"
35
+ },
36
+ "dependencies": {
37
+ "redaxios": "^0.5.1"
38
+ },
39
+ "devDependencies": {
40
+ "@arethetypeswrong/cli": "^0.18.2",
41
+ "@biomejs/biome": "2.1.4",
42
+ "@changesets/cli": "^2.29.7",
43
+ "@types/bun": "latest",
44
+ "arktype": "^2.1.29"
45
+ },
46
+ "peerDependencies": {
47
+ "arktype": "^2.1.29",
48
+ "typescript": "^5"
47
49
  }
48
- }
50
+ }
@@ -1,27 +0,0 @@
1
- import type { Client } from "typesense";
2
- import type { CustomCollectionField, Filters, InferCollectionTypes } from "./types/core.js";
3
- export declare class TSense<Options extends CustomCollectionField, Fields extends Record<string, Options>, Inferred = InferCollectionTypes<Fields>> {
4
- private name;
5
- private data;
6
- infer: Inferred;
7
- constructor(name: string, data: {
8
- fields: Fields;
9
- client: Client;
10
- default_search_field?: NoInfer<keyof Inferred>;
11
- default_sorting_field?: NoInfer<keyof Inferred>;
12
- batch_size?: number;
13
- enable_nested_fields?: boolean;
14
- });
15
- private checkNested;
16
- private buildObjectFilter;
17
- private buildSort;
18
- private buildFilter;
19
- private maybeArray;
20
- delete(): Promise<void>;
21
- create(): Promise<this>;
22
- searchDocuments(data: Filters<Inferred>): Promise<{
23
- count: number;
24
- data: Inferred[];
25
- }>;
26
- upsertDocuments(items: Inferred | Inferred[]): Promise<any[] | undefined>;
27
- }
@@ -1,200 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- const requiresNested = ["object", "object[]"];
11
- export class TSense {
12
- constructor(name, data) {
13
- this.name = name;
14
- this.data = data;
15
- this.infer = undefined;
16
- }
17
- checkNested(fields) {
18
- if (this.data.enable_nested_fields) {
19
- return;
20
- }
21
- for (const field of fields) {
22
- requiresNested.includes(field.type);
23
- this.data.enable_nested_fields = true;
24
- return;
25
- }
26
- }
27
- buildObjectFilter(key, value) {
28
- if (Array.isArray(value)) {
29
- return `(${key}:[${value.join(",")}])`;
30
- }
31
- if ("not" in value) {
32
- return `${key}:!=${value.not}`;
33
- }
34
- const min = value.min != null ? value.min : undefined;
35
- const max = value.max != null ? value.max : undefined;
36
- if (min != null && max != null) {
37
- return `${key}:[${min}..${max}]`;
38
- }
39
- if (max != null) {
40
- return `${key}:<=${max}`;
41
- }
42
- if (min != null) {
43
- return `${key}:>=${min}`;
44
- }
45
- }
46
- buildSort(data) {
47
- var _a;
48
- if (!data.order_by) {
49
- return;
50
- }
51
- const order = [];
52
- for (const key of this.maybeArray(data.order_by)) {
53
- const splitted = key.split(" ");
54
- let direction = splitted.at(-1);
55
- if (direction !== "asc" && direction !== "desc") {
56
- direction = (_a = data.direction) !== null && _a !== void 0 ? _a : "desc";
57
- }
58
- // safeguard
59
- if (splitted[0] === "undefined") {
60
- continue;
61
- }
62
- if (splitted[0] === "score") {
63
- splitted[0] = "_text_match";
64
- }
65
- order.push(`${splitted[0]}:${direction}`);
66
- }
67
- return order.join(",");
68
- }
69
- buildFilter(data) {
70
- var _a;
71
- const filter = [];
72
- for (const entry of Object.entries((_a = data.filter) !== null && _a !== void 0 ? _a : {})) {
73
- const [key, value] = entry;
74
- if (value == null)
75
- continue;
76
- if (key === "OR") {
77
- const orFilter = [];
78
- for (const condition of value) {
79
- const filter = this.buildFilter({ filter: condition });
80
- orFilter.push(`(${filter.join("||")})`);
81
- }
82
- filter.push(`(${orFilter.join("||")})`);
83
- continue;
84
- }
85
- switch (typeof value) {
86
- case "string":
87
- case "number":
88
- case "boolean":
89
- filter.push(`${key}:=${value}`);
90
- break;
91
- case "object": {
92
- const built = this.buildObjectFilter(key, value);
93
- if (built) {
94
- filter.push(built);
95
- }
96
- break;
97
- }
98
- default: {
99
- break;
100
- }
101
- }
102
- }
103
- return filter;
104
- }
105
- maybeArray(d) {
106
- if (Array.isArray(d)) {
107
- return d;
108
- }
109
- return [d];
110
- }
111
- delete() {
112
- return __awaiter(this, void 0, void 0, function* () {
113
- yield this.data.client.collections(this.name).delete();
114
- });
115
- }
116
- create() {
117
- return __awaiter(this, void 0, void 0, function* () {
118
- const fields = [
119
- {
120
- name: "id",
121
- type: "int32",
122
- },
123
- ];
124
- for (const [name, field] of Object.entries(this.data.fields)) {
125
- const isSimpleField = typeof field === "string";
126
- if (isSimpleField) {
127
- const isOptional = field[field.length - 1] === "?";
128
- const realType = isOptional ? field.slice(0, -1) : field;
129
- fields.push({
130
- name,
131
- type: realType,
132
- optional: isOptional,
133
- });
134
- continue;
135
- }
136
- fields.push({
137
- name,
138
- type: field.type,
139
- optional: field.optional,
140
- facet: field.facet,
141
- index: field.index,
142
- sort: field.sort,
143
- });
144
- }
145
- this.checkNested(fields);
146
- yield this.data.client.collections().create({
147
- name: this.name,
148
- fields,
149
- default_sorting_field: this.data.default_sorting_field,
150
- enable_nested_fields: this.data.enable_nested_fields,
151
- });
152
- return this;
153
- });
154
- }
155
- searchDocuments(data) {
156
- return __awaiter(this, void 0, void 0, function* () {
157
- var _a, _b, _c;
158
- const res = yield this.data.client
159
- .collections(this.name)
160
- .documents()
161
- .search({
162
- q: (_a = data.search) !== null && _a !== void 0 ? _a : "",
163
- query_by: (_b = data.search_keys) !== null && _b !== void 0 ? _b : [this.data.default_search_field],
164
- sort_by: this.buildSort(data),
165
- filter_by: this.buildFilter(data).join("&&"),
166
- page: data.page,
167
- limit: data.limit,
168
- });
169
- const result = [];
170
- if ((_c = res.hits) === null || _c === void 0 ? void 0 : _c.length) {
171
- for (const hit of res.hits) {
172
- result.push(hit.document);
173
- }
174
- }
175
- return {
176
- data: result,
177
- count: res.found,
178
- };
179
- });
180
- }
181
- upsertDocuments(items) {
182
- return __awaiter(this, void 0, void 0, function* () {
183
- const parsed = [];
184
- for (const item of this.maybeArray(items)) {
185
- parsed.push(JSON.stringify(item));
186
- }
187
- if (!parsed.length) {
188
- return;
189
- }
190
- const res = yield this.data.client
191
- .collections(this.name)
192
- .documents()
193
- .import(parsed.join("\n"), {
194
- action: "upsert",
195
- batch_size: this.data.batch_size,
196
- });
197
- return res.split("\n").map((v) => JSON.parse(v));
198
- });
199
- }
200
- }
@@ -1,66 +0,0 @@
1
- import type { FieldType } from "typesense/lib/Typesense/Collection.js";
2
- import type { BaseIfArray, Simplify, UndefinedToOptional } from "./helpers.js";
3
- export type CustomCollectionField = FieldType | `${FieldType}?` | {
4
- type: FieldType;
5
- optional?: boolean;
6
- facet?: boolean;
7
- index?: boolean;
8
- override?: any;
9
- sort?: boolean;
10
- };
11
- type TypesenseToTS = {
12
- string: string;
13
- int32: number;
14
- int64: number;
15
- float: number;
16
- bool: boolean;
17
- image: string;
18
- "string[]": string[];
19
- "int32[]": number[];
20
- "int64[]": number[];
21
- "float[]": number[];
22
- "bool[]": boolean[];
23
- geopoint: unknown;
24
- "geopoint[]": unknown[];
25
- object: unknown;
26
- "object[]": unknown[];
27
- auto: unknown;
28
- "string*": unknown;
29
- };
30
- type ValidFieldType = keyof TypesenseToTS;
31
- export type InferCollectionTypes<Fields> = Simplify<{
32
- id?: string;
33
- } & UndefinedToOptional<{
34
- [K in keyof Fields]: Fields[K] extends ValidFieldType ? TypesenseToTS[Fields[K]] : Fields[K] extends {
35
- override: infer Override;
36
- } ? Fields[K] extends {
37
- optional: true;
38
- } ? Override | null | undefined : Override : Fields[K] extends {
39
- type: infer T;
40
- } ? T extends ValidFieldType ? Fields[K] extends {
41
- optional: true;
42
- } ? TypesenseToTS[T] | null | undefined : TypesenseToTS[T] : never : Fields[K] extends `${infer RealType}?` ? RealType extends ValidFieldType ? TypesenseToTS[RealType] | null | undefined : never : never;
43
- }>>;
44
- type OrderBy<Inferred, Options = keyof Inferred | "score"> = Options | Options[] | (Options extends infer Q ? Q extends string ? `${Q} ${"asc" | "desc"}` : never : never)[];
45
- type SingleFilter<Inferred> = Partial<{
46
- [K in keyof Inferred]: BaseIfArray<Inferred[K]> | NonNullable<BaseIfArray<Inferred[K]>>[] | {
47
- not?: string | string[] | boolean | null;
48
- } | (NonNullable<Inferred[K]> extends number ? {
49
- min?: number;
50
- max?: number;
51
- } : never);
52
- }>;
53
- type RecursiveFilter<Inferred> = SingleFilter<Inferred> & {
54
- OR?: RecursiveFilter<Inferred>[];
55
- };
56
- export type Filters<Inferred> = {
57
- search?: string;
58
- filter?: RecursiveFilter<Inferred>;
59
- order_by?: OrderBy<Inferred> | undefined;
60
- direction?: "asc" | "desc";
61
- page?: number;
62
- limit?: number;
63
- search_keys?: (keyof Inferred)[];
64
- highlight?: boolean;
65
- };
66
- export {};
@@ -1,11 +0,0 @@
1
- export type UndefinedToOptional<T> = {
2
- [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
3
- } & {
4
- [K in keyof T as undefined extends T[K] ? never : K]: T[K];
5
- };
6
- export type BaseIfArray<T> = T extends (infer Q)[] ? Q : T;
7
- type DrainOuterGeneric<T> = [T] extends [unknown] ? T : never;
8
- export type Simplify<T> = DrainOuterGeneric<{
9
- [K in keyof T]: T[K];
10
- } & {}>;
11
- export {};
File without changes
File without changes