@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,278 @@
|
|
|
1
|
+
import KeyvRedis from '@keyv/redis';
|
|
2
|
+
import Keyv from 'keyv';
|
|
3
|
+
import {REDIS_URL} from '../env';
|
|
4
|
+
import type {AppContext} from '../types';
|
|
5
|
+
|
|
6
|
+
export interface CacheOptions {
|
|
7
|
+
redisUrl?: string;
|
|
8
|
+
ttl?: number; // Default TTL in milliseconds
|
|
9
|
+
namespace?: string;
|
|
10
|
+
serialize?: (value: unknown) => string;
|
|
11
|
+
deserialize?: (value: string) => unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CacheHelpers {
|
|
15
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
16
|
+
set<T = unknown>(key: string, value: T, ttl?: number): Promise<boolean>;
|
|
17
|
+
delete(key: string): Promise<boolean>;
|
|
18
|
+
clear(): Promise<void>;
|
|
19
|
+
has(key: string): Promise<boolean>;
|
|
20
|
+
mget<T = unknown>(...keys: string[]): Promise<Array<T | undefined>>;
|
|
21
|
+
mset(entries: Record<string, unknown>, ttl?: number): Promise<boolean>;
|
|
22
|
+
mdel(...keys: string[]): Promise<boolean>;
|
|
23
|
+
wrap<T = unknown>(
|
|
24
|
+
key: string,
|
|
25
|
+
factory: () => Promise<T> | T,
|
|
26
|
+
ttl?: number
|
|
27
|
+
): Promise<T>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class CacheService {
|
|
31
|
+
private keyv: Keyv;
|
|
32
|
+
private defaultTtl: number;
|
|
33
|
+
|
|
34
|
+
constructor(options: CacheOptions = {}) {
|
|
35
|
+
const {
|
|
36
|
+
redisUrl = REDIS_URL,
|
|
37
|
+
ttl = 5 * 60 * 1000, // 5 minutes default
|
|
38
|
+
namespace = 'cache',
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
this.defaultTtl = ttl;
|
|
42
|
+
|
|
43
|
+
// Configure store based on Redis availability
|
|
44
|
+
const store = redisUrl ? new KeyvRedis(redisUrl) : new Map(); // In-memory fallback
|
|
45
|
+
|
|
46
|
+
this.keyv = new Keyv({
|
|
47
|
+
store,
|
|
48
|
+
namespace,
|
|
49
|
+
ttl,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Error handling
|
|
53
|
+
this.keyv.on('error', () => {
|
|
54
|
+
// Silently handle errors - could be logged to a logger if available
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a value from cache
|
|
60
|
+
*/
|
|
61
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
62
|
+
try {
|
|
63
|
+
return await this.keyv.get<T>(key);
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set a value in cache
|
|
71
|
+
*/
|
|
72
|
+
async set<T = unknown>(
|
|
73
|
+
key: string,
|
|
74
|
+
value: T,
|
|
75
|
+
ttl?: number
|
|
76
|
+
): Promise<boolean> {
|
|
77
|
+
try {
|
|
78
|
+
return await this.keyv.set(key, value, ttl);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Delete a key from cache
|
|
86
|
+
*/
|
|
87
|
+
async delete(key: string): Promise<boolean> {
|
|
88
|
+
try {
|
|
89
|
+
return await this.keyv.delete(key);
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear all keys with the namespace
|
|
97
|
+
*/
|
|
98
|
+
async clear(): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
await this.keyv.clear();
|
|
101
|
+
} catch {
|
|
102
|
+
// Silently handle errors
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a key exists
|
|
108
|
+
*/
|
|
109
|
+
async has(key: string): Promise<boolean> {
|
|
110
|
+
try {
|
|
111
|
+
return await this.keyv.has(key);
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get multiple values at once
|
|
119
|
+
*/
|
|
120
|
+
async mget<T = unknown>(...keys: string[]): Promise<Array<T | undefined>> {
|
|
121
|
+
try {
|
|
122
|
+
const promises = keys.map((key) => this.get<T>(key));
|
|
123
|
+
return await Promise.all(promises);
|
|
124
|
+
} catch {
|
|
125
|
+
return keys.map(() => undefined);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Set multiple values at once
|
|
131
|
+
*/
|
|
132
|
+
async mset(entries: Record<string, unknown>, ttl?: number): Promise<boolean> {
|
|
133
|
+
try {
|
|
134
|
+
const promises = Object.entries(entries).map(([key, value]) =>
|
|
135
|
+
this.set(key, value, ttl)
|
|
136
|
+
);
|
|
137
|
+
const results = await Promise.all(promises);
|
|
138
|
+
return results.every(Boolean);
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Delete multiple keys at once
|
|
146
|
+
*/
|
|
147
|
+
async mdel(...keys: string[]): Promise<boolean> {
|
|
148
|
+
try {
|
|
149
|
+
const promises = keys.map((key) => this.delete(key));
|
|
150
|
+
const results = await Promise.all(promises);
|
|
151
|
+
return results.every(Boolean);
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Wrap pattern: Get from cache or calculate and cache
|
|
159
|
+
*/
|
|
160
|
+
async wrap<T = unknown>(
|
|
161
|
+
key: string,
|
|
162
|
+
factory: () => Promise<T> | T,
|
|
163
|
+
ttl?: number
|
|
164
|
+
): Promise<T> {
|
|
165
|
+
try {
|
|
166
|
+
// Try to get from cache
|
|
167
|
+
const cached = await this.get<T>(key);
|
|
168
|
+
if (cached !== undefined) {
|
|
169
|
+
return cached;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Calculate value
|
|
173
|
+
const value = await factory();
|
|
174
|
+
|
|
175
|
+
// Cache the value
|
|
176
|
+
await this.set(key, value, ttl ?? this.defaultTtl);
|
|
177
|
+
|
|
178
|
+
return value;
|
|
179
|
+
} catch {
|
|
180
|
+
// In case of error, still try to return the factory value
|
|
181
|
+
return await factory();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Disconnect the cache (for cleanup)
|
|
187
|
+
*/
|
|
188
|
+
async disconnect(): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
await this.keyv.disconnect();
|
|
191
|
+
} catch {
|
|
192
|
+
// Silently handle errors
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create cache middleware with specific options for a service
|
|
199
|
+
*/
|
|
200
|
+
export function createCacheMiddleware(options: CacheOptions = {}) {
|
|
201
|
+
let cacheInstance: CacheService | null = null;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
/**
|
|
205
|
+
* Initialize cache on service start
|
|
206
|
+
*/
|
|
207
|
+
started() {
|
|
208
|
+
cacheInstance = new CacheService(options);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Add cache helpers to context
|
|
213
|
+
*/
|
|
214
|
+
localAction(handler: (...args: unknown[]) => unknown) {
|
|
215
|
+
return function CacheMiddlewareWrapper(this: unknown, ctx: AppContext) {
|
|
216
|
+
cacheInstance ??= new CacheService(options);
|
|
217
|
+
|
|
218
|
+
const instance = cacheInstance;
|
|
219
|
+
|
|
220
|
+
// Create scoped cache helpers
|
|
221
|
+
const cache: CacheHelpers = {
|
|
222
|
+
get: <T = unknown>(key: string) => instance.get<T>(key),
|
|
223
|
+
|
|
224
|
+
set: <T = unknown>(key: string, value: T, ttl?: number) =>
|
|
225
|
+
instance.set(key, value, ttl),
|
|
226
|
+
|
|
227
|
+
delete: (key: string) => instance.delete(key),
|
|
228
|
+
|
|
229
|
+
clear: () => instance.clear(),
|
|
230
|
+
|
|
231
|
+
has: (key: string) => instance.has(key),
|
|
232
|
+
|
|
233
|
+
mget: <T = unknown>(...keys: string[]) => instance.mget<T>(...keys),
|
|
234
|
+
|
|
235
|
+
mset: (entries: Record<string, unknown>, ttl?: number) =>
|
|
236
|
+
instance.mset(entries, ttl),
|
|
237
|
+
|
|
238
|
+
mdel: (...keys: string[]) => instance.mdel(...keys),
|
|
239
|
+
|
|
240
|
+
wrap: <T = unknown>(
|
|
241
|
+
key: string,
|
|
242
|
+
factory: () => Promise<T> | T,
|
|
243
|
+
ttl?: number
|
|
244
|
+
) => instance.wrap<T>(key, factory, ttl),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Add cache to context
|
|
248
|
+
ctx.cache = cache;
|
|
249
|
+
|
|
250
|
+
// Call the original handler
|
|
251
|
+
return handler.call(this, ctx);
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Cleanup on service stop
|
|
257
|
+
*/
|
|
258
|
+
stopped() {
|
|
259
|
+
if (cacheInstance) {
|
|
260
|
+
return cacheInstance.disconnect();
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Default cache middleware (backwards compatibility)
|
|
267
|
+
export const CacheMiddleware = createCacheMiddleware();
|
|
268
|
+
|
|
269
|
+
// Singleton instance for direct usage
|
|
270
|
+
let globalCacheInstance: CacheService | null = null;
|
|
271
|
+
|
|
272
|
+
export const getCacheInstance = (options?: CacheOptions) => {
|
|
273
|
+
if (options) {
|
|
274
|
+
return new CacheService(options);
|
|
275
|
+
}
|
|
276
|
+
globalCacheInstance ??= new CacheService();
|
|
277
|
+
return globalCacheInstance;
|
|
278
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type {AppContext, Tenant, User, UserRole} from '../types';
|
|
2
|
+
import {AuthenticationError, TenantError} from '../errors';
|
|
3
|
+
import {ContextCache} from '../utils/context-cache';
|
|
4
|
+
import {PermissionCalculator} from '../utils/permission-calculator';
|
|
5
|
+
|
|
6
|
+
export const ContextHelpersMiddleware = {
|
|
7
|
+
// Add helper functions to context before action handlers
|
|
8
|
+
localAction(handler: (...args: unknown[]) => unknown) {
|
|
9
|
+
return function ContextHelpersWrapper(this: unknown, ctx: AppContext) {
|
|
10
|
+
const cache = ContextCache.getInstance();
|
|
11
|
+
|
|
12
|
+
// Memoized permission checking with caching
|
|
13
|
+
const memoizedPermissions = new Map<string, boolean>();
|
|
14
|
+
|
|
15
|
+
ctx.hasPermission = function (permission: string): boolean {
|
|
16
|
+
// Check in-request memoization first
|
|
17
|
+
if (memoizedPermissions.has(permission)) {
|
|
18
|
+
const cachedResult = memoizedPermissions.get(permission);
|
|
19
|
+
return cachedResult === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const user = ctx.meta.user;
|
|
23
|
+
if (!user) {
|
|
24
|
+
memoizedPermissions.set(permission, false);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Use optimized permission calculator
|
|
29
|
+
const result = PermissionCalculator.hasPermission(user, permission);
|
|
30
|
+
memoizedPermissions.set(permission, result);
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Cached user permissions getter
|
|
35
|
+
ctx.getUserPermissions = async function (): Promise<string[]> {
|
|
36
|
+
const user = ctx.meta.user;
|
|
37
|
+
if (!user) return [];
|
|
38
|
+
|
|
39
|
+
const cacheKey = `permissions:${user.id}:${JSON.stringify(user.roles)}`;
|
|
40
|
+
return cache.get(cacheKey, () => {
|
|
41
|
+
return PermissionCalculator.calculateUserPermissions(user);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
ctx.hasRole = function (role: keyof typeof UserRole): boolean {
|
|
46
|
+
const user = ctx.ensureUser();
|
|
47
|
+
return Array.isArray(user.roles) && user.roles.includes(role);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
ctx.isTenantMember = function (): boolean {
|
|
51
|
+
const user = ctx.ensureUser();
|
|
52
|
+
return !!(
|
|
53
|
+
user.tenantId &&
|
|
54
|
+
ctx.meta.tenantId &&
|
|
55
|
+
user.tenantId === ctx.meta.tenantId
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
ctx.isTenantOwner = function (): boolean {
|
|
60
|
+
return ctx.isTenantMember() && ctx.hasRole('OWNER');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
ctx.ensureUser = (): User => {
|
|
64
|
+
if (!ctx.meta.user) {
|
|
65
|
+
ctx.broker.logger.error('Authentication required', {
|
|
66
|
+
action: ctx.action?.name,
|
|
67
|
+
requestId: ctx.meta.requestId,
|
|
68
|
+
userAgent: ctx.meta.userAgent,
|
|
69
|
+
ip: ctx.meta.clientIP,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
throw new AuthenticationError('Authentication required', {
|
|
73
|
+
code: 'AUTH_REQUIRED',
|
|
74
|
+
statusCode: 401,
|
|
75
|
+
requestId: ctx.meta.requestId,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return ctx.meta.user;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
ctx.ensureTenant = (): Tenant => {
|
|
82
|
+
if (!ctx.meta.tenantId) {
|
|
83
|
+
ctx.broker.logger.error('Tenant required', {
|
|
84
|
+
action: ctx.action?.name,
|
|
85
|
+
userId: ctx.meta.user?.id,
|
|
86
|
+
requestId: ctx.meta.requestId,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
throw new TenantError('Tenant required', {
|
|
90
|
+
code: 'TENANT_REQUIRED',
|
|
91
|
+
statusCode: 401,
|
|
92
|
+
tenantId: ctx.meta.tenantId,
|
|
93
|
+
requestId: ctx.meta.requestId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Return a proper Tenant object with the ID
|
|
98
|
+
return {
|
|
99
|
+
id: ctx.meta.tenantId,
|
|
100
|
+
name: ctx.meta.tenantName ?? '',
|
|
101
|
+
} as Tenant;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Enhanced audit logging function
|
|
105
|
+
ctx.auditLog = function (
|
|
106
|
+
action: string,
|
|
107
|
+
resource?: unknown,
|
|
108
|
+
metadata?: Record<string, unknown>
|
|
109
|
+
): void {
|
|
110
|
+
ctx.broker.logger.info('Audit log', {
|
|
111
|
+
action,
|
|
112
|
+
resource: resource
|
|
113
|
+
? {type: typeof resource, id: (resource as Record<string, unknown>).id}
|
|
114
|
+
: undefined,
|
|
115
|
+
userId: ctx.meta.user?.id,
|
|
116
|
+
tenantId: ctx.meta.tenantId,
|
|
117
|
+
requestId: ctx.meta.requestId,
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
...metadata,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Enhanced error creation with context
|
|
124
|
+
ctx.createError = function (
|
|
125
|
+
message: string,
|
|
126
|
+
code: string,
|
|
127
|
+
statusCode = 400
|
|
128
|
+
): Error {
|
|
129
|
+
const errorData = {
|
|
130
|
+
code,
|
|
131
|
+
statusCode,
|
|
132
|
+
userId: ctx.meta.user?.id,
|
|
133
|
+
tenantId: ctx.meta.tenantId,
|
|
134
|
+
requestId: ctx.meta.requestId,
|
|
135
|
+
action: ctx.action?.name,
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
ctx.broker.logger.warn('Context error created', {
|
|
140
|
+
message,
|
|
141
|
+
...errorData,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (code === 'AUTH_REQUIRED') {
|
|
145
|
+
return new AuthenticationError(message, errorData);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (code === 'TENANT_REQUIRED') {
|
|
149
|
+
return new TenantError(message, errorData);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return new Error(message);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Call the original handler
|
|
156
|
+
return handler.call(this, ctx);
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {Context} from 'moleculer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Datasource constructor registry type
|
|
5
|
+
*/
|
|
6
|
+
export interface DatasourceConstructorRegistry {
|
|
7
|
+
[key: string]: new () => object;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Datasource instance registry type
|
|
12
|
+
*/
|
|
13
|
+
export interface DatasourceInstanceRegistry {
|
|
14
|
+
[key: string]: object;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Datasource context extension
|
|
19
|
+
*/
|
|
20
|
+
export interface DatasourceContext extends Context {
|
|
21
|
+
datasources: DatasourceInstanceRegistry;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize all datasources
|
|
26
|
+
*/
|
|
27
|
+
function initializeDatasources(
|
|
28
|
+
constructorRegistry: DatasourceConstructorRegistry
|
|
29
|
+
): DatasourceInstanceRegistry {
|
|
30
|
+
const initializedDatasources: DatasourceInstanceRegistry = {};
|
|
31
|
+
|
|
32
|
+
for (const [key, DatasourceClass] of Object.entries(constructorRegistry)) {
|
|
33
|
+
initializedDatasources[key] = new DatasourceClass();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return initializedDatasources;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Datasource middleware that injects datasources into the context
|
|
41
|
+
*/
|
|
42
|
+
export function createDatasourceMiddleware(
|
|
43
|
+
datasources: DatasourceConstructorRegistry
|
|
44
|
+
) {
|
|
45
|
+
const initializedDatasources = initializeDatasources(datasources);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
localAction(handler: (...args: unknown[]) => unknown) {
|
|
49
|
+
return function DatasourceWrapper(this: unknown, ctx: Context) {
|
|
50
|
+
// Inject datasources into context
|
|
51
|
+
(ctx as DatasourceContext).datasources = initializedDatasources;
|
|
52
|
+
return handler.call(this, ctx);
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
remoteAction(handler: (...args: unknown[]) => unknown) {
|
|
57
|
+
return function DatasourceWrapper(this: unknown, ctx: Context) {
|
|
58
|
+
// Inject datasources into context for remote actions too
|
|
59
|
+
(ctx as DatasourceContext).datasources = initializedDatasources;
|
|
60
|
+
return handler.call(this, ctx);
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Type helper for services that use datasources
|
|
68
|
+
*/
|
|
69
|
+
export type ServiceWithDatasources<T extends DatasourceInstanceRegistry> = {
|
|
70
|
+
datasources: T;
|
|
71
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import env from '@ltv/env';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import type {Service} from 'moleculer';
|
|
4
|
+
|
|
5
|
+
const HEALTH_CHECK_PORT = env.int('HEALTH_CHECK_PORT', 3301);
|
|
6
|
+
const HEALTH_CHECK_READINESS_PATH = env.string(
|
|
7
|
+
'HEALTH_CHECK_READINESS_PATH',
|
|
8
|
+
'/readyz'
|
|
9
|
+
);
|
|
10
|
+
const HEALTH_CHECK_LIVENESS_PATH = env.string(
|
|
11
|
+
'HEALTH_CHECK_LIVENESS_PATH',
|
|
12
|
+
'/livez'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
interface HealthCheckOptions {
|
|
16
|
+
port?: number;
|
|
17
|
+
readiness?: {
|
|
18
|
+
path?: string;
|
|
19
|
+
};
|
|
20
|
+
liveness?: {
|
|
21
|
+
path?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface HealthCheckConfig {
|
|
26
|
+
port: number;
|
|
27
|
+
readiness: {
|
|
28
|
+
path: string;
|
|
29
|
+
};
|
|
30
|
+
liveness: {
|
|
31
|
+
path: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Default configuration constants
|
|
36
|
+
export const HEALTH_CHECK_DEFAULTS = {
|
|
37
|
+
PORT: HEALTH_CHECK_PORT,
|
|
38
|
+
READINESS_PATH: HEALTH_CHECK_READINESS_PATH,
|
|
39
|
+
LIVENESS_PATH: HEALTH_CHECK_LIVENESS_PATH,
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export function CreateHealthCheckMiddleware(opts: HealthCheckOptions = {}) {
|
|
43
|
+
// Merge user options with defaults
|
|
44
|
+
const config: HealthCheckConfig = {
|
|
45
|
+
port: opts.port ?? HEALTH_CHECK_DEFAULTS.PORT,
|
|
46
|
+
readiness: {
|
|
47
|
+
path: opts.readiness?.path ?? HEALTH_CHECK_DEFAULTS.READINESS_PATH,
|
|
48
|
+
},
|
|
49
|
+
liveness: {
|
|
50
|
+
path: opts.liveness?.path ?? HEALTH_CHECK_DEFAULTS.LIVENESS_PATH,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type HealthState = 'down' | 'starting' | 'up' | 'stopping';
|
|
55
|
+
|
|
56
|
+
let state: HealthState = 'down';
|
|
57
|
+
let server: http.Server;
|
|
58
|
+
|
|
59
|
+
function handler(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
60
|
+
// Prevent headers from being sent multiple times
|
|
61
|
+
if (res.headersSent) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (req.url === config.readiness.path || req.url === config.liveness.path) {
|
|
66
|
+
const resHeader = {
|
|
67
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const content = {
|
|
71
|
+
state,
|
|
72
|
+
uptime: process.uptime(),
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (req.url === config.readiness.path) {
|
|
77
|
+
// Readiness if the broker started successfully.
|
|
78
|
+
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes
|
|
79
|
+
res.writeHead(state === 'up' ? 200 : 503, resHeader);
|
|
80
|
+
} else {
|
|
81
|
+
// Liveness if the broker is not stopped.
|
|
82
|
+
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-command
|
|
83
|
+
res.writeHead(state !== 'down' ? 200 : 503, resHeader);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
res.end(JSON.stringify(content, null, 2));
|
|
87
|
+
} else {
|
|
88
|
+
res.writeHead(404, http.STATUS_CODES[404] ?? 'Not Found', {});
|
|
89
|
+
res.end();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
created(broker: Service) {
|
|
95
|
+
state = 'starting';
|
|
96
|
+
|
|
97
|
+
server = http.createServer(handler);
|
|
98
|
+
server.listen(config.port, (err?: Error | undefined) => {
|
|
99
|
+
if (err) {
|
|
100
|
+
return broker.logger.error(
|
|
101
|
+
'Unable to start health-check server',
|
|
102
|
+
err
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
broker.logger.info('');
|
|
107
|
+
broker.logger.info('K8s health-check server listening on');
|
|
108
|
+
broker.logger.info(
|
|
109
|
+
` http://localhost:${config.port}${config.readiness.path}`
|
|
110
|
+
);
|
|
111
|
+
broker.logger.info(
|
|
112
|
+
` http://localhost:${config.port}${config.liveness.path}`
|
|
113
|
+
);
|
|
114
|
+
broker.logger.info('');
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// After broker started
|
|
119
|
+
started() {
|
|
120
|
+
state = 'up';
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Before broker stopping
|
|
124
|
+
stopping() {
|
|
125
|
+
state = 'stopping';
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// After broker stopped
|
|
129
|
+
stopped() {
|
|
130
|
+
state = 'down';
|
|
131
|
+
server.close();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {ServiceSchema, ServiceSettingSchema} from 'moleculer';
|
|
2
|
+
|
|
3
|
+
export interface MemoizeMixinOptions {
|
|
4
|
+
ttl?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function MemoizeMixin(
|
|
8
|
+
options?: MemoizeMixinOptions
|
|
9
|
+
): ServiceSchema<ServiceSettingSchema> {
|
|
10
|
+
return {
|
|
11
|
+
name: '',
|
|
12
|
+
methods: {
|
|
13
|
+
async memoize(name: string, params: unknown, fn: () => Promise<unknown>) {
|
|
14
|
+
if (!this.broker.cacher) return fn();
|
|
15
|
+
|
|
16
|
+
const key = this.broker.cacher.defaultKeygen(
|
|
17
|
+
`${this.name}:memoize-${name}`,
|
|
18
|
+
params as object | null,
|
|
19
|
+
{},
|
|
20
|
+
[]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let res = await this.broker.cacher.get(key);
|
|
24
|
+
if (res) return res;
|
|
25
|
+
|
|
26
|
+
res = (await fn()) as unknown as object;
|
|
27
|
+
this.broker.cacher.set(key, res, options?.ttl);
|
|
28
|
+
|
|
29
|
+
return res;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|