@parseapi/sdk 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var VERSION = "0.2.0";
2
+ var VERSION = "0.3.0";
3
3
  var DEFAULT_BASE_URL = "https://api.parseapi.com";
4
4
  var DEFAULT_TIMEOUT_MS = 1e4;
5
5
  var DEFAULT_RETRIES = 2;
@@ -26,8 +26,36 @@ var ParseAPIError = class extends Error {
26
26
  function env(name) {
27
27
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
28
28
  }
29
- function sleep(ms) {
30
- return new Promise((resolve) => setTimeout(resolve, ms));
29
+ function sleep(ms, signal) {
30
+ return new Promise((resolve, reject) => {
31
+ if (signal?.aborted) {
32
+ reject(signal.reason);
33
+ return;
34
+ }
35
+ const onAbort = () => {
36
+ clearTimeout(timer);
37
+ reject(signal.reason);
38
+ };
39
+ const timer = setTimeout(() => {
40
+ signal?.removeEventListener("abort", onAbort);
41
+ resolve();
42
+ }, ms);
43
+ signal?.addEventListener("abort", onAbort, { once: true });
44
+ });
45
+ }
46
+ function validateTimeout(value) {
47
+ if (!Number.isInteger(value) || value <= 0 || value > 2147483647) {
48
+ throw new RangeError("parseAPI: timeoutMs must be an integer from 1 to 2147483647.");
49
+ }
50
+ }
51
+ function validateRetries(value) {
52
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) {
53
+ throw new RangeError("parseAPI: retries must be a non-negative integer.");
54
+ }
55
+ }
56
+ function metered(path, query) {
57
+ const product = path.split("/")[1];
58
+ return ["carrier", "caller", "hlr", "litigator", "reassigned"].includes(product ?? "") || ["email", "vat", "address"].includes(product ?? "") && query?.deep === true;
31
59
  }
32
60
  function retryDelayMs(attempt, retryAfter) {
33
61
  if (retryAfter) {
@@ -35,189 +63,221 @@ function retryDelayMs(attempt, retryAfter) {
35
63
  if (Number.isFinite(seconds) && seconds >= 0) {
36
64
  return Math.min(seconds * 1e3, RETRY_AFTER_CAP_MS);
37
65
  }
66
+ if (Number.isNaN(seconds)) {
67
+ const date = Date.parse(retryAfter);
68
+ if (Number.isFinite(date)) return Math.min(Math.max(date - Date.now(), 0), RETRY_AFTER_CAP_MS);
69
+ }
38
70
  }
39
71
  return Math.random() * 250 * 2 ** attempt;
40
72
  }
41
73
  function parseAPI(apiKey, options = {}) {
42
- const key = apiKey ?? env("PARSEAPI_KEY");
74
+ const key = apiKey || env("PARSEAPI_KEY");
43
75
  if (!key) {
44
76
  throw new Error("parseAPI: missing API key. Pass one or set PARSEAPI_KEY.");
45
77
  }
46
78
  const baseUrl = (options.baseUrl ?? env("PARSEAPI_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
47
79
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
48
- const retries = options.retries ?? DEFAULT_RETRIES;
80
+ const configuredRetries = options.retries;
81
+ validateTimeout(timeoutMs);
82
+ validateRetries(configuredRetries);
49
83
  const doFetch = options.fetch ?? fetch;
50
- async function request(path, query, headers) {
84
+ async function request(path, query, headers, controls = {}) {
85
+ const signal = controls.signal;
86
+ signal?.throwIfAborted();
87
+ const attemptTimeout = controls.timeoutMs ?? timeoutMs;
88
+ const retries = controls.retries ?? configuredRetries ?? (metered(path, query) ? 0 : DEFAULT_RETRIES);
89
+ validateTimeout(attemptTimeout);
90
+ validateRetries(retries);
51
91
  const url = new URL(baseUrl + path);
52
92
  for (const [name, value] of Object.entries(query ?? {})) {
53
93
  if (value !== void 0) url.searchParams.set(name, String(value));
54
94
  }
55
95
  for (let attempt = 0; ; attempt++) {
56
- let res;
96
+ signal?.throwIfAborted();
97
+ const controller = new AbortController();
98
+ const onAbort = () => controller.abort(signal.reason);
99
+ signal?.addEventListener("abort", onAbort, { once: true });
100
+ const timer = setTimeout(() => controller.abort(new DOMException("Request timed out", "TimeoutError")), attemptTimeout);
101
+ let retryAfter = null;
57
102
  try {
58
- res = await doFetch(url, {
59
- headers: {
60
- "X-API-Key": key,
61
- "User-Agent": `parseapi-node/${VERSION}`,
62
- ...headers
63
- },
64
- signal: AbortSignal.timeout(timeoutMs)
65
- });
66
- } catch (err) {
67
- if (attempt < retries) {
68
- await sleep(retryDelayMs(attempt, null));
69
- continue;
103
+ let res;
104
+ try {
105
+ res = await doFetch(url, {
106
+ redirect: "manual",
107
+ headers: { "X-API-Key": key, "User-Agent": `parseapi-node/${VERSION}`, ...headers },
108
+ signal: controller.signal
109
+ });
110
+ } catch (error) {
111
+ signal?.throwIfAborted();
112
+ if (attempt >= retries) throw error;
70
113
  }
71
- throw err;
72
- }
73
- if (res.ok) {
74
- return await res.json();
75
- }
76
- if (RETRY_STATUS.has(res.status) && attempt < retries) {
77
- await sleep(retryDelayMs(attempt, res.headers.get("Retry-After")));
78
- continue;
79
- }
80
- let body = {};
81
- try {
82
- body = await res.json();
83
- } catch {
114
+ signal?.throwIfAborted();
115
+ if (res?.ok) {
116
+ try {
117
+ const result = await res.json();
118
+ signal?.throwIfAborted();
119
+ return result;
120
+ } catch (error) {
121
+ signal?.throwIfAborted();
122
+ if (error instanceof SyntaxError || attempt >= retries) throw error;
123
+ res = void 0;
124
+ }
125
+ }
126
+ if (res && RETRY_STATUS.has(res.status) && attempt < retries) {
127
+ retryAfter = res.headers.get("Retry-After");
128
+ await res.body?.cancel().catch(() => {
129
+ });
130
+ } else if (res) {
131
+ let body = {};
132
+ try {
133
+ const parsed = await res.json();
134
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) body = parsed;
135
+ } catch {
136
+ controller.signal.throwIfAborted();
137
+ }
138
+ signal?.throwIfAborted();
139
+ throw new ParseAPIError(
140
+ res.status,
141
+ typeof body.code === "string" ? body.code : "unknown_error",
142
+ typeof body.message === "string" ? body.message : `Request failed with status ${res.status}`,
143
+ typeof body.docs === "string" ? body.docs : null,
144
+ typeof body.request_id === "string" ? body.request_id : null
145
+ );
146
+ }
147
+ } finally {
148
+ clearTimeout(timer);
149
+ signal?.removeEventListener("abort", onAbort);
84
150
  }
85
- throw new ParseAPIError(
86
- res.status,
87
- typeof body.code === "string" ? body.code : "unknown_error",
88
- typeof body.message === "string" ? body.message : `Request failed with status ${res.status}`,
89
- typeof body.docs === "string" ? body.docs : null,
90
- typeof body.request_id === "string" ? body.request_id : null
91
- );
151
+ await sleep(retryDelayMs(attempt, retryAfter), signal);
92
152
  }
93
153
  }
94
154
  const enc = encodeURIComponent;
95
155
  const deepQuery = (opts) => opts?.deep ? { deep: true } : {};
96
- function timezone(idOrLat, lonOrOpts, opts) {
97
- if (typeof idOrLat === "number") {
98
- return request("/timezone", {
99
- lat: idOrLat,
100
- lon: typeof lonOrOpts === "number" ? lonOrOpts : void 0,
101
- at: opts?.at
102
- });
103
- }
104
- const idOpts = lonOrOpts && typeof lonOrOpts === "object" ? lonOrOpts : void 0;
105
- return request(`/timezone/${enc(idOrLat)}`, {
106
- at: idOpts?.at,
107
- to: idOpts?.to
108
- });
109
- }
110
156
  return {
111
157
  ip: Object.assign(
112
- (ip, opts) => request(`/ip/${enc(ip)}`, deepQuery(opts)),
158
+ (ip, opts) => request(`/ip/${enc(ip)}`, deepQuery(opts), void 0, opts),
113
159
  {
114
- self: (opts) => request("/ip", deepQuery(opts))
160
+ self: (opts) => request("/ip", deepQuery(opts), void 0, opts)
115
161
  }
116
162
  ),
117
163
  continent: Object.assign(
118
- (code) => request(`/continent/${enc(code)}`),
164
+ (code, opts) => request(`/continent/${enc(code)}`, void 0, void 0, opts),
119
165
  {
120
- countries: (code) => request(`/continent/${enc(code)}/countries`)
166
+ countries: (code, opts) => request(`/continent/${enc(code)}/countries`, void 0, void 0, opts)
121
167
  }
122
168
  ),
123
169
  bloc: Object.assign(
124
- (code) => request(`/bloc/${enc(code)}`),
170
+ (code, opts) => request(`/bloc/${enc(code)}`, void 0, void 0, opts),
125
171
  {
126
- countries: (code) => request(`/bloc/${enc(code)}/countries`)
172
+ countries: (code, opts) => request(`/bloc/${enc(code)}/countries`, void 0, void 0, opts)
127
173
  }
128
174
  ),
129
175
  country: Object.assign(
130
- (code) => request(`/country/${enc(code)}`),
176
+ (code, opts) => request(`/country/${enc(code)}`, void 0, void 0, opts),
131
177
  {
132
- states: (code) => request(`/country/${enc(code)}/states`)
178
+ states: (code, opts) => request(`/country/${enc(code)}/states`, void 0, void 0, opts)
133
179
  }
134
180
  ),
135
181
  state: Object.assign(
136
- (code, opts) => request(`/state/${enc(code)}`, { country: opts?.country }),
182
+ (code, opts) => request(`/state/${enc(code)}`, { country: opts?.country }, void 0, opts),
137
183
  {
138
- districts: (code, opts) => request(`/state/${enc(code)}/districts`, { country: opts?.country })
184
+ districts: (code, opts) => request(`/state/${enc(code)}/districts`, { country: opts?.country }, void 0, opts)
139
185
  }
140
186
  ),
141
- district: (code, opts) => request(`/district/${enc(code)}`, { country: opts?.country, state: opts?.state }),
187
+ district: (code, opts) => request(`/district/${enc(code)}`, { country: opts?.country, state: opts?.state }, void 0, opts),
142
188
  city: Object.assign(
143
- (name, opts) => request(`/city/${enc(name)}`, { country: opts?.country, state: opts?.state }),
189
+ (name, opts) => request(`/city/${enc(name)}`, { country: opts?.country, state: opts?.state }, void 0, opts),
144
190
  {
145
- id: (id) => request(`/city/id/${enc(id)}`),
146
- search: (q, opts) => request("/city", { q, country: opts?.country, state: opts?.state, limit: opts?.limit }),
147
- nearest: (lat, lon) => request("/city", { lat, lon }),
191
+ id: (id, opts) => request(`/city/id/${enc(id)}`, void 0, void 0, opts),
192
+ search: (query, opts) => request("/city", { q: query, country: opts?.country, state: opts?.state, limit: opts?.limit }, void 0, opts),
193
+ nearest: (lat, lon, opts) => request("/city", { lat, lon }, void 0, opts),
148
194
  nearby: (name, opts) => request(`/city/${enc(name)}/nearby`, {
149
195
  radius: opts?.radius,
150
196
  unit: opts?.unit,
151
197
  country: opts?.country,
152
198
  state: opts?.state,
153
199
  limit: opts?.limit
154
- })
200
+ }, void 0, opts)
155
201
  }
156
202
  ),
157
203
  postal: Object.assign(
158
- (code, opts) => request(`/postal/${enc(code)}`, { country: opts?.country }),
204
+ (code, opts) => request(`/postal/${enc(code)}`, { country: opts?.country }, void 0, opts),
159
205
  {
160
206
  nearby: (code, opts) => request(`/postal/${enc(code)}/nearby`, {
161
207
  country: opts?.country,
162
208
  radius: opts?.radius,
163
209
  unit: opts?.unit
164
- }),
165
- distance: (from, to, opts) => request(`/postal/${enc(from)}/distance/${enc(to)}`, { country: opts?.country })
210
+ }, void 0, opts),
211
+ distance: (from, to, opts) => request(`/postal/${enc(from)}/distance/${enc(to)}`, { country: opts?.country }, void 0, opts)
212
+ }
213
+ ),
214
+ address: Object.assign(
215
+ (address, opts) => request(`/address/${enc(address)}`, { country: opts?.country, ...deepQuery(opts) }, void 0, opts),
216
+ {
217
+ search: (query, opts) => request("/address", { q: query, country: opts?.country, postal: opts?.postal, city: opts?.city, state: opts?.state, ip: opts?.ip }, void 0, opts)
166
218
  }
167
219
  ),
168
- email: (email, opts) => request(`/email/${enc(email)}`, deepQuery(opts)),
220
+ company: (number, opts) => request(`/company/${enc(number)}`, { country: opts?.country, ...deepQuery(opts) }, void 0, opts),
221
+ email: (email, opts) => request(`/email/${enc(email)}`, deepQuery(opts), void 0, opts),
169
222
  vat: (number, opts) => request(`/vat/${enc(number)}`, {
170
223
  country: opts?.country,
171
224
  from: opts?.from,
172
225
  ...deepQuery(opts)
173
- }),
174
- iban: (iban, opts) => request(`/iban/${enc(iban)}`, { country: opts?.country }),
175
- npi: (npi, opts) => request(`/npi/${enc(npi)}`, deepQuery(opts)),
176
- phone: (number, opts) => request(`/phone/${enc(number)}`, { country: opts?.country, ...deepQuery(opts) }),
177
- carrier: (number, opts) => request(`/carrier/${enc(number)}`, { country: opts?.country }),
178
- caller: (number, opts) => request(`/caller/${enc(number)}`, { country: opts?.country }),
179
- hlr: (number, opts) => request(`/hlr/${enc(number)}`, { country: opts?.country }),
180
- domain: (domain, opts) => request(`/domain/${enc(domain)}`, deepQuery(opts)),
181
- mx: (domain) => request(`/mx/${enc(domain)}`),
182
- useragent: (ua, opts) => request("/useragent", deepQuery(opts), { "User-Agent": ua }),
183
- vin: (vin, opts) => request(`/vin/${enc(vin)}`, deepQuery(opts)),
184
- hts: Object.assign(
185
- (code, opts) => request(`/hts/${enc(code)}`, { origin: opts?.origin, ...deepQuery(opts) }),
226
+ }, void 0, opts),
227
+ iban: (iban, opts) => request(`/iban/${enc(iban)}`, { country: opts?.country }, void 0, opts),
228
+ npi: (npi, opts) => request(`/npi/${enc(npi)}`, deepQuery(opts), void 0, opts),
229
+ phone: (number, opts) => request(`/phone/${enc(number)}`, { country: opts?.country, ...deepQuery(opts) }, void 0, opts),
230
+ carrier: (number, opts) => request(`/carrier/${enc(number)}`, { country: opts?.country }, void 0, opts),
231
+ caller: (number, opts) => request(`/caller/${enc(number)}`, { country: opts?.country }, void 0, opts),
232
+ hlr: (number, opts) => request(`/hlr/${enc(number)}`, { country: opts?.country }, void 0, opts),
233
+ domain: (domain, opts) => request(`/domain/${enc(domain)}`, deepQuery(opts), void 0, opts),
234
+ asn: (asn, opts) => request(`/asn/${enc(asn)}`, void 0, void 0, opts),
235
+ mac: (mac, opts) => request(`/mac/${enc(mac)}`, void 0, void 0, opts),
236
+ mx: (domain, opts) => request(`/mx/${enc(domain)}`, void 0, void 0, opts),
237
+ useragent: (ua, opts) => request("/useragent", deepQuery(opts), { "User-Agent": ua }, opts),
238
+ vin: (vin, opts) => request(`/vin/${enc(vin)}`, deepQuery(opts), void 0, opts),
239
+ tariff: Object.assign(
240
+ (code, opts) => request(`/tariff/${enc(code)}`, { origin: opts?.origin, ...deepQuery(opts) }, void 0, opts),
186
241
  {
187
- search: (q) => request("/hts", { q })
242
+ search: (query, opts) => request("/tariff", { q: query }, void 0, opts)
188
243
  }
189
244
  ),
190
245
  currency: Object.assign(
191
- (code) => request(`/currency/${enc(code)}`),
246
+ (code, opts) => request(`/currency/${enc(code)}`, void 0, void 0, opts),
192
247
  {
193
248
  rate: (base, quote, opts) => request(`/currency/${enc(base)}/${enc(quote)}`, {
194
249
  date: opts?.date,
195
250
  amount: opts?.amount
196
- })
251
+ }, void 0, opts)
252
+ }
253
+ ),
254
+ language: (code, opts) => request(`/language/${enc(code)}`, void 0, void 0, opts),
255
+ name: (name, opts) => request(`/name/${enc(name)}`, void 0, void 0, opts),
256
+ timezone: Object.assign(
257
+ (id, opts) => request(`/timezone/${enc(id)}`, { at: opts?.at, to: opts?.to }, void 0, opts),
258
+ {
259
+ at: (lat, lon, opts) => request("/timezone", { lat, lon, at: opts?.at }, void 0, opts)
197
260
  }
198
261
  ),
199
- language: (code) => request(`/language/${enc(code)}`),
200
- name: (name) => request(`/name/${enc(name)}`),
201
- timezone,
202
262
  date: Object.assign(
203
- (date, opts) => request(`/date/${enc(date)}`, { format: opts?.format, to: opts?.to }),
263
+ (date, opts) => request(`/date/${enc(date)}`, { format: opts?.format, to: opts?.to }, void 0, opts),
204
264
  {
205
- today: (opts) => request("/date", { to: opts?.to })
265
+ today: (opts) => request("/date", { to: opts?.to }, void 0, opts)
206
266
  }
207
267
  ),
208
268
  holiday: Object.assign(
209
- (country, opts) => request(`/holiday/${enc(country)}`, { year: opts?.year }),
269
+ (country, opts) => request(`/holiday/${enc(country)}`, { year: opts?.year }, void 0, opts),
210
270
  {
211
- date: (country, date) => request(`/holiday/${enc(country)}/${enc(date)}`)
271
+ date: (country, date, opts) => request(`/holiday/${enc(country)}/${enc(date)}`, void 0, void 0, opts)
212
272
  }
213
273
  ),
214
- elevation: (lat, lon) => request("/elevation", { lat, lon }),
215
- point: (lat, lon, opts) => request("/point", { lat, lon, ...deepQuery(opts) }),
216
- weather: (lat, lon, opts) => request("/weather", { lat, lon, date: opts?.date, ...deepQuery(opts) }),
274
+ elevation: (lat, lon, opts) => request("/elevation", { lat, lon }, void 0, opts),
275
+ point: (lat, lon, opts) => request("/point", { lat, lon, ...deepQuery(opts) }, void 0, opts),
276
+ weather: (lat, lon, opts) => request("/weather", { lat, lon, date: opts?.date, ...deepQuery(opts) }, void 0, opts),
217
277
  emoji: Object.assign(
218
- (emoji) => request(`/emoji/${enc(emoji)}`),
278
+ (emoji, opts) => request(`/emoji/${enc(emoji)}`, void 0, void 0, opts),
219
279
  {
220
- search: (q, opts) => request("/emoji", { q, limit: opts?.limit })
280
+ search: (query, opts) => request("/emoji", { q: query, limit: opts?.limit }, void 0, opts)
221
281
  }
222
282
  )
223
283
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parseapi/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official parseAPI client for Node and TypeScript. One key, minimal JSON, fast.",
5
5
  "keywords": [
6
6
  "parseapi",
@@ -29,9 +29,14 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "exports": {
31
31
  ".": {
32
- "types": "./dist/index.d.ts",
33
- "import": "./dist/index.js",
34
- "require": "./dist/index.cjs"
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/index.d.cts",
38
+ "default": "./dist/index.cjs"
39
+ }
35
40
  }
36
41
  },
37
42
  "files": [
@@ -43,10 +48,12 @@
43
48
  },
44
49
  "scripts": {
45
50
  "build": "tsup src/index.ts --format esm,cjs --dts --target node18 --clean",
46
- "prepublishOnly": "npm run build && npm test",
51
+ "prepublishOnly": "npm test && npm run typecheck && npm run api:check",
47
52
  "test": "vitest run",
48
53
  "typecheck": "tsc --noEmit",
49
- "smoke": "npm run build && node smoke/smoke.mjs"
54
+ "smoke": "npm run build && node smoke/smoke.mjs",
55
+ "api:check": "npm run build && node scripts/check-api.mjs && node scripts/check-package.mjs",
56
+ "api:update": "npm run build && node scripts/check-api.mjs --write"
50
57
  },
51
58
  "devDependencies": {
52
59
  "@types/node": "^22.10.0",