@onlineapps/service-wrapper 2.0.7 → 2.0.8
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 +11 -0
- package/docs/API_STRUCTURE_STANDARD.md +132 -0
- package/docs/CONFIGURATION_GUIDE.md +261 -0
- package/docs/OPERATIONS_SCHEMA.md +363 -0
- package/docs/SERVICE_TESTING_STANDARD.md +389 -0
- package/package.json +4 -3
- package/src/ServiceWrapper.js +7 -3
- package/test/monitoring-integration.test.js +150 -0
package/README.md
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
# @onlineapps/service-wrapper
|
|
2
2
|
|
|
3
|
+
**Main orchestration package for microservices - NO prefix as it's the primary integration point**
|
|
4
|
+
|
|
3
5
|
Infrastructure wrapper that handles all workflow processing, message queue operations, and service registration for microservices.
|
|
4
6
|
|
|
7
|
+
## Naming Convention
|
|
8
|
+
|
|
9
|
+
This package uses `@onlineapps/service-wrapper` WITHOUT any connector prefix because:
|
|
10
|
+
- It's the main orchestration layer, not a specific connector
|
|
11
|
+
- All services depend on it directly
|
|
12
|
+
- It aggregates all other connectors (base, infra, orch)
|
|
13
|
+
|
|
14
|
+
Directory: `/shared/connector/conn-app-service-wrapper/` (historical, kept for backward compatibility)
|
|
15
|
+
|
|
5
16
|
## Purpose
|
|
6
17
|
|
|
7
18
|
This package provides the infrastructure layer that sits between:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# API Structure Standard for Microservices
|
|
2
|
+
|
|
3
|
+
## Endpoint Structure
|
|
4
|
+
|
|
5
|
+
All microservices follow a simple, unversioned API structure.
|
|
6
|
+
|
|
7
|
+
### URL Pattern
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/ # Service discovery
|
|
11
|
+
/health # Health check
|
|
12
|
+
/status # Service status
|
|
13
|
+
/metrics # Prometheus metrics
|
|
14
|
+
/specification # Operations specification
|
|
15
|
+
/api/{operation} # Business operations
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Endpoint Categories
|
|
19
|
+
|
|
20
|
+
- **System endpoints**: Health, status, metrics, specification
|
|
21
|
+
- **Business endpoints** (`/api/*`): Application-specific operations
|
|
22
|
+
|
|
23
|
+
## Standard Endpoints
|
|
24
|
+
|
|
25
|
+
### Discovery Endpoint
|
|
26
|
+
|
|
27
|
+
**GET /**
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"service": "service-name",
|
|
31
|
+
"version": "1.0.0",
|
|
32
|
+
"description": "Service description",
|
|
33
|
+
"specification": "/specification",
|
|
34
|
+
"health": "/health",
|
|
35
|
+
"status": "/status"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### System Endpoints
|
|
40
|
+
|
|
41
|
+
| Endpoint | Purpose | Response |
|
|
42
|
+
|----------|---------|----------|
|
|
43
|
+
| `GET /health` | Basic health check | `{"status": "healthy", "timestamp": "..."}` |
|
|
44
|
+
| `GET /status` | Detailed service status | Service metrics and dependencies |
|
|
45
|
+
| `GET /specification` | Operations specification | Operations JSON schema |
|
|
46
|
+
| `GET /metrics` | Prometheus metrics | Metrics in Prometheus format |
|
|
47
|
+
|
|
48
|
+
### Business Endpoints
|
|
49
|
+
|
|
50
|
+
Service-specific operations under `/api/*`:
|
|
51
|
+
- `POST /api/create-user`
|
|
52
|
+
- `POST /api/process-order`
|
|
53
|
+
- `GET /api/get-product`
|
|
54
|
+
|
|
55
|
+
## Implementation Guide
|
|
56
|
+
|
|
57
|
+
### Express.js Structure
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const express = require('express');
|
|
61
|
+
const app = express();
|
|
62
|
+
|
|
63
|
+
// Root endpoint - service discovery
|
|
64
|
+
app.get('/', (req, res) => {
|
|
65
|
+
res.json({
|
|
66
|
+
service: process.env.SERVICE_NAME,
|
|
67
|
+
version: require('./package.json').version,
|
|
68
|
+
specification: '/specification',
|
|
69
|
+
health: '/health',
|
|
70
|
+
status: '/status'
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// System endpoints
|
|
75
|
+
app.get('/health', healthHandler);
|
|
76
|
+
app.get('/status', statusHandler);
|
|
77
|
+
app.get('/specification', specificationHandler);
|
|
78
|
+
app.get('/metrics', metricsHandler);
|
|
79
|
+
|
|
80
|
+
// Business endpoints under /api
|
|
81
|
+
app.post('/api/create-user', createUserHandler);
|
|
82
|
+
app.post('/api/process-order', processOrderHandler);
|
|
83
|
+
app.get('/api/get-product', getProductHandler);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration Structure
|
|
87
|
+
|
|
88
|
+
All service configurations are stored in `conn-config/` directory:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
/your-service
|
|
92
|
+
/conn-config
|
|
93
|
+
/config.json # Main configuration
|
|
94
|
+
/operations.json # Operations specification
|
|
95
|
+
/middleware.json # Middleware configuration
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
See [Configuration Guide](./CONFIGURATION_GUIDE.md) for details.
|
|
99
|
+
|
|
100
|
+
## Migration Guide
|
|
101
|
+
|
|
102
|
+
### From OpenAPI to Operations
|
|
103
|
+
|
|
104
|
+
| Old | New | Purpose |
|
|
105
|
+
|-----|-----|------|
|
|
106
|
+
| `/api/v1/specification` | `/specification` | Operations schema |
|
|
107
|
+
| `/v1/system/health` | `/health` | Health check |
|
|
108
|
+
| `/v1/system/status` | `/status` | Service status |
|
|
109
|
+
| `/v1/{operation}` | `/api/{operation}` | Business operations |
|
|
110
|
+
|
|
111
|
+
### Simple Migration Steps
|
|
112
|
+
|
|
113
|
+
1. Create `operations.json` from OpenAPI
|
|
114
|
+
2. Remove version prefixes from endpoints
|
|
115
|
+
3. Move business endpoints under `/api`
|
|
116
|
+
4. Update service discovery response
|
|
117
|
+
|
|
118
|
+
## Testing Requirements
|
|
119
|
+
|
|
120
|
+
Tests must verify:
|
|
121
|
+
|
|
122
|
+
1. **Discovery endpoint** returns service info and specification link
|
|
123
|
+
2. **System endpoints** work correctly
|
|
124
|
+
3. **Business endpoints** under `/api` process operations
|
|
125
|
+
4. **Operations schema** is valid and complete
|
|
126
|
+
|
|
127
|
+
## Benefits
|
|
128
|
+
|
|
129
|
+
1. **Simplicity** - No version complexity
|
|
130
|
+
2. **Clear structure** - System vs business separation
|
|
131
|
+
3. **Direct mapping** - Operations to Cookbook steps
|
|
132
|
+
4. **Lightweight** - Minimal configuration needed
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Service Configuration Guide
|
|
2
|
+
|
|
3
|
+
## Configuration Directory Structure
|
|
4
|
+
|
|
5
|
+
Every service must have a `conn-config/` directory with these configuration files:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/service-root
|
|
9
|
+
/conn-config
|
|
10
|
+
config.json # Main service configuration
|
|
11
|
+
operations.json # Operations specification
|
|
12
|
+
middleware.json # Middleware stack configuration
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuration Files
|
|
16
|
+
|
|
17
|
+
### 1. config.json - Main Configuration
|
|
18
|
+
|
|
19
|
+
Primary service configuration with environment overrides:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"service": {
|
|
24
|
+
"name": "hello-service",
|
|
25
|
+
"version": "1.0.0",
|
|
26
|
+
"description": "Greeting service",
|
|
27
|
+
"environment": "${NODE_ENV:development}"
|
|
28
|
+
},
|
|
29
|
+
"server": {
|
|
30
|
+
"port": "${PORT:3000}",
|
|
31
|
+
"host": "${HOST:0.0.0.0}",
|
|
32
|
+
"timeout": 30000
|
|
33
|
+
},
|
|
34
|
+
"api": {
|
|
35
|
+
"versions": {
|
|
36
|
+
"available": ["v1"],
|
|
37
|
+
"current": "v1",
|
|
38
|
+
"deprecated": []
|
|
39
|
+
},
|
|
40
|
+
"rateLimit": {
|
|
41
|
+
"enabled": true,
|
|
42
|
+
"windowMs": 60000,
|
|
43
|
+
"max": 100
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"monitoring": {
|
|
47
|
+
"healthCheck": {
|
|
48
|
+
"interval": 30000,
|
|
49
|
+
"timeout": 5000
|
|
50
|
+
},
|
|
51
|
+
"metrics": {
|
|
52
|
+
"enabled": "${METRICS_ENABLED:true}",
|
|
53
|
+
"port": "${METRICS_PORT:9090}"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"rabbitmq": {
|
|
58
|
+
"url": "${RABBITMQ_URL:amqp://localhost:5672}",
|
|
59
|
+
"enabled": "${MQ_ENABLED:true}"
|
|
60
|
+
},
|
|
61
|
+
"redis": {
|
|
62
|
+
"url": "${REDIS_URL:redis://localhost:6379}",
|
|
63
|
+
"enabled": "${CACHE_ENABLED:false}"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. operations.json - Service Operations
|
|
70
|
+
|
|
71
|
+
Defines all operations the service provides:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"operations": {
|
|
76
|
+
"create-greeting": {
|
|
77
|
+
"description": "Generate a greeting message",
|
|
78
|
+
"handler": "handlers.greetings.create",
|
|
79
|
+
"endpoint": "/api/create-greeting",
|
|
80
|
+
"method": "POST",
|
|
81
|
+
"input": {
|
|
82
|
+
"name": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"required": true,
|
|
85
|
+
"description": "Name to greet"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"output": {
|
|
89
|
+
"message": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Generated greeting"
|
|
92
|
+
},
|
|
93
|
+
"timestamp": {
|
|
94
|
+
"type": "datetime"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. middleware.json - Middleware Configuration
|
|
103
|
+
|
|
104
|
+
Middleware stack and configuration:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"global": [
|
|
109
|
+
{
|
|
110
|
+
"name": "cors",
|
|
111
|
+
"enabled": true,
|
|
112
|
+
"config": {
|
|
113
|
+
"origin": "${CORS_ORIGIN:*}",
|
|
114
|
+
"credentials": true
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "helmet",
|
|
119
|
+
"enabled": true,
|
|
120
|
+
"config": {}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "requestLogger",
|
|
124
|
+
"enabled": true,
|
|
125
|
+
"config": {
|
|
126
|
+
"level": "info"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "bodyParser",
|
|
131
|
+
"enabled": true,
|
|
132
|
+
"config": {
|
|
133
|
+
"limit": "10mb"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"routes": {
|
|
138
|
+
"authenticate": {
|
|
139
|
+
"handler": "middleware/auth.authenticate",
|
|
140
|
+
"config": {
|
|
141
|
+
"required": true
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"authorize": {
|
|
145
|
+
"handler": "middleware/auth.authorize",
|
|
146
|
+
"config": {
|
|
147
|
+
"roles": ["admin", "user"]
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"validate": {
|
|
151
|
+
"handler": "middleware/validation.validate"
|
|
152
|
+
},
|
|
153
|
+
"rateLimit": {
|
|
154
|
+
"handler": "middleware/rateLimit",
|
|
155
|
+
"config": {
|
|
156
|
+
"max": 100,
|
|
157
|
+
"windowMs": 60000
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"error": [
|
|
162
|
+
{
|
|
163
|
+
"name": "notFound",
|
|
164
|
+
"handler": "middleware/errors.notFound"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "errorHandler",
|
|
168
|
+
"handler": "middleware/errors.handler"
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Environment Variables
|
|
176
|
+
|
|
177
|
+
Configuration supports environment variable substitution:
|
|
178
|
+
|
|
179
|
+
- Format: `${VARIABLE_NAME:default_value}`
|
|
180
|
+
- Example: `${PORT:3000}` uses PORT env var or defaults to 3000
|
|
181
|
+
|
|
182
|
+
### Standard Environment Variables
|
|
183
|
+
|
|
184
|
+
| Variable | Purpose | Default |
|
|
185
|
+
|----------|---------|---------|
|
|
186
|
+
| `NODE_ENV` | Environment (development/production) | development |
|
|
187
|
+
| `SERVICE_NAME` | Service name | From config.json |
|
|
188
|
+
| `PORT` | Server port | 3000 |
|
|
189
|
+
| `LOG_LEVEL` | Logging level | info |
|
|
190
|
+
| `RABBITMQ_URL` | RabbitMQ connection | amqp://localhost:5672 |
|
|
191
|
+
| `REDIS_URL` | Redis connection | redis://localhost:6379 |
|
|
192
|
+
| `METRICS_ENABLED` | Enable metrics | true |
|
|
193
|
+
|
|
194
|
+
## Loading Configuration
|
|
195
|
+
|
|
196
|
+
Service wrapper automatically loads configuration:
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
const ServiceWrapper = require('@onlineapps/service-wrapper');
|
|
200
|
+
|
|
201
|
+
// Automatically loads from ./conn-config/
|
|
202
|
+
const wrapper = new ServiceWrapper();
|
|
203
|
+
|
|
204
|
+
// Or specify custom path
|
|
205
|
+
const wrapper = new ServiceWrapper({
|
|
206
|
+
configPath: './custom-config/'
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Access configuration
|
|
210
|
+
const config = wrapper.config;
|
|
211
|
+
console.log(config.service.name);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Validation
|
|
215
|
+
|
|
216
|
+
Configuration is validated on startup:
|
|
217
|
+
|
|
218
|
+
1. Required files exist
|
|
219
|
+
2. JSON syntax is valid
|
|
220
|
+
3. Required fields are present
|
|
221
|
+
4. Environment variables are resolved
|
|
222
|
+
5. Operations schema is valid
|
|
223
|
+
|
|
224
|
+
## Best Practices
|
|
225
|
+
|
|
226
|
+
1. **Keep sensitive data in environment variables** - Never commit secrets
|
|
227
|
+
2. **Use defaults** - Provide sensible defaults for all configs
|
|
228
|
+
3. **Document all options** - Include descriptions in configs
|
|
229
|
+
4. **Validate early** - Fail fast on invalid configuration
|
|
230
|
+
5. **Version your configs** - Track configuration changes
|
|
231
|
+
|
|
232
|
+
## Service Configuration Example
|
|
233
|
+
|
|
234
|
+
### config.json with Execution Mode
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"service": {
|
|
238
|
+
"name": "hello-service",
|
|
239
|
+
"version": "1.0.0",
|
|
240
|
+
"execution": {
|
|
241
|
+
"mode": "${EXECUTION_MODE:direct}",
|
|
242
|
+
"direct": {
|
|
243
|
+
"handler": "./src/handlers"
|
|
244
|
+
},
|
|
245
|
+
"http": {
|
|
246
|
+
"baseUrl": "${SERVICE_URL:http://localhost:3000}"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"monitoring": {
|
|
251
|
+
"healthCheck": {
|
|
252
|
+
"interval": 30000
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Execution Modes
|
|
259
|
+
|
|
260
|
+
- **direct**: Service Wrapper calls handlers directly (50% memory savings)
|
|
261
|
+
- **http**: Service Wrapper makes HTTP calls (for external services)
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Operations Schema Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Operations Schema replaces OpenAPI for internal microservices, providing a simpler, more focused specification for service operations that map directly to Cookbook workflow steps.
|
|
6
|
+
|
|
7
|
+
## Why Operations Schema?
|
|
8
|
+
|
|
9
|
+
- **Simpler than OpenAPI** - Only what's needed for workflow orchestration
|
|
10
|
+
- **Direct Cookbook mapping** - Operations correspond 1:1 with workflow steps
|
|
11
|
+
- **Flexible execution** - Supports both direct function calls and HTTP
|
|
12
|
+
- **Extensible** - Easy to add support for external APIs (REST, GraphQL, gRPC)
|
|
13
|
+
|
|
14
|
+
## File Location
|
|
15
|
+
|
|
16
|
+
Operations are defined in `/conn-config/operations.json` for each service.
|
|
17
|
+
|
|
18
|
+
## Schema Structure
|
|
19
|
+
|
|
20
|
+
### Basic Operation (Internal Service)
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"operations": {
|
|
25
|
+
"operation-name": {
|
|
26
|
+
"description": "Human-readable description",
|
|
27
|
+
"handler": "path.to.handler.function",
|
|
28
|
+
"endpoint": "/api/operation-name",
|
|
29
|
+
"method": "POST",
|
|
30
|
+
"input": {
|
|
31
|
+
"field1": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"required": true,
|
|
34
|
+
"description": "Field description"
|
|
35
|
+
},
|
|
36
|
+
"field2": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"default": 0
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"output": {
|
|
42
|
+
"result": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Result description"
|
|
45
|
+
},
|
|
46
|
+
"timestamp": {
|
|
47
|
+
"type": "datetime"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### External REST API Operation
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"operations": {
|
|
60
|
+
"send-email": {
|
|
61
|
+
"description": "Send email via SendGrid",
|
|
62
|
+
"handler": "handlers.email.send",
|
|
63
|
+
"external": {
|
|
64
|
+
"type": "rest",
|
|
65
|
+
"endpoint": "https://api.sendgrid.com/v3/mail/send",
|
|
66
|
+
"method": "POST",
|
|
67
|
+
"headers": {
|
|
68
|
+
"Authorization": "Bearer ${SENDGRID_API_KEY}",
|
|
69
|
+
"Content-Type": "application/json"
|
|
70
|
+
},
|
|
71
|
+
"mapping": {
|
|
72
|
+
"input": {
|
|
73
|
+
"personalizations[0].to[0].email": "$.recipient",
|
|
74
|
+
"from.email": "${SENDER_EMAIL}",
|
|
75
|
+
"subject": "$.subject",
|
|
76
|
+
"content[0].value": "$.body"
|
|
77
|
+
},
|
|
78
|
+
"output": {
|
|
79
|
+
"messageId": "$.x-message-id",
|
|
80
|
+
"status": "$.status"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"input": {
|
|
85
|
+
"recipient": { "type": "string", "required": true },
|
|
86
|
+
"subject": { "type": "string", "required": true },
|
|
87
|
+
"body": { "type": "string", "required": true }
|
|
88
|
+
},
|
|
89
|
+
"output": {
|
|
90
|
+
"messageId": "string",
|
|
91
|
+
"status": "string"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### GraphQL Operation
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"operations": {
|
|
103
|
+
"get-user-repos": {
|
|
104
|
+
"description": "Get user repositories from GitHub",
|
|
105
|
+
"external": {
|
|
106
|
+
"type": "graphql",
|
|
107
|
+
"endpoint": "https://api.github.com/graphql",
|
|
108
|
+
"headers": {
|
|
109
|
+
"Authorization": "Bearer ${GITHUB_TOKEN}"
|
|
110
|
+
},
|
|
111
|
+
"query": "query($login: String!) { user(login: $login) { repositories(first: 10) { nodes { name, description } } } }",
|
|
112
|
+
"variables": {
|
|
113
|
+
"login": "$.username"
|
|
114
|
+
},
|
|
115
|
+
"mapping": {
|
|
116
|
+
"output": {
|
|
117
|
+
"repositories": "$.data.user.repositories.nodes"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"input": {
|
|
122
|
+
"username": { "type": "string", "required": true }
|
|
123
|
+
},
|
|
124
|
+
"output": {
|
|
125
|
+
"repositories": { "type": "array" }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Field Types
|
|
133
|
+
|
|
134
|
+
Supported types for input/output fields:
|
|
135
|
+
|
|
136
|
+
- `string` - Text data
|
|
137
|
+
- `number` - Numeric value (integer or float)
|
|
138
|
+
- `boolean` - True/false
|
|
139
|
+
- `object` - Nested object structure
|
|
140
|
+
- `array` - Array of items
|
|
141
|
+
- `datetime` - ISO 8601 datetime string
|
|
142
|
+
- `date` - ISO 8601 date string
|
|
143
|
+
- `email` - Email address (validated)
|
|
144
|
+
- `url` - URL (validated)
|
|
145
|
+
- `uuid` - UUID v4 (validated)
|
|
146
|
+
|
|
147
|
+
## Execution Modes
|
|
148
|
+
|
|
149
|
+
Services can run in different execution modes, configured in `config.json`:
|
|
150
|
+
|
|
151
|
+
### Direct Mode (Internal Services)
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"service": {
|
|
155
|
+
"execution": {
|
|
156
|
+
"mode": "direct",
|
|
157
|
+
"handler": "./src/handlers"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
- Service Wrapper calls handlers directly
|
|
163
|
+
- No HTTP overhead
|
|
164
|
+
- 50% memory savings
|
|
165
|
+
- Single process
|
|
166
|
+
|
|
167
|
+
### HTTP Mode (Default/External)
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"service": {
|
|
171
|
+
"execution": {
|
|
172
|
+
"mode": "http",
|
|
173
|
+
"baseUrl": "${SERVICE_URL:http://localhost:3000}"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
- Service Wrapper makes HTTP calls
|
|
179
|
+
- Service runs separately
|
|
180
|
+
- Better for testing
|
|
181
|
+
- Required for external APIs
|
|
182
|
+
|
|
183
|
+
## Cookbook Integration
|
|
184
|
+
|
|
185
|
+
Operations map directly to Cookbook workflow steps:
|
|
186
|
+
|
|
187
|
+
### Cookbook Step
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"step_id": "greet_user",
|
|
191
|
+
"type": "service_call",
|
|
192
|
+
"service": "hello-service",
|
|
193
|
+
"operation": "good-day",
|
|
194
|
+
"input": {
|
|
195
|
+
"name": "$.user_name"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Corresponding Operation
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"operations": {
|
|
204
|
+
"good-day": {
|
|
205
|
+
"description": "Generate good day greeting",
|
|
206
|
+
"handler": "handlers.greetings.goodDay",
|
|
207
|
+
"endpoint": "/api/good-day",
|
|
208
|
+
"input": {
|
|
209
|
+
"name": { "type": "string", "required": true }
|
|
210
|
+
},
|
|
211
|
+
"output": {
|
|
212
|
+
"message": "string",
|
|
213
|
+
"timestamp": "datetime"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Handler Implementation
|
|
221
|
+
|
|
222
|
+
### Direct Mode Handler
|
|
223
|
+
```javascript
|
|
224
|
+
// handlers/greetings.js
|
|
225
|
+
exports.goodDay = async (input) => {
|
|
226
|
+
const { name } = input;
|
|
227
|
+
return {
|
|
228
|
+
message: `Good day, ${name}!`,
|
|
229
|
+
timestamp: new Date().toISOString()
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### HTTP Mode Endpoint
|
|
235
|
+
```javascript
|
|
236
|
+
// src/app.js
|
|
237
|
+
app.post('/api/good-day', async (req, res) => {
|
|
238
|
+
const { name } = req.body;
|
|
239
|
+
res.json({
|
|
240
|
+
message: `Good day, ${name}!`,
|
|
241
|
+
timestamp: new Date().toISOString()
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Validation
|
|
247
|
+
|
|
248
|
+
Input/output validation is automatic based on the schema:
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
// Service Wrapper validates automatically
|
|
252
|
+
const operation = operations['good-day'];
|
|
253
|
+
validateInput(input, operation.input); // Throws if invalid
|
|
254
|
+
const result = await executeOperation(operation, input);
|
|
255
|
+
validateOutput(result, operation.output); // Throws if invalid
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Migration from OpenAPI
|
|
259
|
+
|
|
260
|
+
### Before (OpenAPI)
|
|
261
|
+
```yaml
|
|
262
|
+
paths:
|
|
263
|
+
/v1/good-day:
|
|
264
|
+
post:
|
|
265
|
+
operationId: createGoodDay
|
|
266
|
+
requestBody:
|
|
267
|
+
content:
|
|
268
|
+
application/json:
|
|
269
|
+
schema:
|
|
270
|
+
$ref: '#/components/schemas/GreetingRequest'
|
|
271
|
+
responses:
|
|
272
|
+
'200':
|
|
273
|
+
content:
|
|
274
|
+
application/json:
|
|
275
|
+
schema:
|
|
276
|
+
$ref: '#/components/schemas/GreetingResponse'
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### After (Operations Schema)
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"operations": {
|
|
283
|
+
"good-day": {
|
|
284
|
+
"handler": "handlers.greetings.goodDay",
|
|
285
|
+
"endpoint": "/api/good-day",
|
|
286
|
+
"input": {
|
|
287
|
+
"name": { "type": "string", "required": true }
|
|
288
|
+
},
|
|
289
|
+
"output": {
|
|
290
|
+
"message": "string",
|
|
291
|
+
"timestamp": "datetime"
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Best Practices
|
|
299
|
+
|
|
300
|
+
1. **Keep operations focused** - One operation = one business action
|
|
301
|
+
2. **Use descriptive names** - Operation names should be self-explanatory
|
|
302
|
+
3. **Document fields** - Add descriptions to complex fields
|
|
303
|
+
4. **Validate strictly** - Mark required fields appropriately
|
|
304
|
+
5. **Handle errors gracefully** - Define error responses in output
|
|
305
|
+
6. **Use environment variables** - For URLs, keys, and configuration
|
|
306
|
+
7. **Test both modes** - Ensure operations work in both direct and HTTP mode
|
|
307
|
+
|
|
308
|
+
## Service Wrapper Integration
|
|
309
|
+
|
|
310
|
+
The Service Wrapper automatically:
|
|
311
|
+
- Loads operations.json on startup
|
|
312
|
+
- Routes Cookbook steps to operations
|
|
313
|
+
- Validates input/output
|
|
314
|
+
- Handles execution mode (direct/HTTP)
|
|
315
|
+
- Manages external API calls
|
|
316
|
+
- Provides metrics and logging
|
|
317
|
+
|
|
318
|
+
## Example Service Structure
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
hello-service/
|
|
322
|
+
├── conn-config/
|
|
323
|
+
│ ├── config.json # Service configuration
|
|
324
|
+
│ ├── operations.json # Operations schema
|
|
325
|
+
│ └── middleware.json # Middleware config
|
|
326
|
+
├── src/
|
|
327
|
+
│ ├── app.js # Express app (for HTTP mode)
|
|
328
|
+
│ └── handlers/ # Handler functions (for direct mode)
|
|
329
|
+
│ └── greetings.js
|
|
330
|
+
└── package.json
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Testing Operations
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
// Test operation directly
|
|
337
|
+
const operations = require('./conn-config/operations.json');
|
|
338
|
+
const handlers = require('./src/handlers');
|
|
339
|
+
|
|
340
|
+
test('good-day operation', async () => {
|
|
341
|
+
const input = { name: 'World' };
|
|
342
|
+
const result = await handlers.greetings.goodDay(input);
|
|
343
|
+
|
|
344
|
+
expect(result.message).toBe('Good day, World!');
|
|
345
|
+
expect(result.timestamp).toBeDefined();
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Future Extensions
|
|
350
|
+
|
|
351
|
+
The schema is designed to be extensible:
|
|
352
|
+
|
|
353
|
+
- **WebSocket operations** - Real-time communication
|
|
354
|
+
- **Message queue operations** - Direct MQ publishing
|
|
355
|
+
- **Database operations** - Direct DB queries
|
|
356
|
+
- **File operations** - S3, MinIO uploads
|
|
357
|
+
- **Batch operations** - Bulk processing
|
|
358
|
+
|
|
359
|
+
Each extension follows the same pattern:
|
|
360
|
+
1. Define operation in operations.json
|
|
361
|
+
2. Specify type in `external` block
|
|
362
|
+
3. Map inputs/outputs
|
|
363
|
+
4. Service Wrapper handles execution
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# Service Testing Standard and OpenAPI Generation
|
|
2
|
+
|
|
3
|
+
## 1. OpenAPI Schema Generation
|
|
4
|
+
|
|
5
|
+
### Step 1: Analyze Your Service Endpoints
|
|
6
|
+
|
|
7
|
+
List all your service endpoints and their:
|
|
8
|
+
- HTTP methods (GET, POST, PUT, DELETE)
|
|
9
|
+
- Request parameters (query, path, body)
|
|
10
|
+
- Response structures
|
|
11
|
+
- Error responses
|
|
12
|
+
|
|
13
|
+
### Step 2: Create openapi.yaml
|
|
14
|
+
|
|
15
|
+
Create `openapi.yaml` in your service root directory with this structure:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
openapi: 3.0.0
|
|
19
|
+
info:
|
|
20
|
+
title: # Your service name from package.json
|
|
21
|
+
version: # Your service version from package.json
|
|
22
|
+
description: # Brief description of what your service does
|
|
23
|
+
|
|
24
|
+
servers:
|
|
25
|
+
- url: http://localhost:{your-port}
|
|
26
|
+
description: Local development
|
|
27
|
+
- url: http://{container-name}:3000
|
|
28
|
+
description: Docker container
|
|
29
|
+
|
|
30
|
+
paths:
|
|
31
|
+
# Document each endpoint
|
|
32
|
+
/your-endpoint:
|
|
33
|
+
get/post/put/delete:
|
|
34
|
+
summary: # One line description
|
|
35
|
+
operationId: # Unique identifier (e.g., getUsers, createOrder)
|
|
36
|
+
parameters: # If applicable
|
|
37
|
+
requestBody: # If applicable
|
|
38
|
+
responses:
|
|
39
|
+
'200':
|
|
40
|
+
description: # Success response
|
|
41
|
+
content:
|
|
42
|
+
application/json:
|
|
43
|
+
schema:
|
|
44
|
+
# Define or reference schema
|
|
45
|
+
'400':
|
|
46
|
+
# Bad request
|
|
47
|
+
'500':
|
|
48
|
+
# Server error
|
|
49
|
+
|
|
50
|
+
components:
|
|
51
|
+
schemas:
|
|
52
|
+
# Define reusable schemas here
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Step 3: Generate Schema from Code (Alternative)
|
|
56
|
+
|
|
57
|
+
If you have many endpoints, generate initial schema:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Install openapi generator
|
|
61
|
+
npm install --save-dev @apidevtools/swagger-cli
|
|
62
|
+
|
|
63
|
+
# For Express apps, use express-openapi-generator
|
|
64
|
+
npm install --save-dev express-openapi-generator
|
|
65
|
+
|
|
66
|
+
# Add generation script to package.json
|
|
67
|
+
"scripts": {
|
|
68
|
+
"generate:openapi": "node scripts/generate-openapi.js"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Create `scripts/generate-openapi.js`:
|
|
73
|
+
```javascript
|
|
74
|
+
const app = require('../src/app');
|
|
75
|
+
const generator = require('express-openapi-generator');
|
|
76
|
+
|
|
77
|
+
generator(app, {
|
|
78
|
+
outputFile: './openapi.yaml',
|
|
79
|
+
info: {
|
|
80
|
+
title: process.env.SERVICE_NAME,
|
|
81
|
+
version: require('../package.json').version
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Step 4: Validate Your Schema
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Install validator
|
|
90
|
+
npm install --save-dev @apidevtools/swagger-parser
|
|
91
|
+
|
|
92
|
+
# Add validation script
|
|
93
|
+
"scripts": {
|
|
94
|
+
"validate:openapi": "swagger-cli validate openapi.yaml"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 2. Test Structure
|
|
99
|
+
|
|
100
|
+
### Directory Structure
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
/your-service
|
|
104
|
+
/test
|
|
105
|
+
/unit # Unit tests for individual functions
|
|
106
|
+
/integration # Integration tests with mocked dependencies
|
|
107
|
+
/api # API endpoint tests
|
|
108
|
+
/compliance # OpenAPI compliance tests
|
|
109
|
+
test.config.js # Shared test configuration
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Test Configuration
|
|
113
|
+
|
|
114
|
+
Create `test/test.config.js`:
|
|
115
|
+
```javascript
|
|
116
|
+
module.exports = {
|
|
117
|
+
// Service configuration for tests
|
|
118
|
+
service: {
|
|
119
|
+
name: process.env.SERVICE_NAME || 'your-service',
|
|
120
|
+
port: process.env.PORT || 3000,
|
|
121
|
+
baseUrl: `http://localhost:${process.env.PORT || 3000}`
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Timeouts
|
|
125
|
+
timeouts: {
|
|
126
|
+
unit: 5000,
|
|
127
|
+
integration: 10000,
|
|
128
|
+
api: 15000
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// Mock data
|
|
132
|
+
mocks: {
|
|
133
|
+
validRequest: { /* valid request data */ },
|
|
134
|
+
invalidRequest: { /* invalid request data */ }
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 3. Test Implementation
|
|
140
|
+
|
|
141
|
+
### Unit Tests (`test/unit/`)
|
|
142
|
+
|
|
143
|
+
Test individual functions without external dependencies:
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
describe('Business Logic', () => {
|
|
147
|
+
test('should process valid input', () => {
|
|
148
|
+
const result = processFunction(validInput);
|
|
149
|
+
expect(result).toBeDefined();
|
|
150
|
+
expect(result.status).toBe('success');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should handle invalid input', () => {
|
|
154
|
+
expect(() => processFunction(invalidInput)).toThrow();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### API Tests (`test/api/`)
|
|
160
|
+
|
|
161
|
+
Test actual HTTP endpoints:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
const request = require('supertest');
|
|
165
|
+
const app = require('../../src/app');
|
|
166
|
+
|
|
167
|
+
describe('API Endpoints', () => {
|
|
168
|
+
describe('GET /your-endpoint', () => {
|
|
169
|
+
test('should return 200 with valid response', async () => {
|
|
170
|
+
const response = await request(app)
|
|
171
|
+
.get('/your-endpoint')
|
|
172
|
+
.expect(200);
|
|
173
|
+
|
|
174
|
+
expect(response.body).toHaveProperty('expectedField');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should return 400 for invalid request', async () => {
|
|
178
|
+
const response = await request(app)
|
|
179
|
+
.get('/your-endpoint?invalid=true')
|
|
180
|
+
.expect(400);
|
|
181
|
+
|
|
182
|
+
expect(response.body).toHaveProperty('error');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Compliance Tests (`test/compliance/`)
|
|
189
|
+
|
|
190
|
+
Test OpenAPI compliance:
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const SwaggerParser = require('@apidevtools/swagger-parser');
|
|
194
|
+
const request = require('supertest');
|
|
195
|
+
const app = require('../../src/app');
|
|
196
|
+
const openApiSchema = require('../../openapi.yaml');
|
|
197
|
+
|
|
198
|
+
describe('OpenAPI Compliance', () => {
|
|
199
|
+
let api;
|
|
200
|
+
|
|
201
|
+
beforeAll(async () => {
|
|
202
|
+
// Validate and parse schema
|
|
203
|
+
api = await SwaggerParser.validate(openApiSchema);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('schema should be valid OpenAPI 3.0', () => {
|
|
207
|
+
expect(api.openapi).toBe('3.0.0');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('all endpoints should match schema', async () => {
|
|
211
|
+
for (const [path, methods] of Object.entries(api.paths)) {
|
|
212
|
+
for (const [method, spec] of Object.entries(methods)) {
|
|
213
|
+
if (['get', 'post', 'put', 'delete'].includes(method)) {
|
|
214
|
+
const response = await request(app)[method](path);
|
|
215
|
+
// Validate response matches schema
|
|
216
|
+
expect(response.status).toBeDefined();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## 4. NPM Scripts
|
|
225
|
+
|
|
226
|
+
Add to `package.json`:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"scripts": {
|
|
231
|
+
"test": "npm run test:unit && npm run test:api && npm run test:compliance",
|
|
232
|
+
"test:unit": "jest test/unit",
|
|
233
|
+
"test:integration": "jest test/integration",
|
|
234
|
+
"test:api": "jest test/api",
|
|
235
|
+
"test:compliance": "jest test/compliance",
|
|
236
|
+
"test:coverage": "jest --coverage",
|
|
237
|
+
"validate:openapi": "swagger-cli validate openapi.yaml",
|
|
238
|
+
"generate:openapi": "node scripts/generate-openapi.js",
|
|
239
|
+
"serve:docs": "swagger-ui-express openapi.yaml"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Coverage Requirements
|
|
245
|
+
|
|
246
|
+
**Minimum recommended coverage: 80%**
|
|
247
|
+
|
|
248
|
+
Configure Jest coverage thresholds in `package.json`:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"jest": {
|
|
253
|
+
"coverageThreshold": {
|
|
254
|
+
"global": {
|
|
255
|
+
"branches": 80,
|
|
256
|
+
"functions": 80,
|
|
257
|
+
"lines": 80,
|
|
258
|
+
"statements": 80
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"collectCoverageFrom": [
|
|
262
|
+
"src/**/*.js",
|
|
263
|
+
"!src/**/*.test.js",
|
|
264
|
+
"!src/**/*.spec.js"
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 5. Service Wrapper Integration
|
|
271
|
+
|
|
272
|
+
### Endpoint for OpenAPI Schema
|
|
273
|
+
|
|
274
|
+
Your service must expose its schema:
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
// In your app.js or routes
|
|
278
|
+
const fs = require('fs');
|
|
279
|
+
const yaml = require('js-yaml');
|
|
280
|
+
|
|
281
|
+
app.get('/openapi', (req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
const doc = yaml.load(fs.readFileSync('./openapi.yaml', 'utf8'));
|
|
284
|
+
res.json(doc);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
res.status(500).json({ error: 'Schema not available' });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Validation Middleware
|
|
292
|
+
|
|
293
|
+
Use Service Wrapper's validation:
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
const { validateRequest } = require('@onlineapps/service-wrapper/middleware');
|
|
297
|
+
const openApiSchema = require('./openapi.yaml');
|
|
298
|
+
|
|
299
|
+
// Apply validation to routes
|
|
300
|
+
app.post('/your-endpoint',
|
|
301
|
+
validateRequest(openApiSchema, '/your-endpoint', 'post'),
|
|
302
|
+
(req, res) => {
|
|
303
|
+
// Request is already validated
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## 6. Continuous Validation
|
|
309
|
+
|
|
310
|
+
### Pre-commit Hook
|
|
311
|
+
|
|
312
|
+
`.husky/pre-commit`:
|
|
313
|
+
```bash
|
|
314
|
+
#!/bin/sh
|
|
315
|
+
npm run validate:openapi
|
|
316
|
+
npm run test:compliance
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### CI/CD Pipeline
|
|
320
|
+
|
|
321
|
+
```yaml
|
|
322
|
+
# .github/workflows/test.yml or gitlab-ci.yml
|
|
323
|
+
test:
|
|
324
|
+
script:
|
|
325
|
+
- npm install
|
|
326
|
+
- npm run validate:openapi
|
|
327
|
+
- npm run test
|
|
328
|
+
- npm run test:coverage
|
|
329
|
+
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## 7. Validation Checklist
|
|
333
|
+
|
|
334
|
+
Before deployment, ensure:
|
|
335
|
+
|
|
336
|
+
- [ ] OpenAPI schema exists at `/openapi.yaml`
|
|
337
|
+
- [ ] Schema validates with `swagger-cli validate`
|
|
338
|
+
- [ ] All endpoints documented in schema
|
|
339
|
+
- [ ] Request/response schemas defined
|
|
340
|
+
- [ ] Error responses documented
|
|
341
|
+
- [ ] Schema version matches package.json
|
|
342
|
+
- [ ] `/openapi` endpoint returns schema
|
|
343
|
+
- [ ] Unit tests pass (>80% coverage)
|
|
344
|
+
- [ ] API tests pass
|
|
345
|
+
- [ ] Compliance tests pass
|
|
346
|
+
- [ ] Service Wrapper can load schema
|
|
347
|
+
- [ ] Validation middleware works
|
|
348
|
+
|
|
349
|
+
## 8. Common Issues and Solutions
|
|
350
|
+
|
|
351
|
+
### Issue: Schema doesn't match implementation
|
|
352
|
+
**Solution**: Generate schema from code first, then refine manually
|
|
353
|
+
|
|
354
|
+
### Issue: Validation too strict
|
|
355
|
+
**Solution**: Use `additionalProperties: true` in schemas during development
|
|
356
|
+
|
|
357
|
+
### Issue: Complex nested objects
|
|
358
|
+
**Solution**: Break into components and use `$ref`
|
|
359
|
+
|
|
360
|
+
### Issue: Different environments need different configs
|
|
361
|
+
**Solution**: Use environment variables in schema:
|
|
362
|
+
```yaml
|
|
363
|
+
servers:
|
|
364
|
+
- url: ${API_BASE_URL}
|
|
365
|
+
description: Configured base URL
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## 9. Testing with conn-e2e-testing
|
|
369
|
+
|
|
370
|
+
The connector testing framework will:
|
|
371
|
+
1. Load your OpenAPI schema
|
|
372
|
+
2. Start your service
|
|
373
|
+
3. Test each endpoint against schema
|
|
374
|
+
4. Validate requests and responses
|
|
375
|
+
5. Generate compliance report
|
|
376
|
+
|
|
377
|
+
Ensure your service is ready by:
|
|
378
|
+
1. Exposing `/openapi` endpoint
|
|
379
|
+
2. Having all endpoints documented
|
|
380
|
+
3. Returning proper HTTP status codes
|
|
381
|
+
4. Using consistent error format
|
|
382
|
+
|
|
383
|
+
## 10. Resources
|
|
384
|
+
|
|
385
|
+
- [OpenAPI 3.0 Specification](https://swagger.io/specification/)
|
|
386
|
+
- [JSON Schema Validation](https://json-schema.org/draft/2019-09/json-schema-validation.html)
|
|
387
|
+
- [swagger-cli Documentation](https://apitools.dev/swagger-cli/)
|
|
388
|
+
- Service Wrapper documentation: `/shared/connector/service-wrapper/README.md`
|
|
389
|
+
- Testing examples: `/shared/connector/conn-e2e-testing/examples/`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-wrapper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@onlineapps/conn-base-cache": "^1.0.0",
|
|
28
|
-
"@onlineapps/conn-base-
|
|
28
|
+
"@onlineapps/conn-base-monitoring": "file:../conn-base-monitoring/onlineapps-conn-base-monitoring-1.0.0.tgz",
|
|
29
29
|
"@onlineapps/conn-infra-error-handler": "^1.0.0",
|
|
30
30
|
"@onlineapps/conn-infra-mq": "^1.1.0",
|
|
31
31
|
"@onlineapps/conn-orch-api-mapper": "^1.0.0",
|
|
32
32
|
"@onlineapps/conn-orch-cookbook": "^2.0.0",
|
|
33
33
|
"@onlineapps/conn-orch-orchestrator": "^1.0.1",
|
|
34
|
-
"@onlineapps/conn-orch-registry": "^1.1.4"
|
|
34
|
+
"@onlineapps/conn-orch-registry": "^1.1.4",
|
|
35
|
+
"@onlineapps/monitoring-core": "file:../../../api/shared/monitoring-core/onlineapps-monitoring-core-1.0.0.tgz"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"express": "^5.1.0",
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Import connectors
|
|
14
14
|
const MQConnector = require('@onlineapps/conn-infra-mq');
|
|
15
15
|
const RegistryConnector = require('@onlineapps/conn-orch-registry');
|
|
16
|
-
const
|
|
16
|
+
const MonitoringConnector = require('@onlineapps/conn-base-monitoring');
|
|
17
17
|
const OrchestratorConnector = require('@onlineapps/conn-orch-orchestrator');
|
|
18
18
|
const ApiMapperConnector = require('@onlineapps/conn-orch-api-mapper');
|
|
19
19
|
const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
|
|
@@ -69,7 +69,8 @@ class ServiceWrapper {
|
|
|
69
69
|
this.config = options.config || {};
|
|
70
70
|
|
|
71
71
|
// Initialize connectors
|
|
72
|
-
this.logger =
|
|
72
|
+
this.logger = MonitoringConnector; // Singleton, will be initialized later
|
|
73
|
+
this.monitoring = MonitoringConnector; // Also expose as monitoring for clarity
|
|
73
74
|
this.mqClient = null;
|
|
74
75
|
this.registryClient = null;
|
|
75
76
|
this.orchestrator = null;
|
|
@@ -109,11 +110,14 @@ class ServiceWrapper {
|
|
|
109
110
|
*/
|
|
110
111
|
async start() {
|
|
111
112
|
if (this.isRunning) {
|
|
112
|
-
|
|
113
|
+
console.warn('Service wrapper already running');
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
try {
|
|
118
|
+
// Initialize monitoring first
|
|
119
|
+
await this.monitoring.init({ serviceName: this.serviceName });
|
|
120
|
+
|
|
117
121
|
this.logger.info(`Starting service wrapper for ${this.serviceName}`);
|
|
118
122
|
|
|
119
123
|
// 1. Initialize infrastructure connectors
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test monitoring integration in service-wrapper
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('ServiceWrapper Monitoring Integration', () => {
|
|
6
|
+
let ServiceWrapper;
|
|
7
|
+
let express;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Reset modules
|
|
11
|
+
jest.resetModules();
|
|
12
|
+
|
|
13
|
+
// Mock dependencies
|
|
14
|
+
jest.mock('@onlineapps/conn-infra-mq', () => {
|
|
15
|
+
return jest.fn().mockImplementation(() => ({
|
|
16
|
+
connect: jest.fn().mockResolvedValue(true),
|
|
17
|
+
disconnect: jest.fn().mockResolvedValue(true),
|
|
18
|
+
consume: jest.fn().mockResolvedValue(true),
|
|
19
|
+
isConnected: jest.fn().mockReturnValue(true)
|
|
20
|
+
}));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
jest.mock('@onlineapps/conn-orch-registry', () => ({
|
|
24
|
+
ServiceRegistryClient: jest.fn().mockImplementation(() => ({
|
|
25
|
+
register: jest.fn().mockResolvedValue(true),
|
|
26
|
+
unregister: jest.fn().mockResolvedValue(true),
|
|
27
|
+
sendHeartbeat: jest.fn().mockResolvedValue(true),
|
|
28
|
+
isConnected: jest.fn().mockReturnValue(true)
|
|
29
|
+
}))
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock('@onlineapps/conn-base-monitoring', () => ({
|
|
33
|
+
init: jest.fn().mockResolvedValue({
|
|
34
|
+
info: jest.fn(),
|
|
35
|
+
warn: jest.fn(),
|
|
36
|
+
error: jest.fn(),
|
|
37
|
+
debug: jest.fn()
|
|
38
|
+
}),
|
|
39
|
+
info: jest.fn(),
|
|
40
|
+
warn: jest.fn(),
|
|
41
|
+
error: jest.fn(),
|
|
42
|
+
debug: jest.fn()
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
jest.mock('@onlineapps/conn-orch-orchestrator', () => ({
|
|
46
|
+
create: jest.fn().mockReturnValue({
|
|
47
|
+
processWorkflowMessage: jest.fn().mockResolvedValue({ success: true })
|
|
48
|
+
})
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
jest.mock('@onlineapps/conn-orch-api-mapper', () => ({
|
|
52
|
+
create: jest.fn().mockReturnValue({})
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
jest.mock('@onlineapps/conn-orch-cookbook', () => ({}));
|
|
56
|
+
jest.mock('@onlineapps/conn-base-cache', () => jest.fn());
|
|
57
|
+
jest.mock('@onlineapps/conn-infra-error-handler', () => jest.fn());
|
|
58
|
+
|
|
59
|
+
ServiceWrapper = require('../src/ServiceWrapper');
|
|
60
|
+
express = require('express');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should initialize monitoring on start', async () => {
|
|
64
|
+
const app = express();
|
|
65
|
+
const monitoring = require('@onlineapps/conn-base-monitoring');
|
|
66
|
+
|
|
67
|
+
const wrapper = new ServiceWrapper({
|
|
68
|
+
service: app,
|
|
69
|
+
serviceName: 'test-service',
|
|
70
|
+
openApiSpec: { info: { version: '1.0.0' } },
|
|
71
|
+
config: {}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await wrapper.start();
|
|
75
|
+
|
|
76
|
+
expect(monitoring.init).toHaveBeenCalledWith({
|
|
77
|
+
serviceName: 'test-service'
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(wrapper.logger).toBeDefined();
|
|
81
|
+
expect(wrapper.monitoring).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should use monitoring for logging', async () => {
|
|
85
|
+
const app = express();
|
|
86
|
+
const wrapper = new ServiceWrapper({
|
|
87
|
+
service: app,
|
|
88
|
+
serviceName: 'test-service',
|
|
89
|
+
openApiSpec: { info: { version: '1.0.0' } },
|
|
90
|
+
config: {}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await wrapper.start();
|
|
94
|
+
|
|
95
|
+
// Test logger methods are available
|
|
96
|
+
expect(wrapper.logger.info).toBeDefined();
|
|
97
|
+
expect(wrapper.logger.error).toBeDefined();
|
|
98
|
+
expect(wrapper.logger.warn).toBeDefined();
|
|
99
|
+
expect(wrapper.logger.debug).toBeDefined();
|
|
100
|
+
|
|
101
|
+
// Test logger is used
|
|
102
|
+
wrapper.logger.info('Test message', { data: 'test' });
|
|
103
|
+
wrapper.logger.error('Error message', { error: 'test' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should pass monitoring to orchestrator', async () => {
|
|
107
|
+
const app = express();
|
|
108
|
+
const OrchestratorConnector = require('@onlineapps/conn-orch-orchestrator');
|
|
109
|
+
|
|
110
|
+
const wrapper = new ServiceWrapper({
|
|
111
|
+
service: app,
|
|
112
|
+
serviceName: 'test-service',
|
|
113
|
+
openApiSpec: { info: { version: '1.0.0' } },
|
|
114
|
+
config: {}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await wrapper.start();
|
|
118
|
+
|
|
119
|
+
expect(OrchestratorConnector.create).toHaveBeenCalledWith(
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
logger: expect.objectContaining({
|
|
122
|
+
info: expect.any(Function),
|
|
123
|
+
error: expect.any(Function),
|
|
124
|
+
warn: expect.any(Function),
|
|
125
|
+
debug: expect.any(Function)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should handle monitoring mode from config', async () => {
|
|
132
|
+
const app = express();
|
|
133
|
+
const monitoring = require('@onlineapps/conn-base-monitoring');
|
|
134
|
+
|
|
135
|
+
const wrapper = new ServiceWrapper({
|
|
136
|
+
service: app,
|
|
137
|
+
serviceName: 'test-service',
|
|
138
|
+
openApiSpec: { info: { version: '1.0.0' } },
|
|
139
|
+
config: {
|
|
140
|
+
monitoringMode: 'debug'
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await wrapper.start();
|
|
145
|
+
|
|
146
|
+
// Monitoring should be initialized but mode is not passed
|
|
147
|
+
// because ServiceWrapper doesn't pass it through yet
|
|
148
|
+
expect(monitoring.init).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
});
|