@searchspring/snap-controller 0.20.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/cjs/Abstract/AbstractController.d.ts +40 -0
  4. package/dist/cjs/Abstract/AbstractController.d.ts.map +1 -0
  5. package/dist/cjs/Abstract/AbstractController.js +281 -0
  6. package/dist/cjs/Autocomplete/AutocompleteController.d.ts +40 -0
  7. package/dist/cjs/Autocomplete/AutocompleteController.d.ts.map +1 -0
  8. package/dist/cjs/Autocomplete/AutocompleteController.js +687 -0
  9. package/dist/cjs/Finder/FinderController.d.ts +14 -0
  10. package/dist/cjs/Finder/FinderController.d.ts.map +1 -0
  11. package/dist/cjs/Finder/FinderController.js +286 -0
  12. package/dist/cjs/Recommendation/RecommendationController.d.ts +31 -0
  13. package/dist/cjs/Recommendation/RecommendationController.d.ts.map +1 -0
  14. package/dist/cjs/Recommendation/RecommendationController.js +452 -0
  15. package/dist/cjs/Search/SearchController.d.ts +23 -0
  16. package/dist/cjs/Search/SearchController.d.ts.map +1 -0
  17. package/dist/cjs/Search/SearchController.js +429 -0
  18. package/dist/cjs/index.d.ts +7 -0
  19. package/dist/cjs/index.d.ts.map +1 -0
  20. package/dist/cjs/index.js +24 -0
  21. package/dist/cjs/types.d.ts +52 -0
  22. package/dist/cjs/types.d.ts.map +1 -0
  23. package/dist/cjs/types.js +2 -0
  24. package/dist/cjs/utils/getParams.d.ts +2 -0
  25. package/dist/cjs/utils/getParams.d.ts.map +1 -0
  26. package/dist/cjs/utils/getParams.js +72 -0
  27. package/dist/esm/Abstract/AbstractController.d.ts +40 -0
  28. package/dist/esm/Abstract/AbstractController.d.ts.map +1 -0
  29. package/dist/esm/Abstract/AbstractController.js +181 -0
  30. package/dist/esm/Autocomplete/AutocompleteController.d.ts +40 -0
  31. package/dist/esm/Autocomplete/AutocompleteController.d.ts.map +1 -0
  32. package/dist/esm/Autocomplete/AutocompleteController.js +472 -0
  33. package/dist/esm/Finder/FinderController.d.ts +14 -0
  34. package/dist/esm/Finder/FinderController.d.ts.map +1 -0
  35. package/dist/esm/Finder/FinderController.js +164 -0
  36. package/dist/esm/Recommendation/RecommendationController.d.ts +31 -0
  37. package/dist/esm/Recommendation/RecommendationController.d.ts.map +1 -0
  38. package/dist/esm/Recommendation/RecommendationController.js +330 -0
  39. package/dist/esm/Search/SearchController.d.ts +23 -0
  40. package/dist/esm/Search/SearchController.d.ts.map +1 -0
  41. package/dist/esm/Search/SearchController.js +282 -0
  42. package/dist/esm/index.d.ts +7 -0
  43. package/dist/esm/index.d.ts.map +1 -0
  44. package/dist/esm/index.js +6 -0
  45. package/dist/esm/types.d.ts +52 -0
  46. package/dist/esm/types.d.ts.map +1 -0
  47. package/dist/esm/types.js +1 -0
  48. package/dist/esm/utils/getParams.d.ts +2 -0
  49. package/dist/esm/utils/getParams.d.ts.map +1 -0
  50. package/dist/esm/utils/getParams.js +68 -0
  51. package/package.json +40 -0
@@ -0,0 +1,472 @@
1
+ import deepmerge from 'deepmerge';
2
+ import { StorageStore, StorageType, ErrorType } from '@searchspring/snap-store-mobx';
3
+ import { url } from '@searchspring/snap-toolbox';
4
+ import { AbstractController } from '../Abstract/AbstractController';
5
+ import { getSearchParams } from '../utils/getParams';
6
+ const INPUT_ATTRIBUTE = 'ss-autocomplete-input';
7
+ const INPUT_DELAY = 200;
8
+ const KEY_ENTER = 13;
9
+ const KEY_ESCAPE = 27;
10
+ const PARAM_ORIGINAL_QUERY = 'oq';
11
+ const defaultConfig = {
12
+ id: 'autocomplete',
13
+ selector: '',
14
+ action: '',
15
+ globals: {},
16
+ settings: {
17
+ initializeFromUrl: true,
18
+ syncInputs: true,
19
+ facets: {
20
+ trim: true,
21
+ pinFiltered: true,
22
+ },
23
+ },
24
+ };
25
+ export class AutocompleteController extends AbstractController {
26
+ constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }) {
27
+ super(config, { client, store, urlManager, eventManager, profiler, logger, tracker });
28
+ this.type = 'autocomplete';
29
+ this.track = {
30
+ // TODO: add in future when autocomplete supports result click tracking
31
+ product: {
32
+ click: (e, result) => {
33
+ this.log.warn('product.click tracking is not currently supported in this controller type');
34
+ },
35
+ },
36
+ };
37
+ this.handlers = {
38
+ input: {
39
+ enterKey: async (e) => {
40
+ if (e.keyCode == KEY_ENTER) {
41
+ const actionUrl = url(this.config.action);
42
+ const input = e.target;
43
+ // when spellCorrection is enabled
44
+ if (this.config.globals?.search?.query?.spellCorrection) {
45
+ // wait until loading is complete before submission
46
+ // TODO make this better
47
+ await timeout(INPUT_DELAY + 1);
48
+ while (this.store.loading) {
49
+ await timeout(INPUT_DELAY);
50
+ }
51
+ // use corrected query and originalQuery
52
+ if (this.store.search.originalQuery) {
53
+ input.value = this.store.search.query.string;
54
+ actionUrl.params.query[PARAM_ORIGINAL_QUERY] = this.store.search.originalQuery.string;
55
+ }
56
+ }
57
+ const inputParam = input.name || this.urlManager.getTranslatorConfig().queryParameter;
58
+ actionUrl.params.query[inputParam] = input.value;
59
+ // TODO expected spell correct behavior queryAssumption
60
+ try {
61
+ await this.eventManager.fire('beforeSubmit', {
62
+ controller: this,
63
+ input,
64
+ });
65
+ }
66
+ catch (err) {
67
+ if (err?.message == 'cancelled') {
68
+ this.log.warn(`'beforeSubmit' middleware cancelled`);
69
+ return;
70
+ }
71
+ else {
72
+ this.log.error(`error in 'beforeSubmit' middleware`);
73
+ console.error(err);
74
+ }
75
+ }
76
+ const newUrl = actionUrl.url();
77
+ window.location.href = newUrl;
78
+ }
79
+ },
80
+ escKey: (e) => {
81
+ if (e.keyCode == KEY_ESCAPE) {
82
+ e.target.blur();
83
+ this.setFocused();
84
+ }
85
+ },
86
+ focus: (e) => {
87
+ e.stopPropagation();
88
+ // timeout to ensure focus happens AFTER click
89
+ setTimeout(() => {
90
+ this.setFocused(e.target);
91
+ });
92
+ },
93
+ formSubmit: async (e) => {
94
+ const form = e.target;
95
+ const input = form.querySelector(`input[${INPUT_ATTRIBUTE}]`);
96
+ e.preventDefault();
97
+ // when spellCorrection is enabled
98
+ if (this.config.globals?.search?.query?.spellCorrection) {
99
+ // wait until loading is complete before submission
100
+ // TODO make this better
101
+ await timeout(INPUT_DELAY + 1);
102
+ while (this.store.loading) {
103
+ await timeout(INPUT_DELAY);
104
+ }
105
+ if (this.store.search.originalQuery) {
106
+ input.value = this.store.search.query.string;
107
+ addHiddenFormInput(form, PARAM_ORIGINAL_QUERY, this.store.search.originalQuery.string);
108
+ }
109
+ }
110
+ // TODO expected spell correct behavior queryAssumption
111
+ try {
112
+ await this.eventManager.fire('beforeSubmit', {
113
+ controller: this,
114
+ input,
115
+ });
116
+ }
117
+ catch (err) {
118
+ if (err?.message == 'cancelled') {
119
+ this.log.warn(`'beforeSubmit' middleware cancelled`);
120
+ return;
121
+ }
122
+ else {
123
+ this.log.error(`error in 'beforeSubmit' middleware`);
124
+ console.error(err);
125
+ }
126
+ }
127
+ form.submit();
128
+ },
129
+ keyUp: (e) => {
130
+ // ignore enter and escape keys
131
+ if (e?.keyCode == KEY_ENTER || e?.keyCode == KEY_ESCAPE)
132
+ return;
133
+ // return focus on keyup if it was lost
134
+ if (e.isTrusted && this.store.state.focusedInput !== e.target) {
135
+ this.setFocused(e.target);
136
+ }
137
+ const value = e.target.value;
138
+ // prevent search when value is unchanged
139
+ if (this.store.state.input == value && this.store.loaded) {
140
+ return;
141
+ }
142
+ this.store.state.input = value;
143
+ if (this.config.settings.syncInputs) {
144
+ const inputs = document.querySelectorAll(this.config.selector);
145
+ inputs.forEach((input) => {
146
+ input.value = value;
147
+ });
148
+ }
149
+ clearTimeout(this.handlers.input.timeoutDelay);
150
+ if (!value) {
151
+ // TODO cancel any current requests?
152
+ this.store.reset();
153
+ this.urlManager.reset().go();
154
+ }
155
+ else {
156
+ this.handlers.input.timeoutDelay = setTimeout(() => {
157
+ this.store.state.locks.terms.unlock();
158
+ this.store.state.locks.facets.unlock();
159
+ this.urlManager.set({ query: this.store.state.input }).go();
160
+ }, INPUT_DELAY);
161
+ }
162
+ },
163
+ timeoutDelay: undefined,
164
+ },
165
+ document: {
166
+ click: (e) => {
167
+ const inputs = document.querySelectorAll(this.config.selector);
168
+ if (Array.from(inputs).includes(e.target)) {
169
+ e.stopPropagation();
170
+ }
171
+ else {
172
+ this.setFocused();
173
+ }
174
+ },
175
+ },
176
+ };
177
+ this.searchTrending = async () => {
178
+ let terms;
179
+ const storedTerms = this.storage.get('terms');
180
+ if (storedTerms) {
181
+ // terms exist in storage, update store
182
+ terms = JSON.parse(storedTerms);
183
+ }
184
+ else {
185
+ // query for trending terms, save to storage, update store
186
+ const trendingParams = {
187
+ limit: this.config.settings?.trending?.limit || 5,
188
+ };
189
+ const trendingProfile = this.profiler.create({ type: 'event', name: 'trending', context: trendingParams }).start();
190
+ terms = await this.client.trending(trendingParams);
191
+ trendingProfile.stop();
192
+ this.log.profile(trendingProfile);
193
+ this.storage.set('terms', JSON.stringify(terms));
194
+ }
195
+ this.store.updateTrendingTerms(terms);
196
+ };
197
+ this.search = async () => {
198
+ const params = this.params;
199
+ if (!params?.search?.query?.string) {
200
+ return;
201
+ }
202
+ try {
203
+ try {
204
+ await this.eventManager.fire('beforeSearch', {
205
+ controller: this,
206
+ request: params,
207
+ });
208
+ }
209
+ catch (err) {
210
+ if (err?.message == 'cancelled') {
211
+ this.log.warn(`'beforeSearch' middleware cancelled`);
212
+ return;
213
+ }
214
+ else {
215
+ this.log.error(`error in 'beforeSearch' middleware`);
216
+ throw err;
217
+ }
218
+ }
219
+ const searchProfile = this.profiler.create({ type: 'event', name: 'search', context: params }).start();
220
+ const [meta, response] = await this.client.autocomplete(params);
221
+ if (!response.meta) {
222
+ /**
223
+ * MockClient will overwrite the client search() method and use
224
+ * SearchData to return mock data which already contains meta data
225
+ */
226
+ response.meta = meta;
227
+ }
228
+ searchProfile.stop();
229
+ this.log.profile(searchProfile);
230
+ const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
231
+ try {
232
+ await this.eventManager.fire('afterSearch', {
233
+ controller: this,
234
+ request: params,
235
+ response,
236
+ });
237
+ }
238
+ catch (err) {
239
+ if (err?.message == 'cancelled') {
240
+ this.log.warn(`'afterSearch' middleware cancelled`);
241
+ afterSearchProfile.stop();
242
+ return;
243
+ }
244
+ else {
245
+ this.log.error(`error in 'afterSearch' middleware`);
246
+ throw err;
247
+ }
248
+ }
249
+ afterSearchProfile.stop();
250
+ this.log.profile(afterSearchProfile);
251
+ // update the store
252
+ this.store.update(response);
253
+ const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
254
+ try {
255
+ await this.eventManager.fire('afterStore', {
256
+ controller: this,
257
+ request: params,
258
+ response,
259
+ });
260
+ }
261
+ catch (err) {
262
+ if (err?.message == 'cancelled') {
263
+ this.log.warn(`'afterStore' middleware cancelled`);
264
+ afterStoreProfile.stop();
265
+ return;
266
+ }
267
+ else {
268
+ this.log.error(`error in 'afterStore' middleware`);
269
+ throw err;
270
+ }
271
+ }
272
+ afterStoreProfile.stop();
273
+ this.log.profile(afterStoreProfile);
274
+ }
275
+ catch (err) {
276
+ if (err) {
277
+ switch (err) {
278
+ case 429:
279
+ this.store.error = {
280
+ code: 429,
281
+ type: ErrorType.WARNING,
282
+ message: 'Too many requests try again later',
283
+ };
284
+ this.log.warn(this.store.error);
285
+ break;
286
+ case 500:
287
+ this.store.error = {
288
+ code: 500,
289
+ type: ErrorType.ERROR,
290
+ message: 'Invalid Search Request or Service Unavailable',
291
+ };
292
+ this.log.error(this.store.error);
293
+ break;
294
+ default:
295
+ this.log.error(err);
296
+ break;
297
+ }
298
+ this.store.loading = false;
299
+ }
300
+ }
301
+ };
302
+ // deep merge config with defaults
303
+ this.config = deepmerge(defaultConfig, this.config);
304
+ this.store.setConfig(this.config);
305
+ // get current search from url before detaching
306
+ if (this.config.settings.initializeFromUrl) {
307
+ this.store.state.input = this.urlManager.state.query;
308
+ // reset to force search on focus
309
+ // TODO: make a config setting for this
310
+ this.urlManager.reset().go();
311
+ }
312
+ // persist trending terms in local storage
313
+ this.storage = new StorageStore({
314
+ type: StorageType.SESSION,
315
+ key: `ss-controller-${this.config.id}`,
316
+ });
317
+ // add 'beforeSearch' middleware
318
+ this.eventManager.on('beforeSearch', async (ac, next) => {
319
+ ac.controller.store.loading = true;
320
+ await next();
321
+ });
322
+ // add 'afterSearch' middleware
323
+ this.eventManager.on('afterSearch', async (ac, next) => {
324
+ await next();
325
+ // cancel search if no input or query doesn't match current urlState
326
+ if (ac.response.autocomplete.query != ac.controller.urlManager.state.query) {
327
+ return false;
328
+ }
329
+ });
330
+ // add 'afterStore' middleware
331
+ this.eventManager.on('afterStore', async (ac, next) => {
332
+ await next();
333
+ ac.controller.store.loading = false;
334
+ });
335
+ // attach config plugins and event middleware
336
+ this.use(this.config);
337
+ }
338
+ get params() {
339
+ const urlState = this.urlManager.state;
340
+ const params = deepmerge({ ...getSearchParams(urlState) }, this.config.globals);
341
+ const userId = this.tracker.getUserId();
342
+ if (userId) {
343
+ params.tracking = params.tracking || {};
344
+ params.tracking.userId = userId;
345
+ }
346
+ if (!this.config.globals?.personalization?.disabled) {
347
+ const cartItems = this.tracker.cookies.cart.get();
348
+ if (cartItems.length) {
349
+ params.personalization = params.personalization || {};
350
+ params.personalization.cart = cartItems.join(',');
351
+ }
352
+ const lastViewedItems = this.tracker.cookies.viewed.get();
353
+ if (lastViewedItems.length) {
354
+ params.personalization = params.personalization || {};
355
+ params.personalization.lastViewed = lastViewedItems.join(',');
356
+ }
357
+ const shopperId = this.tracker.getShopperId();
358
+ if (shopperId) {
359
+ params.personalization = params.personalization || {};
360
+ params.personalization.shopper = shopperId;
361
+ }
362
+ }
363
+ return params;
364
+ }
365
+ async setFocused(inputElement) {
366
+ if (this.store.state.focusedInput !== inputElement) {
367
+ this.store.state.focusedInput = inputElement;
368
+ // fire focusChange event
369
+ try {
370
+ try {
371
+ await this.eventManager.fire('focusChange', {
372
+ controller: this,
373
+ });
374
+ }
375
+ catch (err) {
376
+ if (err?.message == 'cancelled') {
377
+ this.log.warn(`'focusChange' middleware cancelled`);
378
+ }
379
+ else {
380
+ this.log.error(`error in 'focusChange' middleware`);
381
+ throw err;
382
+ }
383
+ }
384
+ }
385
+ catch (err) {
386
+ if (err) {
387
+ console.error(err);
388
+ }
389
+ }
390
+ }
391
+ inputElement?.dispatchEvent(new Event('keyup'));
392
+ }
393
+ reset() {
394
+ // reset input values and state
395
+ const inputs = document.querySelectorAll(this.config.selector);
396
+ inputs.forEach((input) => {
397
+ input.value = '';
398
+ });
399
+ this.store.reset();
400
+ }
401
+ unbind() {
402
+ const inputs = document.querySelectorAll(`input[${INPUT_ATTRIBUTE}]`);
403
+ inputs?.forEach((input) => {
404
+ input.removeEventListener('keyup', this.handlers.input.keyUp);
405
+ input.removeEventListener('keyup', this.handlers.input.enterKey);
406
+ input.removeEventListener('keyup', this.handlers.input.escKey);
407
+ input.removeEventListener('focus', this.handlers.input.focus);
408
+ input.form?.removeEventListener('submit', this.handlers.input.formSubmit);
409
+ });
410
+ document.removeEventListener('click', this.handlers.document.click);
411
+ }
412
+ async bind() {
413
+ if (!this.initialized) {
414
+ await this.init();
415
+ }
416
+ this.unbind();
417
+ const inputs = document.querySelectorAll(this.config.selector);
418
+ inputs.forEach((input) => {
419
+ input.setAttribute('spellcheck', 'false');
420
+ input.setAttribute('autocomplete', 'off');
421
+ input.setAttribute(INPUT_ATTRIBUTE, '');
422
+ input.addEventListener('keyup', this.handlers.input.keyUp);
423
+ if (this.config.settings.initializeFromUrl && !input.value && this.store.state.input) {
424
+ input.value = this.store.state.input;
425
+ }
426
+ input.addEventListener('focus', this.handlers.input.focus);
427
+ input.addEventListener('keyup', this.handlers.input.escKey);
428
+ const form = input.form;
429
+ let formActionUrl = this.config.action;
430
+ if (!form && this.config.action) {
431
+ input.addEventListener('keyup', this.handlers.input.enterKey);
432
+ }
433
+ else if (form) {
434
+ if (this.config.action) {
435
+ form.action = this.config.action;
436
+ }
437
+ else {
438
+ formActionUrl = form.action;
439
+ }
440
+ form.addEventListener('submit', this.handlers.input.formSubmit);
441
+ }
442
+ // set the root URL on urlManager
443
+ if (formActionUrl) {
444
+ this.store.setService('urlManager', this.urlManager.withConfig((translatorConfig) => {
445
+ return {
446
+ ...translatorConfig,
447
+ urlRoot: formActionUrl,
448
+ };
449
+ }));
450
+ }
451
+ if (document.activeElement === input) {
452
+ this.setFocused(input);
453
+ }
454
+ });
455
+ if (this.config.settings?.trending?.limit > 0 && !this.store.trending?.length) {
456
+ this.searchTrending();
457
+ }
458
+ document.addEventListener('click', this.handlers.document.click);
459
+ }
460
+ }
461
+ function addHiddenFormInput(form, name, value) {
462
+ const inputElem = document.createElement('input');
463
+ inputElem.type = 'hidden';
464
+ inputElem.name = name;
465
+ inputElem.value = value;
466
+ form.append(inputElem);
467
+ }
468
+ async function timeout(time) {
469
+ return new Promise((resolve) => {
470
+ window.setTimeout(resolve, time);
471
+ });
472
+ }
@@ -0,0 +1,14 @@
1
+ import { AbstractController } from '../Abstract/AbstractController';
2
+ import type { FinderStore } from '@searchspring/snap-store-mobx';
3
+ import type { FinderControllerConfig, ControllerServices } from '../types';
4
+ export declare class FinderController extends AbstractController {
5
+ type: string;
6
+ store: FinderStore;
7
+ config: FinderControllerConfig;
8
+ constructor(config: FinderControllerConfig, { client, store, urlManager, eventManager, profiler, logger, tracker }: ControllerServices);
9
+ get params(): Record<string, any>;
10
+ find: () => void;
11
+ reset: () => void;
12
+ search: () => Promise<void>;
13
+ }
14
+ //# sourceMappingURL=FinderController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FinderController.d.ts","sourceRoot":"","sources":["../../../src/Finder/FinderController.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAAE,sBAAsB,EAAmC,kBAAkB,EAAa,MAAM,UAAU,CAAC;AAQvH,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,SAAY;IAChB,KAAK,EAAE,WAAW,CAAC;IAC1B,MAAM,EAAE,sBAAsB,CAAC;gBAEnB,MAAM,EAAE,sBAAsB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB;IAoCtI,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAUhC;IAED,IAAI,QAAO,IAAI,CAEb;IAEF,KAAK,QAAO,IAAI,CAMd;IAEF,MAAM,QAAa,QAAQ,IAAI,CAAC,CA6G9B;CACF"}
@@ -0,0 +1,164 @@
1
+ import deepmerge from 'deepmerge';
2
+ import { ErrorType } from '@searchspring/snap-store-mobx';
3
+ import { AbstractController } from '../Abstract/AbstractController';
4
+ import { getSearchParams } from '../utils/getParams';
5
+ const defaultConfig = {
6
+ id: 'finder',
7
+ globals: {},
8
+ fields: [],
9
+ };
10
+ export class FinderController extends AbstractController {
11
+ constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }) {
12
+ super(config, { client, store, urlManager, eventManager, profiler, logger, tracker });
13
+ this.type = 'finder';
14
+ this.find = () => {
15
+ window.location.href = this.urlManager.href;
16
+ };
17
+ this.reset = () => {
18
+ // only need to reset when selections have been made
19
+ if (this.urlManager.state.filter) {
20
+ this.store.storage.clear();
21
+ this.urlManager.remove('filter').go();
22
+ }
23
+ };
24
+ this.search = async () => {
25
+ if (!this.initialized) {
26
+ await this.init();
27
+ }
28
+ const params = this.params;
29
+ try {
30
+ try {
31
+ await this.eventManager.fire('beforeSearch', {
32
+ controller: this,
33
+ request: params,
34
+ });
35
+ }
36
+ catch (err) {
37
+ if (err?.message == 'cancelled') {
38
+ this.log.warn(`'beforeSearch' middleware cancelled`);
39
+ return;
40
+ }
41
+ else {
42
+ this.log.error(`error in 'beforeSearch' middleware`);
43
+ throw err;
44
+ }
45
+ }
46
+ const searchProfile = this.profiler.create({ type: 'event', name: 'search', context: params }).start();
47
+ const [meta, response] = await this.client.search(params);
48
+ if (!response.meta) {
49
+ /**
50
+ * MockClient will overwrite the client search() method and use
51
+ * SearchData to return mock data which already contains meta data
52
+ */
53
+ response.meta = meta;
54
+ }
55
+ searchProfile.stop();
56
+ this.log.profile(searchProfile);
57
+ const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
58
+ try {
59
+ await this.eventManager.fire('afterSearch', {
60
+ controller: this,
61
+ request: params,
62
+ response,
63
+ });
64
+ }
65
+ catch (err) {
66
+ if (err?.message == 'cancelled') {
67
+ this.log.warn(`'afterSearch' middleware cancelled`);
68
+ afterSearchProfile.stop();
69
+ return;
70
+ }
71
+ else {
72
+ this.log.error(`error in 'afterSearch' middleware`);
73
+ throw err;
74
+ }
75
+ }
76
+ afterSearchProfile.stop();
77
+ this.log.profile(afterSearchProfile);
78
+ // update the store
79
+ this.store.update(response);
80
+ const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
81
+ try {
82
+ await this.eventManager.fire('afterStore', {
83
+ controller: this,
84
+ request: params,
85
+ response,
86
+ });
87
+ }
88
+ catch (err) {
89
+ if (err?.message == 'cancelled') {
90
+ this.log.warn(`'afterStore' middleware cancelled`);
91
+ afterStoreProfile.stop();
92
+ return;
93
+ }
94
+ else {
95
+ this.log.error(`error in 'afterStore' middleware`);
96
+ throw err;
97
+ }
98
+ }
99
+ afterStoreProfile.stop();
100
+ this.log.profile(afterStoreProfile);
101
+ }
102
+ catch (err) {
103
+ if (err) {
104
+ switch (err) {
105
+ case 429:
106
+ this.store.error = {
107
+ code: 429,
108
+ type: ErrorType.WARNING,
109
+ message: 'Too many requests try again later',
110
+ };
111
+ this.log.warn(this.store.error);
112
+ break;
113
+ case 500:
114
+ this.store.error = {
115
+ code: 500,
116
+ type: ErrorType.ERROR,
117
+ message: 'Invalid Search Request or Service Unavailable',
118
+ };
119
+ this.log.error(this.store.error);
120
+ break;
121
+ default:
122
+ this.log.error(err);
123
+ break;
124
+ }
125
+ this.store.loading = false;
126
+ }
127
+ }
128
+ };
129
+ // deep merge config with defaults
130
+ this.config = deepmerge(defaultConfig, this.config);
131
+ this.store.setConfig(this.config);
132
+ // set the root URL on urlManager
133
+ if (this.config.url) {
134
+ this.urlManager = this.urlManager.withConfig((translatorConfig) => {
135
+ return {
136
+ ...translatorConfig,
137
+ urlRoot: this.config.url,
138
+ };
139
+ });
140
+ }
141
+ // add 'beforeSearch' middleware
142
+ this.eventManager.on('beforeSearch', async (finder, next) => {
143
+ finder.controller.store.loading = true;
144
+ await next();
145
+ });
146
+ // TODO: move this to afterStore
147
+ // add 'afterSearch' middleware
148
+ this.eventManager.on('afterSearch', async (finder, next) => {
149
+ await next();
150
+ finder.controller.store.loading = false;
151
+ });
152
+ // attach config plugins and event middleware
153
+ this.use(this.config);
154
+ }
155
+ get params() {
156
+ const urlState = this.urlManager.state;
157
+ const params = deepmerge({ ...getSearchParams(urlState) }, this.config.globals);
158
+ // get only the finder fields
159
+ params.facets = {
160
+ include: this.config.fields.map((fieldConfig) => fieldConfig.field),
161
+ };
162
+ return params;
163
+ }
164
+ }
@@ -0,0 +1,31 @@
1
+ import { AbstractController } from '../Abstract/AbstractController';
2
+ import type { BeaconEvent } from '@searchspring/snap-tracker';
3
+ import type { RecommendationStore } from '@searchspring/snap-store-mobx';
4
+ import type { RecommendationControllerConfig, ControllerServices } from '../types';
5
+ declare type RecommendationTrackMethods = {
6
+ product: {
7
+ click: (e: any, result: any) => BeaconEvent;
8
+ render: (result: any) => BeaconEvent;
9
+ impression: (result: any) => BeaconEvent;
10
+ };
11
+ click: (e: any) => BeaconEvent;
12
+ impression: () => BeaconEvent;
13
+ render: () => BeaconEvent;
14
+ };
15
+ export declare class RecommendationController extends AbstractController {
16
+ type: string;
17
+ store: RecommendationStore;
18
+ config: RecommendationControllerConfig;
19
+ events: {
20
+ click: any;
21
+ impression: any;
22
+ render: any;
23
+ product: {};
24
+ };
25
+ constructor(config: RecommendationControllerConfig, { client, store, urlManager, eventManager, profiler, logger, tracker }: ControllerServices);
26
+ track: RecommendationTrackMethods;
27
+ get params(): Record<string, any>;
28
+ search: () => Promise<void>;
29
+ }
30
+ export {};
31
+ //# sourceMappingURL=RecommendationController.d.ts.map