@ketrics/sdk-backend 0.7.1 → 0.9.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 +660 -700
- 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/documentdb-errors.d.ts +171 -0
- package/dist/documentdb-errors.d.ts.map +1 -0
- package/dist/documentdb-errors.js +237 -0
- package/dist/documentdb-errors.js.map +1 -0
- package/dist/documentdb.d.ts +153 -0
- package/dist/documentdb.d.ts.map +1 -0
- package/dist/documentdb.js +36 -0
- package/dist/documentdb.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/excel-errors.d.ts +2 -2
- package/dist/excel-errors.js +2 -2
- package/dist/index.d.ts +468 -207
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -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 +1 -1
package/README.md
CHANGED
|
@@ -1,760 +1,720 @@
|
|
|
1
|
-
# @ketrics/sdk-backend
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
# Ketrics SDK for Backend (@ketrics/sdk-backend)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
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
|
|
12
132
|
|
|
13
133
|
```
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
│ │ export async function handler() { │ │
|
|
35
|
-
│ │ const volume = await ketrics.Volume.connect('uploads'); │
|
|
36
|
-
│ │ // Full TypeScript support via this SDK │ │
|
|
37
|
-
│ │ } │ │
|
|
38
|
-
│ └───────────────────────────────────────────────────┘ │
|
|
39
|
-
└─────────────────────────────────────────────────────────────────────────────┘
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The SDK sits between the Data Plane runtime (which injects the actual `ketrics` global object) and tenant application code. It provides:
|
|
43
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
### Key Responsibilities
|
|
50
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
### Boundaries
|
|
58
|
-
|
|
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
|
|
63
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
## 2. Business Logic
|
|
70
|
-
|
|
71
|
-
### Problem Solved
|
|
72
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
### Core Workflows
|
|
80
|
-
|
|
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.
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
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
|
|
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)
|
|
89
154
|
```
|
|
90
155
|
|
|
91
|
-
|
|
92
|
-
|
|
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)
|
|
93
251
|
|
|
94
|
-
|
|
95
|
-
2. **Perform Operations** - Use methods on the returned interface
|
|
96
|
-
3. **Handle Errors** - Catch typed errors using `instanceof`
|
|
252
|
+
### External Integrations
|
|
97
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
|
|
358
|
+
|
|
359
|
+
## Error Handling
|
|
360
|
+
|
|
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**:
|
|
98
414
|
```typescript
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
425
|
}
|
|
106
426
|
}
|
|
107
|
-
```
|
|
108
|
-
|
|
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, ...
|
|
229
|
-
}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
#### IVolume (S3-Backed Storage)
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
interface IVolume {
|
|
236
|
-
readonly code: string;
|
|
237
|
-
readonly permissions: ReadonlySet<string>;
|
|
238
|
-
|
|
239
|
-
// Read
|
|
240
|
-
get(key: string): Promise<FileContent>;
|
|
241
|
-
exists(key: string): Promise<boolean>;
|
|
242
|
-
getMetadata(key: string): Promise<FileMetadata>;
|
|
243
|
-
|
|
244
|
-
// Write
|
|
245
|
-
put(key: string, content: PutContent, options?: PutOptions): Promise<PutResult>;
|
|
246
|
-
|
|
247
|
-
// Delete
|
|
248
|
-
delete(key: string): Promise<DeleteResult>;
|
|
249
|
-
deleteByPrefix(prefix: string): Promise<DeleteByPrefixResult>;
|
|
250
|
-
|
|
251
|
-
// List
|
|
252
|
-
list(options?: ListOptions): Promise<ListResult>;
|
|
253
|
-
|
|
254
|
-
// File Management
|
|
255
|
-
copy(src: string, dest: string, options?: CopyOptions): Promise<CopyResult>;
|
|
256
|
-
move(src: string, dest: string, options?: MoveOptions): Promise<MoveResult>;
|
|
257
|
-
|
|
258
|
-
// URL Generation
|
|
259
|
-
generateDownloadUrl(key: string, options?: DownloadUrlOptions): Promise<PresignedUrl>;
|
|
260
|
-
generateUploadUrl(key: string, options?: UploadUrlOptions): Promise<PresignedUrl>;
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
#### IDatabaseConnection (External Database Access)
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
interface IDatabaseConnection {
|
|
268
|
-
readonly code: string;
|
|
269
|
-
readonly permissions: ReadonlySet<string>;
|
|
270
|
-
|
|
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>;
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
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
427
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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' }
|
|
433
|
+
});
|
|
438
434
|
```
|
|
439
435
|
|
|
440
|
-
|
|
436
|
+
### Logging Approach
|
|
441
437
|
|
|
442
|
-
|
|
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
|
|
443
446
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
Error
|
|
448
|
-
└── ModuleError (abstract base)
|
|
449
|
-
├── ModuleNotFoundError
|
|
450
|
-
├── ModuleAccessDeniedError
|
|
451
|
-
└── ModuleSpecificError1
|
|
452
|
-
└── ModuleSpecificError2
|
|
453
|
-
└── ...
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Volume Errors
|
|
457
|
-
|
|
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 |
|
|
469
|
-
|
|
470
|
-
### Database Errors
|
|
471
|
-
|
|
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) |
|
|
480
|
-
|
|
481
|
-
### Secret Errors
|
|
482
|
-
|
|
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) |
|
|
489
|
-
|
|
490
|
-
### File Processing Errors
|
|
491
|
-
|
|
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 |
|
|
500
|
-
|
|
501
|
-
### Job Errors
|
|
502
|
-
|
|
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 |
|
|
510
|
-
|
|
511
|
-
### Message Errors
|
|
512
|
-
|
|
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 |
|
|
519
|
-
|
|
520
|
-
### Type Guards
|
|
521
|
-
|
|
522
|
-
Each module provides type guard functions:
|
|
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
|
|
523
451
|
|
|
452
|
+
**Example Logging Pattern**:
|
|
524
453
|
```typescript
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
454
|
+
try {
|
|
455
|
+
const db = await ketrics.DatabaseConnection.connect('main-db');
|
|
456
|
+
ketrics.console.log('Connected to database');
|
|
457
|
+
const result = await db.query('SELECT * FROM users');
|
|
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
|
+
}
|
|
533
465
|
}
|
|
534
466
|
```
|
|
535
467
|
|
|
536
|
-
|
|
537
|
-
|
|
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)
|
|
543
|
-
|
|
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`
|
|
550
|
-
|
|
551
|
-
### Security Considerations
|
|
552
|
-
|
|
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`
|
|
468
|
+
## Usage
|
|
558
469
|
|
|
559
|
-
|
|
470
|
+
### How to Run/Deploy This Component
|
|
560
471
|
|
|
561
|
-
|
|
472
|
+
This is a type definition library. It does not run standalone but is consumed by tenant applications.
|
|
562
473
|
|
|
474
|
+
**Installation in Tenant Application**:
|
|
563
475
|
```bash
|
|
564
476
|
npm install @ketrics/sdk-backend
|
|
565
477
|
```
|
|
566
478
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
479
|
+
**TypeScript Configuration** (tsconfig.json):
|
|
480
|
+
```json
|
|
481
|
+
{
|
|
482
|
+
"compilerOptions": {
|
|
483
|
+
"strict": true,
|
|
484
|
+
"lib": ["ES2020"],
|
|
485
|
+
"types": ["@ketrics/sdk-backend"]
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
573
489
|
|
|
490
|
+
**Building the SDK Package**:
|
|
574
491
|
```bash
|
|
575
492
|
# Install dependencies
|
|
576
493
|
npm install
|
|
577
494
|
|
|
578
|
-
#
|
|
495
|
+
# Compile TypeScript to JavaScript
|
|
579
496
|
npm run build
|
|
580
497
|
|
|
581
|
-
#
|
|
582
|
-
|
|
498
|
+
# Output: dist/ folder with .js, .d.ts, .map files
|
|
499
|
+
```
|
|
583
500
|
|
|
584
|
-
|
|
585
|
-
|
|
501
|
+
**Publishing**:
|
|
502
|
+
```bash
|
|
503
|
+
npm publish
|
|
586
504
|
```
|
|
587
505
|
|
|
588
|
-
|
|
506
|
+
This publishes to npm registry at https://www.npmjs.com/package/@ketrics/sdk-backend
|
|
589
507
|
|
|
590
|
-
|
|
508
|
+
### Example Invocations
|
|
591
509
|
|
|
510
|
+
**Complete Tenant Application Example**:
|
|
592
511
|
```typescript
|
|
593
|
-
//
|
|
594
|
-
import type {
|
|
595
|
-
IVolume,
|
|
596
|
-
FileContent,
|
|
597
|
-
IDatabaseConnection,
|
|
598
|
-
DatabaseQueryResult
|
|
599
|
-
} from '@ketrics/sdk-backend';
|
|
600
|
-
|
|
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}`);
|
|
605
|
-
|
|
606
|
-
// Log to CloudWatch
|
|
607
|
-
ketrics.console.log('Starting order processing', { orderId });
|
|
608
|
-
|
|
609
|
-
// Access S3 volume
|
|
610
|
-
const volume: IVolume = await ketrics.Volume.connect('orders');
|
|
611
|
-
const orderFile: FileContent = await volume.get(`${orderId}.json`);
|
|
612
|
-
|
|
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();
|
|
620
|
-
|
|
621
|
-
return { success: true, newStatus: result.rows[0].status };
|
|
622
|
-
}
|
|
623
|
-
```
|
|
512
|
+
// handler.ts - Tenant application code
|
|
624
513
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
```typescript
|
|
628
|
-
export async function safeVolumeRead(volumeCode: string, key: string) {
|
|
514
|
+
export async function processOrder(payload: { orderId: string }) {
|
|
629
515
|
try {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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();
|
|
642
582
|
}
|
|
583
|
+
} catch (error) {
|
|
584
|
+
ketrics.console.error('Order processing failed', { error: error.message });
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
643
588
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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;
|
|
647
633
|
}
|
|
648
|
-
|
|
649
|
-
// Re-throw unexpected errors
|
|
650
|
-
throw error;
|
|
651
634
|
}
|
|
652
635
|
}
|
|
653
636
|
```
|
|
654
637
|
|
|
655
|
-
|
|
656
|
-
|
|
638
|
+
**Error Handling Patterns**:
|
|
657
639
|
```typescript
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
export async function generateSalesReport(month: string) {
|
|
668
|
-
// Get secrets for external API
|
|
669
|
-
const apiKey = await ketrics.Secret.get('analytics-api-key');
|
|
670
|
-
|
|
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]
|
|
676
|
-
);
|
|
677
|
-
await db.close();
|
|
678
|
-
|
|
679
|
-
// Create Excel report
|
|
680
|
-
const workbook: IExcelWorkbook = ketrics.Excel.create();
|
|
681
|
-
const sheet = workbook.addWorksheet('Sales Report');
|
|
682
|
-
|
|
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]);
|
|
640
|
+
// Type guards for error handling
|
|
641
|
+
try {
|
|
642
|
+
const db = await ketrics.DatabaseConnection.connect('main-db');
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (ketrics.isDatabaseError(error)) {
|
|
645
|
+
// Specific database error
|
|
646
|
+
console.log(`Database error: ${error.message}`);
|
|
686
647
|
}
|
|
687
648
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
pdf.setTitle(`Sales Report - ${month}`);
|
|
693
|
-
pdf.setAuthor(ketrics.requestor.name);
|
|
694
|
-
|
|
695
|
-
const page = pdf.addPage('A4');
|
|
696
|
-
const font = await pdf.embedStandardFont('HelveticaBold');
|
|
697
|
-
|
|
698
|
-
await page.drawText(`Sales Report: ${month}`, {
|
|
699
|
-
x: 50, y: 750, size: 24, font
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
await page.drawText(`Total Records: ${sales.rowCount}`, {
|
|
703
|
-
x: 50, y: 700, size: 14
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
const pdfBuffer = await pdf.toBuffer();
|
|
707
|
-
|
|
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);
|
|
712
|
-
|
|
713
|
-
// Generate download URLs
|
|
714
|
-
const excelUrl = await volume.generateDownloadUrl(`${month}/sales.xlsx`, {
|
|
715
|
-
expiresIn: 86400
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
ketrics.console.log('Report generated', {
|
|
719
|
-
month,
|
|
720
|
-
recordCount: sales.rowCount
|
|
721
|
-
});
|
|
649
|
+
if (ketrics.isDatabaseErrorType(error, 'DatabaseNotFoundError')) {
|
|
650
|
+
// Exact error type check
|
|
651
|
+
console.log(`Database not found: ${error.databaseCode}`);
|
|
652
|
+
}
|
|
722
653
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
};
|
|
654
|
+
if (error instanceof ketrics.DatabaseAccessDeniedError) {
|
|
655
|
+
// Direct instanceof check
|
|
656
|
+
console.log('Application lacks database access grant');
|
|
657
|
+
}
|
|
728
658
|
}
|
|
729
659
|
```
|
|
730
660
|
|
|
731
|
-
### Testing
|
|
732
|
-
|
|
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`
|
|
737
|
-
|
|
738
|
-
The actual runtime behavior is tested in the Data Plane API where the SDK is implemented.
|
|
739
|
-
|
|
740
|
-
```bash
|
|
741
|
-
# Verify types compile correctly
|
|
742
|
-
npm run build
|
|
743
|
-
|
|
744
|
-
# Type checking is performed as part of build
|
|
745
|
-
# No separate test command needed for type-only package
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
### Publishing
|
|
661
|
+
### Testing Approach
|
|
749
662
|
|
|
663
|
+
**Unit Testing Tenant Code**:
|
|
750
664
|
```bash
|
|
751
|
-
#
|
|
752
|
-
npm
|
|
665
|
+
# Tenant applications test their handlers without running in Ketrics platform
|
|
666
|
+
npm install --save-dev jest @types/jest ts-jest
|
|
753
667
|
|
|
754
|
-
#
|
|
755
|
-
|
|
668
|
+
# Mock the global ketrics object for tests
|
|
669
|
+
jest.mock('@ketrics/sdk-backend', () => ({
|
|
670
|
+
// Provide mock implementations
|
|
671
|
+
}));
|
|
756
672
|
```
|
|
757
673
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|