@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.
- package/dist/{chunk-VL6QOQ2T.js → chunk-5GS3ZEPG.js} +167 -5
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { Hono as
|
|
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
|
-
|
|
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
|
|
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
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
package/dist/server.js
CHANGED