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 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
+ }
@@ -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
+ }