This repository has been archived by the owner on May 28, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 337
/
Copy pathcatalog.ts
executable file
·219 lines (196 loc) · 7.8 KB
/
catalog.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import jwt from 'jwt-simple'
import request from 'request'
import ProcessorFactory from '../processor/factory'
import { adjustBackendProxyUrl } from '../lib/elastic'
import cache from '../lib/cache-instance'
import { sha3_224 } from 'js-sha3'
import AttributeService from './attribute/service'
import bodybuilder from 'bodybuilder'
import loadCustomFilters from '../helpers/loadCustomFilters'
import { elasticsearch, SearchQuery } from 'storefront-query-builder'
import { apiError } from '../lib/util'
async function _cacheStorageHandler (config, result, hash, tags) {
if (config.server.useOutputCache && cache) {
return cache.set(
'api:' + hash,
result,
tags
).catch((err) => {
console.error(err)
})
}
}
function _outputFormatter (responseBody, format = 'standard') {
if (format === 'compact') { // simple formatter
delete responseBody.took
delete responseBody.timed_out
delete responseBody._shards
if (responseBody.hits) {
delete responseBody.hits.max_score
responseBody.total = responseBody.hits.total
responseBody.hits = responseBody.hits.hits.map(hit => {
return Object.assign(hit._source, { _score: hit._score })
})
}
}
return responseBody
}
export default ({config, db}) => async function (req, res, body) {
let groupId = null
// Request method handling: exit if not GET or POST
// Other methods - like PUT, DELETE etc. should be available only for authorized users or not available at all)
if (!(req.method === 'GET' || req.method === 'POST' || req.method === 'OPTIONS')) {
const errMessage = 'ERROR: ' + req.method + ' request method is not supported.';
console.error(errMessage);
apiError(res, errMessage);
return;
}
let responseFormat = 'standard'
let requestBody = req.body
if (req.method === 'GET') {
if (req.query.request) { // this is in fact optional
try {
requestBody = JSON.parse(decodeURIComponent(req.query.request))
} catch (err) {
console.error(err);
apiError(res, err);
return;
}
}
}
if (req.query.request_format === 'search-query') { // search query and not Elastic DSL - we need to translate it
try {
const customFilters = await loadCustomFilters(config)
requestBody = await elasticsearch.buildQueryBodyFromSearchQuery({ config, queryChain: bodybuilder(), searchQuery: new SearchQuery(requestBody), customFilters })
} catch (err) {
console.error(err);
apiError(res, err);
return;
}
}
if (req.query.response_format) responseFormat = req.query.response_format
const urlSegments = req.url.split('/')
let indexName = ''
let entityType = ''
if (urlSegments.length < 2) {
const errMessage = 'No index name given in the URL. Please do use following URL format: /api/catalog/<index_name>/<entity_type>_search';
console.error(errMessage);
apiError(res, errMessage);
return;
} else {
indexName = urlSegments[1]
if (urlSegments.length > 2) { entityType = urlSegments[2] }
if (config.elasticsearch.indices.indexOf(indexName) < 0) {
const errMessage = 'Invalid / inaccessible index name given in the URL. Please do use following URL format: /api/catalog/<index_name>/_search';
console.error(errMessage);
apiError(res, errMessage);
return;
}
if (urlSegments[urlSegments.length - 1].indexOf('_search') !== 0) {
const errMessage = 'Please do use following URL format: /api/catalog/<index_name>/_search';
console.error(errMessage);
apiError(res, errMessage);
return;
}
}
// pass the request to elasticsearch
const elasticBackendUrl = adjustBackendProxyUrl(req, indexName, entityType, config)
const userToken = requestBody.groupToken
// Decode token and get group id
if (userToken && userToken.length > 10) {
const decodeToken = jwt.decode(userToken, config.authHashSecret ? config.authHashSecret : config.objHashSecret)
groupId = decodeToken.group_id || groupId
} else if (requestBody.groupId) {
groupId = requestBody.groupId || groupId
}
delete requestBody.groupToken
delete requestBody.groupId
let auth = null
// Only pass auth if configured
if (config.elasticsearch.user || config.elasticsearch.password) {
auth = {
user: config.elasticsearch.user,
pass: config.elasticsearch.password
}
}
const s = Date.now()
const reqHash = sha3_224(`${JSON.stringify(requestBody)}${req.url}`)
const dynamicRequestHandler = () => {
request({ // do the elasticsearch request
uri: elasticBackendUrl,
method: req.method,
body: requestBody,
json: true,
auth: auth
}, async (_err, _res, _resBody) => { // TODO: add caching layer to speed up SSR? How to invalidate products (checksum on the response BEFORE processing it)
if (_err || _resBody.error) {
console.error(_err || _resBody.error)
apiError(res, _err || _resBody.error)
return
}
try {
if (_resBody && _resBody.hits && _resBody.hits.hits) { // we're signing up all objects returned to the client to be able to validate them when (for example order)
const factory = new ProcessorFactory(config)
const tagsArray = []
if (config.server.useOutputCache && cache) {
const tagPrefix = entityType[0].toUpperCase() // first letter of entity name: P, T, A ...
tagsArray.push(entityType)
_resBody.hits.hits.map(item => {
if (item._source.id) { // has common identifier
tagsArray.push(`${tagPrefix}${item._source.id}`)
}
})
const cacheTags = tagsArray.join(' ')
res.setHeader('X-VS-Cache-Tags', cacheTags)
}
let resultProcessor = factory.getAdapter(entityType, indexName, req, res)
if (!resultProcessor) { resultProcessor = factory.getAdapter('default', indexName, req, res) } // get the default processor
const productGroupId = entityType === 'product' ? groupId : undefined
const result = await resultProcessor.process(_resBody.hits.hits, productGroupId)
_resBody.hits.hits = result
if (entityType === 'product' && _resBody.aggregations && config.entities.attribute.loadByAttributeMetadata) {
const attributeListParam = AttributeService.transformAggsToAttributeListParam(_resBody.aggregations)
// find attribute list
const attributeList = await AttributeService.list(attributeListParam, config, indexName)
_resBody.attribute_metadata = attributeList.map(AttributeService.transformToMetadata)
}
_resBody = _outputFormatter(_resBody, responseFormat)
if (config.get('varnish.enabled')) {
// Add tags to cache, so we can display them in response headers then
_cacheStorageHandler(config, {
..._resBody,
tags: tagsArray
}, reqHash, tagsArray)
} else {
_cacheStorageHandler(config, _resBody, reqHash, tagsArray)
}
}
res.json(_resBody)
} catch (err) {
apiError(res, err)
}
})
}
if (config.server.useOutputCache && cache) {
cache.get(
'api:' + reqHash
).then(output => {
if (output !== null) {
res.setHeader('X-VS-Cache', 'Hit')
if (config.get('varnish.enabled')) {
const tagsHeader = output.tags.join(' ')
res.setHeader('X-VS-Cache-Tag', tagsHeader)
delete output.tags
}
res.json(output)
console.log(`cache hit [${req.url}], cached request: ${Date.now() - s}ms`)
} else {
res.setHeader('X-VS-Cache', 'Miss')
console.log(`cache miss [${req.url}], request: ${Date.now() - s}ms`)
dynamicRequestHandler()
}
}).catch(err => console.error(err))
} else {
dynamicRequestHandler()
}
}