ti2 1.0.123 → 1.0.124
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/README.md +6 -5
- package/api.yml +3 -3
- package/controllers/README.md +1 -0
- package/controllers/__tests__/bookings-searchProducts.js +31 -14
- package/controllers/bookings.js +17 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,11 +73,12 @@ calling the integration; `cacheFound` distinguishes a cache miss from a cached
|
|
|
73
73
|
empty catalog.
|
|
74
74
|
|
|
75
75
|
A forced request is a full-catalog refresh by default. `fullSyncTrigger` may be
|
|
76
|
-
`scheduled`, `manual`, or `organic`;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`
|
|
76
|
+
`scheduled`, `manual`, or `organic`; an omitted trigger on a forced request is
|
|
77
|
+
treated as manual. Manual refreshes require a non-blank `admissionOverrideReason`.
|
|
78
|
+
Scheduled and manual refreshes reject `searchInput`, `optionId`, `productId`,
|
|
79
|
+
`productName`, or `lastUpdatedFrom` selectors. An empty `searchInput` or
|
|
80
|
+
`searchInput: "*"` represents the full catalog. Scheduler-owned requests may
|
|
81
|
+
also carry `fullSyncStartedAt` and `fullSyncAdmissionToken`.
|
|
81
82
|
|
|
82
83
|
Unscoped forced responses include `catalogRefreshOutcome`, `cacheUpdated`,
|
|
83
84
|
`cachePreserved`, and `cachedProductCount`. Consumers that update a downstream
|
package/api.yml
CHANGED
|
@@ -455,7 +455,7 @@ components:
|
|
|
455
455
|
forceRefresh:
|
|
456
456
|
type: boolean
|
|
457
457
|
default: false
|
|
458
|
-
description: Call the plugin directly. Full-catalog refreshes require the app's weeklyProductCatalogSync capability and default to a
|
|
458
|
+
description: Call the plugin directly. Full-catalog refreshes require the app's weeklyProductCatalogSync capability and default to a manual trigger when fullSyncTrigger is omitted. Manual refreshes require admissionOverrideReason.
|
|
459
459
|
cacheOnly:
|
|
460
460
|
type: boolean
|
|
461
461
|
default: false
|
|
@@ -466,14 +466,14 @@ components:
|
|
|
466
466
|
description: Execute the request through the Ti2 worker.
|
|
467
467
|
fullSyncTrigger:
|
|
468
468
|
type: string
|
|
469
|
-
description: Catalog refresh owner. Scheduled and manual triggers reject selector fields.
|
|
469
|
+
description: Catalog refresh owner. Forced requests default to manual when omitted. Scheduled and manual triggers reject selector fields.
|
|
470
470
|
enum:
|
|
471
471
|
- organic
|
|
472
472
|
- scheduled
|
|
473
473
|
- manual
|
|
474
474
|
admissionOverrideReason:
|
|
475
475
|
type: string
|
|
476
|
-
description:
|
|
476
|
+
description: Required non-blank audit reason for a manual refresh override.
|
|
477
477
|
fullSyncStartedAt:
|
|
478
478
|
type: number
|
|
479
479
|
description: Start time supplied by the full-sync admission owner.
|
package/controllers/README.md
CHANGED
|
@@ -29,6 +29,7 @@ The `$bookingsProductSearch` function in `controllers/bookings.js` implements a
|
|
|
29
29
|
* **Condition 3: `forceRefresh` is true**:
|
|
30
30
|
* This is the catalog refresh owner. The system fetches fresh data from the plugin.
|
|
31
31
|
* Ti2 rejects the request unless the host declared `weeklyProductCatalogSync: true` for the plugin. Manual audit reasons may override timing admission, but not catalog-completeness eligibility.
|
|
32
|
+
* An omitted `fullSyncTrigger` is treated as `manual`, and manual refreshes require a non-blank `admissionOverrideReason`. Weekly scheduler requests must explicitly send `fullSyncTrigger: scheduled`.
|
|
32
33
|
* Unless `fullSyncTrigger: organic` is explicit, a force refresh is a scheduled or manual full-catalog request and rejects `searchInput`, `optionId`, `productId`, `productName`, and `lastUpdatedFrom` selectors. `searchInput: "*"` is unscoped.
|
|
33
34
|
* The `fetchFromPluginAndCache` helper function is invoked. This function:
|
|
34
35
|
* Sets the `pluginExecutionLockKey` before calling the plugin to prevent other concurrent direct calls.
|
|
@@ -134,7 +134,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
134
134
|
const response = await doApiPost({
|
|
135
135
|
url: `/products/${testAppName}/${testUserId}/${testHint}/search`,
|
|
136
136
|
token: userToken,
|
|
137
|
-
payload: { forceRefresh: true },
|
|
137
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
138
138
|
expectStatusCode: 500,
|
|
139
139
|
});
|
|
140
140
|
|
|
@@ -146,6 +146,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
146
146
|
// NOTE: we SHOULD NOT need to remove the cache first, since we are forceRefreshing, we are testing the endpoint get's called while having a cache created
|
|
147
147
|
const payload = {
|
|
148
148
|
forceRefresh: true,
|
|
149
|
+
fullSyncTrigger: 'scheduled',
|
|
149
150
|
};
|
|
150
151
|
const { products } = await doApiPost({
|
|
151
152
|
url: `/products/${testAppName}/${testUserId}/${testHint}/search`,
|
|
@@ -253,7 +254,10 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
253
254
|
await doApiPost({
|
|
254
255
|
url: `/products/${testAppName}/${testUserId}/${testHint}/search`,
|
|
255
256
|
token: userToken,
|
|
256
|
-
payload: {
|
|
257
|
+
payload: {
|
|
258
|
+
forceRefresh: true,
|
|
259
|
+
admissionOverrideReason: 'legacy manual refresh',
|
|
260
|
+
},
|
|
257
261
|
});
|
|
258
262
|
const cacheSavePayload = emitSpy.mock.calls
|
|
259
263
|
.filter(call => call[0] === 'bookingsProductSearch:cache:save')
|
|
@@ -265,6 +269,8 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
265
269
|
expect(cacheSavePayload.hint).toBe(testHint);
|
|
266
270
|
expect(cacheSavePayload.userIdHash).toBeTruthy();
|
|
267
271
|
expect(cacheSavePayload.hintHash).toBeTruthy();
|
|
272
|
+
expect(cacheSavePayload.fullSyncTrigger).toBe('manual');
|
|
273
|
+
expect(cacheSavePayload.admissionOverrideReason).toBe('legacy manual refresh');
|
|
268
274
|
emitSpy.mockRestore();
|
|
269
275
|
});
|
|
270
276
|
|
|
@@ -355,7 +361,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
355
361
|
await doApiPost({
|
|
356
362
|
url: `/products/${testAppName}/${testUserId}/${doNotCallHint}/search`,
|
|
357
363
|
token: userToken,
|
|
358
|
-
payload: { forceRefresh: true },
|
|
364
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
359
365
|
}).then(({ products: p }) => {
|
|
360
366
|
products = p; // Assign to the outer scoped 'products'
|
|
361
367
|
});
|
|
@@ -415,7 +421,11 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
415
421
|
await doApiPost({
|
|
416
422
|
url: `/products/${testAppName}/${testUserId}/${configuredOmitHint}/search`,
|
|
417
423
|
token: userToken,
|
|
418
|
-
payload: {
|
|
424
|
+
payload: {
|
|
425
|
+
forceRefresh: true,
|
|
426
|
+
fullSyncTrigger: 'scheduled',
|
|
427
|
+
omitServiceCodes: [],
|
|
428
|
+
},
|
|
419
429
|
});
|
|
420
430
|
|
|
421
431
|
expect(travelgatePlugin.searchProducts).toHaveBeenCalledTimes(1);
|
|
@@ -642,7 +652,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
642
652
|
const result = await doApiPost({
|
|
643
653
|
url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
|
|
644
654
|
token: userToken,
|
|
645
|
-
payload: { forceRefresh: true },
|
|
655
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
646
656
|
});
|
|
647
657
|
|
|
648
658
|
const cached = await cache.get({ pluginName: testAppName, key: cacheKeyForTest });
|
|
@@ -687,7 +697,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
687
697
|
await doApiPost({
|
|
688
698
|
url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
|
|
689
699
|
token: userToken,
|
|
690
|
-
payload: { forceRefresh: true },
|
|
700
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
691
701
|
});
|
|
692
702
|
|
|
693
703
|
const cached = await cache.get({ pluginName: testAppName, key: cacheKeyForTest });
|
|
@@ -725,7 +735,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
725
735
|
const result = await doApiPost({
|
|
726
736
|
url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
|
|
727
737
|
token: userToken,
|
|
728
|
-
payload: { forceRefresh: true },
|
|
738
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
729
739
|
});
|
|
730
740
|
|
|
731
741
|
const cached = await cache.get({ pluginName: testAppName, key: cacheKeyForTest });
|
|
@@ -764,15 +774,22 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
764
774
|
},
|
|
765
775
|
);
|
|
766
776
|
|
|
767
|
-
it(
|
|
777
|
+
it.each([
|
|
778
|
+
['an omitted trigger', { forceRefresh: true }],
|
|
779
|
+
['a blank reason', {
|
|
780
|
+
forceRefresh: true,
|
|
781
|
+
fullSyncTrigger: 'manual',
|
|
782
|
+
admissionOverrideReason: ' ',
|
|
783
|
+
}],
|
|
784
|
+
])('rejects a manual catalog refresh with %s', async (_description, payload) => {
|
|
768
785
|
const response = await doApiPost({
|
|
769
786
|
url: `/products/${testAppName}/${testUserId}/${staleCacheTestHint}/search`,
|
|
770
787
|
token: userToken,
|
|
771
|
-
payload
|
|
788
|
+
payload,
|
|
772
789
|
expectStatusCode: 400,
|
|
773
790
|
});
|
|
774
791
|
|
|
775
|
-
expect(response.message).toContain('
|
|
792
|
+
expect(response.message).toContain('requires a non-blank admissionOverrideReason');
|
|
776
793
|
expect(travelgatePlugin.searchProducts).not.toHaveBeenCalled();
|
|
777
794
|
});
|
|
778
795
|
|
|
@@ -847,7 +864,7 @@ describe('user: bookings controller - searchProducts', () => {
|
|
|
847
864
|
const result = await doApiPost({
|
|
848
865
|
url: `/products/${testAppName}/${testUserId}/${configuredHint}/search`,
|
|
849
866
|
token: userToken,
|
|
850
|
-
payload: { forceRefresh: true },
|
|
867
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
851
868
|
});
|
|
852
869
|
|
|
853
870
|
expect(result).toEqual(expect.objectContaining({
|
|
@@ -1130,7 +1147,7 @@ describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)',
|
|
|
1130
1147
|
const makeForceRequest = () => doApiPost({
|
|
1131
1148
|
url: `/products/${testAppName}/${testUserId}/${ttrTestHint}/search`,
|
|
1132
1149
|
token: userToken,
|
|
1133
|
-
payload: { forceRefresh: true },
|
|
1150
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
1134
1151
|
});
|
|
1135
1152
|
|
|
1136
1153
|
const requestPromises = [makeForceRequest(), makeForceRequest(), makeForceRequest()];
|
|
@@ -1185,7 +1202,7 @@ describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)',
|
|
|
1185
1202
|
const makeForceRequest = () => doApiPost({
|
|
1186
1203
|
url: `/products/${testAppName}/${testUserId}/${ttrTestHint}/search`,
|
|
1187
1204
|
token: userToken,
|
|
1188
|
-
payload: { forceRefresh: true },
|
|
1205
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
1189
1206
|
});
|
|
1190
1207
|
|
|
1191
1208
|
const requestPromises = [makeForceRequest(), makeForceRequest(), makeForceRequest()];
|
|
@@ -1236,7 +1253,7 @@ describe('Bookings Product Search Lock Mechanism (Job Queuing on Stale Cache)',
|
|
|
1236
1253
|
const result = await doApiPost({
|
|
1237
1254
|
url: `/products/${testAppName}/${testUserId}/${ttrTestHint}/search`,
|
|
1238
1255
|
token: userToken,
|
|
1239
|
-
payload: { forceRefresh: true },
|
|
1256
|
+
payload: { forceRefresh: true, fullSyncTrigger: 'scheduled' },
|
|
1240
1257
|
});
|
|
1241
1258
|
|
|
1242
1259
|
expect(result).toEqual(expect.objectContaining({
|
package/controllers/bookings.js
CHANGED
|
@@ -193,6 +193,14 @@ const createScopedCatalogRefreshError = () => {
|
|
|
193
193
|
return err;
|
|
194
194
|
};
|
|
195
195
|
|
|
196
|
+
const createManualCatalogRefreshReasonError = () => {
|
|
197
|
+
const err = new Error(
|
|
198
|
+
'A manual catalog refresh requires a non-blank admissionOverrideReason',
|
|
199
|
+
);
|
|
200
|
+
err.status = 400;
|
|
201
|
+
return err;
|
|
202
|
+
};
|
|
203
|
+
|
|
196
204
|
const createUnsupportedCatalogRefreshError = appKey => {
|
|
197
205
|
const err = new Error(
|
|
198
206
|
`${appKey} does not support complete product catalog refreshes`,
|
|
@@ -402,7 +410,7 @@ const $bookingsProductSearch = plugins => async ({
|
|
|
402
410
|
requestedFullSyncTrigger,
|
|
403
411
|
) >= 0
|
|
404
412
|
? requestedFullSyncTrigger
|
|
405
|
-
: (forceRefresh ? '
|
|
413
|
+
: (forceRefresh ? 'manual' : 'organic');
|
|
406
414
|
const admissionOverrideReason = typeof requestedOverrideReason === 'string'
|
|
407
415
|
? requestedOverrideReason.trim() || undefined
|
|
408
416
|
: undefined;
|
|
@@ -431,6 +439,14 @@ const $bookingsProductSearch = plugins => async ({
|
|
|
431
439
|
const isScopedSearch = hasProductSearchSelector(normalizedRequestBody);
|
|
432
440
|
const isDeclaredFullCatalogRefresh = forceRefresh
|
|
433
441
|
&& ['scheduled', 'manual'].indexOf(fullSyncTrigger) >= 0;
|
|
442
|
+
if (
|
|
443
|
+
forceRefresh
|
|
444
|
+
&& !cacheOnly
|
|
445
|
+
&& fullSyncTrigger === 'manual'
|
|
446
|
+
&& !admissionOverrideReason
|
|
447
|
+
) {
|
|
448
|
+
throw createManualCatalogRefreshReasonError();
|
|
449
|
+
}
|
|
434
450
|
if (isDeclaredFullCatalogRefresh && isScopedSearch) {
|
|
435
451
|
throw createScopedCatalogRefreshError();
|
|
436
452
|
}
|