Skip to content

Commit 182daa1

Browse files
dmitry-tveryanovichkibin
authored andcommitted
added the ability to pass a specialized fetcher
this allows to use nodejs with non-standart environments (zendesk, telligent) where there's no direct acces to the fetch or https , but the application provides special request API
1 parent e19fb5c commit 182daa1

File tree

4 files changed

+288
-29
lines changed

4 files changed

+288
-29
lines changed

package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222
"lint": "eslint src test",
2323
"eslint": "eslint",
2424
"jest": "jest --env node",
25-
"test": "export $(cat .env) && jest --env node --coverage --collectCoverageFrom=src/*.js --forceExit test/*",
26-
"test:win": "cross-env NODE_ENV=production && jest --env node --coverage --collectCoverageFrom=src/*.js --forceExit test/*",
25+
"test_old": "export $(cat .env) && jest --env node --coverage --collectCoverageFrom=src/*.js --forceExit test/*",
26+
"test": "cross-env NODE_ENV=production && jest --env node --coverage --collectCoverageFrom=src/*.js --forceExit test/*",
2727
"test-in-browser": "serve . && echo 'Visit http://localhost:5000/samples/browser-app'",
2828
"build_pack": "cross-env BABEL_ENV=production babel src --out-dir lib",
2929
"build_dist": "webpack",
30-
"build": "yarn build_pack; yarn build_dist",
30+
"build_old": "yarn build_pack; yarn build_dist",
31+
"build": "run-s build_pack build_dist",
3132
"prepublish": "yarn clean && yarn lint && yarn test && yarn build",
3233
"format_test": "prettier --config test/.prettierrc --write 'test*/*.js'",
3334
"format_ex": "prettier --config .prettierrc --write '*.js'",
@@ -51,6 +52,7 @@
5152
"https-browserify": "^1.0.0",
5253
"jest": "^29.5.0",
5354
"node-stdlib-browser": "^1.2.0",
55+
"node-fetch": "^3.3.1",
5456
"rimraf": "^5.0.1",
5557
"stream-http": "^3.2.0",
5658
"url": "^0.11.0",

src/index.js

+37-26
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,37 @@ const {
1616
} = require('./utils')
1717
const HOST = process.env.INTENTO_API_HOST || 'api.inten.to'
1818

19+
/**
20+
* Default fetcher based on https.request
21+
*
22+
* @returns {undefined}
23+
*/
24+
function defaultFetcher({ requestOptions, debug, verbose, data, content }) {
25+
return new Promise((resolve, reject) => {
26+
27+
try {
28+
const req = https.request(requestOptions, resp =>
29+
responseHandler(resp, resolve, reject, debug, verbose)
30+
)
31+
32+
req.on('error', function (err) {
33+
if (err.code === 'ENOTFOUND') {
34+
console.error('Host look up failed: \n', err)
35+
console.log('\nPlease, check internet connection\n')
36+
} else {
37+
customErrorLog(err, 'Fails getting a response from the API')
38+
}
39+
})
40+
req.on('timeout', function (err) {
41+
customErrorLog(err, 'Are you offline?')
42+
})
43+
req.write(data || JSON.stringify(content) || '')
44+
req.end()
45+
} catch (e) {
46+
customErrorLog(e, 'Fails to send a request to the API')
47+
}
48+
})
49+
}
1950
/**
2051
* Main class for connectiong to Intento API
2152
* Typical usage:
@@ -32,6 +63,7 @@ function IntentoConnector(credentials = {}, options = {}) {
3263
curl = false,
3364
dryRun = false,
3465
userAgent,
66+
fetcher = defaultFetcher
3567
} = options
3668
if (typeof credentials === 'string') {
3769
this.credentials = { apikey: credentials }
@@ -45,7 +77,7 @@ function IntentoConnector(credentials = {}, options = {}) {
4577
this.verbose = verbose
4678
this.dryRun = dryRun
4779
this.userAgent = userAgent
48-
80+
this.fetcher = fetcher
4981
const { apikey, host = HOST } = this.credentials
5082

5183
if (!apikey) {
@@ -250,33 +282,12 @@ IntentoConnector.prototype.makeRequest = function (options = {}) {
250282
console.log(`\nTest request\n${requestString}`)
251283
}
252284

253-
return new Promise((resolve, reject) => {
254-
if (this.dryRun) {
255-
resolve(data || content || requestOptions.path || '')
256-
}
285+
if (this.dryRun) {
286+
return Promise.resolve(data || content || requestOptions.path || '')
287+
}
257288

258-
try {
259-
const req = https.request(requestOptions, resp =>
260-
responseHandler(resp, resolve, reject, this.debug, this.verbose)
261-
)
289+
return this.fetcher({ requestOptions, debug: this.debug, verbose: this.verbose, data, content })
262290

263-
req.on('error', function (err) {
264-
if (err.code === 'ENOTFOUND') {
265-
console.error('Host look up failed: \n', err)
266-
console.log('\nPlease, check internet connection\n')
267-
} else {
268-
customErrorLog(err, 'Fails getting a response from the API')
269-
}
270-
})
271-
req.on('timeout', function (err) {
272-
customErrorLog(err, 'Are you offline?')
273-
})
274-
req.write(data || JSON.stringify(content) || '')
275-
req.end()
276-
} catch (e) {
277-
customErrorLog(e, 'Fails to send a request to the API')
278-
}
279-
})
280291
}
281292

282293
IntentoConnector.prototype.fulfill = function (slug, parameters = {}) {

testAPI/zd.test.js

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
'use strict'
2+
const fetch = require('node-fetch')
3+
const IntentoConnector = require('../src/index')
4+
5+
// Quickly load .env files into the environment
6+
require('dotenv').load()
7+
const apikey = process.env.INTENTO_API_KEY
8+
const host = process.env.INTENTO_API_HOST
9+
10+
const DEBUG = false
11+
12+
/**
13+
* ZD Client mockup
14+
*
15+
* https://developer.zendesk.com/apps/docs/developer-guide/using_sdk#using-secure-settings
16+
* @param {*} params all the params
17+
* @returns {undefined}
18+
*/
19+
class ZDClient {
20+
21+
request(params) {
22+
23+
const {
24+
url,
25+
headers,
26+
secure,
27+
type,
28+
contentType,
29+
data,
30+
} = params
31+
32+
33+
if (secure) {
34+
for (const key in headers) {
35+
headers[key] = headers[key].replace('{{setting.token}}', apikey)
36+
}
37+
}
38+
39+
return fetch(url, {
40+
method: type,
41+
headers: { ...headers, 'content-type': contentType },
42+
body: type !== 'GET' ? data : undefined
43+
}).then(response => {
44+
// here mimicing zd request API as we understand it
45+
//
46+
47+
// zd request API: https://developer.zendesk.com/apps/docs/core-api/client_api#client.requestoptions
48+
//
49+
// console.log(response.responseJSON); // body of the HTTP response
50+
// console.log(response.responseText); // body of the HTTP response
51+
// console.log(response.status); // HTTP response status
52+
// console.log(response.statusText); // Is either 'success' or 'error'
53+
// console.log(response.headers); // HTTP response headers
54+
55+
const { status, headers } = response
56+
57+
return response.text().then(bodytext => {
58+
// it's not exactly clear whether JSON parsing error
59+
// forces zd request to return 'error', but let's consider this as true for now
60+
const zdResponse = { responseText: bodytext, status, statusText: 'error', headers }
61+
try {
62+
zdResponse.responseJSON = JSON.parse(bodytext)
63+
zdResponse.statusText = 'success'
64+
} catch (exception) {
65+
zdResponse.error = exception
66+
}
67+
68+
return zdResponse
69+
})
70+
})
71+
}
72+
}
73+
74+
const zdclient = new ZDClient()
75+
const HTTP_CODES = {
76+
404: 'Not Found'
77+
}
78+
/**
79+
* Fetcher function which can work with zendesk client
80+
* @param {*} param0 incoming parameters
81+
* @returns {undefined}
82+
*/
83+
function zdfetcher({ requestOptions, /*debug, verbose,*/ data, content }) {
84+
// console.log('zd fetcher ', requestOptions, data, content)
85+
let { headers, host, path, method } = requestOptions
86+
87+
delete headers["content-type"]
88+
headers.apikey = "{{setting.token}}"
89+
return zdclient.request({
90+
url: `https://${host}${path}`, // for ex. api.inten.to/ai/text/translate
91+
headers,
92+
secure: true,
93+
type: method, // POST, GET, etc
94+
contentType: 'application/json',
95+
data: data || JSON.stringify(content) || ''
96+
}).then(zdresponse => {
97+
// console.log(' got zdresponse ', zdresponse)
98+
const { status, statusText } = zdresponse
99+
100+
// default fetcher treats 404 as errors and throws, so should we
101+
102+
// here other non 200 statues should be checked
103+
if (statusText === 'success' && status !== 404) {
104+
return zdresponse.responseJSON
105+
}
106+
107+
// might be that zd request returns actual statusMessage in some undocumented field
108+
let error = { statusCode: status, statusMessage: HTTP_CODES[status] }
109+
try {
110+
error.error = zdresponse.responseJSON.error
111+
}
112+
catch (exception) {
113+
error.error = exception
114+
}
115+
throw error
116+
})
117+
118+
}
119+
120+
121+
122+
const client_for_zd = new IntentoConnector({ apikey, host }, { debug: DEBUG, fetcher: zdfetcher })
123+
124+
describe('zd fetcher test', () => {
125+
it('get translation', async () => {
126+
expect.assertions(10)
127+
const translate = await client_for_zd.ai.text.translate.fulfill({
128+
text: 'A sample text',
129+
to: 'es',
130+
})
131+
if (DEBUG) {
132+
console.info('Current apikey settings: ', translate)
133+
}
134+
135+
expect(translate).toBeInstanceOf(Object)
136+
expect(translate.hasOwnProperty('id')).toBeTruthy()
137+
expect(translate.hasOwnProperty('done')).toBeTruthy()
138+
expect(translate.hasOwnProperty('response')).toBeTruthy()
139+
expect(translate.hasOwnProperty('meta')).toBeTruthy()
140+
expect(translate.hasOwnProperty('error')).toBeTruthy()
141+
142+
const res = translate.response[0]
143+
expect(res).toBeDefined()
144+
expect(res.hasOwnProperty('results')).toBeTruthy()
145+
expect(res.hasOwnProperty('meta')).toBeTruthy()
146+
expect(res.hasOwnProperty('service')).toBeTruthy()
147+
})
148+
149+
150+
it('fails without options specified', async () => {
151+
expect.assertions(2)
152+
await client_for_zd.makeRequest().catch(e => {
153+
expect(e.statusCode).toEqual(404)
154+
expect(e.statusMessage).toEqual('Not Found')
155+
})
156+
})
157+
158+
it('fails with an incorrect path specified: /', async () => {
159+
expect.assertions(2)
160+
await client_for_zd.makeRequest({ path: '/' }).catch(e => {
161+
expect(e.statusCode).toEqual(404)
162+
expect(e.statusMessage).toEqual('Not Found')
163+
})
164+
})
165+
166+
it('fails with an incorrect path specified: /settings', async () => {
167+
expect.assertions(3)
168+
await client_for_zd.makeRequest({ path: '/settings' }).catch(e => {
169+
expect(e.error).toBeDefined()
170+
expect(e.error.code).toEqual(404)
171+
expect(e.error.message).toEqual('no such intent settings/')
172+
})
173+
})
174+
175+
it('fails with an incorrect path specified: /ai', async () => {
176+
expect.assertions(3)
177+
await client_for_zd.makeRequest({ path: '/ai' }).catch(e => {
178+
expect(e.error).toBeDefined()
179+
expect(e.error.code).toEqual(404)
180+
expect(e.error.message).toEqual('no such intent ai/')
181+
})
182+
})
183+
184+
it('fails with an incorrect path specified: /usage', async () => {
185+
expect.assertions(2)
186+
await client_for_zd
187+
.makeRequest({ path: '/usage' })
188+
.catch(e => {
189+
expect(e.error).toBeDefined()
190+
expect(e.error).toEqual('No such endpoint.')
191+
// expect(e.error.code).toEqual(404)
192+
// expect(e.error.message).toEqual('No such endpoint.')
193+
})
194+
})
195+
196+
it('shows settings/languages', async () => {
197+
expect.assertions(1)
198+
const langSettings = await client_for_zd.makeRequest({
199+
path: '/settings/languages',
200+
})
201+
if (DEBUG) {
202+
console.info('Current apikey settings: ', langSettings)
203+
}
204+
205+
expect(langSettings).toBeInstanceOf(Object)
206+
})
207+
})

yarn.lock

+39
Original file line numberDiff line numberDiff line change
@@ -2477,6 +2477,11 @@ crypto-browserify@^3.11.0:
24772477
randombytes "^2.0.0"
24782478
randomfill "^1.0.3"
24792479

2480+
data-uri-to-buffer@^4.0.0:
2481+
version "4.0.1"
2482+
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
2483+
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
2484+
24802485
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
24812486
version "4.3.4"
24822487
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@@ -2830,6 +2835,14 @@ fb-watchman@^2.0.0:
28302835
dependencies:
28312836
bser "2.1.1"
28322837

2838+
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
2839+
version "3.2.0"
2840+
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
2841+
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
2842+
dependencies:
2843+
node-domexception "^1.0.0"
2844+
web-streams-polyfill "^3.0.3"
2845+
28332846
file-entry-cache@^6.0.1:
28342847
version "6.0.1"
28352848
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -2888,6 +2901,13 @@ foreground-child@^3.1.0:
28882901
cross-spawn "^7.0.0"
28892902
signal-exit "^4.0.1"
28902903

2904+
formdata-polyfill@^4.0.10:
2905+
version "4.0.10"
2906+
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
2907+
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
2908+
dependencies:
2909+
fetch-blob "^3.1.2"
2910+
28912911
fs-readdir-recursive@^1.1.0:
28922912
version "1.1.0"
28932913
resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
@@ -3929,6 +3949,20 @@ neo-async@^2.6.2:
39293949
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
39303950
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
39313951

3952+
node-domexception@^1.0.0:
3953+
version "1.0.0"
3954+
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
3955+
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
3956+
3957+
node-fetch@^3.3.1:
3958+
version "3.3.1"
3959+
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.1.tgz#b3eea7b54b3a48020e46f4f88b9c5a7430d20b2e"
3960+
integrity sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==
3961+
dependencies:
3962+
data-uri-to-buffer "^4.0.0"
3963+
fetch-blob "^3.1.4"
3964+
formdata-polyfill "^4.0.10"
3965+
39323966
node-int64@^0.4.0:
39333967
version "0.4.0"
39343968
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -4823,6 +4857,11 @@ watchpack@^2.4.0:
48234857
glob-to-regexp "^0.4.1"
48244858
graceful-fs "^4.1.2"
48254859

4860+
web-streams-polyfill@^3.0.3:
4861+
version "3.2.1"
4862+
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
4863+
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
4864+
48264865
webpack-cli@^5.1.4:
48274866
version "5.1.4"
48284867
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b"

0 commit comments

Comments
 (0)