@ketrics/sdk-backend 0.7.0 → 0.8.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 CHANGED
@@ -1,1513 +1,720 @@
1
- # @ketrics/sdk-backend
2
-
3
- TypeScript type definitions for building tenant applications on the Ketrics platform.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @ketrics/sdk-backend
9
- ```
1
+ # Ketrics SDK for Backend (@ketrics/sdk-backend)
10
2
 
11
3
  ## Overview
12
4
 
13
- The SDK provides TypeScript types for the global `ketrics` object that is automatically injected into your tenant application code at runtime. Everything is accessible via the `ketrics` object:
14
-
15
- - **Context**: `ketrics.tenant`, `ketrics.application`, `ketrics.user`, `ketrics.env`, `ketrics.environment`
16
- - **Utilities**: `ketrics.console`, `ketrics.http`
17
- - **Volume**: `ketrics.Volume.connect()` for S3-backed storage
18
- - **Database**: `ketrics.DatabaseConnection.connect()` for external database access
19
- - **Secrets**: `ketrics.Secret.get()` for encrypted secrets
20
- - **Excel**: `ketrics.Excel.read()` and `ketrics.Excel.create()` for Excel files
21
- - **PDF**: `ketrics.Pdf.read()` and `ketrics.Pdf.create()` for PDF files
22
- - **Error classes**: `ketrics.VolumeNotFoundError`, `ketrics.DatabaseError`, etc. for `instanceof` checks
23
-
24
- ---
25
-
26
- ## Quick Start
27
-
28
- ```typescript
29
- // Import types only if needed for type annotations
30
- import type { FileContent, IVolume, IDatabaseConnection } from '@ketrics/sdk-backend';
31
-
32
- export async function handler() {
33
- // Context is always available
34
- console.log('Tenant:', ketrics.tenant.name);
35
- console.log('User:', ketrics.user.email);
36
- console.log('App:', ketrics.application.code);
37
-
38
- // Access S3 volumes
39
- const volume = await ketrics.Volume.connect('uploads');
40
- const file = await volume.get('report.pdf');
41
-
42
- // Access external databases
43
- const db = await ketrics.DatabaseConnection.connect('main-db');
44
- const result = await db.query('SELECT * FROM users');
45
- await db.close();
46
-
47
- // Access encrypted secrets
48
- const apiKey = await ketrics.Secret.get('stripe-api-key');
49
-
50
- // Read/write Excel files
51
- const workbook = await ketrics.Excel.read(file.content);
52
- const sheet = workbook.getWorksheet('Sheet1');
53
-
54
- return { success: true };
55
- }
56
- ```
57
-
58
- ---
59
-
60
- ## Context Properties (Read-only)
61
-
62
- ### Tenant Context
63
-
64
- Access information about the current tenant:
65
-
66
- ```typescript
67
- ketrics.tenant.id // Tenant UUID (e.g., "550e8400-e29b-41d4-a716-446655440000")
68
- ketrics.tenant.code // Tenant code/slug (e.g., "acme-corp")
69
- ketrics.tenant.name // Tenant display name (e.g., "ACME Corporation")
70
- ```
71
-
72
- ### Application Context
73
-
74
- Access information about the current application:
75
-
76
- ```typescript
77
- ketrics.application.id // Application UUID
78
- ketrics.application.code // Application code (e.g., "inventory")
79
- ketrics.application.name // Application display name (e.g., "Inventory Manager")
80
- ketrics.application.version // Application version (optional)
81
- ketrics.application.deploymentId // Current deployment ID
82
- ```
83
-
84
- ### User Context
85
-
86
- Access information about the user making the request:
87
-
88
- ```typescript
89
- ketrics.user.id // User UUID
90
- ketrics.user.email // User email (e.g., "john@example.com")
91
- ketrics.user.name // User full name (e.g., "John Doe")
92
- ketrics.user.isAdmin // Whether user is tenant admin (boolean)
93
- ```
94
-
95
- ### Environment Context
96
-
97
- Access runtime environment information:
98
-
99
- ```typescript
100
- ketrics.env.nodeVersion // Node.js version (e.g., "v18.17.0")
101
- ketrics.env.runtime // Runtime platform ("ecs-fargate")
102
- ketrics.env.region // AWS region (e.g., "us-east-1")
103
- ```
104
-
105
- ### Environment Variables
106
-
107
- Access application-level environment variables configured in the Ketrics portal:
108
-
109
- ```typescript
110
- // Access custom environment variables set for your application
111
- const apiEndpoint = ketrics.environment['API_ENDPOINT'];
112
- const debugMode = ketrics.environment['DEBUG_MODE'];
113
- const maxRetries = ketrics.environment['MAX_RETRIES'];
114
-
115
- // Example: Configure behavior based on env vars
116
- if (ketrics.environment['FEATURE_FLAG_NEW_UI'] === 'true') {
117
- // Use new UI logic
118
- }
119
- ```
120
-
121
- ---
122
-
123
- ## Console Logging
124
-
125
- Logs are automatically forwarded to CloudWatch with proper context (tenant, application, user, request ID):
126
-
127
- ```typescript
128
- ketrics.console.log('Processing order...');
129
- ketrics.console.info('Order received', { orderId: 123 });
130
- ketrics.console.warn('Rate limit approaching');
131
- ketrics.console.error('Failed to process order', { error: err.message });
132
- ketrics.console.debug('Debug info', { state: currentState });
133
- ```
134
-
135
- All log levels are supported:
136
- - `log()` - General logging
137
- - `info()` - Informational messages
138
- - `warn()` - Warning messages
139
- - `error()` - Error messages
140
- - `debug()` - Debug messages (may be filtered in production)
141
-
142
- ---
143
-
144
- ## HTTP Client
145
-
146
- Make external API requests with built-in timeout and user agent:
5
+ The Ketrics SDK for Backend is a TypeScript type definition library that provides the complete interface for building tenant applications on the Ketrics platform. This SDK defines the `ketrics` global object—a comprehensive runtime API that tenant code uses to access platform features including storage, databases, secrets management, document generation, background jobs, and messaging.
6
+
7
+ **Purpose**: This package serves as the type contract between tenant application code and the Ketrics runtime execution environment. It is published as an npm package and consumed by tenant developers building applications that run in isolated VM sandboxes.
8
+
9
+ **Architecture Context**:
10
+ - Tenant applications run in ECS Fargate containers with Node.js 24+
11
+ - The global `ketrics` object is injected at runtime by the Ketrics platform
12
+ - This SDK provides TypeScript definitions so tenant code has IDE support and type safety
13
+ - The SDK is a pure type definition library (no implementation)
14
+
15
+ **Key Responsibilities**:
16
+ 1. Define all TypeScript interfaces and types for the `ketrics` global object
17
+ 2. Provide comprehensive error classes for all SDK operations
18
+ 3. Establish the contract between tenant code and the Ketrics runtime
19
+ 4. Enable type-safe access to platform capabilities (storage, databases, secrets, etc.)
20
+
21
+ ## Business Logic
22
+
23
+ ### What Problem Does This Solve?
24
+
25
+ Tenant applications need standardized, type-safe access to platform infrastructure without direct access to underlying services. This SDK enables developers to:
26
+
27
+ - Read/write files to S3-backed volumes with permission controls
28
+ - Query external databases with transaction support
29
+ - Retrieve encrypted secrets via KMS
30
+ - Generate PDF and Excel documents programmatically
31
+ - Execute long-running tasks asynchronously
32
+ - Send messages to users with multiple channels (inbox, push notifications)
33
+ - Make external API calls with a standardized HTTP client
34
+ - Access tenant and application metadata
35
+ - Log to CloudWatch with proper context
36
+
37
+ ### Core Workflows and Processes
38
+
39
+ **1. Storage Access (Volumes)**
40
+ - Connect to an S3-backed volume by code
41
+ - Get files (with content, metadata, etag)
42
+ - Put files (with metadata and conditional create)
43
+ - Delete files (single or by prefix)
44
+ - List files with pagination and filtering
45
+ - Copy/move files within volumes
46
+ - Generate presigned URLs for direct browser access
47
+
48
+ **2. Database Operations**
49
+ - Connect to external databases by code
50
+ - Execute SELECT queries with typed results
51
+ - Execute INSERT/UPDATE/DELETE with affected row counts
52
+ - Run queries within transactions (auto-commit on success, auto-rollback on error)
53
+ - Connection pooling and lifecycle management
54
+
55
+ **3. Secrets Management**
56
+ - Retrieve encrypted secrets by code
57
+ - Check if a secret exists before accessing
58
+ - Secrets are decrypted using tenant-specific KMS keys
59
+ - Subject to application access grants
60
+
61
+ **4. Document Generation**
62
+ - **Excel**: Read/parse existing Excel files, create new workbooks, add worksheets, format cells, write files
63
+ - **PDF**: Read/parse existing PDFs, create new documents, draw text/shapes/images, embed fonts, write files
64
+
65
+ **5. Background Jobs**
66
+ - Queue async function execution with payloads
67
+ - Support cross-app jobs with permission validation
68
+ - Track job status (pending, running, completed, failed)
69
+ - Idempotency keys to prevent duplicate execution
70
+ - Configurable timeouts (default 5min, max 15min)
71
+
72
+ **6. User Messaging**
73
+ - Send messages to individual users
74
+ - Bulk send to multiple users
75
+ - Send to group members (requires IAM-data permissions)
76
+ - Support for multiple channels: inbox (always) + push notifications (optional)
77
+ - Priority levels: LOW, MEDIUM, HIGH
78
+ - Custom action URLs for deep linking
79
+
80
+ ### Business Rules Implemented
81
+
82
+ **Access Control**:
83
+ - Volume access requires an access grant to the specific volume
84
+ - Database access requires an access grant with specific permissions
85
+ - Secrets access requires an access grant with decrypt permission
86
+ - Messages to groups require IAM-data tenant permissions (via application role)
87
+ - Cross-app jobs require application grants with background job permission
88
+
89
+ **Data Boundaries**:
90
+ - Requestor context distinguishes between users (type: "USER") and service accounts (type: "SERVICE_ACCOUNT")
91
+ - Requestor's application permissions determine what operations are allowed
92
+ - Each tenant's secrets are encrypted with tenant-specific KMS keys
93
+
94
+ **Input/Output Expectations**:
95
+ - File operations support streams, buffers, and strings
96
+ - Database queries return generic typed result objects
97
+ - Job operations return UUID job identifiers
98
+ - List operations support pagination with cursors
99
+ - All timestamps are ISO 8601 strings
100
+
101
+ **Edge Cases Handled**:
102
+ - File already exists with `ifNotExists` option
103
+ - File size limits enforced on put operations
104
+ - Content type restrictions on volumes
105
+ - Invalid path characters and path traversal detection
106
+ - Connection timeouts with retry semantics
107
+ - Null/undefined secret values
108
+ - Transaction rollback on error
109
+ - Job timeout after max execution time
110
+ - Bulk messaging with partial failures
111
+
112
+ ## Technical Details
113
+
114
+ ### Technology Stack and Dependencies
115
+
116
+ **Runtime Environment**:
117
+ - Node.js 24.0.0+ (specified in package.json engines)
118
+ - TypeScript 5.0.0+ for compilation
119
+ - ES2020 target with CommonJS module format
120
+
121
+ **Package Dependencies**:
122
+ - `@types/node@^24.10.2` - Node.js type definitions for stream types (Readable)
123
+ - `typescript@^5.0.0` - TypeScript compiler (dev dependency)
124
+
125
+ **Key Type System Features**:
126
+ - Strict mode enabled for type safety
127
+ - Declaration maps for source map debugging
128
+ - Module resolution set to "node" for standard npm resolution
129
+ - Force consistent casing for file imports
130
+
131
+ ### File Structure with Purpose
132
+
133
+ ```
134
+ src/
135
+ ├── index.ts # Main entry point, exports all public types and interfaces
136
+ ├── context.ts # Runtime context types (tenant, app, requestor, environment)
137
+ ├── console.ts # Console logger interface
138
+ ├── http.ts # HTTP client interface for external API calls
139
+ ├── databases.ts # Database connection interfaces and types
140
+ ├── database-errors.ts # Database error class hierarchy
141
+ ├── volumes.ts # Volume storage interfaces and types
142
+ ├── errors.ts # Volume error class hierarchy
143
+ ├── secrets.ts # Secret retrieval interfaces
144
+ ├── secret-errors.ts # Secret error classes
145
+ ├── excel.ts # Excel workbook interfaces and types
146
+ ├── excel-errors.ts # Excel error classes
147
+ ├── pdf.ts # PDF document interfaces and types
148
+ ├── pdf-errors.ts # PDF error classes
149
+ ├── job.ts # Background job types and interfaces
150
+ ├── job-errors.ts # Job execution error classes
151
+ ├── messages.ts # Message sending interfaces and types
152
+ ├── messages-errors.ts # Message error classes
153
+ └── dist/ # Compiled JavaScript and .d.ts files (generated)
154
+ ```
155
+
156
+ ### Key Functions/Classes and Their Purposes
157
+
158
+ **Context Interfaces** (`context.ts`):
159
+ - `TenantContext`: Tenant ID, code, and name
160
+ - `ApplicationContext`: Application ID, code, name, version, deployment ID
161
+ - `RequestorContext`: Identifies if request is from USER or SERVICE_ACCOUNT with permissions
162
+ - `RuntimeContext`: Node.js version, AWS region, runtime platform
163
+ - `EnvironmentVariables`: Tenant application environment variables
164
+
165
+ **SDK Manager Classes** (defined in index.ts as static interfaces):
166
+
167
+ 1. **Volume** - S3-backed file storage
168
+ - `connect(volumeCode: string): Promise<IVolume>`
169
+ - Methods: get, put, delete, deleteByPrefix, list, copy, move, downloadUrl, uploadUrl
170
+
171
+ 2. **DatabaseConnection** - External database access
172
+ - `connect(databaseCode: string): Promise<IDatabaseConnection>`
173
+ - Methods: query<T>(), execute(), transaction(), close()
174
+
175
+ 3. **Secret** - Encrypted secret retrieval
176
+ - `get(secretCode: string): Promise<string>`
177
+ - `exists(secretCode: string): Promise<boolean>`
178
+
179
+ 4. **Excel** - Excel file operations
180
+ - `read(buffer: Buffer): Promise<IExcelWorkbook>`
181
+ - `create(): IExcelWorkbook`
182
+ - Workbook methods: getWorksheet, addWorksheet, writeFile
183
+
184
+ 5. **Pdf** - PDF document operations
185
+ - `read(buffer: Buffer): Promise<IPdfDocument>`
186
+ - `create(): Promise<IPdfDocument>`
187
+ - `rgb(r, g, b): PdfRgbColor`
188
+ - Document methods: page manipulation, drawing, embedding
189
+
190
+ 6. **Job** - Background job execution
191
+ - `runInBackground(params: RunInBackgroundParams): Promise<string>` - Returns job ID
192
+ - `getStatus(jobId: string): Promise<JobStatus>`
193
+ - `list(params?: JobListParams): Promise<JobListResult>`
194
+
195
+ 7. **Messages** - User messaging
196
+ - `send(params: SendMessageParams): Promise<SendMessageResult>`
197
+ - `sendBulk(params: SendBulkMessageParams): Promise<SendBulkMessageResult>`
198
+ - `sendToGroup(params: SendGroupMessageParams): Promise<SendBulkMessageResult>`
199
+
200
+ **Error Class Hierarchy**:
201
+ Each feature domain (Volume, Database, Secret, Excel, PDF, Job, Messages) defines:
202
+ - Abstract base error class (extends Error)
203
+ - Specific error subclasses for different failure scenarios
204
+ - Type guards: `is[Feature]Error()`, `is[Feature]ErrorType()`
205
+
206
+ Example Volume errors:
207
+ - `VolumeError` (abstract base)
208
+ - `VolumeNotFoundError`, `VolumeAccessDeniedError`, `VolumePermissionDeniedError`
209
+ - `FileNotFoundError`, `FileAlreadyExistsError`, `InvalidPathError`
210
+ - `FileSizeLimitError`, `ContentTypeNotAllowedError`
211
+
212
+ ### Configuration Options and Environment Variables
213
+
214
+ **Tenant Environment Variables** (via `ketrics.environment`):
215
+ - Application-defined key-value pairs passed at deployment time
216
+ - All keys are uppercase with letters, numbers, underscores only
217
+ - Accessed at runtime for configuration (API keys, feature flags, etc.)
218
+
219
+ **Volume Configuration**:
220
+ - Volume codes define which S3 bucket is accessed
221
+ - Permissions defined per volume: ReadObject, CreateObject, UpdateObject, DeleteObject, ListObjects
222
+ - Content-type restrictions enforced on put operations
223
+ - File size limits enforced per volume
224
+
225
+ **Database Configuration**:
226
+ - Database codes identify external database servers (PostgreSQL, MySQL, etc.)
227
+ - Permissions per database: read, write, admin
228
+ - Connection pooling managed by runtime
229
+ - Query timeouts configurable
230
+
231
+ **Excel Configuration**:
232
+ - Tab colors, default row height, default column width configurable per worksheet
233
+ - Column definitions with header, key, width
234
+
235
+ **PDF Configuration**:
236
+ - Standard page sizes: A4, Letter, Legal, Tabloid, A3, A5
237
+ - Standard fonts: Courier variants, Helvetica variants, TimesRoman variants, Symbol, ZapfDingbats
238
+ - RGB color values (0-1 range)
239
+ - Drawing options: size, opacity, rotation, line height, max width
240
+
241
+ **Job Configuration**:
242
+ - Timeout range: 1ms to 900,000ms (15 minutes), default 300,000ms (5 minutes)
243
+ - Idempotency keys prevent duplicate job creation
244
+ - Cross-app execution requires application grants
245
+
246
+ **Message Configuration**:
247
+ - Subject max length: 200 chars
248
+ - Body max length: 10,000 chars (markdown supported)
249
+ - Priority levels: LOW, MEDIUM, HIGH
250
+ - Channels: inbox (always enabled) + push notifications (optional per message)
251
+
252
+ ### External Integrations
253
+
254
+ **AWS S3** (Volumes):
255
+ - S3 buckets mapped to volume codes
256
+ - Presigned URLs for direct browser download/upload
257
+ - Versioning support via version IDs
258
+ - ETag support for content hashing
259
+
260
+ **External Databases**:
261
+ - PostgreSQL, MySQL, and other JDBC-compatible databases
262
+ - Connection strings and credentials never exposed to tenant code
263
+ - Parameterized queries to prevent SQL injection
264
+ - Transaction support with automatic rollback
265
+
266
+ **AWS KMS** (Secrets):
267
+ - Tenant-specific KMS keys for secret encryption
268
+ - Secrets decrypted at request time
269
+ - Decryption errors surface as SecretDecryptionError
270
+
271
+ **CloudWatch** (Logging):
272
+ - Console logs forwarded to CloudWatch with tenant/app/requestor context
273
+ - Proper log grouping and filtering capabilities
274
+
275
+ **Application Job Queue**:
276
+ - Background job execution in same or different applications
277
+ - Status tracking in centralized job store
278
+ - Timeout enforcement and error capture
279
+
280
+ **User Messaging Platform**:
281
+ - Inbox storage for messages
282
+ - Push notification delivery
283
+ - Group membership resolution via IAM service
284
+
285
+ ## Data Flow
286
+
287
+ ### How Data Enters the Component
288
+
289
+ 1. **Context Injection**: At runtime, the Ketrics platform injects a global `ketrics` object with:
290
+ - Tenant metadata (ID, code, name)
291
+ - Application metadata (ID, code, version, deploymentId)
292
+ - Requestor information (type, ID, permissions)
293
+ - Runtime environment (Node.js version, AWS region)
294
+ - Environment variables (key-value pairs)
295
+
296
+ 2. **Function Parameters**: Tenant code receives:
297
+ - HTTP request payloads (JSON, form data, etc.)
298
+ - File uploads to volumes
299
+ - Database query parameters
300
+ - Job payloads
301
+ - Message content from users
302
+
303
+ 3. **External Services**: Data retrieved from:
304
+ - S3 via volume.get()
305
+ - Database queries via db.query()
306
+ - Secrets via Secret.get()
307
+ - Uploaded Excel/PDF files via Excel.read(), Pdf.read()
308
+
309
+ ### Transformations Applied
310
+
311
+ **Volume Operations**:
312
+ - Raw S3 objects → FileContent (buffer + metadata)
313
+ - List operations → Paginated FileInfo arrays with cursors
314
+ - Put operations → PutResult with etag and optional versionId
315
+ - Copy/move → CopyResult/MoveResult with operation status
316
+
317
+ **Database Operations**:
318
+ - Raw SQL results → DatabaseQueryResult<T> with typed rows
319
+ - Execute operations → DatabaseExecuteResult with affectedRows and insertId
320
+ - Transactions → Implicit begin/commit/rollback with error handling
321
+
322
+ **Document Operations**:
323
+ - Raw file buffers → IExcelWorkbook/IPdfDocument with method interfaces
324
+ - Workbook operations → Cells, rows, worksheets with formatting
325
+ - PDF drawing → Rendered text, shapes, images on pages
326
+
327
+ **Job Operations**:
328
+ - RunInBackgroundParams → Job ID string
329
+ - Job polling → JobStatus with timestamps and error details
330
+ - Job listing → Paginated JobStatus arrays with cursor
331
+
332
+ **Message Operations**:
333
+ - SendMessageParams → SendMessageResult with messageId and status
334
+ - Bulk operations → Aggregated counts (sent, failed) + per-user results
335
+ - Group operations → Member resolution + bulk message delivery
336
+
337
+ ### How Data Exits or Is Persisted
338
+
339
+ **Persistent Storage**:
340
+ - Volume.put() → Data stored in S3 (encrypted at rest)
341
+ - Database execute() → Data persisted in external database
342
+ - Excel workbook → Buffer written to volume or returned to caller
343
+ - PDF document → Buffer written to volume or returned to caller
344
+ - Message → Stored in user inbox, push notification sent
345
+
346
+ **Return Values**:
347
+ - Query results → Returned synchronously to caller
348
+ - File content → Streamed or buffered to caller
349
+ - Generated documents → Returned as buffers
350
+ - Job IDs → Returned immediately, execution async
351
+ - Operation results → Status objects with metadata
352
+
353
+ **External API Calls**:
354
+ - ketrics.http → External API responses via HttpResponse<T>
355
+ - Logging → CloudWatch via ketrics.console
356
+ - Notifications → Push delivery platform
357
+ - Messages → Inbox storage + notification delivery
147
358
 
148
- ```typescript
149
- // GET request
150
- const response = await ketrics.http.get<MyData>('https://api.example.com/data');
151
- console.log(response.data, response.status);
152
-
153
- // POST request with body
154
- const result = await ketrics.http.post('https://api.example.com/orders', {
155
- product: 'Widget',
156
- quantity: 5,
157
- });
158
-
159
- // PUT request
160
- await ketrics.http.put('https://api.example.com/orders/123', {
161
- status: 'shipped',
162
- });
163
-
164
- // DELETE request
165
- await ketrics.http.delete('https://api.example.com/orders/123');
166
-
167
- // With configuration options
168
- const configured = await ketrics.http.get('https://api.example.com/data', {
169
- headers: {
170
- 'Authorization': 'Bearer token123',
171
- 'X-Custom-Header': 'value',
172
- },
173
- timeout: 5000, // 5 seconds
174
- params: { page: 1, limit: 10 },
175
- maxRedirects: 3,
176
- });
177
- ```
178
-
179
- ### Response Structure
180
-
181
- ```typescript
182
- interface HttpResponse<T> {
183
- data: T; // Response body
184
- status: number; // HTTP status code (e.g., 200)
185
- statusText: string; // Status text (e.g., "OK")
186
- headers: Record<string, string>; // Response headers
187
- }
188
- ```
189
-
190
- ---
191
-
192
- ## Volume Storage (S3-backed)
193
-
194
- Access S3-backed file storage with granular permissions:
195
-
196
- ### Connecting to a Volume
197
-
198
- ```typescript
199
- import type { IVolume, FileContent, PutResult, ListResult } from '@ketrics/sdk-backend';
200
-
201
- // Connect to a volume by code
202
- const volume = await ketrics.Volume.connect('uploads');
203
-
204
- // Check volume info
205
- console.log(volume.code); // 'uploads'
206
- console.log(volume.permissions); // Set { 'ReadObject', 'CreateObject', ... }
207
- ```
208
-
209
- ### Reading Files
210
-
211
- ```typescript
212
- // Get file content
213
- const file: FileContent = await volume.get('documents/report.pdf');
214
- console.log(file.content); // Buffer containing file data
215
- console.log(file.contentType); // 'application/pdf'
216
- console.log(file.contentLength); // 12345 (bytes)
217
- console.log(file.lastModified); // Date object
218
- console.log(file.etag); // '"abc123..."'
219
- console.log(file.metadata); // { author: 'john@example.com' }
220
-
221
- // Check if file exists (without downloading)
222
- const exists = await volume.exists('config.json');
223
-
224
- // Get file metadata only (no content download)
225
- const meta = await volume.getMetadata('large-file.zip');
226
- console.log(meta.size, meta.contentType, meta.lastModified);
227
- ```
228
-
229
- ### Writing Files
230
-
231
- ```typescript
232
- // Write text/JSON content
233
- const result = await volume.put('output/data.json', JSON.stringify(data), {
234
- contentType: 'application/json',
235
- });
236
- console.log(result.key, result.etag, result.size);
237
-
238
- // Write binary content
239
- const buffer = Buffer.from('Hello World');
240
- await volume.put('files/hello.txt', buffer);
241
-
242
- // Write with metadata
243
- await volume.put('documents/report.pdf', pdfBuffer, {
244
- contentType: 'application/pdf',
245
- metadata: {
246
- author: ketrics.user.email,
247
- version: '1.0',
248
- },
249
- });
359
+ ## Error Handling
250
360
 
251
- // Create only if file doesn't exist
252
- try {
253
- await volume.put('config.json', defaultConfig, { ifNotExists: true });
254
- } catch (error) {
255
- if (error instanceof ketrics.FileAlreadyExistsError) {
256
- console.log('Config already exists, skipping');
361
+ ### Error Scenarios Considered
362
+
363
+ Each feature domain handles specific error scenarios:
364
+
365
+ **Volume Errors**:
366
+ - `VolumeNotFoundError`: Volume code doesn't exist in tenant's namespace
367
+ - `VolumeAccessDeniedError`: Application lacks access grant
368
+ - `VolumePermissionDeniedError`: Specific operation (read/write/delete/list) not permitted
369
+ - `FileNotFoundError`: Requested file doesn't exist
370
+ - `FileAlreadyExistsError`: File exists when `ifNotExists: true`
371
+ - `InvalidPathError`: Path contains invalid characters or traversal attempts (../)
372
+ - `FileSizeLimitError`: File exceeds volume's size limit
373
+ - `ContentTypeNotAllowedError`: MIME type not allowed for volume
374
+
375
+ **Database Errors**:
376
+ - `DatabaseNotFoundError`: Database code doesn't exist
377
+ - `DatabaseAccessDeniedError`: No access grant to database
378
+ - `DatabaseConnectionError`: Cannot establish connection (network, credentials, server down)
379
+ - `DatabaseQueryError`: SQL syntax error, constraint violation, etc.
380
+ - `DatabaseTransactionError`: Transaction commit/rollback failed
381
+
382
+ **Secret Errors**:
383
+ - `SecretNotFoundError`: Secret code doesn't exist
384
+ - `SecretAccessDeniedError`: Application lacks access grant
385
+ - `SecretDecryptionError`: KMS decryption failed (corrupted value, key revoked)
386
+
387
+ **Excel Errors**:
388
+ - `ExcelParseError`: Provided buffer is not valid Excel format
389
+ - `ExcelWriteError`: File write operation failed
390
+
391
+ **PDF Errors**:
392
+ - `PdfParseError`: Provided buffer is not valid PDF format
393
+ - `PdfWriteError`: File write operation failed
394
+
395
+ **Job Errors**:
396
+ - `JobNotFoundError`: Job ID doesn't exist
397
+ - `InvalidFunctionError`: Target function name invalid or missing
398
+ - `CrossAppPermissionError`: Application lacks permission for cross-app job
399
+ - `EltJobExecutionError`: Job failed during execution (timeout, runtime error)
400
+
401
+ **Message Errors**:
402
+ - `MessageValidationError`: Subject/body too long, invalid parameters
403
+ - `GroupNotFoundError`: Group code doesn't exist
404
+ - `TenantGrantPermissionDeniedError`: Application lacks IAM-data permissions for group access
405
+
406
+ ### Retry Logic or Fallback Mechanisms
407
+
408
+ **Built-in Retry Behaviors**:
409
+ - Database connection pooling automatically handles connection reuse
410
+ - Connection timeouts trigger error rather than unlimited retries (tenant responsibility to retry)
411
+ - Failed bulk messages don't prevent other messages in batch (partial success tracked)
412
+
413
+ **Tenant-Implemented Retry Patterns**:
414
+ ```typescript
415
+ // Retry with exponential backoff
416
+ async function retryWithBackoff(fn, maxAttempts = 3) {
417
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
418
+ try {
419
+ return await fn();
420
+ } catch (error) {
421
+ if (attempt === maxAttempts) throw error;
422
+ const delay = Math.pow(2, attempt) * 1000;
423
+ await new Promise(resolve => setTimeout(resolve, delay));
424
+ }
257
425
  }
258
426
  }
259
- ```
260
-
261
- ### Listing Files
262
-
263
- ```typescript
264
- // List all files
265
- const list = await volume.list();
266
- for (const file of list.files) {
267
- console.log(file.key, file.size, file.lastModified);
268
- }
269
-
270
- // List with prefix filter
271
- const docs = await volume.list({ prefix: 'documents/' });
272
-
273
- // Paginated listing
274
- const page1 = await volume.list({ maxResults: 100 });
275
- if (page1.isTruncated) {
276
- const page2 = await volume.list({
277
- maxResults: 100,
278
- continuationToken: page1.continuationToken,
279
- });
280
- }
281
-
282
- // Hierarchical listing with delimiter
283
- const folders = await volume.list({ delimiter: '/' });
284
- console.log(folders.folders); // ['documents/', 'images/', 'uploads/']
285
- ```
286
-
287
- ### Deleting Files
288
-
289
- ```typescript
290
- // Delete single file
291
- await volume.delete('temp/file.txt');
292
427
 
293
- // Delete by prefix (batch delete)
294
- const result = await volume.deleteByPrefix('temp/');
295
- console.log(`Deleted ${result.deletedCount} files`);
296
- console.log('Deleted keys:', result.deletedKeys);
297
- ```
298
-
299
- ### Copying and Moving Files
300
-
301
- ```typescript
302
- // Copy a file
303
- await volume.copy('source.pdf', 'backup/source.pdf');
304
-
305
- // Copy with metadata replacement
306
- await volume.copy('source.pdf', 'archive/source.pdf', {
307
- metadataDirective: 'REPLACE',
308
- metadata: { archived: 'true', archivedBy: ketrics.user.email },
309
- });
310
-
311
- // Move a file (copy + delete)
312
- await volume.move('temp/upload.pdf', 'documents/final.pdf');
313
- ```
314
-
315
- ### Generating Presigned URLs
316
-
317
- ```typescript
318
- // Generate download URL (for sharing)
319
- const downloadUrl = await volume.generateDownloadUrl('report.pdf', {
320
- expiresIn: 3600, // 1 hour
321
- responseContentDisposition: 'attachment; filename="report.pdf"',
322
- });
323
- console.log(downloadUrl.url); // Presigned S3 URL
324
- console.log(downloadUrl.expiresAt); // Expiration Date
325
-
326
- // Generate upload URL (for direct client uploads)
327
- const uploadUrl = await volume.generateUploadUrl('uploads/new-file.pdf', {
328
- expiresIn: 3600,
329
- contentType: 'application/pdf',
330
- maxSize: 10 * 1024 * 1024, // 10MB limit
428
+ // Idempotency keys prevent duplicate job execution
429
+ const jobId = await ketrics.Job.runInBackground({
430
+ function: 'processPayment',
431
+ payload: { orderId: '123' },
432
+ options: { idempotencyKey: 'order-123-payment' }
331
433
  });
332
- // Client can PUT directly to uploadUrl.url
333
434
  ```
334
435
 
335
- ---
336
-
337
- ## Database Connections
338
-
339
- Access external databases (PostgreSQL, MySQL, MSSQL) with connection pooling:
340
-
341
- ### Connecting to a Database
342
-
343
- ```typescript
344
- import type { IDatabaseConnection, DatabaseQueryResult } from '@ketrics/sdk-backend';
345
-
346
- // Connect to a database by code
347
- const db = await ketrics.DatabaseConnection.connect('main-db');
348
-
349
- // Check connection info
350
- console.log(db.code); // 'main-db'
351
- console.log(db.permissions); // Set { 'query', 'execute', ... }
352
- ```
353
-
354
- ### Querying Data
355
-
356
- ```typescript
357
- interface User {
358
- id: number;
359
- name: string;
360
- email: string;
361
- created_at: Date;
362
- }
363
-
364
- // Simple query
365
- const result = await db.query<User>('SELECT * FROM users');
366
- console.log(result.rows); // [{ id: 1, name: 'John', ... }, ...]
367
- console.log(result.rowCount); // 10
368
-
369
- // Query with parameters (prevents SQL injection)
370
- const users = await db.query<User>(
371
- 'SELECT * FROM users WHERE age > ? AND status = ?',
372
- [18, 'active']
373
- );
374
-
375
- // Query single record
376
- const user = await db.query<User>(
377
- 'SELECT * FROM users WHERE id = ?',
378
- [userId]
379
- );
380
- if (user.rows.length > 0) {
381
- console.log('Found user:', user.rows[0].name);
382
- }
383
- ```
384
-
385
- ### Executing Statements
386
-
387
- ```typescript
388
- // INSERT
389
- const insertResult = await db.execute(
390
- 'INSERT INTO users (name, email) VALUES (?, ?)',
391
- ['John Doe', 'john@example.com']
392
- );
393
- console.log(insertResult.affectedRows); // 1
394
- console.log(insertResult.insertId); // 123 (auto-increment ID)
395
-
396
- // UPDATE
397
- const updateResult = await db.execute(
398
- 'UPDATE users SET status = ? WHERE id = ?',
399
- ['inactive', userId]
400
- );
401
- console.log(updateResult.affectedRows); // Number of rows updated
402
-
403
- // DELETE
404
- const deleteResult = await db.execute(
405
- 'DELETE FROM users WHERE status = ?',
406
- ['deleted']
407
- );
408
- console.log(deleteResult.affectedRows); // Number of rows deleted
409
- ```
410
-
411
- ### Transactions
412
-
413
- ```typescript
414
- // Automatic commit on success, rollback on error
415
- const result = await db.transaction(async (tx) => {
416
- // Debit from account
417
- await tx.execute(
418
- 'UPDATE accounts SET balance = balance - ? WHERE id = ?',
419
- [100, fromAccountId]
420
- );
421
-
422
- // Credit to account
423
- await tx.execute(
424
- 'UPDATE accounts SET balance = balance + ? WHERE id = ?',
425
- [100, toAccountId]
426
- );
427
-
428
- // Record transfer
429
- const transfer = await tx.execute(
430
- 'INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)',
431
- [fromAccountId, toAccountId, 100]
432
- );
433
-
434
- return { transferId: transfer.insertId };
435
- });
436
+ ### Logging Approach
436
437
 
437
- console.log('Transfer completed:', result.transferId);
438
- ```
438
+ **Console Logging** (ketrics.console):
439
+ - Methods: log(), error(), warn(), info(), debug()
440
+ - Forwarded to CloudWatch with context:
441
+ - Tenant ID and code
442
+ - Application ID and code
443
+ - Requestor information (user ID or service account ID)
444
+ - Timestamp
445
+ - Log level
439
446
 
440
- ### Closing Connections
447
+ **Error Logging**:
448
+ - Error classes implement toJSON() for serialization
449
+ - Error properties: name, message, code (operation), timestamp, resource code
450
+ - Security note: Error messages never expose credentials, connection strings, or internal IDs
441
451
 
452
+ **Example Logging Pattern**:
442
453
  ```typescript
443
- // Always close when done to return connection to pool
444
- const db = await ketrics.DatabaseConnection.connect('main-db');
445
454
  try {
455
+ const db = await ketrics.DatabaseConnection.connect('main-db');
456
+ ketrics.console.log('Connected to database');
446
457
  const result = await db.query('SELECT * FROM users');
447
- return result.rows;
448
- } finally {
449
- await db.close();
450
- }
451
- ```
452
-
453
- ---
454
-
455
- ## Secrets
456
-
457
- Access encrypted secrets stored with tenant-specific KMS encryption:
458
-
459
- ### Getting Secrets
460
-
461
- ```typescript
462
- // Get a secret value
463
- const apiKey = await ketrics.Secret.get('stripe-api-key');
464
-
465
- // Use the secret (value is decrypted automatically)
466
- const response = await ketrics.http.post('https://api.stripe.com/v1/charges', data, {
467
- headers: { 'Authorization': `Bearer ${apiKey}` },
468
- });
469
- ```
470
-
471
- ### Checking Secret Existence
472
-
473
- ```typescript
474
- // Check if a secret exists before using it
475
- if (await ketrics.Secret.exists('optional-webhook-secret')) {
476
- const secret = await ketrics.Secret.get('optional-webhook-secret');
477
- // Use the optional secret
478
- } else {
479
- // Use default behavior
480
- }
481
- ```
482
-
483
- ### Common Patterns
484
-
485
- ```typescript
486
- // Database password from secret
487
- const dbPassword = await ketrics.Secret.get('db-password');
488
-
489
- // API keys
490
- const stripeKey = await ketrics.Secret.get('stripe-api-key');
491
- const sendgridKey = await ketrics.Secret.get('sendgrid-api-key');
492
-
493
- // OAuth credentials
494
- const clientId = await ketrics.Secret.get('oauth-client-id');
495
- const clientSecret = await ketrics.Secret.get('oauth-client-secret');
496
- ```
497
-
498
- ---
499
-
500
- ## Excel Files
501
-
502
- Read and write Excel files (.xlsx) using a simple API:
503
-
504
- ### Reading Excel Files
505
-
506
- ```typescript
507
- import type { IExcelWorkbook, IExcelWorksheet, IExcelRow } from '@ketrics/sdk-backend';
508
-
509
- // Read from volume
510
- const volume = await ketrics.Volume.connect('uploads');
511
- const file = await volume.get('data/report.xlsx');
512
-
513
- // Parse Excel file
514
- const workbook = await ketrics.Excel.read(file.content);
515
-
516
- // Get worksheet by name
517
- const sheet = workbook.getWorksheet('Sheet1');
518
- if (!sheet) {
519
- throw new Error('Sheet not found');
520
- }
521
-
522
- // Get worksheet by index (1-indexed)
523
- const firstSheet = workbook.getWorksheet(1);
524
-
525
- // List all worksheets
526
- console.log(`Workbook has ${workbook.worksheetCount} sheets`);
527
- for (const ws of workbook.worksheets) {
528
- console.log(`- ${ws.name}: ${ws.actualRowCount} rows`);
529
- }
530
- ```
531
-
532
- ### Reading Rows and Cells
533
-
534
- ```typescript
535
- // Get all rows with values
536
- const rows = sheet.getRows();
537
- for (const row of rows) {
538
- console.log('Row', row.number, ':', row.values);
458
+ ketrics.console.log(`Retrieved ${result.rowCount} users`);
459
+ } catch (error) {
460
+ if (error instanceof ketrics.DatabaseQueryError) {
461
+ ketrics.console.error('Database query failed', error.toJSON());
462
+ } else {
463
+ ketrics.console.error('Unexpected error', { message: error.message });
464
+ }
539
465
  }
540
-
541
- // Get a specific row (1-indexed)
542
- const headerRow = sheet.getRow(1);
543
- console.log('Headers:', headerRow.values);
544
-
545
- // Get a specific cell
546
- const cell = sheet.getCell('A1');
547
- console.log(cell.value, cell.text, cell.address);
548
-
549
- // Get cell by coordinates
550
- const cellB2 = sheet.getCell(2, 2);
551
-
552
- // Iterate over rows
553
- sheet.eachRow((row, rowNumber) => {
554
- console.log(`Row ${rowNumber}:`, row.values);
555
- });
556
-
557
- // Include empty rows
558
- sheet.eachRow((row, rowNumber) => {
559
- console.log(`Row ${rowNumber}:`, row.values);
560
- }, { includeEmpty: true });
561
-
562
- // Get all values as 2D array
563
- const allValues = sheet.getSheetValues();
564
- // allValues[1] = first row, allValues[1][1] = cell A1
565
- ```
566
-
567
- ### Working with Cells
568
-
569
- ```typescript
570
- // Get cell properties
571
- const cell = sheet.getCell('B5');
572
- console.log(cell.value); // Cell value (number, string, Date, etc.)
573
- console.log(cell.text); // Text representation
574
- console.log(cell.address); // 'B5'
575
- console.log(cell.row); // 5
576
- console.log(cell.col); // 2
577
- console.log(cell.formula); // Formula if present (e.g., '=SUM(A1:A10)')
578
- console.log(cell.type); // Cell type
579
-
580
- // Iterate over cells in a row
581
- const row = sheet.getRow(1);
582
- row.eachCell((cell, colNumber) => {
583
- console.log(`Column ${colNumber}:`, cell.value);
584
- });
585
- ```
586
-
587
- ### Creating Excel Files
588
-
589
- ```typescript
590
- // Create new workbook
591
- const workbook = ketrics.Excel.create();
592
-
593
- // Add worksheet
594
- const sheet = workbook.addWorksheet('Sales Report', {
595
- tabColor: 'FF0000', // Red tab
596
- defaultRowHeight: 20,
597
- defaultColWidth: 15,
598
- });
599
-
600
- // Define columns (optional)
601
- sheet.columns = [
602
- { header: 'ID', key: 'id', width: 10 },
603
- { header: 'Product', key: 'product', width: 30 },
604
- { header: 'Price', key: 'price', width: 15 },
605
- { header: 'Quantity', key: 'qty', width: 10 },
606
- ];
607
-
608
- // Add rows
609
- sheet.addRow(['1', 'Widget A', 9.99, 100]);
610
- sheet.addRow(['2', 'Widget B', 19.99, 50]);
611
- sheet.addRow(['3', 'Widget C', 29.99, 25]);
612
-
613
- // Add multiple rows at once
614
- sheet.addRows([
615
- ['4', 'Widget D', 39.99, 10],
616
- ['5', 'Widget E', 49.99, 5],
617
- ]);
618
-
619
- // Insert row at position
620
- sheet.insertRow(2, ['INSERTED', 'Row', 0, 0]);
621
-
622
- // Merge cells
623
- sheet.mergeCells('A1', 'D1'); // Merge A1:D1
624
- sheet.mergeCells(5, 1, 5, 4); // Merge row 5, columns 1-4
625
-
626
- // Write to buffer
627
- const buffer = await workbook.toBuffer();
628
-
629
- // Save to volume
630
- await volume.put('output/report.xlsx', buffer, {
631
- contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
632
- });
633
466
  ```
634
467
 
635
- ### Converting to CSV
636
-
637
- ```typescript
638
- // Convert worksheet to CSV
639
- const csvContent = await workbook.toCsv('Sheet1');
468
+ ## Usage
640
469
 
641
- // Save as CSV
642
- await volume.put('output/data.csv', csvContent, {
643
- contentType: 'text/csv',
644
- });
645
- ```
470
+ ### How to Run/Deploy This Component
646
471
 
647
- ### Managing Worksheets
472
+ This is a type definition library. It does not run standalone but is consumed by tenant applications.
648
473
 
649
- ```typescript
650
- // Remove worksheet
651
- workbook.removeWorksheet('TempSheet');
652
- workbook.removeWorksheet(2); // By index
653
-
654
- // Rename worksheet
655
- const sheet = workbook.getWorksheet('OldName');
656
- if (sheet) {
657
- sheet.name = 'NewName';
658
- }
474
+ **Installation in Tenant Application**:
475
+ ```bash
476
+ npm install @ketrics/sdk-backend
659
477
  ```
660
478
 
661
- ---
662
-
663
- ## PDF Files
664
-
665
- Read, create, and modify PDF files using the pdf-lib wrapper:
666
-
667
- ### Reading PDF Files
668
-
669
- ```typescript
670
- import type { IPdfDocument, IPdfPage } from '@ketrics/sdk-backend';
671
-
672
- // Read from volume
673
- const volume = await ketrics.Volume.connect('uploads');
674
- const file = await volume.get('documents/report.pdf');
675
-
676
- // Parse PDF file
677
- const doc = await ketrics.Pdf.read(file.content);
678
-
679
- // Get document info
680
- console.log('Page count:', doc.getPageCount());
681
- console.log('Title:', doc.getTitle());
682
- console.log('Author:', doc.getAuthor());
683
-
684
- // Get all pages
685
- const pages = doc.getPages();
686
- for (const page of pages) {
687
- console.log(`Page size: ${page.width}x${page.height}`);
479
+ **TypeScript Configuration** (tsconfig.json):
480
+ ```json
481
+ {
482
+ "compilerOptions": {
483
+ "strict": true,
484
+ "lib": ["ES2020"],
485
+ "types": ["@ketrics/sdk-backend"]
486
+ }
688
487
  }
689
-
690
- // Get specific page (0-indexed)
691
- const firstPage = doc.getPage(0);
692
- console.log('First page size:', firstPage.getSize());
693
- ```
694
-
695
- ### Creating PDF Files
696
-
697
- ```typescript
698
- // Create new empty document
699
- const doc = await ketrics.Pdf.create();
700
-
701
- // Set document metadata
702
- doc.setTitle('Sales Report Q4 2024');
703
- doc.setAuthor(ketrics.user.name);
704
- doc.setSubject('Quarterly sales data');
705
- doc.setKeywords(['sales', 'report', 'Q4', '2024']);
706
- doc.setCreator('Ketrics Application');
707
-
708
- // Add pages with different sizes
709
- const pageA4 = doc.addPage('A4'); // Standard A4
710
- const pageLetter = doc.addPage('Letter'); // US Letter
711
- const pageCustom = doc.addPage([600, 400]); // Custom size in points
712
-
713
- // Insert page at specific position (0-indexed)
714
- const insertedPage = doc.insertPage(1, 'A4');
715
-
716
- // Remove a page
717
- doc.removePage(2);
718
-
719
- // Write to buffer
720
- const buffer = await doc.toBuffer();
721
-
722
- // Save to volume
723
- await volume.put('output/report.pdf', buffer, {
724
- contentType: 'application/pdf',
725
- });
726
- ```
727
-
728
- ### Drawing Text
729
-
730
- ```typescript
731
- const doc = await ketrics.Pdf.create();
732
- const page = doc.addPage('A4');
733
-
734
- // Simple text (default: Helvetica, 12pt, black)
735
- await page.drawText('Hello, World!', { x: 50, y: 700 });
736
-
737
- // Styled text
738
- await page.drawText('Large Red Title', {
739
- x: 50,
740
- y: 750,
741
- size: 32,
742
- color: ketrics.Pdf.rgb(1, 0, 0), // Red (RGB values 0-1)
743
- });
744
-
745
- // Text with custom font
746
- const boldFont = await doc.embedStandardFont('HelveticaBold');
747
- await page.drawText('Bold Text', {
748
- x: 50,
749
- y: 650,
750
- size: 16,
751
- font: boldFont,
752
- });
753
-
754
- // Rotated text
755
- await page.drawText('Rotated', {
756
- x: 200,
757
- y: 500,
758
- size: 14,
759
- rotate: 45, // degrees
760
- });
761
-
762
- // Text with opacity
763
- await page.drawText('Semi-transparent', {
764
- x: 50,
765
- y: 600,
766
- size: 14,
767
- opacity: 0.5,
768
- });
769
-
770
- // Multiline text with wrapping
771
- await page.drawText('This is a long paragraph that will wrap to multiple lines when it exceeds the maximum width.', {
772
- x: 50,
773
- y: 550,
774
- size: 12,
775
- maxWidth: 200,
776
- lineHeight: 16,
777
- });
778
488
  ```
779
489
 
780
- ### Drawing Shapes
781
-
782
- ```typescript
783
- const doc = await ketrics.Pdf.create();
784
- const page = doc.addPage('A4');
785
-
786
- // Rectangle (filled)
787
- page.drawRectangle({
788
- x: 50,
789
- y: 600,
790
- width: 200,
791
- height: 100,
792
- color: ketrics.Pdf.rgb(0.2, 0.4, 0.8), // Blue fill
793
- });
794
-
795
- // Rectangle (outlined)
796
- page.drawRectangle({
797
- x: 50,
798
- y: 480,
799
- width: 200,
800
- height: 100,
801
- borderColor: ketrics.Pdf.rgb(0, 0, 0),
802
- borderWidth: 2,
803
- });
804
-
805
- // Rectangle (filled with border)
806
- page.drawRectangle({
807
- x: 50,
808
- y: 360,
809
- width: 200,
810
- height: 100,
811
- color: ketrics.Pdf.rgb(0.9, 0.9, 0.9), // Light gray fill
812
- borderColor: ketrics.Pdf.rgb(0, 0, 0),
813
- borderWidth: 1,
814
- opacity: 0.8,
815
- });
816
-
817
- // Line
818
- page.drawLine({
819
- start: { x: 50, y: 340 },
820
- end: { x: 250, y: 340 },
821
- thickness: 2,
822
- color: ketrics.Pdf.rgb(0, 0, 0),
823
- });
824
-
825
- // Circle
826
- page.drawCircle({
827
- x: 150,
828
- y: 250,
829
- radius: 50,
830
- color: ketrics.Pdf.rgb(0.8, 0.2, 0.2), // Red fill
831
- borderColor: ketrics.Pdf.rgb(0, 0, 0),
832
- borderWidth: 2,
833
- });
834
- ```
835
-
836
- ### Embedding Images
837
-
838
- ```typescript
839
- const doc = await ketrics.Pdf.create();
840
- const page = doc.addPage('A4');
841
-
842
- // Read image from volume
843
- const volume = await ketrics.Volume.connect('uploads');
844
- const logoFile = await volume.get('images/logo.png');
845
- const photoFile = await volume.get('images/photo.jpg');
846
-
847
- // Embed PNG image
848
- const pngImage = await doc.embedPng(logoFile.content);
849
- console.log('PNG dimensions:', pngImage.width, 'x', pngImage.height);
850
-
851
- // Embed JPG image
852
- const jpgImage = await doc.embedJpg(photoFile.content);
853
-
854
- // Draw image at original size
855
- page.drawImage(pngImage, { x: 50, y: 700 });
856
-
857
- // Draw image at specific size
858
- page.drawImage(jpgImage, {
859
- x: 50,
860
- y: 500,
861
- width: 200,
862
- height: 150,
863
- });
864
-
865
- // Draw image with opacity
866
- page.drawImage(pngImage, {
867
- x: 300,
868
- y: 700,
869
- opacity: 0.5,
870
- });
490
+ **Building the SDK Package**:
491
+ ```bash
492
+ # Install dependencies
493
+ npm install
871
494
 
872
- // Scale image to fit within bounds
873
- const scaled = pngImage.scaleToFit(150, 100);
874
- page.drawImage(pngImage, {
875
- x: 50,
876
- y: 350,
877
- width: scaled.width,
878
- height: scaled.height,
879
- });
495
+ # Compile TypeScript to JavaScript
496
+ npm run build
880
497
 
881
- // Scale image by factor
882
- const factor = pngImage.scale(0.5);
883
- page.drawImage(pngImage, {
884
- x: 250,
885
- y: 350,
886
- width: factor.width,
887
- height: factor.height,
888
- });
498
+ # Output: dist/ folder with .js, .d.ts, .map files
889
499
  ```
890
500
 
891
- ### Working with Fonts
892
-
893
- ```typescript
894
- const doc = await ketrics.Pdf.create();
895
- const page = doc.addPage('A4');
896
-
897
- // Embed standard fonts (14 built-in fonts available)
898
- const helvetica = await doc.embedStandardFont('Helvetica');
899
- const helveticaBold = await doc.embedStandardFont('HelveticaBold');
900
- const timesRoman = await doc.embedStandardFont('TimesRoman');
901
- const courier = await doc.embedStandardFont('Courier');
902
-
903
- // Use different fonts
904
- await page.drawText('Helvetica Regular', { x: 50, y: 750, font: helvetica, size: 14 });
905
- await page.drawText('Helvetica Bold', { x: 50, y: 720, font: helveticaBold, size: 14 });
906
- await page.drawText('Times Roman', { x: 50, y: 690, font: timesRoman, size: 14 });
907
- await page.drawText('Courier', { x: 50, y: 660, font: courier, size: 14 });
908
-
909
- // Calculate text width for positioning
910
- const text = 'Right-aligned text';
911
- const textWidth = helvetica.widthOfTextAtSize(text, 14);
912
- await page.drawText(text, {
913
- x: page.width - 50 - textWidth, // Right-align with 50pt margin
914
- y: 600,
915
- font: helvetica,
916
- size: 14,
917
- });
918
-
919
- // Get font height
920
- const lineHeight = helvetica.heightAtSize(14);
921
- console.log('Line height:', lineHeight);
922
-
923
- // Embed custom font from file
924
- const customFontFile = await volume.get('fonts/OpenSans-Regular.ttf');
925
- const customFont = await doc.embedFont(customFontFile.content);
926
- await page.drawText('Custom Font', { x: 50, y: 550, font: customFont, size: 14 });
501
+ **Publishing**:
502
+ ```bash
503
+ npm publish
927
504
  ```
928
505
 
929
- ### Available Standard Fonts
930
-
931
- | Font Name | Description |
932
- |-----------|-------------|
933
- | `Helvetica` | Sans-serif regular |
934
- | `HelveticaBold` | Sans-serif bold |
935
- | `HelveticaOblique` | Sans-serif italic |
936
- | `HelveticaBoldOblique` | Sans-serif bold italic |
937
- | `TimesRoman` | Serif regular |
938
- | `TimesRomanBold` | Serif bold |
939
- | `TimesRomanItalic` | Serif italic |
940
- | `TimesRomanBoldItalic` | Serif bold italic |
941
- | `Courier` | Monospace regular |
942
- | `CourierBold` | Monospace bold |
943
- | `CourierOblique` | Monospace italic |
944
- | `CourierBoldOblique` | Monospace bold italic |
945
- | `Symbol` | Symbol characters |
946
- | `ZapfDingbats` | Decorative symbols |
947
-
948
- ### Page Sizes
949
-
950
- | Size | Dimensions (points) |
951
- |------|---------------------|
952
- | `A4` | 595 x 842 |
953
- | `A3` | 842 x 1191 |
954
- | `A5` | 420 x 595 |
955
- | `Letter` | 612 x 792 |
956
- | `Legal` | 612 x 1008 |
957
- | `Tabloid` | 792 x 1224 |
958
-
959
- ### Merging PDF Documents
506
+ This publishes to npm registry at https://www.npmjs.com/package/@ketrics/sdk-backend
960
507
 
961
- ```typescript
962
- // Read source documents
963
- const volume = await ketrics.Volume.connect('uploads');
964
- const file1 = await volume.get('doc1.pdf');
965
- const file2 = await volume.get('doc2.pdf');
966
-
967
- const doc1 = await ketrics.Pdf.read(file1.content);
968
- const doc2 = await ketrics.Pdf.read(file2.content);
969
-
970
- // Create merged document
971
- const mergedDoc = await ketrics.Pdf.create();
972
- mergedDoc.setTitle('Merged Document');
973
-
974
- // Copy all pages from doc1
975
- const doc1Pages = await mergedDoc.copyPages(doc1,
976
- Array.from({ length: doc1.getPageCount() }, (_, i) => i)
977
- );
978
-
979
- // Copy specific pages from doc2 (pages 0 and 2)
980
- const doc2Pages = await mergedDoc.copyPages(doc2, [0, 2]);
981
-
982
- // Save merged document
983
- const buffer = await mergedDoc.toBuffer();
984
- await volume.put('merged.pdf', buffer, {
985
- contentType: 'application/pdf',
986
- });
987
- ```
988
-
989
- ### Modifying Existing PDFs
508
+ ### Example Invocations
990
509
 
510
+ **Complete Tenant Application Example**:
991
511
  ```typescript
992
- // Read existing PDF
993
- const volume = await ketrics.Volume.connect('uploads');
994
- const file = await volume.get('template.pdf');
995
- const doc = await ketrics.Pdf.read(file.content);
996
-
997
- // Get first page and add watermark
998
- const page = doc.getPage(0);
999
- await page.drawText('CONFIDENTIAL', {
1000
- x: page.width / 2 - 100,
1001
- y: page.height / 2,
1002
- size: 48,
1003
- color: ketrics.Pdf.rgb(0.9, 0.1, 0.1),
1004
- opacity: 0.3,
1005
- rotate: 45,
1006
- });
1007
-
1008
- // Add footer to all pages
1009
- const pages = doc.getPages();
1010
- for (let i = 0; i < pages.length; i++) {
1011
- const p = pages[i];
1012
- await p.drawText(`Page ${i + 1} of ${pages.length}`, {
1013
- x: p.width / 2 - 30,
1014
- y: 30,
1015
- size: 10,
1016
- color: ketrics.Pdf.rgb(0.5, 0.5, 0.5),
1017
- });
1018
- }
1019
-
1020
- // Save modified document
1021
- const buffer = await doc.toBuffer();
1022
- await volume.put('modified.pdf', buffer, {
1023
- contentType: 'application/pdf',
1024
- });
1025
- ```
1026
-
1027
- ### Complete PDF Generation Example
512
+ // handler.ts - Tenant application code
1028
513
 
1029
- ```typescript
1030
- export async function generateInvoice(orderId: string) {
1031
- // Get order data from database
1032
- const db = await ketrics.DatabaseConnection.connect('orders-db');
1033
- const orderResult = await db.query(
1034
- 'SELECT * FROM orders WHERE id = ?',
1035
- [orderId]
1036
- );
1037
- const order = orderResult.rows[0];
1038
- await db.close();
1039
-
1040
- // Create PDF
1041
- const doc = await ketrics.Pdf.create();
1042
- doc.setTitle(`Invoice #${order.invoice_number}`);
1043
- doc.setAuthor(ketrics.tenant.name);
1044
-
1045
- const page = doc.addPage('A4');
1046
- const { width, height } = page.getSize();
1047
-
1048
- // Embed fonts
1049
- const boldFont = await doc.embedStandardFont('HelveticaBold');
1050
- const regularFont = await doc.embedStandardFont('Helvetica');
1051
-
1052
- // Header
1053
- await page.drawText(ketrics.tenant.name, {
1054
- x: 50,
1055
- y: height - 50,
1056
- size: 24,
1057
- font: boldFont,
1058
- });
1059
-
1060
- await page.drawText(`Invoice #${order.invoice_number}`, {
1061
- x: 50,
1062
- y: height - 90,
1063
- size: 18,
1064
- font: regularFont,
1065
- color: ketrics.Pdf.rgb(0.4, 0.4, 0.4),
1066
- });
1067
-
1068
- // Customer info
1069
- await page.drawText('Bill To:', {
1070
- x: 50,
1071
- y: height - 150,
1072
- size: 12,
1073
- font: boldFont,
1074
- });
1075
-
1076
- await page.drawText(order.customer_name, {
1077
- x: 50,
1078
- y: height - 170,
1079
- size: 12,
1080
- font: regularFont,
1081
- });
1082
-
1083
- // Line items header
1084
- const tableTop = height - 250;
1085
- page.drawRectangle({
1086
- x: 50,
1087
- y: tableTop - 5,
1088
- width: width - 100,
1089
- height: 25,
1090
- color: ketrics.Pdf.rgb(0.9, 0.9, 0.9),
1091
- });
1092
-
1093
- await page.drawText('Item', { x: 55, y: tableTop, size: 10, font: boldFont });
1094
- await page.drawText('Qty', { x: 300, y: tableTop, size: 10, font: boldFont });
1095
- await page.drawText('Price', { x: 380, y: tableTop, size: 10, font: boldFont });
1096
- await page.drawText('Total', { x: 460, y: tableTop, size: 10, font: boldFont });
1097
-
1098
- // Line items (simplified example)
1099
- let yPos = tableTop - 30;
1100
- for (const item of order.items) {
1101
- await page.drawText(item.name, { x: 55, y: yPos, size: 10, font: regularFont });
1102
- await page.drawText(String(item.quantity), { x: 300, y: yPos, size: 10, font: regularFont });
1103
- await page.drawText(`$${item.price.toFixed(2)}`, { x: 380, y: yPos, size: 10, font: regularFont });
1104
- await page.drawText(`$${(item.quantity * item.price).toFixed(2)}`, { x: 460, y: yPos, size: 10, font: regularFont });
1105
- yPos -= 20;
514
+ export async function processOrder(payload: { orderId: string }) {
515
+ try {
516
+ // Access context
517
+ ketrics.console.log(`Processing for tenant: ${ketrics.tenant.name}`);
518
+ ketrics.console.log(`Requestor: ${ketrics.requestor.name} (${ketrics.requestor.type})`);
519
+
520
+ // Get secret
521
+ const apiKey = await ketrics.Secret.get('stripe-api-key');
522
+
523
+ // Query database
524
+ const db = await ketrics.DatabaseConnection.connect('orders-db');
525
+ try {
526
+ const result = await db.query<Order>(
527
+ 'SELECT * FROM orders WHERE id = ?',
528
+ [payload.orderId]
529
+ );
530
+ if (result.rowCount === 0) {
531
+ return { error: 'Order not found' };
532
+ }
533
+ const order = result.rows[0];
534
+
535
+ // Make external API call
536
+ const response = await ketrics.http.post<ShipmentResponse>(
537
+ 'https://shipping-api.example.com/create-shipment',
538
+ { orderId: order.id, address: order.shippingAddress },
539
+ { headers: { 'Authorization': `Bearer ${apiKey}` } }
540
+ );
541
+
542
+ // Store file in volume
543
+ const volume = await ketrics.Volume.connect('orders');
544
+ const result = await volume.put(
545
+ `shipments/${order.id}/label.pdf`,
546
+ response.data.labelPdf,
547
+ { contentType: 'application/pdf' }
548
+ );
549
+
550
+ // Update database in transaction
551
+ await db.transaction(async (tx) => {
552
+ await tx.execute(
553
+ 'UPDATE orders SET status = ?, shipment_id = ? WHERE id = ?',
554
+ ['shipped', response.data.shipmentId, order.id]
555
+ );
556
+ });
557
+
558
+ // Send message to user
559
+ await ketrics.Messages.send({
560
+ userId: order.userId,
561
+ type: 'ORDER_SHIPPED',
562
+ subject: 'Your order has shipped!',
563
+ body: `Track your package at: ${response.data.trackingUrl}`,
564
+ priority: 'HIGH',
565
+ actionUrl: response.data.trackingUrl
566
+ });
567
+
568
+ // Queue async job
569
+ const jobId = await ketrics.Job.runInBackground({
570
+ function: 'sendShipmentNotifications',
571
+ payload: { orderId: order.id },
572
+ options: { timeout: 60000 }
573
+ });
574
+
575
+ return {
576
+ success: true,
577
+ shipmentId: response.data.shipmentId,
578
+ jobId: jobId
579
+ };
580
+ } finally {
581
+ await db.close();
582
+ }
583
+ } catch (error) {
584
+ ketrics.console.error('Order processing failed', { error: error.message });
585
+ throw error;
1106
586
  }
1107
-
1108
- // Total line
1109
- page.drawLine({
1110
- start: { x: 380, y: yPos + 5 },
1111
- end: { x: width - 50, y: yPos + 5 },
1112
- thickness: 1,
1113
- color: ketrics.Pdf.rgb(0, 0, 0),
1114
- });
1115
-
1116
- await page.drawText('Total:', { x: 380, y: yPos - 15, size: 12, font: boldFont });
1117
- await page.drawText(`$${order.total.toFixed(2)}`, { x: 460, y: yPos - 15, size: 12, font: boldFont });
1118
-
1119
- // Footer
1120
- await page.drawText(`Generated on ${new Date().toLocaleDateString()}`, {
1121
- x: 50,
1122
- y: 30,
1123
- size: 8,
1124
- color: ketrics.Pdf.rgb(0.6, 0.6, 0.6),
1125
- });
1126
-
1127
- // Save to volume
1128
- const volume = await ketrics.Volume.connect('invoices');
1129
- const buffer = await doc.toBuffer();
1130
- await volume.put(`${order.invoice_number}.pdf`, buffer, {
1131
- contentType: 'application/pdf',
1132
- metadata: {
1133
- orderId: order.id,
1134
- generatedBy: ketrics.user.email,
1135
- },
1136
- });
1137
-
1138
- return {
1139
- success: true,
1140
- invoiceNumber: order.invoice_number,
1141
- };
1142
587
  }
1143
- ```
1144
-
1145
- ---
1146
-
1147
- ## Error Handling
1148
588
 
1149
- All error classes are available on the `ketrics` object for `instanceof` checks:
1150
-
1151
- ### Volume Errors
1152
-
1153
- ```typescript
1154
- try {
1155
- const volume = await ketrics.Volume.connect('uploads');
1156
- const file = await volume.get('missing.txt');
1157
- } catch (error) {
1158
- if (error instanceof ketrics.VolumeNotFoundError) {
1159
- console.log(`Volume '${error.volumeCode}' not found`);
1160
- } else if (error instanceof ketrics.VolumeAccessDeniedError) {
1161
- console.log(`No access to volume '${error.volumeCode}'`);
1162
- } else if (error instanceof ketrics.VolumePermissionDeniedError) {
1163
- console.log(`Missing permission: ${error.requiredPermission}`);
1164
- } else if (error instanceof ketrics.FileNotFoundError) {
1165
- console.log(`File '${error.key}' not found`);
1166
- } else if (error instanceof ketrics.FileAlreadyExistsError) {
1167
- console.log(`File '${error.key}' already exists`);
1168
- } else if (error instanceof ketrics.FileSizeLimitError) {
1169
- console.log(`File too large: ${error.size} > ${error.maxSize}`);
1170
- } else if (error instanceof ketrics.ContentTypeNotAllowedError) {
1171
- console.log(`Type '${error.contentType}' not allowed. Use: ${error.allowedTypes.join(', ')}`);
1172
- } else if (error instanceof ketrics.InvalidPathError) {
1173
- console.log(`Invalid path '${error.path}': ${error.reason}`);
1174
- } else if (error instanceof ketrics.VolumeError) {
1175
- // Base class catches all volume errors
1176
- console.log(`Volume error: ${error.message}`);
1177
- console.log(error.toJSON());
1178
- } else {
1179
- throw error;
589
+ export async function generateMonthlyReport() {
590
+ try {
591
+ // Create Excel workbook
592
+ const workbook = ketrics.Excel.create();
593
+ const sheet = workbook.addWorksheet('Sales Report');
594
+
595
+ // Query all transactions
596
+ const db = await ketrics.DatabaseConnection.connect('analytics-db');
597
+ try {
598
+ const result = await db.query<Transaction>(
599
+ 'SELECT * FROM transactions WHERE month = MONTH(NOW())'
600
+ );
601
+
602
+ // Populate worksheet
603
+ sheet.addRow(['Date', 'Amount', 'Category']);
604
+ for (const tx of result.rows) {
605
+ sheet.addRow([new Date(tx.date), tx.amount, tx.category]);
606
+ }
607
+
608
+ // Write to volume
609
+ const buffer = await workbook.writeFile();
610
+ const volume = await ketrics.Volume.connect('reports');
611
+ await volume.put(`monthly/${new Date().toISOString().split('T')[0]}.xlsx`, buffer);
612
+
613
+ // Send to group (requires IAM-data permissions)
614
+ await ketrics.Messages.sendToGroup({
615
+ groupCode: 'finance-team',
616
+ type: 'REPORT_READY',
617
+ subject: 'Monthly Sales Report',
618
+ body: 'Your monthly sales report is ready',
619
+ priority: 'MEDIUM'
620
+ });
621
+
622
+ return { success: true };
623
+ } finally {
624
+ await db.close();
625
+ }
626
+ } catch (error) {
627
+ if (error instanceof ketrics.GroupNotFoundError) {
628
+ ketrics.console.error(`Group not found: ${error.message}`);
629
+ } else if (error instanceof ketrics.ExcelWriteError) {
630
+ ketrics.console.error('Failed to write Excel file');
631
+ } else {
632
+ throw error;
633
+ }
1180
634
  }
1181
635
  }
1182
636
  ```
1183
637
 
1184
- ### Database Errors
1185
-
638
+ **Error Handling Patterns**:
1186
639
  ```typescript
640
+ // Type guards for error handling
1187
641
  try {
1188
642
  const db = await ketrics.DatabaseConnection.connect('main-db');
1189
- await db.query('SELECT * FROM users');
1190
643
  } catch (error) {
1191
- if (error instanceof ketrics.DatabaseNotFoundError) {
1192
- console.log(`Database '${error.databaseCode}' not found`);
1193
- } else if (error instanceof ketrics.DatabaseAccessDeniedError) {
1194
- console.log(`No access to database '${error.databaseCode}'`);
1195
- } else if (error instanceof ketrics.DatabaseConnectionError) {
1196
- console.log(`Connection failed: ${error.reason}`);
1197
- } else if (error instanceof ketrics.DatabaseQueryError) {
1198
- console.log(`Query failed: ${error.reason}`);
1199
- if (error.sql) console.log(`SQL: ${error.sql}`);
1200
- } else if (error instanceof ketrics.DatabaseTransactionError) {
1201
- console.log(`Transaction failed: ${error.reason}`);
1202
- console.log(`Rolled back: ${error.rolledBack}`);
1203
- } else if (error instanceof ketrics.DatabaseError) {
1204
- // Base class catches all database errors
644
+ if (ketrics.isDatabaseError(error)) {
645
+ // Specific database error
1205
646
  console.log(`Database error: ${error.message}`);
1206
- } else {
1207
- throw error;
1208
- }
1209
- }
1210
- ```
1211
-
1212
- ### Secret Errors
1213
-
1214
- ```typescript
1215
- try {
1216
- const secret = await ketrics.Secret.get('api-key');
1217
- } catch (error) {
1218
- if (error instanceof ketrics.SecretNotFoundError) {
1219
- console.log(`Secret '${error.secretCode}' not found`);
1220
- } else if (error instanceof ketrics.SecretAccessDeniedError) {
1221
- console.log(`No access to secret '${error.secretCode}'`);
1222
- } else if (error instanceof ketrics.SecretDecryptionError) {
1223
- console.log(`Decryption failed: ${error.reason}`);
1224
- } else if (error instanceof ketrics.SecretError) {
1225
- // Base class catches all secret errors
1226
- console.log(`Secret error: ${error.message}`);
1227
- } else {
1228
- throw error;
1229
647
  }
1230
- }
1231
- ```
1232
-
1233
- ### Excel Errors
1234
-
1235
- ```typescript
1236
- try {
1237
- const workbook = await ketrics.Excel.read(buffer);
1238
- const outputBuffer = await workbook.toBuffer();
1239
- } catch (error) {
1240
- if (error instanceof ketrics.ExcelParseError) {
1241
- console.log(`Failed to parse Excel: ${error.reason}`);
1242
- } else if (error instanceof ketrics.ExcelWriteError) {
1243
- console.log(`Failed to write Excel: ${error.reason}`);
1244
- } else if (error instanceof ketrics.ExcelError) {
1245
- // Base class catches all Excel errors
1246
- console.log(`Excel error: ${error.message}`);
1247
- } else {
1248
- throw error;
1249
- }
1250
- }
1251
- ```
1252
-
1253
- ### PDF Errors
1254
648
 
1255
- ```typescript
1256
- try {
1257
- const doc = await ketrics.Pdf.read(buffer);
1258
- const page = doc.addPage('A4');
1259
- await page.drawText('Hello');
1260
- const outputBuffer = await doc.toBuffer();
1261
- } catch (error) {
1262
- if (error instanceof ketrics.PdfParseError) {
1263
- console.log(`Failed to parse PDF: ${error.reason}`);
1264
- } else if (error instanceof ketrics.PdfWriteError) {
1265
- console.log(`Failed to write PDF: ${error.reason}`);
1266
- } else if (error instanceof ketrics.PdfError) {
1267
- // Base class catches all PDF errors
1268
- console.log(`PDF error: ${error.message}`);
1269
- } else {
1270
- throw error;
649
+ if (ketrics.isDatabaseErrorType(error, 'DatabaseNotFoundError')) {
650
+ // Exact error type check
651
+ console.log(`Database not found: ${error.databaseCode}`);
1271
652
  }
1272
- }
1273
- ```
1274
-
1275
- ---
1276
-
1277
- ## Error Class Reference
1278
-
1279
- ### Volume Errors
1280
-
1281
- | Error Class | When Thrown |
1282
- |-------------|-------------|
1283
- | `VolumeError` | Base class for all volume errors |
1284
- | `VolumeNotFoundError` | Volume doesn't exist |
1285
- | `VolumeAccessDeniedError` | Application has no access grant |
1286
- | `VolumePermissionDeniedError` | Missing required permission (Read, Create, Update, Delete, List) |
1287
- | `FileNotFoundError` | File doesn't exist |
1288
- | `FileAlreadyExistsError` | File exists (with `ifNotExists` option) |
1289
- | `InvalidPathError` | Invalid file path (e.g., path traversal attempt) |
1290
- | `FileSizeLimitError` | File exceeds volume size limit |
1291
- | `ContentTypeNotAllowedError` | Content type not allowed by volume |
1292
-
1293
- ### Database Errors
1294
-
1295
- | Error Class | When Thrown |
1296
- |-------------|-------------|
1297
- | `DatabaseError` | Base class for all database errors |
1298
- | `DatabaseNotFoundError` | Database doesn't exist |
1299
- | `DatabaseAccessDeniedError` | Application has no access grant |
1300
- | `DatabaseConnectionError` | Connection cannot be established |
1301
- | `DatabaseQueryError` | SQL query fails |
1302
- | `DatabaseTransactionError` | Transaction fails (automatically rolled back) |
1303
-
1304
- ### Secret Errors
1305
-
1306
- | Error Class | When Thrown |
1307
- |-------------|-------------|
1308
- | `SecretError` | Base class for all secret errors |
1309
- | `SecretNotFoundError` | Secret doesn't exist |
1310
- | `SecretAccessDeniedError` | Application has no access grant |
1311
- | `SecretDecryptionError` | KMS decryption fails |
1312
-
1313
- ### Excel Errors
1314
-
1315
- | Error Class | When Thrown |
1316
- |-------------|-------------|
1317
- | `ExcelError` | Base class for all Excel errors |
1318
- | `ExcelParseError` | File cannot be parsed (invalid/corrupted) |
1319
- | `ExcelWriteError` | File cannot be written |
1320
-
1321
- ### PDF Errors
1322
-
1323
- | Error Class | When Thrown |
1324
- |-------------|-------------|
1325
- | `PdfError` | Base class for all PDF errors |
1326
- | `PdfParseError` | PDF file cannot be parsed (invalid/corrupted) |
1327
- | `PdfWriteError` | PDF file cannot be written |
1328
-
1329
- ---
1330
-
1331
- ## Type Exports
1332
-
1333
- All types are exported for use in your application:
1334
653
 
1335
- ```typescript
1336
- import type {
1337
- // Context
1338
- TenantContext,
1339
- ApplicationContext,
1340
- UserContext,
1341
- EnvironmentContext,
1342
-
1343
- // Console
1344
- ConsoleLogger,
1345
-
1346
- // HTTP
1347
- HttpRequestConfig,
1348
- HttpResponse,
1349
- HttpClient,
1350
-
1351
- // Volumes
1352
- IVolume,
1353
- PutContent,
1354
- FileContent,
1355
- FileMetadata,
1356
- FileInfo,
1357
- PutOptions,
1358
- PutResult,
1359
- DeleteResult,
1360
- DeleteByPrefixResult,
1361
- ListOptions,
1362
- ListResult,
1363
- CopyOptions,
1364
- CopyResult,
1365
- MoveOptions,
1366
- MoveResult,
1367
- DownloadUrlOptions,
1368
- UploadUrlOptions,
1369
- PresignedUrl,
1370
-
1371
- // Databases
1372
- IDatabaseConnection,
1373
- DatabaseQueryResult,
1374
- DatabaseExecuteResult,
1375
- DatabaseManager,
1376
-
1377
- // Secrets
1378
- ISecret,
1379
-
1380
- // Excel
1381
- IExcelWorkbook,
1382
- IExcelWorksheet,
1383
- IExcelRow,
1384
- IExcelCell,
1385
- ExcelCellValue,
1386
- ExcelRowValues,
1387
- ExcelColumnDefinition,
1388
- AddWorksheetOptions,
1389
- ExcelManager,
1390
-
1391
- // PDF
1392
- IPdfDocument,
1393
- IPdfPage,
1394
- PdfPageSize,
1395
- PdfStandardFont,
1396
- PdfRgbColor,
1397
- PdfDrawTextOptions,
1398
- PdfDrawRectOptions,
1399
- PdfDrawLineOptions,
1400
- PdfDrawCircleOptions,
1401
- PdfDrawImageOptions,
1402
- PdfEmbeddedImage,
1403
- PdfEmbeddedFont,
1404
- PdfManager,
1405
-
1406
- // Main SDK
1407
- KetricsSdkV1,
1408
- } from '@ketrics/sdk-backend';
1409
- ```
1410
-
1411
- ---
1412
-
1413
- ## Complete Example
1414
-
1415
- Here's a complete example showing multiple SDK features working together:
1416
-
1417
- ```typescript
1418
- import type { IVolume, IDatabaseConnection, FileContent } from '@ketrics/sdk-backend';
1419
-
1420
- interface Order {
1421
- id: number;
1422
- customer_name: string;
1423
- total: number;
1424
- status: string;
1425
- }
1426
-
1427
- export async function generateReport() {
1428
- ketrics.console.log('Starting report generation', {
1429
- tenant: ketrics.tenant.code,
1430
- user: ketrics.user.email,
1431
- });
1432
-
1433
- // Get API key from secrets
1434
- const emailApiKey = await ketrics.Secret.get('sendgrid-api-key');
1435
-
1436
- // Connect to database
1437
- const db = await ketrics.DatabaseConnection.connect('orders-db');
1438
-
1439
- try {
1440
- // Query orders from database
1441
- const orders = await db.query<Order>(
1442
- 'SELECT * FROM orders WHERE status = ? AND created_at > ?',
1443
- ['completed', '2024-01-01']
1444
- );
1445
-
1446
- ketrics.console.info(`Found ${orders.rowCount} orders`);
1447
-
1448
- // Create Excel report
1449
- const workbook = ketrics.Excel.create();
1450
- const sheet = workbook.addWorksheet('Orders Report');
1451
-
1452
- // Add header row
1453
- sheet.addRow(['Order ID', 'Customer', 'Total', 'Status']);
1454
-
1455
- // Add data rows
1456
- for (const order of orders.rows) {
1457
- sheet.addRow([order.id, order.customer_name, order.total, order.status]);
1458
- }
1459
-
1460
- // Calculate total
1461
- const total = orders.rows.reduce((sum, o) => sum + o.total, 0);
1462
- sheet.addRow(['', 'TOTAL', total, '']);
1463
-
1464
- // Generate buffer
1465
- const buffer = await workbook.toBuffer();
1466
-
1467
- // Save to volume
1468
- const volume = await ketrics.Volume.connect('reports');
1469
- const fileName = `orders-${new Date().toISOString().split('T')[0]}.xlsx`;
1470
-
1471
- await volume.put(`monthly/${fileName}`, buffer, {
1472
- contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1473
- metadata: {
1474
- generatedBy: ketrics.user.email,
1475
- orderCount: String(orders.rowCount),
1476
- },
1477
- });
1478
-
1479
- // Generate download URL
1480
- const downloadUrl = await volume.generateDownloadUrl(`monthly/${fileName}`, {
1481
- expiresIn: 86400, // 24 hours
1482
- });
1483
-
1484
- // Send email notification via external API
1485
- await ketrics.http.post('https://api.sendgrid.com/v3/mail/send', {
1486
- to: ketrics.user.email,
1487
- subject: 'Your report is ready',
1488
- body: `Download your report: ${downloadUrl.url}`,
1489
- }, {
1490
- headers: { 'Authorization': `Bearer ${emailApiKey}` },
1491
- });
1492
-
1493
- ketrics.console.log('Report generated successfully', { fileName });
1494
-
1495
- return {
1496
- success: true,
1497
- fileName,
1498
- downloadUrl: downloadUrl.url,
1499
- orderCount: orders.rowCount,
1500
- total,
1501
- };
1502
-
1503
- } finally {
1504
- await db.close();
654
+ if (error instanceof ketrics.DatabaseAccessDeniedError) {
655
+ // Direct instanceof check
656
+ console.log('Application lacks database access grant');
1505
657
  }
1506
658
  }
1507
659
  ```
1508
660
 
1509
- ---
661
+ ### Testing Approach
1510
662
 
1511
- ## License
1512
-
1513
- MIT
663
+ **Unit Testing Tenant Code**:
664
+ ```bash
665
+ # Tenant applications test their handlers without running in Ketrics platform
666
+ npm install --save-dev jest @types/jest ts-jest
667
+
668
+ # Mock the global ketrics object for tests
669
+ jest.mock('@ketrics/sdk-backend', () => ({
670
+ // Provide mock implementations
671
+ }));
672
+ ```
673
+
674
+ **Integration Testing** (in Ketrics platform):
675
+ - Deploy application to staging environment
676
+ - Ketrics platform verifies type compatibility at build time
677
+ - Runtime validates against actual service permissions
678
+ - Test tenant isolation and error scenarios
679
+
680
+ **Type Safety**:
681
+ - TypeScript compilation enforces correct API usage at build time
682
+ - No runtime type checking (SDK is pure types)
683
+ - Type definitions include JSDoc comments with usage examples
684
+
685
+ ## Architecture Patterns
686
+
687
+ ### API Design Patterns
688
+
689
+ **Static Factory Pattern**:
690
+ - Volume.connect(), DatabaseConnection.connect(), Secret.get()
691
+ - No constructor calls, explicit factory methods
692
+ - Enables lazy connection pooling and resource management
693
+
694
+ **Error Type Guards**:
695
+ - `isVolumeError(error)` - Checks if error is any VolumeError
696
+ - `isVolumeErrorType(error, 'VolumeNotFoundError')` - Checks specific type
697
+ - Enables type-safe error narrowing without instanceof
698
+
699
+ **Transaction Scope**:
700
+ - `db.transaction(async (tx) => { ... })`
701
+ - Callback receives transaction connection
702
+ - Automatic commit on success, rollback on error
703
+
704
+ **Pagination with Cursors**:
705
+ - List operations return cursor for next page
706
+ - Enables efficient large result set handling
707
+ - Stateless pagination without offset/limit complexity
708
+
709
+ ### File Export Strategy
710
+
711
+ The index.ts file re-exports all types from feature modules:
712
+ 1. Interfaces and types for tenant usage
713
+ 2. Error classes for instanceof checking
714
+ 3. Type guards for runtime error discrimination
715
+ 4. Global KetricsSdkV1 interface defining the complete ketrics object
716
+
717
+ This central export point ensures:
718
+ - Single source of truth for SDK surface
719
+ - Complete type definitions for IDE autocomplete
720
+ - Proper TypeScript declaration file generation