@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,EACtB,gBAAgB,EAChB,uBAAuB,EACvB,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EACL,oBAAoB,EAIrB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AACpF,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAQzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAe5E;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B;;;;OAIG;IACH,oBAAoB,EAAE,wBAAwB,CAAC;IAC/C;;;;;OAKG;IACH,eAAe,EAAE,sBAAsB,CAAC;IACxC;;;;;OAKG;IACH,iBAAiB,EAAE,uBAAuB,CAAC;IAC3C;;;;OAIG;IACH,sBAAsB,EAAE,0BAA0B,CAAC;IACnD;;;;;;;;;;OAUG;IACH,eAAe,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,WAAW,EAAE,qBAAqB,CAAC;IACnC,6EAA6E;IAC7E,oBAAoB,EAAE,iBAAiB,CAAC;IACxC,yDAAyD;IACzD,YAAY,EAAE,mBAAmB,CAAC;IAClC,wBAAwB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,yBAAyB,CAAC;IAC7E;;;;OAIG;IACH,uBAAuB,EAAE,yBAAyB,CAAC;IACnD;;;;OAIG;IACH,aAAa,EAAE,gBAAgB,CAAC;IAChC;;;;;OAKG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,qBAAqB,CAAC;IAClC,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,cAAc,EAAE,oBAAoB,CAAC;IACrC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,aAAa,EAAE,mBAAmB,CAAC;IACnC,cAAc,EAAE,oBAAoB,CAAC;IACrC;;;;OAIG;IACH,6BAA6B,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAyQ7E"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SearchIndexer } from './services/search-indexer.js';
|
|
3
|
+
import { SearchEventSubscriber } from './services/search-event-subscriber.js';
|
|
4
|
+
import { SearchQueryService } from './services/search-query.service.js';
|
|
5
|
+
import { SearchSuggestService, DEFAULT_SUGGESTION_COUNT, DEFAULT_MINIMUM_QUERY_LENGTH, } from './services/search-suggest.service.js';
|
|
6
|
+
import { LlmToggleService } from './services/llm-toggle.service.js';
|
|
7
|
+
import { SearchReindexWorker } from './services/search-reindex-worker.js';
|
|
8
|
+
import { SearchPhraseRecorder } from './services/search-phrase-recorder.service.js';
|
|
9
|
+
import { registerSearchPublicRoutes, } from './routes.public.js';
|
|
10
|
+
import { registerSearchAdminRoutes } from './routes.admin.js';
|
|
11
|
+
import { SettingNotRegistered, SettingOutOfScopeForChannel } from '@endora-commerce/platform/kernel';
|
|
12
|
+
import { effectiveState } from '@endora-commerce/platform/kernel';
|
|
13
|
+
import { enterSystemScope } from '@endora-commerce/platform/kernel';
|
|
14
|
+
import { DEFAULT_INDEX_TASK_TIMEOUT_SECONDS, DEFAULT_REINDEX_INTERVAL_MINUTES, SEARCH_SETTING_CODES, } from '../manifest.js';
|
|
15
|
+
/**
|
|
16
|
+
* Conditions this module has already reported (D-43's warn-once). Module scope
|
|
17
|
+
* and never reset: an out-of-scope setting is a deployment fact, and the reindex
|
|
18
|
+
* timer would otherwise log it on every tick.
|
|
19
|
+
*/
|
|
20
|
+
const warnedSearchConditions = new Set();
|
|
21
|
+
function warnOnceForSearch(condition, message) {
|
|
22
|
+
if (warnedSearchConditions.has(condition))
|
|
23
|
+
return;
|
|
24
|
+
warnedSearchConditions.add(condition);
|
|
25
|
+
console.warn(message);
|
|
26
|
+
}
|
|
27
|
+
const numberSchema = z.number();
|
|
28
|
+
export function searchModule(options) {
|
|
29
|
+
/**
|
|
30
|
+
* The indexer's Meilisearch task wait, in milliseconds — this module's own
|
|
31
|
+
* setting, read inside the module (composition checklist item 6) and not a
|
|
32
|
+
* knob a composition root resolves on its behalf. It sits here rather than
|
|
33
|
+
* inside `SearchIndexer` for the same reason
|
|
34
|
+
* `resolveReindexIntervalMinutes` does: this is the one place that already
|
|
35
|
+
* holds the module's settings-read idiom, including which two failures may
|
|
36
|
+
* be absorbed.
|
|
37
|
+
*
|
|
38
|
+
* Re-read per wait rather than captured at boot, so raising it during an
|
|
39
|
+
* incident takes effect on the next task instead of on the next restart.
|
|
40
|
+
*
|
|
41
|
+
* Absorbs only what D-43 allows — a setting the reconciler has not written
|
|
42
|
+
* yet, and a channel-scoped override of a platform-wide read. A shape
|
|
43
|
+
* mismatch or a driver error propagates, and so does `ModuleDisabledError`:
|
|
44
|
+
* neither branch below is a status test.
|
|
45
|
+
*/
|
|
46
|
+
const resolveIndexTaskTimeoutMs = async () => {
|
|
47
|
+
try {
|
|
48
|
+
const seconds = await options.settingsService.get(SEARCH_SETTING_CODES.INDEX_TASK_TIMEOUT_SECONDS, null, z.number().int().positive());
|
|
49
|
+
return seconds * 1000;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof SettingNotRegistered) {
|
|
53
|
+
return DEFAULT_INDEX_TASK_TIMEOUT_SECONDS * 1000;
|
|
54
|
+
}
|
|
55
|
+
if (error instanceof SettingOutOfScopeForChannel) {
|
|
56
|
+
warnOnceForSearch(`out-of-scope:${SEARCH_SETTING_CODES.INDEX_TASK_TIMEOUT_SECONDS}`, `[search] setting "${SEARCH_SETTING_CODES.INDEX_TASK_TIMEOUT_SECONDS}" is scoped ` +
|
|
57
|
+
`to specific sales channels, so it has no platform-wide value — falling back ` +
|
|
58
|
+
`to the manifest default (logged once per process).`);
|
|
59
|
+
return DEFAULT_INDEX_TASK_TIMEOUT_SECONDS * 1000;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const indexer = new SearchIndexer({
|
|
65
|
+
attributeRead: options.catalogAttributeRead,
|
|
66
|
+
products: options.catalogProducts,
|
|
67
|
+
categories: options.catalogCategories,
|
|
68
|
+
channelMembership: options.salesChannelMembership,
|
|
69
|
+
resolveTaskTimeoutMs: resolveIndexTaskTimeoutMs,
|
|
70
|
+
});
|
|
71
|
+
// Handlers only: `backend.ts` registers them through `ctx.subscribe`, which
|
|
72
|
+
// is what makes this module's effective state decide whether they run.
|
|
73
|
+
const subscriber = new SearchEventSubscriber({
|
|
74
|
+
emFactory: options.emFactory,
|
|
75
|
+
indexer,
|
|
76
|
+
...(options.settingsService !== undefined
|
|
77
|
+
? { settingsService: options.settingsService }
|
|
78
|
+
: {}),
|
|
79
|
+
...(options.credentials !== undefined ? { credentials: options.credentials } : {}),
|
|
80
|
+
});
|
|
81
|
+
const searchQueryService = new SearchQueryService(options.catalogProducts, options.catalogAttributeRead, {}, options.listingPrices, options.organizations);
|
|
82
|
+
// Settings-aware suggest config, with fallback to manifest defaults
|
|
83
|
+
// when the resolver fails for any reason (e.g. Redis hiccup,
|
|
84
|
+
// `SettingNotRegistered`). The popup must never 500 because of a
|
|
85
|
+
// Settings glitch.
|
|
86
|
+
const resolveSuggestConfig = async (ctx) => {
|
|
87
|
+
const fallback = {
|
|
88
|
+
defaultLimit: DEFAULT_SUGGESTION_COUNT,
|
|
89
|
+
minimumQueryLength: DEFAULT_MINIMUM_QUERY_LENGTH,
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
// Feature 053 / FR-002: per-channel settings key off the channel
|
|
93
|
+
// resolved once upstream — no re-resolution here.
|
|
94
|
+
const channelId = ctx.resolvedChannel.id;
|
|
95
|
+
const [defaultLimit, minimumQueryLength] = await Promise.all([
|
|
96
|
+
options.settingsService.get(SEARCH_SETTING_CODES.POPUP_SUGGESTION_COUNT, channelId, numberSchema),
|
|
97
|
+
options.settingsService.get(SEARCH_SETTING_CODES.POPUP_MINIMUM_QUERY_LENGTH, channelId, numberSchema),
|
|
98
|
+
]);
|
|
99
|
+
return { defaultLimit, minimumQueryLength };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const suggestService = new SearchSuggestService(searchQueryService, resolveSuggestConfig);
|
|
106
|
+
const llmToggleService = new LlmToggleService(options.emFactory, options.settingsService, options.settingsAdminService, options.credentials);
|
|
107
|
+
/**
|
|
108
|
+
* The sweep's cadence is this module's own setting, so it reads it itself
|
|
109
|
+
* rather than taking a resolver from a composition root — which is where the
|
|
110
|
+
* identical `try`/`catch`-to-the-manifest-default lived before T123.
|
|
111
|
+
*
|
|
112
|
+
* Read **platform-wide** (feature 072, D-41). A background reindex is one
|
|
113
|
+
* timer in one process covering every channel's index, so there is no channel
|
|
114
|
+
* whose value it could sensibly take; it used to pass the literal `'default'`,
|
|
115
|
+
* a channel **code** against a `uuid` column, so PostgreSQL rejected every
|
|
116
|
+
* read, the `catch` answered with the constant, and an operator changing the
|
|
117
|
+
* cadence changed nothing.
|
|
118
|
+
*
|
|
119
|
+
* Absorbs only what D-43 allows; a shape mismatch or a driver error
|
|
120
|
+
* propagates to the caller, which logs it and keeps polling.
|
|
121
|
+
*/
|
|
122
|
+
const resolveReindexIntervalMinutes = async () => {
|
|
123
|
+
try {
|
|
124
|
+
return await options.settingsService.get(SEARCH_SETTING_CODES.REINDEX_INTERVAL_MINUTES, null, z.number().int().nonnegative());
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof SettingNotRegistered)
|
|
128
|
+
return DEFAULT_REINDEX_INTERVAL_MINUTES;
|
|
129
|
+
if (error instanceof SettingOutOfScopeForChannel) {
|
|
130
|
+
warnOnceForSearch(`out-of-scope:${SEARCH_SETTING_CODES.REINDEX_INTERVAL_MINUTES}`, `[search] setting "${SEARCH_SETTING_CODES.REINDEX_INTERVAL_MINUTES}" is scoped ` +
|
|
131
|
+
`to specific sales channels, so it has no platform-wide value — falling back ` +
|
|
132
|
+
`to the manifest default (logged once per process).`);
|
|
133
|
+
return DEFAULT_REINDEX_INTERVAL_MINUTES;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const phraseRecorder = new SearchPhraseRecorder(options.emFactory, options.settingsService);
|
|
139
|
+
const reindexWorker = new SearchReindexWorker({
|
|
140
|
+
emFactory: options.emFactory,
|
|
141
|
+
indexer,
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
handle: {
|
|
145
|
+
indexer,
|
|
146
|
+
subscriber,
|
|
147
|
+
searchQueryService,
|
|
148
|
+
suggestService,
|
|
149
|
+
reindexWorker,
|
|
150
|
+
phraseRecorder,
|
|
151
|
+
llmToggleService,
|
|
152
|
+
resolveReindexIntervalMinutes,
|
|
153
|
+
},
|
|
154
|
+
plugin: async (app) => {
|
|
155
|
+
// US1 — typeahead popup feed.
|
|
156
|
+
// US3 — analytics ingest. Both live in routes.public.ts.
|
|
157
|
+
await registerSearchPublicRoutes(app, {
|
|
158
|
+
suggestService,
|
|
159
|
+
phraseRecorder,
|
|
160
|
+
enrichSuggestionPricing: options.enrichSuggestionPricing,
|
|
161
|
+
});
|
|
162
|
+
// US2 — LLM toggle wrapper. Mounts only when the admin gate +
|
|
163
|
+
// settings admin service are both wired (test-server passes them).
|
|
164
|
+
await registerSearchAdminRoutes(app, {
|
|
165
|
+
llmToggleService,
|
|
166
|
+
reindexWorker,
|
|
167
|
+
requireAdmin: options.requireAdmin,
|
|
168
|
+
resolveAdminAuditContext: options.resolveAdminAuditContext,
|
|
169
|
+
});
|
|
170
|
+
// Periodic full Meilisearch reindex (FR — keep the catalogue in sync
|
|
171
|
+
// even when an incremental event was missed). The interval is read
|
|
172
|
+
// from Settings on every tick via a self-rescheduling timer, so an
|
|
173
|
+
// operator changing `search.reindex_interval_minutes` takes effect on
|
|
174
|
+
// the next cycle without a restart. A value <= 0 disables the sweep but
|
|
175
|
+
// the timer keeps polling the setting so it can be re-enabled live.
|
|
176
|
+
if (options.enableReindexScheduler) {
|
|
177
|
+
const DISABLED_POLL_MS = 60_000;
|
|
178
|
+
let timer;
|
|
179
|
+
let stopped = false;
|
|
180
|
+
const scheduleNext = (delayMs) => {
|
|
181
|
+
if (stopped)
|
|
182
|
+
return;
|
|
183
|
+
timer = setTimeout(tick, delayMs);
|
|
184
|
+
if (typeof timer.unref === 'function')
|
|
185
|
+
timer.unref();
|
|
186
|
+
};
|
|
187
|
+
const tick = () => {
|
|
188
|
+
// Presence is decided here, before any work and outside the `try`s
|
|
189
|
+
// below (issue #126). A timer callback has nowhere to throw to, so
|
|
190
|
+
// `ModuleDisabledError` cannot propagate from it and must be decided;
|
|
191
|
+
// asked from inside one of those `try`s, a switched-off module and a
|
|
192
|
+
// failed sweep would land in the same handler.
|
|
193
|
+
//
|
|
194
|
+
// The reschedule is deliberate and is not a leak: this timer *is* the
|
|
195
|
+
// loop, so returning without re-arming would stop the scheduler for
|
|
196
|
+
// the life of the process and no re-enable would bring it back. It is
|
|
197
|
+
// the same shape as the `minutes <= 0` branch below — keep polling,
|
|
198
|
+
// do nothing.
|
|
199
|
+
if (!effectiveState.isPresent('search')) {
|
|
200
|
+
scheduleNext(DISABLED_POLL_MS);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Feature 072 (FR-020), issue #128 — the timer is the entry point, so
|
|
204
|
+
// the scope opens here: the setting read and the sweep both query, and
|
|
205
|
+
// outside a scope they would run on whatever tenancy the ambient store
|
|
206
|
+
// happened to hold. It goes around the *work* and not around the guard
|
|
207
|
+
// above, and the reschedule stays outside it, because this timer is the
|
|
208
|
+
// loop — a tick that failed to re-arm would stop the scheduler for the
|
|
209
|
+
// life of the process. `reindex()` is also reachable from the admin
|
|
210
|
+
// route, where the request's scope is already open and a second one
|
|
211
|
+
// would be wrong; that is why the scope lives at the timer rather than
|
|
212
|
+
// inside the worker.
|
|
213
|
+
void enterSystemScope('search: periodic reindex sweep', async () => {
|
|
214
|
+
let minutes = 0;
|
|
215
|
+
try {
|
|
216
|
+
minutes = await resolveReindexIntervalMinutes();
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
app.log.error({ err }, 'search reindex: failed to resolve interval setting');
|
|
220
|
+
}
|
|
221
|
+
// Disabled — poll on, so a re-enable takes effect live.
|
|
222
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
223
|
+
return DISABLED_POLL_MS;
|
|
224
|
+
try {
|
|
225
|
+
const result = await reindexWorker.reindex();
|
|
226
|
+
app.log.info({ result }, 'search reindex sweep completed');
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
app.log.error({ err }, 'search reindex sweep failed');
|
|
230
|
+
}
|
|
231
|
+
return minutes * 60_000;
|
|
232
|
+
}, { entryPoint: 'interval' })
|
|
233
|
+
.then(scheduleNext)
|
|
234
|
+
.catch((err) => {
|
|
235
|
+
app.log.error({ err }, 'search reindex tick failed');
|
|
236
|
+
scheduleNext(DISABLED_POLL_MS);
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
// Kick off the first poll without an immediate reindex at boot.
|
|
240
|
+
scheduleNext(DISABLED_POLL_MS);
|
|
241
|
+
app.addHook('onClose', async () => {
|
|
242
|
+
stopped = true;
|
|
243
|
+
if (timer)
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import { type SettingsAdminAuditContext } from '@endora-commerce/contracts';
|
|
3
|
+
import type { LlmToggleService } from './services/llm-toggle.service.js';
|
|
4
|
+
import type { SearchReindexWorker } from './services/search-reindex-worker.js';
|
|
5
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
6
|
+
/**
|
|
7
|
+
* Admin HTTP surface — feature 006 / US2 (T025).
|
|
8
|
+
*
|
|
9
|
+
* Mounts under `/api/v1/admin/search/*`:
|
|
10
|
+
* - POST /llm/toggle — flip `search.llm.enabled` on/off with cross-
|
|
11
|
+
* setting validation. Refuses to enable for any channel whose
|
|
12
|
+
* embedder.* triplet is incomplete (FR-011).
|
|
13
|
+
*
|
|
14
|
+
* The three embedder.* values themselves are still saved through the
|
|
15
|
+
* generic Settings admin route (`PUT /api/v1/admin/settings/:code/value`).
|
|
16
|
+
* That keeps the per-field audit trail intact while the toggle remains
|
|
17
|
+
* the single guarded entry point for `enabled`.
|
|
18
|
+
*/
|
|
19
|
+
export interface SearchAdminDeps {
|
|
20
|
+
llmToggleService: LlmToggleService;
|
|
21
|
+
/** Backs the manual "Reindex now" admin action. */
|
|
22
|
+
reindexWorker: SearchReindexWorker;
|
|
23
|
+
requireAdmin: RequireAdminFactory;
|
|
24
|
+
/** Resolves the audit actor from the request; mirrors Settings' shape. */
|
|
25
|
+
resolveAdminAuditContext?: (req: FastifyRequest) => SettingsAdminAuditContext;
|
|
26
|
+
}
|
|
27
|
+
export declare function registerSearchAdminRoutes(app: FastifyInstance, deps: SearchAdminDeps): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=routes.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAA0B,KAAK,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACpG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,mDAAmD;IACnD,aAAa,EAAE,mBAAmB,CAAC;IACnC,YAAY,EAAE,mBAAmB,CAAC;IAClC,0EAA0E;IAC1E,wBAAwB,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,yBAAyB,CAAC;CAC/E;AAED,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAwCf"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LlmToggleRequestSchema } from '@endora-commerce/contracts';
|
|
2
|
+
export async function registerSearchAdminRoutes(app, deps) {
|
|
3
|
+
const { llmToggleService, reindexWorker, requireAdmin, resolveAdminAuditContext } = deps;
|
|
4
|
+
// Manual full Meilisearch reindex — the on-demand equivalent of the
|
|
5
|
+
// periodic `search.reindex_interval_minutes` sweep. Runs synchronously and
|
|
6
|
+
// returns a per-run summary. Gated by the existing `search:write`
|
|
7
|
+
// permission (no new permission code introduced).
|
|
8
|
+
app.post('/api/v1/admin/search/reindex', { preHandler: requireAdmin('search:write') }, async (_request, reply) => {
|
|
9
|
+
const result = await reindexWorker.reindex();
|
|
10
|
+
return reply.send(result);
|
|
11
|
+
});
|
|
12
|
+
app.post('/api/v1/admin/search/llm/toggle', { preHandler: requireAdmin('search:write') }, async (request) => {
|
|
13
|
+
const body = LlmToggleRequestSchema.parse(request.body);
|
|
14
|
+
const actor = resolveAdminAuditContext
|
|
15
|
+
? resolveAdminAuditContext(request)
|
|
16
|
+
: { actorAdminUserId: null };
|
|
17
|
+
const result = await llmToggleService.toggle({
|
|
18
|
+
enabled: body.enabled,
|
|
19
|
+
...(body.salesChannelCodes !== undefined
|
|
20
|
+
? { salesChannelCodes: body.salesChannelCodes }
|
|
21
|
+
: {}),
|
|
22
|
+
expectedVersion: body.expectedVersion ?? null,
|
|
23
|
+
actor,
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
code: 'search.llm.enabled',
|
|
27
|
+
enabled: result.enabled,
|
|
28
|
+
newVersion: result.newVersion,
|
|
29
|
+
appliedChannelIds: result.appliedChannelIds,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=routes.admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAkC,MAAM,4BAA4B,CAAC;AA4BpG,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,GAAoB,EACpB,IAAqB;IAErB,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,wBAAwB,EAAE,GAAG,IAAI,CAAC;IAEzF,oEAAoE;IACpE,2EAA2E;IAC3E,kEAAkE;IAClE,kDAAkD;IAClD,GAAG,CAAC,IAAI,CACN,8BAA8B,EAC9B,EAAE,UAAU,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,EAC5C,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACxB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC;QAC7C,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,iCAAiC,EACjC,EAAE,UAAU,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,EAC5C,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,sBAAsB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,KAAK,GAA8B,wBAAwB;YAC/D,CAAC,CAAC,wBAAwB,CAAC,OAAO,CAAC;YACnC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC;YAC3C,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,CAAC,IAAI,CAAC,iBAAiB,KAAK,SAAS;gBACtC,CAAC,CAAC,EAAE,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE;gBAC/C,CAAC,CAAC,EAAE,CAAC;YACP,eAAe,EAAE,IAAI,CAAC,eAAe,IAAI,IAAI;YAC7C,KAAK;SACN,CAAC,CAAC;QACH,OAAO;YACL,IAAI,EAAE,oBAA6B;YACnC,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;SAC5C,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { type ProductSummary, type SearchSuggestItem } from '@endora-commerce/contracts';
|
|
3
|
+
import { type SearchSuggestService } from './services/search-suggest.service.js';
|
|
4
|
+
import type { SearchPhraseRecorder } from './services/search-phrase-recorder.service.js';
|
|
5
|
+
export type SuggestionPricingEnricher = (items: ProductSummary[], ctx: {
|
|
6
|
+
/** The request's resolved sales channel (feature 053 / FR-002). */
|
|
7
|
+
resolvedChannel: {
|
|
8
|
+
id: string;
|
|
9
|
+
defaultCurrency: string;
|
|
10
|
+
};
|
|
11
|
+
/** Organization of the signed-in customer, or null for guests. */
|
|
12
|
+
organizationId?: string | null;
|
|
13
|
+
}) => Promise<SearchSuggestItem[]>;
|
|
14
|
+
export interface SearchPublicDeps {
|
|
15
|
+
suggestService: SearchSuggestService;
|
|
16
|
+
/** When provided, `POST /api/v1/search/record` is mounted (US3). */
|
|
17
|
+
phraseRecorder?: SearchPhraseRecorder;
|
|
18
|
+
/** When provided, suggestions carry the per-customer resolved price. */
|
|
19
|
+
enrichSuggestionPricing?: SuggestionPricingEnricher;
|
|
20
|
+
}
|
|
21
|
+
export declare function registerSearchPublicRoutes(app: FastifyInstance, deps: SearchPublicDeps): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=routes.public.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.public.d.ts","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAE/D,OAAO,EAOL,KAAK,cAAc,EAEnB,KAAK,iBAAiB,EAEvB,MAAM,4BAA4B,CAAC;AAQpC,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AA+CzF,MAAM,MAAM,yBAAyB,GAAG,CACtC,KAAK,EAAE,cAAc,EAAE,EACvB,GAAG,EAAE;IACH,mEAAmE;IACnE,eAAe,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,KACE,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;AAElC,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,oBAAoB,CAAC;IACrC,oEAAoE;IACpE,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,wEAAwE;IACxE,uBAAuB,CAAC,EAAE,yBAAyB,CAAC;CACrD;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CAmHf"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ERROR_CODES, RecordPhraseRequestSchema, SearchSuggestQuerySchema, SEARCH_PHRASE_MAX_LENGTH, SEARCH_SUGGEST_LIMIT_MAX, } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { markPersonalisedPricing, productAudienceOf } from '@endora-commerce/platform/http';
|
|
5
|
+
import { currentSalesChannel, getResolvedChannel, } from '@endora-commerce/platform/kernel';
|
|
6
|
+
import { QueryTooShort, SearchBackendUnavailable, } from './services/search-suggest.service.js';
|
|
7
|
+
/**
|
|
8
|
+
* Public HTTP surface — feature 006 / US1 (T013) and US3 (T036).
|
|
9
|
+
*
|
|
10
|
+
* Routes mounted under `/api/v1/search/*`:
|
|
11
|
+
* - GET /suggest — typeahead popup feed.
|
|
12
|
+
* - POST /record — analytics ingest. Returns 202 immediately;
|
|
13
|
+
* persistence is queued via the recorder service
|
|
14
|
+
* so the storefront response is never delayed
|
|
15
|
+
* (FR-015).
|
|
16
|
+
*
|
|
17
|
+
* Sales-channel scoping mirrors the catalog public routes: the channel
|
|
18
|
+
* is resolved from the request scope (filled by the resolver
|
|
19
|
+
* middleware) with a header fallback for the contract-level read in
|
|
20
|
+
* `readContext`.
|
|
21
|
+
*/
|
|
22
|
+
const acceptLanguageHeaderSchema = z.string().optional();
|
|
23
|
+
/**
|
|
24
|
+
* Enriches raw suggestions with the per-customer price-list resolution so the
|
|
25
|
+
* typeahead popup can show the price the searching user would actually pay,
|
|
26
|
+
* honouring their price list and price-visibility. Wired from the composition
|
|
27
|
+
* root (where the price-lists `PricingService` lives). When absent, the popup
|
|
28
|
+
* degrades to the legacy `ProductSummary.price` projection.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The signed-in customer's organization, or `null`.
|
|
32
|
+
*
|
|
33
|
+
* `request.actor` is not on `FastifyRequest`: it is a declaration-merging
|
|
34
|
+
* augmentation the `auth` module contributes (`modules/auth/plugin.ts`). Inside
|
|
35
|
+
* `backend/src` it arrived ambiently through the host's own `types` graph, so
|
|
36
|
+
* nothing in this module ever named it; a package compiles against its own
|
|
37
|
+
* manifest and `auth` publishes nothing a package can name, so the property is
|
|
38
|
+
* simply not there (TS2339). Read structurally and narrowed rather than
|
|
39
|
+
* asserted — an absent actor answers `null`, which is the guest price, exactly
|
|
40
|
+
* as before.
|
|
41
|
+
*/
|
|
42
|
+
function customerOrganizationId(request) {
|
|
43
|
+
const { actor } = request;
|
|
44
|
+
if (actor?.kind !== 'customer')
|
|
45
|
+
return null;
|
|
46
|
+
return actor.organizationId ?? null;
|
|
47
|
+
}
|
|
48
|
+
export async function registerSearchPublicRoutes(app, deps) {
|
|
49
|
+
const { suggestService, phraseRecorder, enrichSuggestionPricing } = deps;
|
|
50
|
+
// POST /api/v1/search/record — feature 006 / US3 / T036.
|
|
51
|
+
// Mounted only when a recorder was wired (search/plugin.ts gates
|
|
52
|
+
// this on `settingsService` being provided so the recorder can read
|
|
53
|
+
// the per-channel minimum-length threshold).
|
|
54
|
+
if (phraseRecorder) {
|
|
55
|
+
app.post('/api/v1/search/record', async (request, reply) => {
|
|
56
|
+
const parseResult = RecordPhraseRequestSchema.safeParse(request.body);
|
|
57
|
+
if (!parseResult.success) {
|
|
58
|
+
// The two negative paths the contract calls out
|
|
59
|
+
// (PHRASE_REQUIRED, PHRASE_TOO_LONG) are derived from the Zod
|
|
60
|
+
// issue codes so the wire surface is stable.
|
|
61
|
+
const issue = parseResult.error.issues[0];
|
|
62
|
+
const code = issue?.path[0] === 'phrase' && issue?.code === 'too_big'
|
|
63
|
+
? ERROR_CODES.PHRASE_TOO_LONG
|
|
64
|
+
: issue?.path[0] === 'resultCount'
|
|
65
|
+
? ERROR_CODES.RESULT_COUNT_INVALID
|
|
66
|
+
: ERROR_CODES.PHRASE_REQUIRED;
|
|
67
|
+
throw new HttpError(400, code, issue?.message ?? 'Invalid record phrase request.', parseResult.error.issues.map((i) => ({
|
|
68
|
+
path: i.path.join('.') || '(root)',
|
|
69
|
+
issue: i.message,
|
|
70
|
+
})));
|
|
71
|
+
}
|
|
72
|
+
// The resolver middleware (kernel) fills the request scope's channel
|
|
73
|
+
// slot. Storefront paths fall back to the system-default channel; admin
|
|
74
|
+
// paths refuse with MISSING_SALES_CHANNEL_CONTEXT before reaching this
|
|
75
|
+
// handler.
|
|
76
|
+
const salesChannelId = currentSalesChannel()?.id;
|
|
77
|
+
if (!salesChannelId) {
|
|
78
|
+
throw new HttpError(400, ERROR_CODES.MISSING_SALES_CHANNEL_CONTEXT, 'No sales channel could be resolved for this request.');
|
|
79
|
+
}
|
|
80
|
+
// Fire-and-forget: the recorder swallows its own errors and the
|
|
81
|
+
// storefront response is not delayed by persistence (FR-015).
|
|
82
|
+
void phraseRecorder.record({
|
|
83
|
+
phrase: parseResult.data.phrase,
|
|
84
|
+
salesChannelId,
|
|
85
|
+
resultCount: parseResult.data.resultCount ?? 0,
|
|
86
|
+
});
|
|
87
|
+
return reply.code(202).send({ ok: true });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// GET /api/v1/search/suggest
|
|
91
|
+
app.get('/api/v1/search/suggest', async (request, reply) => {
|
|
92
|
+
const parsed = parseSuggestQuery(request);
|
|
93
|
+
const ctx = readContext(request);
|
|
94
|
+
// The popup has carried the buyer's own resolved price since feature 075
|
|
95
|
+
// and said nothing about it to a cache. It is the same private response the
|
|
96
|
+
// result feed below it now returns, so it gets the same header; the
|
|
97
|
+
// storefront's own typeahead fetch already asks for `no-store`.
|
|
98
|
+
markPersonalisedPricing(reply, ctx.audience);
|
|
99
|
+
try {
|
|
100
|
+
const result = await suggestService.suggest({
|
|
101
|
+
q: parsed.q,
|
|
102
|
+
...(parsed.limit !== undefined ? { limit: parsed.limit } : {}),
|
|
103
|
+
}, ctx);
|
|
104
|
+
// Enrich with the per-customer price-list resolution when wired. The
|
|
105
|
+
// organization comes from the request actor (the suggest fetch carries
|
|
106
|
+
// the session cookie), so a signed-in buyer sees their negotiated price
|
|
107
|
+
// and a guest sees the public one. Enrichment failures degrade to the
|
|
108
|
+
// raw projection — the popup must never 500 over a pricing hiccup.
|
|
109
|
+
if (enrichSuggestionPricing) {
|
|
110
|
+
const organizationId = customerOrganizationId(request);
|
|
111
|
+
try {
|
|
112
|
+
const enriched = await enrichSuggestionPricing(result.data, {
|
|
113
|
+
resolvedChannel: ctx.resolvedChannel,
|
|
114
|
+
organizationId,
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
data: enriched,
|
|
118
|
+
meta: result.meta,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// fall through to the un-enriched response
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof QueryTooShort) {
|
|
129
|
+
throw new HttpError(400, ERROR_CODES.QUERY_TOO_SHORT, `Query is shorter than the minimum length of ${err.minimumQueryLength} characters.`, [{ path: 'q', issue: 'phrase below minimum length' }]);
|
|
130
|
+
}
|
|
131
|
+
if (err instanceof SearchBackendUnavailable) {
|
|
132
|
+
throw new HttpError(503, ERROR_CODES.SEARCH_BACKEND_UNAVAILABLE, 'Search is temporarily unavailable.');
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Parses + validates `?q=…&limit=N`. Returns the canonicalised pair or
|
|
140
|
+
* raises an HttpError that the route handler does NOT need to translate.
|
|
141
|
+
*
|
|
142
|
+
* We hand-parse instead of letting Zod's `safeParse` populate the body
|
|
143
|
+
* because we want the search-specific error codes (`QUERY_TOO_LONG`,
|
|
144
|
+
* `LIMIT_OUT_OF_RANGE`) rather than the generic `VALIDATION_FAILED`.
|
|
145
|
+
*/
|
|
146
|
+
function parseSuggestQuery(request) {
|
|
147
|
+
const raw = (request.query ?? {});
|
|
148
|
+
const qRaw = typeof raw['q'] === 'string' ? raw['q'] : undefined;
|
|
149
|
+
const limitRaw = typeof raw['limit'] === 'string' ? Number(raw['limit']) : undefined;
|
|
150
|
+
if (qRaw === undefined || qRaw.length === 0) {
|
|
151
|
+
throw new HttpError(400, ERROR_CODES.QUERY_TOO_SHORT, 'Query parameter "q" is required.', [{ path: 'q', issue: 'missing' }]);
|
|
152
|
+
}
|
|
153
|
+
if (qRaw.length > SEARCH_PHRASE_MAX_LENGTH) {
|
|
154
|
+
throw new HttpError(400, ERROR_CODES.QUERY_TOO_LONG, `Query exceeds the maximum length of ${SEARCH_PHRASE_MAX_LENGTH} characters.`, [{ path: 'q', issue: 'phrase too long' }]);
|
|
155
|
+
}
|
|
156
|
+
let limit;
|
|
157
|
+
if (limitRaw !== undefined) {
|
|
158
|
+
if (!Number.isInteger(limitRaw) ||
|
|
159
|
+
limitRaw <= 0 ||
|
|
160
|
+
limitRaw > SEARCH_SUGGEST_LIMIT_MAX) {
|
|
161
|
+
throw new HttpError(400, ERROR_CODES.LIMIT_OUT_OF_RANGE, `Limit must be an integer between 1 and ${SEARCH_SUGGEST_LIMIT_MAX}.`, [{ path: 'limit', issue: 'out of range' }]);
|
|
162
|
+
}
|
|
163
|
+
limit = limitRaw;
|
|
164
|
+
}
|
|
165
|
+
// Round-trip through the contract schema so the type system knows the
|
|
166
|
+
// values match the wire shape exactly. The schema mirrors the manual
|
|
167
|
+
// checks above, so this never throws — but it would catch any future
|
|
168
|
+
// drift between the route and the contract.
|
|
169
|
+
SearchSuggestQuerySchema.parse({ q: qRaw, ...(limit !== undefined ? { limit } : {}) });
|
|
170
|
+
return { q: qRaw, ...(limit !== undefined ? { limit } : {}) };
|
|
171
|
+
}
|
|
172
|
+
function readContext(request) {
|
|
173
|
+
// Feature 053 / FR-002: read the channel resolved once by the canonical
|
|
174
|
+
// middleware instead of re-parsing the `x-sales-channel` header.
|
|
175
|
+
const ch = getResolvedChannel(request);
|
|
176
|
+
const acceptLanguage = acceptLanguageHeaderSchema.parse(request.headers['accept-language']);
|
|
177
|
+
const preferredLanguage = acceptLanguage
|
|
178
|
+
? acceptLanguage.split(',')[0]?.trim()
|
|
179
|
+
: undefined;
|
|
180
|
+
return {
|
|
181
|
+
resolvedChannel: {
|
|
182
|
+
id: ch.id,
|
|
183
|
+
code: ch.code,
|
|
184
|
+
isPublic: ch.isPublic,
|
|
185
|
+
defaultCurrency: ch.defaultCurrency,
|
|
186
|
+
defaultLanguage: ch.defaultLanguage,
|
|
187
|
+
},
|
|
188
|
+
audience: productAudienceOf(request),
|
|
189
|
+
...(preferredLanguage !== undefined ? { preferredLanguage } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=routes.public.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.public.js","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ResolveResult } from '@endora-commerce/contracts';
|
|
2
|
+
import { type SettingsReadPort } from '@endora-commerce/platform/kernel';
|
|
3
|
+
/** Narrow port over CredentialsService.resolve (feature 058, Principle I). */
|
|
4
|
+
export interface CredentialResolvePort {
|
|
5
|
+
resolve(configurationCode: string): Promise<ResolveResult>;
|
|
6
|
+
}
|
|
7
|
+
export interface EmbedderConfig {
|
|
8
|
+
url: string;
|
|
9
|
+
apiKey: string;
|
|
10
|
+
model: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the Meilisearch embedder config for a channel (feature 058, T060).
|
|
14
|
+
*
|
|
15
|
+
* The `search.llm.embedder_credentials` reference (a reusable `llm` credential
|
|
16
|
+
* configuration) is the SINGLE source: its `baseUrl` → embedder URL, `apiKey` →
|
|
17
|
+
* embedder API key, `model` → embedder model. When the reference is unset or
|
|
18
|
+
* unresolvable, every field comes back empty and the caller treats LLM search as
|
|
19
|
+
* not configured (fail closed). There are no separate embedder settings.
|
|
20
|
+
*
|
|
21
|
+
* The secret `apiKey` is decrypted in-memory by `resolve`; it never leaves the
|
|
22
|
+
* process.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveEmbedderConfig(settings: SettingsReadPort, channelId: string, credentials?: CredentialResolvePort): Promise<EmbedderConfig>;
|
|
25
|
+
//# sourceMappingURL=embedder-config-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedder-config-resolver.d.ts","sourceRoot":"","sources":["../../../src/backend/services/embedder-config-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAGlG,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,qBAAqB,GAClC,OAAO,CAAC,cAAc,CAAC,CAmCzB"}
|