@tsproxy/api 0.0.2 → 0.0.3

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.
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { Hono as Hono5 } from "hono";
2
+ import { Hono as Hono7 } from "hono";
3
3
  import { cors } from "hono/cors";
4
4
  import { logger } from "hono/logger";
5
5
 
@@ -81,13 +81,30 @@ function resolveCollection(config, collection, locale) {
81
81
  }
82
82
 
83
83
  // src/middleware/error-handler.ts
84
+ var FRIENDLY_ERRORS = {
85
+ ECONNREFUSED: "Cannot connect to Typesense. Is it running? Start with: docker compose up -d",
86
+ ENOTFOUND: "Typesense host not found. Check your TYPESENSE_HOST setting.",
87
+ ETIMEDOUT: "Connection to Typesense timed out. Check your network and host settings.",
88
+ "Request failed with status code 401": "Invalid Typesense API key. Check your TYPESENSE_API_KEY.",
89
+ "Request failed with status code 403": "Typesense API key lacks permissions for this operation."
90
+ };
91
+ function getFriendlyMessage(err) {
92
+ for (const [key, message] of Object.entries(FRIENDLY_ERRORS)) {
93
+ if (err.message.includes(key)) return message;
94
+ }
95
+ return void 0;
96
+ }
84
97
  function errorHandler(err, c) {
85
- console.error(`[Error] ${err.message}`, err.stack);
98
+ const friendly = getFriendlyMessage(err);
99
+ if (friendly) {
100
+ console.error(`[tsproxy] ${friendly}`);
101
+ } else {
102
+ console.error(`[tsproxy] ${err.message}`);
103
+ }
86
104
  const status = err.status ?? err.statusCode ?? 500;
87
105
  return c.json(
88
106
  {
89
- error: status === 500 ? "Internal Server Error" : err.message,
90
- ...process.env["NODE_ENV"] === "development" && { stack: err.stack }
107
+ error: friendly || (status === 500 ? "Internal Server Error" : err.message)
91
108
  },
92
109
  status
93
110
  );
@@ -284,6 +301,25 @@ function transformAlgoliaRequestToTypesense(request, resolvedCollection) {
284
301
  if (params["sortBy"]) {
285
302
  result.sort_by = params["sortBy"];
286
303
  }
304
+ if (params["aroundLatLng"]) {
305
+ const geo = params["aroundLatLng"];
306
+ const radius = params["aroundRadius"] || "10000";
307
+ const geoField = params["geoField"] || "_geoloc";
308
+ const [lat, lng] = geo.split(",").map((s) => s.trim());
309
+ if (lat && lng) {
310
+ const existing = result.filter_by ? `${result.filter_by} && ` : "";
311
+ result.filter_by = `${existing}${geoField}:(${lat}, ${lng}, ${radius} m)`;
312
+ if (!result.sort_by) {
313
+ result.sort_by = `${geoField}(${lat}, ${lng}):asc`;
314
+ }
315
+ }
316
+ }
317
+ if (params["groupBy"]) {
318
+ result.group_by = params["groupBy"];
319
+ if (params["groupLimit"]) {
320
+ result.group_limit = parseInt(params["groupLimit"], 10);
321
+ }
322
+ }
287
323
  return result;
288
324
  }
289
325
  function transformHighlights(highlights, document) {
@@ -1169,10 +1205,132 @@ function createDocsRoutes() {
1169
1205
  return app;
1170
1206
  }
1171
1207
 
1208
+ // src/routes/suggestions.ts
1209
+ import { Hono as Hono5 } from "hono";
1210
+ function createSuggestionsRoutes(config, collectionDefs) {
1211
+ const app = new Hono5();
1212
+ const typesense = getTypesenseClient(config);
1213
+ app.get("/api/suggestions", async (c) => {
1214
+ const query = c.req.query("q") || "";
1215
+ const collection = c.req.query("collection") || "products";
1216
+ const limit = parseInt(c.req.query("limit") || "5", 10);
1217
+ const locale = c.req.header("X-Locale") ?? c.req.query("locale");
1218
+ if (!query) {
1219
+ return c.json({ suggestions: [] });
1220
+ }
1221
+ const resolved = resolveCollection(config.collections, collection, locale);
1222
+ let queryBy = "name";
1223
+ if (collectionDefs?.[collection]) {
1224
+ const searchable = getSearchableFields(collectionDefs[collection]);
1225
+ if (searchable.length > 0) {
1226
+ queryBy = searchable.join(",");
1227
+ }
1228
+ }
1229
+ try {
1230
+ const result = await typesense.collections(resolved).documents().search({
1231
+ q: query,
1232
+ query_by: queryBy,
1233
+ per_page: limit,
1234
+ prefix: "true",
1235
+ highlight_full_fields: queryBy,
1236
+ highlight_start_tag: "<mark>",
1237
+ highlight_end_tag: "</mark>"
1238
+ });
1239
+ const suggestions = (result.hits || []).map((hit) => {
1240
+ const doc = hit.document || {};
1241
+ const highlights = hit.highlights || [];
1242
+ let highlightedValue = "";
1243
+ for (const hl of highlights) {
1244
+ if (hl.snippet) {
1245
+ highlightedValue = hl.snippet;
1246
+ break;
1247
+ }
1248
+ }
1249
+ return {
1250
+ objectID: String(doc.id || ""),
1251
+ query: String(doc[queryBy.split(",")[0]] || ""),
1252
+ highlight: highlightedValue || String(doc[queryBy.split(",")[0]] || "")
1253
+ };
1254
+ });
1255
+ return c.json({
1256
+ suggestions,
1257
+ query,
1258
+ found: result.found
1259
+ });
1260
+ } catch (err) {
1261
+ console.error("[suggestions] Error:", err.message);
1262
+ return c.json({ suggestions: [], query, error: err.message });
1263
+ }
1264
+ });
1265
+ return app;
1266
+ }
1267
+
1268
+ // src/routes/analytics.ts
1269
+ import { Hono as Hono6 } from "hono";
1270
+ function createAnalyticsRoutes(config) {
1271
+ const app = new Hono6();
1272
+ const typesense = getTypesenseClient(config);
1273
+ app.post("/api/analytics/click", async (c) => {
1274
+ const body = await c.req.json();
1275
+ if (!body.query || !body.documentId) {
1276
+ return c.json({ error: "query and documentId are required" }, 400);
1277
+ }
1278
+ try {
1279
+ await typesense.analytics.events().create({
1280
+ type: "click",
1281
+ name: "search_click",
1282
+ data: {
1283
+ q: body.query,
1284
+ doc_id: body.documentId,
1285
+ position: body.position || 1,
1286
+ collection: body.collection || "products"
1287
+ }
1288
+ });
1289
+ return c.json({ ok: true });
1290
+ } catch (err) {
1291
+ console.warn("[analytics] Click event failed:", err.message);
1292
+ return c.json({ ok: false, error: err.message });
1293
+ }
1294
+ });
1295
+ app.post("/api/analytics/conversion", async (c) => {
1296
+ const body = await c.req.json();
1297
+ if (!body.query || !body.documentId) {
1298
+ return c.json({ error: "query and documentId are required" }, 400);
1299
+ }
1300
+ try {
1301
+ await typesense.analytics.events().create({
1302
+ type: "conversion",
1303
+ name: "search_conversion",
1304
+ data: {
1305
+ q: body.query,
1306
+ doc_id: body.documentId,
1307
+ collection: body.collection || "products"
1308
+ }
1309
+ });
1310
+ return c.json({ ok: true });
1311
+ } catch (err) {
1312
+ console.warn("[analytics] Conversion event failed:", err.message);
1313
+ return c.json({ ok: false, error: err.message });
1314
+ }
1315
+ });
1316
+ app.get("/api/analytics/popular", async (c) => {
1317
+ const collection = c.req.query("collection") || "products";
1318
+ const limit = parseInt(c.req.query("limit") || "10", 10);
1319
+ try {
1320
+ const rules = await typesense.analytics.rules().retrieve();
1321
+ return c.json({ rules, collection, limit });
1322
+ } catch (err) {
1323
+ console.warn("[analytics] Popular queries failed:", err.message);
1324
+ return c.json({ queries: [], error: err.message });
1325
+ }
1326
+ });
1327
+ return app;
1328
+ }
1329
+
1172
1330
  // src/index.ts
1173
1331
  function createApp(config, collectionDefs) {
1174
1332
  const cfg = config ?? loadConfig();
1175
- const app = new Hono5();
1333
+ const app = new Hono7();
1176
1334
  app.use("*", cors());
1177
1335
  app.use("*", logger());
1178
1336
  app.onError(errorHandler);
@@ -1180,10 +1338,14 @@ function createApp(config, collectionDefs) {
1180
1338
  const { app: ingestApp, queue: ingestQueue } = createIngestRoutes(cfg, collectionDefs);
1181
1339
  const healthApp = createHealthRoutes(cfg);
1182
1340
  const docsApp = createDocsRoutes();
1341
+ const suggestionsApp = createSuggestionsRoutes(cfg, collectionDefs);
1342
+ const analyticsApp = createAnalyticsRoutes(cfg);
1183
1343
  app.route("/", searchApp);
1184
1344
  app.route("/", ingestApp);
1185
1345
  app.route("/", healthApp);
1186
1346
  app.route("/", docsApp);
1347
+ app.route("/", suggestionsApp);
1348
+ app.route("/", analyticsApp);
1187
1349
  return { app, config: cfg, searchCache, ingestQueue };
1188
1350
  }
1189
1351
 
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createApp,
4
4
  proxyConfigToConfig
5
- } from "./chunk-VL6QOQ2T.js";
5
+ } from "./chunk-5GS3ZEPG.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { resolve } from "path";
package/dist/index.d.ts CHANGED
@@ -83,6 +83,24 @@ interface FieldConfig {
83
83
  /**
84
84
  * Collection definition in the proxy config
85
85
  */
86
+ interface SynonymDefinition {
87
+ /** Synonym words — all are interchangeable */
88
+ synonyms?: string[];
89
+ /** One-way: root word */
90
+ root?: string;
91
+ /** One-way: words that map to the root */
92
+ words?: string[];
93
+ }
94
+ interface CurationDefinition {
95
+ /** Query to match */
96
+ query: string;
97
+ /** Match type: exact or contains */
98
+ match?: "exact" | "contains";
99
+ /** Document IDs to pin to the top */
100
+ pinnedIds?: string[];
101
+ /** Document IDs to hide from results */
102
+ hiddenIds?: string[];
103
+ }
86
104
  interface CollectionDefinition {
87
105
  /** Field definitions */
88
106
  fields: Record<string, FieldConfig>;
@@ -96,6 +114,10 @@ interface CollectionDefinition {
96
114
  symbolsToIndex?: string[];
97
115
  /** Enable nested fields */
98
116
  enableNestedFields?: boolean;
117
+ /** Synonym definitions */
118
+ synonyms?: Record<string, SynonymDefinition>;
119
+ /** Curation/pinning rules */
120
+ curations?: Record<string, CurationDefinition>;
99
121
  }
100
122
  /**
101
123
  * Proxy configuration file schema (tsproxy.config.ts)
@@ -375,4 +397,4 @@ declare function createApp(config?: Config, collectionDefs?: Record<string, Coll
375
397
  ingestQueue: IngestionQueue;
376
398
  };
377
399
 
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 };
400
+ export { type AlgoliaHighlightResult, type AlgoliaHit, type AlgoliaMultiSearchRequest, type AlgoliaMultiSearchResponse, type AlgoliaSearchRequest, type AlgoliaSearchResult, type CacheStats, type CollectionDefinition, type Config, type CurationDefinition, type FieldConfig, IngestionQueue, LRUCache, type ProxyConfig, type QueueStats, type SynonymDefinition, 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 CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  transformAlgoliaRequestToTypesense,
17
17
  transformMultiSearchResponse,
18
18
  transformTypesenseResponseToAlgolia
19
- } from "./chunk-VL6QOQ2T.js";
19
+ } from "./chunk-5GS3ZEPG.js";
20
20
  export {
21
21
  IngestionQueue,
22
22
  LRUCache,
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "./chunk-VL6QOQ2T.js";
3
+ } from "./chunk-5GS3ZEPG.js";
4
4
 
5
5
  // src/server.ts
6
6
  import { serve } from "@hono/node-server";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsproxy/api",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "HonoJS proxy server for Typesense with caching, rate limiting, and ingestion queue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",