@hiliosai/sdk 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/configs/constants.ts +13 -0
- package/src/service/define-integration.ts +142 -83
- package/src/service/define-service.ts +6 -2
- package/src/types/channels.ts +60 -0
- package/src/types/context.ts +22 -2
- package/src/types/index.ts +1 -0
- package/src/types/integration.ts +0 -1
- package/src/types/message.ts +2 -3
- package/src/types/service.ts +34 -31
- package/src/types/user.ts +2 -2
- package/INTEGRATION_SERVICE.md +0 -394
- package/src/service/example-user/datasources/index.ts +0 -8
- package/src/service/example-user/datasources/user.datasource.ts +0 -7
- package/src/service/example-user/user.service.ts +0 -23
- package/src/service/example-user/utils.ts +0 -0
package/package.json
CHANGED
package/src/configs/constants.ts
CHANGED
|
@@ -66,6 +66,19 @@ export const CHANNELS = {
|
|
|
66
66
|
},
|
|
67
67
|
} as const;
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Integration-specific channel names
|
|
71
|
+
*/
|
|
72
|
+
export const INTEGRATION_CHANNELS = {
|
|
73
|
+
// Message events
|
|
74
|
+
MESSAGE_RECEIVED: `${NAMESPACE}.processing.message.received`,
|
|
75
|
+
MESSAGE_SENT: `${NAMESPACE}.processing.message.sent`,
|
|
76
|
+
MESSAGE_FAILED: `${NAMESPACE}.processing.message.failed`,
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
79
|
+
export type IntegrationChannelName =
|
|
80
|
+
(typeof INTEGRATION_CHANNELS)[keyof typeof INTEGRATION_CHANNELS];
|
|
81
|
+
|
|
69
82
|
/**
|
|
70
83
|
* Channel Configuration Constants
|
|
71
84
|
*/
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {CHANNEL_CONFIG, INTEGRATION_CHANNELS} from '../configs/constants';
|
|
4
|
+
import type {DatasourceConstructorRegistry} from '../middlewares/datasource.middleware';
|
|
5
|
+
import {
|
|
6
|
+
type IntegrationMessageFailedPayload,
|
|
7
|
+
type IntegrationMessageReceivedPayload,
|
|
8
|
+
type IntegrationMessageSentPayload,
|
|
9
|
+
} from '../types/channels';
|
|
4
10
|
import type {AppContext} from '../types/context';
|
|
5
11
|
import type {IntegrationConfig} from '../types/integration';
|
|
6
12
|
import type {
|
|
7
13
|
NormalizedMessage,
|
|
8
|
-
PlatformMessage,
|
|
9
14
|
SendResult,
|
|
10
15
|
WebhookEvent,
|
|
11
16
|
} from '../types/message';
|
|
17
|
+
import type {DatasourceInstanceTypes} from '../types/datasource';
|
|
12
18
|
import type {
|
|
13
19
|
IntegrationServiceConfig,
|
|
14
20
|
IntegrationServiceSchema,
|
|
15
21
|
} from '../types/service';
|
|
22
|
+
import {defineService} from './define-service';
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Security helpers for webhook validation
|
|
@@ -38,13 +45,6 @@ const SecurityHelpers = {
|
|
|
38
45
|
validateTimestamp(timestamp: number, maxAgeMs = 5 * 60 * 1000): boolean {
|
|
39
46
|
return Date.now() - timestamp <= maxAgeMs;
|
|
40
47
|
},
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Generate correlation ID for request tracking
|
|
44
|
-
*/
|
|
45
|
-
generateCorrelationId(): string {
|
|
46
|
-
return crypto.randomUUID();
|
|
47
|
-
},
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -76,37 +76,52 @@ async function executeWithRetry<T>(
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function defineIntegration<
|
|
79
|
-
TPlatformMessage extends PlatformMessage = PlatformMessage,
|
|
80
79
|
TSettings = unknown,
|
|
81
|
-
|
|
80
|
+
TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry
|
|
82
81
|
>(
|
|
83
|
-
config: IntegrationServiceConfig<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
): IntegrationServiceSchema<TPlatformMessage, TSettings> {
|
|
82
|
+
config: IntegrationServiceConfig<TSettings, TDatasourceConstructors>
|
|
83
|
+
): IntegrationServiceSchema<TSettings> {
|
|
84
|
+
// Type alias for inferred datasource instances
|
|
85
|
+
type TDatasources = DatasourceInstanceTypes<TDatasourceConstructors>;
|
|
86
|
+
|
|
89
87
|
// Create actions from integration methods
|
|
90
88
|
const actions = {
|
|
91
89
|
i_receiveWebhook: {
|
|
92
90
|
rest: {
|
|
93
91
|
method: 'POST' as const,
|
|
94
|
-
path: '/:
|
|
92
|
+
path: '/:channelId',
|
|
95
93
|
},
|
|
96
94
|
params: {
|
|
97
|
-
|
|
95
|
+
channelId: 'string',
|
|
98
96
|
payload: 'object',
|
|
99
97
|
headers: 'object',
|
|
100
98
|
timestamp: 'number',
|
|
101
99
|
},
|
|
102
100
|
async handler(
|
|
103
|
-
ctx: AppContext<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
ctx: AppContext<
|
|
102
|
+
TDatasources,
|
|
103
|
+
{
|
|
104
|
+
channelId: string;
|
|
105
|
+
payload: object;
|
|
106
|
+
headers: object;
|
|
107
|
+
timestamp: number;
|
|
108
|
+
}
|
|
109
|
+
>
|
|
110
|
+
): Promise<{success: boolean; messages: number; failed: number}> {
|
|
111
|
+
const {channelId, payload, headers, timestamp} = ctx.params;
|
|
112
|
+
|
|
113
|
+
// TODO: Look up channel configuration by channelId
|
|
114
|
+
// const channel = await getChannelConfig(channelId);
|
|
115
|
+
// For now, we'll construct webhook from meta
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
const webhook: WebhookEvent = {
|
|
118
|
+
tenantId: ctx.meta.tenantId ?? 'unknown', // Should come from channel lookup
|
|
119
|
+
channelId,
|
|
120
|
+
platform: config.spec.platform,
|
|
121
|
+
payload, // Raw webhook payload from gateway
|
|
122
|
+
headers: headers as Record<string, string>,
|
|
123
|
+
timestamp,
|
|
124
|
+
};
|
|
110
125
|
|
|
111
126
|
// Validate timestamp to prevent replay attacks
|
|
112
127
|
if (!SecurityHelpers.validateTimestamp(webhook.timestamp)) {
|
|
@@ -130,25 +145,49 @@ export function defineIntegration<
|
|
|
130
145
|
}
|
|
131
146
|
|
|
132
147
|
// Normalize the message
|
|
133
|
-
const normalizedMessages
|
|
134
|
-
|
|
148
|
+
const normalizedMessages = await config.normalize(webhook);
|
|
149
|
+
|
|
150
|
+
// Process each message with reliable delivery (parallel)
|
|
151
|
+
const results = await Promise.allSettled(
|
|
152
|
+
normalizedMessages.map(async (message) => {
|
|
153
|
+
const payload: IntegrationMessageReceivedPayload = {
|
|
154
|
+
tenantId: webhook.tenantId,
|
|
155
|
+
channelId: webhook.channelId,
|
|
156
|
+
platform: webhook.platform,
|
|
157
|
+
message,
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Send to reliable message channel (NATS JetStream)
|
|
162
|
+
return ctx.broker.sendToChannel(
|
|
163
|
+
INTEGRATION_CHANNELS.MESSAGE_RECEIVED,
|
|
164
|
+
payload,
|
|
165
|
+
CHANNEL_CONFIG.HIGH_PRIORITY
|
|
166
|
+
);
|
|
167
|
+
})
|
|
135
168
|
);
|
|
136
169
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
// Count successful vs failed messages
|
|
171
|
+
const successful = results.filter(
|
|
172
|
+
(result) => result.status === 'fulfilled'
|
|
173
|
+
).length;
|
|
174
|
+
const failed = results.filter(
|
|
175
|
+
(result) => result.status === 'rejected'
|
|
176
|
+
).length;
|
|
177
|
+
|
|
178
|
+
// Log failures for monitoring (but don't fail the entire webhook)
|
|
179
|
+
if (failed > 0) {
|
|
180
|
+
const failures = results
|
|
181
|
+
.filter((result) => result.status === 'rejected')
|
|
182
|
+
.map((result) => (result as PromiseRejectedResult).reason);
|
|
183
|
+
|
|
184
|
+
ctx.broker.logger.warn(
|
|
185
|
+
`Webhook processing partial failure: ${failed}/${normalizedMessages.length} messages failed`,
|
|
186
|
+
{failures}
|
|
187
|
+
);
|
|
149
188
|
}
|
|
150
189
|
|
|
151
|
-
return {success: true, messages:
|
|
190
|
+
return {success: true, messages: successful, failed};
|
|
152
191
|
},
|
|
153
192
|
},
|
|
154
193
|
|
|
@@ -159,20 +198,26 @@ export function defineIntegration<
|
|
|
159
198
|
},
|
|
160
199
|
params: {
|
|
161
200
|
message: 'object',
|
|
162
|
-
|
|
201
|
+
channelId: 'string',
|
|
163
202
|
},
|
|
164
|
-
async handler(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
203
|
+
async handler(
|
|
204
|
+
ctx: AppContext<
|
|
205
|
+
TDatasources,
|
|
206
|
+
{
|
|
207
|
+
message: NormalizedMessage;
|
|
208
|
+
channelId: string;
|
|
209
|
+
}
|
|
210
|
+
>
|
|
211
|
+
): Promise<SendResult> {
|
|
212
|
+
const {message, channelId} = ctx.params;
|
|
169
213
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
// Load channel configuration
|
|
215
|
+
const integrationConfig = await config.getChannelConfig(ctx, channelId);
|
|
216
|
+
if (!integrationConfig) {
|
|
217
|
+
throw new Error(`Channel configuration not found: ${channelId}`);
|
|
173
218
|
}
|
|
174
219
|
|
|
175
|
-
// Send the message with retry mechanism
|
|
220
|
+
// Send the message with retry mechanism (transformation happens inside sendMessage)
|
|
176
221
|
const result: SendResult = await executeWithRetry(
|
|
177
222
|
() => config.sendMessage(ctx, message, integrationConfig),
|
|
178
223
|
3,
|
|
@@ -180,18 +225,49 @@ export function defineIntegration<
|
|
|
180
225
|
`Send message via ${config.spec.platform}`
|
|
181
226
|
);
|
|
182
227
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
228
|
+
// Send reliable events based on result (with error handling)
|
|
229
|
+
try {
|
|
230
|
+
if (result.success) {
|
|
231
|
+
const sentPayload: IntegrationMessageSentPayload = {
|
|
232
|
+
tenantId: integrationConfig.tenantId,
|
|
233
|
+
channelId,
|
|
234
|
+
platform: config.spec.platform,
|
|
235
|
+
messageId: result.messageId,
|
|
236
|
+
metadata: result.metadata,
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await ctx.broker.sendToChannel(
|
|
241
|
+
INTEGRATION_CHANNELS.MESSAGE_SENT,
|
|
242
|
+
sentPayload,
|
|
243
|
+
CHANNEL_CONFIG.DEFAULTS
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
const failedPayload: IntegrationMessageFailedPayload = {
|
|
247
|
+
tenantId: integrationConfig.tenantId,
|
|
248
|
+
channelId,
|
|
249
|
+
platform: config.spec.platform,
|
|
250
|
+
error: result.error?.message ?? 'Unknown error',
|
|
251
|
+
message,
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
await ctx.broker.sendToChannel(
|
|
256
|
+
INTEGRATION_CHANNELS.MESSAGE_FAILED,
|
|
257
|
+
failedPayload,
|
|
258
|
+
CHANNEL_CONFIG.DEFAULTS
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
} catch (channelError: unknown) {
|
|
262
|
+
// Log channel error but don't fail the send operation
|
|
263
|
+
const err =
|
|
264
|
+
channelError instanceof Error
|
|
265
|
+
? channelError
|
|
266
|
+
: new Error(String(channelError));
|
|
267
|
+
ctx.broker.logger.warn('Failed to send message event to channel', {
|
|
268
|
+
error: err.message,
|
|
186
269
|
messageId: result.messageId,
|
|
187
270
|
platform: config.spec.platform,
|
|
188
|
-
metadata: result.metadata,
|
|
189
|
-
});
|
|
190
|
-
} else {
|
|
191
|
-
ctx.emit('integration.message.failed', {
|
|
192
|
-
error: result.error,
|
|
193
|
-
platform: config.spec.platform,
|
|
194
|
-
message,
|
|
195
271
|
});
|
|
196
272
|
}
|
|
197
273
|
|
|
@@ -267,23 +343,6 @@ export function defineIntegration<
|
|
|
267
343
|
},
|
|
268
344
|
},
|
|
269
345
|
|
|
270
|
-
i_status: {
|
|
271
|
-
rest: {
|
|
272
|
-
method: 'GET' as const,
|
|
273
|
-
path: '/status',
|
|
274
|
-
},
|
|
275
|
-
handler(): object {
|
|
276
|
-
return {
|
|
277
|
-
id: config.spec.id,
|
|
278
|
-
name: config.spec.name,
|
|
279
|
-
platform: config.spec.platform,
|
|
280
|
-
version: config.spec.version,
|
|
281
|
-
status: config.spec.status,
|
|
282
|
-
capabilities: config.spec.capabilities,
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
|
|
287
346
|
i_validateCredentials: {
|
|
288
347
|
params: {
|
|
289
348
|
credentials: 'object',
|
|
@@ -305,7 +364,7 @@ export function defineIntegration<
|
|
|
305
364
|
}
|
|
306
365
|
|
|
307
366
|
// Use defineService to get all the mixins and proper setup
|
|
308
|
-
const baseService = defineService<
|
|
367
|
+
const baseService = defineService<TSettings, TDatasourceConstructors>({
|
|
309
368
|
name: config.name,
|
|
310
369
|
version: config.version,
|
|
311
370
|
settings: config.settings as TSettings,
|
|
@@ -320,12 +379,12 @@ export function defineIntegration<
|
|
|
320
379
|
methods: {
|
|
321
380
|
...(config.methods ?? {}),
|
|
322
381
|
// Add integration-specific methods to the service methods (filter out undefined)
|
|
323
|
-
//
|
|
382
|
+
// Required methods - no need for conditional
|
|
324
383
|
normalize: config.normalize,
|
|
325
|
-
|
|
326
|
-
...(config.validateWebhook && {validateWebhook: config.validateWebhook}),
|
|
327
|
-
// sendMessage is required, no need for conditional
|
|
384
|
+
getChannelConfig: config.getChannelConfig,
|
|
328
385
|
sendMessage: config.sendMessage,
|
|
386
|
+
// Optional methods
|
|
387
|
+
...(config.validateWebhook && {validateWebhook: config.validateWebhook}),
|
|
329
388
|
...(config.verifyWebhook && {verifyWebhook: config.verifyWebhook}),
|
|
330
389
|
...(config.checkHealth && {checkHealth: config.checkHealth}),
|
|
331
390
|
...(config.validateCredentials && {
|
|
@@ -345,5 +404,5 @@ export function defineIntegration<
|
|
|
345
404
|
...baseService,
|
|
346
405
|
// Only add the integration spec
|
|
347
406
|
spec: config.spec,
|
|
348
|
-
} as IntegrationServiceSchema<
|
|
407
|
+
} as IntegrationServiceSchema<TSettings>;
|
|
349
408
|
}
|
|
@@ -2,6 +2,7 @@ import omit from 'lodash/omit';
|
|
|
2
2
|
import type {ServiceSchema as MoleculerServiceSchema} from 'moleculer';
|
|
3
3
|
|
|
4
4
|
import {MemoizeMixin} from '../middlewares';
|
|
5
|
+
import type {DatasourceConstructorRegistry} from '../middlewares/datasource.middleware';
|
|
5
6
|
import {DatasourceMixin} from '../mixins';
|
|
6
7
|
import type {ServiceConfig} from '../types/service';
|
|
7
8
|
|
|
@@ -34,8 +35,11 @@ import type {ServiceConfig} from '../types/service';
|
|
|
34
35
|
* });
|
|
35
36
|
* ```
|
|
36
37
|
*/
|
|
37
|
-
export function defineService<
|
|
38
|
-
|
|
38
|
+
export function defineService<
|
|
39
|
+
TSettings = unknown,
|
|
40
|
+
TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry
|
|
41
|
+
>(
|
|
42
|
+
config: ServiceConfig<TSettings, TDatasourceConstructors>
|
|
39
43
|
): MoleculerServiceSchema<TSettings> {
|
|
40
44
|
const propsToOmit = ['datasources'];
|
|
41
45
|
const serviceSchema = omit(
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {IntegrationPlatform} from './platform';
|
|
2
|
+
import type {NormalizedMessage} from './message';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Channel event payload types for reliable messaging via @moleculer/channels
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Integration message events
|
|
9
|
+
export interface IntegrationMessageReceivedPayload {
|
|
10
|
+
tenantId: string;
|
|
11
|
+
channelId: string;
|
|
12
|
+
platform: IntegrationPlatform;
|
|
13
|
+
message: NormalizedMessage;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IntegrationMessageSentPayload {
|
|
19
|
+
tenantId: string;
|
|
20
|
+
channelId: string;
|
|
21
|
+
platform: IntegrationPlatform;
|
|
22
|
+
messageId?: string;
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IntegrationMessageFailedPayload {
|
|
28
|
+
tenantId: string;
|
|
29
|
+
channelId: string;
|
|
30
|
+
platform: IntegrationPlatform;
|
|
31
|
+
error: string; // Serialized error message
|
|
32
|
+
message?: NormalizedMessage;
|
|
33
|
+
timestamp: number;
|
|
34
|
+
retryCount?: number;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// System events
|
|
39
|
+
export interface IntegrationRegisteredPayload {
|
|
40
|
+
tenantId: string;
|
|
41
|
+
channelId: string;
|
|
42
|
+
platform: IntegrationPlatform;
|
|
43
|
+
config: Record<string, unknown>;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IntegrationUnregisteredPayload {
|
|
48
|
+
tenantId: string;
|
|
49
|
+
channelId: string;
|
|
50
|
+
platform: IntegrationPlatform;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Re-export constants to avoid duplication
|
|
55
|
+
export {
|
|
56
|
+
CHANNELS,
|
|
57
|
+
INTEGRATION_CHANNELS,
|
|
58
|
+
NAMESPACE,
|
|
59
|
+
type IntegrationChannelName,
|
|
60
|
+
} from '../configs/constants';
|
package/src/types/context.ts
CHANGED
|
@@ -2,15 +2,32 @@ import type {Context} from 'moleculer';
|
|
|
2
2
|
import type {Tenant} from './tenant';
|
|
3
3
|
import type {User} from './user';
|
|
4
4
|
|
|
5
|
+
// Moleculer Channels types
|
|
6
|
+
export interface ChannelSendOptions {
|
|
7
|
+
group?: string;
|
|
8
|
+
maxInFlight?: number;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
deadLettering?: {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
queueName?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SendToChannelMethod {
|
|
17
|
+
(
|
|
18
|
+
channelName: string,
|
|
19
|
+
payload: unknown,
|
|
20
|
+
options?: ChannelSendOptions
|
|
21
|
+
): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
export interface AppMeta {
|
|
6
25
|
user?: User;
|
|
7
26
|
tenantId?: string;
|
|
8
27
|
tenantName?: string;
|
|
9
28
|
userId?: string;
|
|
10
|
-
integrationId?: string;
|
|
11
29
|
channelId?: string;
|
|
12
30
|
requestId?: string;
|
|
13
|
-
correlationId?: string;
|
|
14
31
|
userAgent?: string;
|
|
15
32
|
clientIP?: string;
|
|
16
33
|
[key: string]: unknown;
|
|
@@ -41,4 +58,7 @@ export type AppContext<
|
|
|
41
58
|
> = Context<TParams, TMeta, TLocals> &
|
|
42
59
|
PermissionHelpers & {
|
|
43
60
|
datasources: TDatasources;
|
|
61
|
+
broker: Context['broker'] & {
|
|
62
|
+
sendToChannel: SendToChannelMethod;
|
|
63
|
+
};
|
|
44
64
|
};
|
package/src/types/index.ts
CHANGED
package/src/types/integration.ts
CHANGED
package/src/types/message.ts
CHANGED
|
@@ -77,12 +77,11 @@ export interface PlatformMessage {
|
|
|
77
77
|
payload: any;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export interface WebhookEvent {
|
|
80
|
+
export interface WebhookEvent<TPayload = any> {
|
|
81
81
|
tenantId: string;
|
|
82
82
|
channelId: string;
|
|
83
|
-
integrationId: string;
|
|
84
83
|
platform: IntegrationPlatform;
|
|
85
|
-
payload:
|
|
84
|
+
payload: TPayload;
|
|
86
85
|
headers: Record<string, string>;
|
|
87
86
|
timestamp: number;
|
|
88
87
|
}
|
package/src/types/service.ts
CHANGED
|
@@ -9,13 +9,10 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
import type {DatasourceConstructorRegistry} from '../middlewares/datasource.middleware';
|
|
11
11
|
import type {AppContext} from './context';
|
|
12
|
+
import type {DatasourceInstanceTypes} from './datasource';
|
|
12
13
|
import type {BaseSpec, IntegrationConfig} from './integration';
|
|
13
|
-
import type {
|
|
14
|
-
|
|
15
|
-
PlatformMessage,
|
|
16
|
-
SendResult,
|
|
17
|
-
WebhookEvent,
|
|
18
|
-
} from './message';
|
|
14
|
+
import type {NormalizedMessage, SendResult, WebhookEvent} from './message';
|
|
15
|
+
|
|
19
16
|
|
|
20
17
|
// Type to infer TypeScript types from Moleculer parameter schemas
|
|
21
18
|
type InferParamsType<T> = T extends Record<string, any>
|
|
@@ -121,28 +118,34 @@ export interface ServiceSchema<TSettings = unknown, TDatasources = unknown>
|
|
|
121
118
|
// ServiceConfig is what users provide to defineService
|
|
122
119
|
export type ServiceConfig<
|
|
123
120
|
TSettings = unknown,
|
|
124
|
-
|
|
125
|
-
> = ServiceSchema<
|
|
126
|
-
|
|
121
|
+
TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry
|
|
122
|
+
> = ServiceSchema<
|
|
123
|
+
TSettings,
|
|
124
|
+
DatasourceInstanceTypes<TDatasourceConstructors>
|
|
125
|
+
> & {
|
|
126
|
+
datasources?: TDatasourceConstructors;
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
// Integration-specific types
|
|
130
130
|
export interface IntegrationServiceConfig<
|
|
131
|
-
TPlatformMessage extends PlatformMessage = PlatformMessage,
|
|
132
131
|
TSettings = unknown,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry,
|
|
133
|
+
TContext extends AppContext<
|
|
134
|
+
DatasourceInstanceTypes<TDatasourceConstructors>
|
|
135
|
+
> = AppContext<DatasourceInstanceTypes<TDatasourceConstructors>>
|
|
136
|
+
> extends ServiceConfig<TSettings, TDatasourceConstructors> {
|
|
136
137
|
name: string;
|
|
137
138
|
spec: BaseSpec;
|
|
138
139
|
|
|
139
140
|
// Core integration methods
|
|
140
|
-
normalize
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
normalize<TPayload = any>(
|
|
142
|
+
webhook: WebhookEvent<TPayload>
|
|
143
|
+
): Promise<NormalizedMessage[]>;
|
|
144
|
+
getChannelConfig(
|
|
145
|
+
ctx: TContext,
|
|
146
|
+
channelId: string
|
|
147
|
+
): Promise<IntegrationConfig | null>;
|
|
148
|
+
validateWebhook?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
|
|
146
149
|
sendMessage(
|
|
147
150
|
ctx: TContext,
|
|
148
151
|
message: NormalizedMessage,
|
|
@@ -170,20 +173,20 @@ export interface IntegrationServiceConfig<
|
|
|
170
173
|
validateCredentials?(credentials: Record<string, string>): Promise<boolean>;
|
|
171
174
|
|
|
172
175
|
// Optional signature validation
|
|
173
|
-
validateSignature
|
|
176
|
+
validateSignature?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
export interface IntegrationServiceSchema<
|
|
177
|
-
|
|
178
|
-
TSettings = unknown
|
|
179
|
-
> extends MoleculerServiceSchema<TSettings> {
|
|
179
|
+
export interface IntegrationServiceSchema<TSettings = unknown>
|
|
180
|
+
extends MoleculerServiceSchema<TSettings> {
|
|
180
181
|
spec: BaseSpec;
|
|
181
|
-
normalize
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
normalize<TPayload = any>(
|
|
183
|
+
webhook: WebhookEvent<TPayload>
|
|
184
|
+
): Promise<NormalizedMessage[]>;
|
|
185
|
+
getChannelConfig(
|
|
186
|
+
ctx: AppContext,
|
|
187
|
+
channelId: string
|
|
188
|
+
): Promise<IntegrationConfig | null>;
|
|
189
|
+
validateWebhook?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
|
|
187
190
|
sendMessage(
|
|
188
191
|
ctx: AppContext,
|
|
189
192
|
message: NormalizedMessage,
|
|
@@ -203,5 +206,5 @@ export interface IntegrationServiceSchema<
|
|
|
203
206
|
details?: Record<string, any>;
|
|
204
207
|
}>;
|
|
205
208
|
validateCredentials?(credentials: Record<string, string>): Promise<boolean>;
|
|
206
|
-
validateSignature
|
|
209
|
+
validateSignature?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
|
|
207
210
|
}
|
package/src/types/user.ts
CHANGED
package/INTEGRATION_SERVICE.md
DELETED
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
# Integration Service Architecture
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Integration Services are specialized Moleculer services that provide a standardized way to connect external messaging platforms (WhatsApp, Telegram, Discord, etc.) with the application. They handle webhook reception, message normalization, and platform-specific communication patterns.
|
|
6
|
-
|
|
7
|
-
## Purpose
|
|
8
|
-
|
|
9
|
-
The Integration Service pattern solves several key challenges:
|
|
10
|
-
|
|
11
|
-
1. **Platform Abstraction**: Provides unified interface for different messaging platforms
|
|
12
|
-
2. **Webhook Security**: Implements signature validation and replay attack prevention
|
|
13
|
-
3. **Message Normalization**: Converts platform-specific messages to unified format
|
|
14
|
-
4. **Retry Logic**: Handles transient failures with exponential backoff
|
|
15
|
-
5. **Event Broadcasting**: Emits standardized events for message processing
|
|
16
|
-
6. **Health Monitoring**: Provides health checks and status reporting
|
|
17
|
-
|
|
18
|
-
## Architecture
|
|
19
|
-
|
|
20
|
-
```mermaid
|
|
21
|
-
graph TB
|
|
22
|
-
subgraph "External Platforms"
|
|
23
|
-
WA[WhatsApp]
|
|
24
|
-
TG[Telegram]
|
|
25
|
-
DC[Discord]
|
|
26
|
-
SL[Slack]
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
subgraph "Integration Layer"
|
|
30
|
-
subgraph "Integration Services"
|
|
31
|
-
WAS[WhatsApp Service]
|
|
32
|
-
TGS[Telegram Service]
|
|
33
|
-
DCS[Discord Service]
|
|
34
|
-
SLS[Slack Service]
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
subgraph "Service Actions"
|
|
38
|
-
RW[i_receiveWebhook]
|
|
39
|
-
SM[i_sendMessage]
|
|
40
|
-
HC[i_healthCheck]
|
|
41
|
-
VW[i_verifyWebhook]
|
|
42
|
-
ST[i_status]
|
|
43
|
-
VC[i_validateCredentials]
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
subgraph "Core Application"
|
|
48
|
-
subgraph "Message Processing"
|
|
49
|
-
MP[Message Processor]
|
|
50
|
-
MR[Message Router]
|
|
51
|
-
MS[Message Store]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
subgraph "Event System"
|
|
55
|
-
EB[Event Bus]
|
|
56
|
-
EH[Event Handlers]
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
subgraph "Infrastructure"
|
|
61
|
-
DS[Datasources]
|
|
62
|
-
CA[Cache]
|
|
63
|
-
PE[Permissions]
|
|
64
|
-
CH[Context Helpers]
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
%% External webhook flows
|
|
68
|
-
WA -->|Webhook| WAS
|
|
69
|
-
TG -->|Webhook| TGS
|
|
70
|
-
DC -->|Webhook| DCS
|
|
71
|
-
SL -->|Webhook| SLS
|
|
72
|
-
|
|
73
|
-
%% Integration service actions
|
|
74
|
-
WAS --> RW
|
|
75
|
-
WAS --> SM
|
|
76
|
-
WAS --> HC
|
|
77
|
-
|
|
78
|
-
%% Service infrastructure
|
|
79
|
-
WAS --> DS
|
|
80
|
-
TGS --> CA
|
|
81
|
-
DCS --> PE
|
|
82
|
-
SLS --> CH
|
|
83
|
-
|
|
84
|
-
%% Event flows
|
|
85
|
-
RW -->|integration.message.received| EB
|
|
86
|
-
SM -->|integration.message.sent| EB
|
|
87
|
-
SM -->|integration.message.failed| EB
|
|
88
|
-
|
|
89
|
-
%% Processing flows
|
|
90
|
-
EB --> EH
|
|
91
|
-
EH --> MP
|
|
92
|
-
MP --> MR
|
|
93
|
-
MR --> MS
|
|
94
|
-
|
|
95
|
-
%% Response flows
|
|
96
|
-
MP -->|Send Response| SM
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Integration Service Structure
|
|
100
|
-
|
|
101
|
-
### Core Components
|
|
102
|
-
|
|
103
|
-
1. **defineIntegration()** - Factory function that creates integration services
|
|
104
|
-
2. **Security Helpers** - Webhook validation, timestamp checking, correlation IDs
|
|
105
|
-
3. **Retry Mechanism** - Exponential backoff for failed operations
|
|
106
|
-
4. **Standard Actions** - Predefined actions for common integration operations
|
|
107
|
-
|
|
108
|
-
### Standard Actions
|
|
109
|
-
|
|
110
|
-
All integration services automatically get these actions:
|
|
111
|
-
|
|
112
|
-
| Action | REST Endpoint | Purpose |
|
|
113
|
-
| ----------------------- | ---------------- | ------------------------------------- |
|
|
114
|
-
| `i_receiveWebhook` | `POST /webhook` | Receive and process incoming webhooks |
|
|
115
|
-
| `i_sendMessage` | `POST /send` | Send messages to external platform |
|
|
116
|
-
| `i_healthCheck` | `GET /health` | Check integration health status |
|
|
117
|
-
| `i_verifyWebhook` | `GET /webhook` | Verify webhook subscriptions |
|
|
118
|
-
| `i_status` | `GET /status` | Get integration status information |
|
|
119
|
-
| `i_validateCredentials` | `POST /validate` | Validate platform credentials |
|
|
120
|
-
|
|
121
|
-
### Service Methods
|
|
122
|
-
|
|
123
|
-
Integration-specific logic is implemented as service methods:
|
|
124
|
-
|
|
125
|
-
| Method | Purpose | Required |
|
|
126
|
-
| ----------------------- | --------------------------------------------- | -------- |
|
|
127
|
-
| `normalize()` | Convert platform message to unified format | ✅ |
|
|
128
|
-
| `sendMessage()` | Send message to platform | ✅ |
|
|
129
|
-
| `transform()` | Transform outbound message to platform format | ❌ |
|
|
130
|
-
| `validateWebhook()` | Validate webhook payload structure | ❌ |
|
|
131
|
-
| `validateSignature()` | Validate webhook signature | ❌ |
|
|
132
|
-
| `verifyWebhook()` | Handle webhook verification challenge | ❌ |
|
|
133
|
-
| `checkHealth()` | Custom health check logic | ❌ |
|
|
134
|
-
| `validateCredentials()` | Validate platform credentials | ❌ |
|
|
135
|
-
|
|
136
|
-
## Usage Example
|
|
137
|
-
|
|
138
|
-
### Basic WhatsApp Integration
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
import {defineIntegration} from '@pkg/sdk';
|
|
142
|
-
import type {BaseSpec, NormalizedMessage, WebhookEvent} from '@pkg/sdk';
|
|
143
|
-
|
|
144
|
-
const whatsappIntegration: BaseSpec = {
|
|
145
|
-
id: 'whatsapp-business',
|
|
146
|
-
name: 'WhatsApp Business',
|
|
147
|
-
platform: 'whatsapp',
|
|
148
|
-
version: '1.0.0',
|
|
149
|
-
status: 'ACTIVE',
|
|
150
|
-
capabilities: ['SEND_MESSAGE', 'RECEIVE_MESSAGE', 'WEBHOOK_VALIDATION']
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
export default defineIntegration({
|
|
154
|
-
name: 'whatsapp',
|
|
155
|
-
spec: whatsappIntegration,
|
|
156
|
-
|
|
157
|
-
// Required: Convert WhatsApp webhook to normalized message
|
|
158
|
-
async normalize(webhook: WebhookEvent): Promise<NormalizedMessage[]> {
|
|
159
|
-
const messages: NormalizedMessage[] = [];
|
|
160
|
-
|
|
161
|
-
for (const entry of webhook.payload.entry || []) {
|
|
162
|
-
for (const change of entry.changes || []) {
|
|
163
|
-
if (change.field === 'messages') {
|
|
164
|
-
for (const message of change.value.messages || []) {
|
|
165
|
-
messages.push({
|
|
166
|
-
id: message.id,
|
|
167
|
-
conversationId: message.from,
|
|
168
|
-
from: {
|
|
169
|
-
id: message.from,
|
|
170
|
-
name: change.value.contacts?.[0]?.profile?.name,
|
|
171
|
-
},
|
|
172
|
-
to: {
|
|
173
|
-
id: webhook.tenantId,
|
|
174
|
-
},
|
|
175
|
-
content: {
|
|
176
|
-
type: message.type === 'text' ? 'TEXT' : 'FILE',
|
|
177
|
-
text: message.text?.body,
|
|
178
|
-
media: message.image ? {
|
|
179
|
-
url: message.image.id,
|
|
180
|
-
mimeType: message.image.mime_type
|
|
181
|
-
} : undefined
|
|
182
|
-
},
|
|
183
|
-
timestamp: parseInt(message.timestamp) * 1000,
|
|
184
|
-
platform: 'whatsapp',
|
|
185
|
-
metadata: {
|
|
186
|
-
phoneNumberId: change.value.metadata.phone_number_id
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return messages;
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
// Required: Send message to WhatsApp
|
|
198
|
-
async sendMessage(ctx, message, config) {
|
|
199
|
-
const phoneNumberId = config.credentials.phoneNumberId;
|
|
200
|
-
const accessToken = config.credentials.accessToken;
|
|
201
|
-
|
|
202
|
-
const response = await fetch(
|
|
203
|
-
`https://graph.facebook.com/v18.0/${phoneNumberId}/messages`,
|
|
204
|
-
{
|
|
205
|
-
method: 'POST',
|
|
206
|
-
headers: {
|
|
207
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
208
|
-
'Content-Type': 'application/json'
|
|
209
|
-
},
|
|
210
|
-
body: JSON.stringify({
|
|
211
|
-
messaging_product: 'whatsapp',
|
|
212
|
-
to: message.to.id,
|
|
213
|
-
text: { body: message.content.text }
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
if (!response.ok) {
|
|
219
|
-
const error = await response.text();
|
|
220
|
-
return {
|
|
221
|
-
success: false,
|
|
222
|
-
error: new Error(`WhatsApp API error: ${error}`)
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const result = await response.json();
|
|
227
|
-
return {
|
|
228
|
-
success: true,
|
|
229
|
-
messageId: result.messages[0].id,
|
|
230
|
-
metadata: { phoneNumberId }
|
|
231
|
-
};
|
|
232
|
-
},
|
|
233
|
-
|
|
234
|
-
// Optional: Validate webhook signature
|
|
235
|
-
validateSignature(webhook) {
|
|
236
|
-
const signature = webhook.headers['x-hub-signature-256'];
|
|
237
|
-
const secret = process.env.WHATSAPP_WEBHOOK_SECRET;
|
|
238
|
-
|
|
239
|
-
if (!signature || !secret) return false;
|
|
240
|
-
|
|
241
|
-
const expectedSignature = 'sha256=' +
|
|
242
|
-
crypto.createHmac('sha256', secret)
|
|
243
|
-
.update(webhook.rawBody || '')
|
|
244
|
-
.digest('hex');
|
|
245
|
-
|
|
246
|
-
return SecurityHelpers.secureCompare(signature, expectedSignature);
|
|
247
|
-
},
|
|
248
|
-
|
|
249
|
-
// Optional: Handle webhook verification
|
|
250
|
-
verifyWebhook(params) {
|
|
251
|
-
const { mode, token, challenge } = params;
|
|
252
|
-
const verifyToken = process.env.WHATSAPP_VERIFY_TOKEN;
|
|
253
|
-
|
|
254
|
-
if (mode === 'subscribe' && token === verifyToken) {
|
|
255
|
-
return challenge;
|
|
256
|
-
}
|
|
257
|
-
return null;
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
// Optional: Custom health check
|
|
261
|
-
async checkHealth(ctx, config) {
|
|
262
|
-
try {
|
|
263
|
-
const response = await fetch(
|
|
264
|
-
`https://graph.facebook.com/v18.0/${config.credentials.phoneNumberId}`,
|
|
265
|
-
{
|
|
266
|
-
headers: {
|
|
267
|
-
'Authorization': `Bearer ${config.credentials.accessToken}`
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
status: response.ok ? 'healthy' : 'unhealthy',
|
|
274
|
-
message: response.ok ? 'WhatsApp API accessible' : 'WhatsApp API error',
|
|
275
|
-
details: {
|
|
276
|
-
statusCode: response.status,
|
|
277
|
-
timestamp: new Date().toISOString()
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
} catch (error) {
|
|
281
|
-
return {
|
|
282
|
-
status: 'unhealthy',
|
|
283
|
-
message: error.message,
|
|
284
|
-
details: { timestamp: new Date().toISOString() }
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Advanced Integration with Datasources and Cache
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
294
|
-
import {defineIntegration} from '@pkg/sdk';
|
|
295
|
-
import {WhatsAppDatasource} from './datasources';
|
|
296
|
-
|
|
297
|
-
export default defineIntegration({
|
|
298
|
-
name: 'whatsapp',
|
|
299
|
-
spec: whatsappIntegration,
|
|
300
|
-
|
|
301
|
-
// Configure per-service datasources
|
|
302
|
-
datasources: {
|
|
303
|
-
whatsapp: WhatsAppDatasource
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
// Configure per-service cache
|
|
307
|
-
cache: {
|
|
308
|
-
redisUrl: process.env.WHATSAPP_REDIS_URL,
|
|
309
|
-
ttl: 5 * 60 * 1000, // 5 minutes
|
|
310
|
-
namespace: 'whatsapp'
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
async normalize(webhook) {
|
|
314
|
-
// Access datasource via context in actions
|
|
315
|
-
// this.datasources is not available in methods
|
|
316
|
-
// Use dependency injection pattern instead
|
|
317
|
-
return await this.processWebhookMessages(webhook);
|
|
318
|
-
},
|
|
319
|
-
|
|
320
|
-
async sendMessage(ctx, message, config) {
|
|
321
|
-
// Use cache for rate limiting
|
|
322
|
-
const rateLimitKey = `rate_limit:${config.credentials.phoneNumberId}`;
|
|
323
|
-
const currentCount = await ctx.cache.get(rateLimitKey) || 0;
|
|
324
|
-
|
|
325
|
-
if (currentCount >= 1000) {
|
|
326
|
-
return {
|
|
327
|
-
success: false,
|
|
328
|
-
error: new Error('Rate limit exceeded')
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Send message...
|
|
333
|
-
const result = await this.sendToWhatsApp(message, config);
|
|
334
|
-
|
|
335
|
-
// Update rate limit counter
|
|
336
|
-
await ctx.cache.set(rateLimitKey, currentCount + 1, 60 * 60 * 1000);
|
|
337
|
-
|
|
338
|
-
return result;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
## Event Flow
|
|
344
|
-
|
|
345
|
-
### Inbound Message Flow
|
|
346
|
-
|
|
347
|
-
1. **Webhook Received** → `i_receiveWebhook` action
|
|
348
|
-
2. **Security Validation** → Timestamp, signature, payload checks
|
|
349
|
-
3. **Message Normalization** → Convert to unified format
|
|
350
|
-
4. **Event Emission** → `integration.message.received` event
|
|
351
|
-
5. **Message Processing** → Application-specific logic
|
|
352
|
-
6. **Response Generation** → Optional automated responses
|
|
353
|
-
|
|
354
|
-
### Outbound Message Flow
|
|
355
|
-
|
|
356
|
-
1. **Send Request** → `i_sendMessage` action
|
|
357
|
-
2. **Message Transformation** → Convert to platform format
|
|
358
|
-
3. **Retry Logic** → Exponential backoff for failures
|
|
359
|
-
4. **Platform API Call** → Send via platform API
|
|
360
|
-
5. **Event Emission** → `integration.message.sent` or `integration.message.failed`
|
|
361
|
-
6. **Status Tracking** → Update message delivery status
|
|
362
|
-
|
|
363
|
-
## Security Features
|
|
364
|
-
|
|
365
|
-
### Webhook Validation
|
|
366
|
-
|
|
367
|
-
- **Signature Verification**: HMAC-SHA256 validation using platform secrets
|
|
368
|
-
- **Replay Attack Prevention**: Timestamp validation (5-minute window)
|
|
369
|
-
- **Timing Attack Protection**: Constant-time string comparison
|
|
370
|
-
- **Correlation IDs**: Request tracking for debugging and audit trails
|
|
371
|
-
|
|
372
|
-
### Best Practices
|
|
373
|
-
|
|
374
|
-
1. **Always validate webhook signatures** in production
|
|
375
|
-
2. **Use environment variables** for secrets and tokens
|
|
376
|
-
3. **Implement proper error handling** with structured errors
|
|
377
|
-
4. **Add correlation IDs** for request tracking
|
|
378
|
-
5. **Use retry logic** for transient failures
|
|
379
|
-
6. **Monitor health endpoints** for platform availability
|
|
380
|
-
7. **Cache frequently accessed data** to reduce API calls
|
|
381
|
-
|
|
382
|
-
## Infrastructure Integration
|
|
383
|
-
|
|
384
|
-
Integration services automatically inherit:
|
|
385
|
-
|
|
386
|
-
- **Datasource Access**: Database connections, external APIs
|
|
387
|
-
- **Cache Layer**: Redis/in-memory caching with TTL
|
|
388
|
-
- **Permission System**: Role-based access control
|
|
389
|
-
- **Context Helpers**: Common utilities and operations
|
|
390
|
-
- **Memoization**: Action result caching
|
|
391
|
-
- **Health Monitoring**: Automated health checks
|
|
392
|
-
- **Event System**: Pub/sub event broadcasting
|
|
393
|
-
|
|
394
|
-
This architecture provides a robust, scalable foundation for building messaging platform integrations while maintaining consistency, security, and observability across all platforms.
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import {defineService} from '../define-service';
|
|
2
|
-
import {datasources, type UserDatasourceTypes} from './datasources';
|
|
3
|
-
|
|
4
|
-
export type GetUserActionInputParams = {
|
|
5
|
-
id: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export default defineService<UserDatasourceTypes>({
|
|
9
|
-
name: 'test',
|
|
10
|
-
datasources,
|
|
11
|
-
actions: {
|
|
12
|
-
user: {
|
|
13
|
-
params: {
|
|
14
|
-
id: 'string',
|
|
15
|
-
},
|
|
16
|
-
handler(ctx) {
|
|
17
|
-
const userDs = ctx.datasources.user;
|
|
18
|
-
|
|
19
|
-
return userDs.getUser();
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
});
|
|
File without changes
|