@tsproxy/api 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/dist/chunk-VL6QOQ2T.js +1208 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +152 -0
- package/dist/index.d.ts +378 -0
- package/dist/index.js +38 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +19 -0
- package/package.json +59 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createApp,
|
|
4
|
+
proxyConfigToConfig
|
|
5
|
+
} from "./chunk-VL6QOQ2T.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
import { serve } from "@hono/node-server";
|
|
12
|
+
var args = process.argv.slice(2);
|
|
13
|
+
var command = args[0] || "dev";
|
|
14
|
+
if (command === "--help" || command === "-h") {
|
|
15
|
+
console.log(`
|
|
16
|
+
tsproxy - Typesense Proxy Server
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
tsproxy dev Start in development mode (with hot reload hints)
|
|
20
|
+
tsproxy start Start in production mode
|
|
21
|
+
tsproxy validate Validate the config file
|
|
22
|
+
tsproxy --help Show this help
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--config, -c <path> Path to config file (default: tsproxy.config.ts)
|
|
26
|
+
--port, -p <port> Override the port
|
|
27
|
+
|
|
28
|
+
Config File:
|
|
29
|
+
Create a tsproxy.config.ts in your project root:
|
|
30
|
+
|
|
31
|
+
import { defineConfig } from "@tsproxy/api";
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
typesense: {
|
|
35
|
+
host: "localhost",
|
|
36
|
+
port: 8108,
|
|
37
|
+
apiKey: "your-api-key",
|
|
38
|
+
},
|
|
39
|
+
collections: {
|
|
40
|
+
products: {
|
|
41
|
+
fields: {
|
|
42
|
+
title: { type: "string", searchable: true },
|
|
43
|
+
price: { type: "float", sortable: true },
|
|
44
|
+
category: { type: "string", facet: true },
|
|
45
|
+
},
|
|
46
|
+
locales: ["en", "fr"],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
if (command === "--version" || command === "-V") {
|
|
54
|
+
console.log("0.1.0");
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
var configPath;
|
|
58
|
+
var portOverride;
|
|
59
|
+
for (let i = 1; i < args.length; i++) {
|
|
60
|
+
const arg = args[i];
|
|
61
|
+
if ((arg === "--config" || arg === "-c") && args[i + 1]) {
|
|
62
|
+
configPath = args[++i];
|
|
63
|
+
} else if ((arg === "--port" || arg === "-p") && args[i + 1]) {
|
|
64
|
+
portOverride = parseInt(args[++i], 10);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function loadProxyConfig() {
|
|
68
|
+
if (configPath) {
|
|
69
|
+
const filePath = resolve(configPath);
|
|
70
|
+
if (existsSync(filePath)) {
|
|
71
|
+
return importConfig(filePath);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const configNames = ["tsproxy.config.ts", "tsproxy.config.js", "tsproxy.config.mjs"];
|
|
76
|
+
let dir = resolve(process.cwd());
|
|
77
|
+
const root = resolve("/");
|
|
78
|
+
while (dir !== root) {
|
|
79
|
+
for (const name of configNames) {
|
|
80
|
+
const filePath = resolve(dir, name);
|
|
81
|
+
if (existsSync(filePath)) {
|
|
82
|
+
return importConfig(filePath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
dir = resolve(dir, "..");
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
async function importConfig(filePath) {
|
|
90
|
+
try {
|
|
91
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
92
|
+
const mod = await import(`${fileUrl}?t=${Date.now()}`);
|
|
93
|
+
const config = mod.default || mod;
|
|
94
|
+
console.log(`Loaded config from ${filePath}`);
|
|
95
|
+
return config;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`Error loading config from ${filePath}:`, error);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function main() {
|
|
102
|
+
const proxyConfig = await loadProxyConfig();
|
|
103
|
+
if (command === "validate") {
|
|
104
|
+
if (!proxyConfig) {
|
|
105
|
+
console.error("No config file found. Create tsproxy.config.ts");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log("Config is valid!");
|
|
109
|
+
console.log(`Collections: ${Object.keys(proxyConfig.collections || {}).join(", ") || "(none)"}`);
|
|
110
|
+
if (proxyConfig.typesense) {
|
|
111
|
+
console.log(`Typesense: ${proxyConfig.typesense.protocol || "http"}://${proxyConfig.typesense.host}:${proxyConfig.typesense.port}`);
|
|
112
|
+
}
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
const config = proxyConfig ? proxyConfigToConfig(proxyConfig) : void 0;
|
|
116
|
+
if (portOverride && config) {
|
|
117
|
+
config.proxy.port = portOverride;
|
|
118
|
+
}
|
|
119
|
+
const { app, config: finalConfig } = createApp(config, proxyConfig?.collections);
|
|
120
|
+
const port = portOverride || finalConfig.proxy.port;
|
|
121
|
+
const isDev = command === "dev";
|
|
122
|
+
if (isDev) {
|
|
123
|
+
console.log("\n tsproxy dev server\n");
|
|
124
|
+
} else {
|
|
125
|
+
console.log("\n tsproxy production server\n");
|
|
126
|
+
}
|
|
127
|
+
console.log(` Typesense: ${finalConfig.typesense.protocol}://${finalConfig.typesense.host}:${finalConfig.typesense.port}`);
|
|
128
|
+
console.log(` Cache: TTL=${finalConfig.cache.ttl}s, max=${finalConfig.cache.maxSize}`);
|
|
129
|
+
console.log(` Queue: concurrency=${finalConfig.queue.concurrency}, max=${finalConfig.queue.maxSize}`);
|
|
130
|
+
console.log(` Rate limit: search=${finalConfig.rateLimit.search}/min, ingest=${finalConfig.rateLimit.ingest}/min`);
|
|
131
|
+
const collectionNames = Object.keys(finalConfig.collections.collections);
|
|
132
|
+
if (collectionNames.length > 0) {
|
|
133
|
+
console.log(` Collections: ${collectionNames.join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
serve(
|
|
137
|
+
{
|
|
138
|
+
fetch: app.fetch,
|
|
139
|
+
port
|
|
140
|
+
},
|
|
141
|
+
(info) => {
|
|
142
|
+
console.log(` Listening on http://localhost:${info.port}`);
|
|
143
|
+
console.log(` API docs: http://localhost:${info.port}/api/docs`);
|
|
144
|
+
console.log(` Health: http://localhost:${info.port}/api/health`);
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
main().catch((error) => {
|
|
150
|
+
console.error("Failed to start:", error);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import * as hono_types from 'hono/types';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
|
|
4
|
+
interface LocaleMap {
|
|
5
|
+
[locale: string]: string;
|
|
6
|
+
}
|
|
7
|
+
interface CollectionConfig {
|
|
8
|
+
locales?: LocaleMap;
|
|
9
|
+
}
|
|
10
|
+
interface CollectionsConfig {
|
|
11
|
+
collections: Record<string, CollectionConfig>;
|
|
12
|
+
}
|
|
13
|
+
interface Config {
|
|
14
|
+
typesense: {
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
protocol: string;
|
|
18
|
+
apiKey: string;
|
|
19
|
+
};
|
|
20
|
+
proxy: {
|
|
21
|
+
port: number;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
};
|
|
24
|
+
cache: {
|
|
25
|
+
ttl: number;
|
|
26
|
+
maxSize: number;
|
|
27
|
+
};
|
|
28
|
+
queue: {
|
|
29
|
+
concurrency: number;
|
|
30
|
+
maxSize: number;
|
|
31
|
+
redis?: {
|
|
32
|
+
host: string;
|
|
33
|
+
port: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
rateLimit: {
|
|
37
|
+
search: number;
|
|
38
|
+
ingest: number;
|
|
39
|
+
};
|
|
40
|
+
collections: CollectionsConfig;
|
|
41
|
+
}
|
|
42
|
+
declare function loadConfig(): Config;
|
|
43
|
+
declare function resolveCollection(config: CollectionsConfig, collection: string, locale?: string): string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Field type definition for a collection field
|
|
47
|
+
*/
|
|
48
|
+
interface FieldConfig {
|
|
49
|
+
type: "string" | "string[]" | "int32" | "int32[]" | "int64" | "int64[]" | "float" | "float[]" | "bool" | "bool[]" | "geopoint" | "geopoint[]" | "object" | "object[]" | "auto";
|
|
50
|
+
/** Make this field searchable (included in query_by) */
|
|
51
|
+
searchable?: boolean;
|
|
52
|
+
/** Enable faceting on this field */
|
|
53
|
+
facet?: boolean;
|
|
54
|
+
/** Enable sorting on this field */
|
|
55
|
+
sortable?: boolean;
|
|
56
|
+
/** Allow null/missing values */
|
|
57
|
+
optional?: boolean;
|
|
58
|
+
/** Language locale for stemming */
|
|
59
|
+
locale?: string;
|
|
60
|
+
/** Enable infix search */
|
|
61
|
+
infix?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Compute this field's value from the document during ingestion.
|
|
64
|
+
* The function receives the full document and the locale (if available)
|
|
65
|
+
* and returns the field value.
|
|
66
|
+
* Computed fields are automatically added before sending to Typesense.
|
|
67
|
+
*
|
|
68
|
+
* Example:
|
|
69
|
+
* ```ts
|
|
70
|
+
* category_page_slug: {
|
|
71
|
+
* type: "string",
|
|
72
|
+
* facet: true,
|
|
73
|
+
* compute: (doc, locale) => {
|
|
74
|
+
* const color = String(doc.color || "").toLowerCase();
|
|
75
|
+
* const category = String(doc.category || "").toLowerCase();
|
|
76
|
+
* return `${color}-${category}`.replace(/\s+/g, '-');
|
|
77
|
+
* },
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
compute?: (doc: Record<string, unknown>, locale?: string) => unknown;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Collection definition in the proxy config
|
|
85
|
+
*/
|
|
86
|
+
interface CollectionDefinition {
|
|
87
|
+
/** Field definitions */
|
|
88
|
+
fields: Record<string, FieldConfig>;
|
|
89
|
+
/** Supported locales — creates separate collections per locale */
|
|
90
|
+
locales?: string[];
|
|
91
|
+
/** Default sorting field */
|
|
92
|
+
defaultSortBy?: string;
|
|
93
|
+
/** Token separators */
|
|
94
|
+
tokenSeparators?: string[];
|
|
95
|
+
/** Symbols to index */
|
|
96
|
+
symbolsToIndex?: string[];
|
|
97
|
+
/** Enable nested fields */
|
|
98
|
+
enableNestedFields?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Proxy configuration file schema (tsproxy.config.ts)
|
|
102
|
+
*/
|
|
103
|
+
interface ProxyConfig {
|
|
104
|
+
/** Typesense connection settings */
|
|
105
|
+
typesense?: {
|
|
106
|
+
host?: string;
|
|
107
|
+
port?: number;
|
|
108
|
+
protocol?: "http" | "https";
|
|
109
|
+
apiKey?: string;
|
|
110
|
+
};
|
|
111
|
+
/** Proxy server settings */
|
|
112
|
+
server?: {
|
|
113
|
+
port?: number;
|
|
114
|
+
/** API key required for ingest endpoints */
|
|
115
|
+
apiKey?: string;
|
|
116
|
+
};
|
|
117
|
+
/** Search cache settings */
|
|
118
|
+
cache?: {
|
|
119
|
+
/** TTL in seconds (default: 60) */
|
|
120
|
+
ttl?: number;
|
|
121
|
+
/** Max cached entries (default: 1000) */
|
|
122
|
+
maxSize?: number;
|
|
123
|
+
};
|
|
124
|
+
/** Ingestion queue settings */
|
|
125
|
+
queue?: {
|
|
126
|
+
/** Max concurrent Typesense writes (default: 5) */
|
|
127
|
+
concurrency?: number;
|
|
128
|
+
/** Max queued tasks before rejecting (default: 10000) */
|
|
129
|
+
maxSize?: number;
|
|
130
|
+
/** Redis connection for persistent queue (falls back to in-memory) */
|
|
131
|
+
redis?: {
|
|
132
|
+
host?: string;
|
|
133
|
+
port?: number;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/** Rate limiting (requests per minute) */
|
|
137
|
+
rateLimit?: {
|
|
138
|
+
/** Search endpoint rate limit (default: 100) */
|
|
139
|
+
search?: number;
|
|
140
|
+
/** Ingest endpoint rate limit (default: 30) */
|
|
141
|
+
ingest?: number;
|
|
142
|
+
};
|
|
143
|
+
/** Collection definitions */
|
|
144
|
+
collections?: Record<string, CollectionDefinition>;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Helper to define a typed proxy config
|
|
148
|
+
*/
|
|
149
|
+
declare function defineConfig(config: ProxyConfig): ProxyConfig;
|
|
150
|
+
/**
|
|
151
|
+
* Convert ProxyConfig to the internal Config format
|
|
152
|
+
*/
|
|
153
|
+
declare function proxyConfigToConfig(proxyConfig: ProxyConfig): Config;
|
|
154
|
+
/**
|
|
155
|
+
* Get searchable fields from a collection definition
|
|
156
|
+
*/
|
|
157
|
+
declare function getSearchableFields(def: CollectionDefinition): string[];
|
|
158
|
+
/**
|
|
159
|
+
* Get facet fields from a collection definition
|
|
160
|
+
*/
|
|
161
|
+
declare function getFacetFields(def: CollectionDefinition): string[];
|
|
162
|
+
/**
|
|
163
|
+
* Get sortable fields from a collection definition
|
|
164
|
+
*/
|
|
165
|
+
declare function getSortableFields(def: CollectionDefinition): string[];
|
|
166
|
+
/**
|
|
167
|
+
* Convert a collection definition to a Typesense collection schema
|
|
168
|
+
* (compatible with tsctl's defineConfig format)
|
|
169
|
+
*/
|
|
170
|
+
declare function toTypesenseSchema(name: string, def: CollectionDefinition): {
|
|
171
|
+
enable_nested_fields?: boolean | undefined;
|
|
172
|
+
symbols_to_index?: string[] | undefined;
|
|
173
|
+
token_separators?: string[] | undefined;
|
|
174
|
+
default_sorting_field?: string | undefined;
|
|
175
|
+
name: string;
|
|
176
|
+
fields: Record<string, unknown>[];
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Get computed fields from a collection definition
|
|
180
|
+
*/
|
|
181
|
+
declare function getComputedFields(def: CollectionDefinition): Array<{
|
|
182
|
+
name: string;
|
|
183
|
+
compute: (doc: Record<string, unknown>, locale?: string) => unknown;
|
|
184
|
+
}>;
|
|
185
|
+
/**
|
|
186
|
+
* Apply computed fields to a document.
|
|
187
|
+
* Runs all compute functions defined in the collection config
|
|
188
|
+
* and adds the resulting values to the document.
|
|
189
|
+
* The locale is passed to compute functions for localized field generation.
|
|
190
|
+
*/
|
|
191
|
+
declare function applyComputedFields(doc: Record<string, unknown>, collectionDef: CollectionDefinition, locale?: string): Record<string, unknown>;
|
|
192
|
+
/**
|
|
193
|
+
* Apply computed fields to an array of documents.
|
|
194
|
+
*/
|
|
195
|
+
declare function applyComputedFieldsBatch(docs: Record<string, unknown>[], collectionDef: CollectionDefinition, locale?: string): Record<string, unknown>[];
|
|
196
|
+
|
|
197
|
+
interface CacheStats {
|
|
198
|
+
size: number;
|
|
199
|
+
maxSize: number;
|
|
200
|
+
ttl: number;
|
|
201
|
+
hits: number;
|
|
202
|
+
misses: number;
|
|
203
|
+
hitRate: number;
|
|
204
|
+
}
|
|
205
|
+
declare class LRUCache<T = unknown> {
|
|
206
|
+
private cache;
|
|
207
|
+
private readonly maxSize;
|
|
208
|
+
private readonly ttl;
|
|
209
|
+
private hits;
|
|
210
|
+
private misses;
|
|
211
|
+
constructor(options: {
|
|
212
|
+
maxSize: number;
|
|
213
|
+
ttl: number;
|
|
214
|
+
});
|
|
215
|
+
get(key: string): T | undefined;
|
|
216
|
+
set(key: string, value: T): void;
|
|
217
|
+
invalidate(key?: string): void;
|
|
218
|
+
stats(): CacheStats;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface QueueStats {
|
|
222
|
+
pending: number;
|
|
223
|
+
active: number;
|
|
224
|
+
completed: number;
|
|
225
|
+
failed: number;
|
|
226
|
+
maxSize: number;
|
|
227
|
+
concurrency: number;
|
|
228
|
+
backend: "redis" | "memory";
|
|
229
|
+
}
|
|
230
|
+
interface QueueOptions {
|
|
231
|
+
concurrency: number;
|
|
232
|
+
maxSize: number;
|
|
233
|
+
redis?: {
|
|
234
|
+
host?: string;
|
|
235
|
+
port?: number;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Ingestion queue backed by BullMQ (Redis) with in-memory fallback.
|
|
240
|
+
*
|
|
241
|
+
* When Redis is available, jobs are persisted and survive server restarts.
|
|
242
|
+
* When Redis is unavailable, falls back to a simple in-memory queue.
|
|
243
|
+
*/
|
|
244
|
+
declare class IngestionQueue {
|
|
245
|
+
private readonly concurrency;
|
|
246
|
+
private readonly maxSize;
|
|
247
|
+
private backend;
|
|
248
|
+
private bullQueue;
|
|
249
|
+
private bullWorker;
|
|
250
|
+
private jobHandlers;
|
|
251
|
+
private memPending;
|
|
252
|
+
private memActiveCount;
|
|
253
|
+
private memCompletedCount;
|
|
254
|
+
private memFailedCount;
|
|
255
|
+
constructor(options: QueueOptions);
|
|
256
|
+
private initRedis;
|
|
257
|
+
private fallbackToMemory;
|
|
258
|
+
enqueue<T>(fn: () => Promise<T>): Promise<T>;
|
|
259
|
+
private enqueueRedis;
|
|
260
|
+
private enqueueMemory;
|
|
261
|
+
stats(): Promise<QueueStats>;
|
|
262
|
+
close(): Promise<void>;
|
|
263
|
+
private processMemory;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Transform between Algolia InstantSearch format and Typesense search format.
|
|
268
|
+
*/
|
|
269
|
+
interface AlgoliaSearchRequest {
|
|
270
|
+
indexName: string;
|
|
271
|
+
params: string | Record<string, unknown>;
|
|
272
|
+
}
|
|
273
|
+
interface AlgoliaMultiSearchRequest {
|
|
274
|
+
requests: AlgoliaSearchRequest[];
|
|
275
|
+
}
|
|
276
|
+
interface AlgoliaHighlightResult {
|
|
277
|
+
value: string;
|
|
278
|
+
matchLevel: "none" | "partial" | "full";
|
|
279
|
+
matchedWords: string[];
|
|
280
|
+
fullyHighlighted?: boolean;
|
|
281
|
+
}
|
|
282
|
+
interface AlgoliaHit {
|
|
283
|
+
objectID: string;
|
|
284
|
+
_highlightResult: Record<string, AlgoliaHighlightResult>;
|
|
285
|
+
[key: string]: unknown;
|
|
286
|
+
}
|
|
287
|
+
interface AlgoliaSearchResult {
|
|
288
|
+
hits: AlgoliaHit[];
|
|
289
|
+
nbHits: number;
|
|
290
|
+
page: number;
|
|
291
|
+
nbPages: number;
|
|
292
|
+
hitsPerPage: number;
|
|
293
|
+
processingTimeMS: number;
|
|
294
|
+
query: string;
|
|
295
|
+
params: string;
|
|
296
|
+
facets: Record<string, Record<string, number>>;
|
|
297
|
+
facets_stats: Record<string, unknown>;
|
|
298
|
+
exhaustiveNbHits: boolean;
|
|
299
|
+
}
|
|
300
|
+
interface AlgoliaMultiSearchResponse {
|
|
301
|
+
results: AlgoliaSearchResult[];
|
|
302
|
+
}
|
|
303
|
+
interface TypesenseSearchParams {
|
|
304
|
+
q: string;
|
|
305
|
+
query_by?: string;
|
|
306
|
+
filter_by?: string;
|
|
307
|
+
sort_by?: string;
|
|
308
|
+
facet_by?: string;
|
|
309
|
+
max_facet_values?: number;
|
|
310
|
+
page?: number;
|
|
311
|
+
per_page?: number;
|
|
312
|
+
highlight_full_fields?: string;
|
|
313
|
+
highlight_affix_num_tokens?: number;
|
|
314
|
+
highlight_start_tag?: string;
|
|
315
|
+
highlight_end_tag?: string;
|
|
316
|
+
[key: string]: unknown;
|
|
317
|
+
}
|
|
318
|
+
interface TypesenseMultiSearchParams {
|
|
319
|
+
searches: Array<TypesenseSearchParams & {
|
|
320
|
+
collection: string;
|
|
321
|
+
}>;
|
|
322
|
+
}
|
|
323
|
+
interface TypesenseHighlight {
|
|
324
|
+
field: string;
|
|
325
|
+
snippet?: string;
|
|
326
|
+
value?: string;
|
|
327
|
+
matched_tokens?: string[];
|
|
328
|
+
snippets?: Array<{
|
|
329
|
+
value: string;
|
|
330
|
+
matched_tokens?: string[];
|
|
331
|
+
}>;
|
|
332
|
+
indices?: number[];
|
|
333
|
+
}
|
|
334
|
+
interface TypesenseHit {
|
|
335
|
+
document: Record<string, unknown>;
|
|
336
|
+
highlights?: TypesenseHighlight[];
|
|
337
|
+
text_match?: number;
|
|
338
|
+
text_match_info?: Record<string, unknown>;
|
|
339
|
+
}
|
|
340
|
+
interface TypesenseFacetCount {
|
|
341
|
+
field_name: string;
|
|
342
|
+
counts: Array<{
|
|
343
|
+
value: string;
|
|
344
|
+
count: number;
|
|
345
|
+
}>;
|
|
346
|
+
stats?: {
|
|
347
|
+
min?: number;
|
|
348
|
+
max?: number;
|
|
349
|
+
avg?: number;
|
|
350
|
+
sum?: number;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
interface TypesenseSearchResponse {
|
|
354
|
+
found: number;
|
|
355
|
+
hits?: TypesenseHit[];
|
|
356
|
+
search_time_ms: number;
|
|
357
|
+
facet_counts?: TypesenseFacetCount[];
|
|
358
|
+
page?: number;
|
|
359
|
+
out_of?: number;
|
|
360
|
+
request_params?: Record<string, unknown>;
|
|
361
|
+
}
|
|
362
|
+
interface TypesenseMultiSearchResponse {
|
|
363
|
+
results: TypesenseSearchResponse[];
|
|
364
|
+
}
|
|
365
|
+
declare function transformAlgoliaRequestToTypesense(request: AlgoliaSearchRequest, resolvedCollection: string): TypesenseSearchParams & {
|
|
366
|
+
collection: string;
|
|
367
|
+
};
|
|
368
|
+
declare function transformTypesenseResponseToAlgolia(tsResponse: TypesenseSearchResponse, originalRequest: AlgoliaSearchRequest): AlgoliaSearchResult;
|
|
369
|
+
declare function transformMultiSearchResponse(tsResponse: TypesenseMultiSearchResponse, originalRequests: AlgoliaSearchRequest[]): AlgoliaMultiSearchResponse;
|
|
370
|
+
|
|
371
|
+
declare function createApp(config?: Config, collectionDefs?: Record<string, CollectionDefinition>): {
|
|
372
|
+
app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
373
|
+
config: Config;
|
|
374
|
+
searchCache: LRUCache<unknown>;
|
|
375
|
+
ingestQueue: IngestionQueue;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export { type AlgoliaHighlightResult, type AlgoliaHit, type AlgoliaMultiSearchRequest, type AlgoliaMultiSearchResponse, type AlgoliaSearchRequest, type AlgoliaSearchResult, type CacheStats, type CollectionDefinition, type Config, type FieldConfig, IngestionQueue, LRUCache, type ProxyConfig, type QueueStats, type TypesenseMultiSearchParams, type TypesenseMultiSearchResponse, type TypesenseSearchParams, type TypesenseSearchResponse, applyComputedFields, applyComputedFieldsBatch, createApp, defineConfig, getComputedFields, getFacetFields, getSearchableFields, getSortableFields, loadConfig, proxyConfigToConfig, resolveCollection, toTypesenseSchema, transformAlgoliaRequestToTypesense, transformMultiSearchResponse, transformTypesenseResponseToAlgolia };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IngestionQueue,
|
|
3
|
+
LRUCache,
|
|
4
|
+
applyComputedFields,
|
|
5
|
+
applyComputedFieldsBatch,
|
|
6
|
+
createApp,
|
|
7
|
+
defineConfig,
|
|
8
|
+
getComputedFields,
|
|
9
|
+
getFacetFields,
|
|
10
|
+
getSearchableFields,
|
|
11
|
+
getSortableFields,
|
|
12
|
+
loadConfig,
|
|
13
|
+
proxyConfigToConfig,
|
|
14
|
+
resolveCollection,
|
|
15
|
+
toTypesenseSchema,
|
|
16
|
+
transformAlgoliaRequestToTypesense,
|
|
17
|
+
transformMultiSearchResponse,
|
|
18
|
+
transformTypesenseResponseToAlgolia
|
|
19
|
+
} from "./chunk-VL6QOQ2T.js";
|
|
20
|
+
export {
|
|
21
|
+
IngestionQueue,
|
|
22
|
+
LRUCache,
|
|
23
|
+
applyComputedFields,
|
|
24
|
+
applyComputedFieldsBatch,
|
|
25
|
+
createApp,
|
|
26
|
+
defineConfig,
|
|
27
|
+
getComputedFields,
|
|
28
|
+
getFacetFields,
|
|
29
|
+
getSearchableFields,
|
|
30
|
+
getSortableFields,
|
|
31
|
+
loadConfig,
|
|
32
|
+
proxyConfigToConfig,
|
|
33
|
+
resolveCollection,
|
|
34
|
+
toTypesenseSchema,
|
|
35
|
+
transformAlgoliaRequestToTypesense,
|
|
36
|
+
transformMultiSearchResponse,
|
|
37
|
+
transformTypesenseResponseToAlgolia
|
|
38
|
+
};
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createApp
|
|
3
|
+
} from "./chunk-VL6QOQ2T.js";
|
|
4
|
+
|
|
5
|
+
// src/server.ts
|
|
6
|
+
import { serve } from "@hono/node-server";
|
|
7
|
+
var { app, config } = createApp();
|
|
8
|
+
console.log(`Starting Typesense Proxy on port ${config.proxy.port}...`);
|
|
9
|
+
console.log(`Typesense backend: ${config.typesense.protocol}://${config.typesense.host}:${config.typesense.port}`);
|
|
10
|
+
serve(
|
|
11
|
+
{
|
|
12
|
+
fetch: app.fetch,
|
|
13
|
+
port: config.proxy.port
|
|
14
|
+
},
|
|
15
|
+
(info) => {
|
|
16
|
+
console.log(`Proxy server listening on http://localhost:${info.port}`);
|
|
17
|
+
console.log(`API docs available at http://localhost:${info.port}/api/docs`);
|
|
18
|
+
}
|
|
19
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsproxy/api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "HonoJS proxy server for Typesense with caching, rate limiting, and ingestion queue",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"tsproxy-server": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/akshitkrnagpal/tsproxy.git",
|
|
26
|
+
"directory": "packages/api"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"typesense",
|
|
31
|
+
"search",
|
|
32
|
+
"proxy",
|
|
33
|
+
"hono",
|
|
34
|
+
"cache",
|
|
35
|
+
"rate-limit"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@hono/node-server": "^1.13.0",
|
|
39
|
+
"@hono/zod-openapi": "^0.18.0",
|
|
40
|
+
"bullmq": "^5.73.0",
|
|
41
|
+
"hono": "^4.7.0",
|
|
42
|
+
"hono-rate-limiter": "^0.5.0",
|
|
43
|
+
"typesense": "^3.0.0-2",
|
|
44
|
+
"zod": "^3.23.8"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.19.37",
|
|
48
|
+
"tsup": "^8.4.0",
|
|
49
|
+
"tsx": "^4.19.0",
|
|
50
|
+
"vitest": "^3.2.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "tsx watch src/cli.ts dev",
|
|
54
|
+
"build": "tsup src/index.ts src/server.ts src/cli.ts --format esm --dts --tsconfig tsconfig.build.json",
|
|
55
|
+
"start": "node dist/cli.js start",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"typecheck": "tsc --noEmit"
|
|
58
|
+
}
|
|
59
|
+
}
|