@kravc/dos 1.12.2 → 1.12.4
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 +1019 -3
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
package/README.md
CHANGED
|
@@ -1,5 +1,1021 @@
|
|
|
1
1
|
# @kravc/dos
|
|
2
2
|
|
|
3
|
-
**DOS** (`D`document `O`peration `S`ervice) — convention-based, easy-to-use
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
**DOS** (`D`document `O`peration `S`ervice) — convention-based, easy-to-use library for building API-driven serverless services. Inspired by **Ruby on Rails**.
|
|
4
|
+
|
|
5
|
+
## Content
|
|
6
|
+
|
|
7
|
+
- [Usage](#usage)
|
|
8
|
+
- [1. Define a Document](#1-define-a-document)
|
|
9
|
+
- [2. Create a Schema](#2-create-a-schema)
|
|
10
|
+
- [3. Create Operations](#3-create-operations)
|
|
11
|
+
- [4. Initialize the Service](#4-initialize-the-service)
|
|
12
|
+
- [5. Making Requests](#5-making-requests)
|
|
13
|
+
- [6. Accessing the OpenAPI Specification](#6-accessing-the-openapi-specification)
|
|
14
|
+
- [Document](#document)
|
|
15
|
+
- [Component](#component)
|
|
16
|
+
- [Schema](#schema)
|
|
17
|
+
- [Attributes](#attributes)
|
|
18
|
+
- [Default Attributes](#default-attributes)
|
|
19
|
+
- [Methods](#methods)
|
|
20
|
+
- [Static Methods (Class-level operations)](#static-methods-class-level-operations)
|
|
21
|
+
- [Instance Methods](#instance-methods)
|
|
22
|
+
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
23
|
+
- [Storage](#storage)
|
|
24
|
+
- [Operation](#operation)
|
|
25
|
+
- [Base Operations](#base-operations)
|
|
26
|
+
- [Query Schema](#query-schema)
|
|
27
|
+
- [Mutation Schema](#mutation-schema)
|
|
28
|
+
- [Output Schema](#output-schema)
|
|
29
|
+
- [Before, Action, After](#before-action-after)
|
|
30
|
+
- [Errors](#errors)
|
|
31
|
+
- [Security](#security)
|
|
32
|
+
- [Default Pagination Interface](#default-pagination-interface)
|
|
33
|
+
- [Default Update Interface](#default-update-interface)
|
|
34
|
+
- [Activities](#activities)
|
|
35
|
+
- [Service](#service)
|
|
36
|
+
- [Specification](#specification)
|
|
37
|
+
- [Parameters Validation](#parameters-validation)
|
|
38
|
+
- [Execution Context](#execution-context)
|
|
39
|
+
- [Identity](#identity)
|
|
40
|
+
- [Output Validation](#output-validation)
|
|
41
|
+
- [Errors](#errors-1)
|
|
42
|
+
- [HTTP](#http)
|
|
43
|
+
- [Kafka](#kafka)
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
This section provides a complete example of building an API service with DOS. We'll create a Profile service with full CRUD operations.
|
|
48
|
+
|
|
49
|
+
### 1. Define a Document
|
|
50
|
+
|
|
51
|
+
First, create a Document class that represents your data model:
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
// Profile.js
|
|
55
|
+
const { Document } = require('@kravc/dos')
|
|
56
|
+
|
|
57
|
+
class Profile extends Document {}
|
|
58
|
+
|
|
59
|
+
module.exports = Profile
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. Create a Schema
|
|
63
|
+
|
|
64
|
+
Define the schema for your document (typically in a YAML file):
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# Profile.yaml
|
|
68
|
+
id:
|
|
69
|
+
required: true
|
|
70
|
+
|
|
71
|
+
name:
|
|
72
|
+
type: string
|
|
73
|
+
required: true
|
|
74
|
+
|
|
75
|
+
email:
|
|
76
|
+
type: string
|
|
77
|
+
format: email
|
|
78
|
+
required: true
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Create Operations
|
|
82
|
+
|
|
83
|
+
Define operations for each CRUD action:
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// CreateProfile.js
|
|
87
|
+
const { Create } = require('@kravc/dos')
|
|
88
|
+
const Profile = require('./Profile')
|
|
89
|
+
const JwtAuthorization = require('@kravc/dos/security/JwtAuthorization')
|
|
90
|
+
|
|
91
|
+
class CreateProfile extends Create(Profile) {
|
|
92
|
+
static get tags() {
|
|
93
|
+
return ['Profiles']
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static get security() {
|
|
97
|
+
return [
|
|
98
|
+
JwtAuthorization.createRequirement({
|
|
99
|
+
publicKey: process.env.PUBLIC_KEY,
|
|
100
|
+
algorithm: 'RS256'
|
|
101
|
+
})
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = CreateProfile
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// ReadProfile.js
|
|
111
|
+
const { Read } = require('@kravc/dos')
|
|
112
|
+
const Profile = require('./Profile')
|
|
113
|
+
|
|
114
|
+
class ReadProfile extends Read(Profile) {
|
|
115
|
+
static get query() {
|
|
116
|
+
return {
|
|
117
|
+
id: {
|
|
118
|
+
description: 'Profile ID',
|
|
119
|
+
required: true,
|
|
120
|
+
example: 'Profile_01ARZ3NDEKTSV4RRFFQ69G5FAV'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = ReadProfile
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
// UpdateProfile.js
|
|
131
|
+
const { Update } = require('@kravc/dos')
|
|
132
|
+
const Profile = require('./Profile')
|
|
133
|
+
|
|
134
|
+
class UpdateProfile extends Update(Profile) {}
|
|
135
|
+
|
|
136
|
+
module.exports = UpdateProfile
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// DeleteProfile.js
|
|
141
|
+
const { Delete } = require('@kravc/dos')
|
|
142
|
+
const Profile = require('./Profile')
|
|
143
|
+
|
|
144
|
+
class DeleteProfile extends Delete(Profile) {}
|
|
145
|
+
|
|
146
|
+
module.exports = DeleteProfile
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
// IndexProfiles.js
|
|
151
|
+
const { Index } = require('@kravc/dos')
|
|
152
|
+
const Profile = require('./Profile')
|
|
153
|
+
|
|
154
|
+
class IndexProfiles extends Index(Profile) {}
|
|
155
|
+
|
|
156
|
+
module.exports = IndexProfiles
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 4. Initialize the Service
|
|
160
|
+
|
|
161
|
+
Create a Service instance that brings together all your operations:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// index.js
|
|
165
|
+
const { Service, handler } = require('@kravc/dos')
|
|
166
|
+
const Profile = require('./Profile')
|
|
167
|
+
const CreateProfile = require('./CreateProfile')
|
|
168
|
+
const ReadProfile = require('./ReadProfile')
|
|
169
|
+
const UpdateProfile = require('./UpdateProfile')
|
|
170
|
+
const DeleteProfile = require('./DeleteProfile')
|
|
171
|
+
const IndexProfiles = require('./IndexProfiles')
|
|
172
|
+
|
|
173
|
+
const modules = [
|
|
174
|
+
Profile,
|
|
175
|
+
CreateProfile,
|
|
176
|
+
ReadProfile,
|
|
177
|
+
UpdateProfile,
|
|
178
|
+
DeleteProfile,
|
|
179
|
+
IndexProfiles
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
const service = new Service(modules, {
|
|
183
|
+
url: 'https://api.example.com/',
|
|
184
|
+
path: `${process.cwd()}/src`
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Export handler for serverless platforms
|
|
188
|
+
exports.handler = handler(service)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 5. Making Requests
|
|
192
|
+
|
|
193
|
+
Once deployed, you can make HTTP requests to your service:
|
|
194
|
+
|
|
195
|
+
**Create a Profile:**
|
|
196
|
+
```bash
|
|
197
|
+
POST /CreateProfile
|
|
198
|
+
Content-Type: application/json
|
|
199
|
+
Authorization: Bearer <token>
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
"mutation": {
|
|
203
|
+
"name": "John Doe",
|
|
204
|
+
"email": "john@example.com"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Read a Profile:**
|
|
210
|
+
```bash
|
|
211
|
+
GET /ReadProfile?id=Profile_01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
212
|
+
Authorization: Bearer <token>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Update a Profile:**
|
|
216
|
+
```bash
|
|
217
|
+
PATCH /UpdateProfile?id=Profile_01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
218
|
+
Content-Type: application/json
|
|
219
|
+
Authorization: Bearer <token>
|
|
220
|
+
|
|
221
|
+
{
|
|
222
|
+
"mutation": {
|
|
223
|
+
"name": "Jane Doe"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Delete a Profile:**
|
|
229
|
+
```bash
|
|
230
|
+
DELETE /DeleteProfile?id=Profile_01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
231
|
+
Authorization: Bearer <token>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**List Profiles:**
|
|
235
|
+
```bash
|
|
236
|
+
GET /IndexProfiles?limit=20&sort=desc
|
|
237
|
+
Authorization: Bearer <token>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 6. Accessing the OpenAPI Specification
|
|
241
|
+
|
|
242
|
+
The service automatically generates an OpenAPI 2.0 specification:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
GET /Spec
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This returns the complete API specification that can be used with Swagger UI or other OpenAPI tools.
|
|
249
|
+
|
|
250
|
+
## Document
|
|
251
|
+
|
|
252
|
+
Document is the core class for modeling data entities. It extends Component and provides CRUD operations with automatic validation, timestamps, and identity tracking.
|
|
253
|
+
|
|
254
|
+
### Component
|
|
255
|
+
|
|
256
|
+
Every Document is a Component, which provides:
|
|
257
|
+
- **Component ID**: The class name (via `Component.id`) used to identify the component type
|
|
258
|
+
- **Context**: Execution context passed to each document instance, containing validator, identity, and other runtime information
|
|
259
|
+
- **Schema Validation**: Built-in validation using the component's schema via `validate()`
|
|
260
|
+
- **JSON Serialization**: Automatic conversion via `toJSON()` method that returns the document attributes
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
class Profile extends Document {}
|
|
264
|
+
|
|
265
|
+
// Component ID is automatically set to "Profile"
|
|
266
|
+
Profile.id // => "Profile"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Schema
|
|
270
|
+
|
|
271
|
+
Documents use schemas for validation and normalization. The schema is set as a class property and is automatically extended with default attributes.
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
class Profile extends Document {}
|
|
275
|
+
|
|
276
|
+
// Set schema (typically from a YAML file)
|
|
277
|
+
Profile.schema = loadSchema('Profile.yaml')
|
|
278
|
+
|
|
279
|
+
// Schema is extended with default attributes (id, createdAt, updatedAt, etc.)
|
|
280
|
+
Profile.schema // Extended schema (includes defaults)
|
|
281
|
+
Profile.bodySchema // Original schema (without defaults)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
When a schema is set on a Document, it's automatically extended with default attributes to create the full schema, while the original body schema is preserved separately.
|
|
285
|
+
|
|
286
|
+
### Attributes
|
|
287
|
+
|
|
288
|
+
Attributes are the data fields of a document instance. They are stored in the `_attributes` property and accessed via the `attributes` getter.
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
const profile = new Profile(context, {
|
|
292
|
+
id: 'profile_abc123',
|
|
293
|
+
name: 'John Doe',
|
|
294
|
+
email: 'john@example.com'
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
profile.attributes // => { id: 'profile_abc123', name: 'John Doe', email: 'john@example.com' }
|
|
298
|
+
profile.id // => 'profile_abc123'
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Attributes are validated against the document's schema when using the `validate()` method or during CRUD operations.
|
|
302
|
+
|
|
303
|
+
### Default Attributes
|
|
304
|
+
|
|
305
|
+
Documents automatically include default schema attributes that are added to every schema:
|
|
306
|
+
|
|
307
|
+
- **`id`** (required): Unique identifier for the document. Automatically generated using ULID format with prefix (e.g., `Profile_01ARZ3NDEKTSV4RRFFQ69G5FAV`)
|
|
308
|
+
- **`createdAt`** (required): ISO 8601 timestamp when the document was created
|
|
309
|
+
- **`createdBy`**: ID of the user who created the document (from `context.identity.sub`)
|
|
310
|
+
- **`updatedAt`**: ISO 8601 timestamp when the document was last updated
|
|
311
|
+
- **`updatedBy`**: ID of the user who last updated the document (from `context.identity.sub`)
|
|
312
|
+
|
|
313
|
+
These attributes are automatically managed during create and update operations and cannot be directly mutated through mutation parameters.
|
|
314
|
+
|
|
315
|
+
### Methods
|
|
316
|
+
|
|
317
|
+
#### Static Methods (Class-level operations)
|
|
318
|
+
|
|
319
|
+
- **`create(context, query, mutation)`**: Create a new document. Automatically adds `id`, `createdAt`, and `createdBy`. Supports `beforeCreate` and `afterCreate` hooks.
|
|
320
|
+
- **`read(context, query, options)`**: Read a single document by query. Throws `DocumentNotFoundError` if not found.
|
|
321
|
+
- **`index(context, query, options)`**: List documents matching the query. Returns `{ objects, count, ...rest }`. Supports partition filtering.
|
|
322
|
+
- **`indexAll(context, query, options)`**: List all documents matching the query (alias for `index`).
|
|
323
|
+
- **`update(context, query, mutation, originalDocument)`**: Update a document. Automatically adds `updatedAt` and `updatedBy`. Supports `beforeUpdate` and `afterUpdate` hooks. Preserves `id`, `createdAt`, and `createdBy`.
|
|
324
|
+
- **`delete(context, query)`**: Delete a document. Supports `beforeDelete` and `afterDelete` hooks.
|
|
325
|
+
- **`createId(attributes)`**: Generate a unique ID for the document (format: `{prefix}_{ulid}`)
|
|
326
|
+
- **`reset()`**: Reset/clear the document storage (testing utility)
|
|
327
|
+
|
|
328
|
+
#### Instance Methods
|
|
329
|
+
|
|
330
|
+
- **`update(mutation, shouldMutate)`**: Update this document instance. If `shouldMutate` is `true`, updates the instance in-place.
|
|
331
|
+
- **`delete()`**: Delete this document instance.
|
|
332
|
+
- **`hasAttributeChanged(attributePath)`**: Check if an attribute changed during update (requires `originalDocument`).
|
|
333
|
+
- **`validate()`**: Validate the document attributes against its schema.
|
|
334
|
+
- **`toJSON()`**: Serialize the document to plain JSON (returns attributes).
|
|
335
|
+
|
|
336
|
+
#### Lifecycle Hooks
|
|
337
|
+
|
|
338
|
+
- **`beforeCreate(context, query, mutation)`**: Called before document creation
|
|
339
|
+
- **`afterCreate(context, query, mutation, document)`**: Called after document creation
|
|
340
|
+
- **`beforeUpdate(context, query, mutation)`**: Called before document update
|
|
341
|
+
- **`afterUpdate(context, query, mutation, document)`**: Called after document update
|
|
342
|
+
- **`beforeDelete(context, query, originalDocument)`**: Called before document deletion
|
|
343
|
+
- **`afterDelete(context, query, originalDocument)`**: Called after document deletion
|
|
344
|
+
|
|
345
|
+
### Storage
|
|
346
|
+
|
|
347
|
+
Documents use an in-memory storage system by default. The storage is implemented as a class-level `STORE` object indexed by document class name and document ID.
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
// Storage structure
|
|
351
|
+
STORE = {
|
|
352
|
+
Profile: {
|
|
353
|
+
'profile_abc123': { /* document attributes */ },
|
|
354
|
+
'profile_def456': { /* document attributes */ }
|
|
355
|
+
},
|
|
356
|
+
User: {
|
|
357
|
+
'user_xyz789': { /* document attributes */ }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
To use a custom storage backend (e.g., database, DynamoDB, etc.), override the private static methods:
|
|
363
|
+
- **`_create(attributes)`**: Implement custom creation logic
|
|
364
|
+
- **`_read(query, options)`**: Implement custom read logic
|
|
365
|
+
- **`_index(query, options)`**: Implement custom indexing logic
|
|
366
|
+
- **`_update(query, mutation)`**: Implement custom update logic
|
|
367
|
+
- **`_delete(context, query)`**: Implement custom deletion logic
|
|
368
|
+
|
|
369
|
+
The public methods (`create`, `read`, `index`, `update`, `delete`) handle validation, timestamps, partitioning, and lifecycle hooks, then delegate to these private storage methods.
|
|
370
|
+
|
|
371
|
+
## Operation
|
|
372
|
+
|
|
373
|
+
Operations define the API endpoints for interacting with Documents. They encapsulate the business logic, validation, security, and lifecycle hooks for each operation type.
|
|
374
|
+
|
|
375
|
+
### Base Operations
|
|
376
|
+
|
|
377
|
+
Operations are created using factory functions that take a Component class and optional component action name. The library provides five base operation types:
|
|
378
|
+
|
|
379
|
+
- **`Create(Component, componentAction = 'create')`**: Creates a new document instance
|
|
380
|
+
- **`Read(Component, componentAction = 'read')`**: Retrieves a single document by ID
|
|
381
|
+
- **`Update(Component, componentAction = 'update')`**: Updates an existing document
|
|
382
|
+
- **`Delete(Component, componentAction = 'delete')`**: Deletes a document
|
|
383
|
+
- **`Index(Component, componentAction = 'index')`**: Lists documents with pagination support
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
const { Create, Read, Update, Delete, Index } = require('@kravc/dos')
|
|
387
|
+
const Profile = require('./Profile')
|
|
388
|
+
|
|
389
|
+
// Create operation classes
|
|
390
|
+
class CreateProfile extends Create(Profile) {}
|
|
391
|
+
class ReadProfile extends Read(Profile) {}
|
|
392
|
+
class UpdateProfile extends Update(Profile) {}
|
|
393
|
+
class DeleteProfile extends Delete(Profile) {}
|
|
394
|
+
class IndexProfile extends Index(Profile) {}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Each operation automatically derives metadata (ID, summary, tags, schemas) from the Component class.
|
|
398
|
+
|
|
399
|
+
### Query Schema
|
|
400
|
+
|
|
401
|
+
The query schema defines the input parameters used to identify or filter documents. It's defined via the static `query` getter and becomes part of the operation's `inputSchema`.
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
class ReadProfile extends Read(Profile) {
|
|
405
|
+
static get query() {
|
|
406
|
+
return {
|
|
407
|
+
id: {
|
|
408
|
+
description: 'Profile ID',
|
|
409
|
+
required: true,
|
|
410
|
+
example: 'PRO_1'
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Default Query Schemas:**
|
|
418
|
+
- **Read/Update/Delete**: Automatically includes `id` (required) based on component name
|
|
419
|
+
- **Index**: Automatically includes pagination parameters (`limit`, `sort`, `exclusiveStartKey`)
|
|
420
|
+
|
|
421
|
+
The query schema is merged with the mutation schema (if present) to create the complete `inputSchema` for the operation.
|
|
422
|
+
|
|
423
|
+
### Mutation Schema
|
|
424
|
+
|
|
425
|
+
The mutation schema defines the data structure for creating or updating documents. It's automatically derived from the Component's `bodySchema` (or `schema` if `bodySchema` is not available).
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
// Component defines bodySchema
|
|
429
|
+
Profile.bodySchema = loadSchema('Profile.yaml') // { name: {}, email: {} }
|
|
430
|
+
|
|
431
|
+
// CREATE operation: Uses cloned schema (all fields as-is)
|
|
432
|
+
CreateProfile.mutationSchema // => { name: {}, email: {} }
|
|
433
|
+
|
|
434
|
+
// UPDATE operation: Uses pure schema (all fields optional, removes defaults)
|
|
435
|
+
UpdateProfile.mutationSchema // => { name: {}, email: {} } (all optional)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Schema Transformation:**
|
|
439
|
+
- **CREATE**: Uses `bodySchema.clone()` - preserves all schema definitions
|
|
440
|
+
- **UPDATE**: Uses `bodySchema.pure()` - makes all fields optional and removes default values
|
|
441
|
+
|
|
442
|
+
The mutation schema is embedded in the input schema as a `mutation` property (required for CREATE/UPDATE operations).
|
|
443
|
+
|
|
444
|
+
### Output Schema
|
|
445
|
+
|
|
446
|
+
The output schema defines the structure of the operation's response. It's automatically derived from the Component's schema and wrapped in a `data` property.
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
449
|
+
// Output schema for operations with Component
|
|
450
|
+
{
|
|
451
|
+
data: {
|
|
452
|
+
$ref: 'Profile', // References Component.schema.id
|
|
453
|
+
required: true
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Special Cases:**
|
|
459
|
+
- **Delete**: Returns `null` output schema (204 No Content response)
|
|
460
|
+
- **Index**: Returns paginated output with `data` (array) and `pageInfo` object
|
|
461
|
+
|
|
462
|
+
The output schema is validated after the action executes to ensure the response conforms to the specification.
|
|
463
|
+
|
|
464
|
+
### Before, Action, After
|
|
465
|
+
|
|
466
|
+
Operations support three lifecycle hooks that are executed in sequence during the `exec()` method:
|
|
467
|
+
|
|
468
|
+
1. **`before(parameters)`**: Called before the action. Can modify parameters by returning a new parameter object, or return `undefined` to keep original parameters.
|
|
469
|
+
|
|
470
|
+
2. **`action(parameters)`**: The main operation logic. Receives normalized parameters and calls the Component's action method (e.g., `Component.create()`, `Component.read()`). Returns `{ data }` object.
|
|
471
|
+
|
|
472
|
+
3. **`after(parameters, data)`**: Called after the action. Receives parameters and the data result. Can modify the result by returning a new value, or return `undefined` to keep original result.
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
class CreateProfile extends Create(Profile) {
|
|
476
|
+
async before(parameters) {
|
|
477
|
+
// Pre-process parameters
|
|
478
|
+
const { mutation } = parameters
|
|
479
|
+
mutation.normalizedField = normalize(mutation.field)
|
|
480
|
+
|
|
481
|
+
return parameters // Return modified parameters, or undefined to keep original
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async action(parameters) {
|
|
485
|
+
// Default action calls Component.create(context, query, mutation)
|
|
486
|
+
// Override if custom logic needed
|
|
487
|
+
return super.action(parameters)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async after(parameters, data) {
|
|
491
|
+
// Post-process result
|
|
492
|
+
data.enriched = true
|
|
493
|
+
|
|
494
|
+
return data // Return modified data, or undefined to keep original
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The execution flow is: `before()` → `action()` → `after()`, with each hook able to modify the data passed to the next stage.
|
|
500
|
+
|
|
501
|
+
### Errors
|
|
502
|
+
|
|
503
|
+
Operations automatically collect errors from multiple sources:
|
|
504
|
+
|
|
505
|
+
1. **Security Errors**: Errors from all security requirements (UnauthorizedError, AccessDeniedError, etc.)
|
|
506
|
+
2. **Input Validation Errors**: `InvalidInputError` (400) and `InvalidParametersError` (400) if `inputSchema` is defined
|
|
507
|
+
3. **Output Validation Errors**: `InvalidOutputError` (500) if `outputSchema` is defined
|
|
508
|
+
4. **Operation-Specific Errors**: Each operation type adds component-specific errors:
|
|
509
|
+
- **Create**: `DocumentExistsError` (422)
|
|
510
|
+
- **Read/Update/Delete**: `DocumentNotFoundError` (404)
|
|
511
|
+
5. **Default Error**: `UnprocessibleConditionError` (422) - always included
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
class CreateProfile extends Create(Profile) {
|
|
515
|
+
static get errors() {
|
|
516
|
+
return {
|
|
517
|
+
...super.errors, // Includes base errors
|
|
518
|
+
// Custom errors can be added here
|
|
519
|
+
CustomError: {
|
|
520
|
+
statusCode: 400,
|
|
521
|
+
description: 'Custom error description'
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Errors are mapped to HTTP status codes and included in the OpenAPI specification. When an operation throws an error, the Service maps it to the appropriate status code using the error's `code` property.
|
|
529
|
+
|
|
530
|
+
### Security
|
|
531
|
+
|
|
532
|
+
Security is defined via the static `security` getter, which returns an array of security requirement objects. Each requirement object represents an OR condition, and within each object, properties represent AND conditions.
|
|
533
|
+
|
|
534
|
+
```javascript
|
|
535
|
+
class CreateProfile extends Create(Profile) {
|
|
536
|
+
static get security() {
|
|
537
|
+
const algorithm = 'RS256'
|
|
538
|
+
|
|
539
|
+
const accessVerificationMethod = (context, { group }) => {
|
|
540
|
+
const isAccessGranted = [ 'Administrators' ].includes(group)
|
|
541
|
+
return [ isAccessGranted, 'Access denied' ]
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const tokenVerificationMethod = (...args) => verifyToken(...args)
|
|
545
|
+
|
|
546
|
+
return [
|
|
547
|
+
// OR requirement 1: JWT with Admin access
|
|
548
|
+
JwtAuthorization.createRequirement({
|
|
549
|
+
publicKey,
|
|
550
|
+
algorithm,
|
|
551
|
+
tokenVerificationMethod,
|
|
552
|
+
accessVerificationMethod
|
|
553
|
+
}),
|
|
554
|
+
// OR requirement 2: System authorization
|
|
555
|
+
SystemAuthorization.createRequirement({
|
|
556
|
+
accessVerificationMethod: verifySystemAccess
|
|
557
|
+
})
|
|
558
|
+
]
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Security Evaluation:**
|
|
564
|
+
- Operations with empty `security` array (`[]`) are public (no authorization required)
|
|
565
|
+
- Security requirements are evaluated as: `(req1 AND req2) OR (req3 AND req4)`
|
|
566
|
+
- First matching requirement grants access
|
|
567
|
+
- If no requirements match, an `UnauthorizedError` or `AccessDeniedError` is thrown
|
|
568
|
+
|
|
569
|
+
Security classes must implement a `verify(context)` method that returns `{ isAuthorized, error, ...rest }`. The `rest` properties are merged into the context as `context.identity`.
|
|
570
|
+
|
|
571
|
+
### Default Pagination Interface
|
|
572
|
+
|
|
573
|
+
The `Index` operation provides built-in pagination support with the following interface:
|
|
574
|
+
|
|
575
|
+
**Query Parameters:**
|
|
576
|
+
- **`limit`** (integer, default: 20): Maximum number of items to return
|
|
577
|
+
- **`sort`** (enum: 'asc' | 'desc', default: 'desc'): Sort direction
|
|
578
|
+
- **`exclusiveStartKey`** (string, optional): Pagination token to start from
|
|
579
|
+
|
|
580
|
+
**Output Structure:**
|
|
581
|
+
```javascript
|
|
582
|
+
{
|
|
583
|
+
data: [ /* array of documents */ ],
|
|
584
|
+
pageInfo: {
|
|
585
|
+
count: 10, // Number of items in current page
|
|
586
|
+
limit: 20, // Limit used
|
|
587
|
+
sort: 'desc', // Sort direction used
|
|
588
|
+
exclusiveStartKey: 'token1', // Start key used (if any)
|
|
589
|
+
lastEvaluatedKey: 'token2' // Token for next page (if more results exist)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**Customization:**
|
|
595
|
+
```javascript
|
|
596
|
+
class IndexProfile extends Index(Profile) {
|
|
597
|
+
static get defaultLimit() {
|
|
598
|
+
return 50 // Override default limit
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
static get defaultSort() {
|
|
602
|
+
return 'asc' // Override default sort
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
static get query() {
|
|
606
|
+
return {
|
|
607
|
+
...super.query, // Includes default pagination params
|
|
608
|
+
// Add custom query parameters
|
|
609
|
+
status: {
|
|
610
|
+
enum: [ 'active', 'inactive' ],
|
|
611
|
+
default: 'active'
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Pagination tokens (exclusiveStartKey/lastEvaluatedKey) are typically opaque strings that encode the position in the result set, allowing efficient cursor-based pagination.
|
|
619
|
+
|
|
620
|
+
### Default Update Interface
|
|
621
|
+
|
|
622
|
+
The Update operation uses a "pure" mutation schema that makes all fields optional and removes default values. This allows partial updates where only specified fields are modified.
|
|
623
|
+
|
|
624
|
+
```javascript
|
|
625
|
+
// Component bodySchema
|
|
626
|
+
Profile.bodySchema = {
|
|
627
|
+
name: { required: true, default: 'Unknown' },
|
|
628
|
+
email: { required: true },
|
|
629
|
+
age: { type: 'integer' }
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Update mutation schema (pure)
|
|
633
|
+
UpdateProfile.mutationSchema = {
|
|
634
|
+
name: {}, // Optional, no default
|
|
635
|
+
email: {}, // Optional
|
|
636
|
+
age: { type: 'integer' } // Optional
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**Update Behavior:**
|
|
641
|
+
- Only fields present in the mutation are updated
|
|
642
|
+
- Fields not included remain unchanged
|
|
643
|
+
- The `id`, `createdAt`, and `createdBy` fields are automatically omitted from mutations
|
|
644
|
+
- `updatedAt` and `updatedBy` are automatically added by the Document class
|
|
645
|
+
|
|
646
|
+
**Example:**
|
|
647
|
+
```javascript
|
|
648
|
+
// Update only the name field
|
|
649
|
+
await UpdateProfile.exec({ id: 'profile_1', mutation: { name: 'New Name' } })
|
|
650
|
+
|
|
651
|
+
// Email and age remain unchanged
|
|
652
|
+
// updatedAt and updatedBy are automatically set
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
This interface follows the PATCH semantics where partial updates are the default behavior.
|
|
656
|
+
|
|
657
|
+
### Activities
|
|
658
|
+
|
|
659
|
+
Activities represent a potential extension point for operation lifecycle tracking and logging. While not currently implemented in the core library, the operation's lifecycle hooks (`before`, `action`, `after`) provide the foundation for implementing activity tracking.
|
|
660
|
+
|
|
661
|
+
Potential use cases for activities:
|
|
662
|
+
- **Audit Logging**: Track all operations performed with context (who, what, when, parameters)
|
|
663
|
+
- **Activity Feed**: Generate user-visible activity streams
|
|
664
|
+
- **Analytics**: Collect metrics on operation usage and performance
|
|
665
|
+
- **Notifications**: Trigger side effects based on operation completion
|
|
666
|
+
|
|
667
|
+
Activities could be implemented by:
|
|
668
|
+
- Extending the `after()` hook to record activities
|
|
669
|
+
- Adding an `activities` static getter to define which operations should be tracked
|
|
670
|
+
- Integrating with external services (logging, analytics, event streaming)
|
|
671
|
+
|
|
672
|
+
This is a placeholder for future functionality that could enhance observability and auditing capabilities.
|
|
673
|
+
|
|
674
|
+
## Service
|
|
675
|
+
|
|
676
|
+
Service is the central orchestrator that brings together Documents, Operations, and Schemas to create a complete API service. It handles request routing, validation, authorization, execution, and response generation.
|
|
677
|
+
|
|
678
|
+
### Specification
|
|
679
|
+
|
|
680
|
+
Service automatically generates an OpenAPI 2.0 (Swagger) specification from all registered operations and components. The specification is created during Service initialization and includes:
|
|
681
|
+
|
|
682
|
+
- **API Metadata**: Title and version from `package.json`
|
|
683
|
+
- **Base URL**: Derived from the `url` option (default: `http://localhost:3000/`)
|
|
684
|
+
- **Paths**: Each operation becomes a path (`/{OperationId}`) with its HTTP method
|
|
685
|
+
- **Schemas**: All component schemas, operation input/output schemas, and error schemas
|
|
686
|
+
- **Security Definitions**: Security schemes from operation requirements
|
|
687
|
+
- **Tags**: Automatically extracted from operation tags
|
|
688
|
+
|
|
689
|
+
```javascript
|
|
690
|
+
const { Service } = require('@kravc/dos')
|
|
691
|
+
|
|
692
|
+
const modules = [
|
|
693
|
+
Profile,
|
|
694
|
+
CreateProfile,
|
|
695
|
+
ReadProfile,
|
|
696
|
+
UpdateProfile,
|
|
697
|
+
DeleteProfile,
|
|
698
|
+
IndexProfile
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
const service = new Service(modules, {
|
|
702
|
+
url: 'https://api.example.com/',
|
|
703
|
+
path: `${ROOT_PATH}/src`
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
// Access the generated specification
|
|
707
|
+
service.spec // OpenAPI 2.0 JSON specification
|
|
708
|
+
service.baseUrl // 'https://api.example.com/'
|
|
709
|
+
service.basePath // '/'
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
**Specification Endpoints:**
|
|
713
|
+
- **GET `/`**: Returns Swagger UI HTML (development mode) or 'healthy' (production)
|
|
714
|
+
- **GET `/Spec`**: Returns the full OpenAPI specification JSON (development) or minimal info (production)
|
|
715
|
+
- **GET `/Schemas.yaml`, `/Operations.yaml`, etc.**: Returns composer specification files (development only)
|
|
716
|
+
|
|
717
|
+
The specification is validated against the OpenAPI 2.0 schema during initialization to ensure correctness.
|
|
718
|
+
|
|
719
|
+
### Parameters Validation
|
|
720
|
+
|
|
721
|
+
All operation parameters are validated against the operation's `inputSchema` before execution. The validation process:
|
|
722
|
+
|
|
723
|
+
1. **Extracts Input**: Combines `context.query` and `context.mutation` into a single input object
|
|
724
|
+
2. **Normalizes Values**: Converts query string values, decodes URLs, parses JSON arrays in query strings
|
|
725
|
+
3. **Validates Schema**: Uses the operation's `inputSchema` to validate structure and types
|
|
726
|
+
4. **Handles UPDATE Special Case**: For UPDATE operations, empty values are nullified (to support partial updates)
|
|
727
|
+
|
|
728
|
+
```javascript
|
|
729
|
+
// Inside Service.process()
|
|
730
|
+
const parameters = this._getParameters(Operation.inputSchema, context, isUpdate)
|
|
731
|
+
|
|
732
|
+
// Validation errors throw InvalidInputError (400) or InvalidParametersError (400)
|
|
733
|
+
try {
|
|
734
|
+
result = this._validator.validate(input, inputSchema.id, shouldNullifyEmptyValues)
|
|
735
|
+
} catch (validationError) {
|
|
736
|
+
throw new InvalidInputError(validationError, context)
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Query Parameter Handling:**
|
|
741
|
+
- Query string parameters are automatically decoded
|
|
742
|
+
- JSON arrays in query strings are parsed: `?ids=["id1","id2"]` → `['id1', 'id2']`
|
|
743
|
+
- Body (mutation) is parsed as JSON if it's a string
|
|
744
|
+
|
|
745
|
+
**Validation Errors:**
|
|
746
|
+
- **InvalidInputError** (400): Schema validation failed (structure, types, required fields)
|
|
747
|
+
- **InvalidParametersError** (400): Syntax is correct but values are invalid (e.g., enum mismatch)
|
|
748
|
+
|
|
749
|
+
### Execution Context
|
|
750
|
+
|
|
751
|
+
The execution context is created from the incoming request and contains all information needed for operation execution. The context is built by `createContext()` helper:
|
|
752
|
+
|
|
753
|
+
```javascript
|
|
754
|
+
{
|
|
755
|
+
// Request identification
|
|
756
|
+
requestId: string, // UUID generated or from requestContext
|
|
757
|
+
operationId: string, // Operation ID from path/method mapping
|
|
758
|
+
httpMethod: string, // Lowercase HTTP method (get, post, patch, delete)
|
|
759
|
+
httpPath: string, // Normalized path relative to basePath
|
|
760
|
+
requestReceivedAt: string, // ISO 8601 timestamp
|
|
761
|
+
|
|
762
|
+
// Request data
|
|
763
|
+
headers: object, // Normalized (lowercase keys) request headers
|
|
764
|
+
query: object, // Parsed query string parameters
|
|
765
|
+
mutation: object, // Parsed request body (for POST/PATCH)
|
|
766
|
+
bodyJson: string, // Raw JSON body (if provided)
|
|
767
|
+
|
|
768
|
+
// Service infrastructure
|
|
769
|
+
baseUrl: string, // Service base URL
|
|
770
|
+
validator: Validator, // Schema validator instance
|
|
771
|
+
logger: object, // Logger instance (from extraContext, default: console)
|
|
772
|
+
|
|
773
|
+
// Security
|
|
774
|
+
identity: object, // Set by authorize() - contains authenticated user info
|
|
775
|
+
|
|
776
|
+
// Custom context
|
|
777
|
+
...extraContext // Additional context passed to handler()
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
**Context Creation Flow:**
|
|
782
|
+
1. Extract or determine `operationId` from request path and HTTP method
|
|
783
|
+
2. Parse and normalize headers (all lowercase keys)
|
|
784
|
+
3. Extract query parameters from URL or `queryStringParameters`
|
|
785
|
+
4. Parse request body as JSON (if present)
|
|
786
|
+
5. Merge with `extraContext` provided to handler
|
|
787
|
+
|
|
788
|
+
The context is passed to all operations and is available throughout the execution lifecycle.
|
|
789
|
+
|
|
790
|
+
### Identity
|
|
791
|
+
|
|
792
|
+
Identity is established through the authorization process and represents the authenticated entity making the request. The `identity` object is added to the context after successful authorization:
|
|
793
|
+
|
|
794
|
+
```javascript
|
|
795
|
+
// Inside Service.process()
|
|
796
|
+
context.identity = await authorize(Operation, context)
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Identity Structure:**
|
|
800
|
+
The identity object is built from the security requirement's `verify()` method return value. All properties except `isAuthorized` and `error` are merged into the context as `identity`:
|
|
801
|
+
|
|
802
|
+
```javascript
|
|
803
|
+
// Example: JWT Authorization
|
|
804
|
+
const { isAuthorized, error, sub, group, permissions } = await security.verify(context)
|
|
805
|
+
|
|
806
|
+
// If authorized:
|
|
807
|
+
context.identity = {
|
|
808
|
+
sub: 'user_123',
|
|
809
|
+
group: 'Administrators',
|
|
810
|
+
permissions: ['read', 'write']
|
|
811
|
+
}
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
**Identity Usage:**
|
|
815
|
+
- Document operations automatically use `identity.sub` for `createdBy` and `updatedBy`
|
|
816
|
+
- Operations can access `context.identity` to make authorization decisions
|
|
817
|
+
- Custom authorization logic can read identity properties
|
|
818
|
+
|
|
819
|
+
**No Identity (Public Operations):**
|
|
820
|
+
Operations with empty `security` array (`[]`) skip authorization and `context.identity` remains undefined. Document operations default to `'SYSTEM'` for identity-related fields.
|
|
821
|
+
|
|
822
|
+
### Output Validation
|
|
823
|
+
|
|
824
|
+
Operation outputs are validated against the operation's `outputSchema` after successful execution. This ensures the response conforms to the specification:
|
|
825
|
+
|
|
826
|
+
```javascript
|
|
827
|
+
// Inside Service.process()
|
|
828
|
+
response.output = this._getOutput(Operation.outputSchema, response.result)
|
|
829
|
+
|
|
830
|
+
// Validation throws InvalidOutputError (500) if output doesn't match schema
|
|
831
|
+
try {
|
|
832
|
+
output = this._validator.validate(object, outputSchema.id, false, true)
|
|
833
|
+
} catch (validationError) {
|
|
834
|
+
throw new InvalidOutputError(object, validationError)
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**Validation Behavior:**
|
|
839
|
+
- Validates the entire output structure against the `outputSchema`
|
|
840
|
+
- Throws `InvalidOutputError` (500) if validation fails (indicates a bug in the operation)
|
|
841
|
+
- Operations without `outputSchema` (e.g., Delete) return `null` output (204 No Content)
|
|
842
|
+
|
|
843
|
+
**Output Structure:**
|
|
844
|
+
Operations should return `{ data, headers, multiValueHeaders }` from their `exec()` method:
|
|
845
|
+
- **data**: The main response data (validated against `outputSchema`)
|
|
846
|
+
- **headers**: Standard HTTP headers object
|
|
847
|
+
- **multiValueHeaders**: Multi-value headers (for some serverless platforms)
|
|
848
|
+
|
|
849
|
+
### Errors
|
|
850
|
+
|
|
851
|
+
Service provides comprehensive error handling that maps errors to appropriate HTTP status codes and formats error responses consistently:
|
|
852
|
+
|
|
853
|
+
**Error Processing Flow:**
|
|
854
|
+
1. Errors thrown during execution are caught by `Service.process()`
|
|
855
|
+
2. Error's `code` property is used to look up status code in `Operation.errors`
|
|
856
|
+
3. If no matching error definition, status code 500 is used
|
|
857
|
+
4. Error is wrapped in `OperationError` component for consistent formatting
|
|
858
|
+
5. `OperationError` is validated against its schema before returning
|
|
859
|
+
|
|
860
|
+
```javascript
|
|
861
|
+
catch (error) {
|
|
862
|
+
const { code } = error
|
|
863
|
+
const errorStatusCode = Operation
|
|
864
|
+
? get(Operation.errors, `${code}.statusCode`, 500)
|
|
865
|
+
: get(error, 'statusCode', 500)
|
|
866
|
+
|
|
867
|
+
response.output = new OperationError(context, errorStatusCode, error).validate()
|
|
868
|
+
response.statusCode = errorStatusCode
|
|
869
|
+
}
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
**Error Response Format:**
|
|
873
|
+
```javascript
|
|
874
|
+
{
|
|
875
|
+
error: {
|
|
876
|
+
code: string, // Error code (e.g., 'DocumentNotFoundError')
|
|
877
|
+
message: string, // Human-readable error message
|
|
878
|
+
statusCode: number, // HTTP status code
|
|
879
|
+
validationErrors?: object // Schema validation errors (if applicable)
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**Error Types:**
|
|
885
|
+
- **400**: `InvalidInputError`, `InvalidParametersError`
|
|
886
|
+
- **401**: `UnauthorizedError` (from security)
|
|
887
|
+
- **403**: `AccessDeniedError` (from security)
|
|
888
|
+
- **404**: `DocumentNotFoundError`, `OperationNotFoundError`
|
|
889
|
+
- **422**: `DocumentExistsError`, `UnprocessibleConditionError`
|
|
890
|
+
- **500**: `InvalidOutputError`, `OperationError` (unexpected errors)
|
|
891
|
+
|
|
892
|
+
**Error Logging:**
|
|
893
|
+
- 500 errors are automatically logged with full context (masked for secrets)
|
|
894
|
+
- Other errors are not logged (expected business logic errors)
|
|
895
|
+
- Context includes: `query`, `mutation`, `identity`, `requestId`, `operationId`, `requestReceivedAt`
|
|
896
|
+
|
|
897
|
+
### HTTP
|
|
898
|
+
|
|
899
|
+
Service is designed to work with HTTP-based serverless platforms (AWS Lambda, Azure Functions, Google Cloud Functions, etc.). The `handler()` function creates a request handler that processes HTTP requests:
|
|
900
|
+
|
|
901
|
+
```javascript
|
|
902
|
+
const { Service, handler } = require('@kravc/dos')
|
|
903
|
+
|
|
904
|
+
const service = new Service(modules, { url: 'https://api.example.com/' })
|
|
905
|
+
exports.handler = handler(service)
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
**Request Format:**
|
|
909
|
+
The handler accepts requests in a standardized format that works across platforms:
|
|
910
|
+
|
|
911
|
+
```javascript
|
|
912
|
+
{
|
|
913
|
+
// HTTP method (required)
|
|
914
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT', // or httpMethod
|
|
915
|
+
|
|
916
|
+
// Path information (one of these)
|
|
917
|
+
url: '/CreateProfile?id=123', // Full URL
|
|
918
|
+
path: '/CreateProfile', // Path only
|
|
919
|
+
|
|
920
|
+
// Query parameters (one of these)
|
|
921
|
+
queryStringParameters: { id: '123' },
|
|
922
|
+
// or parsed from url
|
|
923
|
+
|
|
924
|
+
// Request body (for POST/PATCH)
|
|
925
|
+
body: '{"name":"John"}', // JSON string
|
|
926
|
+
// or already parsed object
|
|
927
|
+
|
|
928
|
+
// Headers
|
|
929
|
+
headers: {
|
|
930
|
+
'Authorization': 'Bearer token',
|
|
931
|
+
'Content-Type': 'application/json'
|
|
932
|
+
},
|
|
933
|
+
|
|
934
|
+
// Request context (optional)
|
|
935
|
+
requestContext: {
|
|
936
|
+
requestId: 'uuid-here'
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
**Response Format:**
|
|
942
|
+
```javascript
|
|
943
|
+
{
|
|
944
|
+
statusCode: number, // HTTP status code (200, 201, 204, 400, etc.)
|
|
945
|
+
body: string, // JSON stringified output (if present)
|
|
946
|
+
headers: object, // HTTP headers (lowercase keys)
|
|
947
|
+
multiValueHeaders: object // Multi-value headers (if needed)
|
|
948
|
+
}
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
**HTTP Method Mapping:**
|
|
952
|
+
Operations are automatically mapped to HTTP methods:
|
|
953
|
+
- **CREATE** → `POST`
|
|
954
|
+
- **READ** → `GET`
|
|
955
|
+
- **UPDATE** → `PATCH`
|
|
956
|
+
- **DELETE** → `DELETE`
|
|
957
|
+
- **INDEX** → `GET`
|
|
958
|
+
|
|
959
|
+
**Path Structure:**
|
|
960
|
+
Each operation is exposed at `/{OperationId}` (e.g., `/CreateProfile`, `/ReadProfile`).
|
|
961
|
+
|
|
962
|
+
**Request Processing:**
|
|
963
|
+
1. `handler()` receives request
|
|
964
|
+
2. `createContext()` builds execution context
|
|
965
|
+
3. `specMiddleware()` handles special paths (`/`, `/Spec`, composer specs)
|
|
966
|
+
4. `logRequest()` logs request metadata
|
|
967
|
+
5. `service.process()` executes operation
|
|
968
|
+
6. Returns HTTP response
|
|
969
|
+
|
|
970
|
+
**CORS and Headers:**
|
|
971
|
+
Operations can set custom headers via `setHeader()`:
|
|
972
|
+
```javascript
|
|
973
|
+
async action(parameters) {
|
|
974
|
+
this.setHeader('X-Custom-Header', 'value')
|
|
975
|
+
return super.action(parameters)
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
Headers set by operations are included in the HTTP response.
|
|
980
|
+
|
|
981
|
+
### Kafka
|
|
982
|
+
|
|
983
|
+
Kafka integration is a planned feature for event-driven architectures. While not currently implemented in the core library, the Service architecture supports extension for Kafka message processing.
|
|
984
|
+
|
|
985
|
+
**Potential Implementation:**
|
|
986
|
+
The Service's `process(context)` method can be invoked directly with a context object, making it possible to create Kafka consumers that:
|
|
987
|
+
|
|
988
|
+
1. **Receive Messages**: Kafka consumer receives messages from topics
|
|
989
|
+
2. **Create Context**: Transform Kafka message into execution context format
|
|
990
|
+
3. **Process Operation**: Call `service.process(context)` with the operation context
|
|
991
|
+
4. **Handle Response**: Process the result (ack/nack message, publish to output topic, etc.)
|
|
992
|
+
|
|
993
|
+
**Kafka Context Structure:**
|
|
994
|
+
```javascript
|
|
995
|
+
{
|
|
996
|
+
// Kafka-specific
|
|
997
|
+
topic: string,
|
|
998
|
+
partition: number,
|
|
999
|
+
offset: number,
|
|
1000
|
+
key: string,
|
|
1001
|
+
|
|
1002
|
+
// Standard context
|
|
1003
|
+
operationId: string,
|
|
1004
|
+
mutation: object, // Message payload
|
|
1005
|
+
query: object, // Message headers/metadata
|
|
1006
|
+
...
|
|
1007
|
+
}
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**Event-Driven Patterns:**
|
|
1011
|
+
- **Command Pattern**: Kafka messages trigger operations (Create, Update, Delete)
|
|
1012
|
+
- **Event Sourcing**: Operations produce events that are published to Kafka
|
|
1013
|
+
- **CQRS**: Separate read and write operations via Kafka topics
|
|
1014
|
+
- **Saga Pattern**: Coordinate distributed transactions via Kafka events
|
|
1015
|
+
|
|
1016
|
+
This integration point allows the Service to participate in event-driven microservices architectures while maintaining the same operation, validation, and error handling logic.
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
Revision: January 9, 2026<br/>
|
|
1021
|
+
By: Alex Kravets (@alexkravets)
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED