@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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/backend/cli/reindex.d.ts +4 -0
- package/dist/backend/cli/reindex.d.ts.map +1 -0
- package/dist/backend/cli/reindex.js +32 -0
- package/dist/backend/cli/reindex.js.map +1 -0
- package/dist/backend/entities/search-phrase-record.entity.d.ts +40 -0
- package/dist/backend/entities/search-phrase-record.entity.d.ts.map +1 -0
- package/dist/backend/entities/search-phrase-record.entity.js +85 -0
- package/dist/backend/entities/search-phrase-record.entity.js.map +1 -0
- package/dist/backend/index.d.ts +87 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +153 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +133 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +250 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +28 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +33 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.public.d.ts +22 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +192 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/embedder-config-resolver.d.ts +25 -0
- package/dist/backend/services/embedder-config-resolver.d.ts.map +1 -0
- package/dist/backend/services/embedder-config-resolver.js +47 -0
- package/dist/backend/services/embedder-config-resolver.js.map +1 -0
- package/dist/backend/services/llm-toggle.service.d.ts +50 -0
- package/dist/backend/services/llm-toggle.service.d.ts.map +1 -0
- package/dist/backend/services/llm-toggle.service.js +78 -0
- package/dist/backend/services/llm-toggle.service.js.map +1 -0
- package/dist/backend/services/search-event-subscriber.d.ts +125 -0
- package/dist/backend/services/search-event-subscriber.d.ts.map +1 -0
- package/dist/backend/services/search-event-subscriber.js +113 -0
- package/dist/backend/services/search-event-subscriber.js.map +1 -0
- package/dist/backend/services/search-indexer.d.ts +346 -0
- package/dist/backend/services/search-indexer.d.ts.map +1 -0
- package/dist/backend/services/search-indexer.js +670 -0
- package/dist/backend/services/search-indexer.js.map +1 -0
- package/dist/backend/services/search-phrase-recorder.service.d.ts +25 -0
- package/dist/backend/services/search-phrase-recorder.service.d.ts.map +1 -0
- package/dist/backend/services/search-phrase-recorder.service.js +89 -0
- package/dist/backend/services/search-phrase-recorder.service.js.map +1 -0
- package/dist/backend/services/search-query-port.d.ts +29 -0
- package/dist/backend/services/search-query-port.d.ts.map +1 -0
- package/dist/backend/services/search-query-port.js +20 -0
- package/dist/backend/services/search-query-port.js.map +1 -0
- package/dist/backend/services/search-query.service.d.ts +205 -0
- package/dist/backend/services/search-query.service.d.ts.map +1 -0
- package/dist/backend/services/search-query.service.js +346 -0
- package/dist/backend/services/search-query.service.js.map +1 -0
- package/dist/backend/services/search-reindex-worker.d.ts +27 -0
- package/dist/backend/services/search-reindex-worker.d.ts.map +1 -0
- package/dist/backend/services/search-reindex-worker.js +13 -0
- package/dist/backend/services/search-reindex-worker.js.map +1 -0
- package/dist/backend/services/search-suggest.service.d.ts +53 -0
- package/dist/backend/services/search-suggest.service.d.ts.map +1 -0
- package/dist/backend/services/search-suggest.service.js +64 -0
- package/dist/backend/services/search-suggest.service.js.map +1 -0
- package/dist/backend/services/suggestion-pricing-enricher.d.ts +62 -0
- package/dist/backend/services/suggestion-pricing-enricher.d.ts.map +1 -0
- package/dist/backend/services/suggestion-pricing-enricher.js +41 -0
- package/dist/backend/services/suggestion-pricing-enricher.js.map +1 -0
- package/dist/manifest.d.ts +263 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +300 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts +13 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts.map +1 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.js +40 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/search.md +284 -0
- package/i18n/en.json +10 -0
- package/i18n/pl.json +10 -0
- 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
|