tsense 0.0.1
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 +92 -0
- package/dist/collection.d.ts +27 -0
- package/dist/collection.js +195 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types/core.d.ts +66 -0
- package/dist/types/core.js +1 -0
- package/dist/types/helpers.d.ts +11 -0
- package/dist/types/helpers.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
## TSense (WIP)
|
|
2
|
+
|
|
3
|
+
Opinionated, fully-typed typesense client
|
|
4
|
+
|
|
5
|
+
# TO-DO
|
|
6
|
+
- [ ] Remove base typesense dependency
|
|
7
|
+
- [ ] Better filter support
|
|
8
|
+
- [ ] Documentation
|
|
9
|
+
- [ ] Improve tests
|
|
10
|
+
- [ ] Facet
|
|
11
|
+
|
|
12
|
+
# Example
|
|
13
|
+
```typescript
|
|
14
|
+
import { Client } from "typesense";
|
|
15
|
+
import { TSense } from "tsense";
|
|
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,
|
|
27
|
+
});
|
|
28
|
+
|
|
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
|
+
work_history: {
|
|
42
|
+
// object and object[] auto-infers enable_nested_fields
|
|
43
|
+
type: "object[]",
|
|
44
|
+
index: false,
|
|
45
|
+
optional: true,
|
|
46
|
+
override: {} as {
|
|
47
|
+
company: string;
|
|
48
|
+
date: string;
|
|
49
|
+
}[],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
default_search_field: "name",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// infer the collection type (undefined at runtime)
|
|
56
|
+
typeof UsersCollection.infer;
|
|
57
|
+
/*
|
|
58
|
+
{
|
|
59
|
+
id?: string | undefined;
|
|
60
|
+
phone?: string | null | undefined;
|
|
61
|
+
work_history?: {
|
|
62
|
+
company: string;
|
|
63
|
+
date: string;
|
|
64
|
+
}[] | null | undefined;
|
|
65
|
+
email: string;
|
|
66
|
+
age: number;
|
|
67
|
+
name: string;
|
|
68
|
+
}
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
const results = await UsersCollection.searchDocuments({
|
|
72
|
+
search: "john",
|
|
73
|
+
search_keys: ["name"],
|
|
74
|
+
// can sort multiple fields
|
|
75
|
+
order_by: ["age desc", "id asc"],
|
|
76
|
+
filter: {
|
|
77
|
+
// min and max range on numbers
|
|
78
|
+
age: {
|
|
79
|
+
min: 20,
|
|
80
|
+
},
|
|
81
|
+
// OR syntax similar to prisma
|
|
82
|
+
OR: [
|
|
83
|
+
{
|
|
84
|
+
email: "@google.com",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
email: "@netflix.com",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
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
|
+
});
|
|
139
|
+
}
|
|
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,
|
|
146
|
+
});
|
|
147
|
+
return this;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
searchDocuments(data) {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
var _a, _b, _c;
|
|
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
|
+
});
|
|
164
|
+
const result = [];
|
|
165
|
+
if ((_c = res.hits) === null || _c === void 0 ? void 0 : _c.length) {
|
|
166
|
+
for (const hit of res.hits) {
|
|
167
|
+
result.push(hit.document);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
data: result,
|
|
172
|
+
count: res.found,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
upsertDocuments(items) {
|
|
177
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
178
|
+
const parsed = [];
|
|
179
|
+
for (const item of this.maybeArray(items)) {
|
|
180
|
+
parsed.push(JSON.stringify(item));
|
|
181
|
+
}
|
|
182
|
+
if (!parsed.length) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const res = yield this.data.client
|
|
186
|
+
.collections(this.name)
|
|
187
|
+
.documents()
|
|
188
|
+
.import(parsed.join("\n"), {
|
|
189
|
+
action: "upsert",
|
|
190
|
+
batch_size: this.data.batch_size,
|
|
191
|
+
});
|
|
192
|
+
return res.split("\n").map((v) => JSON.parse(v));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TSense } from "./collection.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TSense } from "./collection.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
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 {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
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 {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tsense",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"description": "Opinionated, fully typed typesense client, inspired by prisma syntax",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"typesense"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/lobomfz/tsense",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"private": false,
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
21
|
+
"@biomejs/biome": "2.1.4",
|
|
22
|
+
"@changesets/cli": "^2.29.7",
|
|
23
|
+
"@types/bun": "latest"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"typescript": "^5"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"typesense": "^2.1.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"ci": "bun run build && bun run format && bun run check-exports",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"format": "biome check --write",
|
|
36
|
+
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
|
|
37
|
+
"local-release": "changeset version && changeset publish",
|
|
38
|
+
"prepublishOnly": "bun run ci"
|
|
39
|
+
}
|
|
40
|
+
}
|