ti2-bokun 1.0.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/README.md +91 -0
- package/index.js +1026 -0
- package/package.json +58 -0
- package/resolvers/availability.js +366 -0
- package/resolvers/booking.js +356 -0
- package/resolvers/pickup-point.js +158 -0
- package/resolvers/product.js +155 -0
- package/resolvers/rate.js +33 -0
- package/utils/wildcardMatch.js +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ti2-bokun",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bokun's TI2 Plugin",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:watch": "jest --watch",
|
|
9
|
+
"test:coverage": "jest --coverage",
|
|
10
|
+
"lint": "eslint . --ext .js",
|
|
11
|
+
"lint:fix": "eslint . --ext .js --fix",
|
|
12
|
+
"prepublishOnly": "npm test",
|
|
13
|
+
"version": "git add -A .",
|
|
14
|
+
"postversion": "git push && git push --tags"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/TourConnect/ti2-bokun.git"
|
|
19
|
+
},
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "GPLV3",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/TourConnect/ti2-bokun/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/TourConnect/ti2-bokun#readme",
|
|
26
|
+
"jest": {
|
|
27
|
+
"testEnvironment": "node",
|
|
28
|
+
"coveragePathIgnorePatterns": [
|
|
29
|
+
"/node_modules/"
|
|
30
|
+
],
|
|
31
|
+
"setupFilesAfterEnv": [
|
|
32
|
+
"./jest.setup.js"
|
|
33
|
+
],
|
|
34
|
+
"testTimeout": 10000
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@graphql-tools/schema": "^9.0.12",
|
|
38
|
+
"bluebird": "^3.7.2",
|
|
39
|
+
"graphql": "^16.6.0",
|
|
40
|
+
"jsonwebtoken": "^8.5.1",
|
|
41
|
+
"moment": "^2.29.4",
|
|
42
|
+
"ramda": "^0.27.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"axios": "^0.27.2",
|
|
46
|
+
"chalk": "^4.1.2",
|
|
47
|
+
"eslint": "^8.4.1",
|
|
48
|
+
"eslint-config-airbnb": "^19.0.2",
|
|
49
|
+
"eslint-plugin-import": "^2.25.4",
|
|
50
|
+
"faker": "^5.5.3",
|
|
51
|
+
"jest": "^27.4.5",
|
|
52
|
+
"jest-cli": "^27.4.7",
|
|
53
|
+
"jest-diff": "^27.4.2",
|
|
54
|
+
"jest-environment-node": "27.4",
|
|
55
|
+
"jest-util": "27.4",
|
|
56
|
+
"ti2": "latest"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
const { makeExecutableSchema } = require('@graphql-tools/schema');
|
|
2
|
+
const R = require('ramda');
|
|
3
|
+
const { graphql } = require('graphql');
|
|
4
|
+
const jwt = require('jsonwebtoken');
|
|
5
|
+
const moment = require('moment');
|
|
6
|
+
|
|
7
|
+
const baseAvailabilityResolvers = {
|
|
8
|
+
key: (root, args) => {
|
|
9
|
+
const {
|
|
10
|
+
productId,
|
|
11
|
+
optionId,
|
|
12
|
+
currency,
|
|
13
|
+
unitsWithQuantity,
|
|
14
|
+
jwtKey,
|
|
15
|
+
} = args;
|
|
16
|
+
|
|
17
|
+
if (!jwtKey) return null;
|
|
18
|
+
|
|
19
|
+
const availabilityId = root.availabilityId || root.id;
|
|
20
|
+
const startTimeId = root.startTimeId || availabilityId;
|
|
21
|
+
|
|
22
|
+
if (!availabilityId) return null;
|
|
23
|
+
|
|
24
|
+
const unitItems = (unitsWithQuantity || []).map(unit => ({
|
|
25
|
+
unitId: unit.unitId,
|
|
26
|
+
quantity: unit.quantity || 1,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
return jwt.sign({
|
|
30
|
+
productId,
|
|
31
|
+
availabilityId,
|
|
32
|
+
startTimeId,
|
|
33
|
+
rateId: optionId,
|
|
34
|
+
currency,
|
|
35
|
+
unitItems,
|
|
36
|
+
localDateTimeStart: root.localDateTimeStart,
|
|
37
|
+
}, jwtKey);
|
|
38
|
+
},
|
|
39
|
+
localDateTimeStart: o => {
|
|
40
|
+
const date = R.path(['date'], o);
|
|
41
|
+
const time = R.path(['time'], o) || R.path(['startTime'], o) || '00:00';
|
|
42
|
+
const dateTime = R.path(['localDateTimeStart'], o);
|
|
43
|
+
|
|
44
|
+
if (dateTime) return dateTime;
|
|
45
|
+
if (date && time) return `${date}T${time}`;
|
|
46
|
+
|
|
47
|
+
return moment().format();
|
|
48
|
+
},
|
|
49
|
+
dateTimeStart: o => {
|
|
50
|
+
const dateTimeStart = R.path(['dateTimeStart'], o);
|
|
51
|
+
if (dateTimeStart) return dateTimeStart;
|
|
52
|
+
return R.path(['localDateTimeStart'], o) || R.path(['utcDateTimeStart'], o) || null;
|
|
53
|
+
},
|
|
54
|
+
localDateTimeEnd: o => {
|
|
55
|
+
const dateTimeEnd = R.path(['localDateTimeEnd'], o);
|
|
56
|
+
if (dateTimeEnd) return dateTimeEnd;
|
|
57
|
+
|
|
58
|
+
const date = R.path(['date'], o);
|
|
59
|
+
const time = R.path(['time'], o) || R.path(['startTime'], o) || '00:00';
|
|
60
|
+
const duration = R.path(['duration'], o) || 120;
|
|
61
|
+
|
|
62
|
+
if (date && time) {
|
|
63
|
+
return moment(`${date}T${time}`).add(duration, 'minutes').format();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return moment().add(2, 'hours').format();
|
|
67
|
+
},
|
|
68
|
+
dateTimeEnd: o => {
|
|
69
|
+
const dateTimeEnd = R.path(['dateTimeEnd'], o);
|
|
70
|
+
if (dateTimeEnd) return dateTimeEnd;
|
|
71
|
+
return R.path(['localDateTimeEnd'], o) || R.path(['utcDateTimeEnd'], o) || null;
|
|
72
|
+
},
|
|
73
|
+
allDay: o => R.path(['allDay'], o) || false,
|
|
74
|
+
available: o => {
|
|
75
|
+
const available = R.path(['available'], o);
|
|
76
|
+
const availabilityCount = R.path(['availabilityCount'], o);
|
|
77
|
+
const status = R.path(['status'], o);
|
|
78
|
+
|
|
79
|
+
if (available !== undefined) return available;
|
|
80
|
+
if (availabilityCount !== undefined) return availabilityCount > 0;
|
|
81
|
+
if (status) return status === 'AVAILABLE';
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
status: o => {
|
|
86
|
+
const status = R.path(['status'], o);
|
|
87
|
+
if (status) return status;
|
|
88
|
+
|
|
89
|
+
const availabilityCount = R.path(['availabilityCount'], o);
|
|
90
|
+
const available = R.path(['available'], o);
|
|
91
|
+
|
|
92
|
+
if (availabilityCount !== undefined) {
|
|
93
|
+
return availabilityCount > 0 ? 'AVAILABLE' : 'SOLD_OUT';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (available !== undefined) {
|
|
97
|
+
return available ? 'AVAILABLE' : 'SOLD_OUT';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 'AVAILABLE';
|
|
101
|
+
},
|
|
102
|
+
vacancies: o => R.path(['vacancies'], o) || R.path(['availabilityCount'], o) || 0,
|
|
103
|
+
capacity: o => R.path(['capacity'], o) || R.path(['availabilityCount'], o) || 0,
|
|
104
|
+
maxUnits: o => R.path(['maxUnits'], o) || R.path(['availabilityCount'], o) || 999,
|
|
105
|
+
utcDateTimeStart: o => {
|
|
106
|
+
const utcDateTime = R.path(['utcDateTimeStart'], o);
|
|
107
|
+
if (utcDateTime) return utcDateTime;
|
|
108
|
+
|
|
109
|
+
const date = R.path(['date'], o);
|
|
110
|
+
const time = R.path(['time'], o) || R.path(['startTime'], o) || '00:00';
|
|
111
|
+
|
|
112
|
+
if (date && time) {
|
|
113
|
+
return moment.utc(`${date}T${time}`).format();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return moment.utc().format();
|
|
117
|
+
},
|
|
118
|
+
utcDateTimeEnd: o => {
|
|
119
|
+
const utcDateTimeEnd = R.path(['utcDateTimeEnd'], o);
|
|
120
|
+
if (utcDateTimeEnd) return utcDateTimeEnd;
|
|
121
|
+
|
|
122
|
+
const date = R.path(['date'], o);
|
|
123
|
+
const time = R.path(['time'], o) || R.path(['startTime'], o) || '00:00';
|
|
124
|
+
const duration = R.path(['duration'], o) || 120;
|
|
125
|
+
|
|
126
|
+
if (date && time) {
|
|
127
|
+
return moment.utc(`${date}T${time}`).add(duration, 'minutes').format();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return moment.utc().add(2, 'hours').format();
|
|
131
|
+
},
|
|
132
|
+
unitPricing: o => {
|
|
133
|
+
const raw = R.path(['unitPricing'], o) || [];
|
|
134
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
135
|
+
return raw.map(unit => {
|
|
136
|
+
const hasNestedPricing = unit.pricing != null
|
|
137
|
+
&& (Array.isArray(unit.pricing) ? unit.pricing.length > 0 : typeof unit.pricing === 'object');
|
|
138
|
+
if (hasNestedPricing) {
|
|
139
|
+
const arr = Array.isArray(unit.pricing) ? unit.pricing : [unit.pricing];
|
|
140
|
+
return {
|
|
141
|
+
...unit,
|
|
142
|
+
pricing: arr.map(p => {
|
|
143
|
+
const price = p.price != null ? p.price : (p.retail != null ? p.retail : (p.original != null ? p.original : (p.net != null ? p.net : 0)));
|
|
144
|
+
return {
|
|
145
|
+
...p,
|
|
146
|
+
price,
|
|
147
|
+
original: p.original != null ? p.original : (p.retail != null ? p.retail : price),
|
|
148
|
+
retail: p.retail != null ? p.retail : (p.original != null ? p.original : price),
|
|
149
|
+
net: p.net != null ? p.net : (p.retail != null ? p.retail : (p.original != null ? p.original : price)),
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const price = unit.retail != null ? unit.retail : (unit.original != null ? unit.original : (unit.net != null ? unit.net : (unit.price != null ? unit.price : 0)));
|
|
155
|
+
const p = {
|
|
156
|
+
original: unit.original != null ? unit.original : (unit.retail != null ? unit.retail : price),
|
|
157
|
+
retail: unit.retail != null ? unit.retail : (unit.original != null ? unit.original : price),
|
|
158
|
+
net: unit.net != null ? unit.net : (unit.retail != null ? unit.retail : (unit.original != null ? unit.original : price)),
|
|
159
|
+
currency: unit.currency || 'USD',
|
|
160
|
+
currencyPrecision: unit.currencyPrecision != null ? unit.currencyPrecision : 2,
|
|
161
|
+
price,
|
|
162
|
+
};
|
|
163
|
+
return { ...unit, pricing: [p] };
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
pricing: (o, _args, _context, info) => {
|
|
167
|
+
const pricing = R.path(['pricing'], o);
|
|
168
|
+
const firstUnitId = R.path(['unitPricing', 0, 'unitId'], o);
|
|
169
|
+
const withUnitId = p => ({ ...p, unitId: p.unitId != null ? p.unitId : firstUnitId });
|
|
170
|
+
const maybeList = items => {
|
|
171
|
+
if (!info || !info.returnType) return items;
|
|
172
|
+
const typeString = info.returnType.toString();
|
|
173
|
+
const expectsList = typeString.startsWith('[');
|
|
174
|
+
return expectsList ? items : (items[0] || null);
|
|
175
|
+
};
|
|
176
|
+
if (pricing != null) {
|
|
177
|
+
const arr = Array.isArray(pricing) ? pricing : [pricing];
|
|
178
|
+
const mapped = arr.map(p => {
|
|
179
|
+
const price = p.price != null ? p.price : (p.retail != null ? p.retail : (p.original != null ? p.original : (p.net != null ? p.net : 0)));
|
|
180
|
+
return withUnitId({
|
|
181
|
+
...p,
|
|
182
|
+
price,
|
|
183
|
+
original: p.original != null ? p.original : (p.retail != null ? p.retail : price),
|
|
184
|
+
retail: p.retail != null ? p.retail : (p.original != null ? p.original : price),
|
|
185
|
+
net: p.net != null ? p.net : (p.retail != null ? p.retail : (p.original != null ? p.original : price)),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
return maybeList(mapped);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const price = R.path(['price'], o);
|
|
192
|
+
const currency = R.path(['currency'], o) || 'USD';
|
|
193
|
+
|
|
194
|
+
if (price !== undefined) {
|
|
195
|
+
return maybeList([withUnitId({
|
|
196
|
+
currency,
|
|
197
|
+
currencyPrecision: 2,
|
|
198
|
+
price,
|
|
199
|
+
retail: price,
|
|
200
|
+
original: price,
|
|
201
|
+
})]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const unitPricing = R.path(['unitPricing'], o);
|
|
205
|
+
if (unitPricing && unitPricing.length > 0) {
|
|
206
|
+
const first = unitPricing[0];
|
|
207
|
+
if (first.pricing && (Array.isArray(first.pricing) ? first.pricing.length > 0 : true)) {
|
|
208
|
+
const arr = Array.isArray(first.pricing) ? first.pricing : [first.pricing];
|
|
209
|
+
const mapped = arr.map(p => withUnitId({
|
|
210
|
+
...p,
|
|
211
|
+
price: p.price != null ? p.price : (p.retail != null ? p.retail : p.original),
|
|
212
|
+
original: p.original != null ? p.original : (p.retail != null ? p.retail : (p.price != null ? p.price : 0)),
|
|
213
|
+
retail: p.retail != null ? p.retail : (p.original != null ? p.original : (p.price != null ? p.price : 0)),
|
|
214
|
+
net: p.net != null ? p.net : (p.retail != null ? p.retail : (p.original != null ? p.original : (p.price != null ? p.price : 0))),
|
|
215
|
+
}));
|
|
216
|
+
return maybeList(mapped);
|
|
217
|
+
}
|
|
218
|
+
return maybeList([withUnitId({
|
|
219
|
+
original: first.original,
|
|
220
|
+
retail: first.retail,
|
|
221
|
+
net: first.net,
|
|
222
|
+
currency: first.currency || 'USD',
|
|
223
|
+
currencyPrecision: first.currencyPrecision != null ? first.currencyPrecision : 2,
|
|
224
|
+
price: first.retail != null ? first.retail : (first.original != null ? first.original : first.net),
|
|
225
|
+
})]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return maybeList([withUnitId({
|
|
229
|
+
currency: 'USD',
|
|
230
|
+
currencyPrecision: 2,
|
|
231
|
+
price: 0,
|
|
232
|
+
retail: 0,
|
|
233
|
+
original: 0,
|
|
234
|
+
})]);
|
|
235
|
+
},
|
|
236
|
+
offer: R.path(['offer']),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const COMMON_AVAILABILITY_TYPES = [
|
|
240
|
+
'Availability',
|
|
241
|
+
'AvailabilityNode',
|
|
242
|
+
'AvailabilityEdge',
|
|
243
|
+
'AvailabilityResult',
|
|
244
|
+
'AvailabilityItem',
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const PICKUP_TYPE_NAMES = ['PickupPoint', 'PickupPointNode', 'PickupLocation'];
|
|
248
|
+
|
|
249
|
+
const getSchemaTypeNames = typeDefs => {
|
|
250
|
+
const names = new Set();
|
|
251
|
+
const strings = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
|
|
252
|
+
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}/g;
|
|
253
|
+
|
|
254
|
+
strings.forEach(def => {
|
|
255
|
+
if (typeof def !== 'string') return;
|
|
256
|
+
let match = typeRegex.exec(def);
|
|
257
|
+
while (match) {
|
|
258
|
+
names.add(match[1]);
|
|
259
|
+
match = typeRegex.exec(def);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return names;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const getTypeFields = typeDefs => {
|
|
267
|
+
const typeFields = {};
|
|
268
|
+
const strings = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
|
|
269
|
+
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}/g;
|
|
270
|
+
const fieldRegex = /([A-Za-z0-9_]+)\s*[:([]/g;
|
|
271
|
+
|
|
272
|
+
strings.forEach(def => {
|
|
273
|
+
if (typeof def !== 'string') return;
|
|
274
|
+
let match = typeRegex.exec(def);
|
|
275
|
+
while (match) {
|
|
276
|
+
const typeName = match[1];
|
|
277
|
+
const body = match[2];
|
|
278
|
+
const fields = new Set();
|
|
279
|
+
let fieldMatch = fieldRegex.exec(body);
|
|
280
|
+
while (fieldMatch) {
|
|
281
|
+
fields.add(fieldMatch[1]);
|
|
282
|
+
fieldMatch = fieldRegex.exec(body);
|
|
283
|
+
}
|
|
284
|
+
typeFields[typeName] = fields;
|
|
285
|
+
match = typeRegex.exec(def);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return typeFields;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const findAvailabilityTypeNames = typeDefs => {
|
|
293
|
+
if (!typeDefs) return [];
|
|
294
|
+
|
|
295
|
+
const names = new Set();
|
|
296
|
+
const schemaTypeNames = getSchemaTypeNames(typeDefs);
|
|
297
|
+
const strings = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
|
|
298
|
+
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}/g;
|
|
299
|
+
const availabilityFieldPattern = /localDate|localDateTimeStart|availabilityId|availabilityCount|unitPricing/;
|
|
300
|
+
|
|
301
|
+
strings.forEach(def => {
|
|
302
|
+
if (typeof def !== 'string') return;
|
|
303
|
+
|
|
304
|
+
let match = typeRegex.exec(def);
|
|
305
|
+
while (match) {
|
|
306
|
+
const typeName = match[1];
|
|
307
|
+
const body = match[2];
|
|
308
|
+
const isPickupType = PICKUP_TYPE_NAMES.some(name => typeName === name || /pickup/i.test(typeName));
|
|
309
|
+
if (!isPickupType && availabilityFieldPattern.test(body)) {
|
|
310
|
+
names.add(typeName);
|
|
311
|
+
}
|
|
312
|
+
match = typeRegex.exec(def);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (names.size === 0) {
|
|
317
|
+
COMMON_AVAILABILITY_TYPES.filter(name => schemaTypeNames.has(name))
|
|
318
|
+
.forEach(name => names.add(name));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return Array.from(names);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const translateAvailability = async ({
|
|
325
|
+
rootValue,
|
|
326
|
+
typeDefs,
|
|
327
|
+
query,
|
|
328
|
+
variableValues,
|
|
329
|
+
}) => {
|
|
330
|
+
const availabilityTypeNames = findAvailabilityTypeNames(typeDefs);
|
|
331
|
+
const typeFields = getTypeFields(typeDefs);
|
|
332
|
+
|
|
333
|
+
const resolvers = availabilityTypeNames.reduce((acc, typeName) => {
|
|
334
|
+
const fields = typeFields[typeName];
|
|
335
|
+
if (!fields || fields.size === 0) return acc;
|
|
336
|
+
|
|
337
|
+
const filteredResolvers = {};
|
|
338
|
+
Object.keys(baseAvailabilityResolvers).forEach(fieldName => {
|
|
339
|
+
if (fields.has(fieldName)) {
|
|
340
|
+
filteredResolvers[fieldName] = baseAvailabilityResolvers[fieldName];
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (Object.keys(filteredResolvers).length > 0) {
|
|
345
|
+
acc[typeName] = filteredResolvers;
|
|
346
|
+
}
|
|
347
|
+
return acc;
|
|
348
|
+
}, {});
|
|
349
|
+
|
|
350
|
+
const schema = makeExecutableSchema({
|
|
351
|
+
typeDefs,
|
|
352
|
+
resolvers,
|
|
353
|
+
});
|
|
354
|
+
const retVal = await graphql({
|
|
355
|
+
schema,
|
|
356
|
+
rootValue,
|
|
357
|
+
source: query,
|
|
358
|
+
variableValues,
|
|
359
|
+
});
|
|
360
|
+
if (retVal.errors) throw new Error(retVal.errors);
|
|
361
|
+
return retVal.data;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
translateAvailability,
|
|
366
|
+
};
|