@voltro/plugin-search 0.32.0 → 0.34.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 CHANGED
@@ -1,9 +1,127 @@
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) => {
1
+ import { a as e, i as t, n, r } from "./rpc-BQBNr0OU.js";
2
+ import { Data as i, Duration as a, Effect as o, Schedule as s } from "effect";
3
+ import { definePlugin as c, pluginInstanceName as l } from "@voltro/protocol";
4
+ import { and as u, count as d, eq as f, id as p, integer as m, table as h, text as g, timestamp as _ } from "@voltro/database";
5
+ //#region src/sanitize.ts
6
+ var v = /^[A-Za-z_][A-Za-z0-9_.]*$/, y = (e) => v.test(e), ee = (e, t) => {
7
+ let n = t ? new Set(t) : void 0;
8
+ for (let [t, r] of e) {
9
+ if (!y(r)) return {
10
+ field: r,
11
+ where: t,
12
+ reason: "not-an-identifier"
13
+ };
14
+ if (n && !n.has(r)) return {
15
+ field: r,
16
+ where: t,
17
+ reason: "not-queryable"
18
+ };
19
+ }
20
+ }, b = {
21
+ typesense: [
22
+ "per_page",
23
+ "page",
24
+ "offset",
25
+ "limit",
26
+ "sort_by",
27
+ "num_typos",
28
+ "typo_tokens_threshold",
29
+ "drop_tokens_threshold",
30
+ "prefix",
31
+ "prioritize_exact_match",
32
+ "highlight_start_tag",
33
+ "highlight_end_tag",
34
+ "highlight_affix_num_tokens",
35
+ "snippet_threshold",
36
+ "max_facet_values"
37
+ ],
38
+ meilisearch: [
39
+ "limit",
40
+ "offset",
41
+ "page",
42
+ "hitsPerPage",
43
+ "sort",
44
+ "matchingStrategy",
45
+ "showRankingScore",
46
+ "rankingScoreThreshold",
47
+ "distinct",
48
+ "attributesToHighlight",
49
+ "highlightPreTag",
50
+ "highlightPostTag",
51
+ "attributesToCrop",
52
+ "cropLength",
53
+ "cropMarker",
54
+ "showMatchesPosition"
55
+ ],
56
+ algolia: [
57
+ "hitsPerPage",
58
+ "page",
59
+ "offset",
60
+ "length",
61
+ "typoTolerance",
62
+ "minWordSizefor1Typo",
63
+ "minWordSizefor2Typos",
64
+ "ignorePlurals",
65
+ "removeStopWords",
66
+ "queryType",
67
+ "distinct",
68
+ "attributesToHighlight",
69
+ "attributesToSnippet",
70
+ "snippetEllipsisText",
71
+ "highlightPreTag",
72
+ "highlightPostTag"
73
+ ]
74
+ }, x = {
75
+ typesense: [
76
+ "filter_by",
77
+ "q",
78
+ "preset",
79
+ "pinned_hits",
80
+ "hidden_hits",
81
+ "enable_overrides",
82
+ "override_tags",
83
+ "include_fields",
84
+ "exclude_fields",
85
+ "vector_query"
86
+ ],
87
+ meilisearch: [
88
+ "filter",
89
+ "q",
90
+ "attributesToSearchOn",
91
+ "attributesToRetrieve",
92
+ "hybrid",
93
+ "vector",
94
+ "federation"
95
+ ],
96
+ algolia: [
97
+ "filters",
98
+ "facetFilters",
99
+ "numericFilters",
100
+ "tagFilters",
101
+ "optionalFilters",
102
+ "query",
103
+ "restrictSearchableAttributes",
104
+ "attributesToRetrieve",
105
+ "enableRules",
106
+ "ruleContexts",
107
+ "similarQuery",
108
+ "aroundLatLng",
109
+ "insideBoundingBox"
110
+ ]
111
+ }, S = (e, t, n) => {
112
+ if (!t) return {
113
+ params: {},
114
+ appAllowed: {},
115
+ dropped: []
116
+ };
117
+ let r = new Set(x[e]), i = new Set(b[e]), a = new Set(n ?? []), o = {}, s = {}, c = [];
118
+ for (let [e, n] of Object.entries(t)) r.has(e) ? c.push(e) : i.has(e) ? o[e] = n : a.has(e) ? s[e] = n : c.push(e);
119
+ return {
120
+ params: o,
121
+ appAllowed: s,
122
+ dropped: c
123
+ };
124
+ }, C = (e) => b[e], w = class extends i.TaggedError("SearchBackendError") {}, T = (e) => Array.isArray(e) ? e : [e], E = (e, t, n) => {
7
125
  if (Math.abs(e.length - t.length) > n) return !1;
8
126
  if (e === t) return !0;
9
127
  let r = t.length, i = Array.from({ length: r + 1 }, (e, t) => t);
@@ -19,7 +137,7 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
19
137
  i = o;
20
138
  }
21
139
  return (i[r] ?? Infinity) <= n;
22
- }, h = (e) => e.length <= 4 ? 1 : 2, g = () => {
140
+ }, D = (e) => e.length <= 4 ? 1 : 2, O = () => {
23
141
  let e = /* @__PURE__ */ new Map(), t = (t) => {
24
142
  let n = e.get(t);
25
143
  return n || (n = /* @__PURE__ */ new Map(), e.set(t, n)), n;
@@ -28,13 +146,13 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
28
146
  switch (t.op) {
29
147
  case "eq": return String(n) === String(t.value);
30
148
  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));
149
+ case "in": return T(t.value).some((e) => String(n) === String(e));
150
+ case "nin": return T(t.value).every((e) => String(n) !== String(e));
33
151
  case "gt":
34
152
  case "gte":
35
153
  case "lt":
36
154
  case "lte": {
37
- let [e, r] = [_(n), _(t.value)];
155
+ let [e, r] = [k(n), k(t.value)];
38
156
  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
157
  }
40
158
  }
@@ -44,8 +162,8 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
44
162
  let e = r.toLowerCase();
45
163
  if (e.includes(t)) return !0;
46
164
  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;
165
+ let r = n === "auto" ? D(t) : n;
166
+ for (let n of e.split(/\s+/)) if (E(n, t, r)) return !0;
49
167
  }
50
168
  }
51
169
  return !1;
@@ -56,6 +174,7 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
56
174
  return s;
57
175
  };
58
176
  return {
177
+ engineKind: "memory",
59
178
  upsert: async (e, n) => {
60
179
  let r = t(e);
61
180
  for (let e of n) r.set(e.id, e);
@@ -88,7 +207,7 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
88
207
  };
89
208
  }
90
209
  };
91
- }, _ = (e) => {
210
+ }, k = (e) => {
92
211
  if (typeof e == "number") return e;
93
212
  if (typeof e == "boolean") return +!!e;
94
213
  if (typeof e == "string") {
@@ -97,41 +216,58 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
97
216
  let n = Date.parse(e);
98
217
  return Number.isNaN(n) ? void 0 : n;
99
218
  }
100
- }, v = async (e, t, n) => {
219
+ }, A = async (e, t, n) => {
101
220
  try {
102
221
  return await n();
103
222
  } catch {
104
- throw new f({
223
+ throw new w({
105
224
  engine: t,
106
225
  op: "query",
107
226
  message: `the '${t}' search backend requires the '${e}' optional dependency. Install it to enable it (e.g. \`npm i ${e}\`).`,
108
227
  transient: !1
109
228
  });
110
229
  }
111
- }, y = (e) => {
112
- let t = (e) => JSON.stringify(e);
230
+ }, j = (e, t) => (n) => {
231
+ let { params: r, appAllowed: i, dropped: a } = S(e, n, t.allowEngineParams);
232
+ return a.length > 0 && t.onWarn?.("search dropped disallowed engineParams", {
233
+ engine: e,
234
+ keys: a
235
+ }), {
236
+ before: r,
237
+ after: i
238
+ };
239
+ }, M = (e, t) => {
240
+ if (!y(t)) throw new w({
241
+ engine: e,
242
+ op: "query",
243
+ message: `filter field "${t}" is not a plain field path (^[A-Za-z_][A-Za-z0-9_.]*$)`,
244
+ transient: !1
245
+ });
246
+ return t;
247
+ }, te = (e) => {
248
+ let t = (e) => JSON.stringify(e), n = M("typesense", e.field);
113
249
  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(",")}]`;
250
+ case "eq": return `${n}:=${t(e.value)}`;
251
+ case "neq": return `${n}:!=${t(e.value)}`;
252
+ case "gt": return `${n}:>${t(e.value)}`;
253
+ case "gte": return `${n}:>=${t(e.value)}`;
254
+ case "lt": return `${n}:<${t(e.value)}`;
255
+ case "lte": return `${n}:<=${t(e.value)}`;
256
+ case "in": return `${n}:[${T(e.value).map(t).join(",")}]`;
257
+ case "nin": return `${n}:!=[${T(e.value).map(t).join(",")}]`;
122
258
  }
123
- }, b = (e) => {
124
- let t, n = async () => (t ||= v("typesense", "typesense", () => import("typesense")).then((t) => {
259
+ }, ne = (e, t = {}) => {
260
+ let n = j("typesense", t), r, i = async () => (r ||= A("typesense", "typesense", () => import("typesense")).then((t) => {
125
261
  let n = t.Client;
126
262
  return new n({
127
263
  nodes: [{ url: e.url ?? e.host ?? "http://localhost:8108" }],
128
264
  apiKey: e.apiKey
129
265
  });
130
- }), t), r = (e) => e && e.length > 0 ? e.map(y).join(" && ") : void 0, i = async (e, t) => {
266
+ }), r), a = (e) => e && e.length > 0 ? e.map(te).join(" && ") : void 0, o = async (e, t) => {
131
267
  try {
132
268
  return await t();
133
269
  } catch (t) {
134
- throw t instanceof f ? t : new f({
270
+ throw t instanceof w ? t : new w({
135
271
  engine: "typesense",
136
272
  op: e,
137
273
  message: String(t?.message ?? t),
@@ -140,32 +276,33 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
140
276
  }
141
277
  };
142
278
  return {
143
- upsert: (e, t) => i("upsert", async () => {
144
- await (await n()).collections(e).documents().import(t, { action: "upsert" });
279
+ upsert: (e, t) => o("upsert", async () => {
280
+ await (await i()).collections(e).documents().import(t, { action: "upsert" });
145
281
  }),
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(",")}]` });
282
+ remove: (e, t) => o("remove", async () => {
283
+ await (await i()).collections(e).documents().delete({ filter_by: `id:[${t.map((e) => JSON.stringify(e)).join(",")}]` });
148
284
  }),
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 = {
285
+ query: (e, { q: t, limit: r, offset: s, filters: c, facets: l, highlight: u, fuzziness: d, engineParams: f }) => o("query", async () => {
286
+ let o = await i(), p = n(f), m = {
287
+ ...p.before,
151
288
  q: t || "*",
152
289
  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 } : {}
290
+ per_page: r ?? 20,
291
+ ...s ? { offset: s } : {},
292
+ ...a(c) ? { filter_by: a(c) } : {},
293
+ ...l && l.length > 0 ? { facet_by: l.join(",") } : {},
294
+ ...u ? {
295
+ ...u.fields && u.fields.length > 0 ? { highlight_fields: u.fields.join(",") } : { highlight_full_fields: "*" },
296
+ ...u.preTag ? { highlight_start_tag: u.preTag } : {},
297
+ ...u.postTag ? { highlight_end_tag: u.postTag } : {}
161
298
  } : {},
162
- ...u !== void 0 && u !== "auto" ? { num_typos: Math.min(2, Math.max(0, u)) } : {},
163
- ...d ?? {}
299
+ ...d !== void 0 && d !== "auto" ? { num_typos: Math.min(2, Math.max(0, d)) } : {},
300
+ ...p.after
164
301
  };
165
- return x(await i.collections(e).documents().search(f));
302
+ return re(await o.collections(e).documents().search(m));
166
303
  })
167
304
  };
168
- }, x = (e) => {
305
+ }, re = (e) => {
169
306
  let t = (e.hits ?? []).map((e) => {
170
307
  let t = {};
171
308
  for (let n of e.highlights ?? []) {
@@ -182,27 +319,27 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
182
319
  hits: t,
183
320
  facets: n
184
321
  };
185
- }, S = (e) => {
186
- let t = (e) => JSON.stringify(e);
322
+ }, ie = (e) => {
323
+ let t = (e) => JSON.stringify(e), n = M("meilisearch", e.field);
187
324
  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(", ")}]`;
325
+ case "eq": return `${n} = ${t(e.value)}`;
326
+ case "neq": return `${n} != ${t(e.value)}`;
327
+ case "gt": return `${n} > ${t(e.value)}`;
328
+ case "gte": return `${n} >= ${t(e.value)}`;
329
+ case "lt": return `${n} < ${t(e.value)}`;
330
+ case "lte": return `${n} <= ${t(e.value)}`;
331
+ case "in": return `${n} IN [${T(e.value).map(t).join(", ")}]`;
332
+ case "nin": return `${n} NOT IN [${T(e.value).map(t).join(", ")}]`;
196
333
  }
197
- }, C = (e) => {
198
- let t, n = async () => (t ||= v("meilisearch", "meilisearch", () => import("meilisearch")).then((t) => new t.MeiliSearch({
334
+ }, ae = (e, t = {}) => {
335
+ let n = j("meilisearch", t), r, i = async () => (r ||= A("meilisearch", "meilisearch", () => import("meilisearch")).then((t) => new t.MeiliSearch({
199
336
  host: e.host ?? e.url ?? "http://localhost:7700",
200
337
  apiKey: e.apiKey
201
- })), t), r = (e) => e && e.length > 0 ? e.map(S) : void 0, i = async (e, t) => {
338
+ })), r), a = (e) => e && e.length > 0 ? e.map(ie) : void 0, o = async (e, t) => {
202
339
  try {
203
340
  return await t();
204
341
  } catch (t) {
205
- throw t instanceof f ? t : new f({
342
+ throw t instanceof w ? t : new w({
206
343
  engine: "meilisearch",
207
344
  op: e,
208
345
  message: String(t?.message ?? t),
@@ -211,33 +348,34 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
211
348
  }
212
349
  };
213
350
  return {
214
- upsert: (e, t) => i("upsert", async () => {
215
- await (await n()).index(e).addDocuments(t);
351
+ upsert: (e, t) => o("upsert", async () => {
352
+ await (await i()).index(e).addDocuments(t);
216
353
  }),
217
- remove: (e, t) => i("remove", async () => {
218
- await (await n()).index(e).deleteDocuments([...t]);
354
+ remove: (e, t) => o("remove", async () => {
355
+ await (await i()).index(e).deleteDocuments([...t]);
219
356
  }),
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 } : {}
357
+ query: (e, { q: t, limit: r, offset: s, filters: c, facets: l, highlight: u, fuzziness: d, engineParams: f }) => o("query", async () => {
358
+ let o = n(f), p = {
359
+ ...o.before,
360
+ limit: r ?? 20,
361
+ ...s ? { offset: s } : {},
362
+ ...a(c) ? { filter: a(c) } : {},
363
+ ...l && l.length > 0 ? { facets: [...l] } : {},
364
+ ...u ? {
365
+ attributesToHighlight: u.fields && u.fields.length > 0 ? [...u.fields] : ["*"],
366
+ ...u.preTag ? { highlightPreTag: u.preTag } : {},
367
+ ...u.postTag ? { highlightPostTag: u.postTag } : {}
230
368
  } : {},
231
- ...u === 0 ? {
369
+ ...d === 0 ? {
232
370
  rankingScoreThreshold: void 0,
233
371
  typoTolerance: { enabled: !1 }
234
372
  } : {},
235
- ...d ?? {}
373
+ ...o.after
236
374
  };
237
- return w(await (await n()).index(e).search(t, i), l);
375
+ return oe(await (await i()).index(e).search(t, p), u);
238
376
  })
239
377
  };
240
- }, w = (e, t) => ({
378
+ }, oe = (e, t) => ({
241
379
  hits: e.hits.map((e) => {
242
380
  let { _formatted: n, ...r } = e;
243
381
  if (t && n) {
@@ -251,44 +389,56 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
251
389
  return { doc: r };
252
390
  }),
253
391
  facets: e.facetDistribution ?? {}
254
- }), T = (e) => {
392
+ }), se = (e) => String(e).replace(/^-/, "\\-"), ce = (e, t) => {
393
+ let n = typeof t == "boolean" ? +!!t : Number(t);
394
+ if (!Number.isFinite(n)) throw new w({
395
+ engine: "algolia",
396
+ op: "query",
397
+ message: `range filter on "${e}" needs a numeric value (got ${JSON.stringify(t)})`,
398
+ transient: !1
399
+ });
400
+ return n;
401
+ }, le = (e) => {
255
402
  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;
403
+ for (let r of e ?? []) {
404
+ let e = M("algolia", r.field), i = (e) => se(e), a = (t) => ce(e, t);
405
+ switch (r.op) {
406
+ case "eq":
407
+ t.push(`${e}:${i(r.value)}`);
408
+ break;
409
+ case "neq":
410
+ t.push(`${e}:-${i(r.value)}`);
411
+ break;
412
+ case "in":
413
+ t.push(T(r.value).map((t) => `${e}:${i(t)}`));
414
+ break;
415
+ case "nin":
416
+ for (let n of T(r.value)) t.push(`${e}:-${i(n)}`);
417
+ break;
418
+ case "gt":
419
+ n.push(`${e} > ${a(r.value)}`);
420
+ break;
421
+ case "gte":
422
+ n.push(`${e} >= ${a(r.value)}`);
423
+ break;
424
+ case "lt":
425
+ n.push(`${e} < ${a(r.value)}`);
426
+ break;
427
+ case "lte":
428
+ n.push(`${e} <= ${a(r.value)}`);
429
+ break;
430
+ }
281
431
  }
282
432
  return {
283
433
  facetFilters: t,
284
434
  numericFilters: n
285
435
  };
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) => {
436
+ }, N = (e, t = {}) => {
437
+ let n = j("algolia", t), r, i = async () => (r ||= A("algoliasearch", "algolia", () => import("algoliasearch")).then((t) => t.algoliasearch(e.appId ?? "", e.apiKey)), r), a = async (e, t) => {
288
438
  try {
289
439
  return await t();
290
440
  } catch (t) {
291
- throw t instanceof f ? t : new f({
441
+ throw t instanceof w ? t : new w({
292
442
  engine: "algolia",
293
443
  op: e,
294
444
  message: String(t?.message ?? t),
@@ -297,8 +447,8 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
297
447
  }
298
448
  };
299
449
  return {
300
- upsert: (e, t) => r("upsert", async () => {
301
- await (await n()).saveObjects({
450
+ upsert: (e, t) => a("upsert", async () => {
451
+ await (await i()).saveObjects({
302
452
  indexName: e,
303
453
  objects: t.map((e) => ({
304
454
  ...e,
@@ -306,37 +456,38 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
306
456
  }))
307
457
  });
308
458
  }),
309
- remove: (e, t) => r("remove", async () => {
310
- await (await n()).deleteObjects({
459
+ remove: (e, t) => a("remove", async () => {
460
+ await (await i()).deleteObjects({
311
461
  indexName: e,
312
462
  objectIDs: [...t]
313
463
  });
314
464
  }),
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 = {
465
+ query: (e, { q: t, limit: r, offset: o, filters: s, facets: c, highlight: l, fuzziness: u, engineParams: d }) => a("query", async () => {
466
+ let { facetFilters: a, numericFilters: f } = le(s), p = o ? {
467
+ offset: o,
468
+ length: r ?? 20
469
+ } : { hitsPerPage: r ?? 20 }, m = n(d), h = {
470
+ ...m.before,
317
471
  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 } : {}
472
+ ...p,
473
+ ...a.length > 0 ? { facetFilters: a } : {},
474
+ ...f.length > 0 ? { numericFilters: f } : {},
475
+ ...c && c.length > 0 ? { facets: [...c] } : {},
476
+ ...l ? {
477
+ ...l.fields && l.fields.length > 0 ? { attributesToHighlight: [...l.fields] } : {},
478
+ ...l.preTag ? { highlightPreTag: l.preTag } : {},
479
+ ...l.postTag ? { highlightPostTag: l.postTag } : {}
329
480
  } : {},
330
- ...l === 0 ? { typoTolerance: !1 } : {},
331
- ...u ?? {}
481
+ ...u === 0 ? { typoTolerance: !1 } : {},
482
+ ...m.after
332
483
  };
333
- return D(await (await n()).searchSingleIndex({
484
+ return ue(await (await i()).searchSingleIndex({
334
485
  indexName: e,
335
- searchParams: f
336
- }), c);
486
+ searchParams: h
487
+ }), l);
337
488
  })
338
489
  };
339
- }, D = (e, t) => ({
490
+ }, ue = (e, t) => ({
340
491
  hits: e.hits.map((e) => {
341
492
  let { _highlightResult: n, ...r } = e;
342
493
  if (t && n) {
@@ -350,31 +501,37 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
350
501
  return { doc: r };
351
502
  }),
352
503
  facets: e.facets ?? {}
353
- }), O = "_voltro_search_stats", k = () => ({
504
+ }), P = "_voltro_search_stats", F = () => ({
354
505
  synced: 0,
355
506
  removed: 0,
507
+ dropped: 0,
356
508
  lastReindexAt: null,
357
509
  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();
510
+ }), I = h(P, {
511
+ id: p({ prefix: "sstat" }),
512
+ indexName: g().maxLength(191),
513
+ replicaId: g().maxLength(191),
514
+ synced: m().default(0),
515
+ removed: m().default(0),
516
+ dropped: m().default(0),
517
+ scope: g().maxLength(8).default("local"),
518
+ lastReindexAt: _().nullable(),
519
+ lastReindexCount: m().default(0),
520
+ updatedAt: _()
521
+ }).unique("bySearchStatsIndexReplica", ["indexName", "replicaId"]).index("bySearchStatsIndex", ["indexName"]), L = () => [I], R = () => {
522
+ let e = /* @__PURE__ */ new Map(), t = (t) => e.get(t) ?? F();
369
523
  return {
370
524
  bump: async (n, r) => {
371
525
  let i = t(n);
372
526
  e.set(n, r === "synced" ? {
373
527
  ...i,
374
528
  synced: i.synced + 1
375
- } : {
529
+ } : r === "removed" ? {
376
530
  ...i,
377
531
  removed: i.removed + 1
532
+ } : {
533
+ ...i,
534
+ dropped: i.dropped + 1
378
535
  });
379
536
  },
380
537
  recordReindex: async (n, r, i) => {
@@ -388,65 +545,69 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
388
545
  },
389
546
  get: async (e) => t(e)
390
547
  };
391
- }, N = (e) => ({
392
- table: O,
548
+ }, z = (e) => ({
549
+ table: P,
393
550
  predicate: e,
394
551
  order: [],
395
552
  take: void 0,
396
553
  skip: void 0,
397
554
  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) => {
555
+ }), de = () => `sstat_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, fe = () => process.env.VOLTRO_REPLICA_ID ?? process.env.HOSTNAME ?? process.env.POD_NAME ?? `pid_${process.pid}_${Math.random().toString(36).slice(2, 8)}`, B = 32, V = (e, t = fe()) => {
556
+ let n = async (n) => (await e.query(z(u(f("indexName", n), f("replicaId", t)))))[0] ?? null, r = async (e) => {
400
557
  try {
401
558
  return await e();
402
559
  } catch (e) {
403
- throw e instanceof f ? e : new f({
560
+ throw e instanceof w ? e : new w({
404
561
  engine: "stats",
405
562
  op: "upsert",
406
563
  message: String(e?.message ?? e),
407
564
  transient: !0
408
565
  });
409
566
  }
410
- }, i = (i, s, c) => r(async () => {
411
- for (let r = 0; r < I; r++) {
567
+ }, i = (i, a, o, s) => r(async () => {
568
+ for (let r = 0; r < B; r++) {
412
569
  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 = {
570
+ let c = await n(i);
571
+ if (c) {
572
+ let t = Number(c.synced ?? 0), n = Number(c.removed ?? 0), r = Number(c.dropped ?? 0), i = {
416
573
  synced: t,
417
574
  removed: n,
418
- lastReindexAt: l.lastReindexAt ? new Date(l.lastReindexAt).toISOString() : null,
419
- lastReindexCount: Number(l.lastReindexCount ?? 0)
575
+ dropped: r,
576
+ lastReindexAt: c.lastReindexAt ? new Date(c.lastReindexAt).toISOString() : null,
577
+ lastReindexCount: Number(c.lastReindexCount ?? 0)
420
578
  };
421
579
  if (await e.updateMany("_voltro_search_stats", {
422
- ...s(r),
580
+ ...o(i),
581
+ ...a ? { scope: a } : {},
423
582
  updatedAt: /* @__PURE__ */ new Date()
424
- }, { where: a(o("id", String(l.id)), o("synced", t), o("removed", n)) }) === 1) return;
583
+ }, { where: u(f("id", String(c.id)), f("synced", t), f("removed", n), f("dropped", r)) }) === 1) return;
425
584
  } else {
426
- let n = P(), r = await e.insertIgnore(O, {
585
+ let n = de(), r = await e.insertIgnore(P, {
427
586
  id: n,
428
587
  indexName: i,
429
588
  replicaId: t,
430
589
  synced: 0,
431
590
  removed: 0,
591
+ dropped: 0,
592
+ scope: a ?? "local",
432
593
  lastReindexAt: null,
433
594
  lastReindexCount: 0,
434
595
  updatedAt: /* @__PURE__ */ new Date(),
435
- ...c
596
+ ...s
436
597
  }, { conflictColumns: ["indexName", "replicaId"] });
437
598
  if (String(r.id) === n) return;
438
599
  }
439
600
  }
440
- throw new f({
601
+ throw new w({
441
602
  engine: "stats",
442
603
  op: "upsert",
443
- message: `search stats: contention on (${i}, ${t}) exceeded ${I} attempts`,
604
+ message: `search stats: contention on (${i}, ${t}) exceeded ${B} attempts`,
444
605
  transient: !0
445
606
  });
446
607
  });
447
608
  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) => ({
609
+ bump: (e, t, n) => i(e, n, (e) => t === "synced" ? { synced: e.synced + 1 } : t === "removed" ? { removed: e.removed + 1 } : { dropped: e.dropped + 1 }, t === "synced" ? { synced: 1 } : t === "removed" ? { removed: 1 } : { dropped: 1 }),
610
+ recordReindex: (e, t, n) => i(e, void 0, (e) => ({
450
611
  synced: e.synced + t,
451
612
  lastReindexAt: new Date(n),
452
613
  lastReindexCount: t
@@ -456,37 +617,211 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
456
617
  lastReindexCount: t
457
618
  }),
458
619
  get: (t) => r(async () => {
459
- let n = await e.query(N(o("indexName", t))), r = k(), i = 0, a = 0, s = null, c = 0;
620
+ let n = await e.query(z(f("indexName", t))), r = n.some((e) => e.scope === "fleet"), i = (e) => {
621
+ let t = 0;
622
+ for (let i of n) {
623
+ let n = Number(i[e] ?? 0);
624
+ t = r ? Math.max(t, n) : t + n;
625
+ }
626
+ return t;
627
+ }, a = null, o = 0;
460
628
  for (let e of n) {
461
- i += Number(e.synced ?? 0), a += Number(e.removed ?? 0);
462
629
  let t = e.lastReindexAt ? new Date(e.lastReindexAt).toISOString() : null;
463
- t && (s === null || t > s) && (s = t, c = Number(e.lastReindexCount ?? 0));
630
+ t && (a === null || t > a) && (a = t, o = Number(e.lastReindexCount ?? 0));
464
631
  }
465
632
  return {
466
- ...r,
467
- synced: i,
468
- removed: a,
469
- lastReindexAt: s,
470
- lastReindexCount: c
633
+ ...F(),
634
+ synced: i("synced"),
635
+ removed: i("removed"),
636
+ dropped: i("dropped"),
637
+ lastReindexAt: a,
638
+ lastReindexCount: o
471
639
  };
472
640
  })
473
641
  };
474
- }, R = (e) => {
475
- if (e === void 0 || e === "memory") return g();
642
+ }, H = (e) => e === void 0 || e === "memory" ? "memory" : typeof e.query == "function" ? e.engineKind === "memory" ? "memory" : "custom" : "vendor", U = class extends Error {
643
+ _tag = "SearchBackendNotDurable";
644
+ constructor(e) {
645
+ super(e), this.name = "SearchBackendNotDurable";
646
+ }
647
+ }, pe = [
648
+ "plugin-search refuses to boot in production on the in-memory backend.",
649
+ "",
650
+ "The memory index lives in THIS process's heap. Two consequences, both silent:",
651
+ " • every replica holds a different index, so a result depends on which replica served you;",
652
+ " • the index starts EMPTY after every restart/deploy, and nothing re-seeds it automatically.",
653
+ "",
654
+ "Configure a durable engine in app.config.ts:",
655
+ " searchPlugin({ backend: { engine: 'typesense', url: …, apiKey: … }, indexes })",
656
+ " searchPlugin({ backend: { engine: 'meilisearch', url: …, apiKey: … }, indexes })",
657
+ " searchPlugin({ backend: { engine: 'algolia', appId: …, apiKey: … }, indexes })",
658
+ "or pass your own `SearchBackend` implementation.",
659
+ "",
660
+ "If this deployment really is ONE process that re-seeds its index at startup",
661
+ "(a `*.startup.tsx` calling `backfillIndex`), say so explicitly:",
662
+ " searchPlugin({ singleProcessMemoryIndex: true, indexes })",
663
+ "That is a claim about your topology, not a mute switch: if a second replica",
664
+ "ever announces itself, the plugin logs that the claim was contradicted."
665
+ ].join("\n"), me = [
666
+ "plugin-search is running the in-memory backend in production under",
667
+ "`singleProcessMemoryIndex: true`. Holding you to the two conditions that makes true:",
668
+ "exactly ONE process serves search, and it re-seeds every index at startup",
669
+ "(`backfillIndex`) — the index is empty after each restart until it does."
670
+ ].join(" "), W = (e) => e.backendKind !== "memory" || !e.production ? { level: "quiet" } : e.singleProcessDeclared ? {
671
+ level: "note",
672
+ message: me
673
+ } : {
674
+ level: "refuse",
675
+ message: pe
676
+ }, G = (e, t) => t ? `search: replica "${e}" joined, but this app declared \`singleProcessMemoryIndex: true\`. That declaration is now false: each replica has its own in-memory index, so search results depend on which one serves the request. Configure a durable backend (typesense / meilisearch / algolia) or run one process.` : `search: replica "${e}" joined while search is served from an in-memory index. Each replica indexes only its OWN writes, so results depend on which replica answers. Configure a durable backend (typesense / meilisearch / algolia) before relying on these results.`, K = "_voltro_search_drift", q = h(K, {
677
+ id: p({ prefix: "sdrft" }),
678
+ indexName: g().maxLength(191),
679
+ sourceTable: g().maxLength(191),
680
+ rowId: g().maxLength(191),
681
+ docId: g().maxLength(191),
682
+ op: g().maxLength(16),
683
+ attempts: m().default(0),
684
+ lastError: g().maxLength(1e3),
685
+ firstFailedAt: _(),
686
+ lastFailedAt: _()
687
+ }).unique("bySearchDriftIndexRow", ["indexName", "rowId"]).index("bySearchDriftIndex", ["indexName"]), J = (e, t) => ({
688
+ table: e,
689
+ predicate: f("id", t),
690
+ order: [],
691
+ take: 1,
692
+ skip: void 0,
693
+ projection: void 0
694
+ }), Y = (e) => e ? new Date(e).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString(), he = (e) => ({
695
+ indexName: String(e.indexName),
696
+ sourceTable: String(e.sourceTable),
697
+ rowId: String(e.rowId),
698
+ docId: String(e.docId),
699
+ op: e.op === "remove" ? "remove" : "upsert",
700
+ attempts: Number(e.attempts ?? 0),
701
+ lastError: String(e.lastError ?? ""),
702
+ firstFailedAt: Y(e.firstFailedAt),
703
+ lastFailedAt: Y(e.lastFailedAt)
704
+ }), ge = () => {
705
+ let e = /* @__PURE__ */ new Map(), t = (e, t) => `${e}\u0000${t}`;
706
+ return {
707
+ record: async (n) => {
708
+ let r = (/* @__PURE__ */ new Date()).toISOString(), i = e.get(t(n.indexName, n.rowId));
709
+ e.set(t(n.indexName, n.rowId), {
710
+ indexName: n.indexName,
711
+ sourceTable: n.sourceTable,
712
+ rowId: n.rowId,
713
+ docId: n.docId,
714
+ op: n.op,
715
+ attempts: (i?.attempts ?? 0) + 1,
716
+ lastError: n.error,
717
+ firstFailedAt: i?.firstFailedAt ?? r,
718
+ lastFailedAt: r
719
+ });
720
+ },
721
+ list: async (t) => [...e.values()].sort((e, t) => e.firstFailedAt.localeCompare(t.firstFailedAt)).slice(0, t),
722
+ summary: async (t) => {
723
+ let n = [...e.values()].filter((e) => e.indexName === t), r = null;
724
+ for (let e of n) (r === null || e.lastFailedAt > r) && (r = e.lastFailedAt);
725
+ return {
726
+ pending: n.length,
727
+ lastFailedAt: r
728
+ };
729
+ },
730
+ clear: async (n, r) => {
731
+ e.delete(t(n, r));
732
+ }
733
+ };
734
+ }, _e = () => `sdrft_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, X = (e, t) => ({
735
+ table: K,
736
+ predicate: e,
737
+ order: [{
738
+ column: "firstFailedAt",
739
+ direction: "asc"
740
+ }],
741
+ take: t,
742
+ skip: void 0,
743
+ projection: void 0
744
+ }), ve = (e) => ({
745
+ record: async (t) => {
746
+ let n = /* @__PURE__ */ new Date(), r = _e(), i = await e.insertIgnore(K, {
747
+ id: r,
748
+ indexName: t.indexName,
749
+ sourceTable: t.sourceTable,
750
+ rowId: t.rowId,
751
+ docId: t.docId,
752
+ op: t.op,
753
+ attempts: 1,
754
+ lastError: t.error.slice(0, 1e3),
755
+ firstFailedAt: n,
756
+ lastFailedAt: n
757
+ }, { conflictColumns: ["indexName", "rowId"] });
758
+ String(i.id) !== r && await e.updateMany(K, {
759
+ attempts: Number(i.attempts ?? 0) + 1,
760
+ op: t.op,
761
+ docId: t.docId,
762
+ lastError: t.error.slice(0, 1e3),
763
+ lastFailedAt: n
764
+ }, { where: f("id", String(i.id)) });
765
+ },
766
+ list: async (t) => (await e.query(X(void 0, t))).map(he),
767
+ summary: async (t) => {
768
+ let n = (await e.query({
769
+ ...X(f("indexName", t), void 0),
770
+ order: [],
771
+ aggregations: [d("n")]
772
+ }))[0], r = Number(n?.n ?? n?.count ?? 0);
773
+ if (r === 0) return {
774
+ pending: 0,
775
+ lastFailedAt: null
776
+ };
777
+ let i = await e.query({
778
+ ...X(f("indexName", t), 1),
779
+ order: [{
780
+ column: "lastFailedAt",
781
+ direction: "desc"
782
+ }]
783
+ });
784
+ return {
785
+ pending: r,
786
+ lastFailedAt: i[0] ? Y(i[0].lastFailedAt) : null
787
+ };
788
+ },
789
+ clear: async (t, n) => {
790
+ await e.deleteMany(K, { where: u(f("indexName", t), f("rowId", n)) });
791
+ }
792
+ }), ye = () => [q], be = (e, t) => {
793
+ if (e === void 0 || e === "memory") return O();
476
794
  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) => {
795
+ let n = e;
796
+ return n.engine === "typesense" ? ne(n, t) : n.engine === "meilisearch" ? ae(n, t) : n.engine === "algolia" ? N(n, t) : O();
797
+ }, xe = async (e, t, n) => {
480
798
  let r = n.map((e) => t.map(e));
481
799
  return r.length > 0 && await e.upsert(t.index, r), r.length;
482
- }, B = async (e, t, n) => {
800
+ }, Se = async (e, t, n) => {
483
801
  if (n.op === "delete") {
484
802
  let r = n.old?.id;
485
803
  typeof r == "string" && await e.remove(t.index, [r]);
486
804
  return;
487
805
  }
488
806
  n.new && await e.upsert(t.index, [t.map(n.new)]);
489
- }, V = (e) => {
807
+ }, Z = {
808
+ retries: 5,
809
+ retryBaseDelayMs: 200,
810
+ retryMaxDelayMs: 1e4,
811
+ resyncIntervalMs: 6e4,
812
+ resyncBatchSize: 200
813
+ }, Ce = (e) => ({
814
+ retries: Math.max(0, e?.retries ?? Z.retries),
815
+ retryBaseDelayMs: Math.max(1, e?.retryBaseDelayMs ?? Z.retryBaseDelayMs),
816
+ retryMaxDelayMs: Math.max(1, e?.retryMaxDelayMs ?? Z.retryMaxDelayMs),
817
+ resyncIntervalMs: Math.max(0, e?.resyncIntervalMs ?? Z.resyncIntervalMs),
818
+ resyncBatchSize: Math.max(1, e?.resyncBatchSize ?? Z.resyncBatchSize)
819
+ }), we = (e) => e instanceof w ? e : new w({
820
+ engine: "unknown",
821
+ op: "upsert",
822
+ message: String(e?.message ?? e),
823
+ transient: !1
824
+ }), Q = (e) => s.exponential(a.millis(e.retryBaseDelayMs), 2).pipe(s.either(s.spaced(a.millis(e.retryMaxDelayMs))), s.intersect(s.recurs(e.retries)), s.whileInput((e) => e.transient)), Te = (e) => {
490
825
  if (!e || e === "memory" || typeof e.query == "function") return;
491
826
  let t = e;
492
827
  try {
@@ -494,131 +829,282 @@ var f = class extends n.TaggedError("SearchBackendError") {}, p = (e) => Array.i
494
829
  } catch {
495
830
  return;
496
831
  }
497
- }, H = (e, t, n) => {
832
+ }, Ee = (e, t, n) => {
498
833
  let r = n ?? [];
499
834
  return e ? t == null ? null : [...r, {
500
835
  field: e,
501
836
  op: "eq",
502
837
  value: t
503
838
  }] : 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,
839
+ }, De = (e) => [
840
+ ...(e.filters ?? []).map((e) => ["filter", e.field]),
841
+ ...(e.facets ?? []).map((e) => ["facet", e]),
842
+ ...(e.highlight?.fields ?? []).map((e) => ["highlight", e])
843
+ ], $ = "@voltro/plugin-search", Oe = (i) => {
844
+ let a = l({
845
+ base: $,
846
+ alias: i.alias,
847
+ instance: i.name
848
+ }), s = () => void 0, u = () => void 0, d = Ce(i.sync), f = Q(d), p = be(i.backend, {
849
+ ...i.allowedEngineParams ? { allowEngineParams: i.allowedEngineParams } : {},
850
+ onWarn: (e, t) => s(e, t)
851
+ }), m = /* @__PURE__ */ new Map();
852
+ for (let e of Object.values(i.indexes)) m.set(e.index, e);
853
+ let h = H(i.backend), g = i.singleProcessMemoryIndex === !0, _ = !1, v, y = {
854
+ ...n,
509
855
  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 {
856
+ execute: (n, r) => {
857
+ let { index: i, q: a, limit: s, offset: c, filters: l, facets: u, highlight: d, fuzziness: f, engineParams: h } = n, g = m.get(i);
858
+ if (!g) return o.fail(new e({ index: i }));
859
+ let _ = ee(De({
860
+ ...l ? { filters: l } : {},
861
+ ...u ? { facets: u } : {},
862
+ ...d ? { highlight: d } : {}
863
+ }), g.queryableFields);
864
+ if (_) return o.fail(new t(_));
865
+ let v = r.request.subject.tenantId ?? null, y = Ee(g.tenantField, v, l);
866
+ if (y === null) return o.succeed({
513
867
  hits: [],
514
868
  facets: {}
869
+ });
870
+ let b = {
871
+ q: a,
872
+ ...s === void 0 ? {} : { limit: s },
873
+ ...c === void 0 ? {} : { offset: c },
874
+ ...y.length > 0 ? { filters: y } : {},
875
+ ...u && u.length > 0 ? { facets: u } : {},
876
+ ...d ? { highlight: d } : {},
877
+ ...f === void 0 ? {} : { fuzziness: f },
878
+ ...h ? { engineParams: h } : {}
515
879
  };
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 = [
880
+ return o.promise(() => p.query(i, b));
881
+ }
882
+ }, b = Te(i.backend), x = [
529
883
  "store:changes:read",
530
884
  "store:write",
531
885
  "inspect:read",
532
886
  "inspect:write",
533
- ...l ? [`network:outbound:${l}`] : []
534
- ], d = () => void 0, f = M(), p, m = (e) => ({
887
+ ...b ? [`network:outbound:${b}`] : []
888
+ ], S = R(), C = ge(), w, T, E = (e, t, n) => o.tryPromise(() => S.bump(e, t, n)).pipe(o.catchAll((n) => o.sync(() => s("search stats bump failed", {
889
+ index: e,
890
+ kind: t,
891
+ error: String(n)
892
+ })))), D = async (e) => {
893
+ let t = await C.list(e), n = 0, r = 0;
894
+ for (let e of t) {
895
+ let t = m.get(e.indexName);
896
+ if (!t) {
897
+ await C.clear(e.indexName, e.rowId);
898
+ continue;
899
+ }
900
+ try {
901
+ if (!T) throw Error("data store not bound");
902
+ let r = (await T.query(J(e.sourceTable, e.rowId)))[0];
903
+ r ? await p.upsert(t.index, [t.map(r)]) : await p.remove(t.index, [e.docId]), await C.clear(e.indexName, e.rowId), n++;
904
+ } catch (t) {
905
+ r++, await C.record({
906
+ ...e,
907
+ error: String(t?.message ?? t)
908
+ }).catch(() => void 0);
909
+ }
910
+ }
911
+ return {
912
+ scanned: t.length,
913
+ repaired: n,
914
+ failed: r
915
+ };
916
+ }, O = (e) => ({
535
917
  kind: "json",
536
918
  data: e
537
- }), h = (e, t) => ({
919
+ }), k = (e, t) => ({
538
920
  kind: "json",
539
921
  status: e,
540
922
  data: { error: t }
541
923
  });
542
- return i({
924
+ return c({
543
925
  name: a,
926
+ baseName: $,
544
927
  description: "External search index sync (Typesense/Meilisearch/Algolia/memory) via the ChangeEvent tap + a tenant-scoped query route.",
545
- permissions: u,
546
- routes: [c],
547
- rpcClientDescriptors: t,
548
- inspectEndpoints: [{
549
- method: "GET",
550
- path: "/indexes",
551
- description: "Configured indexes + durable, cross-replica sync stats.",
552
- handler: () => r.promise(async () => {
553
- let e = await Promise.all(Object.entries(n.indexes).map(async ([e, t]) => {
554
- let n = await f.get(t.index);
555
- return {
556
- table: e,
557
- index: t.index,
558
- tenantField: t.tenantField ?? null,
559
- ...n
560
- };
928
+ permissions: x,
929
+ routes: [y],
930
+ rpcClientDescriptors: r,
931
+ inspectEndpoints: [
932
+ {
933
+ method: "GET",
934
+ path: "/indexes",
935
+ description: "Configured indexes + durable, cross-replica sync stats + current index drift.",
936
+ handler: () => o.promise(async () => {
937
+ let e = await Promise.all(Object.entries(i.indexes).map(async ([e, t]) => {
938
+ let n = await S.get(t.index), r = await C.summary(t.index);
939
+ return {
940
+ table: e,
941
+ index: t.index,
942
+ tenantField: t.tenantField ?? null,
943
+ ...n,
944
+ pendingDrift: r.pending,
945
+ lastDriftAt: r.lastFailedAt,
946
+ drifted: r.pending > 0
947
+ };
948
+ }));
949
+ return O({
950
+ indexes: e,
951
+ backend: typeof i.backend == "string" ? i.backend : i.backend ? "custom/vendor" : "memory",
952
+ durable: h !== "memory",
953
+ singleProcessMemoryIndex: g,
954
+ pendingDrift: e.reduce((e, t) => e + t.pendingDrift, 0),
955
+ sync: {
956
+ ...d,
957
+ resyncArmed: w !== void 0
958
+ }
959
+ });
960
+ })
961
+ },
962
+ {
963
+ method: "GET",
964
+ path: "/drift",
965
+ description: "The changes that did NOT reach the engine, oldest first (the repair queue).",
966
+ handler: (e) => o.promise(async () => {
967
+ let t = Number(new URL(e.url, "http://localhost").searchParams.get("limit") ?? 100), n = await C.list(Number.isFinite(t) && t > 0 ? Math.min(t, 1e3) : 100);
968
+ return O({ entries: n });
969
+ })
970
+ },
971
+ {
972
+ method: "POST",
973
+ path: "/resync",
974
+ description: "Repair drifted rows now — re-reads each from the DB and re-applies it.",
975
+ handler: (e) => o.promise(async () => {
976
+ let t;
977
+ try {
978
+ t = JSON.parse(e.body || "{}");
979
+ } catch {
980
+ return k(400, "invalid JSON body");
981
+ }
982
+ if (!T) return k(503, "data store not bound yet");
983
+ try {
984
+ let e = await D(Math.min(Math.max(1, t.limit ?? d.resyncBatchSize), 1e3));
985
+ return O({
986
+ ok: !0,
987
+ ...e
988
+ });
989
+ } catch (e) {
990
+ return k(500, e.message);
991
+ }
992
+ })
993
+ },
994
+ {
995
+ method: "POST",
996
+ path: "/reindex",
997
+ description: "Re-seed an index from the table's current rows.",
998
+ handler: (e) => o.promise(async () => {
999
+ let t;
1000
+ try {
1001
+ t = JSON.parse(e.body || "{}");
1002
+ } catch {
1003
+ return k(400, "invalid JSON body");
1004
+ }
1005
+ let n = t.table ? i.indexes[t.table] : void 0;
1006
+ if (!t.table || !n) return k(404, `no index configured for table "${t.table ?? ""}"`);
1007
+ if (!T) return k(503, "data store not bound yet");
1008
+ try {
1009
+ let e = await T.query({
1010
+ table: t.table,
1011
+ predicate: void 0,
1012
+ order: [],
1013
+ take: void 0,
1014
+ skip: void 0,
1015
+ projection: void 0
1016
+ }), r = await xe(p, n, e);
1017
+ return await S.recordReindex(n.index, r, (/* @__PURE__ */ new Date()).toISOString()), O({
1018
+ ok: !0,
1019
+ table: t.table,
1020
+ index: n.index,
1021
+ count: r
1022
+ });
1023
+ } catch (e) {
1024
+ return k(500, e.message);
1025
+ }
1026
+ })
1027
+ }
1028
+ ],
1029
+ extendSchema: { tables: [...L(), ...ye()] },
1030
+ bindDataStore: (e, t) => {
1031
+ T = e, S = V(e), C = ve(e), h === "memory" && t?.membership !== void 0 && (v = t.membership.onChange((e) => {
1032
+ e.kind === "left" || _ || t.instanceId !== void 0 && e.instanceId === t.instanceId || (_ = !0, s(G(e.instanceId, g), {
1033
+ instanceId: t.instanceId ?? null,
1034
+ peer: e.instanceId,
1035
+ indexes: Object.keys(i.indexes)
561
1036
  }));
562
- return m({
563
- indexes: e,
564
- backend: typeof n.backend == "string" ? n.backend : n.backend ? "custom/vendor" : "memory"
565
- });
566
- })
567
- }, {
568
- method: "POST",
569
- path: "/reindex",
570
- description: "Re-seed an index from the table's current rows.",
571
- handler: (e) => r.promise(async () => {
572
- let t;
1037
+ })), !(d.resyncIntervalMs <= 0 || t?.scheduleCoordinated === void 0) && (w = t.scheduleCoordinated("search.resync", d.resyncIntervalMs, async () => {
573
1038
  try {
574
- t = JSON.parse(e.body || "{}");
575
- } catch {
576
- return h(400, "invalid JSON body");
577
- }
578
- let r = t.table ? n.indexes[t.table] : void 0;
579
- if (!t.table || !r) return h(404, `no index configured for table "${t.table ?? ""}"`);
580
- if (!p) return h(503, "data store not bound yet");
581
- try {
582
- let e = await p.query({
583
- table: t.table,
584
- predicate: void 0,
585
- order: [],
586
- take: void 0,
587
- skip: void 0,
588
- projection: void 0
589
- }), n = await z(o, r, e);
590
- return await f.recordReindex(r.index, n, (/* @__PURE__ */ new Date()).toISOString()), m({
591
- ok: !0,
592
- table: t.table,
593
- index: r.index,
594
- count: n
595
- });
1039
+ let e = await D(d.resyncBatchSize);
1040
+ return (e.repaired > 0 || e.failed > 0) && u("search: drift repair pass", e), { idle: e.scanned === 0 };
596
1041
  } catch (e) {
597
- return h(500, e.message);
1042
+ return s("search: drift repair pass failed", { error: String(e?.message ?? e) }), { idle: !1 };
598
1043
  }
599
- })
600
- }],
601
- extendSchema: { tables: [...j()] },
602
- bindDataStore: (e) => {
603
- p = e, f = L(e);
1044
+ }));
604
1045
  },
605
1046
  onChangeEvent: (e) => {
606
- let t = n.indexes[e.table];
607
- if (!t) return r.void;
608
- let i = r.tryPromise(() => f.bump(t.index, e.op === "delete" ? "removed" : "synced")).pipe(r.catchAll((t) => r.sync(() => d("search stats bump failed", {
609
- table: e.table,
610
- op: e.op,
611
- error: String(t)
612
- })))), a = r.tryPromise(() => B(o, t, e));
613
- return r.zipRight(i, a);
1047
+ let t = i.indexes[e.table];
1048
+ if (!t) return o.void;
1049
+ let n = e.changeScope, r = e.changeScope === "fleet" || e.origin !== "injected", a = e.op === "delete" ? e.old : e.new, c = typeof a?.id == "string" ? a.id : "", l = e.op === "delete" ? "remove" : "upsert", u = () => {
1050
+ if (e.op === "delete" || !e.new) return c;
1051
+ try {
1052
+ return String(t.map(e.new).id ?? c);
1053
+ } catch {
1054
+ return c;
1055
+ }
1056
+ };
1057
+ return o.tryPromise({
1058
+ try: () => Se(p, t, e),
1059
+ catch: we
1060
+ }).pipe(o.retry(f)).pipe(o.matchEffect({
1061
+ onSuccess: () => r ? E(t.index, e.op === "delete" ? "removed" : "synced", n) : o.void,
1062
+ onFailure: (r) => {
1063
+ let i = {
1064
+ table: e.table,
1065
+ index: t.index,
1066
+ op: e.op,
1067
+ rowId: c,
1068
+ transient: r.transient,
1069
+ error: r.message
1070
+ };
1071
+ return c === "" ? o.sync(() => s("search: change not indexed and NOT recoverable (row has no id)", i)).pipe(o.zipRight(o.fail(r))) : o.tryPromise(() => C.record({
1072
+ indexName: t.index,
1073
+ sourceTable: e.table,
1074
+ rowId: c,
1075
+ docId: u(),
1076
+ op: l,
1077
+ error: r.message
1078
+ })).pipe(o.matchEffect({
1079
+ onSuccess: () => E(t.index, "dropped", n).pipe(o.zipRight(o.sync(() => s("search: index drift recorded — will be repaired from the database", i)))),
1080
+ onFailure: (e) => o.sync(() => s("search: change LOST — engine failed and the drift ledger could not record it", {
1081
+ ...i,
1082
+ ledgerError: String(e)
1083
+ })).pipe(o.zipRight(o.fail(r)))
1084
+ }));
1085
+ }
1086
+ }));
614
1087
  },
615
- onActivate: (e) => r.sync(() => {
616
- d = (t, n) => e.logger.warn(t, n), e.logger.info("search active", {
617
- indexes: Object.keys(n.indexes),
618
- backend: typeof n.backend == "string" ? n.backend : "custom/vendor"
1088
+ onActivate: (e) => o.suspend(() => {
1089
+ s = (t, n) => e.logger.warn(t, n), u = (t, n) => e.logger.info(t, n);
1090
+ let t = W({
1091
+ backendKind: h,
1092
+ production: (e.env?.NODE_ENV ?? process.env.NODE_ENV) === "production",
1093
+ singleProcessDeclared: g
619
1094
  });
1095
+ return t.level === "refuse" ? o.fail(new U(t.message)) : (t.level === "note" && e.logger.info(t.message, { indexes: Object.keys(i.indexes) }), o.sync(() => {
1096
+ e.logger.info("search active", {
1097
+ indexes: Object.keys(i.indexes),
1098
+ backend: typeof i.backend == "string" ? i.backend : "custom/vendor",
1099
+ durable: h !== "memory",
1100
+ sync: d
1101
+ });
1102
+ }));
1103
+ }),
1104
+ onDeactivate: () => o.sync(() => {
1105
+ w?.stop(), w = void 0, v?.(), v = void 0, _ = !1;
620
1106
  })
621
1107
  });
622
1108
  };
623
1109
  //#endregion
624
- 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 };
1110
+ export { v as FIELD_NAME_PATTERN, K as SEARCH_DRIFT_TABLE, P as SEARCH_STATS_TABLE, Z as SYNC_DEFAULTS, w as SearchBackendError, U as SearchBackendNotDurable, t as SearchFieldRejected, e as SearchIndexNotFound, N as algoliaBackend, C as allowedEngineParams, Se as applyChange, we as asBackendError, W as auditMemoryBackend, H as backendKindOf, xe as backfillIndex, ve as dataStoreDriftLedger, V as dataStoreStatsStore, J as driftSourceQuery, ye as driftTables, y as isSafeFieldName, ae as meilisearchBackend, O as memoryBackend, ge as memoryDriftLedger, R as memoryStatsStore, G as peerObservedWarning, Ce as resolveSyncOptions, S as sanitizeEngineParams, Ee as scopeSearchFilters, q as searchDriftTable, Oe as searchPlugin, I as searchStatsTable, L as searchTables, Q as syncRetrySchedule, ne as typesenseBackend };