@parseapi/sdk 0.2.1 → 0.3.1

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 CHANGED
@@ -34,6 +34,9 @@ await parse.postal('SW1A 1AA');
34
34
  await parse.postal('28202', { country: 'US' });
35
35
  await parse.postal.nearby('28202', { country: 'US', radius: 40 });
36
36
  await parse.postal.distance('28202', '10001', { country: 'US' });
37
+ await parse.address('1600 Pennsylvania Ave NW, Washington DC', { country: 'US' });
38
+ await parse.address.search('1600 Pennsylvania', { country: 'US', postal: '20500' });
39
+ await parse.company('51 824 753 556', { country: 'AU' });
37
40
  await parse.city('charlotte', { country: 'US' });
38
41
  await parse.city.id('city_mb8mbqrkz8zb');
39
42
  await parse.city.search('char', { country: 'US', limit: 10 });
@@ -48,26 +51,34 @@ await parse.district('37081');
48
51
  await parse.district('guilford county');
49
52
  await parse.continent('NA');
50
53
  await parse.continent.countries('NA');
54
+ await parse.bloc('EU');
55
+ await parse.bloc.countries('EU');
51
56
  await parse.currency('USD');
52
57
  await parse.currency.rate('USD', 'EUR');
53
58
  await parse.language('en');
54
59
  await parse.name('BILLY OSHALL');
55
60
  await parse.timezone('America/New_York');
56
- await parse.timezone(40.7128, -74.006);
61
+ await parse.timezone.at(40.7128, -74.006);
62
+ await parse.date('03/04/2026', { format: 'mdy' });
63
+ await parse.date.today();
57
64
  await parse.holiday('US', { year: 2026 });
58
65
  await parse.holiday.date('US', '2026-12-25');
59
66
  await parse.elevation(35.2271, -80.8431);
60
67
  await parse.point(36.0726, -79.792);
61
68
  await parse.weather(40.7128, -74.006);
62
69
  await parse.domain('example.com');
70
+ await parse.asn('AS13335');
71
+ await parse.mac('00:1B:63:84:45:E6');
63
72
  await parse.mx('example.com');
64
73
  await parse.useragent(uaString);
65
74
  await parse.vin('1HGCM82633A004352');
75
+ await parse.tariff('8471.30.01.00');
76
+ await parse.tariff.search('sunglasses');
66
77
  await parse.emoji('rocket');
67
78
  await parse.emoji.search('fire');
68
79
  ```
69
80
 
70
- Every response is fully typed.
81
+ Responses are typed, plain JSON data. `country.states('US')` requests the states directly; it does not fetch a country first. Optional arguments go in the final options object, so new options can be added without changing your existing calls.
71
82
 
72
83
  ## Deep
73
84
 
@@ -94,17 +105,29 @@ try {
94
105
  }
95
106
  ```
96
107
 
108
+ Network and decoding failures keep their native error types. Responses such as `valid: false` are successful API answers, not exceptions.
109
+
97
110
  ## Options
98
111
 
99
112
  ```ts
100
113
  const parse = parseAPI('your-api-key', {
101
114
  timeoutMs: 10000, // per-attempt timeout
102
- retries: 2, // automatic retries on network errors, 429, and 5xx
115
+ });
116
+
117
+ const controller = new AbortController();
118
+ const country = await parse.country('US', {
119
+ signal: controller.signal,
120
+ timeoutMs: 5000, // override for this call
121
+ retries: 0, // one attempt
103
122
  });
104
123
  ```
105
124
 
106
125
  Requires Node 18 or later. Zero dependencies.
107
126
 
127
+ Ordinary lookups retry network failures, 429, and 500/502/503/504 responses twice by default. Carrier, caller, HLR, and email or VAT with `deep: true` make one attempt by default. Address with `deep: true` also uses one attempt, reserving the same behavior for future verification.
128
+
129
+ An explicit `retries` setting on the client or call overrides those defaults. Another attempt can consume additional usage if the earlier response was lost. Cancellation stops the request and any retry wait. Automatic redirects are disabled.
130
+
108
131
  ## Docs
109
132
 
110
133
  Full field reference for every endpoint: [parseapi.com/docs](https://parseapi.com/docs)
package/dist/index.cjs CHANGED
@@ -24,7 +24,7 @@ __export(index_exports, {
24
24
  parseAPI: () => parseAPI
25
25
  });
26
26
  module.exports = __toCommonJS(index_exports);
27
- var VERSION = "0.2.1";
27
+ var VERSION = "0.3.1";
28
28
  var DEFAULT_BASE_URL = "https://api.parseapi.com";
29
29
  var DEFAULT_TIMEOUT_MS = 1e4;
30
30
  var DEFAULT_RETRIES = 2;
@@ -51,8 +51,36 @@ var ParseAPIError = class extends Error {
51
51
  function env(name) {
52
52
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
53
53
  }
54
- function sleep(ms) {
55
- return new Promise((resolve) => setTimeout(resolve, ms));
54
+ function sleep(ms, signal) {
55
+ return new Promise((resolve, reject) => {
56
+ if (signal?.aborted) {
57
+ reject(signal.reason);
58
+ return;
59
+ }
60
+ const onAbort = () => {
61
+ clearTimeout(timer);
62
+ reject(signal.reason);
63
+ };
64
+ const timer = setTimeout(() => {
65
+ signal?.removeEventListener("abort", onAbort);
66
+ resolve();
67
+ }, ms);
68
+ signal?.addEventListener("abort", onAbort, { once: true });
69
+ });
70
+ }
71
+ function validateTimeout(value) {
72
+ if (!Number.isInteger(value) || value <= 0 || value > 2147483647) {
73
+ throw new RangeError("parseAPI: timeoutMs must be an integer from 1 to 2147483647.");
74
+ }
75
+ }
76
+ function validateRetries(value) {
77
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0)) {
78
+ throw new RangeError("parseAPI: retries must be a non-negative integer.");
79
+ }
80
+ }
81
+ function metered(path, query) {
82
+ const product = path.split("/")[1];
83
+ return ["carrier", "caller", "hlr", "litigator", "reassigned"].includes(product ?? "") || ["email", "vat", "address"].includes(product ?? "") && query?.deep === true;
56
84
  }
57
85
  function retryDelayMs(attempt, retryAfter) {
58
86
  if (retryAfter) {
@@ -60,189 +88,221 @@ function retryDelayMs(attempt, retryAfter) {
60
88
  if (Number.isFinite(seconds) && seconds >= 0) {
61
89
  return Math.min(seconds * 1e3, RETRY_AFTER_CAP_MS);
62
90
  }
91
+ if (Number.isNaN(seconds)) {
92
+ const date = Date.parse(retryAfter);
93
+ if (Number.isFinite(date)) return Math.min(Math.max(date - Date.now(), 0), RETRY_AFTER_CAP_MS);
94
+ }
63
95
  }
64
96
  return Math.random() * 250 * 2 ** attempt;
65
97
  }
66
98
  function parseAPI(apiKey, options = {}) {
67
- const key = apiKey ?? env("PARSEAPI_KEY");
99
+ const key = apiKey || env("PARSEAPI_KEY");
68
100
  if (!key) {
69
101
  throw new Error("parseAPI: missing API key. Pass one or set PARSEAPI_KEY.");
70
102
  }
71
103
  const baseUrl = (options.baseUrl ?? env("PARSEAPI_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
72
104
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
73
- const retries = options.retries ?? DEFAULT_RETRIES;
105
+ const configuredRetries = options.retries;
106
+ validateTimeout(timeoutMs);
107
+ validateRetries(configuredRetries);
74
108
  const doFetch = options.fetch ?? fetch;
75
- async function request(path, query, headers) {
109
+ async function request(path, query, headers, controls = {}) {
110
+ const signal = controls.signal;
111
+ signal?.throwIfAborted();
112
+ const attemptTimeout = controls.timeoutMs ?? timeoutMs;
113
+ const retries = controls.retries ?? configuredRetries ?? (metered(path, query) ? 0 : DEFAULT_RETRIES);
114
+ validateTimeout(attemptTimeout);
115
+ validateRetries(retries);
76
116
  const url = new URL(baseUrl + path);
77
117
  for (const [name, value] of Object.entries(query ?? {})) {
78
118
  if (value !== void 0) url.searchParams.set(name, String(value));
79
119
  }
80
120
  for (let attempt = 0; ; attempt++) {
81
- let res;
121
+ signal?.throwIfAborted();
122
+ const controller = new AbortController();
123
+ const onAbort = () => controller.abort(signal.reason);
124
+ signal?.addEventListener("abort", onAbort, { once: true });
125
+ const timer = setTimeout(() => controller.abort(new DOMException("Request timed out", "TimeoutError")), attemptTimeout);
126
+ let retryAfter = null;
82
127
  try {
83
- res = await doFetch(url, {
84
- headers: {
85
- "X-API-Key": key,
86
- "User-Agent": `parseapi-node/${VERSION}`,
87
- ...headers
88
- },
89
- signal: AbortSignal.timeout(timeoutMs)
90
- });
91
- } catch (err) {
92
- if (attempt < retries) {
93
- await sleep(retryDelayMs(attempt, null));
94
- continue;
128
+ let res;
129
+ try {
130
+ res = await doFetch(url, {
131
+ redirect: "manual",
132
+ headers: { "X-API-Key": key, "User-Agent": `parseapi-node/${VERSION}`, ...headers },
133
+ signal: controller.signal
134
+ });
135
+ } catch (error) {
136
+ signal?.throwIfAborted();
137
+ if (attempt >= retries) throw error;
95
138
  }
96
- throw err;
97
- }
98
- if (res.ok) {
99
- return await res.json();
100
- }
101
- if (RETRY_STATUS.has(res.status) && attempt < retries) {
102
- await sleep(retryDelayMs(attempt, res.headers.get("Retry-After")));
103
- continue;
104
- }
105
- let body = {};
106
- try {
107
- body = await res.json();
108
- } catch {
139
+ signal?.throwIfAborted();
140
+ if (res?.ok) {
141
+ try {
142
+ const result = await res.json();
143
+ signal?.throwIfAborted();
144
+ return result;
145
+ } catch (error) {
146
+ signal?.throwIfAborted();
147
+ if (error instanceof SyntaxError || attempt >= retries) throw error;
148
+ res = void 0;
149
+ }
150
+ }
151
+ if (res && RETRY_STATUS.has(res.status) && attempt < retries) {
152
+ retryAfter = res.headers.get("Retry-After");
153
+ await res.body?.cancel().catch(() => {
154
+ });
155
+ } else if (res) {
156
+ let body = {};
157
+ try {
158
+ const parsed = await res.json();
159
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) body = parsed;
160
+ } catch {
161
+ controller.signal.throwIfAborted();
162
+ }
163
+ signal?.throwIfAborted();
164
+ throw new ParseAPIError(
165
+ res.status,
166
+ typeof body.code === "string" ? body.code : "unknown_error",
167
+ typeof body.message === "string" ? body.message : `Request failed with status ${res.status}`,
168
+ typeof body.docs === "string" ? body.docs : null,
169
+ typeof body.request_id === "string" ? body.request_id : null
170
+ );
171
+ }
172
+ } finally {
173
+ clearTimeout(timer);
174
+ signal?.removeEventListener("abort", onAbort);
109
175
  }
110
- throw new ParseAPIError(
111
- res.status,
112
- typeof body.code === "string" ? body.code : "unknown_error",
113
- typeof body.message === "string" ? body.message : `Request failed with status ${res.status}`,
114
- typeof body.docs === "string" ? body.docs : null,
115
- typeof body.request_id === "string" ? body.request_id : null
116
- );
176
+ await sleep(retryDelayMs(attempt, retryAfter), signal);
117
177
  }
118
178
  }
119
179
  const enc = encodeURIComponent;
120
180
  const deepQuery = (opts) => opts?.deep ? { deep: true } : {};
121
- function timezone(idOrLat, lonOrOpts, opts) {
122
- if (typeof idOrLat === "number") {
123
- return request("/timezone", {
124
- lat: idOrLat,
125
- lon: typeof lonOrOpts === "number" ? lonOrOpts : void 0,
126
- at: opts?.at
127
- });
128
- }
129
- const idOpts = lonOrOpts && typeof lonOrOpts === "object" ? lonOrOpts : void 0;
130
- return request(`/timezone/${enc(idOrLat)}`, {
131
- at: idOpts?.at,
132
- to: idOpts?.to
133
- });
134
- }
135
181
  return {
136
182
  ip: Object.assign(
137
- (ip, opts) => request(`/ip/${enc(ip)}`, deepQuery(opts)),
183
+ (ip, opts) => request(`/ip/${enc(ip)}`, deepQuery(opts), void 0, opts),
138
184
  {
139
- self: (opts) => request("/ip", deepQuery(opts))
185
+ self: (opts) => request("/ip", deepQuery(opts), void 0, opts)
140
186
  }
141
187
  ),
142
188
  continent: Object.assign(
143
- (code) => request(`/continent/${enc(code)}`),
189
+ (code, opts) => request(`/continent/${enc(code)}`, void 0, void 0, opts),
144
190
  {
145
- countries: (code) => request(`/continent/${enc(code)}/countries`)
191
+ countries: (code, opts) => request(`/continent/${enc(code)}/countries`, void 0, void 0, opts)
146
192
  }
147
193
  ),
148
194
  bloc: Object.assign(
149
- (code) => request(`/bloc/${enc(code)}`),
195
+ (code, opts) => request(`/bloc/${enc(code)}`, void 0, void 0, opts),
150
196
  {
151
- countries: (code) => request(`/bloc/${enc(code)}/countries`)
197
+ countries: (code, opts) => request(`/bloc/${enc(code)}/countries`, void 0, void 0, opts)
152
198
  }
153
199
  ),
154
200
  country: Object.assign(
155
- (code) => request(`/country/${enc(code)}`),
201
+ (code, opts) => request(`/country/${enc(code)}`, void 0, void 0, opts),
156
202
  {
157
- states: (code) => request(`/country/${enc(code)}/states`)
203
+ states: (code, opts) => request(`/country/${enc(code)}/states`, void 0, void 0, opts)
158
204
  }
159
205
  ),
160
206
  state: Object.assign(
161
- (code, opts) => request(`/state/${enc(code)}`, { country: opts?.country }),
207
+ (code, opts) => request(`/state/${enc(code)}`, { country: opts?.country }, void 0, opts),
162
208
  {
163
- districts: (code, opts) => request(`/state/${enc(code)}/districts`, { country: opts?.country })
209
+ districts: (code, opts) => request(`/state/${enc(code)}/districts`, { country: opts?.country }, void 0, opts)
164
210
  }
165
211
  ),
166
- district: (code, opts) => request(`/district/${enc(code)}`, { country: opts?.country, state: opts?.state }),
212
+ district: (code, opts) => request(`/district/${enc(code)}`, { country: opts?.country, state: opts?.state }, void 0, opts),
167
213
  city: Object.assign(
168
- (name, opts) => request(`/city/${enc(name)}`, { country: opts?.country, state: opts?.state }),
214
+ (name, opts) => request(`/city/${enc(name)}`, { country: opts?.country, state: opts?.state }, void 0, opts),
169
215
  {
170
- id: (id) => request(`/city/id/${enc(id)}`),
171
- search: (q, opts) => request("/city", { q, country: opts?.country, state: opts?.state, limit: opts?.limit }),
172
- nearest: (lat, lon) => request("/city", { lat, lon }),
216
+ id: (id, opts) => request(`/city/id/${enc(id)}`, void 0, void 0, opts),
217
+ search: (query, opts) => request("/city", { q: query, country: opts?.country, state: opts?.state, limit: opts?.limit }, void 0, opts),
218
+ nearest: (lat, lon, opts) => request("/city", { lat, lon }, void 0, opts),
173
219
  nearby: (name, opts) => request(`/city/${enc(name)}/nearby`, {
174
220
  radius: opts?.radius,
175
221
  unit: opts?.unit,
176
222
  country: opts?.country,
177
223
  state: opts?.state,
178
224
  limit: opts?.limit
179
- })
225
+ }, void 0, opts)
180
226
  }
181
227
  ),
182
228
  postal: Object.assign(
183
- (code, opts) => request(`/postal/${enc(code)}`, { country: opts?.country }),
229
+ (code, opts) => request(`/postal/${enc(code)}`, { country: opts?.country }, void 0, opts),
184
230
  {
185
231
  nearby: (code, opts) => request(`/postal/${enc(code)}/nearby`, {
186
232
  country: opts?.country,
187
233
  radius: opts?.radius,
188
234
  unit: opts?.unit
189
- }),
190
- distance: (from, to, opts) => request(`/postal/${enc(from)}/distance/${enc(to)}`, { country: opts?.country })
235
+ }, void 0, opts),
236
+ distance: (from, to, opts) => request(`/postal/${enc(from)}/distance/${enc(to)}`, { country: opts?.country }, void 0, opts)
237
+ }
238
+ ),
239
+ address: Object.assign(
240
+ (address, opts) => request(`/address/${enc(address)}`, { country: opts?.country, ...deepQuery(opts) }, void 0, opts),
241
+ {
242
+ 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)
191
243
  }
192
244
  ),
193
- email: (email, opts) => request(`/email/${enc(email)}`, deepQuery(opts)),
245
+ company: (number, opts) => request(`/company/${enc(number)}`, { country: opts?.country, ...deepQuery(opts) }, void 0, opts),
246
+ email: (email, opts) => request(`/email/${enc(email)}`, deepQuery(opts), void 0, opts),
194
247
  vat: (number, opts) => request(`/vat/${enc(number)}`, {
195
248
  country: opts?.country,
196
249
  from: opts?.from,
197
250
  ...deepQuery(opts)
198
- }),
199
- iban: (iban, opts) => request(`/iban/${enc(iban)}`, { country: opts?.country }),
200
- npi: (npi, opts) => request(`/npi/${enc(npi)}`, deepQuery(opts)),
201
- phone: (number, opts) => request(`/phone/${enc(number)}`, { country: opts?.country, ...deepQuery(opts) }),
202
- carrier: (number, opts) => request(`/carrier/${enc(number)}`, { country: opts?.country }),
203
- caller: (number, opts) => request(`/caller/${enc(number)}`, { country: opts?.country }),
204
- hlr: (number, opts) => request(`/hlr/${enc(number)}`, { country: opts?.country }),
205
- domain: (domain, opts) => request(`/domain/${enc(domain)}`, deepQuery(opts)),
206
- mx: (domain) => request(`/mx/${enc(domain)}`),
207
- useragent: (ua, opts) => request("/useragent", deepQuery(opts), { "User-Agent": ua }),
208
- vin: (vin, opts) => request(`/vin/${enc(vin)}`, deepQuery(opts)),
251
+ }, void 0, opts),
252
+ iban: (iban, opts) => request(`/iban/${enc(iban)}`, { country: opts?.country }, void 0, opts),
253
+ npi: (npi, opts) => request(`/npi/${enc(npi)}`, deepQuery(opts), void 0, opts),
254
+ phone: (number, opts) => request(`/phone/${enc(number)}`, { country: opts?.country, ...deepQuery(opts) }, void 0, opts),
255
+ carrier: (number, opts) => request(`/carrier/${enc(number)}`, { country: opts?.country }, void 0, opts),
256
+ caller: (number, opts) => request(`/caller/${enc(number)}`, { country: opts?.country }, void 0, opts),
257
+ hlr: (number, opts) => request(`/hlr/${enc(number)}`, { country: opts?.country }, void 0, opts),
258
+ domain: (domain, opts) => request(`/domain/${enc(domain)}`, deepQuery(opts), void 0, opts),
259
+ asn: (asn, opts) => request(`/asn/${enc(asn)}`, void 0, void 0, opts),
260
+ mac: (mac, opts) => request(`/mac/${enc(mac)}`, void 0, void 0, opts),
261
+ mx: (domain, opts) => request(`/mx/${enc(domain)}`, void 0, void 0, opts),
262
+ useragent: (ua, opts) => request("/useragent", deepQuery(opts), { "User-Agent": ua }, opts),
263
+ vin: (vin, opts) => request(`/vin/${enc(vin)}`, deepQuery(opts), void 0, opts),
209
264
  tariff: Object.assign(
210
- (code, opts) => request(`/tariff/${enc(code)}`, { origin: opts?.origin, ...deepQuery(opts) }),
265
+ (code, opts) => request(`/tariff/${enc(code)}`, { origin: opts?.origin, ...deepQuery(opts) }, void 0, opts),
211
266
  {
212
- search: (q) => request("/tariff", { q })
267
+ search: (query, opts) => request("/tariff", { q: query }, void 0, opts)
213
268
  }
214
269
  ),
215
270
  currency: Object.assign(
216
- (code) => request(`/currency/${enc(code)}`),
271
+ (code, opts) => request(`/currency/${enc(code)}`, void 0, void 0, opts),
217
272
  {
218
273
  rate: (base, quote, opts) => request(`/currency/${enc(base)}/${enc(quote)}`, {
219
274
  date: opts?.date,
220
275
  amount: opts?.amount
221
- })
276
+ }, void 0, opts)
277
+ }
278
+ ),
279
+ language: (code, opts) => request(`/language/${enc(code)}`, void 0, void 0, opts),
280
+ name: (name, opts) => request(`/name/${enc(name)}`, void 0, void 0, opts),
281
+ timezone: Object.assign(
282
+ (id, opts) => request(`/timezone/${enc(id)}`, { at: opts?.at, to: opts?.to }, void 0, opts),
283
+ {
284
+ at: (lat, lon, opts) => request("/timezone", { lat, lon, at: opts?.at }, void 0, opts)
222
285
  }
223
286
  ),
224
- language: (code) => request(`/language/${enc(code)}`),
225
- name: (name) => request(`/name/${enc(name)}`),
226
- timezone,
227
287
  date: Object.assign(
228
- (date, opts) => request(`/date/${enc(date)}`, { format: opts?.format, to: opts?.to }),
288
+ (date, opts) => request(`/date/${enc(date)}`, { format: opts?.format, to: opts?.to }, void 0, opts),
229
289
  {
230
- today: (opts) => request("/date", { to: opts?.to })
290
+ today: (opts) => request("/date", { to: opts?.to }, void 0, opts)
231
291
  }
232
292
  ),
233
293
  holiday: Object.assign(
234
- (country, opts) => request(`/holiday/${enc(country)}`, { year: opts?.year }),
294
+ (country, opts) => request(`/holiday/${enc(country)}`, { year: opts?.year }, void 0, opts),
235
295
  {
236
- date: (country, date) => request(`/holiday/${enc(country)}/${enc(date)}`)
296
+ date: (country, date, opts) => request(`/holiday/${enc(country)}/${enc(date)}`, void 0, void 0, opts)
237
297
  }
238
298
  ),
239
- elevation: (lat, lon) => request("/elevation", { lat, lon }),
240
- point: (lat, lon, opts) => request("/point", { lat, lon, ...deepQuery(opts) }),
241
- weather: (lat, lon, opts) => request("/weather", { lat, lon, date: opts?.date, ...deepQuery(opts) }),
299
+ elevation: (lat, lon, opts) => request("/elevation", { lat, lon }, void 0, opts),
300
+ point: (lat, lon, opts) => request("/point", { lat, lon, ...deepQuery(opts) }, void 0, opts),
301
+ weather: (lat, lon, opts) => request("/weather", { lat, lon, date: opts?.date, ...deepQuery(opts) }, void 0, opts),
242
302
  emoji: Object.assign(
243
- (emoji) => request(`/emoji/${enc(emoji)}`),
303
+ (emoji, opts) => request(`/emoji/${enc(emoji)}`, void 0, void 0, opts),
244
304
  {
245
- search: (q, opts) => request("/emoji", { q, limit: opts?.limit })
305
+ search: (query, opts) => request("/emoji", { q: query, limit: opts?.limit }, void 0, opts)
246
306
  }
247
307
  )
248
308
  };