npm package: @salefony/api-sdk
Description: Salefony API SDK - Official SDK for Salefony Backend API. Full TypeScript support, SSR-ready, Admin & Frontstore resources.
Author: Salefony
Latest version: 1.0.15

# Salefony API SDK

![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![SSR Support](https://img.shields.io/badge/SSR-Ready-success?style=for-the-badge)

Salefony API SDK - Official SDK for Salefony Backend API. Full TypeScript support, SSR-ready, with column autocomplete and Cloudflare-style API design.

---

## Table of Contents

- [Features](#-features)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Configuration](#-configuration)
- [Authentication](#-authentication)
- [Core API Methods](#-core-api-methods)
- [Columns & Autocomplete](#-columns--autocomplete)
- [Filtering & Pagination](#-filtering--pagination)
- [LINQ-style Fluent API](#-linq-style-fluent-api)
- [Response Helpers](#-response-helpers)
- [Admin vs Frontstore](#-admin-vs-frontstore)
- [Integrations (3rd Party OAuth)](#-integrations-3rd-party-oauth)
- [Datasets (Reference Data)](#-datasets-reference-data)
- [SSR Usage](#-ssr-usage)
- [Error Handling](#-error-handling)
- [API Reference](#-api-reference)
- [Troubleshooting](#-troubleshooting)

---

## ✨ Features

| Feature | Description |
|---------|-------------|
| **Full Type Safety** | TypeScript support for models, queries, and column autocomplete |
| **SSR Optimized** | Compatible with Next.js Server Components, Actions, and Nuxt |
| **Cloudflare-style API** | `get`, `list`, `create`, `edit`, `deleteById` syntax |
| **Column Autocomplete** | Typed columns for each resource (e.g. `translations.*`, `parent.id`) |
| **LINQ-style Queries** | Fluent chain: `.where().orderBy().take().toList()` |
| **Response Helpers** | `unwrap()`, `unwrapList()`, `unwrapMeta()` for easy data extraction |
| **Env Fallback** | `createSDKFromEnv()` for automatic config from environment variables |
| **Built-in Retry** | Automatic retry for network and 5xx errors |
| **Debug Mode** | Request/response logging in development |

---

## 📦 Installation

```bash
# Using bun
bun add @salefony/api-sdk

# Using npm
npm install @salefony/api-sdk

# Using pnpm
pnpm add @salefony/api-sdk
```

---

## 🚀 Quick Start

### 1. Create SDK instance

```typescript
import { createSDK, createSDKFromEnv } from '@salefony/api-sdk';

// Option A: Manual configuration
const sdk = createSDK({
  baseUrl: 'https://api.yourdomain.com',
  authToken: 'your-jwt-token',
  debug: true,
});

// Option B: From environment variables (recommended for Next.js)
// Uses: NEXT_PUBLIC_BACKEND_API_URL or BACKEND_API_URL, NODE_ENV
const sdk = createSDKFromEnv({ authToken: process.env.JWT });
```

### 2. Basic CRUD operations

```typescript
// Get single record by ID
const collection = await sdk.admin.collections.get('collection-id-123');

// List records with pagination
const result = await sdk.admin.collections.list({
  filter: { isActive: true },
  paginate: { page: 1, limit: 20 },
  sort: 'order:asc',
});

// Create new record
const created = await sdk.admin.collections.create({
  slug: 'my-collection',
  typeId: 'type-id',
  translations: [{ languageCode: 'en', title: 'My Collection' }],
});

// Update record
await sdk.admin.collections.edit('collection-id', { isActive: false });

// Delete record
await sdk.admin.collections.deleteById('collection-id');
```

### 3. Extract data from response

```typescript
import { unwrap, unwrapList, unwrapMeta } from '@salefony/api-sdk';

const res = await sdk.admin.collections.list({ filter: { isActive: true } });

const items = unwrapList(res);        // res.data ?? [] (always array)
const first = unwrap(res);            // res.data ?? null
const meta = unwrapMeta(res);         // res.meta (pagination info)
const total = meta?.total ?? 0;       // total record count
```

---

## ⚙️ Configuration

### SDK Config Options

```typescript
interface SDKConfig {
  baseUrl: string;           // Required: API base URL
  authToken?: string;       // Bearer token for JWT auth
  vendorKey?: string;       // API key for vendor/marketplace
  debug?: boolean;          // Log requests/responses (default: NODE_ENV === 'development')
  timeout?: number;         // Request timeout in ms
  retries?: number;         // Retry count for failed requests
  headers?: Record<string, string>;  // Custom headers
}
```

### Method chaining

```typescript
sdk
  .setAuthToken('jwt-token')
  .setVendorKey('vnd_xxx')
  .setHeaders({ 'X-Custom-Header': 'value' });

// Clear auth
sdk.logout();
```

---

## 🔐 Authentication

### Admin login (Better Auth)

```typescript
// Sign in with email/password
const { data } = await sdk.admin.auth.mobileLogin({
  email: 'admin@example.com',
  password: 'password',
  provider: 'email',
});

// Set token for subsequent requests
sdk.setAuthToken(data.accessToken);

// Check session
const session = await sdk.admin.auth.getSession();

// Sign out
await sdk.admin.auth.signOut();
sdk.logout();
```

### Passing auth in Next.js

```typescript
// In Server Component or API route
import { cookies } from 'next/headers';

const cookieStore = await cookies();
const token = cookieStore.get('auth-token')?.value;

const sdk = createSDKFromEnv();
if (token) sdk.setAuthToken(token);

const data = await sdk.admin.collections.list();
```

---

## 📋 Core API Methods

Every CRUD resource supports these methods:

| Method | Description | Example |
|--------|-------------|---------|
| `get(id \| params)` | Get single record | `get('id')` or `get({ id, columns })` |
| `list(params?)` | List with filter, paginate, sort | `list({ filter, paginate, sort })` |
| `create(data)` | Create new record | `create({ slug, name, ... })` |
| `edit(id, data)` | Update record | `edit('id', { name: 'New' })` |
| `deleteById(id)` | Delete record | `deleteById('id')` |

### get() – Single record

```typescript
// Simple: get by ID (returns all fields)
const col = await sdk.admin.collections.get('xyz');

// With columns: select specific fields (recommended)
const col = await sdk.admin.collections.get({
  id: 'xyz',
  columns: ['id', 'slug', 'translations.*', 'parent.id', 'type.*'],
  language: 'en',  // Optional: for translations
});

// Response: { data: T, meta?: ListMeta }
const data = col.data;
```

### list() – List records

```typescript
const res = await sdk.admin.collections.list({
  columns: ['id', 'slug', 'order', 'translations.*', 'parent.id'],
  filter: { isActive: true, typeId: 'category' },
  paginate: { page: 1, limit: 20 },
  sort: 'order:asc',
  search: 'keyword',   // Optional: full-text search
  language: 'en',      // Optional: for translations
});

// Response: { data: T[], meta: { total, page, limit, totalPages, ... } }
const items = res.data;
const total = res.meta?.total;
```

### create() – Create record

```typescript
const created = await sdk.admin.collections.create({
  slug: 'new-collection',
  typeId: 'content-type-id',
  parentId: null,
  order: 0,
  isActive: true,
  translations: [
    { languageCode: 'en', title: 'New Collection', description: 'Desc' },
  ],
});
```

### edit() – Update record

```typescript
await sdk.admin.collections.edit('collection-id', {
  isActive: false,
  order: 5,
  translations: [
    { languageCode: 'en', title: 'Updated Title' },
  ],
});
```

### deleteById() – Delete record

```typescript
await sdk.admin.collections.deleteById('collection-id');
// Returns void (204 No Content)
```

---

## 📌 Columns & Autocomplete

Columns use **dot-notation** and are typed per resource for IDE autocomplete.

### Using column helpers (recommended)

```typescript
import {
  collectionColumns,
  contentColumns,
  layoutColumns,
  navigationColumns,
  languageColumns,
  projectColumns,
} from '@salefony/api-sdk';

// TypeScript suggests valid columns
const cols = collectionColumns(
  'id',
  'slug',
  'order',
  'translations.*',
  'parent.id',
  'parent.translations.*',
  'type.id',
  'type.slug',
);

const res = await sdk.admin.collections.list({
  columns: cols,
  filter: { isActive: true },
});
```

### Available column helpers

| Resource | Helper | Example columns |
|----------|--------|-----------------|
| Collections | `collectionColumns` | `id`, `slug`, `translations.*`, `parent.id`, `children.*`, `contents.*` |
| Contents | `contentColumns` | `id`, `slug`, `translations.*`, `collection.*`, `type.*`, `attributes.*` |
| Layouts | `layoutColumns` | `id`, `name`, `slug`, `LayoutTranslation.*`, `layoutData` |
| Navigations | `navigationColumns` | `id`, `name`, `slug`, `items.*`, `translations.*` |
| Languages | `languageColumns` | `id`, `name`, `code`, `isDefault`, `isActive` |
| Projects | `projectColumns` | `id`, `slug`, `sector.*`, `translations.*` |
| Datasource | `datasourceColumns` | `id`, `slug`, `title`, `translations.*`, `Content.*` |
| Metadata | `metadataColumns` | `id`, `slug`, `title`, `type.*`, `entries.*` |
| Vendors | `vendorColumns` | `id`, `name`, `slug`, `collections.*` |
| Store | `storeColumns` | `id`, `name`, `slug`, `defaultLanguage.*` |
| Section | `sectionColumns` | `id`, `name`, `layout.*` |
| Sector | `sectorColumns` | `id`, `slug`, `translations.*` |
| ApiKey | `apiKeyColumns` | `id`, `name`, `key`, `store.*` |

### Generic columns helper

```typescript
import { columns } from '@salefony/api-sdk';
import type { CollectionColumn } from '@salefony/api-sdk';

const cols = columns<CollectionColumn>('id', 'slug', 'translations.*');
```

### Column syntax

| Pattern | Meaning | Example |
|--------|---------|---------|
| `field` | Single scalar field | `id`, `slug`, `name` |
| `relation.field` | Field from relation | `parent.id`, `type.slug` |
| `relation.*` | All fields of relation | `translations.*`, `parent.*` |

---

## 🔍 Filtering & Pagination

### filter

Simple key-value filter (merged into Prisma `where`):

```typescript
filter: {
  isActive: true,
  typeId: 'category-id',
  parentId: null,
  // Nested
  type: { slug: 'blog' },
}
```

### paginate

```typescript
paginate: {
  page: 1,   // 1-based
  limit: 20, // items per page
}
```

### sort

Format: `"field:asc"` or `"field:desc"`:

```typescript
sort: 'order:asc'
sort: 'createdAt:desc'
sort: 'name:asc'
```

### search

Full-text search (backend-dependent):

```typescript
list({ search: 'keyword', language: 'en' })
```

---

## ⛓️ LINQ-style Fluent API

Chain methods for readable queries:

```typescript
// Basic chain
const items = await sdk.admin.collections
  .where({ isActive: true })
  .columns('id', 'slug', 'translations.*')
  .sort('order:asc')
  .take(10)
  .skip(0)
  .toList();

// Start with query()
const items = await sdk.admin.collections
  .query()
  .where({ isActive: true })
  .orderBy({ order: 'asc' })
  .orderByDescending('createdAt')
  .take(20)
  .toList();
```

### LINQ methods

| Method | Description | Example |
|--------|-------------|---------|
| `where(filter)` | Add filter | `.where({ isActive: true })` |
| `columns(...cols)` | Add columns | `.columns('id', 'translations.*')` |
| `filter(obj)` | Alias for where | `.filter({ typeId: 'x' })` |
| `sort(str)` | Set sort | `.sort('order:asc')` |
| `orderBy(obj)` | Ascending | `.orderBy({ order: 'asc' })` |
| `orderByDescending(field)` | Descending | `.orderByDescending('createdAt')` |
| `take(n)` | Limit | `.take(10)` |
| `skip(n)` | Offset | `.skip(20)` |
| `toList()` | Execute, return list | `.toList()` |
| `execute()` | Alias for toList | `.execute()` |
| `first()` | First item, throws if empty | `.first()` |
| `firstOrDefault()` | First or null | `.firstOrDefault()` |
| `single()` | Single item, throws if not 1 | `.single()` |
| `singleOrDefault()` | Single or null | `.singleOrDefault()` |
| `count()` | Count | `.count()` |
| `any()` | Exists? | `.any()` |

### Examples

```typescript
// First matching record
const first = await sdk.admin.collections
  .where({ slug: 'blog' })
  .first();

// Check existence
const exists = await sdk.admin.collections
  .where({ typeId: 'x' })
  .any();

// Count
const count = await sdk.admin.collections
  .where({ isActive: true })
  .count();
```

---

## 📤 Response Helpers

```typescript
import { unwrap, unwrapList, unwrapMeta } from '@salefony/api-sdk';

const res = await sdk.admin.collections.list();

// Extract data with fallback
const data = unwrap(res);           // res.data ?? res ?? null
const items = unwrapList(res);      // res.data ?? [] (always array)
const meta = unwrapMeta(res);       // res.meta

// Pagination
const { total, page, limit, totalPages, hasNextPage } = meta ?? {};
```

---

## 🏢 Admin vs Frontstore

| Namespace | Use case | Example |
|-----------|----------|---------|
| `sdk.admin` | Admin panel, CMS, management | `sdk.admin.collections`, `sdk.admin.contents` |
| `sdk.frontstore` | Public storefront, site | `sdk.frontstore.content`, `sdk.frontstore.collection` |

### Admin resources

- `collections`, `contents`, `layouts`, `navigations`
- `integrations` – 3rd party OAuth (Google Analytics, Facebook, TikTok, etc.)
- `datasets` – reference data (regions/languages, themes, integrations catalog)
- `languages`, `datasource`, `metadata`
- `vendors`, `stores`, `apiKeys`
- `settings`, `seo`, `media`
- `projects`, `sectors`, `sections`
- `cloudflare`, `google`, `themes`
- `subscriptions`, `purchases`, `customData`
- `auth`, `reviews`

### Frontstore resources

- `content`, `collection` – public content/collections
- `navigation`, `media`, `language`
- `store`, `user`, `shop`
- `utility` (settings, sitemap)
- `slugs`, `favorite`, `review`

---

## 🔗 Integrations (3rd Party OAuth)

Connect Google Analytics, Facebook Business, TikTok Pixel, etc. OAuth flow runs on the backend.

```typescript
// Start OAuth – URL oluşturur ve otomatik yönlendirir
sdk.admin.integrations.authorize('google-analytics', '/admin/analytics');

// Check connection status
const { data } = await sdk.admin.integrations.getStatus('google-analytics');

// Disconnect
await sdk.admin.integrations.disconnect('google-analytics');

// Ayarlar (google-analytics: her alan kendi options parametresi ile)
const { data } = await sdk.admin.integrations.getSettings('google-analytics');
// data.fields → [{ key, value, label, type: 'select', options: [...] }]

await sdk.admin.integrations.updateSettings('google-analytics', {
  activeAccountId: '123',
  measurementId: 'G-XXXXXXXXXX',
  uaId: 'UA-123456-1',
});
```

**Platform usage:**

| Platform           | Flow                                                         |
|--------------------|--------------------------------------------------------------|
| React / Next.js    | `authorize('google-analytics', '/admin/analytics')` – otomatik redirect |
| Expo / React Native| `url = authorize(...)` + `Linking.openURL(url)` – sadece URL döner |
| Node.js            | `url = authorize(...)` – sadece URL döner, `res.redirect(url)` |

Full guide: [Integrations Documentation](./docs/INTEGRATIONS.md)

---

## 📊 Datasets (Reference Data)

Dynamic reference data API. Dataset types (regions, themes, integrations, ...) are registered on the backend. New types can be added without API/SDK changes.

```typescript
// List available dataset types (regions, themes, integrations, ...)
const { data } = await sdk.admin.datasets.listTypes();
// data.types → ['regions', 'themes', 'integrations']

// Get dataset by type (works for any registered type)
const { data: regions } = await sdk.admin.datasets.get('regions');
const { data: themes } = await sdk.admin.datasets.get('themes');
const { data: integrations } = await sdk.admin.datasets.get('integrations');

// Type-safe for known types (optional generic)
const { data } = await sdk.admin.datasets.get<Region[]>('regions');
// Future types work without SDK update
const { data: currencies } = await sdk.admin.datasets.get('currencies');
```

---

## 🛠️ SSR Usage

### Next.js Server Component

```typescript
import { createSDKFromEnv, unwrapList } from '@salefony/api-sdk';
import { cookies } from 'next/headers';

export default async function Page() {
  const sdk = createSDKFromEnv();
  const cookieStore = await cookies();
  const token = cookieStore.get('auth-token')?.value;
  if (token) sdk.setAuthToken(token);

  const res = await sdk.admin.collections.list({
    columns: ['id', 'slug', 'translations.*'],
    filter: { isActive: true },
    sort: 'order:asc',
  });

  const items = unwrapList(res);
  return <ul>{items.map((i) => <li key={i.id}>{i.slug}</li>)}</ul>;
}
```

### Next.js API Route

```typescript
// app/api/collections/route.ts
import { NextResponse } from 'next/server';
import { createSDKFromEnv, unwrapList } from '@salefony/api-sdk';
import { getAdminSession } from '@/lib/admin-session';

export async function GET() {
  const session = await getAdminSession();
  if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  const sdk = createSDKFromEnv();
  sdk.setAuthToken(session.accessToken);

  const res = await sdk.admin.collections.list({
    columns: ['id', 'slug', 'order', 'translations.*', 'parent.id'],
    filter: { isActive: true },
    sort: 'order:asc',
  });

  return NextResponse.json(unwrapList(res));
}
```

### Passing headers

```typescript
import { headers } from 'next/headers';

const h = await headers();
sdk.setHeaders({
  cookie: h.get('cookie') ?? '',
  'user-agent': h.get('user-agent') ?? '',
});
```

---

## 🛑 Error Handling

```typescript
import {
  SalefonyError,
  SalefonyAuthError,
  SalefonyNotFoundError,
  SalefonyValidationError,
} from '@salefony/api-sdk';

try {
  await sdk.admin.collections.get('invalid-id');
} catch (error) {
  if (error instanceof SalefonyNotFoundError) {
    console.error(error.message);
    console.error(error.requestContext); // { method, url }
  }
  if (error instanceof SalefonyAuthError) {
    // 401, 403
  }
  if (error instanceof SalefonyValidationError) {
    console.error(error.details); // validation errors
  }
  if (error instanceof SalefonyError) {
    console.error(error.statusCode, error.isClientError, error.isServerError);
  }
}
```

---

## 📂 API Reference

### CRUD methods

| Method | Signature | Returns |
|--------|-----------|---------|
| `get` | `(id \| GetParams) => Promise<ApiResponse<T>>` | Single record |
| `list` | `(ListParams?) => Promise<ApiResponse<T[]>>` | List + meta |
| `create` | `(data) => Promise<ApiResponse<T>>` | Created record |
| `edit` | `(id, data) => Promise<ApiResponse<T>>` | Updated record |
| `deleteById` | `(id) => Promise<void>` | void |

### GetParams

```typescript
interface GetParams {
  id: string;
  columns?: Column | Column[];
  language?: string;
}
```

### ListParams

```typescript
interface ListParams {
  columns?: Column | Column[];
  filter?: Record<string, unknown>;
  paginate?: { page?: number; limit?: number };
  sort?: string;
  search?: string;
  language?: string;
  take?: number;   // Legacy
  skip?: number;   // Legacy
}
```

### ListMeta (pagination)

```typescript
interface ListMeta {
  total?: number;
  page?: number;
  limit?: number;
  totalPages?: number;
  hasNextPage?: boolean;
  hasPreviousPage?: boolean;
}
```

---

## 🔧 Troubleshooting

### "Record not found"

- Check that the ID exists and belongs to the current store.
- Verify auth token and store context.

### Columns not working

- Ensure backend supports `columns` (findMany/findUnique).
- Use correct dot-notation: `translations.*`, `parent.id`.

### CORS / network errors

- Ensure `baseUrl` is correct and reachable.
- SSR: use server-side SDK; do not call from client with sensitive tokens.

### Type errors with columns

- Use resource column helpers: `collectionColumns('id', 'slug', ...)`.
- Or `columns<CollectionColumn>('id', 'slug', ...)`.

---

## 📝 License

This project is proprietary and licensed for commercial use.