@lonca/trendyol 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/README.md +59 -0
- package/dist/index.cjs +249 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +246 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @lonca/trendyol
|
|
2
|
+
|
|
3
|
+
Type-safe TypeScript SDK for the [Trendyol Marketplace API](https://developers.trendyol.com).
|
|
4
|
+
|
|
5
|
+
> ⚠️ **Alpha** — Only the `brands` resource is implemented in this initial scaffold. More endpoints (orders, products, inventory, webhooks) land in follow-up releases.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @lonca/trendyol
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createTrendyolClient } from '@lonca/trendyol';
|
|
17
|
+
import { paginate } from '@lonca/core';
|
|
18
|
+
|
|
19
|
+
const client = createTrendyolClient({
|
|
20
|
+
sellerId: 12345,
|
|
21
|
+
apiKey: process.env.TRENDYOL_API_KEY!,
|
|
22
|
+
apiSecret: process.env.TRENDYOL_API_SECRET!,
|
|
23
|
+
env: 'stage', // or 'prod'
|
|
24
|
+
integratorName: 'MyCompany', // optional; defaults to 'SelfIntegration'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
for await (const brand of paginate((p) => client.brands.list(p))) {
|
|
28
|
+
console.log(brand.id, brand.name);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Authentication
|
|
33
|
+
|
|
34
|
+
Trendyol uses HTTP Basic Auth. Get your `sellerId`, `apiKey`, and `apiSecret` from the [Trendyol Partner Panel → Account Info → Integration Information](https://partner.trendyol.com/account/info?tab=integrationInformation) (master-user only).
|
|
35
|
+
|
|
36
|
+
**Production vs Stage** have different credentials. Stage also requires IP whitelisting — register your CI/server IP with Trendyol support.
|
|
37
|
+
|
|
38
|
+
## Built-in robustness
|
|
39
|
+
|
|
40
|
+
- **Retry with exponential backoff** on 429 (respects `Retry-After`) and 5xx
|
|
41
|
+
- **Per-endpoint rate limiting** (token bucket) sized to Trendyol's documented limits
|
|
42
|
+
- **Structured errors** via `@lonca/core` (`AuthError`, `RateLimitError`, `NotFoundError`, `ServerError`, `ValidationError`, `NetworkError`, `TimeoutError`)
|
|
43
|
+
- **Correlation ID** auto-generated per request (`x-correlationid` header) for Trendyol-side log tracing
|
|
44
|
+
- **`AbortSignal` support** for cancellation throughout
|
|
45
|
+
|
|
46
|
+
## Environments
|
|
47
|
+
|
|
48
|
+
| Env | Base URL | Notes |
|
|
49
|
+
| ------- | --------------------------------- | --------------------------------------------- |
|
|
50
|
+
| `prod` | `https://apigw.trendyol.com` | No IP whitelist |
|
|
51
|
+
| `stage` | `https://stageapigw.trendyol.com` | IP whitelist required — call Trendyol support |
|
|
52
|
+
|
|
53
|
+
## Stability
|
|
54
|
+
|
|
55
|
+
`0.x` — alpha. Public APIs may change between minor versions until `1.0.0`.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@lonca/core');
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
// src/resources/brands.ts
|
|
7
|
+
var DEFAULT_PAGE_SIZE = 1e3;
|
|
8
|
+
var BrandsResource = class {
|
|
9
|
+
constructor(transport, limiter) {
|
|
10
|
+
this.transport = transport;
|
|
11
|
+
this.limiter = limiter ?? new core.TokenBucketRateLimiter({ capacity: 50, intervalMs: 6e4 });
|
|
12
|
+
}
|
|
13
|
+
transport;
|
|
14
|
+
limiter;
|
|
15
|
+
/**
|
|
16
|
+
* List Trendyol brands, one page at a time.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { paginate } from '@lonca/core';
|
|
21
|
+
* for await (const brand of paginate((p) => client.brands.list(p))) {
|
|
22
|
+
* console.log(brand.id, brand.name);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
async list(params = {}) {
|
|
27
|
+
const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
|
|
28
|
+
const size = params.limit ?? DEFAULT_PAGE_SIZE;
|
|
29
|
+
const data = await this.transport.request({
|
|
30
|
+
method: "GET",
|
|
31
|
+
path: "/sapigw/brands",
|
|
32
|
+
query: { page, size },
|
|
33
|
+
rateLimiter: this.limiter
|
|
34
|
+
});
|
|
35
|
+
const items = data.brands.map((b) => ({ id: String(b.id), name: b.name }));
|
|
36
|
+
const nextCursor = page + 1 < data.totalPages ? String(page + 1) : void 0;
|
|
37
|
+
return nextCursor !== void 0 ? { items, nextCursor } : { items };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/auth.ts
|
|
42
|
+
function buildAuthHeader(apiKey, apiSecret) {
|
|
43
|
+
const token = Buffer.from(`${apiKey}:${apiSecret}`, "utf8").toString("base64");
|
|
44
|
+
return `Basic ${token}`;
|
|
45
|
+
}
|
|
46
|
+
function buildUserAgent(sellerId, integratorName = "SelfIntegration") {
|
|
47
|
+
return `${sellerId} - ${integratorName}`;
|
|
48
|
+
}
|
|
49
|
+
function mapHttpError(status, body, retryAfterMs) {
|
|
50
|
+
const data = { body };
|
|
51
|
+
switch (status) {
|
|
52
|
+
case 401:
|
|
53
|
+
return new core.AuthError({
|
|
54
|
+
message: "Trendyol authentication failed (check apiKey / apiSecret)",
|
|
55
|
+
status,
|
|
56
|
+
data
|
|
57
|
+
});
|
|
58
|
+
case 403:
|
|
59
|
+
return new core.ValidationError({
|
|
60
|
+
message: "Trendyol forbidden (check User-Agent header or endpoint path)",
|
|
61
|
+
status,
|
|
62
|
+
data
|
|
63
|
+
});
|
|
64
|
+
case 404:
|
|
65
|
+
return new core.NotFoundError({ message: "Trendyol resource not found", status, data });
|
|
66
|
+
case 429:
|
|
67
|
+
return new core.RateLimitError({
|
|
68
|
+
message: "Trendyol rate limit exceeded",
|
|
69
|
+
status,
|
|
70
|
+
retryAfterMs,
|
|
71
|
+
data
|
|
72
|
+
});
|
|
73
|
+
case 500:
|
|
74
|
+
case 502:
|
|
75
|
+
case 503:
|
|
76
|
+
case 504:
|
|
77
|
+
return new core.ServerError({
|
|
78
|
+
message: `Trendyol server error (${status})`,
|
|
79
|
+
status,
|
|
80
|
+
data
|
|
81
|
+
});
|
|
82
|
+
default:
|
|
83
|
+
return new core.LoncaError({
|
|
84
|
+
code: "UNKNOWN",
|
|
85
|
+
message: `Trendyol unexpected response (${status})`,
|
|
86
|
+
status,
|
|
87
|
+
data,
|
|
88
|
+
retryable: false
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function parseRetryAfter(header) {
|
|
93
|
+
if (!header) return void 0;
|
|
94
|
+
const seconds = Number(header);
|
|
95
|
+
if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
|
|
96
|
+
const epoch = Date.parse(header);
|
|
97
|
+
if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/transport.ts
|
|
102
|
+
var BASE_URLS = {
|
|
103
|
+
prod: "https://apigw.trendyol.com",
|
|
104
|
+
stage: "https://stageapigw.trendyol.com"
|
|
105
|
+
};
|
|
106
|
+
var TrendyolTransport = class {
|
|
107
|
+
constructor(config) {
|
|
108
|
+
this.config = config;
|
|
109
|
+
this.baseUrl = BASE_URLS[config.env];
|
|
110
|
+
this.logger = config.logger ?? core.noopLogger;
|
|
111
|
+
this.timeoutMs = config.timeoutMs ?? 3e4;
|
|
112
|
+
this.fetchImpl = config.fetch ?? fetch;
|
|
113
|
+
}
|
|
114
|
+
config;
|
|
115
|
+
baseUrl;
|
|
116
|
+
logger;
|
|
117
|
+
timeoutMs;
|
|
118
|
+
fetchImpl;
|
|
119
|
+
async request(opts) {
|
|
120
|
+
return core.retry(
|
|
121
|
+
async (attempt) => {
|
|
122
|
+
if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
|
|
123
|
+
const url = this.buildUrl(opts.path, opts.query);
|
|
124
|
+
const correlationId = crypto.randomUUID();
|
|
125
|
+
const headers = this.buildHeaders(correlationId);
|
|
126
|
+
const init = {
|
|
127
|
+
method: opts.method,
|
|
128
|
+
headers,
|
|
129
|
+
signal: this.composeSignal(opts.signal)
|
|
130
|
+
};
|
|
131
|
+
if (opts.body !== void 0 && opts.method !== "GET") {
|
|
132
|
+
init.body = JSON.stringify(opts.body);
|
|
133
|
+
}
|
|
134
|
+
this.logger.debug("trendyol.request", {
|
|
135
|
+
method: opts.method,
|
|
136
|
+
url,
|
|
137
|
+
correlationId,
|
|
138
|
+
attempt
|
|
139
|
+
});
|
|
140
|
+
let response;
|
|
141
|
+
try {
|
|
142
|
+
response = await this.fetchImpl(url, init);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
145
|
+
throw new core.TimeoutError({
|
|
146
|
+
message: `Trendyol request timed out after ${this.timeoutMs}ms`,
|
|
147
|
+
cause: err
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
throw new core.NetworkError({
|
|
151
|
+
message: "Trendyol network failure",
|
|
152
|
+
cause: err
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const body = await safeJson(response);
|
|
157
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
|
|
158
|
+
const error = mapHttpError(response.status, body, retryAfterMs);
|
|
159
|
+
this.logger.warn("trendyol.error", {
|
|
160
|
+
method: opts.method,
|
|
161
|
+
url,
|
|
162
|
+
correlationId,
|
|
163
|
+
status: response.status,
|
|
164
|
+
code: error.code,
|
|
165
|
+
retryable: error.retryable
|
|
166
|
+
});
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
this.logger.debug("trendyol.response", {
|
|
170
|
+
correlationId,
|
|
171
|
+
status: response.status
|
|
172
|
+
});
|
|
173
|
+
if (response.status === 204) return void 0;
|
|
174
|
+
return await safeJson(response);
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
signal: opts.signal,
|
|
178
|
+
onRetry: (err, attempt, delay) => {
|
|
179
|
+
if (err instanceof core.LoncaError) {
|
|
180
|
+
this.logger.warn("trendyol.retry", {
|
|
181
|
+
attempt,
|
|
182
|
+
delayMs: delay,
|
|
183
|
+
code: err.code,
|
|
184
|
+
status: err.status
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
buildUrl(path, query) {
|
|
192
|
+
const url = new URL(path, this.baseUrl);
|
|
193
|
+
if (query) {
|
|
194
|
+
for (const [key, value] of Object.entries(query)) {
|
|
195
|
+
if (value === void 0) continue;
|
|
196
|
+
url.searchParams.set(key, String(value));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return url.toString();
|
|
200
|
+
}
|
|
201
|
+
buildHeaders(correlationId) {
|
|
202
|
+
const integratorName = this.config.integratorName ?? "SelfIntegration";
|
|
203
|
+
return {
|
|
204
|
+
Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),
|
|
205
|
+
"x-clientip": this.config.clientIp ?? "127.0.0.1",
|
|
206
|
+
"x-correlationid": correlationId,
|
|
207
|
+
"x-agentname": integratorName,
|
|
208
|
+
"User-Agent": buildUserAgent(this.config.sellerId, integratorName),
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
Accept: "application/json"
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
composeSignal(external) {
|
|
214
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
215
|
+
if (!external) return timeoutSignal;
|
|
216
|
+
return AbortSignal.any([external, timeoutSignal]);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
async function safeJson(response) {
|
|
220
|
+
const text = await response.text();
|
|
221
|
+
if (!text) return void 0;
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(text);
|
|
224
|
+
} catch {
|
|
225
|
+
return text;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/client.ts
|
|
230
|
+
function createTrendyolClient(opts) {
|
|
231
|
+
const transport = new TrendyolTransport({
|
|
232
|
+
sellerId: opts.sellerId,
|
|
233
|
+
apiKey: opts.apiKey,
|
|
234
|
+
apiSecret: opts.apiSecret,
|
|
235
|
+
env: opts.env,
|
|
236
|
+
integratorName: opts.integratorName,
|
|
237
|
+
clientIp: opts.clientIp,
|
|
238
|
+
logger: opts.logger,
|
|
239
|
+
timeoutMs: opts.timeoutMs
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
brands: new BrandsResource(transport)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
exports.BrandsResource = BrandsResource;
|
|
247
|
+
exports.createTrendyolClient = createTrendyolClient;
|
|
248
|
+
//# sourceMappingURL=index.cjs.map
|
|
249
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resources/brands.ts","../src/auth.ts","../src/errors.ts","../src/transport.ts","../src/client.ts"],"names":["TokenBucketRateLimiter","AuthError","ValidationError","NotFoundError","RateLimitError","ServerError","LoncaError","noopLogger","retry","randomUUID","TimeoutError","NetworkError"],"mappings":"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","file":"index.cjs","sourcesContent":["import { TokenBucketRateLimiter, type CursorPage, type CursorPaginationParams } from '@lonca/core';\nimport type { TrendyolTransport } from '../transport.js';\nimport type { Brand } from '../types/brand.js';\n\n/** Default page size when callers don't specify `limit`. Trendyol allows up to 1000. */\nconst DEFAULT_PAGE_SIZE = 1000;\n\ninterface TrendyolBrandListResponse {\n brands: Array<{ id: number; name: string }>;\n totalPages: number;\n totalElements: number;\n}\n\n/**\n * Trendyol brand-list endpoint group.\n *\n * Rate limit: 50 req/min (per Trendyol service limits).\n *\n * Trendyol uses page-based pagination internally; we expose the cursor-based\n * `CursorPage` shape from `@lonca/core` so callers can drive everything with\n * `paginate()` and stay consistent across Lonca SDKs.\n */\nexport class BrandsResource {\n private readonly limiter: TokenBucketRateLimiter;\n\n constructor(\n private readonly transport: TrendyolTransport,\n limiter?: TokenBucketRateLimiter,\n ) {\n this.limiter = limiter ?? new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n }\n\n /**\n * List Trendyol brands, one page at a time.\n *\n * @example\n * ```ts\n * import { paginate } from '@lonca/core';\n * for await (const brand of paginate((p) => client.brands.list(p))) {\n * console.log(brand.id, brand.name);\n * }\n * ```\n */\n async list(params: CursorPaginationParams = {}): Promise<CursorPage<Brand>> {\n const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;\n const size = params.limit ?? DEFAULT_PAGE_SIZE;\n\n const data = await this.transport.request<TrendyolBrandListResponse>({\n method: 'GET',\n path: '/sapigw/brands',\n query: { page, size },\n rateLimiter: this.limiter,\n });\n\n const items: Brand[] = data.brands.map((b) => ({ id: String(b.id), name: b.name }));\n const nextCursor = page + 1 < data.totalPages ? String(page + 1) : undefined;\n\n return nextCursor !== undefined ? { items, nextCursor } : { items };\n }\n}\n","/** Build the HTTP Basic auth header value from Trendyol credentials. */\nexport function buildAuthHeader(apiKey: string, apiSecret: string): string {\n const token = Buffer.from(`${apiKey}:${apiSecret}`, 'utf8').toString('base64');\n return `Basic ${token}`;\n}\n\n/**\n * Build the `User-Agent` header value Trendyol expects.\n *\n * Per Trendyol's docs:\n * - Solo integrators: `\"<sellerId> - SelfIntegration\"`\n * - Integrator companies: `\"<sellerId> - <CompanyName>\"` (alphanumeric, max 30 chars)\n *\n * A missing or malformed `User-Agent` triggers HTTP 403.\n */\nexport function buildUserAgent(sellerId: number, integratorName = 'SelfIntegration'): string {\n return `${sellerId} - ${integratorName}`;\n}\n","import {\n AuthError,\n LoncaError,\n NotFoundError,\n RateLimitError,\n ServerError,\n ValidationError,\n} from '@lonca/core';\n\n/**\n * Map a Trendyol HTTP response status to a `@lonca/core` error.\n *\n * - `401` → `AuthError` (bad credentials)\n * - `403` → `ValidationError` (usually missing/malformed `User-Agent` header, or wrong endpoint path)\n * - `404` → `NotFoundError`\n * - `429` → `RateLimitError` (carries `retryAfterMs` if the server provided `Retry-After`)\n * - `5xx` → `ServerError` (retryable)\n * - other → `LoncaError` with code `UNKNOWN` (non-retryable)\n */\nexport function mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError {\n const data = { body } as Record<string, unknown>;\n switch (status) {\n case 401:\n return new AuthError({\n message: 'Trendyol authentication failed (check apiKey / apiSecret)',\n status,\n data,\n });\n case 403:\n return new ValidationError({\n message: 'Trendyol forbidden (check User-Agent header or endpoint path)',\n status,\n data,\n });\n case 404:\n return new NotFoundError({ message: 'Trendyol resource not found', status, data });\n case 429:\n return new RateLimitError({\n message: 'Trendyol rate limit exceeded',\n status,\n retryAfterMs,\n data,\n });\n case 500:\n case 502:\n case 503:\n case 504:\n return new ServerError({\n message: `Trendyol server error (${status})`,\n status,\n data,\n });\n default:\n return new LoncaError({\n code: 'UNKNOWN',\n message: `Trendyol unexpected response (${status})`,\n status,\n data,\n retryable: false,\n });\n }\n}\n\n/** Parse a `Retry-After` header value (seconds OR HTTP-date) into milliseconds. */\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1000);\n const epoch = Date.parse(header);\n if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());\n return undefined;\n}\n","import { randomUUID } from 'node:crypto';\nimport {\n LoncaError,\n NetworkError,\n TimeoutError,\n retry,\n noopLogger,\n type Logger,\n type TokenBucketRateLimiter,\n} from '@lonca/core';\nimport { buildAuthHeader, buildUserAgent } from './auth.js';\nimport { mapHttpError, parseRetryAfter } from './errors.js';\n\nconst BASE_URLS = {\n prod: 'https://apigw.trendyol.com',\n stage: 'https://stageapigw.trendyol.com',\n} as const;\n\nexport type TrendyolEnvironment = keyof typeof BASE_URLS;\n\nexport interface TransportConfig {\n sellerId: number;\n apiKey: string;\n apiSecret: string;\n env: TrendyolEnvironment;\n integratorName?: string;\n clientIp?: string;\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n /** Override the underlying `fetch` (tests inject a mock). */\n fetch?: typeof fetch;\n}\n\nexport interface RequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /** Path beginning with `/` (e.g., `/sapigw/brands`). */\n path: string;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n signal?: AbortSignal;\n /** Per-endpoint rate limiter; acquire one token before each attempt. */\n rateLimiter?: TokenBucketRateLimiter;\n}\n\nexport class TrendyolTransport {\n private readonly baseUrl: string;\n private readonly logger: Logger;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(private readonly config: TransportConfig) {\n this.baseUrl = BASE_URLS[config.env];\n this.logger = config.logger ?? noopLogger;\n this.timeoutMs = config.timeoutMs ?? 30_000;\n this.fetchImpl = config.fetch ?? fetch;\n }\n\n async request<T>(opts: RequestOptions): Promise<T> {\n return retry(\n async (attempt) => {\n if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);\n\n const url = this.buildUrl(opts.path, opts.query);\n const correlationId = randomUUID();\n const headers = this.buildHeaders(correlationId);\n const init: RequestInit = {\n method: opts.method,\n headers,\n signal: this.composeSignal(opts.signal),\n };\n if (opts.body !== undefined && opts.method !== 'GET') {\n init.body = JSON.stringify(opts.body);\n }\n\n this.logger.debug('trendyol.request', {\n method: opts.method,\n url,\n correlationId,\n attempt,\n });\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, init);\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new TimeoutError({\n message: `Trendyol request timed out after ${this.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new NetworkError({\n message: 'Trendyol network failure',\n cause: err,\n });\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));\n const error = mapHttpError(response.status, body, retryAfterMs);\n this.logger.warn('trendyol.error', {\n method: opts.method,\n url,\n correlationId,\n status: response.status,\n code: error.code,\n retryable: error.retryable,\n });\n throw error;\n }\n\n this.logger.debug('trendyol.response', {\n correlationId,\n status: response.status,\n });\n\n if (response.status === 204) return undefined as T;\n return (await safeJson(response)) as T;\n },\n {\n signal: opts.signal,\n onRetry: (err, attempt, delay) => {\n if (err instanceof LoncaError) {\n this.logger.warn('trendyol.retry', {\n attempt,\n delayMs: delay,\n code: err.code,\n status: err.status,\n });\n }\n },\n },\n );\n }\n\n private buildUrl(path: string, query?: RequestOptions['query']): string {\n const url = new URL(path, this.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private buildHeaders(correlationId: string): Record<string, string> {\n const integratorName = this.config.integratorName ?? 'SelfIntegration';\n return {\n Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),\n 'x-clientip': this.config.clientIp ?? '127.0.0.1',\n 'x-correlationid': correlationId,\n 'x-agentname': integratorName,\n 'User-Agent': buildUserAgent(this.config.sellerId, integratorName),\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n\n private composeSignal(external?: AbortSignal): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(this.timeoutMs);\n if (!external) return timeoutSignal;\n return AbortSignal.any([external, timeoutSignal]);\n }\n}\n\nasync function safeJson(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n","import type { Logger } from '@lonca/core';\nimport { BrandsResource } from './resources/brands.js';\nimport { TrendyolTransport, type TrendyolEnvironment } from './transport.js';\n\nexport interface CreateClientOptions {\n /** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */\n sellerId: number;\n /** Trendyol API key. */\n apiKey: string;\n /** Trendyol API secret. */\n apiSecret: string;\n /** Which Trendyol environment to target. */\n env: TrendyolEnvironment;\n /**\n * Integrator company name to send in `User-Agent` / `x-agentname`.\n * Defaults to `'SelfIntegration'` if the seller owns the integration code.\n * Trendyol caps this at 30 alphanumeric characters.\n */\n integratorName?: string;\n /**\n * IPv4 address to send in `x-clientip`.\n * Defaults to `'127.0.0.1'` — Trendyol does not validate this against the\n * request origin, the header just has to be present and IPv4-shaped.\n */\n clientIp?: string;\n /** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n}\n\nexport interface TrendyolClient {\n brands: BrandsResource;\n}\n\n/**\n * Create a Trendyol Marketplace SDK client.\n *\n * @example\n * ```ts\n * import { createTrendyolClient } from '@lonca/trendyol';\n *\n * const client = createTrendyolClient({\n * sellerId: 12345,\n * apiKey: process.env.TRENDYOL_API_KEY!,\n * apiSecret: process.env.TRENDYOL_API_SECRET!,\n * env: 'stage',\n * });\n *\n * const page = await client.brands.list({ limit: 100 });\n * ```\n */\nexport function createTrendyolClient(opts: CreateClientOptions): TrendyolClient {\n const transport = new TrendyolTransport({\n sellerId: opts.sellerId,\n apiKey: opts.apiKey,\n apiSecret: opts.apiSecret,\n env: opts.env,\n integratorName: opts.integratorName,\n clientIp: opts.clientIp,\n logger: opts.logger,\n timeoutMs: opts.timeoutMs,\n });\n\n return {\n brands: new BrandsResource(transport),\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Logger, TokenBucketRateLimiter, CursorPaginationParams, CursorPage } from '@lonca/core';
|
|
2
|
+
|
|
3
|
+
declare const BASE_URLS: {
|
|
4
|
+
readonly prod: "https://apigw.trendyol.com";
|
|
5
|
+
readonly stage: "https://stageapigw.trendyol.com";
|
|
6
|
+
};
|
|
7
|
+
type TrendyolEnvironment = keyof typeof BASE_URLS;
|
|
8
|
+
interface TransportConfig {
|
|
9
|
+
sellerId: number;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
apiSecret: string;
|
|
12
|
+
env: TrendyolEnvironment;
|
|
13
|
+
integratorName?: string;
|
|
14
|
+
clientIp?: string;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
/** Request timeout in ms. Default: 30_000. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** Override the underlying `fetch` (tests inject a mock). */
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
interface RequestOptions {
|
|
22
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
23
|
+
/** Path beginning with `/` (e.g., `/sapigw/brands`). */
|
|
24
|
+
path: string;
|
|
25
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
26
|
+
body?: unknown;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
/** Per-endpoint rate limiter; acquire one token before each attempt. */
|
|
29
|
+
rateLimiter?: TokenBucketRateLimiter;
|
|
30
|
+
}
|
|
31
|
+
declare class TrendyolTransport {
|
|
32
|
+
private readonly config;
|
|
33
|
+
private readonly baseUrl;
|
|
34
|
+
private readonly logger;
|
|
35
|
+
private readonly timeoutMs;
|
|
36
|
+
private readonly fetchImpl;
|
|
37
|
+
constructor(config: TransportConfig);
|
|
38
|
+
request<T>(opts: RequestOptions): Promise<T>;
|
|
39
|
+
private buildUrl;
|
|
40
|
+
private buildHeaders;
|
|
41
|
+
private composeSignal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A Trendyol marketplace brand.
|
|
46
|
+
*
|
|
47
|
+
* Trendyol returns numeric IDs; we normalize to `string` to match the
|
|
48
|
+
* `@lonca/core` convention (string IDs across all Lonca SDKs).
|
|
49
|
+
*/
|
|
50
|
+
interface Brand {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Trendyol brand-list endpoint group.
|
|
57
|
+
*
|
|
58
|
+
* Rate limit: 50 req/min (per Trendyol service limits).
|
|
59
|
+
*
|
|
60
|
+
* Trendyol uses page-based pagination internally; we expose the cursor-based
|
|
61
|
+
* `CursorPage` shape from `@lonca/core` so callers can drive everything with
|
|
62
|
+
* `paginate()` and stay consistent across Lonca SDKs.
|
|
63
|
+
*/
|
|
64
|
+
declare class BrandsResource {
|
|
65
|
+
private readonly transport;
|
|
66
|
+
private readonly limiter;
|
|
67
|
+
constructor(transport: TrendyolTransport, limiter?: TokenBucketRateLimiter);
|
|
68
|
+
/**
|
|
69
|
+
* List Trendyol brands, one page at a time.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { paginate } from '@lonca/core';
|
|
74
|
+
* for await (const brand of paginate((p) => client.brands.list(p))) {
|
|
75
|
+
* console.log(brand.id, brand.name);
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
list(params?: CursorPaginationParams): Promise<CursorPage<Brand>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface CreateClientOptions {
|
|
83
|
+
/** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */
|
|
84
|
+
sellerId: number;
|
|
85
|
+
/** Trendyol API key. */
|
|
86
|
+
apiKey: string;
|
|
87
|
+
/** Trendyol API secret. */
|
|
88
|
+
apiSecret: string;
|
|
89
|
+
/** Which Trendyol environment to target. */
|
|
90
|
+
env: TrendyolEnvironment;
|
|
91
|
+
/**
|
|
92
|
+
* Integrator company name to send in `User-Agent` / `x-agentname`.
|
|
93
|
+
* Defaults to `'SelfIntegration'` if the seller owns the integration code.
|
|
94
|
+
* Trendyol caps this at 30 alphanumeric characters.
|
|
95
|
+
*/
|
|
96
|
+
integratorName?: string;
|
|
97
|
+
/**
|
|
98
|
+
* IPv4 address to send in `x-clientip`.
|
|
99
|
+
* Defaults to `'127.0.0.1'` — Trendyol does not validate this against the
|
|
100
|
+
* request origin, the header just has to be present and IPv4-shaped.
|
|
101
|
+
*/
|
|
102
|
+
clientIp?: string;
|
|
103
|
+
/** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */
|
|
104
|
+
logger?: Logger;
|
|
105
|
+
/** Request timeout in ms. Default: 30_000. */
|
|
106
|
+
timeoutMs?: number;
|
|
107
|
+
}
|
|
108
|
+
interface TrendyolClient {
|
|
109
|
+
brands: BrandsResource;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a Trendyol Marketplace SDK client.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { createTrendyolClient } from '@lonca/trendyol';
|
|
117
|
+
*
|
|
118
|
+
* const client = createTrendyolClient({
|
|
119
|
+
* sellerId: 12345,
|
|
120
|
+
* apiKey: process.env.TRENDYOL_API_KEY!,
|
|
121
|
+
* apiSecret: process.env.TRENDYOL_API_SECRET!,
|
|
122
|
+
* env: 'stage',
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* const page = await client.brands.list({ limit: 100 });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
declare function createTrendyolClient(opts: CreateClientOptions): TrendyolClient;
|
|
129
|
+
|
|
130
|
+
export { type Brand, BrandsResource, type CreateClientOptions, type TrendyolClient, type TrendyolEnvironment, createTrendyolClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Logger, TokenBucketRateLimiter, CursorPaginationParams, CursorPage } from '@lonca/core';
|
|
2
|
+
|
|
3
|
+
declare const BASE_URLS: {
|
|
4
|
+
readonly prod: "https://apigw.trendyol.com";
|
|
5
|
+
readonly stage: "https://stageapigw.trendyol.com";
|
|
6
|
+
};
|
|
7
|
+
type TrendyolEnvironment = keyof typeof BASE_URLS;
|
|
8
|
+
interface TransportConfig {
|
|
9
|
+
sellerId: number;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
apiSecret: string;
|
|
12
|
+
env: TrendyolEnvironment;
|
|
13
|
+
integratorName?: string;
|
|
14
|
+
clientIp?: string;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
/** Request timeout in ms. Default: 30_000. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** Override the underlying `fetch` (tests inject a mock). */
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
interface RequestOptions {
|
|
22
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
23
|
+
/** Path beginning with `/` (e.g., `/sapigw/brands`). */
|
|
24
|
+
path: string;
|
|
25
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
26
|
+
body?: unknown;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
/** Per-endpoint rate limiter; acquire one token before each attempt. */
|
|
29
|
+
rateLimiter?: TokenBucketRateLimiter;
|
|
30
|
+
}
|
|
31
|
+
declare class TrendyolTransport {
|
|
32
|
+
private readonly config;
|
|
33
|
+
private readonly baseUrl;
|
|
34
|
+
private readonly logger;
|
|
35
|
+
private readonly timeoutMs;
|
|
36
|
+
private readonly fetchImpl;
|
|
37
|
+
constructor(config: TransportConfig);
|
|
38
|
+
request<T>(opts: RequestOptions): Promise<T>;
|
|
39
|
+
private buildUrl;
|
|
40
|
+
private buildHeaders;
|
|
41
|
+
private composeSignal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A Trendyol marketplace brand.
|
|
46
|
+
*
|
|
47
|
+
* Trendyol returns numeric IDs; we normalize to `string` to match the
|
|
48
|
+
* `@lonca/core` convention (string IDs across all Lonca SDKs).
|
|
49
|
+
*/
|
|
50
|
+
interface Brand {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Trendyol brand-list endpoint group.
|
|
57
|
+
*
|
|
58
|
+
* Rate limit: 50 req/min (per Trendyol service limits).
|
|
59
|
+
*
|
|
60
|
+
* Trendyol uses page-based pagination internally; we expose the cursor-based
|
|
61
|
+
* `CursorPage` shape from `@lonca/core` so callers can drive everything with
|
|
62
|
+
* `paginate()` and stay consistent across Lonca SDKs.
|
|
63
|
+
*/
|
|
64
|
+
declare class BrandsResource {
|
|
65
|
+
private readonly transport;
|
|
66
|
+
private readonly limiter;
|
|
67
|
+
constructor(transport: TrendyolTransport, limiter?: TokenBucketRateLimiter);
|
|
68
|
+
/**
|
|
69
|
+
* List Trendyol brands, one page at a time.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { paginate } from '@lonca/core';
|
|
74
|
+
* for await (const brand of paginate((p) => client.brands.list(p))) {
|
|
75
|
+
* console.log(brand.id, brand.name);
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
list(params?: CursorPaginationParams): Promise<CursorPage<Brand>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface CreateClientOptions {
|
|
83
|
+
/** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */
|
|
84
|
+
sellerId: number;
|
|
85
|
+
/** Trendyol API key. */
|
|
86
|
+
apiKey: string;
|
|
87
|
+
/** Trendyol API secret. */
|
|
88
|
+
apiSecret: string;
|
|
89
|
+
/** Which Trendyol environment to target. */
|
|
90
|
+
env: TrendyolEnvironment;
|
|
91
|
+
/**
|
|
92
|
+
* Integrator company name to send in `User-Agent` / `x-agentname`.
|
|
93
|
+
* Defaults to `'SelfIntegration'` if the seller owns the integration code.
|
|
94
|
+
* Trendyol caps this at 30 alphanumeric characters.
|
|
95
|
+
*/
|
|
96
|
+
integratorName?: string;
|
|
97
|
+
/**
|
|
98
|
+
* IPv4 address to send in `x-clientip`.
|
|
99
|
+
* Defaults to `'127.0.0.1'` — Trendyol does not validate this against the
|
|
100
|
+
* request origin, the header just has to be present and IPv4-shaped.
|
|
101
|
+
*/
|
|
102
|
+
clientIp?: string;
|
|
103
|
+
/** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */
|
|
104
|
+
logger?: Logger;
|
|
105
|
+
/** Request timeout in ms. Default: 30_000. */
|
|
106
|
+
timeoutMs?: number;
|
|
107
|
+
}
|
|
108
|
+
interface TrendyolClient {
|
|
109
|
+
brands: BrandsResource;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a Trendyol Marketplace SDK client.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { createTrendyolClient } from '@lonca/trendyol';
|
|
117
|
+
*
|
|
118
|
+
* const client = createTrendyolClient({
|
|
119
|
+
* sellerId: 12345,
|
|
120
|
+
* apiKey: process.env.TRENDYOL_API_KEY!,
|
|
121
|
+
* apiSecret: process.env.TRENDYOL_API_SECRET!,
|
|
122
|
+
* env: 'stage',
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* const page = await client.brands.list({ limit: 100 });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
declare function createTrendyolClient(opts: CreateClientOptions): TrendyolClient;
|
|
129
|
+
|
|
130
|
+
export { type Brand, BrandsResource, type CreateClientOptions, type TrendyolClient, type TrendyolEnvironment, createTrendyolClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { TokenBucketRateLimiter, noopLogger, retry, TimeoutError, NetworkError, LoncaError, ServerError, RateLimitError, NotFoundError, ValidationError, AuthError } from '@lonca/core';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// src/resources/brands.ts
|
|
5
|
+
var DEFAULT_PAGE_SIZE = 1e3;
|
|
6
|
+
var BrandsResource = class {
|
|
7
|
+
constructor(transport, limiter) {
|
|
8
|
+
this.transport = transport;
|
|
9
|
+
this.limiter = limiter ?? new TokenBucketRateLimiter({ capacity: 50, intervalMs: 6e4 });
|
|
10
|
+
}
|
|
11
|
+
transport;
|
|
12
|
+
limiter;
|
|
13
|
+
/**
|
|
14
|
+
* List Trendyol brands, one page at a time.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { paginate } from '@lonca/core';
|
|
19
|
+
* for await (const brand of paginate((p) => client.brands.list(p))) {
|
|
20
|
+
* console.log(brand.id, brand.name);
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
async list(params = {}) {
|
|
25
|
+
const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
|
|
26
|
+
const size = params.limit ?? DEFAULT_PAGE_SIZE;
|
|
27
|
+
const data = await this.transport.request({
|
|
28
|
+
method: "GET",
|
|
29
|
+
path: "/sapigw/brands",
|
|
30
|
+
query: { page, size },
|
|
31
|
+
rateLimiter: this.limiter
|
|
32
|
+
});
|
|
33
|
+
const items = data.brands.map((b) => ({ id: String(b.id), name: b.name }));
|
|
34
|
+
const nextCursor = page + 1 < data.totalPages ? String(page + 1) : void 0;
|
|
35
|
+
return nextCursor !== void 0 ? { items, nextCursor } : { items };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/auth.ts
|
|
40
|
+
function buildAuthHeader(apiKey, apiSecret) {
|
|
41
|
+
const token = Buffer.from(`${apiKey}:${apiSecret}`, "utf8").toString("base64");
|
|
42
|
+
return `Basic ${token}`;
|
|
43
|
+
}
|
|
44
|
+
function buildUserAgent(sellerId, integratorName = "SelfIntegration") {
|
|
45
|
+
return `${sellerId} - ${integratorName}`;
|
|
46
|
+
}
|
|
47
|
+
function mapHttpError(status, body, retryAfterMs) {
|
|
48
|
+
const data = { body };
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 401:
|
|
51
|
+
return new AuthError({
|
|
52
|
+
message: "Trendyol authentication failed (check apiKey / apiSecret)",
|
|
53
|
+
status,
|
|
54
|
+
data
|
|
55
|
+
});
|
|
56
|
+
case 403:
|
|
57
|
+
return new ValidationError({
|
|
58
|
+
message: "Trendyol forbidden (check User-Agent header or endpoint path)",
|
|
59
|
+
status,
|
|
60
|
+
data
|
|
61
|
+
});
|
|
62
|
+
case 404:
|
|
63
|
+
return new NotFoundError({ message: "Trendyol resource not found", status, data });
|
|
64
|
+
case 429:
|
|
65
|
+
return new RateLimitError({
|
|
66
|
+
message: "Trendyol rate limit exceeded",
|
|
67
|
+
status,
|
|
68
|
+
retryAfterMs,
|
|
69
|
+
data
|
|
70
|
+
});
|
|
71
|
+
case 500:
|
|
72
|
+
case 502:
|
|
73
|
+
case 503:
|
|
74
|
+
case 504:
|
|
75
|
+
return new ServerError({
|
|
76
|
+
message: `Trendyol server error (${status})`,
|
|
77
|
+
status,
|
|
78
|
+
data
|
|
79
|
+
});
|
|
80
|
+
default:
|
|
81
|
+
return new LoncaError({
|
|
82
|
+
code: "UNKNOWN",
|
|
83
|
+
message: `Trendyol unexpected response (${status})`,
|
|
84
|
+
status,
|
|
85
|
+
data,
|
|
86
|
+
retryable: false
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseRetryAfter(header) {
|
|
91
|
+
if (!header) return void 0;
|
|
92
|
+
const seconds = Number(header);
|
|
93
|
+
if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
|
|
94
|
+
const epoch = Date.parse(header);
|
|
95
|
+
if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/transport.ts
|
|
100
|
+
var BASE_URLS = {
|
|
101
|
+
prod: "https://apigw.trendyol.com",
|
|
102
|
+
stage: "https://stageapigw.trendyol.com"
|
|
103
|
+
};
|
|
104
|
+
var TrendyolTransport = class {
|
|
105
|
+
constructor(config) {
|
|
106
|
+
this.config = config;
|
|
107
|
+
this.baseUrl = BASE_URLS[config.env];
|
|
108
|
+
this.logger = config.logger ?? noopLogger;
|
|
109
|
+
this.timeoutMs = config.timeoutMs ?? 3e4;
|
|
110
|
+
this.fetchImpl = config.fetch ?? fetch;
|
|
111
|
+
}
|
|
112
|
+
config;
|
|
113
|
+
baseUrl;
|
|
114
|
+
logger;
|
|
115
|
+
timeoutMs;
|
|
116
|
+
fetchImpl;
|
|
117
|
+
async request(opts) {
|
|
118
|
+
return retry(
|
|
119
|
+
async (attempt) => {
|
|
120
|
+
if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
|
|
121
|
+
const url = this.buildUrl(opts.path, opts.query);
|
|
122
|
+
const correlationId = randomUUID();
|
|
123
|
+
const headers = this.buildHeaders(correlationId);
|
|
124
|
+
const init = {
|
|
125
|
+
method: opts.method,
|
|
126
|
+
headers,
|
|
127
|
+
signal: this.composeSignal(opts.signal)
|
|
128
|
+
};
|
|
129
|
+
if (opts.body !== void 0 && opts.method !== "GET") {
|
|
130
|
+
init.body = JSON.stringify(opts.body);
|
|
131
|
+
}
|
|
132
|
+
this.logger.debug("trendyol.request", {
|
|
133
|
+
method: opts.method,
|
|
134
|
+
url,
|
|
135
|
+
correlationId,
|
|
136
|
+
attempt
|
|
137
|
+
});
|
|
138
|
+
let response;
|
|
139
|
+
try {
|
|
140
|
+
response = await this.fetchImpl(url, init);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
143
|
+
throw new TimeoutError({
|
|
144
|
+
message: `Trendyol request timed out after ${this.timeoutMs}ms`,
|
|
145
|
+
cause: err
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
throw new NetworkError({
|
|
149
|
+
message: "Trendyol network failure",
|
|
150
|
+
cause: err
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const body = await safeJson(response);
|
|
155
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
|
|
156
|
+
const error = mapHttpError(response.status, body, retryAfterMs);
|
|
157
|
+
this.logger.warn("trendyol.error", {
|
|
158
|
+
method: opts.method,
|
|
159
|
+
url,
|
|
160
|
+
correlationId,
|
|
161
|
+
status: response.status,
|
|
162
|
+
code: error.code,
|
|
163
|
+
retryable: error.retryable
|
|
164
|
+
});
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
this.logger.debug("trendyol.response", {
|
|
168
|
+
correlationId,
|
|
169
|
+
status: response.status
|
|
170
|
+
});
|
|
171
|
+
if (response.status === 204) return void 0;
|
|
172
|
+
return await safeJson(response);
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
signal: opts.signal,
|
|
176
|
+
onRetry: (err, attempt, delay) => {
|
|
177
|
+
if (err instanceof LoncaError) {
|
|
178
|
+
this.logger.warn("trendyol.retry", {
|
|
179
|
+
attempt,
|
|
180
|
+
delayMs: delay,
|
|
181
|
+
code: err.code,
|
|
182
|
+
status: err.status
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
buildUrl(path, query) {
|
|
190
|
+
const url = new URL(path, this.baseUrl);
|
|
191
|
+
if (query) {
|
|
192
|
+
for (const [key, value] of Object.entries(query)) {
|
|
193
|
+
if (value === void 0) continue;
|
|
194
|
+
url.searchParams.set(key, String(value));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return url.toString();
|
|
198
|
+
}
|
|
199
|
+
buildHeaders(correlationId) {
|
|
200
|
+
const integratorName = this.config.integratorName ?? "SelfIntegration";
|
|
201
|
+
return {
|
|
202
|
+
Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),
|
|
203
|
+
"x-clientip": this.config.clientIp ?? "127.0.0.1",
|
|
204
|
+
"x-correlationid": correlationId,
|
|
205
|
+
"x-agentname": integratorName,
|
|
206
|
+
"User-Agent": buildUserAgent(this.config.sellerId, integratorName),
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
Accept: "application/json"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
composeSignal(external) {
|
|
212
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
213
|
+
if (!external) return timeoutSignal;
|
|
214
|
+
return AbortSignal.any([external, timeoutSignal]);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
async function safeJson(response) {
|
|
218
|
+
const text = await response.text();
|
|
219
|
+
if (!text) return void 0;
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(text);
|
|
222
|
+
} catch {
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/client.ts
|
|
228
|
+
function createTrendyolClient(opts) {
|
|
229
|
+
const transport = new TrendyolTransport({
|
|
230
|
+
sellerId: opts.sellerId,
|
|
231
|
+
apiKey: opts.apiKey,
|
|
232
|
+
apiSecret: opts.apiSecret,
|
|
233
|
+
env: opts.env,
|
|
234
|
+
integratorName: opts.integratorName,
|
|
235
|
+
clientIp: opts.clientIp,
|
|
236
|
+
logger: opts.logger,
|
|
237
|
+
timeoutMs: opts.timeoutMs
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
brands: new BrandsResource(transport)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export { BrandsResource, createTrendyolClient };
|
|
245
|
+
//# sourceMappingURL=index.js.map
|
|
246
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resources/brands.ts","../src/auth.ts","../src/errors.ts","../src/transport.ts","../src/client.ts"],"names":["LoncaError"],"mappings":"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","file":"index.js","sourcesContent":["import { TokenBucketRateLimiter, type CursorPage, type CursorPaginationParams } from '@lonca/core';\nimport type { TrendyolTransport } from '../transport.js';\nimport type { Brand } from '../types/brand.js';\n\n/** Default page size when callers don't specify `limit`. Trendyol allows up to 1000. */\nconst DEFAULT_PAGE_SIZE = 1000;\n\ninterface TrendyolBrandListResponse {\n brands: Array<{ id: number; name: string }>;\n totalPages: number;\n totalElements: number;\n}\n\n/**\n * Trendyol brand-list endpoint group.\n *\n * Rate limit: 50 req/min (per Trendyol service limits).\n *\n * Trendyol uses page-based pagination internally; we expose the cursor-based\n * `CursorPage` shape from `@lonca/core` so callers can drive everything with\n * `paginate()` and stay consistent across Lonca SDKs.\n */\nexport class BrandsResource {\n private readonly limiter: TokenBucketRateLimiter;\n\n constructor(\n private readonly transport: TrendyolTransport,\n limiter?: TokenBucketRateLimiter,\n ) {\n this.limiter = limiter ?? new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n }\n\n /**\n * List Trendyol brands, one page at a time.\n *\n * @example\n * ```ts\n * import { paginate } from '@lonca/core';\n * for await (const brand of paginate((p) => client.brands.list(p))) {\n * console.log(brand.id, brand.name);\n * }\n * ```\n */\n async list(params: CursorPaginationParams = {}): Promise<CursorPage<Brand>> {\n const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;\n const size = params.limit ?? DEFAULT_PAGE_SIZE;\n\n const data = await this.transport.request<TrendyolBrandListResponse>({\n method: 'GET',\n path: '/sapigw/brands',\n query: { page, size },\n rateLimiter: this.limiter,\n });\n\n const items: Brand[] = data.brands.map((b) => ({ id: String(b.id), name: b.name }));\n const nextCursor = page + 1 < data.totalPages ? String(page + 1) : undefined;\n\n return nextCursor !== undefined ? { items, nextCursor } : { items };\n }\n}\n","/** Build the HTTP Basic auth header value from Trendyol credentials. */\nexport function buildAuthHeader(apiKey: string, apiSecret: string): string {\n const token = Buffer.from(`${apiKey}:${apiSecret}`, 'utf8').toString('base64');\n return `Basic ${token}`;\n}\n\n/**\n * Build the `User-Agent` header value Trendyol expects.\n *\n * Per Trendyol's docs:\n * - Solo integrators: `\"<sellerId> - SelfIntegration\"`\n * - Integrator companies: `\"<sellerId> - <CompanyName>\"` (alphanumeric, max 30 chars)\n *\n * A missing or malformed `User-Agent` triggers HTTP 403.\n */\nexport function buildUserAgent(sellerId: number, integratorName = 'SelfIntegration'): string {\n return `${sellerId} - ${integratorName}`;\n}\n","import {\n AuthError,\n LoncaError,\n NotFoundError,\n RateLimitError,\n ServerError,\n ValidationError,\n} from '@lonca/core';\n\n/**\n * Map a Trendyol HTTP response status to a `@lonca/core` error.\n *\n * - `401` → `AuthError` (bad credentials)\n * - `403` → `ValidationError` (usually missing/malformed `User-Agent` header, or wrong endpoint path)\n * - `404` → `NotFoundError`\n * - `429` → `RateLimitError` (carries `retryAfterMs` if the server provided `Retry-After`)\n * - `5xx` → `ServerError` (retryable)\n * - other → `LoncaError` with code `UNKNOWN` (non-retryable)\n */\nexport function mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError {\n const data = { body } as Record<string, unknown>;\n switch (status) {\n case 401:\n return new AuthError({\n message: 'Trendyol authentication failed (check apiKey / apiSecret)',\n status,\n data,\n });\n case 403:\n return new ValidationError({\n message: 'Trendyol forbidden (check User-Agent header or endpoint path)',\n status,\n data,\n });\n case 404:\n return new NotFoundError({ message: 'Trendyol resource not found', status, data });\n case 429:\n return new RateLimitError({\n message: 'Trendyol rate limit exceeded',\n status,\n retryAfterMs,\n data,\n });\n case 500:\n case 502:\n case 503:\n case 504:\n return new ServerError({\n message: `Trendyol server error (${status})`,\n status,\n data,\n });\n default:\n return new LoncaError({\n code: 'UNKNOWN',\n message: `Trendyol unexpected response (${status})`,\n status,\n data,\n retryable: false,\n });\n }\n}\n\n/** Parse a `Retry-After` header value (seconds OR HTTP-date) into milliseconds. */\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1000);\n const epoch = Date.parse(header);\n if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());\n return undefined;\n}\n","import { randomUUID } from 'node:crypto';\nimport {\n LoncaError,\n NetworkError,\n TimeoutError,\n retry,\n noopLogger,\n type Logger,\n type TokenBucketRateLimiter,\n} from '@lonca/core';\nimport { buildAuthHeader, buildUserAgent } from './auth.js';\nimport { mapHttpError, parseRetryAfter } from './errors.js';\n\nconst BASE_URLS = {\n prod: 'https://apigw.trendyol.com',\n stage: 'https://stageapigw.trendyol.com',\n} as const;\n\nexport type TrendyolEnvironment = keyof typeof BASE_URLS;\n\nexport interface TransportConfig {\n sellerId: number;\n apiKey: string;\n apiSecret: string;\n env: TrendyolEnvironment;\n integratorName?: string;\n clientIp?: string;\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n /** Override the underlying `fetch` (tests inject a mock). */\n fetch?: typeof fetch;\n}\n\nexport interface RequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /** Path beginning with `/` (e.g., `/sapigw/brands`). */\n path: string;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n signal?: AbortSignal;\n /** Per-endpoint rate limiter; acquire one token before each attempt. */\n rateLimiter?: TokenBucketRateLimiter;\n}\n\nexport class TrendyolTransport {\n private readonly baseUrl: string;\n private readonly logger: Logger;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(private readonly config: TransportConfig) {\n this.baseUrl = BASE_URLS[config.env];\n this.logger = config.logger ?? noopLogger;\n this.timeoutMs = config.timeoutMs ?? 30_000;\n this.fetchImpl = config.fetch ?? fetch;\n }\n\n async request<T>(opts: RequestOptions): Promise<T> {\n return retry(\n async (attempt) => {\n if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);\n\n const url = this.buildUrl(opts.path, opts.query);\n const correlationId = randomUUID();\n const headers = this.buildHeaders(correlationId);\n const init: RequestInit = {\n method: opts.method,\n headers,\n signal: this.composeSignal(opts.signal),\n };\n if (opts.body !== undefined && opts.method !== 'GET') {\n init.body = JSON.stringify(opts.body);\n }\n\n this.logger.debug('trendyol.request', {\n method: opts.method,\n url,\n correlationId,\n attempt,\n });\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, init);\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new TimeoutError({\n message: `Trendyol request timed out after ${this.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new NetworkError({\n message: 'Trendyol network failure',\n cause: err,\n });\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));\n const error = mapHttpError(response.status, body, retryAfterMs);\n this.logger.warn('trendyol.error', {\n method: opts.method,\n url,\n correlationId,\n status: response.status,\n code: error.code,\n retryable: error.retryable,\n });\n throw error;\n }\n\n this.logger.debug('trendyol.response', {\n correlationId,\n status: response.status,\n });\n\n if (response.status === 204) return undefined as T;\n return (await safeJson(response)) as T;\n },\n {\n signal: opts.signal,\n onRetry: (err, attempt, delay) => {\n if (err instanceof LoncaError) {\n this.logger.warn('trendyol.retry', {\n attempt,\n delayMs: delay,\n code: err.code,\n status: err.status,\n });\n }\n },\n },\n );\n }\n\n private buildUrl(path: string, query?: RequestOptions['query']): string {\n const url = new URL(path, this.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private buildHeaders(correlationId: string): Record<string, string> {\n const integratorName = this.config.integratorName ?? 'SelfIntegration';\n return {\n Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),\n 'x-clientip': this.config.clientIp ?? '127.0.0.1',\n 'x-correlationid': correlationId,\n 'x-agentname': integratorName,\n 'User-Agent': buildUserAgent(this.config.sellerId, integratorName),\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n\n private composeSignal(external?: AbortSignal): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(this.timeoutMs);\n if (!external) return timeoutSignal;\n return AbortSignal.any([external, timeoutSignal]);\n }\n}\n\nasync function safeJson(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n","import type { Logger } from '@lonca/core';\nimport { BrandsResource } from './resources/brands.js';\nimport { TrendyolTransport, type TrendyolEnvironment } from './transport.js';\n\nexport interface CreateClientOptions {\n /** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */\n sellerId: number;\n /** Trendyol API key. */\n apiKey: string;\n /** Trendyol API secret. */\n apiSecret: string;\n /** Which Trendyol environment to target. */\n env: TrendyolEnvironment;\n /**\n * Integrator company name to send in `User-Agent` / `x-agentname`.\n * Defaults to `'SelfIntegration'` if the seller owns the integration code.\n * Trendyol caps this at 30 alphanumeric characters.\n */\n integratorName?: string;\n /**\n * IPv4 address to send in `x-clientip`.\n * Defaults to `'127.0.0.1'` — Trendyol does not validate this against the\n * request origin, the header just has to be present and IPv4-shaped.\n */\n clientIp?: string;\n /** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n}\n\nexport interface TrendyolClient {\n brands: BrandsResource;\n}\n\n/**\n * Create a Trendyol Marketplace SDK client.\n *\n * @example\n * ```ts\n * import { createTrendyolClient } from '@lonca/trendyol';\n *\n * const client = createTrendyolClient({\n * sellerId: 12345,\n * apiKey: process.env.TRENDYOL_API_KEY!,\n * apiSecret: process.env.TRENDYOL_API_SECRET!,\n * env: 'stage',\n * });\n *\n * const page = await client.brands.list({ limit: 100 });\n * ```\n */\nexport function createTrendyolClient(opts: CreateClientOptions): TrendyolClient {\n const transport = new TrendyolTransport({\n sellerId: opts.sellerId,\n apiKey: opts.apiKey,\n apiSecret: opts.apiSecret,\n env: opts.env,\n integratorName: opts.integratorName,\n clientIp: opts.clientIp,\n logger: opts.logger,\n timeoutMs: opts.timeoutMs,\n });\n\n return {\n brands: new BrandsResource(transport),\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lonca/trendyol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe TypeScript SDK for the Trendyol Marketplace API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/loncadev/lonca.git",
|
|
9
|
+
"directory": "sdks/trendyol"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/loncadev/lonca/tree/main/sdks/trendyol#readme",
|
|
12
|
+
"bugs": "https://github.com/loncadev/lonca/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"trendyol",
|
|
15
|
+
"marketplace",
|
|
16
|
+
"e-commerce",
|
|
17
|
+
"turkey",
|
|
18
|
+
"sdk",
|
|
19
|
+
"api"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"lint": "eslint .",
|
|
52
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('.turbo',{recursive:true,force:true})\""
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@lonca/core": "workspace:^"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "catalog:",
|
|
59
|
+
"tsup": "catalog:",
|
|
60
|
+
"typescript": "catalog:",
|
|
61
|
+
"vitest": "catalog:"
|
|
62
|
+
}
|
|
63
|
+
}
|