@salefony/api-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 ADDED
@@ -0,0 +1,675 @@
1
+ # Salefony API SDK
2
+
3
+ ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
4
+ ![SSR Support](https://img.shields.io/badge/SSR-Ready-success?style=for-the-badge)
5
+
6
+ Salefony API SDK - Official SDK for Salefony Backend API. Full TypeScript support, SSR-ready, with column autocomplete and Cloudflare-style API design.
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ - [Features](#-features)
13
+ - [Installation](#-installation)
14
+ - [Quick Start](#-quick-start)
15
+ - [Configuration](#-configuration)
16
+ - [Authentication](#-authentication)
17
+ - [Core API Methods](#-core-api-methods)
18
+ - [Columns & Autocomplete](#-columns--autocomplete)
19
+ - [Filtering & Pagination](#-filtering--pagination)
20
+ - [LINQ-style Fluent API](#-linq-style-fluent-api)
21
+ - [Response Helpers](#-response-helpers)
22
+ - [Admin vs Frontstore](#-admin-vs-frontstore)
23
+ - [SSR Usage](#-ssr-usage)
24
+ - [Error Handling](#-error-handling)
25
+ - [API Reference](#-api-reference)
26
+ - [Troubleshooting](#-troubleshooting)
27
+
28
+ ---
29
+
30
+ ## ✨ Features
31
+
32
+ | Feature | Description |
33
+ |---------|-------------|
34
+ | **Full Type Safety** | TypeScript support for models, queries, and column autocomplete |
35
+ | **SSR Optimized** | Compatible with Next.js Server Components, Actions, and Nuxt |
36
+ | **Cloudflare-style API** | `get`, `list`, `create`, `edit`, `deleteById` syntax |
37
+ | **Column Autocomplete** | Typed columns for each resource (e.g. `translations.*`, `parent.id`) |
38
+ | **LINQ-style Queries** | Fluent chain: `.where().orderBy().take().toList()` |
39
+ | **Response Helpers** | `unwrap()`, `unwrapList()`, `unwrapMeta()` for easy data extraction |
40
+ | **Env Fallback** | `createSDKFromEnv()` for automatic config from environment variables |
41
+ | **Built-in Retry** | Automatic retry for network and 5xx errors |
42
+ | **Debug Mode** | Request/response logging in development |
43
+
44
+ ---
45
+
46
+ ## 📦 Installation
47
+
48
+ ```bash
49
+ # Using bun
50
+ bun add @salefony/api-sdk
51
+
52
+ # Using npm
53
+ npm install @salefony/api-sdk
54
+
55
+ # Using pnpm
56
+ pnpm add @salefony/api-sdk
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 🚀 Quick Start
62
+
63
+ ### 1. Create SDK instance
64
+
65
+ ```typescript
66
+ import { createSDK, createSDKFromEnv } from '@salefony/api-sdk';
67
+
68
+ // Option A: Manual configuration
69
+ const sdk = createSDK({
70
+ baseUrl: 'https://api.yourdomain.com',
71
+ authToken: 'your-jwt-token',
72
+ debug: true,
73
+ });
74
+
75
+ // Option B: From environment variables (recommended for Next.js)
76
+ // Uses: NEXT_PUBLIC_BACKEND_API_URL or BACKEND_API_URL, NODE_ENV
77
+ const sdk = createSDKFromEnv({ authToken: process.env.JWT });
78
+ ```
79
+
80
+ ### 2. Basic CRUD operations
81
+
82
+ ```typescript
83
+ // Get single record by ID
84
+ const collection = await sdk.admin.collections.get('collection-id-123');
85
+
86
+ // List records with pagination
87
+ const result = await sdk.admin.collections.list({
88
+ filter: { isActive: true },
89
+ paginate: { page: 1, limit: 20 },
90
+ sort: 'order:asc',
91
+ });
92
+
93
+ // Create new record
94
+ const created = await sdk.admin.collections.create({
95
+ slug: 'my-collection',
96
+ typeId: 'type-id',
97
+ translations: [{ languageCode: 'en', title: 'My Collection' }],
98
+ });
99
+
100
+ // Update record
101
+ await sdk.admin.collections.edit('collection-id', { isActive: false });
102
+
103
+ // Delete record
104
+ await sdk.admin.collections.deleteById('collection-id');
105
+ ```
106
+
107
+ ### 3. Extract data from response
108
+
109
+ ```typescript
110
+ import { unwrap, unwrapList, unwrapMeta } from '@salefony/api-sdk';
111
+
112
+ const res = await sdk.admin.collections.list({ filter: { isActive: true } });
113
+
114
+ const items = unwrapList(res); // res.data ?? [] (always array)
115
+ const first = unwrap(res); // res.data ?? null
116
+ const meta = unwrapMeta(res); // res.meta (pagination info)
117
+ const total = meta?.total ?? 0; // total record count
118
+ ```
119
+
120
+ ---
121
+
122
+ ## ⚙️ Configuration
123
+
124
+ ### SDK Config Options
125
+
126
+ ```typescript
127
+ interface SDKConfig {
128
+ baseUrl: string; // Required: API base URL
129
+ authToken?: string; // Bearer token for JWT auth
130
+ vendorKey?: string; // API key for vendor/marketplace
131
+ debug?: boolean; // Log requests/responses (default: NODE_ENV === 'development')
132
+ timeout?: number; // Request timeout in ms
133
+ retries?: number; // Retry count for failed requests
134
+ headers?: Record<string, string>; // Custom headers
135
+ }
136
+ ```
137
+
138
+ ### Method chaining
139
+
140
+ ```typescript
141
+ sdk
142
+ .setAuthToken('jwt-token')
143
+ .setVendorKey('vnd_xxx')
144
+ .setHeaders({ 'X-Custom-Header': 'value' });
145
+
146
+ // Clear auth
147
+ sdk.logout();
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 🔐 Authentication
153
+
154
+ ### Admin login (Better Auth)
155
+
156
+ ```typescript
157
+ // Sign in with email/password
158
+ const { data } = await sdk.admin.auth.mobileLogin({
159
+ email: 'admin@example.com',
160
+ password: 'password',
161
+ provider: 'email',
162
+ });
163
+
164
+ // Set token for subsequent requests
165
+ sdk.setAuthToken(data.accessToken);
166
+
167
+ // Check session
168
+ const session = await sdk.admin.auth.getSession();
169
+
170
+ // Sign out
171
+ await sdk.admin.auth.signOut();
172
+ sdk.logout();
173
+ ```
174
+
175
+ ### Passing auth in Next.js
176
+
177
+ ```typescript
178
+ // In Server Component or API route
179
+ import { cookies } from 'next/headers';
180
+
181
+ const cookieStore = await cookies();
182
+ const token = cookieStore.get('auth-token')?.value;
183
+
184
+ const sdk = createSDKFromEnv();
185
+ if (token) sdk.setAuthToken(token);
186
+
187
+ const data = await sdk.admin.collections.list();
188
+ ```
189
+
190
+ ---
191
+
192
+ ## 📋 Core API Methods
193
+
194
+ Every CRUD resource supports these methods:
195
+
196
+ | Method | Description | Example |
197
+ |--------|-------------|---------|
198
+ | `get(id \| params)` | Get single record | `get('id')` or `get({ id, columns })` |
199
+ | `list(params?)` | List with filter, paginate, sort | `list({ filter, paginate, sort })` |
200
+ | `create(data)` | Create new record | `create({ slug, name, ... })` |
201
+ | `edit(id, data)` | Update record | `edit('id', { name: 'New' })` |
202
+ | `deleteById(id)` | Delete record | `deleteById('id')` |
203
+
204
+ ### get() – Single record
205
+
206
+ ```typescript
207
+ // Simple: get by ID (returns all fields)
208
+ const col = await sdk.admin.collections.get('xyz');
209
+
210
+ // With columns: select specific fields (recommended)
211
+ const col = await sdk.admin.collections.get({
212
+ id: 'xyz',
213
+ columns: ['id', 'slug', 'translations.*', 'parent.id', 'type.*'],
214
+ language: 'en', // Optional: for translations
215
+ });
216
+
217
+ // Response: { data: T, meta?: ListMeta }
218
+ const data = col.data;
219
+ ```
220
+
221
+ ### list() – List records
222
+
223
+ ```typescript
224
+ const res = await sdk.admin.collections.list({
225
+ columns: ['id', 'slug', 'order', 'translations.*', 'parent.id'],
226
+ filter: { isActive: true, typeId: 'category' },
227
+ paginate: { page: 1, limit: 20 },
228
+ sort: 'order:asc',
229
+ search: 'keyword', // Optional: full-text search
230
+ language: 'en', // Optional: for translations
231
+ });
232
+
233
+ // Response: { data: T[], meta: { total, page, limit, totalPages, ... } }
234
+ const items = res.data;
235
+ const total = res.meta?.total;
236
+ ```
237
+
238
+ ### create() – Create record
239
+
240
+ ```typescript
241
+ const created = await sdk.admin.collections.create({
242
+ slug: 'new-collection',
243
+ typeId: 'content-type-id',
244
+ parentId: null,
245
+ order: 0,
246
+ isActive: true,
247
+ translations: [
248
+ { languageCode: 'en', title: 'New Collection', description: 'Desc' },
249
+ ],
250
+ });
251
+ ```
252
+
253
+ ### edit() – Update record
254
+
255
+ ```typescript
256
+ await sdk.admin.collections.edit('collection-id', {
257
+ isActive: false,
258
+ order: 5,
259
+ translations: [
260
+ { languageCode: 'en', title: 'Updated Title' },
261
+ ],
262
+ });
263
+ ```
264
+
265
+ ### deleteById() – Delete record
266
+
267
+ ```typescript
268
+ await sdk.admin.collections.deleteById('collection-id');
269
+ // Returns void (204 No Content)
270
+ ```
271
+
272
+ ---
273
+
274
+ ## 📌 Columns & Autocomplete
275
+
276
+ Columns use **dot-notation** and are typed per resource for IDE autocomplete.
277
+
278
+ ### Using column helpers (recommended)
279
+
280
+ ```typescript
281
+ import {
282
+ collectionColumns,
283
+ contentColumns,
284
+ layoutColumns,
285
+ navigationColumns,
286
+ languageColumns,
287
+ projectColumns,
288
+ } from '@salefony/api-sdk';
289
+
290
+ // TypeScript suggests valid columns
291
+ const cols = collectionColumns(
292
+ 'id',
293
+ 'slug',
294
+ 'order',
295
+ 'translations.*',
296
+ 'parent.id',
297
+ 'parent.translations.*',
298
+ 'type.id',
299
+ 'type.slug',
300
+ );
301
+
302
+ const res = await sdk.admin.collections.list({
303
+ columns: cols,
304
+ filter: { isActive: true },
305
+ });
306
+ ```
307
+
308
+ ### Available column helpers
309
+
310
+ | Resource | Helper | Example columns |
311
+ |----------|--------|-----------------|
312
+ | Collections | `collectionColumns` | `id`, `slug`, `translations.*`, `parent.id`, `children.*`, `contents.*` |
313
+ | Contents | `contentColumns` | `id`, `slug`, `translations.*`, `collection.*`, `type.*`, `attributes.*` |
314
+ | Layouts | `layoutColumns` | `id`, `name`, `slug`, `LayoutTranslation.*`, `layoutData` |
315
+ | Navigations | `navigationColumns` | `id`, `name`, `slug`, `items.*`, `translations.*` |
316
+ | Languages | `languageColumns` | `id`, `name`, `code`, `isDefault`, `isActive` |
317
+ | Projects | `projectColumns` | `id`, `slug`, `sector.*`, `translations.*` |
318
+ | Datasource | `datasourceColumns` | `id`, `slug`, `title`, `translations.*`, `Content.*` |
319
+ | Metadata | `metadataColumns` | `id`, `slug`, `title`, `type.*`, `entries.*` |
320
+ | Vendors | `vendorColumns` | `id`, `name`, `slug`, `collections.*` |
321
+ | Store | `storeColumns` | `id`, `name`, `slug`, `defaultLanguage.*` |
322
+ | Section | `sectionColumns` | `id`, `name`, `layout.*` |
323
+ | Sector | `sectorColumns` | `id`, `slug`, `translations.*` |
324
+ | ApiKey | `apiKeyColumns` | `id`, `name`, `key`, `store.*` |
325
+
326
+ ### Generic columns helper
327
+
328
+ ```typescript
329
+ import { columns } from '@salefony/api-sdk';
330
+ import type { CollectionColumn } from '@salefony/api-sdk';
331
+
332
+ const cols = columns<CollectionColumn>('id', 'slug', 'translations.*');
333
+ ```
334
+
335
+ ### Column syntax
336
+
337
+ | Pattern | Meaning | Example |
338
+ |--------|---------|---------|
339
+ | `field` | Single scalar field | `id`, `slug`, `name` |
340
+ | `relation.field` | Field from relation | `parent.id`, `type.slug` |
341
+ | `relation.*` | All fields of relation | `translations.*`, `parent.*` |
342
+
343
+ ---
344
+
345
+ ## 🔍 Filtering & Pagination
346
+
347
+ ### filter
348
+
349
+ Simple key-value filter (merged into Prisma `where`):
350
+
351
+ ```typescript
352
+ filter: {
353
+ isActive: true,
354
+ typeId: 'category-id',
355
+ parentId: null,
356
+ // Nested
357
+ type: { slug: 'blog' },
358
+ }
359
+ ```
360
+
361
+ ### paginate
362
+
363
+ ```typescript
364
+ paginate: {
365
+ page: 1, // 1-based
366
+ limit: 20, // items per page
367
+ }
368
+ ```
369
+
370
+ ### sort
371
+
372
+ Format: `"field:asc"` or `"field:desc"`:
373
+
374
+ ```typescript
375
+ sort: 'order:asc'
376
+ sort: 'createdAt:desc'
377
+ sort: 'name:asc'
378
+ ```
379
+
380
+ ### search
381
+
382
+ Full-text search (backend-dependent):
383
+
384
+ ```typescript
385
+ list({ search: 'keyword', language: 'en' })
386
+ ```
387
+
388
+ ---
389
+
390
+ ## ⛓️ LINQ-style Fluent API
391
+
392
+ Chain methods for readable queries:
393
+
394
+ ```typescript
395
+ // Basic chain
396
+ const items = await sdk.admin.collections
397
+ .where({ isActive: true })
398
+ .columns('id', 'slug', 'translations.*')
399
+ .sort('order:asc')
400
+ .take(10)
401
+ .skip(0)
402
+ .toList();
403
+
404
+ // Start with query()
405
+ const items = await sdk.admin.collections
406
+ .query()
407
+ .where({ isActive: true })
408
+ .orderBy({ order: 'asc' })
409
+ .orderByDescending('createdAt')
410
+ .take(20)
411
+ .toList();
412
+ ```
413
+
414
+ ### LINQ methods
415
+
416
+ | Method | Description | Example |
417
+ |--------|-------------|---------|
418
+ | `where(filter)` | Add filter | `.where({ isActive: true })` |
419
+ | `columns(...cols)` | Add columns | `.columns('id', 'translations.*')` |
420
+ | `filter(obj)` | Alias for where | `.filter({ typeId: 'x' })` |
421
+ | `sort(str)` | Set sort | `.sort('order:asc')` |
422
+ | `orderBy(obj)` | Ascending | `.orderBy({ order: 'asc' })` |
423
+ | `orderByDescending(field)` | Descending | `.orderByDescending('createdAt')` |
424
+ | `take(n)` | Limit | `.take(10)` |
425
+ | `skip(n)` | Offset | `.skip(20)` |
426
+ | `toList()` | Execute, return list | `.toList()` |
427
+ | `execute()` | Alias for toList | `.execute()` |
428
+ | `first()` | First item, throws if empty | `.first()` |
429
+ | `firstOrDefault()` | First or null | `.firstOrDefault()` |
430
+ | `single()` | Single item, throws if not 1 | `.single()` |
431
+ | `singleOrDefault()` | Single or null | `.singleOrDefault()` |
432
+ | `count()` | Count | `.count()` |
433
+ | `any()` | Exists? | `.any()` |
434
+
435
+ ### Examples
436
+
437
+ ```typescript
438
+ // First matching record
439
+ const first = await sdk.admin.collections
440
+ .where({ slug: 'blog' })
441
+ .first();
442
+
443
+ // Check existence
444
+ const exists = await sdk.admin.collections
445
+ .where({ typeId: 'x' })
446
+ .any();
447
+
448
+ // Count
449
+ const count = await sdk.admin.collections
450
+ .where({ isActive: true })
451
+ .count();
452
+ ```
453
+
454
+ ---
455
+
456
+ ## 📤 Response Helpers
457
+
458
+ ```typescript
459
+ import { unwrap, unwrapList, unwrapMeta } from '@salefony/api-sdk';
460
+
461
+ const res = await sdk.admin.collections.list();
462
+
463
+ // Extract data with fallback
464
+ const data = unwrap(res); // res.data ?? res ?? null
465
+ const items = unwrapList(res); // res.data ?? [] (always array)
466
+ const meta = unwrapMeta(res); // res.meta
467
+
468
+ // Pagination
469
+ const { total, page, limit, totalPages, hasNextPage } = meta ?? {};
470
+ ```
471
+
472
+ ---
473
+
474
+ ## 🏢 Admin vs Frontstore
475
+
476
+ | Namespace | Use case | Example |
477
+ |-----------|----------|---------|
478
+ | `sdk.admin` | Admin panel, CMS, management | `sdk.admin.collections`, `sdk.admin.contents` |
479
+ | `sdk.frontstore` | Public storefront, site | `sdk.frontstore.content`, `sdk.frontstore.collection` |
480
+
481
+ ### Admin resources
482
+
483
+ - `collections`, `contents`, `layouts`, `navigations`
484
+ - `languages`, `datasource`, `metadata`
485
+ - `vendors`, `stores`, `apiKeys`
486
+ - `settings`, `seo`, `media`
487
+ - `projects`, `sectors`, `sections`
488
+ - `cloudflare`, `google`, `themes`
489
+ - `subscriptions`, `purchases`, `customData`
490
+ - `auth`, `reviews`
491
+
492
+ ### Frontstore resources
493
+
494
+ - `content`, `collection` – public content/collections
495
+ - `navigation`, `media`, `language`
496
+ - `store`, `user`, `shop`
497
+ - `utility` (settings, sitemap)
498
+ - `slugs`, `favorite`, `review`
499
+
500
+ ---
501
+
502
+ ## 🛠️ SSR Usage
503
+
504
+ ### Next.js Server Component
505
+
506
+ ```typescript
507
+ import { createSDKFromEnv, unwrapList } from '@salefony/api-sdk';
508
+ import { cookies } from 'next/headers';
509
+
510
+ export default async function Page() {
511
+ const sdk = createSDKFromEnv();
512
+ const cookieStore = await cookies();
513
+ const token = cookieStore.get('auth-token')?.value;
514
+ if (token) sdk.setAuthToken(token);
515
+
516
+ const res = await sdk.admin.collections.list({
517
+ columns: ['id', 'slug', 'translations.*'],
518
+ filter: { isActive: true },
519
+ sort: 'order:asc',
520
+ });
521
+
522
+ const items = unwrapList(res);
523
+ return <ul>{items.map((i) => <li key={i.id}>{i.slug}</li>)}</ul>;
524
+ }
525
+ ```
526
+
527
+ ### Next.js API Route
528
+
529
+ ```typescript
530
+ // app/api/collections/route.ts
531
+ import { NextResponse } from 'next/server';
532
+ import { createSDKFromEnv, unwrapList } from '@salefony/api-sdk';
533
+ import { getAdminSession } from '@/lib/admin-session';
534
+
535
+ export async function GET() {
536
+ const session = await getAdminSession();
537
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
538
+
539
+ const sdk = createSDKFromEnv();
540
+ sdk.setAuthToken(session.accessToken);
541
+
542
+ const res = await sdk.admin.collections.list({
543
+ columns: ['id', 'slug', 'order', 'translations.*', 'parent.id'],
544
+ filter: { isActive: true },
545
+ sort: 'order:asc',
546
+ });
547
+
548
+ return NextResponse.json(unwrapList(res));
549
+ }
550
+ ```
551
+
552
+ ### Passing headers
553
+
554
+ ```typescript
555
+ import { headers } from 'next/headers';
556
+
557
+ const h = await headers();
558
+ sdk.setHeaders({
559
+ cookie: h.get('cookie') ?? '',
560
+ 'user-agent': h.get('user-agent') ?? '',
561
+ });
562
+ ```
563
+
564
+ ---
565
+
566
+ ## 🛑 Error Handling
567
+
568
+ ```typescript
569
+ import {
570
+ SalefonyError,
571
+ SalefonyAuthError,
572
+ SalefonyNotFoundError,
573
+ SalefonyValidationError,
574
+ } from '@salefony/api-sdk';
575
+
576
+ try {
577
+ await sdk.admin.collections.get('invalid-id');
578
+ } catch (error) {
579
+ if (error instanceof SalefonyNotFoundError) {
580
+ console.error(error.message);
581
+ console.error(error.requestContext); // { method, url }
582
+ }
583
+ if (error instanceof SalefonyAuthError) {
584
+ // 401, 403
585
+ }
586
+ if (error instanceof SalefonyValidationError) {
587
+ console.error(error.details); // validation errors
588
+ }
589
+ if (error instanceof SalefonyError) {
590
+ console.error(error.statusCode, error.isClientError, error.isServerError);
591
+ }
592
+ }
593
+ ```
594
+
595
+ ---
596
+
597
+ ## 📂 API Reference
598
+
599
+ ### CRUD methods
600
+
601
+ | Method | Signature | Returns |
602
+ |--------|-----------|---------|
603
+ | `get` | `(id \| GetParams) => Promise<ApiResponse<T>>` | Single record |
604
+ | `list` | `(ListParams?) => Promise<ApiResponse<T[]>>` | List + meta |
605
+ | `create` | `(data) => Promise<ApiResponse<T>>` | Created record |
606
+ | `edit` | `(id, data) => Promise<ApiResponse<T>>` | Updated record |
607
+ | `deleteById` | `(id) => Promise<void>` | void |
608
+
609
+ ### GetParams
610
+
611
+ ```typescript
612
+ interface GetParams {
613
+ id: string;
614
+ columns?: Column | Column[];
615
+ language?: string;
616
+ }
617
+ ```
618
+
619
+ ### ListParams
620
+
621
+ ```typescript
622
+ interface ListParams {
623
+ columns?: Column | Column[];
624
+ filter?: Record<string, unknown>;
625
+ paginate?: { page?: number; limit?: number };
626
+ sort?: string;
627
+ search?: string;
628
+ language?: string;
629
+ take?: number; // Legacy
630
+ skip?: number; // Legacy
631
+ }
632
+ ```
633
+
634
+ ### ListMeta (pagination)
635
+
636
+ ```typescript
637
+ interface ListMeta {
638
+ total?: number;
639
+ page?: number;
640
+ limit?: number;
641
+ totalPages?: number;
642
+ hasNextPage?: boolean;
643
+ hasPreviousPage?: boolean;
644
+ }
645
+ ```
646
+
647
+ ---
648
+
649
+ ## 🔧 Troubleshooting
650
+
651
+ ### "Record not found"
652
+
653
+ - Check that the ID exists and belongs to the current store.
654
+ - Verify auth token and store context.
655
+
656
+ ### Columns not working
657
+
658
+ - Ensure backend supports `columns` (findMany/findUnique).
659
+ - Use correct dot-notation: `translations.*`, `parent.id`.
660
+
661
+ ### CORS / network errors
662
+
663
+ - Ensure `baseUrl` is correct and reachable.
664
+ - SSR: use server-side SDK; do not call from client with sensitive tokens.
665
+
666
+ ### Type errors with columns
667
+
668
+ - Use resource column helpers: `collectionColumns('id', 'slug', ...)`.
669
+ - Or `columns<CollectionColumn>('id', 'slug', ...)`.
670
+
671
+ ---
672
+
673
+ ## 📝 License
674
+
675
+ This project is proprietary and licensed for commercial use.