solometrics 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +295 -2
- package/dist/index.cjs +1163 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +846 -0
- package/dist/index.d.ts +846 -0
- package/dist/index.js +1130 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var SolometricsError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var APIError = class extends SolometricsError {
|
|
9
|
+
};
|
|
10
|
+
var APIConnectionError = class extends APIError {
|
|
11
|
+
constructor(message = "Connection error.", options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var APITimeoutError = class extends APIConnectionError {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
super("Request timed out.", options);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var APIUserAbortError = class extends APIError {
|
|
21
|
+
constructor(options) {
|
|
22
|
+
super("Request was aborted.", options);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var APIStatusError = class extends APIError {
|
|
26
|
+
status;
|
|
27
|
+
detail;
|
|
28
|
+
body;
|
|
29
|
+
headers;
|
|
30
|
+
constructor(message, init) {
|
|
31
|
+
super(`${init.status}: ${message}`);
|
|
32
|
+
this.status = init.status;
|
|
33
|
+
this.headers = init.headers;
|
|
34
|
+
this.body = init.body;
|
|
35
|
+
this.detail = isRecord(init.body) && "detail" in init.body ? init.body.detail : null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var BadRequestError = class extends APIStatusError {
|
|
39
|
+
};
|
|
40
|
+
var AuthenticationError = class extends APIStatusError {
|
|
41
|
+
};
|
|
42
|
+
var PermissionDeniedError = class extends APIStatusError {
|
|
43
|
+
};
|
|
44
|
+
var NotFoundError = class extends APIStatusError {
|
|
45
|
+
};
|
|
46
|
+
var ConflictError = class extends APIStatusError {
|
|
47
|
+
};
|
|
48
|
+
var UnprocessableEntityError = class extends APIStatusError {
|
|
49
|
+
get errors() {
|
|
50
|
+
return Array.isArray(this.detail) ? this.detail : [];
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var RateLimitError = class extends APIStatusError {
|
|
54
|
+
get retryAfter() {
|
|
55
|
+
return parseRetryAfter(this.headers);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var InternalServerError = class extends APIStatusError {
|
|
59
|
+
};
|
|
60
|
+
var UpstreamError = class extends InternalServerError {
|
|
61
|
+
};
|
|
62
|
+
var UpstreamTimeoutError = class extends InternalServerError {
|
|
63
|
+
};
|
|
64
|
+
var JobTimeoutError = class extends SolometricsError {
|
|
65
|
+
taskId;
|
|
66
|
+
lastStatus;
|
|
67
|
+
constructor(taskId, lastStatus) {
|
|
68
|
+
const status = isRecord(lastStatus) ? lastStatus.status : lastStatus;
|
|
69
|
+
super(`Job ${taskId} did not finish in time (last status: ${String(status)})`);
|
|
70
|
+
this.taskId = taskId;
|
|
71
|
+
this.lastStatus = lastStatus;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var STATUS_MAP = {
|
|
75
|
+
400: BadRequestError,
|
|
76
|
+
401: AuthenticationError,
|
|
77
|
+
403: PermissionDeniedError,
|
|
78
|
+
404: NotFoundError,
|
|
79
|
+
409: ConflictError,
|
|
80
|
+
422: UnprocessableEntityError,
|
|
81
|
+
429: RateLimitError,
|
|
82
|
+
502: UpstreamError,
|
|
83
|
+
504: UpstreamTimeoutError
|
|
84
|
+
};
|
|
85
|
+
function parseRetryAfter(headers) {
|
|
86
|
+
const raw = headers.get("retry-after");
|
|
87
|
+
if (raw === null || raw.trim() === "") return null;
|
|
88
|
+
const value = Number(raw);
|
|
89
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
90
|
+
}
|
|
91
|
+
function makeStatusError(status, statusText, headers, text) {
|
|
92
|
+
let body;
|
|
93
|
+
try {
|
|
94
|
+
body = JSON.parse(text);
|
|
95
|
+
} catch {
|
|
96
|
+
body = text || null;
|
|
97
|
+
}
|
|
98
|
+
const detail = isRecord(body) ? body.detail : void 0;
|
|
99
|
+
let message;
|
|
100
|
+
if (typeof detail === "string") {
|
|
101
|
+
message = detail;
|
|
102
|
+
} else if (Array.isArray(detail)) {
|
|
103
|
+
const parts = detail.map((err) => {
|
|
104
|
+
if (isRecord(err)) {
|
|
105
|
+
const loc = Array.isArray(err.loc) ? err.loc.map(String).join(".") : "";
|
|
106
|
+
return loc ? `${loc}: ${String(err.msg)}` : String(err.msg);
|
|
107
|
+
}
|
|
108
|
+
return String(err);
|
|
109
|
+
});
|
|
110
|
+
message = parts.join("; ") || "Validation error";
|
|
111
|
+
} else if (typeof body === "string" && body) {
|
|
112
|
+
message = body.slice(0, 500);
|
|
113
|
+
} else {
|
|
114
|
+
message = statusText || "API error";
|
|
115
|
+
}
|
|
116
|
+
const cls = STATUS_MAP[status] ?? (status >= 500 ? InternalServerError : APIStatusError);
|
|
117
|
+
return new cls(message, { status, headers, body });
|
|
118
|
+
}
|
|
119
|
+
function isRecord(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/core.ts
|
|
124
|
+
var DEFAULT_BASE_URL = "https://api.sneakers.itbanque.com";
|
|
125
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
126
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
127
|
+
var API_KEY_ENV = "SOLOMETRICS_API_KEY";
|
|
128
|
+
var BASE_URL_ENV = "SOLOMETRICS_BASE_URL";
|
|
129
|
+
var TERMINAL_JOB_STATUSES = /* @__PURE__ */ new Set(["SUCCESS", "FAILURE", "CANCELLED"]);
|
|
130
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "PATCH", "DELETE"]);
|
|
131
|
+
var MAX_BACKOFF_MS = 8e3;
|
|
132
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
133
|
+
function readEnv(name) {
|
|
134
|
+
const value = typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
135
|
+
return value || void 0;
|
|
136
|
+
}
|
|
137
|
+
function formatAmount(amount) {
|
|
138
|
+
if (typeof amount === "boolean") throw new TypeError("amount must be a number or string, not boolean");
|
|
139
|
+
if (typeof amount === "number" && !Number.isFinite(amount)) throw new TypeError(`Invalid amount: ${amount}`);
|
|
140
|
+
return String(amount);
|
|
141
|
+
}
|
|
142
|
+
function formatSize(size) {
|
|
143
|
+
return String(size);
|
|
144
|
+
}
|
|
145
|
+
function formatDate(value) {
|
|
146
|
+
return value instanceof Date ? value.toISOString().slice(0, 10) : value;
|
|
147
|
+
}
|
|
148
|
+
function formatDateTime(value) {
|
|
149
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
150
|
+
}
|
|
151
|
+
function pathSegment(value) {
|
|
152
|
+
return encodeURIComponent(String(value));
|
|
153
|
+
}
|
|
154
|
+
function queryString(value) {
|
|
155
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
156
|
+
if (value instanceof Date) return formatDate(value);
|
|
157
|
+
return String(value);
|
|
158
|
+
}
|
|
159
|
+
function buildURL(baseURL, path, query) {
|
|
160
|
+
const params = new URLSearchParams();
|
|
161
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
162
|
+
if (value === void 0 || value === null) continue;
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
for (const item of value) params.append(key, queryString(item));
|
|
165
|
+
} else {
|
|
166
|
+
params.append(key, queryString(value));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const qs = params.toString();
|
|
170
|
+
return `${baseURL}${path}${qs ? `?${qs}` : ""}`;
|
|
171
|
+
}
|
|
172
|
+
function cleanBody(body) {
|
|
173
|
+
const out = {};
|
|
174
|
+
for (const [key, value] of Object.entries(body)) {
|
|
175
|
+
if (value !== void 0 && value !== null) out[key] = value;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function joinStatuses(statuses) {
|
|
180
|
+
if (statuses === void 0 || statuses === null) return void 0;
|
|
181
|
+
return typeof statuses === "string" ? statuses : statuses.join(",");
|
|
182
|
+
}
|
|
183
|
+
function normalizeBatchDeleteItems(items) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const item of items) {
|
|
186
|
+
if (typeof item === "string") {
|
|
187
|
+
out.push({ listingId: item });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let listingId;
|
|
191
|
+
let variantId;
|
|
192
|
+
if (Array.isArray(item)) {
|
|
193
|
+
[listingId, variantId] = item;
|
|
194
|
+
} else if (typeof item === "object" && item !== null) {
|
|
195
|
+
const obj = item;
|
|
196
|
+
listingId = obj.listingId ?? obj.listing_id;
|
|
197
|
+
variantId = obj.variantId ?? obj.variant_id;
|
|
198
|
+
} else {
|
|
199
|
+
throw new TypeError(`Unsupported batch-delete item: ${JSON.stringify(item)}`);
|
|
200
|
+
}
|
|
201
|
+
if (!listingId) throw new TypeError(`Batch-delete item is missing a listing ID: ${JSON.stringify(item)}`);
|
|
202
|
+
const entry = { listingId: String(listingId) };
|
|
203
|
+
if (variantId) entry.variantId = String(variantId);
|
|
204
|
+
out.push(entry);
|
|
205
|
+
}
|
|
206
|
+
if (out.length === 0) throw new TypeError("`items` is empty.");
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
var encoder = new TextEncoder();
|
|
210
|
+
function isRowsSource(source) {
|
|
211
|
+
return typeof source === "object" && source !== null && !(source instanceof Uint8Array) && !(source instanceof ArrayBuffer) && !(typeof Blob !== "undefined" && source instanceof Blob) && "rows" in source;
|
|
212
|
+
}
|
|
213
|
+
function basename(path) {
|
|
214
|
+
return path.split(/[\\/]/).pop() || path;
|
|
215
|
+
}
|
|
216
|
+
async function collect(chunks) {
|
|
217
|
+
const parts = [];
|
|
218
|
+
let length = 0;
|
|
219
|
+
for await (const chunk of chunks) {
|
|
220
|
+
const bytes = typeof chunk === "string" ? encoder.encode(chunk) : new Uint8Array(chunk);
|
|
221
|
+
parts.push(bytes);
|
|
222
|
+
length += bytes.length;
|
|
223
|
+
}
|
|
224
|
+
const out = new Uint8Array(length);
|
|
225
|
+
let offset = 0;
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
out.set(part, offset);
|
|
228
|
+
offset += part.length;
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
async function* readWebStream(stream) {
|
|
233
|
+
const reader = stream.getReader();
|
|
234
|
+
try {
|
|
235
|
+
for (; ; ) {
|
|
236
|
+
const { done, value } = await reader.read();
|
|
237
|
+
if (done) return;
|
|
238
|
+
yield value;
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
reader.releaseLock();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function prepareCsvUpload(source, defaultName) {
|
|
245
|
+
if (source === void 0 || source === null) {
|
|
246
|
+
throw new TypeError("Pass a CSV file (path, bytes, Blob or stream) or `{ rows }`.");
|
|
247
|
+
}
|
|
248
|
+
let name = defaultName;
|
|
249
|
+
let content;
|
|
250
|
+
if (typeof source === "string") {
|
|
251
|
+
const { readFile } = await import("fs/promises");
|
|
252
|
+
content = new Uint8Array(await readFile(source));
|
|
253
|
+
name = basename(source);
|
|
254
|
+
} else if (source instanceof Uint8Array) {
|
|
255
|
+
content = source;
|
|
256
|
+
} else if (source instanceof ArrayBuffer) {
|
|
257
|
+
content = new Uint8Array(source);
|
|
258
|
+
} else if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
259
|
+
content = new Uint8Array(await source.arrayBuffer());
|
|
260
|
+
const fileName = source.name;
|
|
261
|
+
if (typeof fileName === "string" && fileName) name = basename(fileName);
|
|
262
|
+
} else if (isRowsSource(source)) {
|
|
263
|
+
content = rowsToCsv(source.rows);
|
|
264
|
+
} else if (typeof source.getReader === "function") {
|
|
265
|
+
content = await collect(readWebStream(source));
|
|
266
|
+
} else if (typeof source[Symbol.asyncIterator] === "function") {
|
|
267
|
+
content = await collect(source);
|
|
268
|
+
const path = source.path;
|
|
269
|
+
if (typeof path === "string" && path) name = basename(path);
|
|
270
|
+
} else {
|
|
271
|
+
throw new TypeError(`Unsupported CSV source: ${Object.prototype.toString.call(source)}`);
|
|
272
|
+
}
|
|
273
|
+
if (!name.toLowerCase().endsWith(".csv")) name = `${name}.csv`;
|
|
274
|
+
return { name, content };
|
|
275
|
+
}
|
|
276
|
+
function csvField(value) {
|
|
277
|
+
if (value === void 0 || value === null) return "";
|
|
278
|
+
const text = value instanceof Date ? value.toISOString() : String(value);
|
|
279
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
280
|
+
}
|
|
281
|
+
function rowsToCsv(rows) {
|
|
282
|
+
const list = Array.from(rows);
|
|
283
|
+
if (list.length === 0) throw new TypeError("`rows` is empty.");
|
|
284
|
+
const header = [];
|
|
285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
286
|
+
for (const row of list) {
|
|
287
|
+
for (const key of Object.keys(row)) {
|
|
288
|
+
if (!seen.has(key)) {
|
|
289
|
+
seen.add(key);
|
|
290
|
+
header.push(key);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const lines = [header.map(csvField).join(",")];
|
|
295
|
+
for (const row of list) lines.push(header.map((key) => csvField(row[key])).join(","));
|
|
296
|
+
return encoder.encode(`${lines.join("\n")}
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
var CONNECT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
300
|
+
"ECONNREFUSED",
|
|
301
|
+
"ENOTFOUND",
|
|
302
|
+
"EAI_AGAIN",
|
|
303
|
+
"EHOSTUNREACH",
|
|
304
|
+
"ENETUNREACH",
|
|
305
|
+
"EADDRNOTAVAIL",
|
|
306
|
+
"UND_ERR_CONNECT_TIMEOUT"
|
|
307
|
+
]);
|
|
308
|
+
function errorCodes(err, depth = 0) {
|
|
309
|
+
if (typeof err !== "object" || err === null || depth > 3) return [];
|
|
310
|
+
const e = err;
|
|
311
|
+
const codes = typeof e.code === "string" ? [e.code] : [];
|
|
312
|
+
if (Array.isArray(e.errors)) for (const inner of e.errors) codes.push(...errorCodes(inner, depth + 1));
|
|
313
|
+
codes.push(...errorCodes(e.cause, depth + 1));
|
|
314
|
+
return codes;
|
|
315
|
+
}
|
|
316
|
+
function classifyNetworkError(err) {
|
|
317
|
+
const codes = errorCodes(err);
|
|
318
|
+
if (codes.length > 0 && codes.every((code) => CONNECT_ERROR_CODES.has(code))) return "connect";
|
|
319
|
+
return "network";
|
|
320
|
+
}
|
|
321
|
+
function shouldRetryStatus(method, status) {
|
|
322
|
+
if (status === 429) return true;
|
|
323
|
+
if (status === 502 || status === 503 || status === 504) return IDEMPOTENT_METHODS.has(method.toUpperCase());
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
function shouldRetryNetworkError(method, kind) {
|
|
327
|
+
if (kind === "connect") return true;
|
|
328
|
+
return IDEMPOTENT_METHODS.has(method.toUpperCase());
|
|
329
|
+
}
|
|
330
|
+
function retryDelay(attempt, retryAfterSeconds) {
|
|
331
|
+
if (retryAfterSeconds !== null) return Math.min(retryAfterSeconds * 1e3, MAX_RETRY_AFTER_MS);
|
|
332
|
+
const base = Math.min(500 * 2 ** attempt, MAX_BACKOFF_MS);
|
|
333
|
+
return base * (0.75 + Math.random() * 0.5);
|
|
334
|
+
}
|
|
335
|
+
function jobFinished(payload) {
|
|
336
|
+
const status = typeof payload === "object" && payload !== null ? payload.status : void 0;
|
|
337
|
+
return typeof status === "string" && TERMINAL_JOB_STATUSES.has(status.toUpperCase());
|
|
338
|
+
}
|
|
339
|
+
function defaultSleep(ms, signal) {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
if (signal?.aborted) {
|
|
342
|
+
reject(new APIUserAbortError({ cause: signal.reason }));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const onAbort = () => {
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
reject(new APIUserAbortError({ cause: signal?.reason }));
|
|
348
|
+
};
|
|
349
|
+
const timer = setTimeout(
|
|
350
|
+
() => {
|
|
351
|
+
signal?.removeEventListener("abort", onAbort);
|
|
352
|
+
resolve();
|
|
353
|
+
},
|
|
354
|
+
Math.max(0, ms)
|
|
355
|
+
);
|
|
356
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
var timing = {
|
|
360
|
+
sleep: defaultSleep
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/resources/shared.ts
|
|
364
|
+
var APIResource = class {
|
|
365
|
+
_client;
|
|
366
|
+
constructor(client) {
|
|
367
|
+
this._client = client;
|
|
368
|
+
}
|
|
369
|
+
_conn(connectionId) {
|
|
370
|
+
return connectionId ?? this._client.connectionId;
|
|
371
|
+
}
|
|
372
|
+
_get(path, query, options) {
|
|
373
|
+
return this._client._request({ ...requestOptions(options), method: "GET", path, query });
|
|
374
|
+
}
|
|
375
|
+
_send(method, path, init = {}, options) {
|
|
376
|
+
const final = { ...requestOptions(options), method, path, ...init };
|
|
377
|
+
return this._client._request(final);
|
|
378
|
+
}
|
|
379
|
+
async _wait(fetchStatus, taskId, { pollInterval = 5e3, timeout = null, onProgress, signal } = {}) {
|
|
380
|
+
const deadline = timeout === null ? null : performance.now() + timeout;
|
|
381
|
+
for (; ; ) {
|
|
382
|
+
const status = await fetchStatus({ signal });
|
|
383
|
+
if (onProgress) await onProgress(status);
|
|
384
|
+
if (jobFinished(status)) return status;
|
|
385
|
+
if (deadline !== null && performance.now() + pollInterval > deadline) throw new JobTimeoutError(taskId, status);
|
|
386
|
+
await timing.sleep(pollInterval, signal);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function requestOptions(options) {
|
|
391
|
+
if (!options) return {};
|
|
392
|
+
const out = {};
|
|
393
|
+
if (options.timeout !== void 0) out.timeout = options.timeout;
|
|
394
|
+
if (options.maxRetries !== void 0) out.maxRetries = options.maxRetries;
|
|
395
|
+
if (options.signal !== void 0) out.signal = options.signal;
|
|
396
|
+
if (options.headers !== void 0) out.headers = options.headers;
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/resources/goat.ts
|
|
401
|
+
var GoatMarketResource = class extends APIResource {
|
|
402
|
+
/**
|
|
403
|
+
* `GET /v2/goat/market/search` — per-size GOAT prices. Needs a linked GOAT
|
|
404
|
+
* account (`ConflictError` otherwise).
|
|
405
|
+
*/
|
|
406
|
+
search(styleId, options = {}) {
|
|
407
|
+
return this._get("/v2/goat/market/search", { style_id: styleId }, options);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
var GoatResource = class extends APIResource {
|
|
411
|
+
market;
|
|
412
|
+
constructor(client) {
|
|
413
|
+
super(client);
|
|
414
|
+
this.market = new GoatMarketResource(client);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// src/resources/inventory.ts
|
|
419
|
+
var InventoryResource = class extends APIResource {
|
|
420
|
+
/** `GET /v1/inventory/search` — one page of SKUs with stock per location. */
|
|
421
|
+
search(params = {}) {
|
|
422
|
+
return this._get(
|
|
423
|
+
"/v1/inventory/search",
|
|
424
|
+
{
|
|
425
|
+
q: params.q,
|
|
426
|
+
style_color: params.styleColor,
|
|
427
|
+
brand: params.brand,
|
|
428
|
+
retailer: params.retailer,
|
|
429
|
+
size: params.size === void 0 || params.size === null ? void 0 : formatSize(params.size),
|
|
430
|
+
retailer_sku: params.retailerSku,
|
|
431
|
+
supplier_sku: params.supplierSku,
|
|
432
|
+
limit: params.limit,
|
|
433
|
+
offset: params.offset
|
|
434
|
+
},
|
|
435
|
+
params
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Iterate every matching SKU across pages.
|
|
440
|
+
*
|
|
441
|
+
* Stops at the last page. Without provider access for `retailer` the plan's
|
|
442
|
+
* `max_page_number` cap applies, and going past it throws `BadRequestError`.
|
|
443
|
+
*/
|
|
444
|
+
async *iterSearch(params = {}) {
|
|
445
|
+
const { pageSize, offset: start, ...rest } = params;
|
|
446
|
+
let offset = start ?? 0;
|
|
447
|
+
for (; ; ) {
|
|
448
|
+
const page = await this.search({ ...rest, limit: pageSize, offset });
|
|
449
|
+
const items = page.items ?? [];
|
|
450
|
+
yield* items;
|
|
451
|
+
if (!page.has_next_page || items.length === 0) return;
|
|
452
|
+
offset += items.length;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* `GET /v1/reporting/retailers/{retailer}/skus/{retailerSku}/history` —
|
|
457
|
+
* stock history of one SKU, newest first.
|
|
458
|
+
*/
|
|
459
|
+
history(retailer, retailerSku, params = {}) {
|
|
460
|
+
return this._get(
|
|
461
|
+
`/v1/reporting/retailers/${pathSegment(retailer)}/skus/${pathSegment(retailerSku)}/history`,
|
|
462
|
+
{ hours: params.hours },
|
|
463
|
+
params
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// src/resources/stockx/catalog.ts
|
|
469
|
+
var StockXCatalogResource = class extends APIResource {
|
|
470
|
+
/**
|
|
471
|
+
* `GET /v2/stockx/catalog/search` — StockX catalog search (usually by style
|
|
472
|
+
* code), returned as received from StockX.
|
|
473
|
+
*/
|
|
474
|
+
search(q, params = {}) {
|
|
475
|
+
return this._get(
|
|
476
|
+
"/v2/stockx/catalog/search",
|
|
477
|
+
{
|
|
478
|
+
q,
|
|
479
|
+
page_number: params.pageNumber,
|
|
480
|
+
page_size: params.pageSize,
|
|
481
|
+
connection_id: this._conn(params.connectionId)
|
|
482
|
+
},
|
|
483
|
+
params
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* `GET /v2/stockx/catalog/products/{productId}/variants` — every size variant
|
|
488
|
+
* of a product, as received from StockX.
|
|
489
|
+
*/
|
|
490
|
+
variants(productId, params = {}) {
|
|
491
|
+
return this._get(
|
|
492
|
+
`/v2/stockx/catalog/products/${pathSegment(productId)}/variants`,
|
|
493
|
+
{ style_id: params.styleId, connection_id: this._conn(params.connectionId) },
|
|
494
|
+
params
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// src/resources/stockx/costs.ts
|
|
500
|
+
var StockXCostsResource = class extends APIResource {
|
|
501
|
+
/** `PATCH /v2/stockx/cost` — create or overwrite the cost for one style/size/variant. Basic plan. */
|
|
502
|
+
set(styleId, size, variantId, cost, params = {}) {
|
|
503
|
+
return this._send(
|
|
504
|
+
"PATCH",
|
|
505
|
+
"/v2/stockx/cost",
|
|
506
|
+
{
|
|
507
|
+
query: { connection_id: this._conn(params.connectionId) },
|
|
508
|
+
body: { styleId, size: formatSize(size), variantId, cost }
|
|
509
|
+
},
|
|
510
|
+
params
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
/** `GET /v2/stockx/costs` — cost records, newest first. */
|
|
514
|
+
list(params = {}) {
|
|
515
|
+
return this._get(
|
|
516
|
+
"/v2/stockx/costs",
|
|
517
|
+
{
|
|
518
|
+
page: params.page,
|
|
519
|
+
page_size: params.pageSize,
|
|
520
|
+
style_id: params.styleId,
|
|
521
|
+
connection_id: this._conn(params.connectionId)
|
|
522
|
+
},
|
|
523
|
+
params
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
/** Iterate every cost record across pages. */
|
|
527
|
+
async *iterList(params = {}) {
|
|
528
|
+
const { pageSize = 200, ...rest } = params;
|
|
529
|
+
let seen = 0;
|
|
530
|
+
for (let page = 1; ; page++) {
|
|
531
|
+
const data = await this.list({ ...rest, page, pageSize });
|
|
532
|
+
const items = data.items ?? [];
|
|
533
|
+
yield* items;
|
|
534
|
+
seen += items.length;
|
|
535
|
+
if (items.length === 0 || seen >= Number(data.total ?? 0)) return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* `POST /v2/stockx/costs/import` — upsert costs from a CSV with
|
|
540
|
+
* `style_id, size, variant_id, cost`. Synchronous. Advanced plan.
|
|
541
|
+
*/
|
|
542
|
+
async importCsv(source, params = {}) {
|
|
543
|
+
const file = await prepareCsvUpload(source, "costs.csv");
|
|
544
|
+
return this._send(
|
|
545
|
+
"POST",
|
|
546
|
+
"/v2/stockx/costs/import",
|
|
547
|
+
{ query: { connection_id: this._conn(params.connectionId) }, file },
|
|
548
|
+
params
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
/** `PATCH /v2/stockx/costs/{costId}` — edit one record. Basic plan. */
|
|
552
|
+
update(costId, params = {}) {
|
|
553
|
+
return this._send(
|
|
554
|
+
"PATCH",
|
|
555
|
+
`/v2/stockx/costs/${pathSegment(costId)}`,
|
|
556
|
+
{
|
|
557
|
+
query: { connection_id: this._conn(params.connectionId) },
|
|
558
|
+
body: cleanBody({ cost: params.cost, is_active: params.isActive })
|
|
559
|
+
},
|
|
560
|
+
params
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
/** `DELETE /v2/stockx/costs` — delete EVERY cost record on the connection. Cannot be undone. Basic plan. */
|
|
564
|
+
clear(params = {}) {
|
|
565
|
+
return this._send(
|
|
566
|
+
"DELETE",
|
|
567
|
+
"/v2/stockx/costs",
|
|
568
|
+
{ query: { connection_id: this._conn(params.connectionId) } },
|
|
569
|
+
params
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/resources/stockx/listings.ts
|
|
575
|
+
var StockXListingsResource = class extends APIResource {
|
|
576
|
+
// -------------------------------------------------------------- single items
|
|
577
|
+
/**
|
|
578
|
+
* `POST /v2/stockx/simple-listing` — list by style code and size.
|
|
579
|
+
*
|
|
580
|
+
* The backend resolves the variant. Throws `NotFoundError` if no variant
|
|
581
|
+
* matches and `ConflictError` when keep-listing mode skips it. Requires the
|
|
582
|
+
* basic plan.
|
|
583
|
+
*/
|
|
584
|
+
async createBySku(styleId, size, amount, params = {}) {
|
|
585
|
+
return this._send(
|
|
586
|
+
"POST",
|
|
587
|
+
"/v2/stockx/simple-listing",
|
|
588
|
+
{
|
|
589
|
+
query: { connection_id: this._conn(params.connectionId) },
|
|
590
|
+
body: cleanBody({
|
|
591
|
+
styleId,
|
|
592
|
+
size: formatSize(size),
|
|
593
|
+
amount: formatAmount(amount),
|
|
594
|
+
cost: params.cost,
|
|
595
|
+
currencyCode: params.currencyCode,
|
|
596
|
+
expiresAt: params.expiresAt == null ? void 0 : formatDateTime(params.expiresAt),
|
|
597
|
+
active: params.active,
|
|
598
|
+
inventoryType: params.inventoryType
|
|
599
|
+
})
|
|
600
|
+
},
|
|
601
|
+
params
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* `POST /v2/stockx/listings` — list a specific `variantId`.
|
|
606
|
+
*
|
|
607
|
+
* To store a cost record too, pass `cost`, `styleId` and `size` together (the
|
|
608
|
+
* API silently skips the cost if one is missing, so the SDK throws a
|
|
609
|
+
* `TypeError` instead). Requires the basic plan.
|
|
610
|
+
*/
|
|
611
|
+
async create(variantId, amount, params = {}) {
|
|
612
|
+
if (params.cost != null && (params.styleId == null || params.size == null)) {
|
|
613
|
+
throw new TypeError("`cost` is only stored together with `styleId` and `size`; pass all three.");
|
|
614
|
+
}
|
|
615
|
+
return this._send(
|
|
616
|
+
"POST",
|
|
617
|
+
"/v2/stockx/listings",
|
|
618
|
+
{
|
|
619
|
+
query: { connection_id: this._conn(params.connectionId) },
|
|
620
|
+
body: cleanBody({
|
|
621
|
+
variantId,
|
|
622
|
+
amount: formatAmount(amount),
|
|
623
|
+
currencyCode: params.currencyCode,
|
|
624
|
+
expiresAt: params.expiresAt == null ? void 0 : formatDateTime(params.expiresAt),
|
|
625
|
+
active: params.active,
|
|
626
|
+
inventoryType: params.inventoryType,
|
|
627
|
+
cost: params.cost,
|
|
628
|
+
styleId: params.styleId,
|
|
629
|
+
size: params.size == null ? void 0 : formatSize(params.size)
|
|
630
|
+
})
|
|
631
|
+
},
|
|
632
|
+
params
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
/** `PATCH /v2/stockx/listings/{listingId}` — reprice or change expiry. Requires the basic plan. */
|
|
636
|
+
async update(listingId, params = {}) {
|
|
637
|
+
return this._send(
|
|
638
|
+
"PATCH",
|
|
639
|
+
`/v2/stockx/listings/${pathSegment(listingId)}`,
|
|
640
|
+
{
|
|
641
|
+
query: { connection_id: this._conn(params.connectionId) },
|
|
642
|
+
body: cleanBody({
|
|
643
|
+
amount: params.amount == null ? void 0 : formatAmount(params.amount),
|
|
644
|
+
currencyCode: params.currencyCode,
|
|
645
|
+
expiresAt: params.expiresAt == null ? void 0 : formatDateTime(params.expiresAt)
|
|
646
|
+
})
|
|
647
|
+
},
|
|
648
|
+
params
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* `GET /v2/stockx/listings` — your listings, fetched live from StockX.
|
|
653
|
+
* ACTIVE listings carry `cost` and `netPrice`.
|
|
654
|
+
*/
|
|
655
|
+
list(params = {}) {
|
|
656
|
+
return this._get(
|
|
657
|
+
"/v2/stockx/listings",
|
|
658
|
+
{
|
|
659
|
+
page_number: params.pageNumber,
|
|
660
|
+
page_size: params.pageSize,
|
|
661
|
+
listing_statuses: joinStatuses(params.listingStatuses),
|
|
662
|
+
sku: params.sku,
|
|
663
|
+
connection_id: this._conn(params.connectionId)
|
|
664
|
+
},
|
|
665
|
+
params
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
/** `GET /v2/stockx/listings/{listingId}` — one listing. */
|
|
669
|
+
get(listingId, params = {}) {
|
|
670
|
+
return this._get(
|
|
671
|
+
`/v2/stockx/listings/${pathSegment(listingId)}`,
|
|
672
|
+
{ connection_id: this._conn(params.connectionId) },
|
|
673
|
+
params
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* `GET /v2/stockx/listings/{listingId}/operations/{operationId}` — result of
|
|
678
|
+
* an asynchronous StockX operation.
|
|
679
|
+
*/
|
|
680
|
+
getOperation(listingId, operationId, params = {}) {
|
|
681
|
+
return this._get(
|
|
682
|
+
`/v2/stockx/listings/${pathSegment(listingId)}/operations/${pathSegment(operationId)}`,
|
|
683
|
+
{ connection_id: this._conn(params.connectionId) },
|
|
684
|
+
params
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
/** `PUT /v2/stockx/listings/{listingId}/deactivate` — take off market, keep the listing. Basic plan. */
|
|
688
|
+
deactivate(listingId, params = {}) {
|
|
689
|
+
return this._send(
|
|
690
|
+
"PUT",
|
|
691
|
+
`/v2/stockx/listings/${pathSegment(listingId)}/deactivate`,
|
|
692
|
+
{ query: { connection_id: this._conn(params.connectionId) } },
|
|
693
|
+
params
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* `PUT /v2/stockx/listings/{listingId}/activate` — put back on market,
|
|
698
|
+
* optionally at a new `amount`. Basic plan.
|
|
699
|
+
*/
|
|
700
|
+
async activate(listingId, params = {}) {
|
|
701
|
+
return this._send(
|
|
702
|
+
"PUT",
|
|
703
|
+
`/v2/stockx/listings/${pathSegment(listingId)}/activate`,
|
|
704
|
+
{
|
|
705
|
+
query: { connection_id: this._conn(params.connectionId) },
|
|
706
|
+
body: params.amount == null ? void 0 : { amount: formatAmount(params.amount) }
|
|
707
|
+
},
|
|
708
|
+
params
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* `DELETE /v2/stockx/listings/{listingId}` — permanent. With `variantId` the
|
|
713
|
+
* variant's cost records are deleted too. Basic plan.
|
|
714
|
+
*/
|
|
715
|
+
delete(listingId, params = {}) {
|
|
716
|
+
return this._send(
|
|
717
|
+
"DELETE",
|
|
718
|
+
`/v2/stockx/listings/${pathSegment(listingId)}`,
|
|
719
|
+
{ query: { variant_id: params.variantId, connection_id: this._conn(params.connectionId) } },
|
|
720
|
+
params
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* `POST /v2/stockx/batch/delete-listing` — delete several listings in one
|
|
725
|
+
* StockX batch. Advanced plan.
|
|
726
|
+
*
|
|
727
|
+
* `items` may be listing ID strings, `[listingId, variantId]` tuples, or
|
|
728
|
+
* objects with `listingId`/`listing_id` and optional `variantId`/`variant_id`.
|
|
729
|
+
*/
|
|
730
|
+
async batchDelete(items, params = {}) {
|
|
731
|
+
const normalized = normalizeBatchDeleteItems(items);
|
|
732
|
+
return this._send(
|
|
733
|
+
"POST",
|
|
734
|
+
"/v2/stockx/batch/delete-listing",
|
|
735
|
+
{ query: { connection_id: this._conn(params.connectionId) }, body: { items: normalized } },
|
|
736
|
+
params
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
// -------------------------------------------------------------- CSV upload
|
|
740
|
+
/**
|
|
741
|
+
* `POST /v2/stockx/listings/upload` — start a bulk listing job.
|
|
742
|
+
*
|
|
743
|
+
* Pass a CSV (path, bytes, Blob or stream) or `{ rows }` with
|
|
744
|
+
* `sku, size, quantity, amount, cost`. Returns a `taskId`; poll with
|
|
745
|
+
* {@link uploadStatus} or {@link waitForUpload}. Advanced plan.
|
|
746
|
+
*/
|
|
747
|
+
async upload(source, params = {}) {
|
|
748
|
+
const file = await prepareCsvUpload(source, "listings.csv");
|
|
749
|
+
return this._send("POST", "/v2/stockx/listings/upload", { query: { batch_size: params.batchSize }, file }, params);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* `POST /v2/stockx/listings/upload/validate` — count valid, invalid and
|
|
753
|
+
* duplicate rows without listing anything.
|
|
754
|
+
*/
|
|
755
|
+
async validateUpload(source, options = {}) {
|
|
756
|
+
const file = await prepareCsvUpload(source, "listings.csv");
|
|
757
|
+
return this._send("POST", "/v2/stockx/listings/upload/validate", { file }, options);
|
|
758
|
+
}
|
|
759
|
+
/** `GET /v2/stockx/listings/upload/{taskId}` */
|
|
760
|
+
uploadStatus(taskId, options = {}) {
|
|
761
|
+
return this._get(`/v2/stockx/listings/upload/${pathSegment(taskId)}`, void 0, options);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Poll {@link uploadStatus} until the job is SUCCESS, FAILURE or CANCELLED,
|
|
765
|
+
* and return the final status. Throws `JobTimeoutError` after `timeout` ms.
|
|
766
|
+
*/
|
|
767
|
+
waitForUpload(taskId, options = {}) {
|
|
768
|
+
return this._wait((o) => this.uploadStatus(taskId, o), taskId, options);
|
|
769
|
+
}
|
|
770
|
+
/** `GET /v2/stockx/listings/upload/unmatched` — uploaded rows that could not be matched to a variant. */
|
|
771
|
+
unmatched(options = {}) {
|
|
772
|
+
return this._get("/v2/stockx/listings/upload/unmatched", void 0, options);
|
|
773
|
+
}
|
|
774
|
+
// -------------------------------------------------------------- CSV unlist
|
|
775
|
+
/**
|
|
776
|
+
* `POST /v2/stockx/listings/unlist` — start a bulk delist job.
|
|
777
|
+
*
|
|
778
|
+
* Rows carry `listing_id` (optionally `variant_id`), or `sku` (or `style_id`)
|
|
779
|
+
* plus `size` to match ACTIVE listings. Advanced plan.
|
|
780
|
+
*/
|
|
781
|
+
async unlist(source, options = {}) {
|
|
782
|
+
const file = await prepareCsvUpload(source, "unlist.csv");
|
|
783
|
+
return this._send("POST", "/v2/stockx/listings/unlist", { file }, options);
|
|
784
|
+
}
|
|
785
|
+
/** `GET /v2/stockx/listings/unlist/{taskId}` */
|
|
786
|
+
unlistStatus(taskId, options = {}) {
|
|
787
|
+
return this._get(`/v2/stockx/listings/unlist/${pathSegment(taskId)}`, void 0, options);
|
|
788
|
+
}
|
|
789
|
+
/** Poll {@link unlistStatus} until the job finishes. */
|
|
790
|
+
waitForUnlist(taskId, options = {}) {
|
|
791
|
+
return this._wait((o) => this.unlistStatus(taskId, o), taskId, options);
|
|
792
|
+
}
|
|
793
|
+
/** `POST /v2/stockx/listings/unlist/cancel` — cancel your running unlist job. */
|
|
794
|
+
cancelUnlist(options = {}) {
|
|
795
|
+
return this._send("POST", "/v2/stockx/listings/unlist/cancel", {}, options);
|
|
796
|
+
}
|
|
797
|
+
// -------------------------------------------------------------- clear all
|
|
798
|
+
/**
|
|
799
|
+
* `POST /v2/stockx/listings/clear-all` — delete EVERY active listing in the
|
|
800
|
+
* account's `clear_all_scope`. Cannot be undone. Advanced plan.
|
|
801
|
+
*/
|
|
802
|
+
clearAll(options = {}) {
|
|
803
|
+
return this._send("POST", "/v2/stockx/listings/clear-all", {}, options);
|
|
804
|
+
}
|
|
805
|
+
/** `GET /v2/stockx/listings/clear-all/{taskId}` */
|
|
806
|
+
clearAllStatus(taskId, options = {}) {
|
|
807
|
+
return this._get(`/v2/stockx/listings/clear-all/${pathSegment(taskId)}`, void 0, options);
|
|
808
|
+
}
|
|
809
|
+
/** Poll {@link clearAllStatus} until the job finishes. */
|
|
810
|
+
waitForClearAll(taskId, options = {}) {
|
|
811
|
+
return this._wait((o) => this.clearAllStatus(taskId, o), taskId, options);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/resources/stockx/market.ts
|
|
816
|
+
var StockXMarketResource = class extends APIResource {
|
|
817
|
+
/**
|
|
818
|
+
* `GET /v2/stockx/market/search` — lowest ask / highest bid per size.
|
|
819
|
+
*
|
|
820
|
+
* Served from cache when fresh; `force: true` skips the cache and spends
|
|
821
|
+
* StockX quota. Check `fetched_at` for freshness.
|
|
822
|
+
*/
|
|
823
|
+
search(styleId, params = {}) {
|
|
824
|
+
return this._get(
|
|
825
|
+
"/v2/stockx/market/search",
|
|
826
|
+
{
|
|
827
|
+
style_id: styleId,
|
|
828
|
+
size: params.size === void 0 || params.size === null ? void 0 : formatSize(params.size),
|
|
829
|
+
force: params.force
|
|
830
|
+
},
|
|
831
|
+
params
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
/** `GET /v2/stockx/market/hot` — style codes with recently cached quotes. */
|
|
835
|
+
hot(params = {}) {
|
|
836
|
+
return this._get("/v2/stockx/market/hot", { limit: params.limit, page: params.page }, params);
|
|
837
|
+
}
|
|
838
|
+
/** Iterate every recently quoted style code across pages. */
|
|
839
|
+
async *iterHot(params = {}) {
|
|
840
|
+
const { pageSize = 100, ...options } = params;
|
|
841
|
+
for (let page = 1; ; page++) {
|
|
842
|
+
const data = await this.hot({ ...options, limit: pageSize, page });
|
|
843
|
+
yield* data.items ?? [];
|
|
844
|
+
if (!data.has_next) return;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* `GET /v2/stockx/catalog/products/{productId}/variants/{variantId}/market-data`
|
|
849
|
+
*
|
|
850
|
+
* Live quote for one size. Always calls StockX. Returns StockX's market data
|
|
851
|
+
* object as received.
|
|
852
|
+
*/
|
|
853
|
+
variant(productId, variantId, params = {}) {
|
|
854
|
+
return this._get(
|
|
855
|
+
`/v2/stockx/catalog/products/${pathSegment(productId)}/variants/${pathSegment(variantId)}/market-data`,
|
|
856
|
+
{ currency_code: params.currencyCode, connection_id: this._conn(params.connectionId) },
|
|
857
|
+
params
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// src/resources/stockx/orders.ts
|
|
863
|
+
var date = (value) => value == null ? void 0 : formatDate(value);
|
|
864
|
+
var StockXOrdersResource = class extends APIResource {
|
|
865
|
+
/**
|
|
866
|
+
* `POST /v2/stockx/orders/sync` — start a background sync from StockX. There
|
|
867
|
+
* is no status endpoint; query again later.
|
|
868
|
+
*/
|
|
869
|
+
sync(options = {}) {
|
|
870
|
+
return this._send("POST", "/v2/stockx/orders/sync", {}, options);
|
|
871
|
+
}
|
|
872
|
+
/** `GET /v2/stockx/orders` — synced orders, newest first. */
|
|
873
|
+
list(params = {}) {
|
|
874
|
+
return this._get(
|
|
875
|
+
"/v2/stockx/orders",
|
|
876
|
+
{
|
|
877
|
+
page: params.page,
|
|
878
|
+
page_size: params.pageSize,
|
|
879
|
+
order_status: params.orderStatus,
|
|
880
|
+
from_date: date(params.fromDate),
|
|
881
|
+
to_date: date(params.toDate),
|
|
882
|
+
search: params.search
|
|
883
|
+
},
|
|
884
|
+
params
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
/** Iterate every matching order across pages. */
|
|
888
|
+
async *iterList(params = {}) {
|
|
889
|
+
const { pageSize = 200, ...rest } = params;
|
|
890
|
+
let seen = 0;
|
|
891
|
+
for (let page = 1; ; page++) {
|
|
892
|
+
const data = await this.list({ ...rest, page, pageSize });
|
|
893
|
+
const orders = data.orders ?? [];
|
|
894
|
+
yield* orders;
|
|
895
|
+
seen += orders.length;
|
|
896
|
+
if (orders.length === 0 || seen >= Number(data.total ?? 0)) return;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* `GET /v2/stockx/orders/analytics` — summary, top products/brands, sales
|
|
901
|
+
* trend and status breakdown.
|
|
902
|
+
*/
|
|
903
|
+
analytics(params = {}) {
|
|
904
|
+
return this._get(
|
|
905
|
+
"/v2/stockx/orders/analytics",
|
|
906
|
+
{ from_date: date(params.fromDate), to_date: date(params.toDate), granularity: params.granularity },
|
|
907
|
+
params
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/resources/stockx/index.ts
|
|
913
|
+
var StockXResource = class extends APIResource {
|
|
914
|
+
market;
|
|
915
|
+
catalog;
|
|
916
|
+
listings;
|
|
917
|
+
costs;
|
|
918
|
+
orders;
|
|
919
|
+
constructor(client) {
|
|
920
|
+
super(client);
|
|
921
|
+
this.market = new StockXMarketResource(client);
|
|
922
|
+
this.catalog = new StockXCatalogResource(client);
|
|
923
|
+
this.listings = new StockXListingsResource(client);
|
|
924
|
+
this.costs = new StockXCostsResource(client);
|
|
925
|
+
this.orders = new StockXOrdersResource(client);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/version.ts
|
|
930
|
+
var VERSION = "0.1.0";
|
|
931
|
+
|
|
932
|
+
// src/client.ts
|
|
933
|
+
var MAX_TIMER_MS = 2 ** 31 - 1;
|
|
934
|
+
var Client = class _Client {
|
|
935
|
+
baseURL;
|
|
936
|
+
timeout;
|
|
937
|
+
maxRetries;
|
|
938
|
+
connectionId;
|
|
939
|
+
inventory;
|
|
940
|
+
stockx;
|
|
941
|
+
goat;
|
|
942
|
+
#apiKey;
|
|
943
|
+
#customHeaders;
|
|
944
|
+
#fetch;
|
|
945
|
+
constructor(apiKeyOrOptions = {}, maybeOptions = {}) {
|
|
946
|
+
const options = typeof apiKeyOrOptions === "string" ? { ...maybeOptions, apiKey: apiKeyOrOptions } : apiKeyOrOptions;
|
|
947
|
+
const apiKey = options.apiKey || readEnv(API_KEY_ENV);
|
|
948
|
+
if (!apiKey) {
|
|
949
|
+
throw new SolometricsError(
|
|
950
|
+
`No API key provided. Pass { apiKey: '...' } or set the ${API_KEY_ENV} environment variable.`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
this.#apiKey = apiKey;
|
|
954
|
+
this.baseURL = (options.baseURL || readEnv(BASE_URL_ENV) || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
955
|
+
this.timeout = options.timeout === void 0 ? DEFAULT_TIMEOUT : options.timeout;
|
|
956
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
957
|
+
this.connectionId = options.connectionId ?? null;
|
|
958
|
+
this.#customHeaders = { ...options.defaultHeaders };
|
|
959
|
+
if (options.fetch) {
|
|
960
|
+
this.#fetch = options.fetch;
|
|
961
|
+
} else if (typeof globalThis.fetch === "function") {
|
|
962
|
+
this.#fetch = (url, init) => globalThis.fetch(url, init);
|
|
963
|
+
} else {
|
|
964
|
+
throw new SolometricsError("`fetch` is not available; use Node.js 18+ or pass { fetch } in the client options.");
|
|
965
|
+
}
|
|
966
|
+
this.inventory = new InventoryResource(this);
|
|
967
|
+
this.stockx = new StockXResource(this);
|
|
968
|
+
this.goat = new GoatResource(this);
|
|
969
|
+
}
|
|
970
|
+
get apiKey() {
|
|
971
|
+
return this.#apiKey;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Return a copy with some options changed. The copy shares `fetch` and the
|
|
975
|
+
* default headers. Pass `connectionId: null` to clear the default connection.
|
|
976
|
+
*/
|
|
977
|
+
withOptions(options) {
|
|
978
|
+
return new _Client({
|
|
979
|
+
apiKey: options.apiKey || this.#apiKey,
|
|
980
|
+
baseURL: this.baseURL,
|
|
981
|
+
timeout: options.timeout === void 0 ? this.timeout : options.timeout,
|
|
982
|
+
maxRetries: options.maxRetries ?? this.maxRetries,
|
|
983
|
+
connectionId: options.connectionId === void 0 ? this.connectionId : options.connectionId,
|
|
984
|
+
defaultHeaders: this.#customHeaders,
|
|
985
|
+
fetch: this.#fetch
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
// ------------------------------------------------------------------ low-level requests
|
|
989
|
+
/** Low-level GET for endpoints the SDK does not wrap yet. */
|
|
990
|
+
get(path, options = {}) {
|
|
991
|
+
return this._request({ ...options, method: "GET", path });
|
|
992
|
+
}
|
|
993
|
+
post(path, options = {}) {
|
|
994
|
+
return this._request({ ...options, method: "POST", path });
|
|
995
|
+
}
|
|
996
|
+
put(path, options = {}) {
|
|
997
|
+
return this._request({ ...options, method: "PUT", path });
|
|
998
|
+
}
|
|
999
|
+
patch(path, options = {}) {
|
|
1000
|
+
return this._request({ ...options, method: "PATCH", path });
|
|
1001
|
+
}
|
|
1002
|
+
delete(path, options = {}) {
|
|
1003
|
+
return this._request({ ...options, method: "DELETE", path });
|
|
1004
|
+
}
|
|
1005
|
+
/** @internal */
|
|
1006
|
+
async _request(options) {
|
|
1007
|
+
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
1008
|
+
const timeout = options.timeout === void 0 ? this.timeout : options.timeout;
|
|
1009
|
+
const url = buildURL(this.baseURL, options.path, options.query);
|
|
1010
|
+
const userSignal = options.signal ?? void 0;
|
|
1011
|
+
for (let attempt = 0; ; attempt++) {
|
|
1012
|
+
if (userSignal?.aborted) throw new APIUserAbortError({ cause: userSignal.reason });
|
|
1013
|
+
const controller = new AbortController();
|
|
1014
|
+
let timedOut = false;
|
|
1015
|
+
const timer = timeout !== null && Number.isFinite(timeout) ? setTimeout(
|
|
1016
|
+
() => {
|
|
1017
|
+
timedOut = true;
|
|
1018
|
+
controller.abort();
|
|
1019
|
+
},
|
|
1020
|
+
Math.min(Math.max(timeout, 0), MAX_TIMER_MS)
|
|
1021
|
+
) : void 0;
|
|
1022
|
+
const onUserAbort = () => controller.abort(userSignal?.reason);
|
|
1023
|
+
userSignal?.addEventListener("abort", onUserAbort, { once: true });
|
|
1024
|
+
let response;
|
|
1025
|
+
let text;
|
|
1026
|
+
try {
|
|
1027
|
+
response = await this.#fetch(url, {
|
|
1028
|
+
method: options.method,
|
|
1029
|
+
headers: this.#headers(options),
|
|
1030
|
+
body: this.#body(options),
|
|
1031
|
+
signal: controller.signal
|
|
1032
|
+
});
|
|
1033
|
+
text = await response.text();
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
if (userSignal?.aborted) throw new APIUserAbortError({ cause: err });
|
|
1036
|
+
const kind = timedOut ? "timeout" : classifyNetworkError(err);
|
|
1037
|
+
if (attempt < maxRetries && shouldRetryNetworkError(options.method, kind)) {
|
|
1038
|
+
await timing.sleep(retryDelay(attempt, null), userSignal);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (kind === "timeout") throw new APITimeoutError({ cause: err });
|
|
1042
|
+
throw new APIConnectionError(connectionErrorMessage(err), { cause: err });
|
|
1043
|
+
} finally {
|
|
1044
|
+
clearTimeout(timer);
|
|
1045
|
+
userSignal?.removeEventListener("abort", onUserAbort);
|
|
1046
|
+
}
|
|
1047
|
+
if (response.ok) return parseBody(response.status, text);
|
|
1048
|
+
if (attempt < maxRetries && shouldRetryStatus(options.method, response.status)) {
|
|
1049
|
+
const retryAfter = parseRetryAfter(response.headers);
|
|
1050
|
+
await timing.sleep(retryDelay(attempt, retryAfter), userSignal);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
throw makeStatusError(response.status, response.statusText, response.headers, text);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
#headers(options) {
|
|
1057
|
+
const headers = {
|
|
1058
|
+
"X-API-Key": this.#apiKey,
|
|
1059
|
+
Accept: "application/json",
|
|
1060
|
+
"User-Agent": `solometrics-node/${VERSION}`
|
|
1061
|
+
};
|
|
1062
|
+
if (options.body !== void 0 && !options.file) headers["Content-Type"] = "application/json";
|
|
1063
|
+
return { ...headers, ...this.#customHeaders, ...options.headers };
|
|
1064
|
+
}
|
|
1065
|
+
/** Built per attempt, so a multipart body can be resent on retry. */
|
|
1066
|
+
#body(options) {
|
|
1067
|
+
if (options.file) {
|
|
1068
|
+
const form = new FormData();
|
|
1069
|
+
form.append(
|
|
1070
|
+
"file",
|
|
1071
|
+
new Blob([options.file.content], { type: "text/csv" }),
|
|
1072
|
+
options.file.name
|
|
1073
|
+
);
|
|
1074
|
+
return form;
|
|
1075
|
+
}
|
|
1076
|
+
return options.body === void 0 ? void 0 : JSON.stringify(options.body);
|
|
1077
|
+
}
|
|
1078
|
+
toString() {
|
|
1079
|
+
return `Client { baseURL: '${this.baseURL}' }`;
|
|
1080
|
+
}
|
|
1081
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
1082
|
+
return this.toString();
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
function parseBody(status, text) {
|
|
1086
|
+
if (status === 204 || text === "") return null;
|
|
1087
|
+
try {
|
|
1088
|
+
return JSON.parse(text);
|
|
1089
|
+
} catch {
|
|
1090
|
+
return text;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function connectionErrorMessage(err) {
|
|
1094
|
+
if (err instanceof Error) {
|
|
1095
|
+
const cause = err.cause instanceof Error ? `: ${err.cause.message}` : "";
|
|
1096
|
+
return `${err.message}${cause}` || "Connection error.";
|
|
1097
|
+
}
|
|
1098
|
+
return "Connection error.";
|
|
1099
|
+
}
|
|
1100
|
+
export {
|
|
1101
|
+
APIConnectionError,
|
|
1102
|
+
APIError,
|
|
1103
|
+
APIStatusError,
|
|
1104
|
+
APITimeoutError,
|
|
1105
|
+
APIUserAbortError,
|
|
1106
|
+
AuthenticationError,
|
|
1107
|
+
BadRequestError,
|
|
1108
|
+
Client,
|
|
1109
|
+
ConflictError,
|
|
1110
|
+
GoatMarketResource,
|
|
1111
|
+
GoatResource,
|
|
1112
|
+
InternalServerError,
|
|
1113
|
+
InventoryResource,
|
|
1114
|
+
JobTimeoutError,
|
|
1115
|
+
NotFoundError,
|
|
1116
|
+
PermissionDeniedError,
|
|
1117
|
+
RateLimitError,
|
|
1118
|
+
SolometricsError,
|
|
1119
|
+
StockXCatalogResource,
|
|
1120
|
+
StockXCostsResource,
|
|
1121
|
+
StockXListingsResource,
|
|
1122
|
+
StockXMarketResource,
|
|
1123
|
+
StockXOrdersResource,
|
|
1124
|
+
StockXResource,
|
|
1125
|
+
UnprocessableEntityError,
|
|
1126
|
+
UpstreamError,
|
|
1127
|
+
UpstreamTimeoutError,
|
|
1128
|
+
VERSION
|
|
1129
|
+
};
|
|
1130
|
+
//# sourceMappingURL=index.js.map
|