@ketrics/sdk-backend 0.7.0 → 0.7.1

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.
Files changed (3) hide show
  1. package/README.md +609 -1362
  2. package/dist/context.d.ts +2 -2
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,1513 +1,760 @@
1
1
  # @ketrics/sdk-backend
2
2
 
3
- TypeScript type definitions for building tenant applications on the Ketrics platform.
3
+ TypeScript type definitions and runtime interfaces for building tenant applications on the Ketrics platform.
4
4
 
5
- ## Installation
5
+ ## 1. Overview
6
6
 
7
- ```bash
8
- npm install @ketrics/sdk-backend
9
- ```
10
-
11
- ## Overview
12
-
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:
7
+ ### Purpose
14
8
 
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();
9
+ The `@ketrics/sdk-backend` package provides TypeScript type definitions for the Ketrics backend SDK. It defines the contract between tenant application code and the Ketrics Data Plane runtime environment. Tenant developers install this package to get full TypeScript intellisense and type safety when writing application code that runs inside Ketrics' sandboxed JavaScript VM.
46
10
 
47
- // Access encrypted secrets
48
- const apiKey = await ketrics.Secret.get('stripe-api-key');
11
+ ### Role in the Architecture
49
12
 
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
13
  ```
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")
14
+ ┌─────────────────────────────────────────────────────────────────────────────┐
15
+ │ Ketrics Platform │
16
+ ├─────────────────────────────────────────────────────────────────────────────┤
17
+ │ │
18
+ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
19
+ │ │ Tenant API │ │ Data Plane │ │ ketrics-sdk- │ │
20
+ │ │ API │ │ API │ │ backend │ │
21
+ │ │ │ │ │ │ (this pkg) │ │
22
+ │ │ - Tenant mgmt │ │ - VM Sandbox │◄─────│ │ │
23
+ │ │ - App deploy │ │ - Request exec │ │ Type defs for │ │
24
+ │ │ - User mgmt │ │ - SDK injection │ │ global `ketrics`│ │
25
+ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
26
+ │ │ │ ▲ │
27
+ │ │ │ │ │
28
+ │ ▼ ▼ │ │
29
+ │ ┌──────────────────────────────────────────────────┐ │ │
30
+ │ │ Tenant Applications │────────┘ │
31
+ │ │ │ │
32
+ │ │ import type { IVolume } from '@ketrics/sdk-backend' │
33
+ │ │ │ │
34
+ │ │ export async function handler() { │ │
35
+ │ │ const volume = await ketrics.Volume.connect('uploads'); │
36
+ │ │ // Full TypeScript support via this SDK │ │
37
+ │ │ } │ │
38
+ │ └───────────────────────────────────────────────────┘ │
39
+ └─────────────────────────────────────────────────────────────────────────────┘
70
40
  ```
71
41
 
72
- ### Application Context
42
+ The SDK sits between the Data Plane runtime (which injects the actual `ketrics` global object) and tenant application code. It provides:
73
43
 
74
- Access information about the current application:
44
+ 1. **Type definitions** for the global `ketrics` object
45
+ 2. **Interface contracts** for all SDK modules (Volume, Database, Secret, Excel, PDF, Job, Messages)
46
+ 3. **Error class hierarchies** for type-safe error handling
47
+ 4. **Type guards** for runtime error checking
75
48
 
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
- ```
49
+ ### Key Responsibilities
83
50
 
84
- ### User Context
51
+ - Define the shape of the `ketrics` global object available in tenant code
52
+ - Provide TypeScript types for context objects (tenant, application, requestor, runtime)
53
+ - Define interfaces for all platform services (storage, database, secrets, file processing)
54
+ - Export error classes for `instanceof` checks in catch blocks
55
+ - Export type guards (`isVolumeError`, `isDatabaseError`, etc.) for safe error handling
85
56
 
86
- Access information about the user making the request:
57
+ ### Boundaries
87
58
 
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
- ```
59
+ This package is **types only** at development time. It contains:
60
+ - TypeScript interfaces and type definitions
61
+ - Abstract error base classes and concrete error implementations
62
+ - Type guard functions
94
63
 
95
- ### Environment Context
64
+ It does **not** contain:
65
+ - Actual SDK implementations (those are in the Data Plane API)
66
+ - Runtime logic (the Data Plane injects the real `ketrics` object)
67
+ - API clients or network code
96
68
 
97
- Access runtime environment information:
69
+ ## 2. Business Logic
98
70
 
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
- ```
71
+ ### Problem Solved
120
72
 
121
- ---
73
+ Tenant developers need to write application code that runs inside Ketrics' sandboxed VM. Without this SDK, they would have no TypeScript support, leading to:
74
+ - No autocomplete or intellisense in their IDE
75
+ - No compile-time type checking
76
+ - No documentation for available methods and their signatures
77
+ - Difficulty understanding error types for proper handling
122
78
 
123
- ## Console Logging
79
+ ### Core Workflows
124
80
 
125
- Logs are automatically forwarded to CloudWatch with proper context (tenant, application, user, request ID):
81
+ #### 1. Context Access
82
+ Tenant code accesses execution context through `ketrics.tenant`, `ketrics.application`, `ketrics.requestor`, and `ketrics.runtime`. The SDK defines the exact shape of these objects.
126
83
 
127
84
  ```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 });
85
+ // TenantContext: id, code, name
86
+ // ApplicationContext: id, code, name, version, deploymentId
87
+ // RequestorContext: type ('USER' | 'SERVICE_ACCOUNT'), userId?, serviceAccountId?, name, email?, applicationPermissions
88
+ // RuntimeContext: nodeVersion, runtime ('ecs-fargate'), region
133
89
  ```
134
90
 
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
- ---
91
+ #### 2. Resource Access Pattern
92
+ All resources (volumes, databases, secrets) follow a consistent pattern:
143
93
 
144
- ## HTTP Client
145
-
146
- Make external API requests with built-in timeout and user agent:
94
+ 1. **Connect/Get** - Obtain a handle to the resource by code
95
+ 2. **Perform Operations** - Use methods on the returned interface
96
+ 3. **Handle Errors** - Catch typed errors using `instanceof`
147
97
 
148
98
  ```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
- });
250
-
251
- // Create only if file doesn't exist
252
99
  try {
253
- await volume.put('config.json', defaultConfig, { ifNotExists: true });
100
+ const volume = await ketrics.Volume.connect('uploads'); // Step 1
101
+ const file = await volume.get('report.pdf'); // Step 2
254
102
  } catch (error) {
255
- if (error instanceof ketrics.FileAlreadyExistsError) {
256
- console.log('Config already exists, skipping');
103
+ if (error instanceof ketrics.FileNotFoundError) { // Step 3
104
+ // Handle specific error
257
105
  }
258
106
  }
259
107
  ```
260
108
 
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
- });
109
+ #### 3. File Processing
110
+ Excel and PDF modules follow a factory pattern:
111
+ - `ketrics.Excel.read(buffer)` - Parse existing file
112
+ - `ketrics.Excel.create()` - Create new file
113
+ - `ketrics.Pdf.read(buffer)` - Parse existing PDF
114
+ - `ketrics.Pdf.create()` - Create new PDF
115
+
116
+ ### Business Rules
117
+
118
+ 1. **Permission-Based Access**: All resource access is gated by application grants. The SDK types reflect this with `permissions` properties on connection interfaces.
119
+
120
+ 2. **Tenant Isolation**: The SDK's context types (`TenantContext`, `ApplicationContext`) ensure tenant code always knows its execution context.
121
+
122
+ 3. **Requestor Types**: The `RequestorContext` distinguishes between human users (`type: 'USER'`) and service accounts (`type: 'SERVICE_ACCOUNT'`) for audit and access control.
123
+
124
+ 4. **Error Hierarchy**: Each module has a base error class (e.g., `VolumeError`) with specific subclasses. This allows catching all errors from a module or specific error types.
125
+
126
+ ### Input/Output Expectations
127
+
128
+ | Module | Input | Output |
129
+ |--------|-------|--------|
130
+ | Volume.connect | Volume code (string) | IVolume interface |
131
+ | volume.get | File key (string) | FileContent (content, contentType, size, etag) |
132
+ | volume.put | Key, content, options | PutResult (key, etag, size) |
133
+ | DatabaseConnection.connect | Database code (string) | IDatabaseConnection interface |
134
+ | db.query | SQL string, params array | DatabaseQueryResult (rows, rowCount) |
135
+ | Secret.get | Secret code (string) | Decrypted string value |
136
+ | Excel.read | Buffer | IExcelWorkbook |
137
+ | Pdf.create | (none) | IPdfDocument |
138
+ | Job.runInBackground | RunInBackgroundParams | Job ID (string) |
139
+ | Messages.send | SendMessageParams | SendMessageResult |
140
+
141
+ ### Edge Cases Handled
142
+
143
+ 1. **File Not Found**: `FileNotFoundError` with the key that wasn't found
144
+ 2. **Permission Denied**: Separate errors for "no grant" vs "missing specific permission"
145
+ 3. **Path Traversal**: `InvalidPathError` for attempts like `../../../etc/passwd`
146
+ 4. **Size Limits**: `FileSizeLimitError` includes both actual and max size
147
+ 5. **Content Type Restrictions**: `ContentTypeNotAllowedError` includes allowed types
148
+ 6. **Cross-App Jobs**: `CrossAppPermissionError` for background jobs targeting other apps
149
+ 7. **Transaction Failures**: `DatabaseTransactionError` includes whether rollback occurred
150
+
151
+ ## 3. Technical Details
152
+
153
+ ### Technology Stack
154
+
155
+ - **Language**: TypeScript 5.x
156
+ - **Target**: ES2020
157
+ - **Module System**: CommonJS
158
+ - **Node Version**: 24.x (required for `@types/node` peer dependency)
159
+ - **Build**: TypeScript compiler (`tsc`)
160
+ - **Output**: JavaScript + Declaration files (`.d.ts`)
161
+
162
+ ### File Structure
163
+
164
+ ```
165
+ ketrics-sdk-backend/
166
+ ├── package.json # NPM package configuration
167
+ ├── tsconfig.json # TypeScript compiler options
168
+ ├── src/
169
+ │ ├── index.ts # Main entry: exports all types, defines KetricsSdkV1 interface
170
+ │ ├── context.ts # TenantContext, ApplicationContext, RequestorContext, RuntimeContext
171
+ │ ├── console.ts # ConsoleLogger interface (log, error, warn, info, debug)
172
+ │ ├── http.ts # HttpClient, HttpRequestConfig, HttpResponse
173
+ │ ├── volumes.ts # IVolume, FileContent, PutOptions, ListResult, etc.
174
+ │ ├── errors.ts # VolumeError hierarchy (VolumeNotFoundError, FileNotFoundError, etc.)
175
+ │ ├── databases.ts # IDatabaseConnection, DatabaseQueryResult, transaction support
176
+ │ ├── database-errors.ts # DatabaseError hierarchy
177
+ │ ├── secrets.ts # ISecret interface (get, exists)
178
+ │ ├── secret-errors.ts # SecretError hierarchy
179
+ │ ├── excel.ts # IExcelWorkbook, IExcelWorksheet, IExcelRow, IExcelCell
180
+ │ ├── excel-errors.ts # ExcelError, ExcelParseError, ExcelWriteError
181
+ │ ├── pdf.ts # IPdfDocument, IPdfPage, drawing options, embedded resources
182
+ │ ├── pdf-errors.ts # PdfError, PdfParseError, PdfWriteError
183
+ │ ├── job.ts # RunInBackgroundParams, JobStatus, JobListResult
184
+ │ ├── job-errors.ts # JobError hierarchy
185
+ │ ├── messages.ts # SendMessageParams, MessagesManager, bulk/group messaging
186
+ │ └── messages-errors.ts # MessageError hierarchy
187
+ └── dist/ # Compiled output (generated)
188
+ ├── index.js # Compiled JavaScript
189
+ ├── index.d.ts # Type declarations
190
+ └── ...
191
+ ```
192
+
193
+ ### Key Interfaces
194
+
195
+ #### KetricsSdkV1 (Main SDK Interface)
196
+
197
+ The primary interface defining the global `ketrics` object:
198
+
199
+ ```typescript
200
+ interface KetricsSdkV1 {
201
+ // Context (read-only)
202
+ tenant: TenantContext;
203
+ application: ApplicationContext;
204
+ requestor: RequestorContext;
205
+ runtime: RuntimeContext;
206
+ environment: EnvironmentVariables;
207
+
208
+ // Utilities
209
+ console: ConsoleLogger;
210
+ http: HttpClient;
211
+
212
+ // Resource Factories
213
+ Volume: { connect(code: string): Promise<IVolume> };
214
+ DatabaseConnection: { connect(code: string): Promise<IDatabaseConnection> };
215
+ Secret: { get(code: string): Promise<string>; exists(code: string): Promise<boolean> };
216
+ Excel: { read(buffer: Buffer): Promise<IExcelWorkbook>; create(): IExcelWorkbook };
217
+ Pdf: { read(buffer: Buffer): Promise<IPdfDocument>; create(): Promise<IPdfDocument>; rgb(r, g, b): PdfRgbColor };
218
+ Job: { runInBackground(params): Promise<string>; getStatus(id): Promise<JobStatus>; list(params?): Promise<JobListResult> };
219
+ Messages: MessagesManager;
220
+
221
+ // Error Classes (for instanceof checks)
222
+ VolumeError, VolumeNotFoundError, FileNotFoundError, ...
223
+ DatabaseError, DatabaseNotFoundError, DatabaseQueryError, ...
224
+ SecretError, SecretNotFoundError, ...
225
+ ExcelError, PdfError, JobError, MessageError, ...
226
+
227
+ // Type Guards
228
+ isVolumeError, isVolumeErrorType, isDatabaseError, ...
280
229
  }
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
-
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
230
  ```
298
231
 
299
- ### Copying and Moving Files
232
+ #### IVolume (S3-Backed Storage)
300
233
 
301
234
  ```typescript
302
- // Copy a file
303
- await volume.copy('source.pdf', 'backup/source.pdf');
235
+ interface IVolume {
236
+ readonly code: string;
237
+ readonly permissions: ReadonlySet<string>;
304
238
 
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
- });
239
+ // Read
240
+ get(key: string): Promise<FileContent>;
241
+ exists(key: string): Promise<boolean>;
242
+ getMetadata(key: string): Promise<FileMetadata>;
310
243
 
311
- // Move a file (copy + delete)
312
- await volume.move('temp/upload.pdf', 'documents/final.pdf');
313
- ```
244
+ // Write
245
+ put(key: string, content: PutContent, options?: PutOptions): Promise<PutResult>;
314
246
 
315
- ### Generating Presigned URLs
247
+ // Delete
248
+ delete(key: string): Promise<DeleteResult>;
249
+ deleteByPrefix(prefix: string): Promise<DeleteByPrefixResult>;
316
250
 
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
331
- });
332
- // Client can PUT directly to uploadUrl.url
333
- ```
251
+ // List
252
+ list(options?: ListOptions): Promise<ListResult>;
334
253
 
335
- ---
254
+ // File Management
255
+ copy(src: string, dest: string, options?: CopyOptions): Promise<CopyResult>;
256
+ move(src: string, dest: string, options?: MoveOptions): Promise<MoveResult>;
336
257
 
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', ... }
258
+ // URL Generation
259
+ generateDownloadUrl(key: string, options?: DownloadUrlOptions): Promise<PresignedUrl>;
260
+ generateUploadUrl(key: string, options?: UploadUrlOptions): Promise<PresignedUrl>;
261
+ }
352
262
  ```
353
263
 
354
- ### Querying Data
264
+ #### IDatabaseConnection (External Database Access)
355
265
 
356
266
  ```typescript
357
- interface User {
358
- id: number;
359
- name: string;
360
- email: string;
361
- created_at: Date;
362
- }
267
+ interface IDatabaseConnection {
268
+ readonly code: string;
269
+ readonly permissions: ReadonlySet<string>;
363
270
 
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);
271
+ query<T>(sql: string, params?: unknown[]): Promise<DatabaseQueryResult<T>>;
272
+ execute(sql: string, params?: unknown[]): Promise<DatabaseExecuteResult>;
273
+ transaction<T>(fn: (tx: IDatabaseConnection) => Promise<T>): Promise<T>;
274
+ close(): Promise<void>;
382
275
  }
383
276
  ```
384
277
 
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
278
+ ### Configuration
279
+
280
+ **tsconfig.json settings:**
281
+ - `target: "ES2020"` - Modern JavaScript output
282
+ - `module: "commonjs"` - Node.js compatible modules
283
+ - `declaration: true` - Generate `.d.ts` files
284
+ - `declarationMap: true` - Enable declaration sourcemaps
285
+ - `strict: true` - Full TypeScript strictness
286
+ - `outDir: "./dist"` - Output directory
287
+
288
+ **No environment variables** - This is a pure type definition package.
289
+
290
+ ### External Integrations
291
+
292
+ This package has **no runtime dependencies**. It only requires `@types/node` as a peer dependency for Node.js built-in types like `Buffer` and `stream.Readable`.
293
+
294
+ The types it defines correspond to these Data Plane integrations:
295
+ - **AWS S3** (via Volume interface)
296
+ - **PostgreSQL/MySQL/MSSQL** (via DatabaseConnection interface)
297
+ - **AWS KMS** (via Secret interface, for decryption)
298
+ - **ExcelJS** (via Excel interface)
299
+ - **pdf-lib** (via Pdf interface)
300
+ - **AWS CloudWatch Logs** (via ConsoleLogger interface)
301
+
302
+ ## 4. Data Flow
303
+
304
+ ### Type Definition Flow
305
+
306
+ ```
307
+ ┌─────────────────────────────────────────────────────────────────────────────┐
308
+ │ Development Time │
309
+ ├─────────────────────────────────────────────────────────────────────────────┤
310
+ │ │
311
+ │ Tenant Developer's IDE │
312
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
313
+ │ │ │ │
314
+ │ │ import type { IVolume } from '@ketrics/sdk-backend'; │ │
315
+ │ │ ▲ │ │
316
+ │ │ │ │ │
317
+ │ │ // TypeScript uses the │ │ │
318
+ │ │ // SDK types for: │ │ │
319
+ │ │ // - Autocomplete │ │ │
320
+ │ │ // - Type checking │ │ │
321
+ │ │ // - Error detection │ │ │
322
+ │ │ │ │ │
323
+ │ │ const volume = await ketrics.Volume.connect('uploads'); │ │
324
+ │ │ // ▲ │ │
325
+ │ │ // │ TypeScript knows this is IVolume │ │
326
+ │ │ // │ │ │
327
+ │ │ const file = await volume.get('doc.pdf'); │ │
328
+ │ │ // ▲ │ │
329
+ │ │ // │ TypeScript knows this is FileContent │ │
330
+ │ │ │ │
331
+ │ └─────────────────────────────────────────────────────────────────────┘ │
332
+ │ │
333
+ └─────────────────────────────────────────────────────────────────────────────┘
334
+
335
+ ┌─────────────────────────────────────────────────────────────────────────────┐
336
+ │ Runtime (Data Plane) │
337
+ ├─────────────────────────────────────────────────────────────────────────────┤
338
+ │ │
339
+ │ VM Sandbox │
340
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
341
+ │ │ │ │
342
+ │ │ global.ketrics = { │ │
343
+ │ │ tenant: { id: '...', code: 'acme', name: 'ACME Corp' }, │ │
344
+ │ │ Volume: { │ │
345
+ │ │ connect: async (code) => { /* actual S3 implementation */ } │ │
346
+ │ │ }, │ │
347
+ │ │ // ... all other SDK modules injected by Data Plane │ │
348
+ │ │ }; │ │
349
+ │ │ │ │
350
+ │ │ // Tenant code executes with real implementations │ │
351
+ │ │ await handler(); │ │
352
+ │ │ │ │
353
+ │ └─────────────────────────────────────────────────────────────────────┘ │
354
+ │ │
355
+ └─────────────────────────────────────────────────────────────────────────────┘
356
+ ```
357
+
358
+ ### Error Flow
359
+
360
+ ```
361
+ Operation Fails in Data Plane
362
+
363
+
364
+ ┌─────────────────────────────────────┐
365
+ │ Data Plane throws typed error │
366
+ │ e.g., new FileNotFoundError(...) │
367
+ └─────────────────────────────────────┘
368
+
369
+
370
+ ┌─────────────────────────────────────┐
371
+ │ Error propagates to tenant code │
372
+ └─────────────────────────────────────┘
373
+
374
+
375
+ ┌─────────────────────────────────────┐
376
+ │ Tenant catch block │
377
+ │ │
378
+ │ catch (error) { │
379
+ │ if (error instanceof ketrics. │
380
+ │ FileNotFoundError) { │
381
+ │ // TypeScript knows error.key │
382
+ │ // exists on this type │
383
+ │ } │
384
+ │ } │
385
+ └─────────────────────────────────────┘
386
+ ```
387
+
388
+ ### Module Dependency Graph
389
+
390
+ ```
391
+ index.ts (main entry)
392
+
393
+ ├── context.ts (no dependencies)
394
+ ├── console.ts (no dependencies)
395
+ ├── http.ts (no dependencies)
396
+
397
+ ├── volumes.ts
398
+ │ └── uses: stream.Readable from Node.js
399
+
400
+ ├── errors.ts (volume errors)
401
+ │ └── no dependencies
402
+
403
+ ├── databases.ts
404
+ │ └── no dependencies
405
+
406
+ ├── database-errors.ts
407
+ │ └── no dependencies
408
+
409
+ ├── secrets.ts
410
+ │ └── no dependencies
411
+
412
+ ├── secret-errors.ts
413
+ │ └── no dependencies
414
+
415
+ ├── excel.ts
416
+ │ └── no dependencies
417
+
418
+ ├── excel-errors.ts
419
+ │ └── no dependencies
420
+
421
+ ├── pdf.ts
422
+ │ └── no dependencies
423
+
424
+ ├── pdf-errors.ts
425
+ │ └── no dependencies
426
+
427
+ ├── job.ts
428
+ │ └── no dependencies
429
+
430
+ ├── job-errors.ts
431
+ │ └── no dependencies
432
+
433
+ ├── messages.ts
434
+ │ └── no dependencies
435
+
436
+ └── messages-errors.ts
437
+ └── no dependencies
438
+ ```
439
+
440
+ ## 5. Error Handling
441
+
442
+ ### Error Hierarchy Design
443
+
444
+ Each SDK module follows a consistent error hierarchy pattern:
445
+
446
+ ```
447
+ Error (built-in)
448
+ └── ModuleError (abstract base)
449
+ ├── ModuleNotFoundError
450
+ ├── ModuleAccessDeniedError
451
+ └── ModuleSpecificError1
452
+ └── ModuleSpecificError2
453
+ └── ...
409
454
  ```
410
455
 
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
- );
456
+ ### Volume Errors
433
457
 
434
- return { transferId: transfer.insertId };
435
- });
458
+ | Error Class | Code | Description |
459
+ |-------------|------|-------------|
460
+ | `VolumeError` | - | Abstract base class |
461
+ | `VolumeNotFoundError` | - | Volume code doesn't exist in tenant |
462
+ | `VolumeAccessDeniedError` | - | Application lacks grant to volume |
463
+ | `VolumePermissionDeniedError` | - | Operation requires missing permission (ReadObject, CreateObject, etc.) |
464
+ | `FileNotFoundError` | - | File key doesn't exist |
465
+ | `FileAlreadyExistsError` | - | File exists when using `ifNotExists: true` |
466
+ | `InvalidPathError` | - | Path contains invalid characters or traversal attempt |
467
+ | `FileSizeLimitError` | - | File exceeds volume's configured limit |
468
+ | `ContentTypeNotAllowedError` | - | MIME type not in volume's allowlist |
436
469
 
437
- console.log('Transfer completed:', result.transferId);
438
- ```
470
+ ### Database Errors
439
471
 
440
- ### Closing Connections
472
+ | Error Class | Description |
473
+ |-------------|-------------|
474
+ | `DatabaseError` | Abstract base class |
475
+ | `DatabaseNotFoundError` | Database code doesn't exist |
476
+ | `DatabaseAccessDeniedError` | Application lacks grant |
477
+ | `DatabaseConnectionError` | Cannot establish connection (network, auth, etc.) |
478
+ | `DatabaseQueryError` | SQL execution failed (syntax, constraint violation) |
479
+ | `DatabaseTransactionError` | Transaction failed (includes `rolledBack` property) |
441
480
 
442
- ```typescript
443
- // Always close when done to return connection to pool
444
- const db = await ketrics.DatabaseConnection.connect('main-db');
445
- try {
446
- const result = await db.query('SELECT * FROM users');
447
- return result.rows;
448
- } finally {
449
- await db.close();
450
- }
451
- ```
481
+ ### Secret Errors
452
482
 
453
- ---
483
+ | Error Class | Description |
484
+ |-------------|-------------|
485
+ | `SecretError` | Abstract base class |
486
+ | `SecretNotFoundError` | Secret code doesn't exist |
487
+ | `SecretAccessDeniedError` | Application lacks grant |
488
+ | `SecretDecryptionError` | KMS decryption failed (key issue, corrupted data) |
454
489
 
455
- ## Secrets
490
+ ### File Processing Errors
456
491
 
457
- Access encrypted secrets stored with tenant-specific KMS encryption:
492
+ | Error Class | Description |
493
+ |-------------|-------------|
494
+ | `ExcelError` | Abstract base class |
495
+ | `ExcelParseError` | Cannot parse .xlsx file (invalid format, corrupted) |
496
+ | `ExcelWriteError` | Cannot write .xlsx file |
497
+ | `PdfError` | Abstract base class |
498
+ | `PdfParseError` | Cannot parse PDF file |
499
+ | `PdfWriteError` | Cannot write PDF file |
458
500
 
459
- ### Getting Secrets
501
+ ### Job Errors
460
502
 
461
- ```typescript
462
- // Get a secret value
463
- const apiKey = await ketrics.Secret.get('stripe-api-key');
503
+ | Error Class | Description |
504
+ |-------------|-------------|
505
+ | `JobError` | Abstract base class |
506
+ | `JobNotFoundError` | Job ID doesn't exist |
507
+ | `InvalidFunctionError` | Function name is empty or invalid |
508
+ | `CrossAppPermissionError` | Missing permission for cross-app job execution |
509
+ | `JobExecutionError` | Job failed during execution |
464
510
 
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
- ```
511
+ ### Message Errors
470
512
 
471
- ### Checking Secret Existence
513
+ | Error Class | Description |
514
+ |-------------|-------------|
515
+ | `MessageError` | Base class |
516
+ | `MessageValidationError` | Invalid message parameters |
517
+ | `GroupNotFoundError` | Group code doesn't exist |
518
+ | `TenantGrantPermissionDeniedError` | Missing IAM-data permissions for group messaging |
472
519
 
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
- ```
520
+ ### Type Guards
482
521
 
483
- ### Common Patterns
522
+ Each module provides type guard functions:
484
523
 
485
524
  ```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');
525
+ // Check if any error from module
526
+ if (isVolumeError(error)) { /* error is VolumeError */ }
527
+ if (isDatabaseError(error)) { /* error is DatabaseError */ }
528
+ if (isSecretError(error)) { /* error is SecretError */ }
492
529
 
493
- // OAuth credentials
494
- const clientId = await ketrics.Secret.get('oauth-client-id');
495
- const clientSecret = await ketrics.Secret.get('oauth-client-secret');
530
+ // Check for specific error type
531
+ if (isVolumeErrorType(error, FileNotFoundError)) {
532
+ console.log(error.key); // TypeScript knows error.key exists
533
+ }
496
534
  ```
497
535
 
498
- ---
499
-
500
- ## Excel Files
536
+ ### Error Properties
501
537
 
502
- Read and write Excel files (.xlsx) using a simple API:
538
+ All error classes include:
539
+ - `name`: Error class name (e.g., `"FileNotFoundError"`)
540
+ - `message`: Human-readable description
541
+ - `timestamp`: When the error occurred
542
+ - `toJSON()`: Serializes error for logging (no stack trace, no internal details)
503
543
 
504
- ### Reading Excel Files
544
+ Module-specific properties:
545
+ - Volume errors: `volumeCode`, `operation`
546
+ - Database errors: `databaseCode`, `operation`
547
+ - Secret errors: `secretCode`, `operation`
548
+ - File errors: `key` (the file path)
549
+ - Permission errors: `requiredPermission`
505
550
 
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');
551
+ ### Security Considerations
512
552
 
513
- // Parse Excel file
514
- const workbook = await ketrics.Excel.read(file.content);
553
+ Error messages are designed to avoid leaking sensitive information:
554
+ - No connection strings or credentials
555
+ - No internal tenant IDs
556
+ - No AWS account information
557
+ - SQL limited to 200 characters in `DatabaseQueryError`
515
558
 
516
- // Get worksheet by name
517
- const sheet = workbook.getWorksheet('Sheet1');
518
- if (!sheet) {
519
- throw new Error('Sheet not found');
520
- }
559
+ ## 6. Usage
521
560
 
522
- // Get worksheet by index (1-indexed)
523
- const firstSheet = workbook.getWorksheet(1);
561
+ ### Installation
524
562
 
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
- }
563
+ ```bash
564
+ npm install @ketrics/sdk-backend
530
565
  ```
531
566
 
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);
539
- }
567
+ ### Development Requirements
540
568
 
541
- // Get a specific row (1-indexed)
542
- const headerRow = sheet.getRow(1);
543
- console.log('Headers:', headerRow.values);
569
+ - Node.js 24.x or higher
570
+ - TypeScript 5.x
544
571
 
545
- // Get a specific cell
546
- const cell = sheet.getCell('A1');
547
- console.log(cell.value, cell.text, cell.address);
572
+ ### Build Commands
548
573
 
549
- // Get cell by coordinates
550
- const cellB2 = sheet.getCell(2, 2);
574
+ ```bash
575
+ # Install dependencies
576
+ npm install
551
577
 
552
- // Iterate over rows
553
- sheet.eachRow((row, rowNumber) => {
554
- console.log(`Row ${rowNumber}:`, row.values);
555
- });
578
+ # Build TypeScript to JavaScript + declarations
579
+ npm run build
556
580
 
557
- // Include empty rows
558
- sheet.eachRow((row, rowNumber) => {
559
- console.log(`Row ${rowNumber}:`, row.values);
560
- }, { includeEmpty: true });
581
+ # Clean build output
582
+ npm run clean
561
583
 
562
- // Get all values as 2D array
563
- const allValues = sheet.getSheetValues();
564
- // allValues[1] = first row, allValues[1][1] = cell A1
584
+ # Build and publish to npm
585
+ npm run prepublishOnly # Runs build automatically
565
586
  ```
566
587
 
567
- ### Working with Cells
588
+ ### Using in Tenant Applications
568
589
 
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
590
+ The SDK is designed for **type imports only** in tenant code:
588
591
 
589
592
  ```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
- ```
593
+ // Import types for type annotations
594
+ import type {
595
+ IVolume,
596
+ FileContent,
597
+ IDatabaseConnection,
598
+ DatabaseQueryResult
599
+ } from '@ketrics/sdk-backend';
634
600
 
635
- ### Converting to CSV
601
+ // The global `ketrics` object is automatically available at runtime
602
+ export async function processOrder(orderId: string) {
603
+ // Access tenant context
604
+ console.log(`Processing for tenant: ${ketrics.tenant.name}`);
636
605
 
637
- ```typescript
638
- // Convert worksheet to CSV
639
- const csvContent = await workbook.toCsv('Sheet1');
606
+ // Log to CloudWatch
607
+ ketrics.console.log('Starting order processing', { orderId });
640
608
 
641
- // Save as CSV
642
- await volume.put('output/data.csv', csvContent, {
643
- contentType: 'text/csv',
644
- });
645
- ```
609
+ // Access S3 volume
610
+ const volume: IVolume = await ketrics.Volume.connect('orders');
611
+ const orderFile: FileContent = await volume.get(`${orderId}.json`);
646
612
 
647
- ### Managing Worksheets
613
+ // Query database
614
+ const db = await ketrics.DatabaseConnection.connect('orders-db');
615
+ const result = await db.query<{ status: string }>(
616
+ 'UPDATE orders SET status = ? WHERE id = ? RETURNING status',
617
+ ['processed', orderId]
618
+ );
619
+ await db.close();
648
620
 
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';
621
+ return { success: true, newStatus: result.rows[0].status };
658
622
  }
659
623
  ```
660
624
 
661
- ---
662
-
663
- ## PDF Files
664
-
665
- Read, create, and modify PDF files using the pdf-lib wrapper:
666
-
667
- ### Reading PDF Files
625
+ ### Error Handling Pattern
668
626
 
669
627
  ```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);
628
+ export async function safeVolumeRead(volumeCode: string, key: string) {
629
+ try {
630
+ const volume = await ketrics.Volume.connect(volumeCode);
631
+ return await volume.get(key);
632
+ } catch (error) {
633
+ // Check specific error types first
634
+ if (error instanceof ketrics.VolumeNotFoundError) {
635
+ return { error: 'VOLUME_NOT_FOUND', volumeCode: error.volumeCode };
636
+ }
637
+ if (error instanceof ketrics.FileNotFoundError) {
638
+ return { error: 'FILE_NOT_FOUND', key: error.key };
639
+ }
640
+ if (error instanceof ketrics.VolumePermissionDeniedError) {
641
+ return { error: 'PERMISSION_DENIED', permission: error.requiredPermission };
642
+ }
678
643
 
679
- // Get document info
680
- console.log('Page count:', doc.getPageCount());
681
- console.log('Title:', doc.getTitle());
682
- console.log('Author:', doc.getAuthor());
644
+ // Fallback for any other volume error
645
+ if (ketrics.isVolumeError(error)) {
646
+ return { error: 'VOLUME_ERROR', message: error.message };
647
+ }
683
648
 
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}`);
649
+ // Re-throw unexpected errors
650
+ throw error;
651
+ }
688
652
  }
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
653
  ```
779
654
 
780
- ### Drawing Shapes
655
+ ### Complete Example: Report Generation
781
656
 
782
657
  ```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
- });
871
-
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
- });
880
-
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
- });
889
- ```
890
-
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 });
927
- ```
658
+ import type { IExcelWorkbook, IPdfDocument } from '@ketrics/sdk-backend';
928
659
 
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
960
-
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
990
-
991
- ```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
- });
660
+ interface SalesRecord {
661
+ id: number;
662
+ product: string;
663
+ quantity: number;
664
+ total: number;
1018
665
  }
1019
666
 
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
667
+ export async function generateSalesReport(month: string) {
668
+ // Get secrets for external API
669
+ const apiKey = await ketrics.Secret.get('analytics-api-key');
1028
670
 
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]
671
+ // Query database
672
+ const db = await ketrics.DatabaseConnection.connect('sales-db');
673
+ const sales = await db.query<SalesRecord>(
674
+ 'SELECT * FROM sales WHERE month = ? ORDER BY total DESC',
675
+ [month]
1036
676
  );
1037
- const order = orderResult.rows[0];
1038
677
  await db.close();
1039
678
 
1040
- // Create PDF
1041
- const doc = await ketrics.Pdf.create();
1042
- doc.setTitle(`Invoice #${order.invoice_number}`);
1043
- doc.setAuthor(ketrics.tenant.name);
679
+ // Create Excel report
680
+ const workbook: IExcelWorkbook = ketrics.Excel.create();
681
+ const sheet = workbook.addWorksheet('Sales Report');
1044
682
 
1045
- const page = doc.addPage('A4');
1046
- const { width, height } = page.getSize();
683
+ sheet.addRow(['ID', 'Product', 'Quantity', 'Total']);
684
+ for (const sale of sales.rows) {
685
+ sheet.addRow([sale.id, sale.product, sale.quantity, sale.total]);
686
+ }
1047
687
 
1048
- // Embed fonts
1049
- const boldFont = await doc.embedStandardFont('HelveticaBold');
1050
- const regularFont = await doc.embedStandardFont('Helvetica');
688
+ const excelBuffer = await workbook.toBuffer();
1051
689
 
1052
- // Header
1053
- await page.drawText(ketrics.tenant.name, {
1054
- x: 50,
1055
- y: height - 50,
1056
- size: 24,
1057
- font: boldFont,
1058
- });
690
+ // Create PDF summary
691
+ const pdf: IPdfDocument = await ketrics.Pdf.create();
692
+ pdf.setTitle(`Sales Report - ${month}`);
693
+ pdf.setAuthor(ketrics.requestor.name);
1059
694
 
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
- });
695
+ const page = pdf.addPage('A4');
696
+ const font = await pdf.embedStandardFont('HelveticaBold');
1067
697
 
1068
- // Customer info
1069
- await page.drawText('Bill To:', {
1070
- x: 50,
1071
- y: height - 150,
1072
- size: 12,
1073
- font: boldFont,
698
+ await page.drawText(`Sales Report: ${month}`, {
699
+ x: 50, y: 750, size: 24, font
1074
700
  });
1075
701
 
1076
- await page.drawText(order.customer_name, {
1077
- x: 50,
1078
- y: height - 170,
1079
- size: 12,
1080
- font: regularFont,
702
+ await page.drawText(`Total Records: ${sales.rowCount}`, {
703
+ x: 50, y: 700, size: 14
1081
704
  });
1082
705
 
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
- });
706
+ const pdfBuffer = await pdf.toBuffer();
1092
707
 
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;
1106
- }
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 });
708
+ // Save to volume
709
+ const volume = await ketrics.Volume.connect('reports');
710
+ await volume.put(`${month}/sales.xlsx`, excelBuffer);
711
+ await volume.put(`${month}/summary.pdf`, pdfBuffer);
1118
712
 
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),
713
+ // Generate download URLs
714
+ const excelUrl = await volume.generateDownloadUrl(`${month}/sales.xlsx`, {
715
+ expiresIn: 86400
1125
716
  });
1126
717
 
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
- },
718
+ ketrics.console.log('Report generated', {
719
+ month,
720
+ recordCount: sales.rowCount
1136
721
  });
1137
722
 
1138
723
  return {
1139
724
  success: true,
1140
- invoiceNumber: order.invoice_number,
725
+ downloadUrl: excelUrl.url,
726
+ expiresAt: excelUrl.expiresAt
1141
727
  };
1142
728
  }
1143
729
  ```
1144
730
 
1145
- ---
1146
-
1147
- ## Error Handling
1148
-
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;
1180
- }
1181
- }
1182
- ```
1183
-
1184
- ### Database Errors
1185
-
1186
- ```typescript
1187
- try {
1188
- const db = await ketrics.DatabaseConnection.connect('main-db');
1189
- await db.query('SELECT * FROM users');
1190
- } 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
1205
- console.log(`Database error: ${error.message}`);
1206
- } else {
1207
- throw error;
1208
- }
1209
- }
1210
- ```
731
+ ### Testing
1211
732
 
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
- }
1230
- }
1231
- ```
733
+ Since this package contains only type definitions, testing focuses on ensuring:
734
+ 1. TypeScript compiles without errors
735
+ 2. All types are correctly exported
736
+ 3. Error class hierarchies work with `instanceof`
1232
737
 
1233
- ### Excel Errors
738
+ The actual runtime behavior is tested in the Data Plane API where the SDK is implemented.
1234
739
 
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
-
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;
1271
- }
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
-
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,
740
+ ```bash
741
+ # Verify types compile correctly
742
+ npm run build
1350
743
 
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';
744
+ # Type checking is performed as part of build
745
+ # No separate test command needed for type-only package
1409
746
  ```
1410
747
 
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');
748
+ ### Publishing
1451
749
 
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
- }
750
+ ```bash
751
+ # Bump version in package.json
752
+ npm version patch|minor|major
1459
753
 
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();
1505
- }
1506
- }
754
+ # Build and publish
755
+ npm publish --access public
1507
756
  ```
1508
757
 
1509
- ---
1510
-
1511
758
  ## License
1512
759
 
1513
760
  MIT