@hiliosai/sdk 0.1.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.
- package/package.json +27 -0
- package/src/configs/index.ts +1 -0
- package/src/configs/permissions.ts +109 -0
- package/src/env.ts +12 -0
- package/src/errors/auth.error.ts +33 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/permission.error.ts +17 -0
- package/src/examples/cache-usage.example.ts +293 -0
- package/src/index.ts +5 -0
- package/src/middlewares/cache.middleware.ts +278 -0
- package/src/middlewares/context-helpers.middleware.ts +159 -0
- package/src/middlewares/datasource.middleware.ts +71 -0
- package/src/middlewares/health.middleware.ts +134 -0
- package/src/middlewares/index.ts +6 -0
- package/src/middlewares/memoize.middleware.ts +33 -0
- package/src/middlewares/permissions.middleware.ts +162 -0
- package/src/service/define-integration.ts +237 -0
- package/src/service/define-service.ts +77 -0
- package/src/service/example-user/datasources/index.ts +8 -0
- package/src/service/example-user/datasources/user.datasource.ts +7 -0
- package/src/service/example-user/user.service.ts +31 -0
- package/src/service/example-user/utils.ts +0 -0
- package/src/types/context.ts +41 -0
- package/src/types/datasource.ts +23 -0
- package/src/types/index.ts +8 -0
- package/src/types/integration.ts +48 -0
- package/src/types/message.ts +95 -0
- package/src/types/platform.ts +39 -0
- package/src/types/service.ts +208 -0
- package/src/types/tenant.ts +4 -0
- package/src/types/user.ts +16 -0
- package/src/utils/context-cache.ts +70 -0
- package/src/utils/permission-calculator.ts +62 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +6 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type {ActionSchema, Service} from 'moleculer';
|
|
2
|
+
|
|
3
|
+
import {PERMISSIONS, ROLE_PERMISSIONS} from '../configs';
|
|
4
|
+
import {isDev} from '../env';
|
|
5
|
+
|
|
6
|
+
import {PermissionError} from '../errors';
|
|
7
|
+
import type {AppContext} from '../types';
|
|
8
|
+
|
|
9
|
+
export type Permission =
|
|
10
|
+
| keyof typeof PERMISSIONS
|
|
11
|
+
| string
|
|
12
|
+
| ((ctx: AppContext, action: ActionSchema) => Promise<boolean> | boolean);
|
|
13
|
+
|
|
14
|
+
// Middleware interface for type safety
|
|
15
|
+
export interface ActionWithPermissions extends ActionSchema {
|
|
16
|
+
permissions?: Permission | Permission[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const permissionHandlers = {
|
|
20
|
+
[PERMISSIONS.AUTHENTICATED]: async (ctx: AppContext) => !!ctx.meta.user?.id,
|
|
21
|
+
|
|
22
|
+
[PERMISSIONS.TENANT_OWNER]: async (ctx: AppContext) =>
|
|
23
|
+
ctx.meta.user?.roles.includes(PERMISSIONS.OWNER) ?? false,
|
|
24
|
+
|
|
25
|
+
[PERMISSIONS.TENANT_MEMBER]: async (ctx: AppContext) =>
|
|
26
|
+
!!(
|
|
27
|
+
ctx.meta.user?.tenantId &&
|
|
28
|
+
ctx.meta.tenantId &&
|
|
29
|
+
ctx.meta.user.tenantId === ctx.meta.tenantId
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const PermissionsMiddleware = {
|
|
34
|
+
// Wrap local action handlers
|
|
35
|
+
localAction(
|
|
36
|
+
handler: (...args: unknown[]) => unknown,
|
|
37
|
+
action: ActionWithPermissions
|
|
38
|
+
) {
|
|
39
|
+
// If permissions are not defined, return original handler
|
|
40
|
+
if (!action.permissions) {
|
|
41
|
+
return handler;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const permissions = Array.isArray(action.permissions)
|
|
45
|
+
? action.permissions
|
|
46
|
+
: [action.permissions];
|
|
47
|
+
|
|
48
|
+
const permissionNames: string[] = [];
|
|
49
|
+
const permissionFunctions: Array<
|
|
50
|
+
(ctx: AppContext, action: ActionSchema) => Promise<boolean> | boolean
|
|
51
|
+
> = [];
|
|
52
|
+
|
|
53
|
+
// Process each permission
|
|
54
|
+
permissions.forEach((permission) => {
|
|
55
|
+
if (typeof permission === 'function') {
|
|
56
|
+
// Add custom permission function
|
|
57
|
+
permissionFunctions.push(permission);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof permission === 'string') {
|
|
62
|
+
// Check if it's a built-in permission handler
|
|
63
|
+
if (permission in permissionHandlers) {
|
|
64
|
+
const handler =
|
|
65
|
+
permissionHandlers[permission as keyof typeof permissionHandlers];
|
|
66
|
+
permissionFunctions.push(handler);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Otherwise, treat as a permission name to check against user roles
|
|
71
|
+
permissionNames.push(permission);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return async function CheckPermissionsMiddleware(
|
|
77
|
+
this: Service,
|
|
78
|
+
ctx: AppContext
|
|
79
|
+
) {
|
|
80
|
+
let hasAccess = false;
|
|
81
|
+
|
|
82
|
+
// Check if user has OWNER role (super admin within tenant)
|
|
83
|
+
if (ctx.meta.user?.roles.includes(PERMISSIONS.OWNER)) {
|
|
84
|
+
hasAccess = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// REMOVED: DEVELOPER role bypass - security vulnerability
|
|
88
|
+
// Developers must have explicit permissions like any other role
|
|
89
|
+
|
|
90
|
+
// Check custom permission functions
|
|
91
|
+
if (!hasAccess && permissionFunctions.length > 0) {
|
|
92
|
+
const results = await Promise.allSettled(
|
|
93
|
+
permissionFunctions.map((fn) => fn(ctx, action))
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
hasAccess = results.some(
|
|
97
|
+
(result) => result.status === 'fulfilled' && !!result.value
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Log failed permissions without exposing details
|
|
101
|
+
const failures = results.filter((r) => r.status === 'rejected');
|
|
102
|
+
if (failures.length > 0) {
|
|
103
|
+
ctx.broker.logger.warn(
|
|
104
|
+
`${failures.length} permission functions failed`,
|
|
105
|
+
{
|
|
106
|
+
action: action.name,
|
|
107
|
+
userId: ctx.meta.user?.id,
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check named permissions against user roles
|
|
114
|
+
if (!hasAccess && permissionNames.length > 0) {
|
|
115
|
+
const userRoles = ctx.meta.user?.roles ?? [];
|
|
116
|
+
|
|
117
|
+
// Check if user has any role that includes the required permissions
|
|
118
|
+
hasAccess = userRoles.some((role: string) => {
|
|
119
|
+
const rolePermissions = ROLE_PERMISSIONS[role] ?? [];
|
|
120
|
+
return permissionNames.some((permName) =>
|
|
121
|
+
rolePermissions.includes(permName)
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Throw error if access denied
|
|
127
|
+
if (!hasAccess) {
|
|
128
|
+
const user = ctx.meta.user;
|
|
129
|
+
ctx.broker.logger.warn('Access denied:', {
|
|
130
|
+
action: action.name,
|
|
131
|
+
userId: user?.id,
|
|
132
|
+
userRoles: user?.roles,
|
|
133
|
+
tenantId: ctx.meta.tenantId,
|
|
134
|
+
requiredPermissions: permissions,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Sanitize error details for production
|
|
138
|
+
const errorDetails = isDev
|
|
139
|
+
? {
|
|
140
|
+
action: action.name,
|
|
141
|
+
requiredPermissions: permissions.map((p) =>
|
|
142
|
+
typeof p === 'function' ? '[Function]' : String(p)
|
|
143
|
+
),
|
|
144
|
+
userRoles: user?.roles ?? [],
|
|
145
|
+
userId: user?.id,
|
|
146
|
+
tenantId: ctx.meta.tenantId,
|
|
147
|
+
}
|
|
148
|
+
: {
|
|
149
|
+
action: action.name,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
throw new PermissionError(
|
|
153
|
+
'You do not have permission to perform this action',
|
|
154
|
+
errorDetails
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Call the original handler
|
|
159
|
+
return handler.call(this, ctx);
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type {Context, ServiceSchema} from 'moleculer';
|
|
2
|
+
|
|
3
|
+
import type {AppContext} from '../types/context';
|
|
4
|
+
import type {IntegrationConfig} from '../types/integration';
|
|
5
|
+
import type {
|
|
6
|
+
NormalizedMessage,
|
|
7
|
+
PlatformMessage,
|
|
8
|
+
WebhookEvent,
|
|
9
|
+
} from '../types/message';
|
|
10
|
+
import type {
|
|
11
|
+
IntegrationServiceConfig,
|
|
12
|
+
IntegrationServiceSchema,
|
|
13
|
+
} from '../types/service';
|
|
14
|
+
|
|
15
|
+
export function defineIntegration<
|
|
16
|
+
TPlatformMessage extends PlatformMessage = PlatformMessage,
|
|
17
|
+
TSettings = unknown,
|
|
18
|
+
TContext extends AppContext = AppContext
|
|
19
|
+
>(
|
|
20
|
+
config: IntegrationServiceConfig<TPlatformMessage, TSettings, TContext>
|
|
21
|
+
): IntegrationServiceSchema<TPlatformMessage, TSettings> {
|
|
22
|
+
// Create actions from integration methods
|
|
23
|
+
const actions: ServiceSchema['actions'] = {
|
|
24
|
+
'webhook.receive': {
|
|
25
|
+
rest: {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
path: '/webhook',
|
|
28
|
+
},
|
|
29
|
+
params: {
|
|
30
|
+
tenantId: 'string',
|
|
31
|
+
channelId: 'string',
|
|
32
|
+
integrationId: 'string',
|
|
33
|
+
platform: 'string',
|
|
34
|
+
rawPayload: 'any',
|
|
35
|
+
rawBody: {type: 'string', optional: true},
|
|
36
|
+
headers: 'object',
|
|
37
|
+
timestamp: 'number',
|
|
38
|
+
},
|
|
39
|
+
async handler(ctx: Context<WebhookEvent>) {
|
|
40
|
+
const webhook = ctx.params;
|
|
41
|
+
|
|
42
|
+
// Validate webhook if method exists
|
|
43
|
+
if (config.validateWebhook) {
|
|
44
|
+
const isValid = await config.validateWebhook(webhook);
|
|
45
|
+
if (!isValid) {
|
|
46
|
+
throw new Error('Invalid webhook');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate signature if method exists
|
|
51
|
+
if (config.validateSignature) {
|
|
52
|
+
const isValidSignature = config.validateSignature(webhook);
|
|
53
|
+
if (!isValidSignature) {
|
|
54
|
+
throw new Error('Invalid webhook signature');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Normalize the message
|
|
59
|
+
const normalizedMessages = await config.normalize(webhook);
|
|
60
|
+
|
|
61
|
+
// Process each message
|
|
62
|
+
for (const message of normalizedMessages) {
|
|
63
|
+
// Emit event for further processing
|
|
64
|
+
ctx.emit('integration.message.received', {
|
|
65
|
+
tenantId: webhook.tenantId,
|
|
66
|
+
channelId: webhook.channelId,
|
|
67
|
+
integrationId: webhook.integrationId,
|
|
68
|
+
platform: webhook.platform,
|
|
69
|
+
message,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {success: true, messages: normalizedMessages.length};
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
'message.send': {
|
|
78
|
+
rest: {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
path: '/send',
|
|
81
|
+
},
|
|
82
|
+
params: {
|
|
83
|
+
message: 'object',
|
|
84
|
+
config: 'object',
|
|
85
|
+
},
|
|
86
|
+
async handler(ctx: Context) {
|
|
87
|
+
const {message, config: integrationConfig} = ctx.params as {
|
|
88
|
+
message: NormalizedMessage;
|
|
89
|
+
config: IntegrationConfig;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Transform message if method exists
|
|
93
|
+
if (config.transform) {
|
|
94
|
+
await config.transform(message, integrationConfig);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Send the message - cast ctx to expected type
|
|
98
|
+
const result = await config.sendMessage(
|
|
99
|
+
ctx as TContext,
|
|
100
|
+
message,
|
|
101
|
+
integrationConfig
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Emit event based on result
|
|
105
|
+
if (result.success) {
|
|
106
|
+
ctx.emit('integration.message.sent', {
|
|
107
|
+
messageId: result.messageId,
|
|
108
|
+
platform: config.integration.platform,
|
|
109
|
+
metadata: result.metadata,
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
ctx.emit('integration.message.failed', {
|
|
113
|
+
error: result.error,
|
|
114
|
+
platform: config.integration.platform,
|
|
115
|
+
message,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
'health.check': {
|
|
124
|
+
rest: {
|
|
125
|
+
method: 'GET',
|
|
126
|
+
path: '/health',
|
|
127
|
+
},
|
|
128
|
+
params: {
|
|
129
|
+
config: {type: 'object', optional: true},
|
|
130
|
+
},
|
|
131
|
+
async handler(ctx: Context<{config?: IntegrationConfig}>) {
|
|
132
|
+
if (config.checkHealth) {
|
|
133
|
+
const integrationConfig = ctx.params.config;
|
|
134
|
+
if (integrationConfig) {
|
|
135
|
+
return await config.checkHealth(ctx as TContext, integrationConfig);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
status: 'healthy',
|
|
140
|
+
message: 'Integration is running',
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
'webhook.verify': {
|
|
146
|
+
rest: {
|
|
147
|
+
method: 'GET',
|
|
148
|
+
path: '/webhook',
|
|
149
|
+
},
|
|
150
|
+
params: {
|
|
151
|
+
mode: 'string',
|
|
152
|
+
token: 'string',
|
|
153
|
+
challenge: 'string',
|
|
154
|
+
},
|
|
155
|
+
handler(ctx: Context<{mode: string; token: string; challenge: string}>) {
|
|
156
|
+
if (config.verifyWebhook) {
|
|
157
|
+
const result = config.verifyWebhook(ctx.params);
|
|
158
|
+
return result ?? '';
|
|
159
|
+
}
|
|
160
|
+
return ctx.params.challenge;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
'integration.status': {
|
|
165
|
+
rest: {
|
|
166
|
+
method: 'GET',
|
|
167
|
+
path: '/status',
|
|
168
|
+
},
|
|
169
|
+
handler() {
|
|
170
|
+
return {
|
|
171
|
+
id: config.integration.id,
|
|
172
|
+
name: config.integration.name,
|
|
173
|
+
platform: config.integration.platform,
|
|
174
|
+
version: config.integration.version,
|
|
175
|
+
status: config.integration.status,
|
|
176
|
+
capabilities: config.integration.capabilities,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
'credentials.validate': {
|
|
182
|
+
params: {
|
|
183
|
+
credentials: 'object',
|
|
184
|
+
},
|
|
185
|
+
async handler(ctx: Context<{credentials: Record<string, string>}>) {
|
|
186
|
+
if (config.validateCredentials) {
|
|
187
|
+
return await config.validateCredentials(ctx.params.credentials);
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Add any custom actions
|
|
195
|
+
if (config.actions) {
|
|
196
|
+
Object.assign(actions, config.actions);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Create the service schema
|
|
200
|
+
const serviceSchema: IntegrationServiceSchema<TPlatformMessage, TSettings> = {
|
|
201
|
+
name: config.name,
|
|
202
|
+
version: config.version,
|
|
203
|
+
settings: config.settings as TSettings,
|
|
204
|
+
dependencies: config.dependencies,
|
|
205
|
+
metadata: {
|
|
206
|
+
...config.metadata,
|
|
207
|
+
integration: config.integration,
|
|
208
|
+
},
|
|
209
|
+
actions,
|
|
210
|
+
events: config.events ?? {},
|
|
211
|
+
methods: config.methods ?? {},
|
|
212
|
+
hooks: config.hooks ?? {},
|
|
213
|
+
created: config.created,
|
|
214
|
+
started: config.started,
|
|
215
|
+
stopped: config.stopped,
|
|
216
|
+
|
|
217
|
+
// Integration-specific methods
|
|
218
|
+
integration: config.integration,
|
|
219
|
+
normalize: config.normalize,
|
|
220
|
+
transform: config.transform,
|
|
221
|
+
validateWebhook: config.validateWebhook,
|
|
222
|
+
sendMessage: config.sendMessage,
|
|
223
|
+
verifyWebhook: config.verifyWebhook,
|
|
224
|
+
checkHealth: config.checkHealth,
|
|
225
|
+
validateCredentials: config.validateCredentials,
|
|
226
|
+
validateSignature: config.validateSignature,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Remove undefined properties
|
|
230
|
+
Object.keys(serviceSchema).forEach((key) => {
|
|
231
|
+
if (serviceSchema[key as keyof typeof serviceSchema] === undefined) {
|
|
232
|
+
delete serviceSchema[key as keyof typeof serviceSchema];
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return serviceSchema;
|
|
237
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import omit from 'lodash/omit';
|
|
2
|
+
import type {ServiceSchema as MoleculerServiceSchema} from 'moleculer';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ContextHelpersMiddleware,
|
|
6
|
+
MemoizeMixin,
|
|
7
|
+
PermissionsMiddleware,
|
|
8
|
+
createDatasourceMiddleware,
|
|
9
|
+
createCacheMiddleware,
|
|
10
|
+
} from '../middlewares';
|
|
11
|
+
import type {ServiceConfig} from '../types/service';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Define a service
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* export default defineService({
|
|
19
|
+
* name: 'user',
|
|
20
|
+
*
|
|
21
|
+
* // Per-service cache configuration
|
|
22
|
+
* cache: {
|
|
23
|
+
* redisUrl: 'redis://localhost:6379',
|
|
24
|
+
* ttl: 10 * 60 * 1000, // 10 minutes
|
|
25
|
+
* namespace: 'user-service',
|
|
26
|
+
* },
|
|
27
|
+
*
|
|
28
|
+
* actions: {
|
|
29
|
+
* // Action with schema
|
|
30
|
+
* getUser: {
|
|
31
|
+
* params: {
|
|
32
|
+
* id: 'string'
|
|
33
|
+
* },
|
|
34
|
+
* handler(ctx) {
|
|
35
|
+
* // ctx is typed as AppContext with cache helpers
|
|
36
|
+
* const { tenantId } = ctx.meta;
|
|
37
|
+
*
|
|
38
|
+
* // Use cache helpers
|
|
39
|
+
* return ctx.cache.wrap(`user:${ctx.params.id}`, async () => {
|
|
40
|
+
* return { id: ctx.params.id, tenantId };
|
|
41
|
+
* });
|
|
42
|
+
* }
|
|
43
|
+
* },
|
|
44
|
+
*
|
|
45
|
+
* // Direct handler
|
|
46
|
+
* listUsers(ctx) {
|
|
47
|
+
* // ctx is typed as AppContext
|
|
48
|
+
* return [];
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function defineService<TDatasources = unknown, TSettings = unknown>(
|
|
55
|
+
config: ServiceConfig<TSettings, TDatasources>
|
|
56
|
+
): MoleculerServiceSchema<TSettings> {
|
|
57
|
+
const serviceSchema = omit(config, [
|
|
58
|
+
'datasources',
|
|
59
|
+
'cache',
|
|
60
|
+
]) as unknown as MoleculerServiceSchema<TSettings>;
|
|
61
|
+
|
|
62
|
+
const datasources = config.datasources ?? {};
|
|
63
|
+
const cacheOptions = config.cache;
|
|
64
|
+
|
|
65
|
+
// TODO: Add mixins config support
|
|
66
|
+
return {
|
|
67
|
+
...serviceSchema,
|
|
68
|
+
mixins: [
|
|
69
|
+
createDatasourceMiddleware(datasources),
|
|
70
|
+
...(cacheOptions ? [createCacheMiddleware(cacheOptions)] : []),
|
|
71
|
+
MemoizeMixin(),
|
|
72
|
+
PermissionsMiddleware,
|
|
73
|
+
ContextHelpersMiddleware,
|
|
74
|
+
...(serviceSchema.mixins ?? []),
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
cache: {
|
|
12
|
+
redisUrl: process.env.USER_SERVICE_REDIS_URL,
|
|
13
|
+
ttl: 5 * 60 * 1000, // 5 minutes
|
|
14
|
+
namespace: 'user-service',
|
|
15
|
+
},
|
|
16
|
+
actions: {
|
|
17
|
+
user: {
|
|
18
|
+
params: {
|
|
19
|
+
id: 'string',
|
|
20
|
+
},
|
|
21
|
+
handler(ctx) {
|
|
22
|
+
const userDs = ctx.datasources.user;
|
|
23
|
+
|
|
24
|
+
// Use cache with wrap pattern
|
|
25
|
+
return ctx.cache.wrap(`user:${ctx.params.id}`, async () => {
|
|
26
|
+
return userDs.getUser();
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {Context} from 'moleculer';
|
|
2
|
+
import type {User} from './user';
|
|
3
|
+
import type {Tenant} from './tenant';
|
|
4
|
+
import type {CacheHelpers} from '../middlewares/cache.middleware';
|
|
5
|
+
|
|
6
|
+
export interface AppMeta {
|
|
7
|
+
user?: User;
|
|
8
|
+
tenantId?: string;
|
|
9
|
+
tenantName?: string;
|
|
10
|
+
userId?: string;
|
|
11
|
+
integrationId?: string;
|
|
12
|
+
channelId?: string;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
userAgent?: string;
|
|
15
|
+
clientIP?: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PermissionHelpers {
|
|
20
|
+
hasPermission(permission: string): boolean;
|
|
21
|
+
hasRole(role: string): boolean;
|
|
22
|
+
isTenantMember(): boolean;
|
|
23
|
+
isTenantOwner(): boolean;
|
|
24
|
+
ensureUser(): User;
|
|
25
|
+
ensureTenant(): Tenant;
|
|
26
|
+
// New enhanced helpers
|
|
27
|
+
getUserPermissions(): Promise<string[]>;
|
|
28
|
+
auditLog(action: string, resource?: unknown, metadata?: Record<string, unknown>): void;
|
|
29
|
+
createError(message: string, code: string, statusCode?: number): Error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type AppContext<
|
|
33
|
+
TDatasources = unknown,
|
|
34
|
+
TParams = unknown,
|
|
35
|
+
TMeta extends AppMeta = AppMeta,
|
|
36
|
+
TLocals = unknown
|
|
37
|
+
> = Context<TParams, TMeta, TLocals> &
|
|
38
|
+
PermissionHelpers & {
|
|
39
|
+
datasources: TDatasources;
|
|
40
|
+
cache: CacheHelpers;
|
|
41
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility types for datasources
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract instance types from a datasource constructor registry
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const datasources = {
|
|
11
|
+
* user: UserDatasource,
|
|
12
|
+
* whatsapp: WhatsAppDatasource,
|
|
13
|
+
* };
|
|
14
|
+
*
|
|
15
|
+
* type MyDatasources = DatasourceInstanceTypes<typeof datasources>;
|
|
16
|
+
* // Results in: { user: UserDatasource; whatsapp: WhatsAppDatasource; }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type DatasourceInstanceTypes<
|
|
20
|
+
T extends Record<string, new (...args: unknown[]) => unknown>
|
|
21
|
+
> = {
|
|
22
|
+
[K in keyof T]: InstanceType<T[K]>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type {
|
|
3
|
+
IntegrationCapability,
|
|
4
|
+
IntegrationPlatform,
|
|
5
|
+
IntegrationStatus,
|
|
6
|
+
} from './platform';
|
|
7
|
+
|
|
8
|
+
export interface BaseIntegration {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
platform: IntegrationPlatform;
|
|
12
|
+
version: string;
|
|
13
|
+
status: IntegrationStatus;
|
|
14
|
+
capabilities: IntegrationCapability[];
|
|
15
|
+
description?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
documentationUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IntegrationConfig {
|
|
21
|
+
id: string;
|
|
22
|
+
tenantId: string;
|
|
23
|
+
integrationId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
status: IntegrationStatus;
|
|
27
|
+
config: {
|
|
28
|
+
webhookUrl?: string;
|
|
29
|
+
autoReply?: boolean;
|
|
30
|
+
businessHours?: BusinessHoursConfig;
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
};
|
|
33
|
+
credentials: Record<string, string>;
|
|
34
|
+
metadata?: Record<string, any>;
|
|
35
|
+
createdAt?: Date;
|
|
36
|
+
updatedAt?: Date;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BusinessHoursConfig {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
timezone: string;
|
|
42
|
+
schedule: {
|
|
43
|
+
[day: string]: {
|
|
44
|
+
start: string;
|
|
45
|
+
end: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|