@microlink/mql 0.15.0 → 0.16.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/package.json CHANGED
@@ -2,11 +2,13 @@
2
2
  "name": "@microlink/mql",
3
3
  "description": "Microlink Query Language. The official HTTP client to interact with Microlink API for Node.js, browsers & Deno.",
4
4
  "homepage": "https://microlink.io/mql",
5
- "version": "0.15.0",
5
+ "version": "0.16.0",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
- "default": "./src/main.mjs",
9
- "types": "./dist/index.d.ts"
8
+ "types": "./dist/index.d.ts",
9
+ "require": "./src/index.js",
10
+ "import": "./src/main.mjs",
11
+ "default": "./src/main.mjs"
10
12
  },
11
13
  "author": {
12
14
  "email": "josefrancisco.verdu@gmail.com",
@@ -47,7 +49,7 @@
47
49
  ],
48
50
  "dependencies": {
49
51
  "flattie": "~1.1.1",
50
- "ky": "~1.14.3"
52
+ "ky": "~2.0.0"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@commitlint/cli": "latest",
@@ -74,31 +76,14 @@
74
76
  "tsd": "latest"
75
77
  },
76
78
  "engines": {
77
- "node": ">= 18"
79
+ "node": ">= 22"
78
80
  },
79
81
  "files": [
80
82
  "dist",
81
83
  "src/constants.js",
82
- "src/factory.js",
83
84
  "src/index.js",
84
85
  "src/main.mjs"
85
86
  ],
86
- "scripts": {
87
- "build": "rollup -c rollup.config.js --bundleConfigAsCjs",
88
- "clean": "rm -rf node_modules",
89
- "clean:build": "rm -rf dist/index.js",
90
- "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
91
- "dev": "npm run build -- -w",
92
- "lint": "standard && tsd",
93
- "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
94
- "prebuild": "npm run clean:build",
95
- "prepublishOnly": "npm run build",
96
- "pretest": "npm run lint && npm run build",
97
- "release": "standard-version -a",
98
- "release:github": "github-generate-release",
99
- "release:tags": "git push --follow-tags origin HEAD:master",
100
- "test": "c8 ava --verbose"
101
- },
102
87
  "license": "MIT",
103
88
  "ava": {
104
89
  "files": [
@@ -149,5 +134,20 @@
149
134
  }
150
135
  },
151
136
  "directory": "test"
137
+ },
138
+ "scripts": {
139
+ "build": "rollup -c rollup.config.js --bundleConfigAsCjs",
140
+ "clean": "rm -rf node_modules",
141
+ "clean:build": "rm -rf dist/index.js",
142
+ "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
143
+ "dev": "npm run build -- -w",
144
+ "lint": "standard && tsd",
145
+ "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
146
+ "prebuild": "npm run clean:build",
147
+ "pretest": "npm run lint && npm run build",
148
+ "release": "standard-version -a",
149
+ "release:github": "github-generate-release",
150
+ "release:tags": "git push --follow-tags origin HEAD:master",
151
+ "test": "c8 ava --verbose"
152
152
  }
153
- }
153
+ }
package/src/constants.js CHANGED
@@ -6,7 +6,13 @@ module.exports = {
6
6
  VERSION,
7
7
  USER_AGENT: `mql/${VERSION}`,
8
8
  /**
9
- * Retry status codes, excluding 429 (too many requests).
9
+ * Based on Ky default retry status codes, excluding 429:
10
+ * https://github.com/sindresorhus/ky/blob/main/source/core/constants.ts
10
11
  */
11
- RETRY_STATUS_CODES: [408, 413, 500, 502, 503, 504, 521, 522, 524]
12
+ RETRY_STATUS_CODES: [408, 413, 500, 502, 503, 504, 521, 522, 524],
13
+ /**
14
+ * Based on Ky default Retry-After status codes, excluding 429:
15
+ * https://github.com/sindresorhus/ky/blob/main/source/core/constants.ts
16
+ */
17
+ RETRY_AFTER_STATUS_CODES: [413, 503]
12
18
  }
package/src/index.js CHANGED
@@ -3,14 +3,59 @@
3
3
  const { flattie: flatten } = require('flattie')
4
4
  const { default: ky } = require('ky')
5
5
 
6
- const { VERSION, USER_AGENT, RETRY_STATUS_CODES } = require('./constants')
6
+ const {
7
+ VERSION,
8
+ USER_AGENT,
9
+ RETRY_STATUS_CODES,
10
+ RETRY_AFTER_STATUS_CODES
11
+ } = require('./constants')
12
+
13
+ const ENDPOINT = {
14
+ FREE: 'https://api.microlink.io/',
15
+ PRO: 'https://pro.microlink.io/'
16
+ }
17
+
18
+ const STREAM_RESPONSE_TYPE = 'arrayBuffer'
7
19
 
8
20
  const kyInstance = ky.extend({
9
21
  headers: { 'user-agent': USER_AGENT },
10
- retry: { statusCodes: RETRY_STATUS_CODES }
22
+ retry: {
23
+ statusCodes: RETRY_STATUS_CODES,
24
+ afterStatusCodes: RETRY_AFTER_STATUS_CODES
25
+ }
11
26
  })
12
27
 
13
- const factory = require('./factory')('arrayBuffer')
28
+ const isObject = input => input !== null && typeof input === 'object'
29
+
30
+ const isBuffer = input =>
31
+ typeof input?.constructor?.isBuffer === 'function' &&
32
+ input.constructor.isBuffer(input)
33
+
34
+ const parseBody = (input, error, url) => {
35
+ try {
36
+ return JSON.parse(input)
37
+ } catch (_) {
38
+ const message = input || error.message
39
+
40
+ return {
41
+ status: 'error',
42
+ data: { url: message },
43
+ more: 'https://microlink.io/efatalclient',
44
+ code: 'EFATALCLIENT',
45
+ message,
46
+ url
47
+ }
48
+ }
49
+ }
50
+
51
+ const isURL = url => {
52
+ try {
53
+ const { protocol } = new URL(url)
54
+ return protocol === 'http:' || protocol === 'https:'
55
+ } catch (_) {
56
+ return false
57
+ }
58
+ }
14
59
 
15
60
  class MicrolinkError extends Error {
16
61
  constructor (props) {
@@ -24,44 +69,132 @@ class MicrolinkError extends Error {
24
69
  }
25
70
  }
26
71
 
27
- const got = async (url, { responseType, ...opts }) => {
72
+ const assertUrl = (url = '') => {
73
+ if (!isURL(url)) {
74
+ const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`
75
+ throw new MicrolinkError({
76
+ status: 'fail',
77
+ data: { url: message },
78
+ more: 'https://microlink.io/einvalurlclient',
79
+ code: 'EINVALURLCLIENT',
80
+ message,
81
+ url
82
+ })
83
+ }
84
+ }
85
+
86
+ const mapRules = rules => {
87
+ if (!isObject(rules)) return
88
+ return Object.fromEntries(
89
+ Object.entries(flatten(rules)).map(([key, value]) => [
90
+ `data.${key}`,
91
+ value.toString()
92
+ ])
93
+ )
94
+ }
95
+
96
+ const doFetch = async (apiUrl, { responseType, ...opts }) => {
97
+ if (opts.timeout === undefined) opts.timeout = false
98
+ const response = await kyInstance(apiUrl, opts)
99
+ const body = await response[responseType]()
100
+ const { headers, status: statusCode } = response
101
+ return { url: response.url, body, headers, statusCode }
102
+ }
103
+
104
+ const fetchFromApi = async (apiUrl, opts = {}) => {
28
105
  try {
29
- if (opts.timeout === undefined) opts.timeout = false
30
- const response = await kyInstance(url, opts)
31
- const body = await response[responseType]()
32
- const { headers, status: statusCode } = response
33
- return { url: response.url, body, headers, statusCode }
106
+ const response = await doFetch(apiUrl, opts)
107
+ return opts.responseType === STREAM_RESPONSE_TYPE
108
+ ? response
109
+ : { ...response.body, response }
34
110
  } catch (error) {
35
- if (error.response) {
36
- const { response } = error
37
- error.response = {
38
- ...response,
39
- headers: Array.from(response.headers.entries()).reduce(
40
- (acc, [key, value]) => {
41
- acc[key] = value
42
- return acc
43
- },
44
- {}
45
- ),
46
- statusCode: response.status,
47
- body: await response.text()
111
+ const { response = {} } = error
112
+ const { statusCode: responseStatusCode, status } = response
113
+ const {
114
+ body: rawBody,
115
+ headers: responseHeaders,
116
+ url: uri = apiUrl
117
+ } = response
118
+
119
+ const statusCode = responseStatusCode ?? status
120
+ const headers =
121
+ typeof responseHeaders?.entries === 'function'
122
+ ? Object.fromEntries(responseHeaders.entries())
123
+ : responseHeaders || {}
124
+
125
+ let bodyInput = error.data ?? rawBody
126
+ const isBodyReadableStream = typeof bodyInput?.getReader === 'function'
127
+
128
+ if (
129
+ (bodyInput === undefined || isBodyReadableStream) &&
130
+ typeof response.text === 'function'
131
+ ) {
132
+ try {
133
+ bodyInput = await response.text()
134
+ } catch (_) {
135
+ bodyInput = undefined
48
136
  }
49
137
  }
50
- throw error
138
+
139
+ const isBodyBuffer = isBuffer(bodyInput)
140
+ const body =
141
+ isObject(bodyInput) && !isBodyBuffer
142
+ ? bodyInput
143
+ : parseBody(isBodyBuffer ? bodyInput.toString() : bodyInput, error, uri)
144
+
145
+ throw new MicrolinkError({
146
+ ...body,
147
+ url: uri,
148
+ statusCode,
149
+ headers
150
+ })
51
151
  }
52
152
  }
53
153
 
54
- got.stream = (...args) => kyInstance(...args).then(res => res.body)
154
+ const getApiUrl = (
155
+ url,
156
+ { data, apiKey, endpoint, ...opts } = {},
157
+ { responseType = 'json', headers: responseHeaders = {}, ...gotOpts } = {}
158
+ ) => {
159
+ const isPro = !!apiKey
160
+ const apiEndpoint = endpoint || ENDPOINT[isPro ? 'PRO' : 'FREE']
55
161
 
56
- const mql = factory({
57
- MicrolinkError,
58
- got,
59
- flatten,
60
- VERSION
61
- })
162
+ const apiUrl = `${apiEndpoint}?${new URLSearchParams({
163
+ url,
164
+ ...mapRules(data),
165
+ ...flatten(opts)
166
+ })}`
167
+
168
+ const headers = isPro
169
+ ? { ...responseHeaders, 'x-api-key': apiKey }
170
+ : responseHeaders
171
+
172
+ if (opts.stream) responseType = STREAM_RESPONSE_TYPE
173
+
174
+ return [apiUrl, { ...gotOpts, responseType, headers }]
175
+ }
176
+
177
+ const createMql = defaultOpts => async (url, opts, gotOpts) => {
178
+ assertUrl(url)
179
+ const [apiUrl, fetchOpts] = getApiUrl(url, opts, {
180
+ ...defaultOpts,
181
+ ...gotOpts
182
+ })
183
+ return fetchFromApi(apiUrl, fetchOpts)
184
+ }
185
+
186
+ const mql = createMql()
187
+
188
+ mql.extend = createMql
189
+ mql.MicrolinkError = MicrolinkError
190
+ mql.getApiUrl = getApiUrl
191
+ mql.fetchFromApi = fetchFromApi
192
+ mql.mapRules = mapRules
193
+ mql.version = VERSION
194
+ mql.stream = (...args) => kyInstance(...args).then(res => res.body)
62
195
 
63
196
  module.exports = mql
64
- module.exports.arrayBuffer = mql.extend({ responseType: 'arrayBuffer' })
197
+ module.exports.arrayBuffer = mql.extend({ responseType: STREAM_RESPONSE_TYPE })
65
198
  module.exports.buffer = module.exports.arrayBuffer
66
199
  module.exports.extend = mql.extend
67
200
  module.exports.fetchFromApi = mql.fetchFromApi
package/src/factory.js DELETED
@@ -1,144 +0,0 @@
1
- const ENDPOINT = {
2
- FREE: 'https://api.microlink.io/',
3
- PRO: 'https://pro.microlink.io/'
4
- }
5
-
6
- const isObject = input => input !== null && typeof input === 'object'
7
-
8
- const isBuffer = input =>
9
- input != null &&
10
- input.constructor != null &&
11
- typeof input.constructor.isBuffer === 'function' &&
12
- input.constructor.isBuffer(input)
13
-
14
- const parseBody = (input, error, url) => {
15
- try {
16
- return JSON.parse(input)
17
- } catch (_) {
18
- const message = input || error.message
19
-
20
- return {
21
- status: 'error',
22
- data: { url: message },
23
- more: 'https://microlink.io/efatalclient',
24
- code: 'EFATALCLIENT',
25
- message,
26
- url
27
- }
28
- }
29
- }
30
-
31
- const isURL = url => {
32
- try {
33
- return /^https?:\/\//i.test(new URL(url).href)
34
- } catch (_) {
35
- return false
36
- }
37
- }
38
-
39
- const factory = streamResponseType => ({
40
- VERSION,
41
- MicrolinkError,
42
- got,
43
- flatten
44
- }) => {
45
- const assertUrl = (url = '') => {
46
- if (!isURL(url)) {
47
- const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`
48
- throw new MicrolinkError({
49
- status: 'fail',
50
- data: { url: message },
51
- more: 'https://microlink.io/einvalurlclient',
52
- code: 'EINVALURLCLIENT',
53
- message,
54
- url
55
- })
56
- }
57
- }
58
-
59
- const mapRules = rules => {
60
- if (!isObject(rules)) return
61
- const flatRules = flatten(rules)
62
- return Object.keys(flatRules).reduce((acc, key) => {
63
- acc[`data.${key}`] = flatRules[key].toString()
64
- return acc
65
- }, {})
66
- }
67
-
68
- const fetchFromApi = async (apiUrl, opts = {}) => {
69
- try {
70
- const response = await got(apiUrl, opts)
71
- return opts.responseType === streamResponseType
72
- ? response
73
- : { ...response.body, response }
74
- } catch (error) {
75
- const { response = {} } = error
76
- const {
77
- statusCode,
78
- body: rawBody,
79
- headers = {},
80
- url: uri = apiUrl
81
- } = response
82
- const isBodyBuffer = isBuffer(rawBody)
83
-
84
- const body =
85
- isObject(rawBody) && !isBodyBuffer
86
- ? rawBody
87
- : parseBody(isBodyBuffer ? rawBody.toString() : rawBody, error, uri)
88
-
89
- throw new MicrolinkError({
90
- ...body,
91
- message: body.message,
92
- url: uri,
93
- statusCode,
94
- headers
95
- })
96
- }
97
- }
98
-
99
- const getApiUrl = (
100
- url,
101
- { data, apiKey, endpoint, ...opts } = {},
102
- { responseType = 'json', headers: gotHeaders, ...gotOpts } = {}
103
- ) => {
104
- const isPro = !!apiKey
105
- const apiEndpoint = endpoint || ENDPOINT[isPro ? 'PRO' : 'FREE']
106
-
107
- const apiUrl = `${apiEndpoint}?${new URLSearchParams({
108
- url,
109
- ...mapRules(data),
110
- ...flatten(opts)
111
- }).toString()}`
112
-
113
- const headers = isPro
114
- ? { ...gotHeaders, 'x-api-key': apiKey }
115
- : { ...gotHeaders }
116
-
117
- if (opts.stream) {
118
- responseType = streamResponseType
119
- }
120
- return [apiUrl, { ...gotOpts, responseType, headers }]
121
- }
122
-
123
- const createMql = defaultOpts => async (url, opts, gotOpts) => {
124
- assertUrl(url)
125
- const [apiUrl, fetchOpts] = getApiUrl(url, opts, {
126
- ...defaultOpts,
127
- ...gotOpts
128
- })
129
- return fetchFromApi(apiUrl, fetchOpts)
130
- }
131
-
132
- const mql = createMql()
133
- mql.extend = createMql
134
- mql.MicrolinkError = MicrolinkError
135
- mql.getApiUrl = getApiUrl
136
- mql.fetchFromApi = fetchFromApi
137
- mql.mapRules = mapRules
138
- mql.version = VERSION
139
- mql.stream = got.stream
140
-
141
- return mql
142
- }
143
-
144
- module.exports = factory