@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 +661 -1454
- package/dist/context.d.ts +2 -2
- package/dist/database-errors.d.ts +12 -12
- package/dist/database-errors.js +12 -12
- package/dist/databases.d.ts +7 -7
- package/dist/databases.js +1 -1
- package/dist/excel-errors.d.ts +2 -2
- package/dist/excel-errors.js +2 -2
- package/dist/index.d.ts +356 -207
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/job-errors.d.ts +5 -5
- package/dist/job-errors.d.ts.map +1 -1
- package/dist/job-errors.js +11 -11
- package/dist/job-errors.js.map +1 -1
- package/dist/messages-errors.d.ts +3 -3
- package/dist/messages-errors.js +3 -3
- package/dist/pdf-errors.d.ts +2 -2
- package/dist/pdf-errors.js +2 -2
- package/dist/secret-errors.d.ts +3 -3
- package/dist/secret-errors.js +3 -3
- package/package.json +2 -2
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
//
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
```typescript
|
|
638
|
-
// Convert worksheet to CSV
|
|
639
|
-
const csvContent = await workbook.toCsv('Sheet1');
|
|
468
|
+
## Usage
|
|
640
469
|
|
|
641
|
-
|
|
642
|
-
await volume.put('output/data.csv', csvContent, {
|
|
643
|
-
contentType: 'text/csv',
|
|
644
|
-
});
|
|
645
|
-
```
|
|
470
|
+
### How to Run/Deploy This Component
|
|
646
471
|
|
|
647
|
-
|
|
472
|
+
This is a type definition library. It does not run standalone but is consumed by tenant applications.
|
|
648
473
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
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 (
|
|
1192
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|