@shopi-lk/storefront-sdk 1.0.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 +267 -0
- package/dist/index.d.mts +397 -0
- package/dist/index.d.ts +397 -0
- package/dist/index.js +293 -0
- package/dist/index.mjs +267 -0
- package/package.json +26 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://apicall.shopi.lk/v1";
|
|
3
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
4
|
+
var MAX_RETRIES = 2;
|
|
5
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
var Shopi = class {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
/** Populated after every request — reflects the current rate-limit window. */
|
|
12
|
+
this.rateLimit = null;
|
|
13
|
+
// ── Products ───────────────────────────────────────────────────────────
|
|
14
|
+
this.products = {
|
|
15
|
+
list: async (params) => {
|
|
16
|
+
return this.request("products", {
|
|
17
|
+
params
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
getBySlug: async (slug) => {
|
|
21
|
+
const res = await this.request(
|
|
22
|
+
`products/${encodeURIComponent(slug)}`
|
|
23
|
+
);
|
|
24
|
+
return res.product;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
// ── Categories ─────────────────────────────────────────────────────────
|
|
28
|
+
this.categories = {
|
|
29
|
+
list: async () => {
|
|
30
|
+
const res = await this.request("categories");
|
|
31
|
+
return res.categories;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// ── Listings ──────────────────────────────────────────────────────────
|
|
35
|
+
this.listings = {
|
|
36
|
+
list: async (params) => {
|
|
37
|
+
return this.request("listings", {
|
|
38
|
+
params
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
getBySlug: async (slug) => {
|
|
42
|
+
const res = await this.request(
|
|
43
|
+
`listings/${encodeURIComponent(slug)}`
|
|
44
|
+
);
|
|
45
|
+
return res.listing;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
// ── Rental Items ──────────────────────────────────────────────────────
|
|
49
|
+
this.rentalItems = {
|
|
50
|
+
list: async (params) => {
|
|
51
|
+
return this.request("rental-items", {
|
|
52
|
+
params
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
getBySlug: async (slug) => {
|
|
56
|
+
const res = await this.request(
|
|
57
|
+
`rental-items/${encodeURIComponent(slug)}`
|
|
58
|
+
);
|
|
59
|
+
return res.rental_item;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
// ── Services ──────────────────────────────────────────────────────────
|
|
63
|
+
this.services = {
|
|
64
|
+
list: async (params) => {
|
|
65
|
+
return this.request("services", {
|
|
66
|
+
params
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
getBySlug: async (slug) => {
|
|
70
|
+
const res = await this.request(
|
|
71
|
+
`services/${encodeURIComponent(slug)}`
|
|
72
|
+
);
|
|
73
|
+
return res.service;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
// ── Pages ──────────────────────────────────────────────────────────────
|
|
77
|
+
this.pages = {
|
|
78
|
+
list: async () => {
|
|
79
|
+
const res = await this.request("pages");
|
|
80
|
+
return res.pages;
|
|
81
|
+
},
|
|
82
|
+
getBySlug: async (slug) => {
|
|
83
|
+
const res = await this.request(
|
|
84
|
+
`pages/${encodeURIComponent(slug)}`
|
|
85
|
+
);
|
|
86
|
+
return res.page;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// ── Blog ───────────────────────────────────────────────────────────────
|
|
90
|
+
this.blog = {
|
|
91
|
+
list: async (params) => {
|
|
92
|
+
return this.request("blogs", {
|
|
93
|
+
params
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
getBySlug: async (slug) => {
|
|
97
|
+
const res = await this.request(
|
|
98
|
+
`blogs/${encodeURIComponent(slug)}`
|
|
99
|
+
);
|
|
100
|
+
return res.post;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// ── Reviews ────────────────────────────────────────────────────────────
|
|
104
|
+
this.reviews = {
|
|
105
|
+
list: async (productId, limit) => {
|
|
106
|
+
const res = await this.request(
|
|
107
|
+
`reviews/${encodeURIComponent(productId)}`,
|
|
108
|
+
{ params: limit ? { limit } : void 0 }
|
|
109
|
+
);
|
|
110
|
+
return res.reviews;
|
|
111
|
+
},
|
|
112
|
+
submit: async (params) => {
|
|
113
|
+
const res = await this.request("reviews", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
body: params
|
|
116
|
+
});
|
|
117
|
+
return res.review;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// ── Checkout / Orders ──────────────────────────────────────────────────
|
|
121
|
+
this.checkout = {
|
|
122
|
+
createOrder: async (params) => {
|
|
123
|
+
const res = await this.request("orders", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
body: params
|
|
126
|
+
});
|
|
127
|
+
return res.order;
|
|
128
|
+
},
|
|
129
|
+
validatePromo: async (params) => {
|
|
130
|
+
return this.request("promo/validate", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
body: params
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
137
|
+
throw new Error("apiKey is required and must be a string");
|
|
138
|
+
}
|
|
139
|
+
const trimmed = config.apiKey.trim();
|
|
140
|
+
if (!trimmed.startsWith("shopi_pk_")) {
|
|
141
|
+
throw new Error('apiKey must start with "shopi_pk_"');
|
|
142
|
+
}
|
|
143
|
+
this.apiKey = trimmed;
|
|
144
|
+
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
145
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
146
|
+
}
|
|
147
|
+
async request(path, options = {}) {
|
|
148
|
+
const { method = "GET", body, params, _retryCount = 0 } = options;
|
|
149
|
+
let url = `${this.baseUrl}/${path}`;
|
|
150
|
+
if (params) {
|
|
151
|
+
const search = new URLSearchParams();
|
|
152
|
+
for (const [k, v] of Object.entries(params)) {
|
|
153
|
+
if (v !== void 0 && v !== null) search.set(k, String(v));
|
|
154
|
+
}
|
|
155
|
+
const qs = search.toString();
|
|
156
|
+
if (qs) url += `?${qs}`;
|
|
157
|
+
}
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
160
|
+
let res;
|
|
161
|
+
try {
|
|
162
|
+
res = await fetch(url, {
|
|
163
|
+
method,
|
|
164
|
+
headers: {
|
|
165
|
+
"X-Shopi-Api-Key": this.apiKey,
|
|
166
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
167
|
+
},
|
|
168
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
169
|
+
signal: controller.signal
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
174
|
+
throw new ShopiError(
|
|
175
|
+
`Request timed out after ${this.timeoutMs}ms`,
|
|
176
|
+
408
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
throw new ShopiError(
|
|
180
|
+
err instanceof Error ? err.message : "Network error",
|
|
181
|
+
0
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
}
|
|
186
|
+
if (RETRYABLE_STATUSES.has(res.status) && _retryCount < MAX_RETRIES) {
|
|
187
|
+
let delay = 2 ** _retryCount * 300;
|
|
188
|
+
if (res.status === 429) {
|
|
189
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
190
|
+
if (retryAfter) delay = Math.min(parseInt(retryAfter, 10) * 1e3, 1e4);
|
|
191
|
+
}
|
|
192
|
+
await sleep(delay);
|
|
193
|
+
return this.request(path, { ...options, _retryCount: _retryCount + 1 });
|
|
194
|
+
}
|
|
195
|
+
const rlLimit = res.headers.get("X-RateLimit-Limit");
|
|
196
|
+
const rlRemaining = res.headers.get("X-RateLimit-Remaining");
|
|
197
|
+
const rlReset = res.headers.get("X-RateLimit-Reset");
|
|
198
|
+
if (rlLimit && rlRemaining && rlReset) {
|
|
199
|
+
this.rateLimit = {
|
|
200
|
+
limit: parseInt(rlLimit, 10),
|
|
201
|
+
remaining: parseInt(rlRemaining, 10),
|
|
202
|
+
reset: parseInt(rlReset, 10)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
let data;
|
|
206
|
+
try {
|
|
207
|
+
data = await res.json();
|
|
208
|
+
} catch {
|
|
209
|
+
throw new ShopiError(
|
|
210
|
+
`Unexpected server response (status ${res.status})`,
|
|
211
|
+
res.status
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
const errData = data;
|
|
216
|
+
throw new ShopiError(
|
|
217
|
+
typeof errData?.error === "string" ? errData.error : "Request failed",
|
|
218
|
+
res.status,
|
|
219
|
+
data
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
// ── Shop ───────────────────────────────────────────────────────────────
|
|
225
|
+
async getShop() {
|
|
226
|
+
const res = await this.request("shop");
|
|
227
|
+
return res.shop;
|
|
228
|
+
}
|
|
229
|
+
// ── Store Settings ─────────────────────────────────────────────────────
|
|
230
|
+
async getStoreSettings() {
|
|
231
|
+
const res = await this.request(
|
|
232
|
+
"store-settings"
|
|
233
|
+
);
|
|
234
|
+
return res.settings;
|
|
235
|
+
}
|
|
236
|
+
// ── Payment Methods ────────────────────────────────────────────────────
|
|
237
|
+
async getPaymentMethods() {
|
|
238
|
+
return this.request("payment-methods");
|
|
239
|
+
}
|
|
240
|
+
// ── Discounts ──────────────────────────────────────────────────────────
|
|
241
|
+
async getDiscounts() {
|
|
242
|
+
const res = await this.request("discounts");
|
|
243
|
+
return {
|
|
244
|
+
shop_wide: res.shop_wide_discounts,
|
|
245
|
+
product_specific: res.product_discounts
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// ── Theme Settings ─────────────────────────────────────────────────────
|
|
249
|
+
async getThemeSettings() {
|
|
250
|
+
const res = await this.request(
|
|
251
|
+
"theme-settings"
|
|
252
|
+
);
|
|
253
|
+
return res.theme;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
var ShopiError = class extends Error {
|
|
257
|
+
constructor(message, status, data) {
|
|
258
|
+
super(message);
|
|
259
|
+
this.name = "ShopiError";
|
|
260
|
+
this.status = status;
|
|
261
|
+
this.data = data;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
export {
|
|
265
|
+
Shopi,
|
|
266
|
+
ShopiError
|
|
267
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shopi-lk/storefront-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official Shopi.lk Storefront SDK — access products, cart, checkout, and theme settings for custom storefronts",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
18
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["shopi", "storefront", "ecommerce", "sdk", "headless-commerce"],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.3.0"
|
|
25
|
+
}
|
|
26
|
+
}
|