@voltro/plugin-search 0.1.0

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/index.js ADDED
@@ -0,0 +1,623 @@
1
+ import { queryDescriptor as e, searchRpcClientImports as t } from "./rpc.js";
2
+ import { Data as n, Effect as r } from "effect";
3
+ import { definePlugin as i } from "@voltro/protocol";
4
+ import { and as a, eq as o, id as s, integer as c, table as l, text as u, timestamp as d } from "@voltro/database";
5
+ //#region src/backend.ts
6
+ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.isArray(e) ? e : [e], m = (e, t, n) => {
7
+ if (Math.abs(e.length - t.length) > n) return !1;
8
+ if (e === t) return !0;
9
+ let r = t.length, i = Array.from({ length: r + 1 }, (e, t) => t);
10
+ for (let a = 1; a <= e.length; a++) {
11
+ let o = Array(r + 1);
12
+ o[0] = a;
13
+ let s = a, c = e.charCodeAt(a - 1);
14
+ for (let e = 1; e <= r; e++) {
15
+ let n = c === t.charCodeAt(e - 1) ? 0 : 1, r = Math.min((i[e] ?? 0) + 1, (o[e - 1] ?? 0) + 1, (i[e - 1] ?? 0) + n);
16
+ o[e] = r, r < s && (s = r);
17
+ }
18
+ if (s > n) return !1;
19
+ i = o;
20
+ }
21
+ return (i[r] ?? Infinity) <= n;
22
+ }, h = (e) => e.length <= 4 ? 1 : 2, g = () => {
23
+ let e = /* @__PURE__ */ new Map(), t = (t) => {
24
+ let n = e.get(t);
25
+ return n || (n = /* @__PURE__ */ new Map(), e.set(t, n)), n;
26
+ }, n = (e, t) => {
27
+ let n = e[t.field];
28
+ switch (t.op) {
29
+ case "eq": return String(n) === String(t.value);
30
+ case "neq": return String(n) !== String(t.value);
31
+ case "in": return p(t.value).some((e) => String(n) === String(e));
32
+ case "nin": return p(t.value).every((e) => String(n) !== String(e));
33
+ case "gt":
34
+ case "gte":
35
+ case "lt":
36
+ case "lte": {
37
+ let [e, r] = [_(n), _(t.value)];
38
+ return e === void 0 || r === void 0 ? !1 : t.op === "gt" ? e > r : t.op === "gte" ? e >= r : t.op === "lt" ? e < r : e <= r;
39
+ }
40
+ }
41
+ }, r = (e, t) => !t || t.every((t) => n(e, t)), i = (e) => Object.entries(e).filter(([e]) => e !== "id").map(([e, t]) => [e, String(t)]), a = (e, t, n) => {
42
+ if (t.length === 0) return !0;
43
+ for (let [, r] of i(e)) {
44
+ let e = r.toLowerCase();
45
+ if (e.includes(t)) return !0;
46
+ if (n !== void 0 && n !== 0) {
47
+ let r = n === "auto" ? h(t) : n;
48
+ for (let n of e.split(/\s+/)) if (m(n, t, r)) return !0;
49
+ }
50
+ }
51
+ return !1;
52
+ }, o = (e, t, n) => {
53
+ if (t.length === 0) return {};
54
+ let r = n.preTag ?? "<mark>", a = n.postTag ?? "</mark>", o = n.fields && n.fields.length > 0 ? new Set(n.fields) : null, s = {}, c = new RegExp(t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
55
+ for (let [n, l] of i(e)) o && !o.has(n) || l.toLowerCase().includes(t) && (s[n] = l.replace(c, (e) => `${r}${e}${a}`));
56
+ return s;
57
+ };
58
+ return {
59
+ upsert: async (e, n) => {
60
+ let r = t(e);
61
+ for (let e of n) r.set(e.id, e);
62
+ },
63
+ remove: async (e, n) => {
64
+ let r = t(e);
65
+ for (let e of n) r.delete(e);
66
+ },
67
+ query: async (e, { q: n, limit: i, offset: s, filters: c, facets: l, highlight: u, fuzziness: d }) => {
68
+ let f = n.trim().toLowerCase(), p = [];
69
+ for (let n of t(e).values()) r(n, c) && a(n, f, d) && p.push(n);
70
+ let m = {};
71
+ if (l) for (let e of l) {
72
+ let t = {};
73
+ for (let n of p) {
74
+ let r = n[e];
75
+ if (r == null) continue;
76
+ let i = String(r);
77
+ t[i] = (t[i] ?? 0) + 1;
78
+ }
79
+ m[e] = t;
80
+ }
81
+ let h = s ?? 0;
82
+ return {
83
+ hits: (i === void 0 ? p.slice(h) : p.slice(h, h + i)).map((e) => u ? {
84
+ doc: e,
85
+ highlights: o(e, f, u)
86
+ } : { doc: e }),
87
+ facets: m
88
+ };
89
+ }
90
+ };
91
+ }, _ = (e) => {
92
+ if (typeof e == "number") return e;
93
+ if (typeof e == "boolean") return +!!e;
94
+ if (typeof e == "string") {
95
+ let t = Number(e);
96
+ if (!Number.isNaN(t) && e.trim() !== "") return t;
97
+ let n = Date.parse(e);
98
+ return Number.isNaN(n) ? void 0 : n;
99
+ }
100
+ }, v = async (e, t, n) => {
101
+ try {
102
+ return await n();
103
+ } catch {
104
+ throw new f({
105
+ engine: t,
106
+ op: "query",
107
+ message: `the '${t}' search backend requires the '${e}' optional dependency. Install it to enable it (e.g. \`npm i ${e}\`).`,
108
+ transient: !1
109
+ });
110
+ }
111
+ }, y = (e) => {
112
+ let t = (e) => JSON.stringify(e);
113
+ switch (e.op) {
114
+ case "eq": return `${e.field}:=${t(e.value)}`;
115
+ case "neq": return `${e.field}:!=${t(e.value)}`;
116
+ case "gt": return `${e.field}:>${t(e.value)}`;
117
+ case "gte": return `${e.field}:>=${t(e.value)}`;
118
+ case "lt": return `${e.field}:<${t(e.value)}`;
119
+ case "lte": return `${e.field}:<=${t(e.value)}`;
120
+ case "in": return `${e.field}:[${p(e.value).map(t).join(",")}]`;
121
+ case "nin": return `${e.field}:!=[${p(e.value).map(t).join(",")}]`;
122
+ }
123
+ }, b = (e) => {
124
+ let t, n = async () => (t ||= v("typesense", "typesense", () => import("typesense")).then((t) => {
125
+ let n = t.Client;
126
+ return new n({
127
+ nodes: [{ url: e.url ?? e.host ?? "http://localhost:8108" }],
128
+ apiKey: e.apiKey
129
+ });
130
+ }), t), r = (e) => e && e.length > 0 ? e.map(y).join(" && ") : void 0, i = async (e, t) => {
131
+ try {
132
+ return await t();
133
+ } catch (t) {
134
+ throw t instanceof f ? t : new f({
135
+ engine: "typesense",
136
+ op: e,
137
+ message: String(t?.message ?? t),
138
+ transient: !0
139
+ });
140
+ }
141
+ };
142
+ return {
143
+ upsert: (e, t) => i("upsert", async () => {
144
+ await (await n()).collections(e).documents().import(t, { action: "upsert" });
145
+ }),
146
+ remove: (e, t) => i("remove", async () => {
147
+ await (await n()).collections(e).documents().delete({ filter_by: `id:[${t.map((e) => JSON.stringify(e)).join(",")}]` });
148
+ }),
149
+ query: (e, { q: t, limit: a, offset: o, filters: s, facets: c, highlight: l, fuzziness: u, engineParams: d }) => i("query", async () => {
150
+ let i = await n(), f = {
151
+ q: t || "*",
152
+ query_by: "*",
153
+ per_page: a ?? 20,
154
+ ...o ? { offset: o } : {},
155
+ ...r(s) ? { filter_by: r(s) } : {},
156
+ ...c && c.length > 0 ? { facet_by: c.join(",") } : {},
157
+ ...l ? {
158
+ ...l.fields && l.fields.length > 0 ? { highlight_fields: l.fields.join(",") } : { highlight_full_fields: "*" },
159
+ ...l.preTag ? { highlight_start_tag: l.preTag } : {},
160
+ ...l.postTag ? { highlight_end_tag: l.postTag } : {}
161
+ } : {},
162
+ ...u !== void 0 && u !== "auto" ? { num_typos: Math.min(2, Math.max(0, u)) } : {},
163
+ ...d ?? {}
164
+ };
165
+ return x(await i.collections(e).documents().search(f));
166
+ })
167
+ };
168
+ }, x = (e) => {
169
+ let t = (e.hits ?? []).map((e) => {
170
+ let t = {};
171
+ for (let n of e.highlights ?? []) {
172
+ let e = n.snippet ?? n.value;
173
+ e && (t[n.field] = e);
174
+ }
175
+ return Object.keys(t).length > 0 ? {
176
+ doc: e.document,
177
+ highlights: t
178
+ } : { doc: e.document };
179
+ }), n = {};
180
+ for (let t of e.facet_counts ?? []) n[t.field_name] = Object.fromEntries(t.counts.map((e) => [e.value, e.count]));
181
+ return {
182
+ hits: t,
183
+ facets: n
184
+ };
185
+ }, S = (e) => {
186
+ let t = (e) => JSON.stringify(e);
187
+ switch (e.op) {
188
+ case "eq": return `${e.field} = ${t(e.value)}`;
189
+ case "neq": return `${e.field} != ${t(e.value)}`;
190
+ case "gt": return `${e.field} > ${t(e.value)}`;
191
+ case "gte": return `${e.field} >= ${t(e.value)}`;
192
+ case "lt": return `${e.field} < ${t(e.value)}`;
193
+ case "lte": return `${e.field} <= ${t(e.value)}`;
194
+ case "in": return `${e.field} IN [${p(e.value).map(t).join(", ")}]`;
195
+ case "nin": return `${e.field} NOT IN [${p(e.value).map(t).join(", ")}]`;
196
+ }
197
+ }, C = (e) => {
198
+ let t, n = async () => (t ||= v("meilisearch", "meilisearch", () => import("meilisearch")).then((t) => new t.MeiliSearch({
199
+ host: e.host ?? e.url ?? "http://localhost:7700",
200
+ apiKey: e.apiKey
201
+ })), t), r = (e) => e && e.length > 0 ? e.map(S) : void 0, i = async (e, t) => {
202
+ try {
203
+ return await t();
204
+ } catch (t) {
205
+ throw t instanceof f ? t : new f({
206
+ engine: "meilisearch",
207
+ op: e,
208
+ message: String(t?.message ?? t),
209
+ transient: !0
210
+ });
211
+ }
212
+ };
213
+ return {
214
+ upsert: (e, t) => i("upsert", async () => {
215
+ await (await n()).index(e).addDocuments(t);
216
+ }),
217
+ remove: (e, t) => i("remove", async () => {
218
+ await (await n()).index(e).deleteDocuments([...t]);
219
+ }),
220
+ query: (e, { q: t, limit: a, offset: o, filters: s, facets: c, highlight: l, fuzziness: u, engineParams: d }) => i("query", async () => {
221
+ let i = {
222
+ limit: a ?? 20,
223
+ ...o ? { offset: o } : {},
224
+ ...r(s) ? { filter: r(s) } : {},
225
+ ...c && c.length > 0 ? { facets: [...c] } : {},
226
+ ...l ? {
227
+ attributesToHighlight: l.fields && l.fields.length > 0 ? [...l.fields] : ["*"],
228
+ ...l.preTag ? { highlightPreTag: l.preTag } : {},
229
+ ...l.postTag ? { highlightPostTag: l.postTag } : {}
230
+ } : {},
231
+ ...u === 0 ? {
232
+ rankingScoreThreshold: void 0,
233
+ typoTolerance: { enabled: !1 }
234
+ } : {},
235
+ ...d ?? {}
236
+ };
237
+ return w(await (await n()).index(e).search(t, i), l);
238
+ })
239
+ };
240
+ }, w = (e, t) => ({
241
+ hits: e.hits.map((e) => {
242
+ let { _formatted: n, ...r } = e;
243
+ if (t && n) {
244
+ let e = {};
245
+ for (let [t, r] of Object.entries(n)) t !== "id" && (e[t] = String(r));
246
+ return Object.keys(e).length > 0 ? {
247
+ doc: r,
248
+ highlights: e
249
+ } : { doc: r };
250
+ }
251
+ return { doc: r };
252
+ }),
253
+ facets: e.facetDistribution ?? {}
254
+ }), T = (e) => {
255
+ let t = [], n = [];
256
+ for (let r of e ?? []) switch (r.op) {
257
+ case "eq":
258
+ t.push(`${r.field}:${r.value}`);
259
+ break;
260
+ case "neq":
261
+ t.push(`${r.field}:-${r.value}`);
262
+ break;
263
+ case "in":
264
+ t.push(p(r.value).map((e) => `${r.field}:${e}`));
265
+ break;
266
+ case "nin":
267
+ for (let e of p(r.value)) t.push(`${r.field}:-${e}`);
268
+ break;
269
+ case "gt":
270
+ n.push(`${r.field} > ${r.value}`);
271
+ break;
272
+ case "gte":
273
+ n.push(`${r.field} >= ${r.value}`);
274
+ break;
275
+ case "lt":
276
+ n.push(`${r.field} < ${r.value}`);
277
+ break;
278
+ case "lte":
279
+ n.push(`${r.field} <= ${r.value}`);
280
+ break;
281
+ }
282
+ return {
283
+ facetFilters: t,
284
+ numericFilters: n
285
+ };
286
+ }, E = (e) => {
287
+ let t, n = async () => (t ||= v("algoliasearch", "algolia", () => import("algoliasearch")).then((t) => t.algoliasearch(e.appId ?? "", e.apiKey)), t), r = async (e, t) => {
288
+ try {
289
+ return await t();
290
+ } catch (t) {
291
+ throw t instanceof f ? t : new f({
292
+ engine: "algolia",
293
+ op: e,
294
+ message: String(t?.message ?? t),
295
+ transient: !0
296
+ });
297
+ }
298
+ };
299
+ return {
300
+ upsert: (e, t) => r("upsert", async () => {
301
+ await (await n()).saveObjects({
302
+ indexName: e,
303
+ objects: t.map((e) => ({
304
+ ...e,
305
+ objectID: e.id
306
+ }))
307
+ });
308
+ }),
309
+ remove: (e, t) => r("remove", async () => {
310
+ await (await n()).deleteObjects({
311
+ indexName: e,
312
+ objectIDs: [...t]
313
+ });
314
+ }),
315
+ query: (e, { q: t, limit: i, offset: a, filters: o, facets: s, highlight: c, fuzziness: l, engineParams: u }) => r("query", async () => {
316
+ let { facetFilters: r, numericFilters: d } = T(o), f = {
317
+ query: t,
318
+ ...a ? {
319
+ offset: a,
320
+ length: i ?? 20
321
+ } : { hitsPerPage: i ?? 20 },
322
+ ...r.length > 0 ? { facetFilters: r } : {},
323
+ ...d.length > 0 ? { numericFilters: d } : {},
324
+ ...s && s.length > 0 ? { facets: [...s] } : {},
325
+ ...c ? {
326
+ ...c.fields && c.fields.length > 0 ? { attributesToHighlight: [...c.fields] } : {},
327
+ ...c.preTag ? { highlightPreTag: c.preTag } : {},
328
+ ...c.postTag ? { highlightPostTag: c.postTag } : {}
329
+ } : {},
330
+ ...l === 0 ? { typoTolerance: !1 } : {},
331
+ ...u ?? {}
332
+ };
333
+ return D(await (await n()).searchSingleIndex({
334
+ indexName: e,
335
+ searchParams: f
336
+ }), c);
337
+ })
338
+ };
339
+ }, D = (e, t) => ({
340
+ hits: e.hits.map((e) => {
341
+ let { _highlightResult: n, ...r } = e;
342
+ if (t && n) {
343
+ let e = {};
344
+ for (let [t, r] of Object.entries(n)) t !== "id" && r?.value && (e[t] = r.value);
345
+ return Object.keys(e).length > 0 ? {
346
+ doc: r,
347
+ highlights: e
348
+ } : { doc: r };
349
+ }
350
+ return { doc: r };
351
+ }),
352
+ facets: e.facets ?? {}
353
+ }), O = "_voltro_search_stats", k = () => ({
354
+ synced: 0,
355
+ removed: 0,
356
+ lastReindexAt: null,
357
+ lastReindexCount: 0
358
+ }), A = l(O, {
359
+ id: s({ prefix: "sstat" }),
360
+ indexName: u().maxLength(191),
361
+ replicaId: u().maxLength(191),
362
+ synced: c().default(0),
363
+ removed: c().default(0),
364
+ lastReindexAt: d().nullable(),
365
+ lastReindexCount: c().default(0),
366
+ updatedAt: d()
367
+ }).unique("bySearchStatsIndexReplica", ["indexName", "replicaId"]).index("bySearchStatsIndex", ["indexName"]), j = () => [A], M = () => {
368
+ let e = /* @__PURE__ */ new Map(), t = (t) => e.get(t) ?? k();
369
+ return {
370
+ bump: async (n, r) => {
371
+ let i = t(n);
372
+ e.set(n, r === "synced" ? {
373
+ ...i,
374
+ synced: i.synced + 1
375
+ } : {
376
+ ...i,
377
+ removed: i.removed + 1
378
+ });
379
+ },
380
+ recordReindex: async (n, r, i) => {
381
+ let a = t(n);
382
+ e.set(n, {
383
+ ...a,
384
+ synced: a.synced + r,
385
+ lastReindexAt: i,
386
+ lastReindexCount: r
387
+ });
388
+ },
389
+ get: async (e) => t(e)
390
+ };
391
+ }, N = (e) => ({
392
+ table: O,
393
+ predicate: e,
394
+ order: [],
395
+ take: void 0,
396
+ skip: void 0,
397
+ projection: void 0
398
+ }), P = () => `sstat_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, F = () => process.env.VOLTRO_REPLICA_ID ?? process.env.HOSTNAME ?? process.env.POD_NAME ?? `pid_${process.pid}_${Math.random().toString(36).slice(2, 8)}`, I = 32, L = (e, t = F()) => {
399
+ let n = async (n) => (await e.query(N(a(o("indexName", n), o("replicaId", t)))))[0] ?? null, r = async (e) => {
400
+ try {
401
+ return await e();
402
+ } catch (e) {
403
+ throw e instanceof f ? e : new f({
404
+ engine: "stats",
405
+ op: "upsert",
406
+ message: String(e?.message ?? e),
407
+ transient: !0
408
+ });
409
+ }
410
+ }, i = (i, s, c) => r(async () => {
411
+ for (let r = 0; r < I; r++) {
412
+ r > 0 && await new Promise((e) => setTimeout(e, r));
413
+ let l = await n(i);
414
+ if (l) {
415
+ let t = Number(l.synced ?? 0), n = Number(l.removed ?? 0), r = {
416
+ synced: t,
417
+ removed: n,
418
+ lastReindexAt: l.lastReindexAt ? new Date(l.lastReindexAt).toISOString() : null,
419
+ lastReindexCount: Number(l.lastReindexCount ?? 0)
420
+ };
421
+ if (await e.updateMany("_voltro_search_stats", {
422
+ ...s(r),
423
+ updatedAt: /* @__PURE__ */ new Date()
424
+ }, { where: a(o("id", String(l.id)), o("synced", t), o("removed", n)) }) === 1) return;
425
+ } else {
426
+ let n = P(), r = await e.insertIgnore(O, {
427
+ id: n,
428
+ indexName: i,
429
+ replicaId: t,
430
+ synced: 0,
431
+ removed: 0,
432
+ lastReindexAt: null,
433
+ lastReindexCount: 0,
434
+ updatedAt: /* @__PURE__ */ new Date(),
435
+ ...c
436
+ }, { conflictColumns: ["indexName", "replicaId"] });
437
+ if (String(r.id) === n) return;
438
+ }
439
+ }
440
+ throw new f({
441
+ engine: "stats",
442
+ op: "upsert",
443
+ message: `search stats: contention on (${i}, ${t}) exceeded ${I} attempts`,
444
+ transient: !0
445
+ });
446
+ });
447
+ return {
448
+ bump: (e, t) => i(e, (e) => t === "synced" ? { synced: e.synced + 1 } : { removed: e.removed + 1 }, t === "synced" ? { synced: 1 } : { removed: 1 }),
449
+ recordReindex: (e, t, n) => i(e, (e) => ({
450
+ synced: e.synced + t,
451
+ lastReindexAt: new Date(n),
452
+ lastReindexCount: t
453
+ }), {
454
+ synced: t,
455
+ lastReindexAt: new Date(n),
456
+ lastReindexCount: t
457
+ }),
458
+ get: (t) => r(async () => {
459
+ let n = await e.query(N(o("indexName", t))), r = k(), i = 0, a = 0, s = null, c = 0;
460
+ for (let e of n) {
461
+ i += Number(e.synced ?? 0), a += Number(e.removed ?? 0);
462
+ let t = e.lastReindexAt ? new Date(e.lastReindexAt).toISOString() : null;
463
+ t && (s === null || t > s) && (s = t, c = Number(e.lastReindexCount ?? 0));
464
+ }
465
+ return {
466
+ ...r,
467
+ synced: i,
468
+ removed: a,
469
+ lastReindexAt: s,
470
+ lastReindexCount: c
471
+ };
472
+ })
473
+ };
474
+ }, R = (e) => {
475
+ if (e === void 0 || e === "memory") return g();
476
+ if (typeof e.query == "function") return e;
477
+ let t = e;
478
+ return t.engine === "typesense" ? b(t) : t.engine === "meilisearch" ? C(t) : t.engine === "algolia" ? E(t) : g();
479
+ }, z = async (e, t, n) => {
480
+ let r = n.map((e) => t.map(e));
481
+ return r.length > 0 && await e.upsert(t.index, r), r.length;
482
+ }, B = async (e, t, n) => {
483
+ if (n.op === "delete") {
484
+ let r = n.old?.id;
485
+ typeof r == "string" && await e.remove(t.index, [r]);
486
+ return;
487
+ }
488
+ n.new && await e.upsert(t.index, [t.map(n.new)]);
489
+ }, V = (e) => {
490
+ if (!e || e === "memory" || typeof e.query == "function") return;
491
+ let t = e;
492
+ try {
493
+ return t.url || t.host ? new URL(t.url ?? t.host).host : void 0;
494
+ } catch {
495
+ return;
496
+ }
497
+ }, H = (e, t, n) => {
498
+ let r = n ?? [];
499
+ return e ? t == null ? null : [...r, {
500
+ field: e,
501
+ op: "eq",
502
+ value: t
503
+ }] : r;
504
+ }, U = (n) => {
505
+ let a = n.name ? `@voltro/plugin-search#${n.name}` : "@voltro/plugin-search", o = R(n.backend), s = /* @__PURE__ */ new Map();
506
+ for (let e of Object.values(n.indexes)) s.set(e.index, e);
507
+ let c = {
508
+ ...e,
509
+ description: "Full-text search an index (facets/highlight/fuzzy/filters), auto-scoped to the caller tenant.",
510
+ execute: (e, t) => r.promise(async () => {
511
+ let { index: n, q: r, limit: i, offset: a, filters: c, facets: l, highlight: u, fuzziness: d, engineParams: f } = e, p = s.get(n), m = t.request.subject.tenantId ?? null, h = H(p?.tenantField, m, c);
512
+ if (h === null) return {
513
+ hits: [],
514
+ facets: {}
515
+ };
516
+ let g = {
517
+ q: r,
518
+ ...i === void 0 ? {} : { limit: i },
519
+ ...a === void 0 ? {} : { offset: a },
520
+ ...h.length > 0 ? { filters: h } : {},
521
+ ...l && l.length > 0 ? { facets: l } : {},
522
+ ...u ? { highlight: u } : {},
523
+ ...d === void 0 ? {} : { fuzziness: d },
524
+ ...f ? { engineParams: f } : {}
525
+ };
526
+ return o.query(n, g);
527
+ })
528
+ }, l = V(n.backend), u = [
529
+ "store:changes:read",
530
+ "store:write",
531
+ "inspect:read",
532
+ ...l ? [`network:outbound:${l}`] : []
533
+ ], d = () => void 0, f = M(), p, m = (e) => ({
534
+ kind: "json",
535
+ data: e
536
+ }), h = (e, t) => ({
537
+ kind: "json",
538
+ status: e,
539
+ data: { error: t }
540
+ });
541
+ return i({
542
+ name: a,
543
+ description: "External search index sync (Typesense/Meilisearch/Algolia/memory) via the ChangeEvent tap + a tenant-scoped query route.",
544
+ permissions: u,
545
+ routes: [c],
546
+ rpcClientDescriptors: t,
547
+ inspectEndpoints: [{
548
+ method: "GET",
549
+ path: "/indexes",
550
+ description: "Configured indexes + durable, cross-replica sync stats.",
551
+ handler: () => r.promise(async () => {
552
+ let e = await Promise.all(Object.entries(n.indexes).map(async ([e, t]) => {
553
+ let n = await f.get(t.index);
554
+ return {
555
+ table: e,
556
+ index: t.index,
557
+ tenantField: t.tenantField ?? null,
558
+ ...n
559
+ };
560
+ }));
561
+ return m({
562
+ indexes: e,
563
+ backend: typeof n.backend == "string" ? n.backend : n.backend ? "custom/vendor" : "memory"
564
+ });
565
+ })
566
+ }, {
567
+ method: "POST",
568
+ path: "/reindex",
569
+ description: "Re-seed an index from the table's current rows.",
570
+ handler: (e) => r.promise(async () => {
571
+ let t;
572
+ try {
573
+ t = JSON.parse(e.body || "{}");
574
+ } catch {
575
+ return h(400, "invalid JSON body");
576
+ }
577
+ let r = t.table ? n.indexes[t.table] : void 0;
578
+ if (!t.table || !r) return h(404, `no index configured for table "${t.table ?? ""}"`);
579
+ if (!p) return h(503, "data store not bound yet");
580
+ try {
581
+ let e = await p.query({
582
+ table: t.table,
583
+ predicate: void 0,
584
+ order: [],
585
+ take: void 0,
586
+ skip: void 0,
587
+ projection: void 0
588
+ }), n = await z(o, r, e);
589
+ return await f.recordReindex(r.index, n, (/* @__PURE__ */ new Date()).toISOString()), m({
590
+ ok: !0,
591
+ table: t.table,
592
+ index: r.index,
593
+ count: n
594
+ });
595
+ } catch (e) {
596
+ return h(500, e.message);
597
+ }
598
+ })
599
+ }],
600
+ extendSchema: { tables: [...j()] },
601
+ bindDataStore: (e) => {
602
+ p = e, f = L(e);
603
+ },
604
+ onChangeEvent: (e) => {
605
+ let t = n.indexes[e.table];
606
+ if (!t) return r.void;
607
+ let i = r.tryPromise(() => f.bump(t.index, e.op === "delete" ? "removed" : "synced")).pipe(r.catchAll((t) => r.sync(() => d("search stats bump failed", {
608
+ table: e.table,
609
+ op: e.op,
610
+ error: String(t)
611
+ })))), a = r.tryPromise(() => B(o, t, e));
612
+ return r.zipRight(i, a);
613
+ },
614
+ onActivate: (e) => r.sync(() => {
615
+ d = (t, n) => e.logger.warn(t, n), e.logger.info("search active", {
616
+ indexes: Object.keys(n.indexes),
617
+ backend: typeof n.backend == "string" ? n.backend : "custom/vendor"
618
+ });
619
+ })
620
+ });
621
+ };
622
+ //#endregion
623
+ export { O as SEARCH_STATS_TABLE, f as SearchBackendError, E as algoliaBackend, B as applyChange, z as backfillIndex, L as dataStoreStatsStore, C as meilisearchBackend, g as memoryBackend, M as memoryStatsStore, H as scopeSearchFilters, U as searchPlugin, A as searchStatsTable, j as searchTables, b as typesenseBackend };
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { ActionProcedureDescriptor } from '@voltro/protocol';
2
+ import { PluginRpcClientDescriptor } from '@voltro/protocol';
3
+ import { Schema } from 'effect';
4
+
5
+ export declare const queryDescriptor: ActionProcedureDescriptor<"search.query", Schema.Struct<{
6
+ index: typeof Schema.String;
7
+ q: typeof Schema.String;
8
+ limit: Schema.optional<typeof Schema.Number>;
9
+ offset: Schema.optional<typeof Schema.Number>;
10
+ /** Clauses ANDed together — equality, range, negation, set membership. */
11
+ filters: Schema.optional<Schema.Array$<Schema.Struct<{
12
+ field: typeof Schema.String;
13
+ op: Schema.Literal<["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin"]>;
14
+ value: Schema.Union<[Schema.Union<[typeof Schema.String, typeof Schema.Number, typeof Schema.Boolean]>, Schema.Array$<Schema.Union<[typeof Schema.String, typeof Schema.Number, typeof Schema.Boolean]>>]>;
15
+ }>>>;
16
+ /** Facet-count fields. */
17
+ facets: Schema.optional<Schema.Array$<typeof Schema.String>>;
18
+ /** Matched-term highlighting. */
19
+ highlight: Schema.optional<Schema.Struct<{
20
+ fields: Schema.optional<Schema.Array$<typeof Schema.String>>;
21
+ preTag: Schema.optional<typeof Schema.String>;
22
+ postTag: Schema.optional<typeof Schema.String>;
23
+ }>>;
24
+ /** Typo tolerance — a max edit distance (0 = exact) or `'auto'`. */
25
+ fuzziness: Schema.optional<Schema.Union<[typeof Schema.Number, Schema.Literal<["auto"]>]>>;
26
+ /** Escape hatch — forwarded verbatim to the underlying engine's search call. */
27
+ engineParams: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
28
+ }>, Schema.Struct<{
29
+ hits: Schema.Array$<Schema.Struct<{
30
+ doc: Schema.Record$<typeof Schema.String, typeof Schema.Unknown>;
31
+ highlights: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.String>>;
32
+ }>>;
33
+ /** field → (value → count) for the requested facets. */
34
+ facets: Schema.Record$<typeof Schema.String, Schema.Record$<typeof Schema.String, typeof Schema.Number>>;
35
+ }>, typeof Schema.Never>;
36
+
37
+ export declare const SearchFilterSchema: Schema.Struct<{
38
+ field: typeof Schema.String;
39
+ op: Schema.Literal<["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin"]>;
40
+ value: Schema.Union<[Schema.Union<[typeof Schema.String, typeof Schema.Number, typeof Schema.Boolean]>, Schema.Array$<Schema.Union<[typeof Schema.String, typeof Schema.Number, typeof Schema.Boolean]>>]>;
41
+ }>;
42
+
43
+ /** Declared for the codegen (`VoltroPlugin.rpcClientDescriptors`) so each tag is
44
+ * emitted into the generated client group. Kept in lockstep with the exports. */
45
+ export declare const searchRpcClientImports: ReadonlyArray<PluginRpcClientDescriptor>;
46
+
47
+ export { }