@endora-commerce/mod-search 0.100.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/backend/cli/reindex.d.ts +4 -0
  4. package/dist/backend/cli/reindex.d.ts.map +1 -0
  5. package/dist/backend/cli/reindex.js +32 -0
  6. package/dist/backend/cli/reindex.js.map +1 -0
  7. package/dist/backend/entities/search-phrase-record.entity.d.ts +40 -0
  8. package/dist/backend/entities/search-phrase-record.entity.d.ts.map +1 -0
  9. package/dist/backend/entities/search-phrase-record.entity.js +85 -0
  10. package/dist/backend/entities/search-phrase-record.entity.js.map +1 -0
  11. package/dist/backend/index.d.ts +87 -0
  12. package/dist/backend/index.d.ts.map +1 -0
  13. package/dist/backend/index.js +153 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/backend/plugin.d.ts +133 -0
  16. package/dist/backend/plugin.d.ts.map +1 -0
  17. package/dist/backend/plugin.js +250 -0
  18. package/dist/backend/plugin.js.map +1 -0
  19. package/dist/backend/routes.admin.d.ts +28 -0
  20. package/dist/backend/routes.admin.d.ts.map +1 -0
  21. package/dist/backend/routes.admin.js +33 -0
  22. package/dist/backend/routes.admin.js.map +1 -0
  23. package/dist/backend/routes.public.d.ts +22 -0
  24. package/dist/backend/routes.public.d.ts.map +1 -0
  25. package/dist/backend/routes.public.js +192 -0
  26. package/dist/backend/routes.public.js.map +1 -0
  27. package/dist/backend/services/embedder-config-resolver.d.ts +25 -0
  28. package/dist/backend/services/embedder-config-resolver.d.ts.map +1 -0
  29. package/dist/backend/services/embedder-config-resolver.js +47 -0
  30. package/dist/backend/services/embedder-config-resolver.js.map +1 -0
  31. package/dist/backend/services/llm-toggle.service.d.ts +50 -0
  32. package/dist/backend/services/llm-toggle.service.d.ts.map +1 -0
  33. package/dist/backend/services/llm-toggle.service.js +78 -0
  34. package/dist/backend/services/llm-toggle.service.js.map +1 -0
  35. package/dist/backend/services/search-event-subscriber.d.ts +125 -0
  36. package/dist/backend/services/search-event-subscriber.d.ts.map +1 -0
  37. package/dist/backend/services/search-event-subscriber.js +113 -0
  38. package/dist/backend/services/search-event-subscriber.js.map +1 -0
  39. package/dist/backend/services/search-indexer.d.ts +346 -0
  40. package/dist/backend/services/search-indexer.d.ts.map +1 -0
  41. package/dist/backend/services/search-indexer.js +670 -0
  42. package/dist/backend/services/search-indexer.js.map +1 -0
  43. package/dist/backend/services/search-phrase-recorder.service.d.ts +25 -0
  44. package/dist/backend/services/search-phrase-recorder.service.d.ts.map +1 -0
  45. package/dist/backend/services/search-phrase-recorder.service.js +89 -0
  46. package/dist/backend/services/search-phrase-recorder.service.js.map +1 -0
  47. package/dist/backend/services/search-query-port.d.ts +29 -0
  48. package/dist/backend/services/search-query-port.d.ts.map +1 -0
  49. package/dist/backend/services/search-query-port.js +20 -0
  50. package/dist/backend/services/search-query-port.js.map +1 -0
  51. package/dist/backend/services/search-query.service.d.ts +205 -0
  52. package/dist/backend/services/search-query.service.d.ts.map +1 -0
  53. package/dist/backend/services/search-query.service.js +346 -0
  54. package/dist/backend/services/search-query.service.js.map +1 -0
  55. package/dist/backend/services/search-reindex-worker.d.ts +27 -0
  56. package/dist/backend/services/search-reindex-worker.d.ts.map +1 -0
  57. package/dist/backend/services/search-reindex-worker.js +13 -0
  58. package/dist/backend/services/search-reindex-worker.js.map +1 -0
  59. package/dist/backend/services/search-suggest.service.d.ts +53 -0
  60. package/dist/backend/services/search-suggest.service.d.ts.map +1 -0
  61. package/dist/backend/services/search-suggest.service.js +64 -0
  62. package/dist/backend/services/search-suggest.service.js.map +1 -0
  63. package/dist/backend/services/suggestion-pricing-enricher.d.ts +62 -0
  64. package/dist/backend/services/suggestion-pricing-enricher.d.ts.map +1 -0
  65. package/dist/backend/services/suggestion-pricing-enricher.js +41 -0
  66. package/dist/backend/services/suggestion-pricing-enricher.js.map +1 -0
  67. package/dist/manifest.d.ts +263 -0
  68. package/dist/manifest.d.ts.map +1 -0
  69. package/dist/manifest.js +300 -0
  70. package/dist/manifest.js.map +1 -0
  71. package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts +13 -0
  72. package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts.map +1 -0
  73. package/dist/migrations/20260501T123145_search_phrase_records_init.js +40 -0
  74. package/dist/migrations/20260501T123145_search_phrase_records_init.js.map +1 -0
  75. package/dist/migrations/index.d.ts +27 -0
  76. package/dist/migrations/index.d.ts.map +1 -0
  77. package/dist/migrations/index.js +29 -0
  78. package/dist/migrations/index.js.map +1 -0
  79. package/docs/search.md +284 -0
  80. package/i18n/en.json +10 -0
  81. package/i18n/pl.json +10 -0
  82. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ <!-- Generated by `pnpm --filter backend run manifests:generate`. Delete this line to take the file over; a README without it is never regenerated. -->
2
+
3
+ # @endora-commerce/mod-search
4
+
5
+ Per-channel Meilisearch indexes, suggest popup, and optional LLM-augmented search.
6
+
7
+ ## What this is
8
+
9
+ An **Endora Commerce module package**. Its module id is `search` — the identity of record in the platform's registries, in its migrations and in its setting, permission and translation codes.
10
+
11
+ A module is not imported by application code. The platform discovers the extension packages an instance has installed, reads the manifest on the root subpath and composes the module from it, so installing this package and starting the instance is the whole integration.
12
+
13
+ ## Entry points
14
+
15
+ | Import | Contents |
16
+ | --- | --- |
17
+ | `@endora-commerce/mod-search` | the module manifest — its id, version, dependencies, settings and activation |
18
+ | `@endora-commerce/mod-search/backend` | the composition root the platform calls, with the entities, services, routes and workers it registers |
19
+ | `@endora-commerce/mod-search/migrations` | the module’s own schema migrations, in the order the platform runs them |
20
+
21
+ ## Depends on
22
+
23
+ Everything below is a **peer** dependency, so the application resolves exactly one copy of each — two copies of a host or of React are a runtime failure rather than a type error. An entry marked *optional* is needed only by the layers that use it.
24
+
25
+ **Endora packages**
26
+
27
+ - `@endora-commerce/contracts`
28
+ - `@endora-commerce/platform`
29
+
30
+ **Third-party**
31
+
32
+ - `@mikro-orm/core` ^6
33
+ - `@mikro-orm/migrations` ^6
34
+ - `@mikro-orm/postgresql` ^6
35
+ - `fastify` ^5
36
+ - `meilisearch` ^0
37
+ - `zod` ^4
38
+
39
+ ## What the tarball carries
40
+
41
+ - `dist/` — the compiled JavaScript and its type declarations
42
+ - `i18n/` — the translation bundles this package ships (`en`, `pl`), loaded by the platform
43
+ - `docs/` — the package’s own pages in the Endora Commerce documentation site
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pnpm add @endora-commerce/mod-search
49
+ ```
50
+
51
+ ## Licence
52
+
53
+ MIT — the text is in `LICENSE`, beside this file.
@@ -0,0 +1,4 @@
1
+ import type { ModuleCliCommandContext } from '@endora-commerce/contracts';
2
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
3
+ export declare function reindex({ ctx, out, }: ModuleCliCommandContext<ModuleContext>): Promise<number>;
4
+ //# sourceMappingURL=reindex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reindex.d.ts","sourceRoot":"","sources":["../../../src/backend/cli/reindex.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAStE,wBAAsB,OAAO,CAAC,EAC5B,GAAG,EACH,GAAG,GACJ,EAAE,uBAAuB,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAgC1D"}
@@ -0,0 +1,32 @@
1
+ export async function reindex({ ctx, out, }) {
2
+ // A cradle read and **not** `lazyPort<SearchModuleHandle>(ctx, 'searchHandle')`,
3
+ // which compiles and then fails at runtime with
4
+ // `handle.indexer.reindexAllChannels is not a function`. `lazyPort` returns a
5
+ // proxy that answers *every* property with a function so it can forward a
6
+ // method call — the same property that makes optional methods impossible
7
+ // through a port (D-97.3) — so a nested reach like `handle.indexer` gets a
8
+ // function rather than the indexer. `searchHandle` is this module's own name,
9
+ // so a cradle read crosses no boundary and hides no edge from
10
+ // `check:port-dependencies`; presence for this module was decided by the host
11
+ // before this body ran.
12
+ //
13
+ // `reindexAllChannels` rather than `searchReindexPort.reindexAll` because the
14
+ // per-channel breakdown below is what an operator runs this for, and the port
15
+ // returns one total. Both run over the same indexer.
16
+ const cradle = ctx.cradle();
17
+ const results = await cradle.searchHandle.indexer.reindexAllChannels(cradle.emFactory());
18
+ out('');
19
+ out('=== Search reindex complete ===');
20
+ if (results.length === 0) {
21
+ out('No Sales Channels found — run seed:dev first.');
22
+ }
23
+ else {
24
+ for (const r of results) {
25
+ out(` ${r.channelCode.padEnd(16)} → ${r.indexUid.padEnd(28)} ` +
26
+ `(${r.documentCount} document${r.documentCount === 1 ? '' : 's'})`);
27
+ }
28
+ }
29
+ out('');
30
+ return 0;
31
+ }
32
+ //# sourceMappingURL=reindex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reindex.js","sourceRoot":"","sources":["../../../src/backend/cli/reindex.ts"],"names":[],"mappings":"AA6CA,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAC5B,GAAG,EACH,GAAG,GACoC;IACvC,iFAAiF;IACjF,gDAAgD;IAChD,8EAA8E;IAC9E,0EAA0E;IAC1E,yEAAyE;IACzE,2EAA2E;IAC3E,8EAA8E;IAC9E,8DAA8D;IAC9D,8EAA8E;IAC9E,wBAAwB;IACxB,EAAE;IACF,8EAA8E;IAC9E,8EAA8E;IAC9E,qDAAqD;IACrD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAuB,CAAC;IACjD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;IAEzF,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,GAAG,CAAC,iCAAiC,CAAC,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,+CAA+C,CAAC,CAAC;IACvD,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,GAAG,CACD,KAAK,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG;gBACzD,IAAI,CAAC,CAAC,aAAa,YAAY,CAAC,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CACrE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { OptionalProps } from '@mikro-orm/core';
2
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
3
+ /**
4
+ * SearchPhraseRecord — feature 006 / US3 / data-model.md §1.1.
5
+ *
6
+ * One row per committed search (Enter / "Search" action). Append-only
7
+ * dataset consumed by the future Analytics module to surface popular
8
+ * phrases, dead-end phrases, and frequency trends.
9
+ *
10
+ * Storage rules:
11
+ * - `phrase` is verbatim — no typo correction or LLM expansion (FR-013).
12
+ * - `phrase_normalized` is `lower(trim(phrase))`, maintained at insert
13
+ * time; supports case-insensitive aggregation without rewriting the
14
+ * verbatim phrase.
15
+ * - `result_count` reflects the size of the result set the customer
16
+ * actually saw; `0` is recorded for dead-end phrases (FR-014).
17
+ * - FK to `sales_channels.id` is RESTRICT, not CASCADE: removing a
18
+ * sales channel must surface as a manual decision, not silently
19
+ * delete history.
20
+ *
21
+ * Threshold no-op: callers (the `/api/v1/search/record` route + the
22
+ * SearchPhraseRecorder service) refuse to insert when the trimmed
23
+ * phrase length is below the channel's `search.popup.minimum_query_length`
24
+ * setting. The threshold check is the recorder's job, not the entity's.
25
+ */
26
+ export declare class SearchPhraseRecord {
27
+ [OptionalProps]?: 'id' | 'recordedAt';
28
+ id: string;
29
+ phrase: string;
30
+ /**
31
+ * `lower(trim(phrase))` — populated by the recorder service before
32
+ * persistAndFlush. Indexed by `(sales_channel_id, phrase_normalized,
33
+ * recorded_at DESC)` for the analytics aggregation query.
34
+ */
35
+ phraseNormalized: string;
36
+ salesChannel: SalesChannel;
37
+ resultCount: number;
38
+ recordedAt: Date;
39
+ }
40
+ //# sourceMappingURL=search-phrase-record.entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-phrase-record.entity.d.ts","sourceRoot":"","sources":["../../../src/backend/entities/search-phrase-record.entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,aAAa,EAGd,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAEa,kBAAkB;IAC7B,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,GAAG,YAAY,CAAC;IAGtC,EAAE,EAAE,MAAM,CAAgB;IAG1B,MAAM,EAAG,MAAM,CAAC;IAEhB;;;;OAIG;IAEH,gBAAgB,EAAG,MAAM,CAAC;IAI1B,YAAY,EAAG,YAAY,CAAC;IAG5B,WAAW,EAAG,MAAM,CAAC;IAOrB,UAAU,EAAE,IAAI,CAAc;CAC/B"}
@@ -0,0 +1,85 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Entity, Index, ManyToOne, OptionalProps, PrimaryKey, Property, } from '@mikro-orm/core';
11
+ import { GlobalEntity } from '@endora-commerce/platform/tenancy';
12
+ import { randomUUID } from 'crypto';
13
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
14
+ /**
15
+ * SearchPhraseRecord — feature 006 / US3 / data-model.md §1.1.
16
+ *
17
+ * One row per committed search (Enter / "Search" action). Append-only
18
+ * dataset consumed by the future Analytics module to surface popular
19
+ * phrases, dead-end phrases, and frequency trends.
20
+ *
21
+ * Storage rules:
22
+ * - `phrase` is verbatim — no typo correction or LLM expansion (FR-013).
23
+ * - `phrase_normalized` is `lower(trim(phrase))`, maintained at insert
24
+ * time; supports case-insensitive aggregation without rewriting the
25
+ * verbatim phrase.
26
+ * - `result_count` reflects the size of the result set the customer
27
+ * actually saw; `0` is recorded for dead-end phrases (FR-014).
28
+ * - FK to `sales_channels.id` is RESTRICT, not CASCADE: removing a
29
+ * sales channel must surface as a manual decision, not silently
30
+ * delete history.
31
+ *
32
+ * Threshold no-op: callers (the `/api/v1/search/record` route + the
33
+ * SearchPhraseRecorder service) refuse to insert when the trimmed
34
+ * phrase length is below the channel's `search.popup.minimum_query_length`
35
+ * setting. The threshold check is the recorder's job, not the entity's.
36
+ */
37
+ let SearchPhraseRecord = class SearchPhraseRecord {
38
+ [OptionalProps];
39
+ id = randomUUID();
40
+ phrase;
41
+ /**
42
+ * `lower(trim(phrase))` — populated by the recorder service before
43
+ * persistAndFlush. Indexed by `(sales_channel_id, phrase_normalized,
44
+ * recorded_at DESC)` for the analytics aggregation query.
45
+ */
46
+ phraseNormalized;
47
+ salesChannel;
48
+ resultCount;
49
+ recordedAt = new Date();
50
+ };
51
+ __decorate([
52
+ PrimaryKey({ type: 'uuid' }),
53
+ __metadata("design:type", String)
54
+ ], SearchPhraseRecord.prototype, "id", void 0);
55
+ __decorate([
56
+ Property({ type: 'string', length: 512 }),
57
+ __metadata("design:type", String)
58
+ ], SearchPhraseRecord.prototype, "phrase", void 0);
59
+ __decorate([
60
+ Property({ type: 'string', length: 512, fieldName: 'phrase_normalized' }),
61
+ __metadata("design:type", String)
62
+ ], SearchPhraseRecord.prototype, "phraseNormalized", void 0);
63
+ __decorate([
64
+ ManyToOne(() => SalesChannel, { fieldName: 'sales_channel_id' }),
65
+ Index(),
66
+ __metadata("design:type", SalesChannel)
67
+ ], SearchPhraseRecord.prototype, "salesChannel", void 0);
68
+ __decorate([
69
+ Property({ type: 'integer', fieldName: 'result_count' }),
70
+ __metadata("design:type", Number)
71
+ ], SearchPhraseRecord.prototype, "resultCount", void 0);
72
+ __decorate([
73
+ Property({
74
+ type: 'datetime',
75
+ onCreate: () => new Date(),
76
+ fieldName: 'recorded_at',
77
+ }),
78
+ __metadata("design:type", Date)
79
+ ], SearchPhraseRecord.prototype, "recordedAt", void 0);
80
+ SearchPhraseRecord = __decorate([
81
+ GlobalEntity(),
82
+ Entity({ tableName: 'search_phrase_records' })
83
+ ], SearchPhraseRecord);
84
+ export { SearchPhraseRecord };
85
+ //# sourceMappingURL=search-phrase-record.entity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-phrase-record.entity.js","sourceRoot":"","sources":["../../../src/backend/entities/search-phrase-record.entity.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EACL,MAAM,EACN,KAAK,EACL,SAAS,EACT,aAAa,EACb,UAAU,EACV,QAAQ,GACT,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGI,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAC7B,CAAC,aAAa,CAAC,CAAuB;IAGtC,EAAE,GAAW,UAAU,EAAE,CAAC;IAG1B,MAAM,CAAU;IAEhB;;;;OAIG;IAEH,gBAAgB,CAAU;IAI1B,YAAY,CAAgB;IAG5B,WAAW,CAAU;IAOrB,UAAU,GAAS,IAAI,IAAI,EAAE,CAAC;CAC/B,CAAA;AA1BC;IADC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;8CACH;AAG1B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;;kDAC1B;AAQhB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC;;4DAChD;AAI1B;IAFC,SAAS,CAAC,GAAG,EAAE,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC;IAChE,KAAK,EAAE;8BACO,YAAY;wDAAC;AAG5B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;;uDACpC;AAOrB;IALC,QAAQ,CAAC;QACR,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;QAC1B,SAAS,EAAE,aAAa;KACzB,CAAC;8BACU,IAAI;sDAAc;AA7BnB,kBAAkB;IAF9B,YAAY,EAAE;IACd,MAAM,CAAC,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAC;GAClC,kBAAkB,CA8B9B"}
@@ -0,0 +1,87 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
3
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
4
+ import type { SuggestionPriceResolverPort } from './services/suggestion-pricing-enricher.js';
5
+ import { type SearchModuleOptions, type SearchModuleResult } from './plugin.js';
6
+ import { SearchPhraseRecord } from './entities/search-phrase-record.entity.js';
7
+ /**
8
+ * `search` — six optional options that were never actually optional (feature
9
+ * 072, wave 2, T123).
10
+ *
11
+ * `settingsService`, `settingsAdminService`, `requireAdmin`, `credentials`,
12
+ * `resolveAdminAuditContext` and `enrichSuggestionPricing` were all declared
13
+ * optional, and both composition roots passed all six. Their absence did not
14
+ * fail — it silently produced a lesser module: manifest-default suggestion
15
+ * limits, no `settings.value_changed` → embedder reactor, no admin routes at
16
+ * all, and a typeahead popup showing list prices to a customer on a price list.
17
+ * The comment on `settingsService` said it was for "foundation tests that
18
+ * predate Settings"; no such caller existed. They are required now.
19
+ *
20
+ * `resolveReindexIntervalMinutes` is deleted rather than converted. It read
21
+ * `search.reindex_interval_minutes` — this module's own setting — and fell back
22
+ * to this module's own manifest default, in a root, so the module "isn't
23
+ * coupled to the settings read API". It resolves a settings port already; the
24
+ * indirection bought nothing and put a module's configuration in a composition.
25
+ *
26
+ * `enableReindexScheduler` stays root-supplied, and that is the opposite call
27
+ * for a reason that has now bitten three conversions: the harness genuinely has
28
+ * no worker role, and deriving it from `BACKEND_ROLE` here would start a
29
+ * periodic Meilisearch sweep in every one of ~225 test files. Which process
30
+ * runs consumers is a deployment decision (Principle X); which setting drives
31
+ * their cadence is the module's.
32
+ *
33
+ * **`searchReindexPort` came home in T143a cluster 6.** `catalog` runs a full
34
+ * reindex as a `search_reindex` bulk operation when an attribute's `searchable`
35
+ * flag flips, and production built a **second** `SearchIndexer` inside a root
36
+ * closure to do it — while `searchModule` was building its own two lines away.
37
+ * That is the `inventory` finding of cluster 2 in a second module: two
38
+ * instances of one service, neither required to agree with the other, and the
39
+ * root's one ungated, so it kept reindexing with `search` switched off.
40
+ */
41
+ /** What `search` resolves from the container, and the names it owns. */
42
+ export interface SearchCradle {
43
+ readonly emFactory: () => EntityManager;
44
+ readonly requireAdmin: RequireAdminFactory;
45
+ readonly settingsReadPort: SearchModuleOptions['settingsService'];
46
+ readonly settingsAdminService: SearchModuleOptions['settingsAdminService'];
47
+ readonly credentialsService: SearchModuleOptions['credentials'];
48
+ readonly adminAuditActorResolver: SearchModuleOptions['resolveAdminAuditContext'];
49
+ readonly catalogAttributeReadPort: SearchModuleOptions['catalogAttributeRead'];
50
+ /** `price_lists`' resolver, narrowed to what a suggestion needs. */
51
+ readonly pricingService: SuggestionPriceResolverPort;
52
+ /**
53
+ * Whether this composition runs the periodic reindex sweep (Principle X).
54
+ * Root-supplied rather than env-derived: the test harness runs no sweep, and
55
+ * a module should not have to know which of its callers is a test.
56
+ */
57
+ readonly searchRunWorkers: boolean;
58
+ readonly search: SearchModuleResult;
59
+ readonly searchHandle: SearchModuleResult['handle'];
60
+ /**
61
+ * A full reindex of every sales-channel index, as the `search:reindex` CLI
62
+ * and the admin "Reindex products" button run it (T143a).
63
+ *
64
+ * The document count is the whole answer a caller needs — `catalog` reports
65
+ * it on the bulk operation — so the port hands back that rather than the
66
+ * per-channel summaries, and no consumer has to know an index uid exists.
67
+ */
68
+ readonly searchReindexPort: {
69
+ reindexAll(): Promise<{
70
+ documentCount: number;
71
+ }>;
72
+ };
73
+ }
74
+ export declare function registerModule(ctx: ModuleContext): void;
75
+ /**
76
+ * The module's persisted entity classes, on the `./backend` subpath, as one
77
+ * array and **no named class export** (D-168).
78
+ *
79
+ * This is the shape the platform reads when the package is *installed*: the
80
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
81
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
82
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
83
+ * the package pass of `check-entity-tenant-classification`. A missing array is
84
+ * answered with `[]` — zero entities registered, no error anywhere.
85
+ */
86
+ export declare const entities: (typeof SearchPhraseRecord)[];
87
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAI5E,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAC;AAC7F,OAAO,EAAgB,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,wEAAwE;AACxE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;IAClE,QAAQ,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,sBAAsB,CAAC,CAAC;IAC3E,QAAQ,CAAC,kBAAkB,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;IAChE,QAAQ,CAAC,uBAAuB,EAAE,mBAAmB,CAAC,0BAA0B,CAAC,CAAC;IAClF,QAAQ,CAAC,wBAAwB,EAAE,mBAAmB,CAAC,sBAAsB,CAAC,CAAC;IAC/E,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,2BAA2B,CAAC;IACrD;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACpD;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE;QAAE,UAAU,IAAI,OAAO,CAAC;YAAE,aAAa,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CAClF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAuKvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,+BAEpB,CAAC"}
@@ -0,0 +1,153 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { createSearchQueryPort } from './services/search-query-port.js';
3
+ import { createSuggestionPricingEnricher } from './services/suggestion-pricing-enricher.js';
4
+ import { searchModule } from './plugin.js';
5
+ import { SearchPhraseRecord } from './entities/search-phrase-record.entity.js';
6
+ export function registerModule(ctx) {
7
+ ctx.di.register({
8
+ search: ctx
9
+ .asFunction(({ emFactory, searchRunWorkers }) => searchModule({
10
+ emFactory,
11
+ enableReindexScheduler: searchRunWorkers,
12
+ catalogAttributeRead: lazyPort(ctx, 'catalogAttributeReadPort'),
13
+ // Feature 075, Phase C — the indexer's product rows. It read them
14
+ // with `em.find(Product, …)` against `catalog`'s table, which no gate
15
+ // can see; over the port the read answers 503 when `catalog` is off,
16
+ // which is the binding dependency this manifest already declares.
17
+ catalogProducts: lazyPort(ctx, 'catalogProductReadPort'),
18
+ // Feature 075 / D-87 — the last two boundary crossings the indexer
19
+ // made in raw SQL: `catalog`'s `product_categories` / `categories`
20
+ // join, and the `sales_channel_products` bridge Principle XII
21
+ // reserves for the membership accessor. A statement names no import
22
+ // specifier, so neither was visible to Phase C.
23
+ catalogCategories: lazyPort(ctx, 'catalogCategoryReadPort'),
24
+ salesChannelMembership: lazyPort(ctx, 'salesChannelMembershipPort'),
25
+ settingsService: lazyPort(ctx, 'settingsReadPort'),
26
+ settingsAdminService: lazyPort(ctx, 'settingsAdminService'),
27
+ credentials: lazyPort(ctx, 'credentialsService'),
28
+ requireAdmin: (permission) => async (req, reply) => ctx.cradle().requireAdmin(permission)(req, reply),
29
+ resolveAdminAuditContext: (req) => ctx.cradle().adminAuditActorResolver(req),
30
+ // Built here rather than in a root: the enricher is this module's own
31
+ // code, and the only foreign part is the price resolver it wraps.
32
+ enrichSuggestionPricing: createSuggestionPricingEnricher({
33
+ catalogProducts: lazyPort(ctx, 'catalogProductReadPort'),
34
+ organizations: lazyPort(ctx, 'organizationDetailsPort'),
35
+ pricingService: lazyPort(ctx, 'pricingService'),
36
+ }),
37
+ // Issue #132 — the result list prices through the same port the
38
+ // popup already used.
39
+ listingPrices: lazyPort(ctx, 'pricingService'),
40
+ // ...and, now, the same organisation read the popup already used, so
41
+ // both halves of the search box quote one figure to one buyer.
42
+ organizations: lazyPort(ctx, 'organizationDetailsPort'),
43
+ }))
44
+ .singleton(),
45
+ });
46
+ /**
47
+ * Feature 075, Phase P — the storefront product query.
48
+ *
49
+ * `catalog`'s public listing hands the query over when
50
+ * `CATALOG_SEARCH_BACKEND=meilisearch` and serves it from Postgres
51
+ * otherwise — and today it does so by constructing its **own**
52
+ * `SearchQueryService` in its plugin body, so a composition holds two
53
+ * clients and two attribute-read wirings. This port is the instance
54
+ * `searchModule` already built.
55
+ *
56
+ * The Postgres fallback is a declared `nonBindingDependencies` degrade, not
57
+ * a `catch`: catching there would swallow `ModuleDisabledError` and make a
58
+ * switched-off `search` look like a slow one. Which is why the *other*
59
+ * degrade — an index that refuses or times out — is converted here rather
60
+ * than at the call site: `createSearchQueryPort` turns this module's own
61
+ * `SearchBackendUnavailable` into the `index-unavailable` arm of
62
+ * `SearchListOutcome`, so the consumer reads a field instead of writing the
63
+ * conditional re-throw `check:port-catches` refuses.
64
+ */
65
+ ctx.di.providePort('searchQueryPort', ctx
66
+ .asFunction(({ search }) => createSearchQueryPort(search.handle.searchQueryService))
67
+ .singleton());
68
+ ctx.di.providePort('searchHandle', ctx.asFunction(({ search }) => search.handle).singleton());
69
+ /**
70
+ * A **port**, not a contribution point (D-39): a reindex computes, talks to
71
+ * Meilisearch and rewrites every channel index, so a caller reaching for it
72
+ * while an operator has switched `search` off gets the 503 `MODULE_DISABLED`
73
+ * envelope rather than a sweep nobody asked for.
74
+ *
75
+ * It runs over `search.handle.indexer` — the instance `searchModule` already
76
+ * built — so a composition holds exactly one indexer and one Meilisearch
77
+ * client, whichever entry point triggers the reindex.
78
+ */
79
+ ctx.di.providePort('searchReindexPort', ctx
80
+ .asFunction(({ search, emFactory }) => ({
81
+ async reindexAll() {
82
+ const results = await search.handle.indexer.reindexAllChannels(emFactory());
83
+ return { documentCount: results.reduce((sum, r) => sum + r.documentCount, 0) };
84
+ },
85
+ }))
86
+ .singleton());
87
+ /**
88
+ * The seven index-maintenance subscriptions (issue #107).
89
+ *
90
+ * They were seven bare `eventBus.on` calls inside `SearchEventSubscriber`, so
91
+ * the Meilisearch documents kept being rewritten on every catalog write with
92
+ * `search` switched off — the module's routes refused, its indexer did not.
93
+ * `ctx.subscribe` wraps each in `subscribeForModule`, so the effective state
94
+ * decides whether the handler runs at all.
95
+ *
96
+ * The subscriber is reached through the cradle inside each closure rather than
97
+ * captured: registration resolves nothing, and reading the name per event is
98
+ * what lets a decoration or a rebuilt module reach the live instance.
99
+ */
100
+ const subscriber = () => ctx.cradle().search.handle.subscriber;
101
+ ctx.subscribe('product.created.v1', async (payload) => {
102
+ const { productId } = payload;
103
+ await subscriber().onProductUpserted(productId, 'product.created.v1');
104
+ });
105
+ ctx.subscribe('product.updated.v1', async (payload) => {
106
+ const { productId } = payload;
107
+ await subscriber().onProductUpserted(productId, 'product.updated.v1');
108
+ });
109
+ ctx.subscribe('product.archived.v1', async (payload) => {
110
+ const { productId } = payload;
111
+ await subscriber().onProductRemoved(productId, 'product.archived.v1');
112
+ });
113
+ ctx.subscribe('product.deleted.v1', async (payload) => {
114
+ const { productId } = payload;
115
+ await subscriber().onProductRemoved(productId, 'product.deleted.v1');
116
+ });
117
+ ctx.subscribe('category.updated.v1', async (payload) => {
118
+ const { categoryId } = payload;
119
+ await subscriber().onCategoryUpdated(categoryId);
120
+ });
121
+ ctx.subscribe('attribute.updated.v1', async () => {
122
+ await subscriber().onAttributeUpdated();
123
+ });
124
+ /**
125
+ * The LLM reactor (feature 006 / T027).
126
+ *
127
+ * It re-reads `search.llm.enabled` through the settings service, and gets the
128
+ * value that was just written: the settings write seam drops the cache and
129
+ * awaits the drop before it emits (issue #45). Until then this rested on the
130
+ * kernel's invalidator being registered ahead of this handler.
131
+ */
132
+ ctx.subscribe('settings.value_changed', async (payload) => {
133
+ await subscriber().onSettingChanged(payload);
134
+ });
135
+ ctx.routes(async (app) => {
136
+ await ctx.cradle().search.plugin(app);
137
+ });
138
+ }
139
+ /**
140
+ * The module's persisted entity classes, on the `./backend` subpath, as one
141
+ * array and **no named class export** (D-168).
142
+ *
143
+ * This is the shape the platform reads when the package is *installed*: the
144
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
145
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
146
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
147
+ * the package pass of `check-entity-tenant-classification`. A missing array is
148
+ * answered with `[]` — zero entities registered, no error anywhere.
149
+ */
150
+ export const entities = [
151
+ SearchPhraseRecord,
152
+ ];
153
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import type { CatalogAttributeReadPort, CatalogCategoryReadPort, CatalogProductReadPort, ListingPricePort, OrganizationDetailsPort, SettingsAdminAuditContext, SettingsAdminPort } from '@endora-commerce/contracts';
4
+ import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
5
+ import { SearchIndexer } from './services/search-indexer.js';
6
+ import { SearchEventSubscriber } from './services/search-event-subscriber.js';
7
+ import { SearchQueryService } from './services/search-query.service.js';
8
+ import { SearchSuggestService } from './services/search-suggest.service.js';
9
+ import { LlmToggleService } from './services/llm-toggle.service.js';
10
+ import type { CredentialResolvePort } from './services/embedder-config-resolver.js';
11
+ import { SearchReindexWorker } from './services/search-reindex-worker.js';
12
+ import { SearchPhraseRecorder } from './services/search-phrase-recorder.service.js';
13
+ import { type SuggestionPricingEnricher } from './routes.public.js';
14
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
15
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
16
+ /**
17
+ * Composition root for the search module — feature 006.
18
+ *
19
+ * Owns lifecycle for:
20
+ * - {@link SearchIndexer} — per-channel Meilisearch indexer (foundation 001).
21
+ * - {@link SearchEventSubscriber} — bridges catalog events + the
22
+ * `settings.value_changed` reactor (US2) so the per-channel Meilisearch
23
+ * indexes track Postgres mutations + LLM-mode flips automatically.
24
+ * - {@link SearchSuggestService} — typeahead popup adapter on top of
25
+ * {@link SearchQueryService}; reads its per-channel limit and
26
+ * threshold from Settings when wired (US2).
27
+ * - {@link LlmToggleService} — cross-setting validating wrapper around
28
+ * `search.llm.enabled` (US2).
29
+ *
30
+ * Module isolation (Constitution I): catalog publishes
31
+ * `product.*`/`attribute.*` events. Settings publishes
32
+ * `settings.value_changed`. Search subscribes here. Removing this module
33
+ * leaves catalog and settings working — there are no dangling references.
34
+ */
35
+ export interface SearchModuleOptions {
36
+ emFactory: () => EntityManager;
37
+ /**
38
+ * Feature 061 — the catalog's composed attribute read model (Principle I).
39
+ * Backs the indexer's searchable/filterable settings + option-label
40
+ * aggregation and the query service's filterable validation.
41
+ */
42
+ catalogAttributeRead: CatalogAttributeReadPort;
43
+ /**
44
+ * Feature 075, Phase C — the product rows the indexer turns into Meilisearch
45
+ * documents, read over `catalog`'s published port instead of out of its
46
+ * table. The edge is the binding `catalog` dependency the manifest declares:
47
+ * with `catalog` off there is nothing to index and the call answers 503.
48
+ */
49
+ catalogProducts: CatalogProductReadPort;
50
+ /**
51
+ * Feature 075 / D-87 — the category assignments and the category subtree the
52
+ * indexer projects, read over `catalog`'s published port instead of out of
53
+ * its `product_categories` / `categories` tables. Same binding `catalog`
54
+ * dependency as `catalogProducts`, and the same answer when it is off.
55
+ */
56
+ catalogCategories: CatalogCategoryReadPort;
57
+ /**
58
+ * The `sales_channel_products` bridge, through the sanctioned accessor
59
+ * (Constitution XII). Kernel-composed, so it is present for as long as the
60
+ * platform is.
61
+ */
62
+ salesChannelMembership: SalesChannelMembershipPort;
63
+ /**
64
+ * Universal-getter for Settings. Backs the suggest service's per-channel
65
+ * popup-count + minimum-query-length, the `settings.value_changed` →
66
+ * embedder reactor, and the reindex interval.
67
+ *
68
+ * Required since feature 072 (T123). It was optional for "foundation tests
69
+ * that predate Settings", and no such caller was left: both composition
70
+ * roots passed it, and absence silently downgraded the module to manifest
71
+ * defaults with no LLM reactor — a state nothing asked for and nothing
72
+ * detected.
73
+ */
74
+ settingsService: SettingsReadPort;
75
+ /**
76
+ * Feature 058 — resolves the `search.llm.embedder_credentials` reference into
77
+ * the embedder config, falling back per field to the legacy embedder settings.
78
+ * Injected as a narrow port (Principle I).
79
+ */
80
+ credentials: CredentialResolvePort;
81
+ /** Admin Settings write port — drives the `LlmToggleService.toggle` path. */
82
+ settingsAdminService: SettingsAdminPort;
83
+ /** Admin routes mount under `/api/v1/admin/search/*`. */
84
+ requireAdmin: RequireAdminFactory;
85
+ resolveAdminAuditContext: (req: FastifyRequest) => SettingsAdminAuditContext;
86
+ /**
87
+ * Typeahead suggestions carry the per-customer price-list resolution (SKU +
88
+ * image already ride on the summary), so the popup shows the price the
89
+ * searching user would actually pay.
90
+ */
91
+ enrichSuggestionPricing: SuggestionPricingEnricher;
92
+ /**
93
+ * Issue #132 — the pricing engine behind the *result list* (the enricher
94
+ * above covers the typeahead popup). A search hit is a listing and prices
95
+ * through the same chain as the catalogue grid.
96
+ */
97
+ listingPrices: ListingPricePort;
98
+ /**
99
+ * `organizations`' read model — the customer group the pricing engine keys a
100
+ * group-targeted list on. The enricher above already resolves it for the
101
+ * popup; the result list needs the same row, so a buyer who stops at the
102
+ * suggestions and one who presses Enter are quoted the same figure.
103
+ */
104
+ organizations: OrganizationDetailsPort;
105
+ /**
106
+ * When `true`, the module starts the periodic full-reindex sweep. The
107
+ * composition passes its deployment-role gate (`runWorkers`) here so the
108
+ * sweep only runs in worker/all processes, never in a dedicated
109
+ * `BACKEND_ROLE=api` process — and never in a test harness.
110
+ */
111
+ enableReindexScheduler: boolean;
112
+ }
113
+ export interface SearchModuleHandle {
114
+ indexer: SearchIndexer;
115
+ subscriber: SearchEventSubscriber;
116
+ searchQueryService: SearchQueryService;
117
+ suggestService: SearchSuggestService;
118
+ llmToggleService: LlmToggleService;
119
+ reindexWorker: SearchReindexWorker;
120
+ phraseRecorder: SearchPhraseRecorder;
121
+ /**
122
+ * The cadence the reindex timer reschedules itself on. Exposed because the
123
+ * property worth pinning is "the configured interval is the one used", and a
124
+ * self-rescheduling timer is not a thing a test can ask that of.
125
+ */
126
+ resolveReindexIntervalMinutes: () => Promise<number>;
127
+ }
128
+ export interface SearchModuleResult {
129
+ plugin: (app: FastifyInstance) => Promise<void>;
130
+ handle: SearchModuleHandle;
131
+ }
132
+ export declare function searchModule(options: SearchModuleOptions): SearchModuleResult;
133
+ //# sourceMappingURL=plugin.d.ts.map