ti2 1.0.115 → 1.0.118
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 +1 -0
- package/__tests__/cache.js +64 -0
- package/api.yml +112 -0
- package/cache.js +74 -30
- package/controllers/__tests__/bookings-searchProducts.js +483 -8
- package/controllers/__tests__/bookings.js +53 -0
- package/controllers/bookings.js +352 -48
- package/index.js +3 -0
- package/package.json +1 -1
- package/test/plugin.js +41 -0
- package/tutorials/plugin-development.md +2 -0
- package/.windsurfrules +0 -9
- package/CLAUDE.md +0 -159
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Plugins are the connectores to other systems and/or features you intent to use;
|
|
|
56
56
|
|searchAvailability|✓|✓|✓|✓|✓|✓|
|
|
57
57
|
|searchQuote|✓|✓|✓|✓|✓|✓|
|
|
58
58
|
|createBooking|✓|✓|✓|✓|✓|✓|
|
|
59
|
+
|confirmBooking|✓|✓|✓|✓|✓|✓|
|
|
59
60
|
|cancelBooking|✓|✓|✓|✓|✓|✓|
|
|
60
61
|
|searchProductsForItinerary|||||||||✓|
|
|
61
62
|
|searchAvailabilityForItinerary|||||||||✓|
|
package/__tests__/cache.js
CHANGED
|
@@ -15,6 +15,70 @@ describe('cache', () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
describe('saveIfNotExists', () => {
|
|
19
|
+
it('should only save the first value for a key', async () => {
|
|
20
|
+
const key = 'lock:test';
|
|
21
|
+
testKeys.push(key);
|
|
22
|
+
|
|
23
|
+
const firstSaved = await cache.saveIfNotExists({
|
|
24
|
+
pluginName: testPluginName,
|
|
25
|
+
key,
|
|
26
|
+
value: { owner: 'first' },
|
|
27
|
+
ttl: 60,
|
|
28
|
+
});
|
|
29
|
+
const secondSaved = await cache.saveIfNotExists({
|
|
30
|
+
pluginName: testPluginName,
|
|
31
|
+
key,
|
|
32
|
+
value: { owner: 'second' },
|
|
33
|
+
ttl: 60,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(firstSaved).toBe(true);
|
|
37
|
+
expect(secondSaved).toBe(false);
|
|
38
|
+
expect(await cache.get({ pluginName: testPluginName, key })).toEqual({ owner: 'first' });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('owner-token helpers', () => {
|
|
43
|
+
it('should only renew and drop a key when the expected value still owns it', async () => {
|
|
44
|
+
const key = 'owner-lock:test';
|
|
45
|
+
testKeys.push(key);
|
|
46
|
+
|
|
47
|
+
await cache.save({ pluginName: testPluginName, key, value: 'owner-a', ttl: 1 });
|
|
48
|
+
|
|
49
|
+
const renewedByWrongOwner = await cache.expireIfValue({
|
|
50
|
+
pluginName: testPluginName,
|
|
51
|
+
key,
|
|
52
|
+
value: 'owner-b',
|
|
53
|
+
ttl: 60,
|
|
54
|
+
});
|
|
55
|
+
const renewedByOwner = await cache.expireIfValue({
|
|
56
|
+
pluginName: testPluginName,
|
|
57
|
+
key,
|
|
58
|
+
value: 'owner-a',
|
|
59
|
+
ttl: 60,
|
|
60
|
+
});
|
|
61
|
+
const droppedByWrongOwner = await cache.dropIfValue({
|
|
62
|
+
pluginName: testPluginName,
|
|
63
|
+
key,
|
|
64
|
+
value: 'owner-b',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(renewedByWrongOwner).toBe(false);
|
|
68
|
+
expect(renewedByOwner).toBe(true);
|
|
69
|
+
expect(droppedByWrongOwner).toBe(false);
|
|
70
|
+
expect(await cache.get({ pluginName: testPluginName, key })).toBe('owner-a');
|
|
71
|
+
|
|
72
|
+
const droppedByOwner = await cache.dropIfValue({
|
|
73
|
+
pluginName: testPluginName,
|
|
74
|
+
key,
|
|
75
|
+
value: 'owner-a',
|
|
76
|
+
});
|
|
77
|
+
expect(droppedByOwner).toBe(true);
|
|
78
|
+
expect(await cache.get({ pluginName: testPluginName, key })).toBeFalsy();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
18
82
|
describe('scan', () => {
|
|
19
83
|
it('should return keys matching the pattern without plugin prefix', async () => {
|
|
20
84
|
// Save some test keys
|
package/api.yml
CHANGED
|
@@ -358,6 +358,39 @@ components:
|
|
|
358
358
|
type: object
|
|
359
359
|
allOf: # Combines the BasicErrorModel and the inline model
|
|
360
360
|
- $ref: '#/components/schemas/bookingDetail'
|
|
361
|
+
bookingDetailsForConfirm:
|
|
362
|
+
type: object
|
|
363
|
+
additionalProperties: true
|
|
364
|
+
description: Booking identifier; provide bookingId, id, ref, or reference
|
|
365
|
+
properties:
|
|
366
|
+
bookingId:
|
|
367
|
+
type: string
|
|
368
|
+
id:
|
|
369
|
+
type: string
|
|
370
|
+
ref:
|
|
371
|
+
type: string
|
|
372
|
+
reference:
|
|
373
|
+
type: string
|
|
374
|
+
confirmBookingReturn:
|
|
375
|
+
type: object
|
|
376
|
+
additionalProperties: true
|
|
377
|
+
properties:
|
|
378
|
+
confirmBookingReply:
|
|
379
|
+
type: object
|
|
380
|
+
booking:
|
|
381
|
+
type: object
|
|
382
|
+
properties:
|
|
383
|
+
ref:
|
|
384
|
+
type: string
|
|
385
|
+
id:
|
|
386
|
+
type: string
|
|
387
|
+
status:
|
|
388
|
+
type: string
|
|
389
|
+
serviceLines:
|
|
390
|
+
type: array
|
|
391
|
+
items:
|
|
392
|
+
type: object
|
|
393
|
+
additionalProperties: true
|
|
361
394
|
packageSearchQuery:
|
|
362
395
|
type: object
|
|
363
396
|
additionalProperties: true
|
|
@@ -1815,6 +1848,85 @@ paths:
|
|
|
1815
1848
|
application/json:
|
|
1816
1849
|
schema:
|
|
1817
1850
|
$ref: '#/components/schemas/cancellationReturn'
|
|
1851
|
+
/bookings/{appKey}/{userId}/confirm:
|
|
1852
|
+
post:
|
|
1853
|
+
security:
|
|
1854
|
+
- bearerAuth: ['admin', 'user']
|
|
1855
|
+
tags:
|
|
1856
|
+
- bookings
|
|
1857
|
+
summary: Confirms a quote as a booking
|
|
1858
|
+
operationId: confirmBooking
|
|
1859
|
+
parameters:
|
|
1860
|
+
- name: appKey
|
|
1861
|
+
in: path
|
|
1862
|
+
description: app to confirm the booking for, defaults to first token
|
|
1863
|
+
required: true
|
|
1864
|
+
example: 'travelgate-hotelx'
|
|
1865
|
+
schema:
|
|
1866
|
+
type: string
|
|
1867
|
+
- name: userId
|
|
1868
|
+
in: path
|
|
1869
|
+
description: the user/company that the appKey belongs to
|
|
1870
|
+
required: true
|
|
1871
|
+
schema:
|
|
1872
|
+
type: string
|
|
1873
|
+
requestBody:
|
|
1874
|
+
description: Booking identifier to confirm
|
|
1875
|
+
required: true
|
|
1876
|
+
content:
|
|
1877
|
+
application/json:
|
|
1878
|
+
schema:
|
|
1879
|
+
$ref: '#/components/schemas/bookingDetailsForConfirm'
|
|
1880
|
+
responses:
|
|
1881
|
+
'200':
|
|
1882
|
+
description: Booking confirmation details
|
|
1883
|
+
content:
|
|
1884
|
+
application/json:
|
|
1885
|
+
schema:
|
|
1886
|
+
$ref: '#/components/schemas/confirmBookingReturn'
|
|
1887
|
+
/bookings/{appKey}/{userId}/{hint}/confirm:
|
|
1888
|
+
post:
|
|
1889
|
+
security:
|
|
1890
|
+
- bearerAuth: ['admin', 'user']
|
|
1891
|
+
tags:
|
|
1892
|
+
- bookings
|
|
1893
|
+
summary: Confirms a quote as a booking for a specific hint
|
|
1894
|
+
operationId: confirmBooking
|
|
1895
|
+
parameters:
|
|
1896
|
+
- name: appKey
|
|
1897
|
+
in: path
|
|
1898
|
+
description: app to confirm the booking for
|
|
1899
|
+
required: true
|
|
1900
|
+
example: 'travelgate'
|
|
1901
|
+
schema:
|
|
1902
|
+
type: string
|
|
1903
|
+
- name: userId
|
|
1904
|
+
in: path
|
|
1905
|
+
description: the user/company that the appKey belongs to
|
|
1906
|
+
required: true
|
|
1907
|
+
schema:
|
|
1908
|
+
type: string
|
|
1909
|
+
- name: hint
|
|
1910
|
+
in: path
|
|
1911
|
+
description: a specific token hint to be used
|
|
1912
|
+
required: true
|
|
1913
|
+
example: 'travelgate-australia'
|
|
1914
|
+
schema:
|
|
1915
|
+
type: string
|
|
1916
|
+
requestBody:
|
|
1917
|
+
description: Booking identifier to confirm
|
|
1918
|
+
required: true
|
|
1919
|
+
content:
|
|
1920
|
+
application/json:
|
|
1921
|
+
schema:
|
|
1922
|
+
$ref: '#/components/schemas/bookingDetailsForConfirm'
|
|
1923
|
+
responses:
|
|
1924
|
+
'200':
|
|
1925
|
+
description: Booking confirmation details
|
|
1926
|
+
content:
|
|
1927
|
+
application/json:
|
|
1928
|
+
schema:
|
|
1929
|
+
$ref: '#/components/schemas/confirmBookingReturn'
|
|
1818
1930
|
/packages/{appKey}/{userId}/search:
|
|
1819
1931
|
post:
|
|
1820
1932
|
security:
|
package/cache.js
CHANGED
|
@@ -5,6 +5,23 @@ const REDIS_URL = process.env.REDIS_URL || 'redis://redis:6379';
|
|
|
5
5
|
const cache = new Redis(`${REDIS_URL}/2`);
|
|
6
6
|
const defaultTTL = 60 * 60; // 1 hour
|
|
7
7
|
|
|
8
|
+
const normalizeKey = ({
|
|
9
|
+
key: keyParam,
|
|
10
|
+
fn,
|
|
11
|
+
fnParams,
|
|
12
|
+
useFunctionFallback = false,
|
|
13
|
+
}) => {
|
|
14
|
+
if (useFunctionFallback && !keyParam) {
|
|
15
|
+
return hash({ fn, fnParams });
|
|
16
|
+
}
|
|
17
|
+
if (typeof keyParam !== 'string') {
|
|
18
|
+
return hash(keyParam);
|
|
19
|
+
}
|
|
20
|
+
return keyParam;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const buildKey = ({ pluginName, ...args }) => `${pluginName}:${normalizeKey(args)}`;
|
|
24
|
+
|
|
8
25
|
const save = async ({
|
|
9
26
|
pluginName,
|
|
10
27
|
key: keyParam,
|
|
@@ -12,12 +29,7 @@ const save = async ({
|
|
|
12
29
|
skipTTL,
|
|
13
30
|
ttl = defaultTTL,
|
|
14
31
|
}) => {
|
|
15
|
-
const key =
|
|
16
|
-
if (typeof keyParam !== 'string') {
|
|
17
|
-
return hash(keyParam);
|
|
18
|
-
}
|
|
19
|
-
return keyParam;
|
|
20
|
-
})()}`;
|
|
32
|
+
const key = buildKey({ pluginName, key: keyParam });
|
|
21
33
|
// console.log('saving key', key);
|
|
22
34
|
await cache.set(key, JSON.stringify(value));
|
|
23
35
|
if (!skipTTL) {
|
|
@@ -25,16 +37,55 @@ const save = async ({
|
|
|
25
37
|
}
|
|
26
38
|
};
|
|
27
39
|
|
|
40
|
+
const saveIfNotExists = async ({
|
|
41
|
+
pluginName,
|
|
42
|
+
key: keyParam,
|
|
43
|
+
value,
|
|
44
|
+
ttl = defaultTTL,
|
|
45
|
+
}) => {
|
|
46
|
+
const key = buildKey({ pluginName, key: keyParam });
|
|
47
|
+
const result = await cache.set(key, JSON.stringify(value), 'EX', ttl, 'NX');
|
|
48
|
+
return result === 'OK';
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const expireIfValue = async ({
|
|
52
|
+
pluginName,
|
|
53
|
+
key: keyParam,
|
|
54
|
+
value,
|
|
55
|
+
ttl = defaultTTL,
|
|
56
|
+
}) => {
|
|
57
|
+
const key = buildKey({ pluginName, key: keyParam });
|
|
58
|
+
const expectedValue = JSON.stringify(value);
|
|
59
|
+
const result = await cache.eval(`
|
|
60
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
61
|
+
return redis.call('expire', KEYS[1], ARGV[2])
|
|
62
|
+
end
|
|
63
|
+
return 0
|
|
64
|
+
`, 1, key, expectedValue, ttl);
|
|
65
|
+
return result === 1;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const dropIfValue = async ({
|
|
69
|
+
pluginName,
|
|
70
|
+
key: keyParam,
|
|
71
|
+
value,
|
|
72
|
+
}) => {
|
|
73
|
+
const key = buildKey({ pluginName, key: keyParam });
|
|
74
|
+
const expectedValue = JSON.stringify(value);
|
|
75
|
+
const result = await cache.eval(`
|
|
76
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
77
|
+
return redis.call('del', KEYS[1])
|
|
78
|
+
end
|
|
79
|
+
return 0
|
|
80
|
+
`, 1, key, expectedValue);
|
|
81
|
+
return result === 1;
|
|
82
|
+
};
|
|
83
|
+
|
|
28
84
|
const get = async ({
|
|
29
85
|
pluginName,
|
|
30
86
|
key: keyParam,
|
|
31
87
|
}) => {
|
|
32
|
-
const key =
|
|
33
|
-
if (typeof keyParam !== 'string') {
|
|
34
|
-
return hash(keyParam);
|
|
35
|
-
}
|
|
36
|
-
return keyParam;
|
|
37
|
-
})()}`;
|
|
88
|
+
const key = buildKey({ pluginName, key: keyParam });
|
|
38
89
|
const storeVal = await cache.get(key);
|
|
39
90
|
if (!storeVal) return storeVal;
|
|
40
91
|
return JSON.parse(storeVal);
|
|
@@ -48,15 +99,7 @@ const getOrExec = async ({
|
|
|
48
99
|
fnParams,
|
|
49
100
|
forceRefresh,
|
|
50
101
|
}) => {
|
|
51
|
-
const key = (
|
|
52
|
-
if (!keyParam) {
|
|
53
|
-
return hash({ fn, fnParams });
|
|
54
|
-
}
|
|
55
|
-
if (typeof keyParam !== 'string') {
|
|
56
|
-
return hash(keyParam);
|
|
57
|
-
}
|
|
58
|
-
return keyParam;
|
|
59
|
-
})();
|
|
102
|
+
const key = normalizeKey({ key: keyParam, fn, fnParams, useFunctionFallback: true });
|
|
60
103
|
let value;
|
|
61
104
|
if (!forceRefresh) {
|
|
62
105
|
value = await get({ pluginName, key });
|
|
@@ -78,15 +121,13 @@ const drop = async ({
|
|
|
78
121
|
fn,
|
|
79
122
|
fnParams,
|
|
80
123
|
}) => {
|
|
81
|
-
const key =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return keyParam;
|
|
89
|
-
})()}`;
|
|
124
|
+
const key = buildKey({
|
|
125
|
+
pluginName,
|
|
126
|
+
key: keyParam,
|
|
127
|
+
fn,
|
|
128
|
+
fnParams,
|
|
129
|
+
useFunctionFallback: true,
|
|
130
|
+
});
|
|
90
131
|
// console.log('dropping key', key);
|
|
91
132
|
await cache.del(key);
|
|
92
133
|
};
|
|
@@ -116,6 +157,9 @@ const scan = ({
|
|
|
116
157
|
module.exports = {
|
|
117
158
|
cache,
|
|
118
159
|
save,
|
|
160
|
+
saveIfNotExists,
|
|
161
|
+
expireIfValue,
|
|
162
|
+
dropIfValue,
|
|
119
163
|
getOrExec,
|
|
120
164
|
drop,
|
|
121
165
|
get,
|