@onlineapps/service-wrapper 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module @onlineapps/service-wrapper
5
+ * @description Thin orchestration layer for microservices that handles all infrastructure concerns.
6
+ * Delegates all actual work to specialized connectors.
7
+ *
8
+ * @author OA Drive Team
9
+ * @license MIT
10
+ * @since 2.0.0
11
+ */
12
+
13
+ // Import connectors
14
+ const MQConnector = require('@onlineapps/conn-infra-mq');
15
+ const RegistryConnector = require('@onlineapps/conn-orch-registry');
16
+ const LoggerConnector = require('@onlineapps/conn-base-logger');
17
+ const OrchestratorConnector = require('@onlineapps/conn-orch-orchestrator');
18
+ const ApiMapperConnector = require('@onlineapps/conn-orch-api-mapper');
19
+ const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
20
+ const CacheConnector = require('@onlineapps/conn-base-cache');
21
+ const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
22
+
23
+ /**
24
+ * @class ServiceWrapper
25
+ * @description Thin wrapper that orchestrates all infrastructure concerns for a microservice.
26
+ * ALL actual functionality is delegated to specialized connectors.
27
+ *
28
+ * @example
29
+ * const wrapper = new ServiceWrapper({
30
+ * service: expressApp,
31
+ * serviceName: 'hello-service',
32
+ * openApiSpec: require('./openapi.json'),
33
+ * config: {
34
+ * rabbitmq: process.env.RABBITMQ_URL,
35
+ * redis: process.env.REDIS_HOST,
36
+ * registry: process.env.REGISTRY_URL
37
+ * }
38
+ * });
39
+ *
40
+ * await wrapper.start();
41
+ */
42
+ class ServiceWrapper {
43
+ /**
44
+ * Create a new ServiceWrapper instance
45
+ * @constructor
46
+ * @param {Object} options - Configuration options
47
+ * @param {Object} options.service - Express application instance
48
+ * @param {string} options.serviceName - Name of the service
49
+ * @param {Object} options.openApiSpec - OpenAPI specification
50
+ * @param {Object} [options.config={}] - Infrastructure configuration
51
+ * @param {string} [options.config.rabbitmq] - RabbitMQ connection URL
52
+ * @param {string} [options.config.redis] - Redis connection string
53
+ * @param {string} [options.config.registry] - Registry service URL
54
+ * @param {number} [options.config.port=3000] - Service port
55
+ * @param {number} [options.config.prefetch=10] - MQ prefetch count
56
+ *
57
+ * @throws {Error} If required options are missing
58
+ */
59
+ constructor(options = {}) {
60
+ this._validateOptions(options);
61
+
62
+ // Store configuration
63
+ this.service = options.service;
64
+ this.serviceName = options.serviceName;
65
+ this.openApiSpec = options.openApiSpec;
66
+ this.config = options.config || {};
67
+
68
+ // Initialize connectors
69
+ this.logger = LoggerConnector.create(this.serviceName);
70
+ this.mqClient = null;
71
+ this.registryClient = null;
72
+ this.orchestrator = null;
73
+ this.cacheConnector = null;
74
+
75
+ // State
76
+ this.isRunning = false;
77
+ }
78
+
79
+ /**
80
+ * Validate constructor options
81
+ * @private
82
+ * @param {Object} options - Options to validate
83
+ * @throws {Error} If required options are missing
84
+ */
85
+ _validateOptions(options) {
86
+ if (!options.service) {
87
+ throw new Error('Service (Express app) is required');
88
+ }
89
+ if (!options.serviceName) {
90
+ throw new Error('Service name is required');
91
+ }
92
+ if (!options.openApiSpec) {
93
+ throw new Error('OpenAPI specification is required');
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Start the service wrapper
99
+ * @async
100
+ * @method start
101
+ * @returns {Promise<void>}
102
+ *
103
+ * @example
104
+ * await wrapper.start();
105
+ * console.log('Service wrapper started');
106
+ */
107
+ async start() {
108
+ if (this.isRunning) {
109
+ this.logger.warn('Service wrapper already running');
110
+ return;
111
+ }
112
+
113
+ try {
114
+ this.logger.info(`Starting service wrapper for ${this.serviceName}`);
115
+
116
+ // 1. Initialize infrastructure connectors
117
+ await this._initializeConnectors();
118
+
119
+ // 2. Initialize cache connector if Redis is configured
120
+ if (this.config.redis) {
121
+ this.cacheConnector = new CacheConnector({
122
+ host: this.config.redis,
123
+ namespace: this.serviceName,
124
+ defaultTTL: this.config.cacheTTL || 300
125
+ });
126
+ await this.cacheConnector.connect();
127
+ this.logger.info('Cache connector initialized');
128
+ }
129
+
130
+ // 3. Initialize error handler
131
+ const errorHandler = new ErrorHandlerConnector({
132
+ maxRetries: this.config.maxRetries || 3,
133
+ retryDelay: this.config.retryDelay || 1000,
134
+ logger: this.logger
135
+ });
136
+
137
+ // 4. Create the orchestrator with all dependencies
138
+ this.orchestrator = OrchestratorConnector.create({
139
+ mqClient: this.mqClient,
140
+ registryClient: this.registryClient,
141
+ apiMapper: ApiMapperConnector.create({
142
+ openApiSpec: this.openApiSpec,
143
+ serviceUrl: `http://localhost:${this.config.port || 3000}`,
144
+ service: this.service,
145
+ directCall: true,
146
+ logger: this.logger
147
+ }),
148
+ cookbook: CookbookConnector,
149
+ cache: this.cacheConnector,
150
+ errorHandler: errorHandler,
151
+ logger: this.logger,
152
+ defaultTimeout: this.config.defaultTimeout || 30000
153
+ });
154
+
155
+ // 5. Register service with registry
156
+ await this._registerService();
157
+
158
+ // 6. Subscribe to workflow messages
159
+ await this._subscribeToQueues();
160
+
161
+ // 7. Start heartbeat
162
+ this._startHeartbeat();
163
+
164
+ this.isRunning = true;
165
+ this.logger.info(`Service wrapper started successfully for ${this.serviceName}`);
166
+
167
+ } catch (error) {
168
+ this.logger.error('Failed to start service wrapper', { error: error.message });
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Stop the service wrapper
175
+ * @async
176
+ * @method stop
177
+ * @returns {Promise<void>}
178
+ *
179
+ * @example
180
+ * await wrapper.stop();
181
+ * console.log('Service wrapper stopped');
182
+ */
183
+ async stop() {
184
+ if (!this.isRunning) {
185
+ this.logger.warn('Service wrapper not running');
186
+ return;
187
+ }
188
+
189
+ try {
190
+ this.logger.info(`Stopping service wrapper for ${this.serviceName}`);
191
+
192
+ // Stop heartbeat
193
+ if (this.heartbeatInterval) {
194
+ clearInterval(this.heartbeatInterval);
195
+ }
196
+
197
+ // Unregister from registry
198
+ if (this.registryClient) {
199
+ await this.registryClient.unregister(this.serviceName);
200
+ }
201
+
202
+ // Disconnect from cache
203
+ if (this.cacheConnector) {
204
+ await this.cacheConnector.disconnect();
205
+ }
206
+
207
+ // Disconnect from MQ
208
+ if (this.mqClient) {
209
+ await this.mqClient.disconnect();
210
+ }
211
+
212
+ this.isRunning = false;
213
+ this.logger.info(`Service wrapper stopped successfully for ${this.serviceName}`);
214
+
215
+ } catch (error) {
216
+ this.logger.error('Error stopping service wrapper', { error: error.message });
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Initialize infrastructure connectors
223
+ * @private
224
+ * @async
225
+ * @returns {Promise<void>}
226
+ */
227
+ async _initializeConnectors() {
228
+ // Initialize MQ client
229
+ this.mqClient = new MQConnector({
230
+ url: this.config.rabbitmq || process.env.RABBITMQ_URL || 'amqp://localhost:5672',
231
+ serviceName: this.serviceName,
232
+ prefetchCount: this.config.prefetch || 10,
233
+ logger: this.logger
234
+ });
235
+ await this.mqClient.connect();
236
+
237
+ // Initialize registry client
238
+ this.registryClient = new RegistryConnector.ServiceRegistryClient({
239
+ registryUrl: this.config.registry || process.env.REGISTRY_URL || 'http://localhost:4000',
240
+ logger: this.logger
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Register service with registry
246
+ * @private
247
+ * @async
248
+ * @returns {Promise<void>}
249
+ */
250
+ async _registerService() {
251
+ const serviceInfo = {
252
+ name: this.serviceName,
253
+ url: `http://localhost:${this.config.port || 3000}`,
254
+ openapi: this.openApiSpec,
255
+ metadata: {
256
+ version: this.openApiSpec.info?.version || '1.0.0',
257
+ description: this.openApiSpec.info?.description || ''
258
+ }
259
+ };
260
+
261
+ await this.registryClient.register(serviceInfo);
262
+ this.logger.info(`Service registered: ${this.serviceName}`);
263
+ }
264
+
265
+ /**
266
+ * Subscribe to workflow message queues
267
+ * @private
268
+ * @async
269
+ * @returns {Promise<void>}
270
+ */
271
+ async _subscribeToQueues() {
272
+ const queueName = `${this.serviceName}.workflow`;
273
+
274
+ // Subscribe to workflow messages
275
+ await this.mqClient.consume(
276
+ async (message) => {
277
+ try {
278
+ // Delegate ALL processing to orchestrator
279
+ const result = await this.orchestrator.processWorkflowMessage(
280
+ message,
281
+ this.serviceName
282
+ );
283
+
284
+ this.logger.info('Message processed successfully', {
285
+ workflow_id: message.workflow_id,
286
+ step: message.current_step,
287
+ result
288
+ });
289
+
290
+ } catch (error) {
291
+ this.logger.error('Message processing failed', {
292
+ workflow_id: message.workflow_id,
293
+ step: message.current_step,
294
+ error: error.message
295
+ });
296
+ }
297
+ },
298
+ { queue: queueName }
299
+ );
300
+
301
+ this.logger.info(`Subscribed to queue: ${queueName}`);
302
+ }
303
+
304
+ /**
305
+ * Start service heartbeat
306
+ * @private
307
+ */
308
+ _startHeartbeat() {
309
+ this.heartbeatInterval = setInterval(async () => {
310
+ try {
311
+ await this.registryClient.sendHeartbeat(this.serviceName);
312
+ this.logger.debug(`Heartbeat sent for ${this.serviceName}`);
313
+ } catch (error) {
314
+ this.logger.error('Heartbeat failed', { error: error.message });
315
+ }
316
+ }, this.config.heartbeatInterval || 30000);
317
+ }
318
+
319
+ /**
320
+ * Get wrapper status
321
+ * @method getStatus
322
+ * @returns {Object} Status information
323
+ *
324
+ * @example
325
+ * const status = wrapper.getStatus();
326
+ * console.log('Wrapper status:', status);
327
+ */
328
+ getStatus() {
329
+ return {
330
+ serviceName: this.serviceName,
331
+ isRunning: this.isRunning,
332
+ mqConnected: this.mqClient?.isConnected() || false,
333
+ registryConnected: this.registryClient?.isConnected() || false,
334
+ config: {
335
+ port: this.config.port || 3000,
336
+ prefetch: this.config.prefetch || 10,
337
+ heartbeatInterval: this.config.heartbeatInterval || 30000
338
+ }
339
+ };
340
+ }
341
+ }
342
+
343
+ module.exports = ServiceWrapper;
package/src/index.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module @onlineapps/service-wrapper
5
+ * @description Infrastructure orchestration for microservices.
6
+ * Thin layer that delegates all work to specialized connectors.
7
+ *
8
+ * @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/service-wrapper|GitHub Repository}
9
+ * @author OA Drive Team
10
+ * @license MIT
11
+ * @since 2.0.0
12
+ */
13
+
14
+ const ServiceWrapper = require('./ServiceWrapper');
15
+
16
+ // Note: WorkflowProcessor and ApiCaller functionality has been moved to connectors:
17
+ // - WorkflowProcessor -> @onlineapps/conn-orch-orchestrator
18
+ // - ApiCaller -> @onlineapps/conn-orch-api-mapper
19
+
20
+ module.exports = ServiceWrapper;
21
+ module.exports.ServiceWrapper = ServiceWrapper;
22
+ module.exports.default = ServiceWrapper;
23
+ module.exports.VERSION = '2.0.0';