@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
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
3
+ import { SEARCH_SETTING_CODES } from '../../manifest.js';
4
+ const stringSchema = z.string();
5
+ /**
6
+ * Resolve the Meilisearch embedder config for a channel (feature 058, T060).
7
+ *
8
+ * The `search.llm.embedder_credentials` reference (a reusable `llm` credential
9
+ * configuration) is the SINGLE source: its `baseUrl` → embedder URL, `apiKey` →
10
+ * embedder API key, `model` → embedder model. When the reference is unset or
11
+ * unresolvable, every field comes back empty and the caller treats LLM search as
12
+ * not configured (fail closed). There are no separate embedder settings.
13
+ *
14
+ * The secret `apiKey` is decrypted in-memory by `resolve`; it never leaves the
15
+ * process.
16
+ */
17
+ export async function resolveEmbedderConfig(settings, channelId, credentials) {
18
+ let refCode = '';
19
+ try {
20
+ refCode = await settings.get(SEARCH_SETTING_CODES.LLM_EMBEDDER_CREDENTIALS, channelId, stringSchema);
21
+ }
22
+ catch {
23
+ // Not seeded / out of scope — treat as unset.
24
+ refCode = '';
25
+ }
26
+ if (!credentials || !refCode) {
27
+ return { url: '', apiKey: '', model: '' };
28
+ }
29
+ // Tolerated: an unset or unresolvable reference means LLM search is not
30
+ // configured, and the caller is right to serve without it. **A presence
31
+ // answer is not that**: `credentials` being switched off would otherwise read
32
+ // as "the operator never configured this", and the screen would offer to fix
33
+ // a setting that is already correct.
34
+ const resolved = await credentials.resolve(refCode).catch((error) => {
35
+ rethrowIfModuleDisabled(error);
36
+ return null;
37
+ });
38
+ if (!resolved || resolved.status !== 'ok') {
39
+ return { url: '', apiKey: '', model: '' };
40
+ }
41
+ return {
42
+ url: typeof resolved.values['baseUrl'] === 'string' ? resolved.values['baseUrl'] : '',
43
+ apiKey: typeof resolved.values['apiKey'] === 'string' ? resolved.values['apiKey'] : '',
44
+ model: typeof resolved.values['model'] === 'string' ? resolved.values['model'] : '',
45
+ };
46
+ }
47
+ //# sourceMappingURL=embedder-config-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedder-config-resolver.js","sourceRoot":"","sources":["../../../src/backend/services/embedder-config-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,uBAAuB,EAAyB,MAAM,kCAAkC,CAAC;AAClG,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAazD,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;AAEhC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,QAA0B,EAC1B,SAAiB,EACjB,WAAmC;IAEnC,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,QAAQ,CAAC,GAAG,CAC1B,oBAAoB,CAAC,wBAAwB,EAC7C,SAAS,EACT,YAAY,CACb,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;QAC9C,OAAO,GAAG,EAAE,CAAC;IACf,CAAC;IAED,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC5C,CAAC;IAED,wEAAwE;IACxE,wEAAwE;IACxE,8EAA8E;IAC9E,6EAA6E;IAC7E,qCAAqC;IACrC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QAC3E,uBAAuB,CAAC,KAAK,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC5C,CAAC;IAED,OAAO;QACL,GAAG,EAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE;QACrF,MAAM,EAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;QACtF,KAAK,EAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE;KACpF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { type SettingsAdminAuditContext, type SettingsAdminPort } from '@endora-commerce/contracts';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
4
+ import { type CredentialResolvePort } from './embedder-config-resolver.js';
5
+ /**
6
+ * LlmToggleService — feature 006 / US2 / T024.
7
+ *
8
+ * Cross-setting validating wrapper around `search.llm.enabled`. The four
9
+ * `search.llm.*` settings (toggle + url + api_key + model) live in the
10
+ * Settings module's `setting_values` table; the three embedder.* fields
11
+ * are written through the generic Settings admin route (per-field audit
12
+ * trail). This wrapper only writes the `enabled` toggle, but refuses to
13
+ * flip it on for any channel whose embedder.* triplet is incomplete
14
+ * (FR-011).
15
+ *
16
+ * Disabling never refuses — turning off LLM-augmented search must always
17
+ * be possible regardless of the embedder fields' state.
18
+ */
19
+ export interface LlmToggleParams {
20
+ enabled: boolean;
21
+ /** When omitted, applies to every channel in the setting's scope. */
22
+ salesChannelCodes?: string[];
23
+ expectedVersion: string | null;
24
+ actor: SettingsAdminAuditContext;
25
+ }
26
+ export interface LlmToggleResult {
27
+ enabled: boolean;
28
+ newVersion: string;
29
+ appliedChannelIds: string[];
30
+ }
31
+ export declare class LlmToggleService {
32
+ private readonly emFactory;
33
+ private readonly settingsService;
34
+ private readonly settingsAdminService;
35
+ /** Feature 058 — resolves `search.llm.embedder_credentials` (optional). */
36
+ private readonly credentials?;
37
+ constructor(emFactory: () => EntityManager, settingsService: SettingsReadPort, settingsAdminService: SettingsAdminPort,
38
+ /** Feature 058 — resolves `search.llm.embedder_credentials` (optional). */
39
+ credentials?: CredentialResolvePort | undefined);
40
+ toggle(params: LlmToggleParams): Promise<LlmToggleResult>;
41
+ /**
42
+ * Reads the three embedder.* settings for every targeted channel and
43
+ * refuses with `LLM_CONFIG_INCOMPLETE` (HTTP 400) when any value is
44
+ * empty. The error's `details[]` lists each missing
45
+ * (channelCode, settingCode) pair so the admin UI can pinpoint what
46
+ * needs filling.
47
+ */
48
+ private assertEmbedderConfigComplete;
49
+ }
50
+ //# sourceMappingURL=llm-toggle.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-toggle.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/llm-toggle.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAGzE,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAEvC;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IACrC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAJZ,SAAS,EAAE,MAAM,aAAa,EAC9B,eAAe,EAAE,gBAAgB,EACjC,oBAAoB,EAAE,iBAAiB;IACxD,2EAA2E;IAC1D,WAAW,CAAC,EAAE,qBAAqB,YAAA;IAGhD,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IA2B/D;;;;;;OAMG;YACW,4BAA4B;CAwD3C"}
@@ -0,0 +1,78 @@
1
+ import { ERROR_CODES, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
4
+ import { SEARCH_SETTING_CODES } from '../../manifest.js';
5
+ import { resolveEmbedderConfig, } from './embedder-config-resolver.js';
6
+ export class LlmToggleService {
7
+ emFactory;
8
+ settingsService;
9
+ settingsAdminService;
10
+ credentials;
11
+ constructor(emFactory, settingsService, settingsAdminService,
12
+ /** Feature 058 — resolves `search.llm.embedder_credentials` (optional). */
13
+ credentials) {
14
+ this.emFactory = emFactory;
15
+ this.settingsService = settingsService;
16
+ this.settingsAdminService = settingsAdminService;
17
+ this.credentials = credentials;
18
+ }
19
+ async toggle(params) {
20
+ if (params.enabled) {
21
+ await this.assertEmbedderConfigComplete(params.salesChannelCodes);
22
+ }
23
+ const result = params.salesChannelCodes
24
+ ? await this.settingsAdminService.setValueForSubset(SEARCH_SETTING_CODES.LLM_ENABLED, params.salesChannelCodes, params.enabled, params.expectedVersion, params.actor)
25
+ : await this.settingsAdminService.setValueForAllChannels(SEARCH_SETTING_CODES.LLM_ENABLED, params.enabled, params.expectedVersion, params.actor);
26
+ return {
27
+ enabled: params.enabled,
28
+ newVersion: result.newVersion,
29
+ appliedChannelIds: result.affectedChannelIds,
30
+ };
31
+ }
32
+ /**
33
+ * Reads the three embedder.* settings for every targeted channel and
34
+ * refuses with `LLM_CONFIG_INCOMPLETE` (HTTP 400) when any value is
35
+ * empty. The error's `details[]` lists each missing
36
+ * (channelCode, settingCode) pair so the admin UI can pinpoint what
37
+ * needs filling.
38
+ */
39
+ async assertEmbedderConfigComplete(salesChannelCodes) {
40
+ const em = this.emFactory();
41
+ const channels = salesChannelCodes
42
+ ? await em.find(SalesChannel, { code: { $in: salesChannelCodes } })
43
+ : await em.find(SalesChannel, {});
44
+ if (salesChannelCodes && channels.length !== salesChannelCodes.length) {
45
+ const found = new Set(channels.map((c) => c.code));
46
+ const missing = salesChannelCodes.filter((c) => !found.has(c));
47
+ throw new HttpError(400, ERROR_CODES.SETTING_OUT_OF_SCOPE_FOR_CHANNEL, `Unknown sales channel(s): ${missing.join(', ')}.`, missing.map((c) => ({
48
+ path: `salesChannelCodes.${c}`,
49
+ issue: 'unknown sales channel',
50
+ })));
51
+ }
52
+ const missing = [];
53
+ for (const channel of channels) {
54
+ // Feature 058 — the embedder config comes solely from the
55
+ // `search.llm.embedder_credentials` reference (Base URL + API key + model).
56
+ // A field is "missing" when the referenced configuration does not supply
57
+ // it (or no reference is set); the error points at the credential setting.
58
+ const cfg = await resolveEmbedderConfig(this.settingsService, channel.id, this.credentials);
59
+ const fields = [
60
+ ['url', cfg.url],
61
+ ['apiKey', cfg.apiKey],
62
+ ['model', cfg.model],
63
+ ];
64
+ for (const [field, value] of fields) {
65
+ if (!value || value.length === 0) {
66
+ missing.push({ channelCode: channel.code, field });
67
+ }
68
+ }
69
+ }
70
+ if (missing.length > 0) {
71
+ throw new HttpError(400, ERROR_CODES.LLM_CONFIG_INCOMPLETE, 'LLM-augmented search cannot be enabled until the referenced embedder credential supplies a Base URL, API key and model on every targeted channel.', missing.map((m) => ({
72
+ path: `${m.channelCode}.${SEARCH_SETTING_CODES.LLM_EMBEDDER_CREDENTIALS}`,
73
+ issue: `embedder ${m.field} is not provided by the credential`,
74
+ })));
75
+ }
76
+ }
77
+ }
78
+ //# sourceMappingURL=llm-toggle.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-toggle.service.js","sourceRoot":"","sources":["../../../src/backend/services/llm-toggle.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { EventBase } from '@endora-commerce/platform/events';
3
+ import type { SearchIndexer } from './search-indexer.js';
4
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
5
+ import { type CredentialResolvePort } from './embedder-config-resolver.js';
6
+ /**
7
+ * SearchEventSubscriber (T067 — incremental upsert path).
8
+ *
9
+ * The handlers that keep the per-channel Meilisearch indexes in sync with
10
+ * Postgres, so a catalog mutation costs an incremental upsert rather than an
11
+ * offline reindex. **The class no longer subscribes to anything** — the
12
+ * registrations live in this module's `backend.ts`, where `ctx.subscribe` wraps
13
+ * each one in `subscribeForModule` and the module's effective state decides
14
+ * whether the handler runs (issue #107). What used to happen here was a bare
15
+ * `eventBus.on`, so a deployment with `search` switched off went on rewriting
16
+ * Meilisearch documents on every product write.
17
+ *
18
+ * The events `backend.ts` maps onto these methods:
19
+ *
20
+ * - `product.created.v1` → upsertProduct
21
+ * - `product.updated.v1` → upsertProduct
22
+ * - `product.archived.v1` → deleteProduct
23
+ * - `product.deleted.v1` → deleteProduct
24
+ * - `attribute.updated.v1` → refreshAttributeSettings
25
+ * - `category.updated.v1` → reindexCategorySubtree (feature 068: the
26
+ * category's slug and activation state are projected onto every product
27
+ * document under it, so a rename or a deactivation has to reach the index)
28
+ *
29
+ * Feature 006 / T027 also attaches the LLM-augmented-search reactor:
30
+ *
31
+ * - `settings.value_changed` (settingCode = `search.llm.enabled`)
32
+ * → attach or detach a Meilisearch embedder per affected channel,
33
+ * depending on the new boolean value. When `enabled=true` and any
34
+ * of the embedder.* config values are empty, the handler logs and
35
+ * no-ops — the toggle's pre-write validator (LlmToggleService) is
36
+ * the user-visible refusal path; this handler is the
37
+ * belt-and-braces guard for direct generic-Settings writes.
38
+ *
39
+ * Handlers swallow their own errors so a transient Meilisearch outage
40
+ * doesn't break the catalog write path. The reserved-fallback in the read
41
+ * path (catalog/routes.public.ts) keeps search functional even with a
42
+ * stale index until the next offline `search:reindex`.
43
+ */
44
+ interface CatalogEvents extends Record<string, EventBase> {
45
+ 'product.created.v1': EventBase & {
46
+ productId: string;
47
+ sku: string;
48
+ };
49
+ 'product.updated.v1': EventBase & {
50
+ productId: string;
51
+ changedFields: string[];
52
+ };
53
+ 'product.archived.v1': EventBase & {
54
+ productId: string;
55
+ };
56
+ 'product.deleted.v1': EventBase & {
57
+ productId: string;
58
+ };
59
+ 'attribute.updated.v1': EventBase & {
60
+ key: string;
61
+ isSearchable: boolean;
62
+ isFilterable: boolean;
63
+ };
64
+ 'category.updated.v1': EventBase & {
65
+ categoryId: string;
66
+ };
67
+ 'settings.value_changed': EventBase & {
68
+ settingCode: string;
69
+ salesChannelIds: string[];
70
+ /**
71
+ * Set when the platform-wide global override (settings.global_value)
72
+ * changed (feature 042). Channels without a per-channel override
73
+ * resolve from the global; this flag tells the reactor to re-evaluate
74
+ * every in-scope channel, not only the codes listed in `salesChannelIds`.
75
+ */
76
+ globalValueUpdated?: boolean;
77
+ valueType: string;
78
+ };
79
+ }
80
+ /** The `settings.value_changed` payload this module reacts to. */
81
+ export type SettingChangedPayload = CatalogEvents['settings.value_changed'];
82
+ export interface SearchEventSubscriberDeps {
83
+ emFactory: () => EntityManager;
84
+ indexer: SearchIndexer;
85
+ /**
86
+ * Universal-getter for Settings — required when the LLM-toggle reactor
87
+ * is enabled (feature 006). When undefined, the `settings.value_changed`
88
+ * handler is not attached; foundation tests that don't have settings
89
+ * wired stay green.
90
+ */
91
+ settingsService?: SettingsReadPort;
92
+ /**
93
+ * Feature 058 — resolves `search.llm.embedder_credentials` into the embedder
94
+ * config, falling back per field to the legacy embedder settings. Optional.
95
+ */
96
+ credentials?: CredentialResolvePort;
97
+ /**
98
+ * Logger hook for failures. Defaults to console.warn so production logs
99
+ * still surface them; tests pass a vi.fn() to assert.
100
+ */
101
+ onError?: (err: unknown, eventName: string) => void;
102
+ }
103
+ export declare class SearchEventSubscriber {
104
+ private readonly deps;
105
+ constructor(deps: SearchEventSubscriberDeps);
106
+ private get log();
107
+ /** `product.created.v1` / `product.updated.v1`. */
108
+ onProductUpserted(productId: string, eventName: string): Promise<void>;
109
+ /** `product.archived.v1` / `product.deleted.v1`. */
110
+ onProductRemoved(productId: string, eventName: string): Promise<void>;
111
+ /**
112
+ * `category.updated.v1` — feature 068: a category's slug and activation state
113
+ * are projected onto every product document beneath it.
114
+ */
115
+ onCategoryUpdated(categoryId: string): Promise<void>;
116
+ /** `attribute.updated.v1`. */
117
+ onAttributeUpdated(): Promise<void>;
118
+ /**
119
+ * `settings.value_changed` — the LLM-augmented-search reactor (feature 006 /
120
+ * T027). A no-op unless a settings service was supplied.
121
+ */
122
+ onSettingChanged(payload: SettingChangedPayload): Promise<void>;
123
+ }
124
+ export {};
125
+ //# sourceMappingURL=search-event-subscriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-event-subscriber.d.ts","sourceRoot":"","sources":["../../../src/backend/services/search-event-subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAIzE,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,UAAU,aAAc,SAAQ,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC;IACvD,oBAAoB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACrE,oBAAoB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACjF,qBAAqB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,oBAAoB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,sBAAsB,EAAE,SAAS,GAAG;QAClC,GAAG,EAAE,MAAM,CAAC;QACZ,YAAY,EAAE,OAAO,CAAC;QACtB,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,qBAAqB,EAAE,SAAS,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,wBAAwB,EAAE,SAAS,GAAG;QACpC,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B;;;;;WAKG;QACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAC7B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,kEAAkE;AAClE,MAAM,MAAM,qBAAqB,GAAG,aAAa,CAAC,wBAAwB,CAAC,CAAC;AAE5E,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,OAAO,EAAE,aAAa,CAAC;IACvB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC;;;OAGG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAID,qBAAa,qBAAqB;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,yBAAyB;IAE5D,OAAO,KAAK,GAAG,GAEd;IAED,mDAAmD;IAC7C,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5E,oDAAoD;IAC9C,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ3E;;;OAGG;IACG,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ1D,8BAA8B;IACxB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQzC;;;OAGG;IACG,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;CAuDtE"}
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod';
2
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
3
+ import { SEARCH_SETTING_CODES } from '../../manifest.js';
4
+ import { resolveEmbedderConfig, } from './embedder-config-resolver.js';
5
+ const booleanSchema = z.boolean();
6
+ export class SearchEventSubscriber {
7
+ deps;
8
+ constructor(deps) {
9
+ this.deps = deps;
10
+ }
11
+ get log() {
12
+ return this.deps.onError ?? defaultLogger;
13
+ }
14
+ /** `product.created.v1` / `product.updated.v1`. */
15
+ async onProductUpserted(productId, eventName) {
16
+ try {
17
+ await this.deps.indexer.upsertProduct(this.deps.emFactory(), productId);
18
+ }
19
+ catch (err) {
20
+ this.log(err, eventName);
21
+ }
22
+ }
23
+ /** `product.archived.v1` / `product.deleted.v1`. */
24
+ async onProductRemoved(productId, eventName) {
25
+ try {
26
+ await this.deps.indexer.deleteProduct(this.deps.emFactory(), productId);
27
+ }
28
+ catch (err) {
29
+ this.log(err, eventName);
30
+ }
31
+ }
32
+ /**
33
+ * `category.updated.v1` — feature 068: a category's slug and activation state
34
+ * are projected onto every product document beneath it.
35
+ */
36
+ async onCategoryUpdated(categoryId) {
37
+ try {
38
+ await this.deps.indexer.reindexCategorySubtree(this.deps.emFactory(), categoryId);
39
+ }
40
+ catch (err) {
41
+ this.log(err, 'category.updated.v1');
42
+ }
43
+ }
44
+ /** `attribute.updated.v1`. */
45
+ async onAttributeUpdated() {
46
+ try {
47
+ await this.deps.indexer.refreshAttributeSettings(this.deps.emFactory());
48
+ }
49
+ catch (err) {
50
+ this.log(err, 'attribute.updated.v1');
51
+ }
52
+ }
53
+ /**
54
+ * `settings.value_changed` — the LLM-augmented-search reactor (feature 006 /
55
+ * T027). A no-op unless a settings service was supplied.
56
+ */
57
+ async onSettingChanged(payload) {
58
+ const settingsService = this.deps.settingsService;
59
+ if (!settingsService)
60
+ return;
61
+ if (payload.settingCode !== SEARCH_SETTING_CODES.LLM_ENABLED)
62
+ return;
63
+ const { emFactory, indexer } = this.deps;
64
+ const log = this.log;
65
+ try {
66
+ const em = emFactory();
67
+ // Feature 042: a global-override change touches every channel
68
+ // that doesn't carry its own per-channel value — re-evaluate
69
+ // the lot. Per-channel writes still target only the listed ids.
70
+ const channels = payload.globalValueUpdated
71
+ ? await em.find(SalesChannel, {})
72
+ : await em.find(SalesChannel, {
73
+ id: { $in: payload.salesChannelIds },
74
+ });
75
+ for (const channel of channels) {
76
+ const enabled = await settingsService.get(SEARCH_SETTING_CODES.LLM_ENABLED, channel.id, booleanSchema);
77
+ if (enabled) {
78
+ // Feature 058 — prefer the credential reference, fall back per
79
+ // field to the legacy embedder settings.
80
+ const { url, apiKey, model } = await resolveEmbedderConfig(settingsService, channel.id, this.deps.credentials);
81
+ if (url && apiKey && model) {
82
+ await indexer.attachEmbedderForChannel(channel.code, {
83
+ url,
84
+ apiKey,
85
+ model,
86
+ });
87
+ }
88
+ else {
89
+ // Direct generic-Settings write bypassed the toggle
90
+ // validator; surface for ops, leave the index alone so
91
+ // we don't poison Meilisearch with empty creds.
92
+ log(new Error(`LLM enabled on channel ${channel.code} without complete embedder config; embedder NOT attached`), 'settings.value_changed');
93
+ }
94
+ }
95
+ else {
96
+ await indexer.detachEmbedderForChannel(channel.code);
97
+ }
98
+ }
99
+ }
100
+ catch (err) {
101
+ log(err, 'settings.value_changed');
102
+ }
103
+ }
104
+ }
105
+ function defaultLogger(err, eventName) {
106
+ console.warn(JSON.stringify({
107
+ level: 'warn',
108
+ msg: 'search subscriber handler failed',
109
+ eventName,
110
+ error: err instanceof Error ? err.message : String(err),
111
+ }));
112
+ }
113
+ //# sourceMappingURL=search-event-subscriber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-event-subscriber.js","sourceRoot":"","sources":["../../../src/backend/services/search-event-subscriber.ts"],"names":[],"mappings":"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"}