@spree/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vendo Connect Inc., Vendo Sp. z o.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,649 @@
1
+ # @spree/sdk
2
+
3
+ Official TypeScript SDK for Spree Commerce API v3.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @spree/sdk
9
+ # or
10
+ yarn add @spree/sdk
11
+ # or
12
+ pnpm add @spree/sdk
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { createSpreeClient } from '@spree/sdk';
19
+
20
+ // Initialize the client
21
+ const client = createSpreeClient({
22
+ baseUrl: 'https://api.mystore.com',
23
+ apiKey: 'your-publishable-api-key',
24
+ });
25
+
26
+ // Browse products
27
+ const products = await client.products.list({
28
+ per_page: 10,
29
+ includes: 'variants,images',
30
+ });
31
+
32
+ // Get a single product
33
+ const product = await client.products.get('ruby-on-rails-tote');
34
+
35
+ // Authentication
36
+ const { token, user } = await client.auth.login({
37
+ email: 'customer@example.com',
38
+ password: 'password123',
39
+ });
40
+
41
+ // Create a cart and add items
42
+ const cart = await client.cart.create();
43
+ await client.orders.lineItems.create(cart.id, {
44
+ variant_id: 'var_abc123',
45
+ quantity: 2,
46
+ }, { orderToken: cart.token });
47
+
48
+ // Checkout flow
49
+ await client.orders.next(cart.id, { orderToken: cart.token });
50
+ await client.orders.complete(cart.id, { orderToken: cart.token });
51
+ ```
52
+
53
+ ## Authentication
54
+
55
+ The SDK supports multiple authentication modes:
56
+
57
+ ### 1. API Key Only (Guest/Public Access)
58
+
59
+ ```typescript
60
+ const client = createSpreeClient({
61
+ baseUrl: 'https://api.mystore.com',
62
+ apiKey: 'pk_your-publishable-key',
63
+ });
64
+
65
+ // Public endpoints work without user authentication
66
+ const products = await client.products.list();
67
+ ```
68
+
69
+ ### 2. API Key + JWT (Authenticated Customer)
70
+
71
+ ```typescript
72
+ // Login to get tokens
73
+ const { token, user } = await client.auth.login({
74
+ email: 'customer@example.com',
75
+ password: 'password123',
76
+ });
77
+
78
+ // Use token for authenticated requests
79
+ const orders = await client.orders.list({}, { token });
80
+
81
+ // Refresh token when needed
82
+ const newTokens = await client.auth.refresh({ token });
83
+ ```
84
+
85
+ ### 3. Register New Customer
86
+
87
+ ```typescript
88
+ const { token, user } = await client.auth.register({
89
+ email: 'new@example.com',
90
+ password: 'password123',
91
+ password_confirmation: 'password123',
92
+ first_name: 'John',
93
+ last_name: 'Doe',
94
+ });
95
+ ```
96
+
97
+ ## Guest Checkout
98
+
99
+ For guest checkout, use the `token` (or `order_token`) returned when creating a cart:
100
+
101
+ ```typescript
102
+ // Create a cart (guest)
103
+ const cart = await client.cart.create();
104
+
105
+ // Use orderToken for all cart operations
106
+ const options = { orderToken: cart.token };
107
+
108
+ // Add items
109
+ await client.orders.lineItems.create(cart.id, {
110
+ variant_id: 'var_abc123',
111
+ quantity: 1,
112
+ }, options);
113
+
114
+ // Update order with email
115
+ await client.orders.update(cart.id, {
116
+ email: 'guest@example.com',
117
+ }, options);
118
+
119
+ // Complete checkout
120
+ await client.orders.complete(cart.id, options);
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### Store
126
+
127
+ ```typescript
128
+ // Get current store information
129
+ const store = await client.store.get();
130
+ ```
131
+
132
+ ### Products
133
+
134
+ ```typescript
135
+ // List products with filtering
136
+ const products = await client.products.list({
137
+ page: 1,
138
+ per_page: 25,
139
+ 'q[name_cont]': 'shirt',
140
+ includes: 'variants,images,taxons',
141
+ });
142
+
143
+ // Get single product by ID or slug
144
+ const product = await client.products.get('ruby-on-rails-tote', {
145
+ includes: 'variants,images',
146
+ });
147
+
148
+ // Get available filters (price range, availability, options, taxons)
149
+ const filters = await client.products.filters({
150
+ taxon_id: 'txn_abc123', // Optional: scope filters to a taxon
151
+ });
152
+ ```
153
+
154
+ ### Categories (Taxonomies & Taxons)
155
+
156
+ ```typescript
157
+ // List taxonomies
158
+ const taxonomies = await client.taxonomies.list({
159
+ includes: 'taxons',
160
+ });
161
+
162
+ // Get taxonomy with taxons
163
+ const categories = await client.taxonomies.get('tax_123', {
164
+ includes: 'root,taxons',
165
+ });
166
+
167
+ // List taxons with filtering
168
+ const taxons = await client.taxons.list({
169
+ 'q[depth_eq]': 1, // Top-level categories only
170
+ 'q[taxonomy_id_eq]': '123', // Filter by taxonomy
171
+ });
172
+
173
+ // Get single taxon by ID or permalink
174
+ const taxon = await client.taxons.get('categories/clothing', {
175
+ includes: 'ancestors,children', // For breadcrumbs and subcategories
176
+ });
177
+
178
+ // List products in a category
179
+ const categoryProducts = await client.taxons.products.list('categories/clothing', {
180
+ page: 1,
181
+ per_page: 12,
182
+ includes: 'images,default_variant',
183
+ });
184
+ ```
185
+
186
+ ### Cart
187
+
188
+ ```typescript
189
+ // Get current cart
190
+ const cart = await client.cart.get({ orderToken: 'xxx' });
191
+
192
+ // Create a new cart
193
+ const newCart = await client.cart.create();
194
+
195
+ // Associate guest cart with authenticated user
196
+ // (after user logs in, merge their guest cart with their account)
197
+ await client.cart.associate({
198
+ token: jwtToken, // User's JWT token
199
+ orderToken: cart.token, // Guest cart token
200
+ });
201
+ ```
202
+
203
+ ### Orders & Checkout
204
+
205
+ ```typescript
206
+ // List orders for authenticated customer
207
+ const orders = await client.orders.list({}, { token });
208
+
209
+ // Create a new order (cart)
210
+ const cart = await client.orders.create();
211
+ const options = { orderToken: cart.order_token };
212
+
213
+ // Get order by ID or number
214
+ const order = await client.orders.get('R123456789', {
215
+ includes: 'line_items,shipments',
216
+ }, options);
217
+
218
+ // Update order (email, addresses)
219
+ await client.orders.update(cart.id, {
220
+ email: 'customer@example.com',
221
+ ship_address: {
222
+ firstname: 'John',
223
+ lastname: 'Doe',
224
+ address1: '123 Main St',
225
+ city: 'New York',
226
+ zipcode: '10001',
227
+ phone: '+1 555 123 4567',
228
+ country_iso: 'US',
229
+ state_abbr: 'NY',
230
+ },
231
+ bill_address_id: 'addr_xxx', // Or use existing address by ID
232
+ }, options);
233
+
234
+ // Checkout flow
235
+ await client.orders.next(cart.id, options); // Move to next step
236
+ await client.orders.advance(cart.id, options); // Advance through all steps
237
+ await client.orders.complete(cart.id, options); // Complete the order
238
+ ```
239
+
240
+ ### Line Items
241
+
242
+ ```typescript
243
+ const options = { orderToken: cart.token };
244
+
245
+ // Add item
246
+ await client.orders.lineItems.create(cart.id, {
247
+ variant_id: 'var_123',
248
+ quantity: 2,
249
+ }, options);
250
+
251
+ // Update item quantity
252
+ await client.orders.lineItems.update(cart.id, lineItemId, {
253
+ quantity: 3,
254
+ }, options);
255
+
256
+ // Remove item
257
+ await client.orders.lineItems.delete(cart.id, lineItemId, options);
258
+ ```
259
+
260
+ ### Coupon Codes
261
+
262
+ ```typescript
263
+ const options = { orderToken: cart.token };
264
+
265
+ // Apply a coupon code
266
+ await client.orders.couponCodes.apply(cart.id, 'SAVE20', options);
267
+
268
+ // Remove a coupon code
269
+ await client.orders.couponCodes.remove(cart.id, 'promo_xxx', options);
270
+ ```
271
+
272
+ ### Store Credits
273
+
274
+ ```typescript
275
+ const options = { orderToken: cart.token };
276
+
277
+ // Apply store credit to order (applies maximum available by default)
278
+ await client.orders.addStoreCredit(cart.id, undefined, options);
279
+
280
+ // Apply specific amount of store credit
281
+ await client.orders.addStoreCredit(cart.id, 25.00, options);
282
+
283
+ // Remove store credit from order
284
+ await client.orders.removeStoreCredit(cart.id, options);
285
+ ```
286
+
287
+ ### Shipments
288
+
289
+ ```typescript
290
+ const options = { orderToken: cart.token };
291
+
292
+ // List shipments for an order
293
+ const shipments = await client.orders.shipments.list(cart.id, options);
294
+
295
+ // Select a shipping rate
296
+ await client.orders.shipments.update(cart.id, shipmentId, {
297
+ selected_shipping_rate_id: 'rate_xxx',
298
+ }, options);
299
+ ```
300
+
301
+ ### Payments
302
+
303
+ ```typescript
304
+ const options = { orderToken: cart.token };
305
+
306
+ // Get available payment methods for an order
307
+ const methods = await client.orders.paymentMethods.list(cart.id, options);
308
+
309
+ // List payments on an order
310
+ const payments = await client.orders.payments.list(cart.id, options);
311
+
312
+ // Get a specific payment
313
+ const payment = await client.orders.payments.get(cart.id, paymentId, options);
314
+ ```
315
+
316
+ ### Geography
317
+
318
+ ```typescript
319
+ // List countries available for checkout
320
+ const { data: countries } = await client.countries.list();
321
+
322
+ // Get country by ISO code (includes states)
323
+ const usa = await client.countries.get('US');
324
+ console.log(usa.states); // Array of states
325
+ ```
326
+
327
+ ### Customer Account
328
+
329
+ ```typescript
330
+ const options = { token: jwtToken };
331
+
332
+ // Get profile
333
+ const profile = await client.customer.get(options);
334
+
335
+ // Update profile
336
+ await client.customer.update({
337
+ first_name: 'John',
338
+ last_name: 'Doe',
339
+ }, options);
340
+ ```
341
+
342
+ ### Customer Addresses
343
+
344
+ ```typescript
345
+ const options = { token: jwtToken };
346
+
347
+ // List addresses
348
+ const { data: addresses } = await client.customer.addresses.list({}, options);
349
+
350
+ // Get address by ID
351
+ const address = await client.customer.addresses.get('addr_xxx', options);
352
+
353
+ // Create address
354
+ await client.customer.addresses.create({
355
+ firstname: 'John',
356
+ lastname: 'Doe',
357
+ address1: '123 Main St',
358
+ city: 'New York',
359
+ zipcode: '10001',
360
+ country_iso: 'US',
361
+ state_abbr: 'NY',
362
+ }, options);
363
+
364
+ // Update address
365
+ await client.customer.addresses.update('addr_xxx', { city: 'Brooklyn' }, options);
366
+
367
+ // Delete address
368
+ await client.customer.addresses.delete('addr_xxx', options);
369
+ ```
370
+
371
+ ### Customer Credit Cards
372
+
373
+ ```typescript
374
+ const options = { token: jwtToken };
375
+
376
+ // List saved credit cards
377
+ const { data: cards } = await client.customer.creditCards.list({}, options);
378
+
379
+ // Get credit card by ID
380
+ const card = await client.customer.creditCards.get('cc_xxx', options);
381
+
382
+ // Delete credit card
383
+ await client.customer.creditCards.delete('cc_xxx', options);
384
+ ```
385
+
386
+ ### Wishlists
387
+
388
+ ```typescript
389
+ const options = { token: jwtToken };
390
+
391
+ // List wishlists
392
+ const { data: wishlists } = await client.wishlists.list({}, options);
393
+
394
+ // Get wishlist by ID
395
+ const wishlist = await client.wishlists.get('wl_xxx', {
396
+ includes: 'wished_items',
397
+ }, options);
398
+
399
+ // Create wishlist
400
+ const newWishlist = await client.wishlists.create({
401
+ name: 'Birthday Ideas',
402
+ is_private: true,
403
+ }, options);
404
+
405
+ // Update wishlist
406
+ await client.wishlists.update('wl_xxx', {
407
+ name: 'Updated Name',
408
+ }, options);
409
+
410
+ // Delete wishlist
411
+ await client.wishlists.delete('wl_xxx', options);
412
+ ```
413
+
414
+ ### Wishlist Items
415
+
416
+ ```typescript
417
+ const options = { token: jwtToken };
418
+
419
+ // Add item to wishlist
420
+ await client.wishlists.items.create('wl_xxx', {
421
+ variant_id: 'var_123',
422
+ quantity: 1,
423
+ }, options);
424
+
425
+ // Update item quantity
426
+ await client.wishlists.items.update('wl_xxx', 'wi_xxx', {
427
+ quantity: 2,
428
+ }, options);
429
+
430
+ // Remove item from wishlist
431
+ await client.wishlists.items.delete('wl_xxx', 'wi_xxx', options);
432
+ ```
433
+
434
+ ## Nested Resources
435
+
436
+ The SDK uses a resource builder pattern for nested resources:
437
+
438
+ | Parent Resource | Nested Resource | Available Methods |
439
+ |-----------------|-----------------|-------------------|
440
+ | `orders` | `lineItems` | `create`, `update`, `delete` |
441
+ | `orders` | `payments` | `list`, `get` |
442
+ | `orders` | `paymentMethods` | `list` |
443
+ | `orders` | `shipments` | `list`, `update` |
444
+ | `orders` | `couponCodes` | `apply`, `remove` |
445
+ | `customer` | `addresses` | `list`, `get`, `create`, `update`, `delete` |
446
+ | `customer` | `creditCards` | `list`, `get`, `delete` |
447
+ | `taxons` | `products` | `list` |
448
+ | `wishlists` | `items` | `create`, `update`, `delete` |
449
+
450
+ Example:
451
+ ```typescript
452
+ // Nested resources follow the pattern: client.parent.nested.method(parentId, ...)
453
+ await client.orders.lineItems.create(orderId, params, options);
454
+ await client.orders.payments.list(orderId, options);
455
+ await client.orders.shipments.update(orderId, shipmentId, params, options);
456
+ await client.customer.addresses.list({}, options);
457
+ await client.taxons.products.list(taxonId, params, options);
458
+ await client.wishlists.items.create(wishlistId, params, options);
459
+ ```
460
+
461
+ ## Localization & Currency
462
+
463
+ Pass locale and currency headers with any request:
464
+
465
+ ```typescript
466
+ // Set locale and currency per request
467
+ const products = await client.products.list({}, {
468
+ locale: 'fr',
469
+ currency: 'EUR',
470
+ });
471
+
472
+ // Works with all endpoints
473
+ const taxon = await client.taxons.get('categories/clothing', {
474
+ includes: 'ancestors',
475
+ }, {
476
+ locale: 'de',
477
+ currency: 'EUR',
478
+ });
479
+ ```
480
+
481
+ ## Error Handling
482
+
483
+ ```typescript
484
+ import { SpreeError } from '@spree/sdk';
485
+
486
+ try {
487
+ await client.products.get('non-existent');
488
+ } catch (error) {
489
+ if (error instanceof SpreeError) {
490
+ console.log(error.code); // 'record_not_found'
491
+ console.log(error.message); // 'Product not found'
492
+ console.log(error.status); // 404
493
+ console.log(error.details); // Validation errors (if any)
494
+ }
495
+ }
496
+ ```
497
+
498
+ ## TypeScript Support
499
+
500
+ The SDK includes full TypeScript support with generated types from the API serializers:
501
+
502
+ ```typescript
503
+ import type {
504
+ StoreProduct,
505
+ StoreOrder,
506
+ StoreVariant,
507
+ StoreTaxon,
508
+ StoreTaxonomy,
509
+ StoreLineItem,
510
+ StoreAddress,
511
+ StoreUser,
512
+ PaginatedResponse,
513
+ } from '@spree/sdk';
514
+
515
+ // All responses are fully typed
516
+ const products: PaginatedResponse<StoreProduct> = await client.products.list();
517
+ const taxon: StoreTaxon = await client.taxons.get('clothing');
518
+ ```
519
+
520
+ ## Available Types
521
+
522
+ The SDK exports all Store API types:
523
+
524
+ ### Core Types
525
+ - `StoreProduct` - Product data
526
+ - `StoreVariant` - Variant data
527
+ - `StoreOrder` - Order/cart data
528
+ - `StoreLineItem` - Line item in cart
529
+ - `StoreTaxonomy` - Category group
530
+ - `StoreTaxon` - Individual category
531
+ - `StoreCountry` - Country with states
532
+ - `StoreState` - State/province
533
+ - `StoreAddress` - Customer address
534
+ - `StoreUser` - Customer profile
535
+ - `StoreStore` - Store configuration
536
+
537
+ ### Commerce Types
538
+ - `StorePayment` - Payment record
539
+ - `StorePaymentMethod` - Payment method
540
+ - `StoreShipment` - Shipment record
541
+ - `StoreShippingRate` - Shipping rate option
542
+ - `StoreShippingMethod` - Shipping method
543
+ - `StoreCreditCard` - Saved credit card
544
+
545
+ ### Product Types
546
+ - `StoreImage` - Product image
547
+ - `StorePrice` - Price data
548
+ - `StoreOptionType` - Option type (e.g., Size, Color)
549
+ - `StoreOptionValue` - Option value (e.g., Small, Red)
550
+ - `StoreDigitalLink` - Digital download link
551
+
552
+ ### Wishlist Types
553
+ - `StoreWishlist` - Wishlist
554
+ - `StoreWishedItem` - Wishlist item
555
+
556
+ ### Utility Types
557
+ - `PaginatedResponse<T>` - Paginated API response
558
+ - `AuthTokens` - JWT tokens from login
559
+ - `AddressParams` - Address input parameters
560
+ - `ProductFiltersResponse` - Product filters response
561
+
562
+ ## Custom Fetch
563
+
564
+ You can provide a custom fetch implementation:
565
+
566
+ ```typescript
567
+ import { createSpreeClient } from '@spree/sdk';
568
+
569
+ const client = createSpreeClient({
570
+ baseUrl: 'https://api.mystore.com',
571
+ apiKey: 'your-api-key',
572
+ fetch: customFetchImplementation,
573
+ });
574
+ ```
575
+
576
+ ## Development
577
+
578
+ ### Setup
579
+
580
+ ```bash
581
+ cd sdk
582
+ npm install
583
+ ```
584
+
585
+ ### Scripts
586
+
587
+ | Command | Description |
588
+ |---------|-------------|
589
+ | `npm test` | Run tests once |
590
+ | `npm run test:watch` | Run tests in watch mode |
591
+ | `npm run test:coverage` | Run tests with coverage report |
592
+ | `npm run typecheck` | Type-check with `tsc --noEmit` |
593
+ | `npm run build` | Build CJS + ESM bundles with `tsup` |
594
+ | `npm run dev` | Build in watch mode |
595
+ | `npm run console` | Interactive REPL for testing the SDK |
596
+
597
+ ### Testing
598
+
599
+ Tests use [Vitest](https://vitest.dev/) with [MSW](https://mswjs.io/) (Mock Service Worker) for API mocking at the network level.
600
+
601
+ ```bash
602
+ # Run all tests
603
+ npm test
604
+
605
+ # Run in watch mode during development
606
+ npm run test:watch
607
+
608
+ # Run with coverage
609
+ npm run test:coverage
610
+ ```
611
+
612
+ Test files live in `tests/` and follow the structure:
613
+
614
+ - `tests/mocks/handlers.ts` - MSW request handlers with fixture data
615
+ - `tests/mocks/server.ts` - MSW server instance
616
+ - `tests/setup.ts` - Server lifecycle (listen/reset/close)
617
+ - `tests/helpers.ts` - `createTestClient()` and constants
618
+ - `tests/*.test.ts` - Test suites per resource (auth, products, orders, etc.)
619
+
620
+ To add tests for a new endpoint, add an MSW handler in `handlers.ts` and create a corresponding test file.
621
+
622
+ ### Releasing
623
+
624
+ This package uses [Changesets](https://github.com/changesets/changesets) for version management and publishing.
625
+
626
+ **After making changes:**
627
+
628
+ ```bash
629
+ npx changeset
630
+ ```
631
+
632
+ This prompts you to select a semver bump type (patch/minor/major) and write a summary. A changeset file is created in `.changeset/`.
633
+
634
+ **How releases work:**
635
+
636
+ 1. Changeset files are committed with your PR
637
+ 2. When merged to `main`, a GitHub Action creates a "Version Packages" PR that bumps the version and updates the CHANGELOG
638
+ 3. When that PR is merged, the package is automatically published to npm
639
+
640
+ **Manual release (if needed):**
641
+
642
+ ```bash
643
+ npm run version # Apply changesets and bump version
644
+ npm run release # Build and publish to npm
645
+ ```
646
+
647
+ ## License
648
+
649
+ BSD-3-Clause