npm package: @omnilink/sdk
Description: Omnilink Node.js SDK — Remote script execution, script management, and tenant operations
Latest version: 1.0.1

# @omnilink/sdk

Enterprise-grade Node.js SDK for Omnilink — remote script execution, script management, and tenant operations.

## Features

- **Await Mode**: `const result = await client.execute({...})` — senkron kullanım
- **Stream Mode**: Event-based execution lifecycle (`start`, `row`, `end`, `error`)
- **Script Management**: Full CRUD, versioning, tenant overrides
- **Tenant Operations**: List, detail, extension queries
- **Enterprise Ready**: Retry with exponential backoff, rate limit handling, typed errors, pluggable logger, abort support
- **Zero Dependencies**: Uses Node.js native `fetch` and `EventEmitter`
- **Dual Build**: ESM + CJS, tree-shakeable, full TypeScript declarations

## Installation

```bash
npm install @omnilink/sdk
# or
yarn add @omnilink/sdk
```

**Requirements**: Node.js >= 20.0.0

## Quick Start

```typescript
import { OmnilinkClient } from '@omnilink/sdk';

const client = new OmnilinkClient({
  apiKey: 'omni_live_abc123...',
  baseUrl: 'https://omnilink.sisteminiz.com.tr',
});

const result = await client.execute({
  tenantId: 'clx123...',
  extensionId: 'mssql-prod',
  scriptPath: 'mssql/reports/sales.sql',
  params: { startDate: '2024-01-01' },
});

console.log(result.data);
```

## Configuration

```typescript
const client = new OmnilinkClient({
  // Required
  apiKey: 'omni_live_...',       // API key (web panel'den oluşturulur)
  baseUrl: 'https://omnilink.sisteminiz.com.tr',

  // Optional
  timeout: 30000,                // Request timeout (ms). Default: 30000
  retries: 3,                    // Retry count for network/5xx errors. Default: 3
  pollInterval: 1000,            // Execute status polling interval (ms). Default: 1000
  maxPollAttempts: 300,          // Max polling attempts (5 min). Default: 300
  logger: customLogger,          // Custom logger instance
  onRequest: (init) => init,     // Request interceptor
  onResponse: (res) => {},       // Response interceptor
});
```

## Execute — Await Mode

The simplest way to run a script and get the result:

```typescript
// Basic usage
const result = await client.execute({
  tenantId: 'clx123...',
  extensionId: 'mssql-prod',
  scriptPath: 'mssql/reports/sales.sql',
  params: { startDate: '2024-01-01', limit: 100 },
});

console.log(result.data);          // Query result
console.log(result.executionId);   // Unique execution ID
console.log(result.durationMs);    // Total duration in ms

// With TypeScript generics for type-safe responses
interface SalesRow {
  id: number;
  amount: number;
  date: string;
}

const typed = await client.execute<{ rows: SalesRow[]; rowCount: number }>({
  tenantId: 'clx123...',
  extensionId: 'mssql-prod',
  scriptPath: 'mssql/reports/sales.sql',
});
// typed.data.rows → SalesRow[]

// With abort support
const controller = new AbortController();
setTimeout(() => controller.abort(), 10000); // 10s timeout

const abortable = await client.execute({
  tenantId: '...',
  extensionId: '...',
  scriptPath: '...',
}, { signal: controller.signal });
```

## Execute — Stream Mode

For real-time execution tracking and row-by-row processing:

```typescript
const stream = client.executeStream({
  tenantId: 'clx123...',
  extensionId: 'mssql-prod',
  scriptPath: 'mssql/reports/large-dataset.sql',
  params: { limit: 10000 },
});

stream.on('start', ({ executionId }) => {
  console.log(`Execution started: ${executionId}`);
});

stream.on('checking_status', ({ attempt, maxAttempts, status }) => {
  console.log(`Polling ${attempt}/${maxAttempts}: ${status}`);
});

stream.on('downloading', ({ downloadUrl }) => {
  console.log('Downloading result from S3...');
});

// Process each SQL row individually (memory-efficient for large datasets)
stream.on('row', (row) => {
  processRow(row);
});

stream.on('end', ({ executionId, durationMs, totalRows }) => {
  console.log(`Done! ${totalRows} rows in ${durationMs}ms`);
});

stream.on('error', (error) => {
  console.error('Failed:', error.code, error.message);
});

// Cancel execution
stream.abort();

// Or use as a promise
const result = await client.executeStream({ ... }).toPromise();
```

## Script Management

Full CRUD operations for scripts, versioning, and tenant overrides.
Requires `manage_scripts` permission on the API key.

```typescript
// List all scripts
const scripts = await client.scripts.list();

// Get script details (with all content versions)
const script = await client.scripts.get('script-id');

// Create a new script
const newScript = await client.scripts.create({
  name: 'Monthly Sales Report',
  categoryTag: 'mssql',
  initialContent: 'SELECT * FROM sales WHERE month = {{month}}',
  initialVersion: 'v1.0.0',
  params: [{ name: 'month', isOptional: false }],
});

// Update script metadata
await client.scripts.update('script-id', {
  name: 'Updated Name',
  description: 'New description',
});

// Delete script
await client.scripts.delete('script-id');
```

### Versioning

```typescript
// List base versions
const versions = await client.scripts.listVersions('script-id');

// Add a new version
await client.scripts.addVersion('script-id', {
  version: 'v2.0.0',
  content: 'SELECT * FROM sales_v2 WHERE month = {{month}}',
  changelog: 'Migration to new table',
  isStable: true,
});

// Mark a version as stable (used for execution)
await client.scripts.setStable('script-id', 'content-id');

// Update content text
await client.scripts.updateContent('script-id', 'content-id', {
  content: 'SELECT * FROM updated_table...',
  params: [{ name: 'month', isOptional: false }],
});

// Delete a content version
await client.scripts.deleteContent('script-id', 'content-id');
```

### Tenant Overrides

Override script content for specific tenants:

```typescript
// List overrides
const overrides = await client.scripts.listOverrides('script-id');
const tenantOverrides = await client.scripts.listOverrides('script-id', {
  tenantId: 'tenant-123',
});

// Add tenant-specific override
await client.scripts.addOverride('script-id', {
  tenantId: 'tenant-123',
  version: 'v1.0.0-custom',
  content: 'SELECT * FROM tenant_specific_table...',
  isStable: true,
});
```

## Tenant Management

List and query tenants and their extensions.
Requires `read_tenants` permission on the API key.

```typescript
// List all tenants
const tenants = await client.tenants.list();
for (const t of tenants) {
  console.log(`${t.machineName} — online: ${t.isOnline}`);
}

// Get tenant details
const tenant = await client.tenants.get('tenant-id');

// Get tenant extensions
const extensions = await client.tenants.extensions('tenant-id');

// Filter by extension type
const mssqlExts = await client.tenants.extensions('tenant-id', { type: 'mssql' });
```

## Error Handling

All errors extend `OmnilinkError` with `code`, `statusCode`, and optional `requestId`:

```typescript
import {
  OmnilinkError,
  AuthenticationError,
  PermissionError,
  ValidationError,
  NotFoundError,
  RateLimitError,
  ExecutionError,
  TimeoutError,
  NetworkError,
  AbortError,
} from '@omnilink/sdk';

try {
  await client.execute({ ... });
} catch (error) {
  if (error instanceof AuthenticationError) {
    // 401 — Invalid or expired API key
  } else if (error instanceof PermissionError) {
    // 403 — Insufficient permissions
  } else if (error instanceof NotFoundError) {
    // 404 — Script, tenant, or execution not found
  } else if (error instanceof RateLimitError) {
    // 429 — Rate limit exceeded
    console.log('Retry after:', error.retryAfter, 'seconds');
  } else if (error instanceof ExecutionError) {
    // Script execution failed
    console.log('Error code:', error.code);
  } else if (error instanceof TimeoutError) {
    // Execution or request timeout
  } else if (error instanceof AbortError) {
    // Request was cancelled
  } else if (error instanceof NetworkError) {
    // Network connectivity issue
  }
}
```

## Advanced

### Custom Logger

Inject a custom logger (compatible with winston, pino, etc.):

```typescript
import { OmnilinkClient, type Logger } from '@omnilink/sdk';

const logger: Logger = {
  debug: (msg, meta) => myLogger.debug(msg, meta),
  info: (msg, meta) => myLogger.info(msg, meta),
  warn: (msg, meta) => myLogger.warn(msg, meta),
  error: (msg, meta) => myLogger.error(msg, meta),
};

const client = new OmnilinkClient({
  apiKey: '...',
  baseUrl: '...',
  logger,
});
```

### Request/Response Interceptors

Add custom logic before each request or after each response:

```typescript
const client = new OmnilinkClient({
  apiKey: '...',
  baseUrl: '...',
  onRequest: (init) => {
    // Add custom headers, log, etc.
    (init.headers as Record<string, string>)['x-correlation-id'] = generateId();
    return init;
  },
  onResponse: async (response) => {
    // Log response times, metrics, etc.
    metrics.track('api_call', { status: response.status });
  },
});
```

### Abort / Cancel

Cancel any in-flight operation using `AbortController`:

```typescript
const controller = new AbortController();

// Cancel after 15 seconds
setTimeout(() => controller.abort(), 15000);

try {
  const result = await client.execute({ ... }, {
    signal: controller.signal,
  });
} catch (error) {
  if (error instanceof AbortError) {
    console.log('Execution was cancelled');
  }
}

// For streams
const stream = client.executeStream({ ... });
stream.abort(); // Cancel at any time
```

## Error Codes

| Code | Description |
|------|-------------|
| `API_KEY_INVALID` | Invalid API key |
| `API_KEY_EXPIRED` | API key has expired |
| `API_KEY_PERMISSION_DENIED` | Insufficient permissions |
| `SCRIPT_NOT_FOUND` | Script not found |
| `SCRIPT_VERSION_NOT_FOUND` | Script version not found |
| `TENANT_NOT_FOUND` | Tenant not found |
| `TENANT_OFFLINE` | Tenant is offline |
| `EXTENSION_NOT_FOUND` | Extension not found |
| `EXECUTION_TIMEOUT` | Execution timed out |
| `EXECUTION_FAILED` | Script execution failed |
| `RATE_LIMIT_EXCEEDED` | Too many requests |
| `VALIDATION_ERROR` | Invalid input data |
| `NETWORK_ERROR` | Network connectivity issue |

## API Key Permissions

| Permission | Endpoints |
|-----------|-----------|
| `execute` | `POST /execute`, `GET /executions/:id` |
| `manage_scripts` | All `/scripts/*` endpoints |
| `read_tenants` | All `/tenants/*` endpoints |

## License

MIT