@kravc/dos 1.12.3 → 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.
Files changed (2) hide show
  1. package/README.md +1019 -3
  2. package/package.json +1 -1
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
- library for building API-driven serverless services. Inspired by
5
- **Ruby on Rails**.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kravc/dos",
3
- "version": "1.12.3",
3
+ "version": "1.12.4",
4
4
  "description": "Convention-based, easy-to-use library for building API-driven serverless services.",
5
5
  "keywords": [
6
6
  "Service",