@profplum700/etsy-v3-api-client 2.4.0 → 2.4.2
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 +121 -13
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.esm.js +71 -2
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +1 -1
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +71 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.esm.js +71 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/node.cjs +71 -2
- package/dist/node.cjs.map +1 -1
- package/dist/node.esm.js +71 -2
- package/dist/node.esm.js.map +1 -1
- package/package.json +33 -35
- package/dist/index.js +0 -8517
- package/dist/index.js.map +0 -1
- package/dist/index.umd.js +0 -1180
- package/dist/index.umd.js.map +0 -1
- package/dist/index.umd.min.js +0 -2
- package/dist/index.umd.min.js.map +0 -1
package/dist/index.umd.js
DELETED
|
@@ -1,1180 +0,0 @@
|
|
|
1
|
-
(function (global, factory) {
|
|
2
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('crypto')) :
|
|
3
|
-
typeof define === 'function' && define.amd ? define(['exports', 'crypto'], factory) :
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.EtsyV3ApiClient = {}, global.crypto));
|
|
5
|
-
})(this, (function (exports, crypto) { 'use strict';
|
|
6
|
-
|
|
7
|
-
const ETSY_WHEN_MADE_VALUES = [
|
|
8
|
-
'1990s',
|
|
9
|
-
'1980s',
|
|
10
|
-
'1970s',
|
|
11
|
-
'1960s',
|
|
12
|
-
'1950s',
|
|
13
|
-
'1940s',
|
|
14
|
-
'1930s',
|
|
15
|
-
'1920s',
|
|
16
|
-
'1910s',
|
|
17
|
-
'1900s',
|
|
18
|
-
'1800s',
|
|
19
|
-
'1700s',
|
|
20
|
-
'before_1700'
|
|
21
|
-
];
|
|
22
|
-
class EtsyApiError extends Error {
|
|
23
|
-
constructor(message, statusCode, response) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.statusCode = statusCode;
|
|
26
|
-
this.response = response;
|
|
27
|
-
this.name = 'EtsyApiError';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
class EtsyAuthError extends Error {
|
|
31
|
-
constructor(message, code) {
|
|
32
|
-
super(message);
|
|
33
|
-
this.code = code;
|
|
34
|
-
this.name = 'EtsyAuthError';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
class EtsyRateLimitError extends Error {
|
|
38
|
-
constructor(message, retryAfter) {
|
|
39
|
-
super(message);
|
|
40
|
-
this.retryAfter = retryAfter;
|
|
41
|
-
this.name = 'EtsyRateLimitError';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
class MemoryTokenStorage {
|
|
46
|
-
constructor() {
|
|
47
|
-
this.tokens = null;
|
|
48
|
-
}
|
|
49
|
-
async save(tokens) {
|
|
50
|
-
this.tokens = { ...tokens };
|
|
51
|
-
}
|
|
52
|
-
async load() {
|
|
53
|
-
return this.tokens ? { ...this.tokens } : null;
|
|
54
|
-
}
|
|
55
|
-
async clear() {
|
|
56
|
-
this.tokens = null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
class TokenManager {
|
|
60
|
-
constructor(config, storage) {
|
|
61
|
-
this.currentTokens = null;
|
|
62
|
-
this.keystring = config.keystring;
|
|
63
|
-
this.refreshCallback = config.refreshSave;
|
|
64
|
-
this.storage = storage;
|
|
65
|
-
this.currentTokens = {
|
|
66
|
-
access_token: config.accessToken,
|
|
67
|
-
refresh_token: config.refreshToken,
|
|
68
|
-
expires_at: config.expiresAt,
|
|
69
|
-
token_type: 'Bearer',
|
|
70
|
-
scope: ''
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
async getAccessToken() {
|
|
74
|
-
if (!this.currentTokens) {
|
|
75
|
-
if (this.storage) {
|
|
76
|
-
this.currentTokens = await this.storage.load();
|
|
77
|
-
}
|
|
78
|
-
if (!this.currentTokens) {
|
|
79
|
-
throw new EtsyAuthError('No tokens available', 'NO_TOKENS');
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const now = new Date();
|
|
83
|
-
const expiresAt = new Date(this.currentTokens.expires_at);
|
|
84
|
-
const bufferTime = 60 * 1000;
|
|
85
|
-
if (now.getTime() >= (expiresAt.getTime() - bufferTime)) {
|
|
86
|
-
await this.refreshToken();
|
|
87
|
-
}
|
|
88
|
-
return this.currentTokens.access_token;
|
|
89
|
-
}
|
|
90
|
-
async refreshToken() {
|
|
91
|
-
if (this.refreshPromise) {
|
|
92
|
-
return this.refreshPromise;
|
|
93
|
-
}
|
|
94
|
-
if (!this.currentTokens) {
|
|
95
|
-
throw new EtsyAuthError('No tokens available to refresh', 'NO_REFRESH_TOKEN');
|
|
96
|
-
}
|
|
97
|
-
this.refreshPromise = this.performTokenRefresh();
|
|
98
|
-
try {
|
|
99
|
-
const newTokens = await this.refreshPromise;
|
|
100
|
-
this.currentTokens = newTokens;
|
|
101
|
-
if (this.storage) {
|
|
102
|
-
await this.storage.save(newTokens);
|
|
103
|
-
}
|
|
104
|
-
if (this.refreshCallback) {
|
|
105
|
-
this.refreshCallback(newTokens.access_token, newTokens.refresh_token, newTokens.expires_at);
|
|
106
|
-
}
|
|
107
|
-
return newTokens;
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
this.refreshPromise = undefined;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
async performTokenRefresh() {
|
|
114
|
-
if (!this.currentTokens) {
|
|
115
|
-
throw new EtsyAuthError('No tokens available', 'NO_TOKENS');
|
|
116
|
-
}
|
|
117
|
-
const body = new URLSearchParams({
|
|
118
|
-
grant_type: 'refresh_token',
|
|
119
|
-
client_id: this.keystring,
|
|
120
|
-
refresh_token: this.currentTokens.refresh_token
|
|
121
|
-
});
|
|
122
|
-
try {
|
|
123
|
-
const response = await this.fetch('https://api.etsy.com/v3/public/oauth/token', {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: {
|
|
126
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
127
|
-
'Accept': 'application/json'
|
|
128
|
-
},
|
|
129
|
-
body: body.toString()
|
|
130
|
-
});
|
|
131
|
-
if (!response.ok) {
|
|
132
|
-
const errorText = await response.text();
|
|
133
|
-
throw new EtsyAuthError(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`, 'TOKEN_REFRESH_FAILED');
|
|
134
|
-
}
|
|
135
|
-
const tokenResponse = await response.json();
|
|
136
|
-
return {
|
|
137
|
-
access_token: tokenResponse.access_token,
|
|
138
|
-
refresh_token: tokenResponse.refresh_token,
|
|
139
|
-
expires_at: new Date(Date.now() + (tokenResponse.expires_in * 1000)),
|
|
140
|
-
token_type: tokenResponse.token_type,
|
|
141
|
-
scope: tokenResponse.scope
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
if (error instanceof EtsyAuthError) {
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
throw new EtsyAuthError(`Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'TOKEN_REFRESH_ERROR');
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
getCurrentTokens() {
|
|
152
|
-
return this.currentTokens ? { ...this.currentTokens } : null;
|
|
153
|
-
}
|
|
154
|
-
updateTokens(tokens) {
|
|
155
|
-
this.currentTokens = { ...tokens };
|
|
156
|
-
}
|
|
157
|
-
isTokenExpired() {
|
|
158
|
-
if (!this.currentTokens) {
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
const now = new Date();
|
|
162
|
-
const expiresAt = new Date(this.currentTokens.expires_at);
|
|
163
|
-
return now.getTime() >= expiresAt.getTime();
|
|
164
|
-
}
|
|
165
|
-
willTokenExpireSoon(minutes = 5) {
|
|
166
|
-
if (!this.currentTokens) {
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
const now = new Date();
|
|
170
|
-
const expiresAt = new Date(this.currentTokens.expires_at);
|
|
171
|
-
const bufferTime = minutes * 60 * 1000;
|
|
172
|
-
return now.getTime() >= (expiresAt.getTime() - bufferTime);
|
|
173
|
-
}
|
|
174
|
-
async clearTokens() {
|
|
175
|
-
this.currentTokens = null;
|
|
176
|
-
if (this.storage) {
|
|
177
|
-
await this.storage.clear();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
getTimeUntilExpiration() {
|
|
181
|
-
if (!this.currentTokens) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
const now = new Date();
|
|
185
|
-
const expiresAt = new Date(this.currentTokens.expires_at);
|
|
186
|
-
return expiresAt.getTime() - now.getTime();
|
|
187
|
-
}
|
|
188
|
-
async fetch(url, options) {
|
|
189
|
-
if (typeof globalThis.fetch !== 'undefined') {
|
|
190
|
-
return globalThis.fetch(url, options);
|
|
191
|
-
}
|
|
192
|
-
try {
|
|
193
|
-
const { default: fetch } = await import('node-fetch');
|
|
194
|
-
return fetch(url, options);
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
throw new EtsyAuthError('Fetch is not available. Please provide a fetch implementation or use Node.js 18+ or a modern browser.', 'FETCH_NOT_AVAILABLE');
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
class FileTokenStorage {
|
|
202
|
-
constructor(filePath) {
|
|
203
|
-
this.filePath = filePath;
|
|
204
|
-
}
|
|
205
|
-
async save(tokens) {
|
|
206
|
-
const fs = await import('fs/promises');
|
|
207
|
-
const data = JSON.stringify(tokens, null, 2);
|
|
208
|
-
await fs.writeFile(this.filePath, data, 'utf8');
|
|
209
|
-
}
|
|
210
|
-
async load() {
|
|
211
|
-
try {
|
|
212
|
-
const fs = await import('fs/promises');
|
|
213
|
-
const data = await fs.readFile(this.filePath, 'utf8');
|
|
214
|
-
const tokens = JSON.parse(data);
|
|
215
|
-
if (tokens.expires_at) {
|
|
216
|
-
tokens.expires_at = new Date(tokens.expires_at);
|
|
217
|
-
}
|
|
218
|
-
return tokens;
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
async clear() {
|
|
225
|
-
try {
|
|
226
|
-
const fs = await import('fs/promises');
|
|
227
|
-
await fs.unlink(this.filePath);
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
class EtsyRateLimiter {
|
|
235
|
-
constructor(config) {
|
|
236
|
-
this.requestCount = 0;
|
|
237
|
-
this.dailyReset = new Date();
|
|
238
|
-
this.lastRequestTime = 0;
|
|
239
|
-
this.config = {
|
|
240
|
-
maxRequestsPerDay: 10000,
|
|
241
|
-
maxRequestsPerSecond: 10,
|
|
242
|
-
minRequestInterval: 100,
|
|
243
|
-
...config
|
|
244
|
-
};
|
|
245
|
-
this.dailyReset.setUTCHours(24, 0, 0, 0);
|
|
246
|
-
}
|
|
247
|
-
async waitForRateLimit() {
|
|
248
|
-
const now = Date.now();
|
|
249
|
-
if (now > this.dailyReset.getTime()) {
|
|
250
|
-
this.requestCount = 0;
|
|
251
|
-
this.dailyReset = new Date(now);
|
|
252
|
-
this.dailyReset.setUTCHours(24, 0, 0, 0);
|
|
253
|
-
}
|
|
254
|
-
if (this.requestCount >= this.config.maxRequestsPerDay) {
|
|
255
|
-
const timeUntilReset = this.dailyReset.getTime() - now;
|
|
256
|
-
throw new EtsyRateLimitError(`Daily rate limit of ${this.config.maxRequestsPerDay} requests exceeded. Reset in ${Math.ceil(timeUntilReset / 1000 / 60)} minutes.`, timeUntilReset);
|
|
257
|
-
}
|
|
258
|
-
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
259
|
-
if (timeSinceLastRequest < this.config.minRequestInterval) {
|
|
260
|
-
const waitTime = this.config.minRequestInterval - timeSinceLastRequest;
|
|
261
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
262
|
-
}
|
|
263
|
-
this.requestCount++;
|
|
264
|
-
this.lastRequestTime = Date.now();
|
|
265
|
-
}
|
|
266
|
-
getRateLimitStatus() {
|
|
267
|
-
const now = Date.now();
|
|
268
|
-
this.dailyReset.getTime() - now;
|
|
269
|
-
return {
|
|
270
|
-
remainingRequests: Math.max(0, this.config.maxRequestsPerDay - this.requestCount),
|
|
271
|
-
resetTime: this.dailyReset,
|
|
272
|
-
canMakeRequest: this.requestCount < this.config.maxRequestsPerDay &&
|
|
273
|
-
(now - this.lastRequestTime) >= this.config.minRequestInterval
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
getRemainingRequests() {
|
|
277
|
-
return Math.max(0, this.config.maxRequestsPerDay - this.requestCount);
|
|
278
|
-
}
|
|
279
|
-
reset() {
|
|
280
|
-
this.requestCount = 0;
|
|
281
|
-
this.lastRequestTime = 0;
|
|
282
|
-
this.dailyReset = new Date();
|
|
283
|
-
this.dailyReset.setUTCHours(24, 0, 0, 0);
|
|
284
|
-
}
|
|
285
|
-
canMakeRequest() {
|
|
286
|
-
const now = Date.now();
|
|
287
|
-
if (this.requestCount >= this.config.maxRequestsPerDay) {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
return (now - this.lastRequestTime) >= this.config.minRequestInterval;
|
|
291
|
-
}
|
|
292
|
-
getTimeUntilNextRequest() {
|
|
293
|
-
const now = Date.now();
|
|
294
|
-
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
295
|
-
if (timeSinceLastRequest >= this.config.minRequestInterval) {
|
|
296
|
-
return 0;
|
|
297
|
-
}
|
|
298
|
-
return this.config.minRequestInterval - timeSinceLastRequest;
|
|
299
|
-
}
|
|
300
|
-
getConfig() {
|
|
301
|
-
return { ...this.config };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const defaultRateLimiter = new EtsyRateLimiter();
|
|
305
|
-
|
|
306
|
-
class DefaultLogger {
|
|
307
|
-
debug(message, ...args) {
|
|
308
|
-
if (process.env.NODE_ENV === 'development') {
|
|
309
|
-
console.log(`[DEBUG] ${message}`, ...args);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
info(message, ...args) {
|
|
313
|
-
console.log(`[INFO] ${message}`, ...args);
|
|
314
|
-
}
|
|
315
|
-
warn(message, ...args) {
|
|
316
|
-
console.warn(`[WARN] ${message}`, ...args);
|
|
317
|
-
}
|
|
318
|
-
error(message, ...args) {
|
|
319
|
-
console.error(`[ERROR] ${message}`, ...args);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
class MemoryCache {
|
|
323
|
-
constructor() {
|
|
324
|
-
this.cache = new Map();
|
|
325
|
-
}
|
|
326
|
-
async get(key) {
|
|
327
|
-
const entry = this.cache.get(key);
|
|
328
|
-
if (!entry)
|
|
329
|
-
return null;
|
|
330
|
-
if (Date.now() > entry.expires) {
|
|
331
|
-
this.cache.delete(key);
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
return entry.value;
|
|
335
|
-
}
|
|
336
|
-
async set(key, value, ttl = 3600) {
|
|
337
|
-
const expires = Date.now() + (ttl * 1000);
|
|
338
|
-
this.cache.set(key, { value, expires });
|
|
339
|
-
}
|
|
340
|
-
async delete(key) {
|
|
341
|
-
this.cache.delete(key);
|
|
342
|
-
}
|
|
343
|
-
async clear() {
|
|
344
|
-
this.cache.clear();
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
class EtsyClient {
|
|
348
|
-
constructor(config) {
|
|
349
|
-
this.tokenManager = new TokenManager(config);
|
|
350
|
-
this.baseUrl = config.baseUrl || 'https://api.etsy.com/v3/application';
|
|
351
|
-
this.logger = new DefaultLogger();
|
|
352
|
-
this.keystring = config.keystring;
|
|
353
|
-
this.rateLimiter = new EtsyRateLimiter(config.rateLimiting?.enabled !== false ? {
|
|
354
|
-
maxRequestsPerDay: config.rateLimiting?.maxRequestsPerDay || 10000,
|
|
355
|
-
maxRequestsPerSecond: config.rateLimiting?.maxRequestsPerSecond || 10,
|
|
356
|
-
minRequestInterval: 100
|
|
357
|
-
} : undefined);
|
|
358
|
-
if (config.caching?.enabled !== false) {
|
|
359
|
-
this.cache = config.caching?.storage || new MemoryCache();
|
|
360
|
-
this.cacheTtl = config.caching?.ttl || 3600;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
async makeRequest(endpoint, options = {}, useCache = true) {
|
|
364
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
365
|
-
const cacheKey = `${url}:${JSON.stringify(options)}`;
|
|
366
|
-
if (useCache && this.cache && options.method === 'GET') {
|
|
367
|
-
const cached = await this.cache.get(cacheKey);
|
|
368
|
-
if (cached) {
|
|
369
|
-
return JSON.parse(cached);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
await this.rateLimiter.waitForRateLimit();
|
|
373
|
-
const accessToken = await this.tokenManager.getAccessToken();
|
|
374
|
-
const headers = {
|
|
375
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
376
|
-
'x-api-key': this.getApiKey(),
|
|
377
|
-
'Content-Type': 'application/json',
|
|
378
|
-
'Accept': 'application/json',
|
|
379
|
-
...options.headers
|
|
380
|
-
};
|
|
381
|
-
try {
|
|
382
|
-
const response = await this.fetch(url, {
|
|
383
|
-
...options,
|
|
384
|
-
headers
|
|
385
|
-
});
|
|
386
|
-
if (!response.ok) {
|
|
387
|
-
const errorText = await response.text();
|
|
388
|
-
throw new EtsyApiError(`Etsy API error: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
389
|
-
}
|
|
390
|
-
const data = await response.json();
|
|
391
|
-
if (useCache && this.cache && options.method === 'GET') {
|
|
392
|
-
await this.cache.set(cacheKey, JSON.stringify(data), this.cacheTtl);
|
|
393
|
-
}
|
|
394
|
-
return data;
|
|
395
|
-
}
|
|
396
|
-
catch (error) {
|
|
397
|
-
if (error instanceof EtsyApiError) {
|
|
398
|
-
throw error;
|
|
399
|
-
}
|
|
400
|
-
throw new EtsyApiError(`Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 0, error);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
getApiKey() {
|
|
404
|
-
return this.keystring;
|
|
405
|
-
}
|
|
406
|
-
async getUser() {
|
|
407
|
-
return this.makeRequest('/users/me');
|
|
408
|
-
}
|
|
409
|
-
async getShop(shopId) {
|
|
410
|
-
if (shopId) {
|
|
411
|
-
return this.makeRequest(`/shops/${shopId}`);
|
|
412
|
-
}
|
|
413
|
-
const user = await this.getUser();
|
|
414
|
-
if (!user.shop_id) {
|
|
415
|
-
throw new EtsyApiError('User does not have a shop', 404);
|
|
416
|
-
}
|
|
417
|
-
return this.makeRequest(`/shops/${user.shop_id}`);
|
|
418
|
-
}
|
|
419
|
-
async getShopByOwnerUserId(userId) {
|
|
420
|
-
return this.makeRequest(`/users/${userId}/shops`);
|
|
421
|
-
}
|
|
422
|
-
async getShopSections(shopId) {
|
|
423
|
-
let targetShopId = shopId;
|
|
424
|
-
if (!targetShopId) {
|
|
425
|
-
const user = await this.getUser();
|
|
426
|
-
if (!user.shop_id) {
|
|
427
|
-
throw new EtsyApiError('User does not have a shop', 404);
|
|
428
|
-
}
|
|
429
|
-
targetShopId = user.shop_id.toString();
|
|
430
|
-
}
|
|
431
|
-
const response = await this.makeRequest(`/shops/${targetShopId}/sections`);
|
|
432
|
-
this.logger.info(`Found ${response.results.length} shop sections`);
|
|
433
|
-
return response.results;
|
|
434
|
-
}
|
|
435
|
-
async getShopSection(shopId, sectionId) {
|
|
436
|
-
return this.makeRequest(`/shops/${shopId}/sections/${sectionId}`);
|
|
437
|
-
}
|
|
438
|
-
async getListingsByShop(shopId, params = {}) {
|
|
439
|
-
let targetShopId = shopId;
|
|
440
|
-
if (!targetShopId) {
|
|
441
|
-
const user = await this.getUser();
|
|
442
|
-
if (!user.shop_id) {
|
|
443
|
-
throw new EtsyApiError('User does not have a shop', 404);
|
|
444
|
-
}
|
|
445
|
-
targetShopId = user.shop_id.toString();
|
|
446
|
-
}
|
|
447
|
-
const searchParams = new URLSearchParams();
|
|
448
|
-
searchParams.set('state', params.state || 'active');
|
|
449
|
-
if (params.limit)
|
|
450
|
-
searchParams.set('limit', params.limit.toString());
|
|
451
|
-
if (params.offset)
|
|
452
|
-
searchParams.set('offset', params.offset.toString());
|
|
453
|
-
if (params.sort_on)
|
|
454
|
-
searchParams.set('sort_on', params.sort_on);
|
|
455
|
-
if (params.sort_order)
|
|
456
|
-
searchParams.set('sort_order', params.sort_order);
|
|
457
|
-
if (params.includes)
|
|
458
|
-
searchParams.set('includes', params.includes.join(','));
|
|
459
|
-
const response = await this.makeRequest(`/shops/${targetShopId}/listings?${searchParams.toString()}`);
|
|
460
|
-
return response.results;
|
|
461
|
-
}
|
|
462
|
-
async getListing(listingId, includes) {
|
|
463
|
-
const params = includes ? `?includes=${includes.join(',')}` : '';
|
|
464
|
-
return this.makeRequest(`/listings/${listingId}${params}`);
|
|
465
|
-
}
|
|
466
|
-
async findAllListingsActive(params = {}) {
|
|
467
|
-
const searchParams = new URLSearchParams();
|
|
468
|
-
if (params.keywords)
|
|
469
|
-
searchParams.set('keywords', params.keywords);
|
|
470
|
-
if (params.category)
|
|
471
|
-
searchParams.set('category', params.category);
|
|
472
|
-
if (params.limit)
|
|
473
|
-
searchParams.set('limit', params.limit.toString());
|
|
474
|
-
if (params.offset)
|
|
475
|
-
searchParams.set('offset', params.offset.toString());
|
|
476
|
-
if (params.sort_on)
|
|
477
|
-
searchParams.set('sort_on', params.sort_on);
|
|
478
|
-
if (params.sort_order)
|
|
479
|
-
searchParams.set('sort_order', params.sort_order);
|
|
480
|
-
if (params.min_price)
|
|
481
|
-
searchParams.set('min_price', params.min_price.toString());
|
|
482
|
-
if (params.max_price)
|
|
483
|
-
searchParams.set('max_price', params.max_price.toString());
|
|
484
|
-
if (params.tags)
|
|
485
|
-
searchParams.set('tags', params.tags.join(','));
|
|
486
|
-
if (params.location)
|
|
487
|
-
searchParams.set('location', params.location);
|
|
488
|
-
if (params.shop_location)
|
|
489
|
-
searchParams.set('shop_location', params.shop_location);
|
|
490
|
-
const response = await this.makeRequest(`/listings/active?${searchParams.toString()}`);
|
|
491
|
-
return response.results;
|
|
492
|
-
}
|
|
493
|
-
async getListingImages(listingId) {
|
|
494
|
-
const response = await this.makeRequest(`/listings/${listingId}/images`);
|
|
495
|
-
return response.results;
|
|
496
|
-
}
|
|
497
|
-
async getListingInventory(listingId) {
|
|
498
|
-
return this.makeRequest(`/listings/${listingId}/inventory`);
|
|
499
|
-
}
|
|
500
|
-
convertToEtsyProduct(listing) {
|
|
501
|
-
return {
|
|
502
|
-
listing_id: listing.listing_id.toString(),
|
|
503
|
-
title: listing.title,
|
|
504
|
-
description: listing.description,
|
|
505
|
-
price: listing.price.amount / listing.price.divisor,
|
|
506
|
-
url: listing.url,
|
|
507
|
-
images: listing.images?.map(img => img.url_570xN) || [],
|
|
508
|
-
when_made: listing.when_made,
|
|
509
|
-
shop_section_id: listing.shop_section_id?.toString(),
|
|
510
|
-
tags: listing.tags,
|
|
511
|
-
materials: listing.materials,
|
|
512
|
-
style: listing.style,
|
|
513
|
-
item_length: listing.item_length,
|
|
514
|
-
item_width: listing.item_width,
|
|
515
|
-
item_height: listing.item_height,
|
|
516
|
-
item_dimensions_unit: listing.item_dimensions_unit
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
async searchProducts(params) {
|
|
520
|
-
const listings = await this.findAllListingsActive(params);
|
|
521
|
-
return listings.map(listing => this.convertToEtsyProduct(listing));
|
|
522
|
-
}
|
|
523
|
-
getRemainingRequests() {
|
|
524
|
-
return this.rateLimiter.getRemainingRequests();
|
|
525
|
-
}
|
|
526
|
-
getRateLimitStatus() {
|
|
527
|
-
return this.rateLimiter.getRateLimitStatus();
|
|
528
|
-
}
|
|
529
|
-
async clearCache() {
|
|
530
|
-
if (this.cache) {
|
|
531
|
-
await this.cache.clear();
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
getCurrentTokens() {
|
|
535
|
-
return this.tokenManager.getCurrentTokens();
|
|
536
|
-
}
|
|
537
|
-
isTokenExpired() {
|
|
538
|
-
return this.tokenManager.isTokenExpired();
|
|
539
|
-
}
|
|
540
|
-
async refreshToken() {
|
|
541
|
-
return this.tokenManager.refreshToken();
|
|
542
|
-
}
|
|
543
|
-
async fetch(url, options) {
|
|
544
|
-
if (typeof globalThis.fetch !== 'undefined') {
|
|
545
|
-
return globalThis.fetch(url, options);
|
|
546
|
-
}
|
|
547
|
-
try {
|
|
548
|
-
const { default: fetch } = await import('node-fetch');
|
|
549
|
-
return fetch(url, options);
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
throw new EtsyAuthError('Fetch is not available. Please provide a fetch implementation or use Node.js 18+ or a modern browser.', 'FETCH_NOT_AVAILABLE');
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
class AuthHelper {
|
|
558
|
-
constructor(config) {
|
|
559
|
-
this.keystring = config.keystring;
|
|
560
|
-
this.redirectUri = config.redirectUri;
|
|
561
|
-
this.scopes = config.scopes;
|
|
562
|
-
this.codeVerifier = config.codeVerifier || this.generateCodeVerifier();
|
|
563
|
-
this.state = config.state || this.generateState();
|
|
564
|
-
}
|
|
565
|
-
generateCodeVerifier() {
|
|
566
|
-
return crypto.randomBytes(32).toString('base64url');
|
|
567
|
-
}
|
|
568
|
-
generateState() {
|
|
569
|
-
return crypto.randomBytes(32).toString('base64url');
|
|
570
|
-
}
|
|
571
|
-
createCodeChallenge(codeVerifier) {
|
|
572
|
-
return crypto.createHash('sha256')
|
|
573
|
-
.update(codeVerifier)
|
|
574
|
-
.digest('base64url');
|
|
575
|
-
}
|
|
576
|
-
getAuthUrl() {
|
|
577
|
-
const codeChallenge = this.createCodeChallenge(this.codeVerifier);
|
|
578
|
-
const params = new URLSearchParams({
|
|
579
|
-
response_type: 'code',
|
|
580
|
-
client_id: this.keystring,
|
|
581
|
-
redirect_uri: this.redirectUri,
|
|
582
|
-
scope: this.scopes.join(' '),
|
|
583
|
-
state: this.state,
|
|
584
|
-
code_challenge: codeChallenge,
|
|
585
|
-
code_challenge_method: 'S256'
|
|
586
|
-
});
|
|
587
|
-
return `https://www.etsy.com/oauth/connect?${params.toString()}`;
|
|
588
|
-
}
|
|
589
|
-
setAuthorizationCode(code, state) {
|
|
590
|
-
if (state !== this.state) {
|
|
591
|
-
throw new EtsyAuthError('State parameter mismatch', 'INVALID_STATE');
|
|
592
|
-
}
|
|
593
|
-
this.authorizationCode = code;
|
|
594
|
-
this.receivedState = state;
|
|
595
|
-
}
|
|
596
|
-
async getAccessToken() {
|
|
597
|
-
if (!this.authorizationCode) {
|
|
598
|
-
throw new EtsyAuthError('Authorization code not set. Call setAuthorizationCode() first.', 'NO_AUTH_CODE');
|
|
599
|
-
}
|
|
600
|
-
const body = new URLSearchParams({
|
|
601
|
-
grant_type: 'authorization_code',
|
|
602
|
-
client_id: this.keystring,
|
|
603
|
-
redirect_uri: this.redirectUri,
|
|
604
|
-
code: this.authorizationCode,
|
|
605
|
-
code_verifier: this.codeVerifier
|
|
606
|
-
});
|
|
607
|
-
try {
|
|
608
|
-
const response = await this.fetch('https://api.etsy.com/v3/public/oauth/token', {
|
|
609
|
-
method: 'POST',
|
|
610
|
-
headers: {
|
|
611
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
612
|
-
'Accept': 'application/json'
|
|
613
|
-
},
|
|
614
|
-
body: body.toString()
|
|
615
|
-
});
|
|
616
|
-
if (!response.ok) {
|
|
617
|
-
const errorText = await response.text();
|
|
618
|
-
throw new EtsyAuthError(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`, 'TOKEN_EXCHANGE_FAILED');
|
|
619
|
-
}
|
|
620
|
-
const tokenResponse = await response.json();
|
|
621
|
-
return {
|
|
622
|
-
access_token: tokenResponse.access_token,
|
|
623
|
-
refresh_token: tokenResponse.refresh_token,
|
|
624
|
-
expires_at: new Date(Date.now() + (tokenResponse.expires_in * 1000)),
|
|
625
|
-
token_type: tokenResponse.token_type,
|
|
626
|
-
scope: tokenResponse.scope
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
if (error instanceof EtsyAuthError) {
|
|
631
|
-
throw error;
|
|
632
|
-
}
|
|
633
|
-
throw new EtsyAuthError(`Token exchange failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'TOKEN_EXCHANGE_ERROR');
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
getState() {
|
|
637
|
-
return this.state;
|
|
638
|
-
}
|
|
639
|
-
getCodeVerifier() {
|
|
640
|
-
return this.codeVerifier;
|
|
641
|
-
}
|
|
642
|
-
getScopes() {
|
|
643
|
-
return [...this.scopes];
|
|
644
|
-
}
|
|
645
|
-
getRedirectUri() {
|
|
646
|
-
return this.redirectUri;
|
|
647
|
-
}
|
|
648
|
-
async fetch(url, options) {
|
|
649
|
-
if (typeof globalThis.fetch !== 'undefined') {
|
|
650
|
-
return globalThis.fetch(url, options);
|
|
651
|
-
}
|
|
652
|
-
try {
|
|
653
|
-
const { default: fetch } = await import('node-fetch');
|
|
654
|
-
return fetch(url, options);
|
|
655
|
-
}
|
|
656
|
-
catch (error) {
|
|
657
|
-
throw new EtsyAuthError('Fetch is not available. Please provide a fetch implementation or use Node.js 18+ or a modern browser.', 'FETCH_NOT_AVAILABLE');
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
const ETSY_SCOPES = {
|
|
662
|
-
LISTINGS_READ: 'listings_r',
|
|
663
|
-
SHOPS_READ: 'shops_r',
|
|
664
|
-
PROFILE_READ: 'profile_r',
|
|
665
|
-
FAVORITES_READ: 'favorites_r',
|
|
666
|
-
FEEDBACK_READ: 'feedback_r',
|
|
667
|
-
TREASURY_READ: 'treasury_r',
|
|
668
|
-
LISTINGS_WRITE: 'listings_w',
|
|
669
|
-
SHOPS_WRITE: 'shops_w',
|
|
670
|
-
PROFILE_WRITE: 'profile_w',
|
|
671
|
-
FAVORITES_WRITE: 'favorites_w',
|
|
672
|
-
FEEDBACK_WRITE: 'feedback_w',
|
|
673
|
-
TREASURY_WRITE: 'treasury_w',
|
|
674
|
-
LISTINGS_DELETE: 'listings_d',
|
|
675
|
-
SHOPS_DELETE: 'shops_d',
|
|
676
|
-
PROFILE_DELETE: 'profile_d',
|
|
677
|
-
FAVORITES_DELETE: 'favorites_d',
|
|
678
|
-
FEEDBACK_DELETE: 'feedback_d',
|
|
679
|
-
TREASURY_DELETE: 'treasury_d',
|
|
680
|
-
TRANSACTIONS_READ: 'transactions_r',
|
|
681
|
-
TRANSACTIONS_WRITE: 'transactions_w',
|
|
682
|
-
BILLING_READ: 'billing_r',
|
|
683
|
-
CART_READ: 'cart_r',
|
|
684
|
-
CART_WRITE: 'cart_w',
|
|
685
|
-
RECOMMEND_READ: 'recommend_r',
|
|
686
|
-
RECOMMEND_WRITE: 'recommend_w',
|
|
687
|
-
ADDRESS_READ: 'address_r',
|
|
688
|
-
ADDRESS_WRITE: 'address_w',
|
|
689
|
-
EMAIL_READ: 'email_r'
|
|
690
|
-
};
|
|
691
|
-
const COMMON_SCOPE_COMBINATIONS = {
|
|
692
|
-
SHOP_READ_ONLY: [
|
|
693
|
-
ETSY_SCOPES.SHOPS_READ,
|
|
694
|
-
ETSY_SCOPES.LISTINGS_READ,
|
|
695
|
-
ETSY_SCOPES.PROFILE_READ
|
|
696
|
-
],
|
|
697
|
-
SHOP_MANAGEMENT: [
|
|
698
|
-
ETSY_SCOPES.SHOPS_READ,
|
|
699
|
-
ETSY_SCOPES.SHOPS_WRITE,
|
|
700
|
-
ETSY_SCOPES.LISTINGS_READ,
|
|
701
|
-
ETSY_SCOPES.LISTINGS_WRITE,
|
|
702
|
-
ETSY_SCOPES.LISTINGS_DELETE,
|
|
703
|
-
ETSY_SCOPES.PROFILE_READ,
|
|
704
|
-
ETSY_SCOPES.TRANSACTIONS_READ
|
|
705
|
-
],
|
|
706
|
-
BASIC_ACCESS: [
|
|
707
|
-
ETSY_SCOPES.SHOPS_READ,
|
|
708
|
-
ETSY_SCOPES.LISTINGS_READ
|
|
709
|
-
]
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
function calculateItemAge(whenMade) {
|
|
713
|
-
if (!whenMade)
|
|
714
|
-
return null;
|
|
715
|
-
const currentYear = new Date().getFullYear();
|
|
716
|
-
const whenMadeArray = Array.isArray(whenMade) ? whenMade : [whenMade];
|
|
717
|
-
let latestYear = null;
|
|
718
|
-
for (const period of whenMadeArray) {
|
|
719
|
-
const year = extractYearFromPeriod(period);
|
|
720
|
-
if (year && (!latestYear || year > latestYear)) {
|
|
721
|
-
latestYear = year;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
return latestYear ? currentYear - latestYear : null;
|
|
725
|
-
}
|
|
726
|
-
function extractYearFromPeriod(period) {
|
|
727
|
-
const periodMappings = {
|
|
728
|
-
'before_1700': 1650,
|
|
729
|
-
'1700s': 1750,
|
|
730
|
-
'1800s': 1850,
|
|
731
|
-
'1900s': 1905,
|
|
732
|
-
'1910s': 1915,
|
|
733
|
-
'1920s': 1925,
|
|
734
|
-
'1930s': 1935,
|
|
735
|
-
'1940s': 1945,
|
|
736
|
-
'1950s': 1955,
|
|
737
|
-
'1960s': 1965,
|
|
738
|
-
'1970s': 1975,
|
|
739
|
-
'1980s': 1985,
|
|
740
|
-
'1990s': 1995,
|
|
741
|
-
'2000s': 2005,
|
|
742
|
-
'2010s': 2015,
|
|
743
|
-
'2020s': 2025
|
|
744
|
-
};
|
|
745
|
-
if (periodMappings[period]) {
|
|
746
|
-
return periodMappings[period];
|
|
747
|
-
}
|
|
748
|
-
const yearMatch = period.match(/(\d{4})/);
|
|
749
|
-
if (yearMatch) {
|
|
750
|
-
return parseInt(yearMatch[1], 10);
|
|
751
|
-
}
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
function classifyVintageOrAntique(whenMade) {
|
|
755
|
-
const age = calculateItemAge(whenMade);
|
|
756
|
-
if (!age)
|
|
757
|
-
return null;
|
|
758
|
-
if (age >= 100) {
|
|
759
|
-
return 'antique-prints';
|
|
760
|
-
}
|
|
761
|
-
else if (age >= 20) {
|
|
762
|
-
return 'vintage-prints';
|
|
763
|
-
}
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
function mapToEraSlug(whenMade) {
|
|
767
|
-
if (!whenMade)
|
|
768
|
-
return null;
|
|
769
|
-
const whenMadeArray = Array.isArray(whenMade) ? whenMade : [whenMade];
|
|
770
|
-
const eraMappings = {
|
|
771
|
-
'before_1700': 'pre-18th-century',
|
|
772
|
-
'1700s': '18th-century',
|
|
773
|
-
'1800s': '19th-century',
|
|
774
|
-
'1900s': 'early-20th-century',
|
|
775
|
-
'1910s': 'early-20th-century',
|
|
776
|
-
'1920s': 'early-20th-century',
|
|
777
|
-
'1930s': 'mid-20th-century',
|
|
778
|
-
'1940s': 'mid-20th-century',
|
|
779
|
-
'1950s': 'mid-20th-century',
|
|
780
|
-
'1960s': 'mid-20th-century',
|
|
781
|
-
'1970s': 'late-20th-century',
|
|
782
|
-
'1980s': 'late-20th-century',
|
|
783
|
-
'1990s': 'late-20th-century'
|
|
784
|
-
};
|
|
785
|
-
for (const period of [...whenMadeArray].reverse()) {
|
|
786
|
-
if (eraMappings[period]) {
|
|
787
|
-
return eraMappings[period];
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
function determineOrientation(product) {
|
|
793
|
-
const searchText = [
|
|
794
|
-
product.title,
|
|
795
|
-
product.description || '',
|
|
796
|
-
...(product.tags || [])
|
|
797
|
-
].join(' ').toLowerCase();
|
|
798
|
-
if (searchText.includes('portrait') || searchText.includes('vertical')) {
|
|
799
|
-
return 'portrait';
|
|
800
|
-
}
|
|
801
|
-
if (searchText.includes('landscape') || searchText.includes('horizontal')) {
|
|
802
|
-
return 'landscape';
|
|
803
|
-
}
|
|
804
|
-
return 'portrait';
|
|
805
|
-
}
|
|
806
|
-
function extractSize(product) {
|
|
807
|
-
if (product.item_length && product.item_width && product.item_dimensions_unit) {
|
|
808
|
-
const length = product.item_length;
|
|
809
|
-
const width = product.item_width;
|
|
810
|
-
const unit = product.item_dimensions_unit;
|
|
811
|
-
if (product.item_height && product.item_height > 0) {
|
|
812
|
-
return `${length} x ${width} x ${product.item_height} ${unit}`;
|
|
813
|
-
}
|
|
814
|
-
else {
|
|
815
|
-
return `${length} x ${width} ${unit}`;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
const searchText = [product.title, product.description || ''].join(' ');
|
|
819
|
-
const sizePatterns = [
|
|
820
|
-
/(\d+\.?\d*\s*x\s*\d+\.?\d*\s*(?:in|inch|inches|cm|mm))/i,
|
|
821
|
-
/(A[0-9])/i,
|
|
822
|
-
/(letter|legal|tabloid)/i,
|
|
823
|
-
/(\d+\.?\d*"\s*x\s*\d+\.?\d*")/i
|
|
824
|
-
];
|
|
825
|
-
for (const pattern of sizePatterns) {
|
|
826
|
-
const match = searchText.match(pattern);
|
|
827
|
-
if (match) {
|
|
828
|
-
return match[1].trim();
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return null;
|
|
832
|
-
}
|
|
833
|
-
function classifyProduct(etsyProduct) {
|
|
834
|
-
const vintageAntique = classifyVintageOrAntique(etsyProduct.when_made);
|
|
835
|
-
if (!vintageAntique) {
|
|
836
|
-
return null;
|
|
837
|
-
}
|
|
838
|
-
const eraSlug = mapToEraSlug(etsyProduct.when_made);
|
|
839
|
-
const size = extractSize(etsyProduct);
|
|
840
|
-
return {
|
|
841
|
-
etsy_listing_id: etsyProduct.listing_id,
|
|
842
|
-
title: etsyProduct.title,
|
|
843
|
-
description: etsyProduct.description,
|
|
844
|
-
price: etsyProduct.price,
|
|
845
|
-
etsy_listing_url: etsyProduct.url,
|
|
846
|
-
size,
|
|
847
|
-
category_keyword: vintageAntique,
|
|
848
|
-
era_slug: eraSlug,
|
|
849
|
-
section_id: etsyProduct.shop_section_id,
|
|
850
|
-
orientation: null,
|
|
851
|
-
tags: etsyProduct.tags || [],
|
|
852
|
-
materials: etsyProduct.materials || [],
|
|
853
|
-
when_made: Array.isArray(etsyProduct.when_made)
|
|
854
|
-
? etsyProduct.when_made.join(', ')
|
|
855
|
-
: etsyProduct.when_made,
|
|
856
|
-
classification_date: new Date().toISOString()
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
function classifyProducts(etsyProducts) {
|
|
860
|
-
return etsyProducts
|
|
861
|
-
.map(classifyProduct)
|
|
862
|
-
.filter((product) => product !== null);
|
|
863
|
-
}
|
|
864
|
-
function recalculateClassifications(products) {
|
|
865
|
-
return products.map(product => {
|
|
866
|
-
const newClassification = classifyVintageOrAntique(product.when_made);
|
|
867
|
-
const currentIsVintageAntique = ['vintage-prints', 'antique-prints'].includes(product.current_category);
|
|
868
|
-
const ageChanged = currentIsVintageAntique &&
|
|
869
|
-
newClassification &&
|
|
870
|
-
newClassification !== product.current_category;
|
|
871
|
-
return {
|
|
872
|
-
etsy_listing_id: product.etsy_listing_id,
|
|
873
|
-
new_category: newClassification,
|
|
874
|
-
age_changed: ageChanged
|
|
875
|
-
};
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
function validateClassification(product) {
|
|
879
|
-
const errors = [];
|
|
880
|
-
if (product.category_keyword === 'vintage-prints') {
|
|
881
|
-
const age = calculateItemAge(product.when_made);
|
|
882
|
-
if (age && age >= 100) {
|
|
883
|
-
errors.push('Product classified as vintage but is old enough to be antique');
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
if (product.category_keyword === 'antique-prints') {
|
|
887
|
-
const age = calculateItemAge(product.when_made);
|
|
888
|
-
if (age && age < 100) {
|
|
889
|
-
errors.push('Product classified as antique but is too recent');
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
if (!product.etsy_listing_id) {
|
|
893
|
-
errors.push('Missing required etsy_listing_id');
|
|
894
|
-
}
|
|
895
|
-
if (!product.title) {
|
|
896
|
-
errors.push('Missing required title');
|
|
897
|
-
}
|
|
898
|
-
if (product.price <= 0) {
|
|
899
|
-
errors.push('Price must be greater than 0');
|
|
900
|
-
}
|
|
901
|
-
return errors;
|
|
902
|
-
}
|
|
903
|
-
function getClassificationStats(products) {
|
|
904
|
-
const classified = classifyProducts(products);
|
|
905
|
-
const vintage = classified.filter(p => p.category_keyword === 'vintage-prints').length;
|
|
906
|
-
const antique = classified.filter(p => p.category_keyword === 'antique-prints').length;
|
|
907
|
-
const ages = products
|
|
908
|
-
.map(p => calculateItemAge(p.when_made))
|
|
909
|
-
.filter((age) => age !== null);
|
|
910
|
-
const averageAge = ages.length > 0
|
|
911
|
-
? ages.reduce((sum, age) => sum + age, 0) / ages.length
|
|
912
|
-
: null;
|
|
913
|
-
return {
|
|
914
|
-
total: products.length,
|
|
915
|
-
classified: classified.length,
|
|
916
|
-
vintage,
|
|
917
|
-
antique,
|
|
918
|
-
unclassified: products.length - classified.length,
|
|
919
|
-
averageAge
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
const DEFAULT_ERA_MAPPINGS = [
|
|
924
|
-
{
|
|
925
|
-
id: 'pre-18th-century',
|
|
926
|
-
slug: 'pre-18th-century',
|
|
927
|
-
name: 'Pre-18th Century',
|
|
928
|
-
etsy_when_made: ['before_1700'],
|
|
929
|
-
start_year: 1000,
|
|
930
|
-
end_year: 1699,
|
|
931
|
-
description: 'Items from before 1700'
|
|
932
|
-
},
|
|
933
|
-
{
|
|
934
|
-
id: '18th-century',
|
|
935
|
-
slug: '18th-century',
|
|
936
|
-
name: '18th Century',
|
|
937
|
-
etsy_when_made: ['1700s'],
|
|
938
|
-
start_year: 1700,
|
|
939
|
-
end_year: 1799,
|
|
940
|
-
description: 'Items from the 1700s'
|
|
941
|
-
},
|
|
942
|
-
{
|
|
943
|
-
id: '19th-century',
|
|
944
|
-
slug: '19th-century',
|
|
945
|
-
name: '19th Century',
|
|
946
|
-
etsy_when_made: ['1800s'],
|
|
947
|
-
start_year: 1800,
|
|
948
|
-
end_year: 1899,
|
|
949
|
-
description: 'Items from the 1800s'
|
|
950
|
-
},
|
|
951
|
-
{
|
|
952
|
-
id: 'early-20th-century',
|
|
953
|
-
slug: 'early-20th-century',
|
|
954
|
-
name: 'Early 20th Century',
|
|
955
|
-
etsy_when_made: ['1900s', '1910s', '1920s'],
|
|
956
|
-
start_year: 1900,
|
|
957
|
-
end_year: 1929,
|
|
958
|
-
description: 'Items from 1900-1929'
|
|
959
|
-
},
|
|
960
|
-
{
|
|
961
|
-
id: 'mid-20th-century',
|
|
962
|
-
slug: 'mid-20th-century',
|
|
963
|
-
name: 'Mid 20th Century',
|
|
964
|
-
etsy_when_made: ['1930s', '1940s', '1950s', '1960s'],
|
|
965
|
-
start_year: 1930,
|
|
966
|
-
end_year: 1969,
|
|
967
|
-
description: 'Items from 1930-1969'
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
id: 'late-20th-century',
|
|
971
|
-
slug: 'late-20th-century',
|
|
972
|
-
name: 'Late 20th Century',
|
|
973
|
-
etsy_when_made: ['1970s', '1980s', '1990s'],
|
|
974
|
-
start_year: 1970,
|
|
975
|
-
end_year: 1999,
|
|
976
|
-
description: 'Items from 1970-1999'
|
|
977
|
-
}
|
|
978
|
-
];
|
|
979
|
-
class EraMapper {
|
|
980
|
-
constructor(customMappings) {
|
|
981
|
-
this.mappings = customMappings || DEFAULT_ERA_MAPPINGS;
|
|
982
|
-
}
|
|
983
|
-
getEraIdFromEtsyWhenMade(etsyWhenMade) {
|
|
984
|
-
for (const mapping of this.mappings) {
|
|
985
|
-
if (mapping.etsy_when_made.includes(etsyWhenMade)) {
|
|
986
|
-
return mapping.id;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
return null;
|
|
990
|
-
}
|
|
991
|
-
getEraSlugFromEtsyWhenMade(etsyWhenMade) {
|
|
992
|
-
for (const mapping of this.mappings) {
|
|
993
|
-
if (mapping.etsy_when_made.includes(etsyWhenMade)) {
|
|
994
|
-
return mapping.slug;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
getEraNameFromEtsyWhenMade(etsyWhenMade) {
|
|
1000
|
-
for (const mapping of this.mappings) {
|
|
1001
|
-
if (mapping.etsy_when_made.includes(etsyWhenMade)) {
|
|
1002
|
-
return mapping.name;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return null;
|
|
1006
|
-
}
|
|
1007
|
-
getEraById(id) {
|
|
1008
|
-
return this.mappings.find(mapping => mapping.id === id) || null;
|
|
1009
|
-
}
|
|
1010
|
-
getEraBySlug(slug) {
|
|
1011
|
-
return this.mappings.find(mapping => mapping.slug === slug) || null;
|
|
1012
|
-
}
|
|
1013
|
-
getAllEras() {
|
|
1014
|
-
return [...this.mappings];
|
|
1015
|
-
}
|
|
1016
|
-
getErasFromMultipleEtsyWhenMade(etsyWhenMadeValues) {
|
|
1017
|
-
const matchingEras = new Set();
|
|
1018
|
-
for (const etsyWhenMade of etsyWhenMadeValues) {
|
|
1019
|
-
for (const mapping of this.mappings) {
|
|
1020
|
-
if (mapping.etsy_when_made.includes(etsyWhenMade)) {
|
|
1021
|
-
matchingEras.add(mapping);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
return Array.from(matchingEras);
|
|
1026
|
-
}
|
|
1027
|
-
getMostSpecificEra(etsyWhenMadeValues) {
|
|
1028
|
-
const matchingEras = this.getErasFromMultipleEtsyWhenMade(etsyWhenMadeValues);
|
|
1029
|
-
if (matchingEras.length === 0) {
|
|
1030
|
-
return null;
|
|
1031
|
-
}
|
|
1032
|
-
matchingEras.sort((a, b) => (b.start_year || 0) - (a.start_year || 0));
|
|
1033
|
-
return matchingEras[0];
|
|
1034
|
-
}
|
|
1035
|
-
addEraMapping(mapping) {
|
|
1036
|
-
this.mappings.push(mapping);
|
|
1037
|
-
}
|
|
1038
|
-
updateEraMapping(id, updates) {
|
|
1039
|
-
const index = this.mappings.findIndex(mapping => mapping.id === id);
|
|
1040
|
-
if (index === -1) {
|
|
1041
|
-
return false;
|
|
1042
|
-
}
|
|
1043
|
-
this.mappings[index] = { ...this.mappings[index], ...updates };
|
|
1044
|
-
return true;
|
|
1045
|
-
}
|
|
1046
|
-
removeEraMapping(id) {
|
|
1047
|
-
const index = this.mappings.findIndex(mapping => mapping.id === id);
|
|
1048
|
-
if (index === -1) {
|
|
1049
|
-
return false;
|
|
1050
|
-
}
|
|
1051
|
-
this.mappings.splice(index, 1);
|
|
1052
|
-
return true;
|
|
1053
|
-
}
|
|
1054
|
-
validateEraMapping(mapping) {
|
|
1055
|
-
const errors = [];
|
|
1056
|
-
if (!mapping.id) {
|
|
1057
|
-
errors.push('Era mapping must have an ID');
|
|
1058
|
-
}
|
|
1059
|
-
if (!mapping.slug) {
|
|
1060
|
-
errors.push('Era mapping must have a slug');
|
|
1061
|
-
}
|
|
1062
|
-
if (!mapping.name) {
|
|
1063
|
-
errors.push('Era mapping must have a name');
|
|
1064
|
-
}
|
|
1065
|
-
if (!mapping.etsy_when_made || mapping.etsy_when_made.length === 0) {
|
|
1066
|
-
errors.push('Era mapping must have at least one Etsy when_made value');
|
|
1067
|
-
}
|
|
1068
|
-
for (const etsyWhenMade of mapping.etsy_when_made) {
|
|
1069
|
-
if (!ETSY_WHEN_MADE_VALUES.includes(etsyWhenMade)) {
|
|
1070
|
-
errors.push(`Invalid Etsy when_made value: ${etsyWhenMade}`);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
if (mapping.start_year && mapping.end_year && mapping.start_year > mapping.end_year) {
|
|
1074
|
-
errors.push('Start year cannot be greater than end year');
|
|
1075
|
-
}
|
|
1076
|
-
return errors;
|
|
1077
|
-
}
|
|
1078
|
-
getEraStatistics() {
|
|
1079
|
-
const coveredEtsyValues = new Set();
|
|
1080
|
-
let erasWithYears = 0;
|
|
1081
|
-
for (const mapping of this.mappings) {
|
|
1082
|
-
if (mapping.start_year && mapping.end_year) {
|
|
1083
|
-
erasWithYears++;
|
|
1084
|
-
}
|
|
1085
|
-
for (const etsyWhenMade of mapping.etsy_when_made) {
|
|
1086
|
-
coveredEtsyValues.add(etsyWhenMade);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
const uncoveredEtsyValues = ETSY_WHEN_MADE_VALUES.filter(value => !coveredEtsyValues.has(value));
|
|
1090
|
-
return {
|
|
1091
|
-
total_eras: this.mappings.length,
|
|
1092
|
-
eras_with_years: erasWithYears,
|
|
1093
|
-
covered_etsy_values: coveredEtsyValues.size,
|
|
1094
|
-
uncovered_etsy_values: uncoveredEtsyValues
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
const defaultEraMapper = new EraMapper();
|
|
1099
|
-
function getEraIdFromEtsyWhenMade(etsyWhenMade) {
|
|
1100
|
-
return defaultEraMapper.getEraIdFromEtsyWhenMade(etsyWhenMade);
|
|
1101
|
-
}
|
|
1102
|
-
function getEraSlugFromEtsyWhenMade(etsyWhenMade) {
|
|
1103
|
-
return defaultEraMapper.getEraSlugFromEtsyWhenMade(etsyWhenMade);
|
|
1104
|
-
}
|
|
1105
|
-
function getAllErasWithEtsyMapping() {
|
|
1106
|
-
return defaultEraMapper.getAllEras();
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function createEtsyClient(config) {
|
|
1110
|
-
return new EtsyClient(config);
|
|
1111
|
-
}
|
|
1112
|
-
function createAuthHelper(config) {
|
|
1113
|
-
return new AuthHelper(config);
|
|
1114
|
-
}
|
|
1115
|
-
function createTokenManager(config, storage) {
|
|
1116
|
-
return new TokenManager(config, storage);
|
|
1117
|
-
}
|
|
1118
|
-
function createRateLimiter(config) {
|
|
1119
|
-
return new EtsyRateLimiter(config);
|
|
1120
|
-
}
|
|
1121
|
-
function createEraMapper(customMappings) {
|
|
1122
|
-
return new EraMapper(customMappings);
|
|
1123
|
-
}
|
|
1124
|
-
const VERSION = '1.0.0';
|
|
1125
|
-
const LIBRARY_NAME = 'etsy-v3-api-client';
|
|
1126
|
-
function getLibraryInfo() {
|
|
1127
|
-
return {
|
|
1128
|
-
name: LIBRARY_NAME,
|
|
1129
|
-
version: VERSION,
|
|
1130
|
-
description: 'JavaScript/TypeScript client for the Etsy Open API v3 with OAuth 2.0 authentication and product classification utilities',
|
|
1131
|
-
author: 'Henry',
|
|
1132
|
-
license: 'MIT',
|
|
1133
|
-
homepage: 'https://github.com/ForestHillArtsHouse/etsy-v3-api-client#readme'
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
exports.AuthHelper = AuthHelper;
|
|
1138
|
-
exports.COMMON_SCOPE_COMBINATIONS = COMMON_SCOPE_COMBINATIONS;
|
|
1139
|
-
exports.DEFAULT_ERA_MAPPINGS = DEFAULT_ERA_MAPPINGS;
|
|
1140
|
-
exports.ETSY_SCOPES = ETSY_SCOPES;
|
|
1141
|
-
exports.ETSY_WHEN_MADE_VALUES = ETSY_WHEN_MADE_VALUES;
|
|
1142
|
-
exports.EraMapper = EraMapper;
|
|
1143
|
-
exports.EtsyApiError = EtsyApiError;
|
|
1144
|
-
exports.EtsyAuthError = EtsyAuthError;
|
|
1145
|
-
exports.EtsyClient = EtsyClient;
|
|
1146
|
-
exports.EtsyRateLimitError = EtsyRateLimitError;
|
|
1147
|
-
exports.EtsyRateLimiter = EtsyRateLimiter;
|
|
1148
|
-
exports.FileTokenStorage = FileTokenStorage;
|
|
1149
|
-
exports.LIBRARY_NAME = LIBRARY_NAME;
|
|
1150
|
-
exports.MemoryTokenStorage = MemoryTokenStorage;
|
|
1151
|
-
exports.TokenManager = TokenManager;
|
|
1152
|
-
exports.VERSION = VERSION;
|
|
1153
|
-
exports.calculateItemAge = calculateItemAge;
|
|
1154
|
-
exports.classifyProduct = classifyProduct;
|
|
1155
|
-
exports.classifyProducts = classifyProducts;
|
|
1156
|
-
exports.classifyVintageOrAntique = classifyVintageOrAntique;
|
|
1157
|
-
exports.createAuthHelper = createAuthHelper;
|
|
1158
|
-
exports.createEraMapper = createEraMapper;
|
|
1159
|
-
exports.createEtsyClient = createEtsyClient;
|
|
1160
|
-
exports.createRateLimiter = createRateLimiter;
|
|
1161
|
-
exports.createTokenManager = createTokenManager;
|
|
1162
|
-
exports.default = EtsyClient;
|
|
1163
|
-
exports.defaultEraMapper = defaultEraMapper;
|
|
1164
|
-
exports.defaultRateLimiter = defaultRateLimiter;
|
|
1165
|
-
exports.determineOrientation = determineOrientation;
|
|
1166
|
-
exports.extractSize = extractSize;
|
|
1167
|
-
exports.extractYearFromPeriod = extractYearFromPeriod;
|
|
1168
|
-
exports.getAllErasWithEtsyMapping = getAllErasWithEtsyMapping;
|
|
1169
|
-
exports.getClassificationStats = getClassificationStats;
|
|
1170
|
-
exports.getEraIdFromEtsyWhenMade = getEraIdFromEtsyWhenMade;
|
|
1171
|
-
exports.getEraSlugFromEtsyWhenMade = getEraSlugFromEtsyWhenMade;
|
|
1172
|
-
exports.getLibraryInfo = getLibraryInfo;
|
|
1173
|
-
exports.mapToEraSlug = mapToEraSlug;
|
|
1174
|
-
exports.recalculateClassifications = recalculateClassifications;
|
|
1175
|
-
exports.validateClassification = validateClassification;
|
|
1176
|
-
|
|
1177
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
1178
|
-
|
|
1179
|
-
}));
|
|
1180
|
-
//# sourceMappingURL=index.umd.js.map
|