@hiliosai/sdk 0.1.12 → 0.1.14
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/dist/index.d.ts +904 -0
- package/dist/index.js +1809 -0
- package/package.json +11 -2
- package/src/configs/constants.ts +0 -135
- package/src/configs/index.ts +0 -2
- package/src/configs/moleculer/bulkhead.ts +0 -8
- package/src/configs/moleculer/channels.ts +0 -102
- package/src/configs/moleculer/circuit-breaker.ts +0 -17
- package/src/configs/moleculer/index.ts +0 -98
- package/src/configs/moleculer/logger.ts +0 -17
- package/src/configs/moleculer/metrics.ts +0 -20
- package/src/configs/moleculer/registry.ts +0 -7
- package/src/configs/moleculer/retry-policy.ts +0 -17
- package/src/configs/moleculer/tracing.ts +0 -6
- package/src/configs/moleculer/tracking.ts +0 -6
- package/src/configs/permissions.ts +0 -109
- package/src/datasources/base.datasource.ts +0 -111
- package/src/datasources/extensions/index.ts +0 -11
- package/src/datasources/extensions/retry.extension.ts +0 -91
- package/src/datasources/extensions/soft-delete.extension.ts +0 -114
- package/src/datasources/extensions/tenant.extension.ts +0 -105
- package/src/datasources/index.ts +0 -3
- package/src/datasources/prisma.datasource.ts +0 -317
- package/src/env.ts +0 -12
- package/src/errors/auth.error.ts +0 -33
- package/src/errors/index.ts +0 -2
- package/src/errors/permission.error.ts +0 -17
- package/src/index.ts +0 -10
- package/src/middlewares/context-helpers.middleware.ts +0 -162
- package/src/middlewares/datasource.middleware.ts +0 -73
- package/src/middlewares/health.middleware.ts +0 -134
- package/src/middlewares/index.ts +0 -5
- package/src/middlewares/memoize.middleware.ts +0 -33
- package/src/middlewares/permissions.middleware.ts +0 -162
- package/src/mixins/datasource.mixin.ts +0 -111
- package/src/mixins/index.ts +0 -1
- package/src/service/define-integration.ts +0 -404
- package/src/service/define-service.ts +0 -58
- package/src/types/channels.ts +0 -60
- package/src/types/context.ts +0 -64
- package/src/types/datasource.ts +0 -23
- package/src/types/index.ts +0 -9
- package/src/types/integration.ts +0 -28
- package/src/types/message.ts +0 -128
- package/src/types/platform.ts +0 -39
- package/src/types/service.ts +0 -209
- package/src/types/tenant.ts +0 -4
- package/src/types/user.ts +0 -16
- package/src/utils/context-cache.ts +0 -70
- package/src/utils/index.ts +0 -8
- package/src/utils/permission-calculator.ts +0 -62
- package/tsconfig.json +0 -13
- package/tsup.config.ts +0 -5
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import {isDev, isTest} from '../env';
|
|
3
|
-
import type {AppContext} from '../types/context';
|
|
4
|
-
import {AbstractDatasource} from './base.datasource';
|
|
5
|
-
|
|
6
|
-
// Generic Prisma Client interface - will be provided by generated client
|
|
7
|
-
// Made very permissive to work with any generated Prisma client
|
|
8
|
-
interface PrismaClientLike {
|
|
9
|
-
$connect(): Promise<void>;
|
|
10
|
-
$disconnect(): Promise<void>;
|
|
11
|
-
$queryRaw: any;
|
|
12
|
-
$transaction: any;
|
|
13
|
-
$extends: any;
|
|
14
|
-
[key: string]: any;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Extension interfaces for common production patterns
|
|
18
|
-
export interface SoftDeleteExtension {
|
|
19
|
-
// Adds soft delete functionality to models
|
|
20
|
-
softDelete: {
|
|
21
|
-
[model: string]: {
|
|
22
|
-
delete: (args: any) => Promise<any>;
|
|
23
|
-
deleteMany: (args: any) => Promise<any>;
|
|
24
|
-
restore: (args: any) => Promise<any>;
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface AuditTrailExtension {
|
|
30
|
-
// Adds audit trail functionality
|
|
31
|
-
$auditTrail: {
|
|
32
|
-
getHistory: (model: string, id: string) => Promise<any[]>;
|
|
33
|
-
getChanges: (
|
|
34
|
-
model: string,
|
|
35
|
-
id: string,
|
|
36
|
-
from?: Date,
|
|
37
|
-
to?: Date
|
|
38
|
-
) => Promise<any[]>;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface TenantExtension {
|
|
43
|
-
// Adds multi-tenant filtering
|
|
44
|
-
$tenant: {
|
|
45
|
-
setContext: (tenantId: string) => void;
|
|
46
|
-
getCurrentTenant: () => string | null;
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Global singleton pattern for Prisma Client
|
|
51
|
-
declare global {
|
|
52
|
-
var __prisma: PrismaClientLike | undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Prisma datasource that follows singleton pattern and best practices
|
|
57
|
-
* Supports both provided client instances and automatic initialization
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```typescript
|
|
61
|
-
* // Option 1: Let datasource create singleton client
|
|
62
|
-
* datasources: {
|
|
63
|
-
* prisma: PrismaDatasource
|
|
64
|
-
* }
|
|
65
|
-
*
|
|
66
|
-
* // Option 2: Provide custom client
|
|
67
|
-
* const customClient = new PrismaClient({...});
|
|
68
|
-
* datasources: {
|
|
69
|
-
* prisma: () => new PrismaDatasource(customClient)
|
|
70
|
-
* }
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
73
|
-
export class PrismaDatasource<
|
|
74
|
-
TPrismaClient extends PrismaClientLike = PrismaClientLike,
|
|
75
|
-
TContext = AppContext
|
|
76
|
-
> extends AbstractDatasource<TContext> {
|
|
77
|
-
readonly name = 'prisma';
|
|
78
|
-
private _client: TPrismaClient | null = null;
|
|
79
|
-
private providedClient: TPrismaClient | null = null;
|
|
80
|
-
|
|
81
|
-
constructor(prismaClient?: TPrismaClient) {
|
|
82
|
-
super();
|
|
83
|
-
this.providedClient = prismaClient ?? null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Get Prisma client instance (singleton pattern)
|
|
88
|
-
*/
|
|
89
|
-
get client(): TPrismaClient {
|
|
90
|
-
this._client ??= this.initializePrismaClient();
|
|
91
|
-
|
|
92
|
-
// Update tenant context from current service context
|
|
93
|
-
this.updateTenantFromContext();
|
|
94
|
-
|
|
95
|
-
return this._client;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Initialize Prisma client using singleton pattern or provided instance
|
|
100
|
-
*/
|
|
101
|
-
private initializePrismaClient(): TPrismaClient {
|
|
102
|
-
// Option 1: Use provided client instance
|
|
103
|
-
if (this.providedClient) {
|
|
104
|
-
this.broker.logger.info('Using provided PrismaClient instance');
|
|
105
|
-
return this.providedClient;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Option 2: Use global singleton (recommended for most cases)
|
|
109
|
-
if (!globalThis.__prisma) {
|
|
110
|
-
this.broker.logger.info('Creating new PrismaClient singleton');
|
|
111
|
-
const baseClient = this.createClient();
|
|
112
|
-
globalThis.__prisma = this.applyExtensions(baseClient);
|
|
113
|
-
} else {
|
|
114
|
-
this.broker.logger.info('Using existing PrismaClient singleton');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return globalThis.__prisma as TPrismaClient;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Create a new Prisma client instance
|
|
122
|
-
* Override this method in subclasses to provide the actual PrismaClient
|
|
123
|
-
*/
|
|
124
|
-
protected createClient(): TPrismaClient {
|
|
125
|
-
throw new Error(
|
|
126
|
-
'createClient() must be implemented by subclass or provide PrismaClient in constructor. ' +
|
|
127
|
-
'Example: class MyPrismaDataSource extends PrismaDatasource { ' +
|
|
128
|
-
'protected createClient() { return new PrismaClient(); } }'
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Apply extensions to the Prisma client
|
|
134
|
-
* Override this method to add production extensions like soft delete, audit trails, etc.
|
|
135
|
-
*/
|
|
136
|
-
protected applyExtensions(client: TPrismaClient): TPrismaClient {
|
|
137
|
-
// Base implementation returns client as-is
|
|
138
|
-
// Override in subclass to add extensions:
|
|
139
|
-
// return client.$extends(softDeleteExtension).$extends(auditExtension)
|
|
140
|
-
return client;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get extended client with all applied extensions
|
|
145
|
-
*/
|
|
146
|
-
get extendedClient(): TPrismaClient {
|
|
147
|
-
return this.applyExtensions(this.client);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Initialize datasource - called after broker injection
|
|
152
|
-
*/
|
|
153
|
-
async init(): Promise<void> {
|
|
154
|
-
this.broker.logger.info('Initializing Prisma datasource');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Called automatically when context is injected
|
|
159
|
-
* Sets tenant context from service context if available
|
|
160
|
-
*/
|
|
161
|
-
private updateTenantFromContext(): void {
|
|
162
|
-
const tenantId = (this.context as any)?.meta?.tenantId;
|
|
163
|
-
if (tenantId && typeof tenantId === 'string') {
|
|
164
|
-
this.setTenantContext(tenantId);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Connect to database - called when service starts
|
|
170
|
-
*/
|
|
171
|
-
async connect(): Promise<void> {
|
|
172
|
-
try {
|
|
173
|
-
this.broker.logger.info('Connecting to database via Prisma');
|
|
174
|
-
await this.client.$connect();
|
|
175
|
-
this.broker.logger.info('Successfully connected to database');
|
|
176
|
-
} catch (error) {
|
|
177
|
-
this.broker.logger.error('Failed to connect to database:', error);
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Disconnect from database - called when service stops
|
|
184
|
-
*/
|
|
185
|
-
async disconnect(): Promise<void> {
|
|
186
|
-
try {
|
|
187
|
-
this.broker.logger.info('Disconnecting from database');
|
|
188
|
-
await this.client.$disconnect();
|
|
189
|
-
this.broker.logger.info('Successfully disconnected from database');
|
|
190
|
-
} catch (error) {
|
|
191
|
-
this.broker.logger.error('Error disconnecting from database:', error);
|
|
192
|
-
throw error;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Health check - verify database connectivity
|
|
198
|
-
*/
|
|
199
|
-
async healthCheck(): Promise<boolean> {
|
|
200
|
-
try {
|
|
201
|
-
this.broker.logger.info('Running Prisma health check');
|
|
202
|
-
|
|
203
|
-
// Simple connectivity test
|
|
204
|
-
await this.client.$queryRaw`SELECT 1`;
|
|
205
|
-
|
|
206
|
-
this.broker.logger.info('Prisma health check passed');
|
|
207
|
-
return true;
|
|
208
|
-
} catch (error) {
|
|
209
|
-
this.broker.logger.error('Prisma health check failed:', error);
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Clear/reset data - useful for testing
|
|
216
|
-
*/
|
|
217
|
-
async clear(): Promise<void> {
|
|
218
|
-
if (isTest || isDev) {
|
|
219
|
-
this.broker.logger.warn(
|
|
220
|
-
'Clearing database (only allowed in test/dev mode)'
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
// Get all model names dynamically
|
|
224
|
-
const modelNames = Object.keys(this.client).filter(
|
|
225
|
-
(key) => !key.startsWith('_') && !key.startsWith('$')
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
// Delete all data in reverse order (to handle foreign keys)
|
|
229
|
-
for (const modelName of modelNames.reverse()) {
|
|
230
|
-
try {
|
|
231
|
-
const model = this.client[modelName as keyof TPrismaClient] as {
|
|
232
|
-
deleteMany?: () => Promise<unknown>;
|
|
233
|
-
};
|
|
234
|
-
if (model.deleteMany) {
|
|
235
|
-
await model.deleteMany();
|
|
236
|
-
}
|
|
237
|
-
} catch (error) {
|
|
238
|
-
// Some models might not support deleteMany, skip them
|
|
239
|
-
this.broker.logger.debug(`Could not clear ${modelName}:`, error);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
throw new Error(
|
|
244
|
-
'Database clear is only allowed in test/development environments'
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Transaction wrapper with proper error handling
|
|
251
|
-
*/
|
|
252
|
-
async transaction<T>(
|
|
253
|
-
fn: (tx: TPrismaClient) => Promise<T>,
|
|
254
|
-
options?: {
|
|
255
|
-
maxWait?: number;
|
|
256
|
-
timeout?: number;
|
|
257
|
-
}
|
|
258
|
-
): Promise<T> {
|
|
259
|
-
try {
|
|
260
|
-
return await this.client.$transaction(
|
|
261
|
-
(tx: any) => fn(tx as TPrismaClient),
|
|
262
|
-
options
|
|
263
|
-
);
|
|
264
|
-
} catch (error) {
|
|
265
|
-
this.broker.logger.error('Transaction failed:', error);
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Set tenant context for multi-tenant applications
|
|
272
|
-
* Requires tenant extension to be applied
|
|
273
|
-
*/
|
|
274
|
-
setTenantContext(tenantId: string): void {
|
|
275
|
-
const tenantClient = this.client as any;
|
|
276
|
-
if (tenantClient.$setTenant) {
|
|
277
|
-
tenantClient.$setTenant(tenantId);
|
|
278
|
-
this.broker.logger.debug('Tenant context set:', {tenantId});
|
|
279
|
-
} else {
|
|
280
|
-
this.broker.logger.warn(
|
|
281
|
-
'Tenant extension not available - setTenantContext ignored'
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Get current tenant context
|
|
288
|
-
*/
|
|
289
|
-
getCurrentTenant(): string | null {
|
|
290
|
-
const tenantClient = this.client as any;
|
|
291
|
-
if (tenantClient.$getCurrentTenant) {
|
|
292
|
-
return tenantClient.$getCurrentTenant();
|
|
293
|
-
}
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Execute with tenant context (automatically restores previous context)
|
|
299
|
-
*/
|
|
300
|
-
async withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
|
|
301
|
-
const previousTenant = this.getCurrentTenant();
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
this.setTenantContext(tenantId);
|
|
305
|
-
return await fn();
|
|
306
|
-
} finally {
|
|
307
|
-
if (previousTenant) {
|
|
308
|
-
this.setTenantContext(previousTenant);
|
|
309
|
-
} else {
|
|
310
|
-
const tenantClient = this.client as any;
|
|
311
|
-
if (tenantClient.$clearTenant) {
|
|
312
|
-
tenantClient.$clearTenant();
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
package/src/env.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import env from '@ltv/env';
|
|
2
|
-
|
|
3
|
-
const nodeEnv = env.string('NODE_ENV', 'development');
|
|
4
|
-
|
|
5
|
-
export const isDev = nodeEnv === 'development';
|
|
6
|
-
export const isTest = nodeEnv === 'test';
|
|
7
|
-
export const isProd = nodeEnv === 'production';
|
|
8
|
-
export const REDIS_URL = env.string('REDIS_URL');
|
|
9
|
-
|
|
10
|
-
export default env;
|
|
11
|
-
export type Env = typeof env;
|
|
12
|
-
export {nodeEnv};
|
package/src/errors/auth.error.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export class AuthenticationError extends Error {
|
|
2
|
-
public readonly code = 'AUTH_REQUIRED';
|
|
3
|
-
public readonly statusCode = 401;
|
|
4
|
-
public readonly data: Record<string, unknown> | undefined;
|
|
5
|
-
|
|
6
|
-
constructor(message: string, data?: Record<string, unknown>) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = 'AuthenticationError';
|
|
9
|
-
this.data = data;
|
|
10
|
-
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
12
|
-
if (Error.captureStackTrace) {
|
|
13
|
-
Error.captureStackTrace(this, AuthenticationError);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class TenantError extends Error {
|
|
19
|
-
public readonly code = 'TENANT_REQUIRED';
|
|
20
|
-
public readonly statusCode = 401;
|
|
21
|
-
public readonly data: Record<string, unknown> | undefined;
|
|
22
|
-
|
|
23
|
-
constructor(message: string, data?: Record<string, unknown>) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.name = 'TenantError';
|
|
26
|
-
this.data = data;
|
|
27
|
-
|
|
28
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
29
|
-
if (Error.captureStackTrace) {
|
|
30
|
-
Error.captureStackTrace(this, TenantError);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
package/src/errors/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export class PermissionError extends Error {
|
|
2
|
-
public readonly code = 'PERMISSION_DENIED';
|
|
3
|
-
public readonly statusCode = 403;
|
|
4
|
-
public readonly data: Record<string, unknown> | undefined;
|
|
5
|
-
|
|
6
|
-
constructor(message: string, data?: Record<string, unknown>) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = 'PermissionError';
|
|
9
|
-
this.data = data;
|
|
10
|
-
|
|
11
|
-
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
13
|
-
if (Error.captureStackTrace) {
|
|
14
|
-
Error.captureStackTrace(this, PermissionError);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export * from './middlewares';
|
|
2
|
-
export * from './service/define-service';
|
|
3
|
-
export * from './service/define-integration';
|
|
4
|
-
export * from './types';
|
|
5
|
-
export * from './configs';
|
|
6
|
-
export * from './env';
|
|
7
|
-
export * from './datasources';
|
|
8
|
-
export * from './mixins';
|
|
9
|
-
export * from './utils';
|
|
10
|
-
export {default as env} from './env';
|
|
@@ -1,162 +0,0 @@
|
|
|
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
|
-
? {
|
|
114
|
-
type: typeof resource,
|
|
115
|
-
id: (resource as Record<string, unknown>).id,
|
|
116
|
-
}
|
|
117
|
-
: undefined,
|
|
118
|
-
userId: ctx.meta.user?.id,
|
|
119
|
-
tenantId: ctx.meta.tenantId,
|
|
120
|
-
requestId: ctx.meta.requestId,
|
|
121
|
-
timestamp: new Date().toISOString(),
|
|
122
|
-
...metadata,
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Enhanced error creation with context
|
|
127
|
-
ctx.createError = function (
|
|
128
|
-
message: string,
|
|
129
|
-
code: string,
|
|
130
|
-
statusCode = 400
|
|
131
|
-
): Error {
|
|
132
|
-
const errorData = {
|
|
133
|
-
code,
|
|
134
|
-
statusCode,
|
|
135
|
-
userId: ctx.meta.user?.id,
|
|
136
|
-
tenantId: ctx.meta.tenantId,
|
|
137
|
-
requestId: ctx.meta.requestId,
|
|
138
|
-
action: ctx.action?.name,
|
|
139
|
-
timestamp: new Date().toISOString(),
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
ctx.broker.logger.warn('Context error created', {
|
|
143
|
-
message,
|
|
144
|
-
...errorData,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
if (code === 'AUTH_REQUIRED') {
|
|
148
|
-
return new AuthenticationError(message, errorData);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (code === 'TENANT_REQUIRED') {
|
|
152
|
-
return new TenantError(message, errorData);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return new Error(message);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Call the original handler
|
|
159
|
-
return handler.call(this, ctx);
|
|
160
|
-
};
|
|
161
|
-
},
|
|
162
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type {Context} from 'moleculer';
|
|
2
|
-
import type {BaseDatasource} from '../datasources';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Datasource constructor registry type
|
|
6
|
-
* All datasources should implement BaseDatasource interface
|
|
7
|
-
*/
|
|
8
|
-
export interface DatasourceConstructorRegistry {
|
|
9
|
-
[key: string]: new () => BaseDatasource | object;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Datasource instance registry type
|
|
14
|
-
*/
|
|
15
|
-
export interface DatasourceInstanceRegistry {
|
|
16
|
-
[key: string]: object;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Datasource context extension
|
|
21
|
-
*/
|
|
22
|
-
export interface DatasourceContext extends Context {
|
|
23
|
-
datasources: DatasourceInstanceRegistry;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Initialize all datasources
|
|
28
|
-
*/
|
|
29
|
-
function initializeDatasources(
|
|
30
|
-
constructorRegistry: DatasourceConstructorRegistry
|
|
31
|
-
): DatasourceInstanceRegistry {
|
|
32
|
-
const initializedDatasources: DatasourceInstanceRegistry = {};
|
|
33
|
-
|
|
34
|
-
for (const [key, DatasourceClass] of Object.entries(constructorRegistry)) {
|
|
35
|
-
initializedDatasources[key] = new DatasourceClass();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return initializedDatasources;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Datasource middleware that injects datasources into the context
|
|
43
|
-
*/
|
|
44
|
-
export function createDatasourceMiddleware(
|
|
45
|
-
datasources: DatasourceConstructorRegistry
|
|
46
|
-
) {
|
|
47
|
-
const initializedDatasources = initializeDatasources(datasources);
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
localAction(handler: (...args: unknown[]) => unknown) {
|
|
51
|
-
return function DatasourceWrapper(this: unknown, ctx: Context) {
|
|
52
|
-
// Inject datasources into context
|
|
53
|
-
(ctx as DatasourceContext).datasources = initializedDatasources;
|
|
54
|
-
return handler.call(this, ctx);
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
remoteAction(handler: (...args: unknown[]) => unknown) {
|
|
59
|
-
return function DatasourceWrapper(this: unknown, ctx: Context) {
|
|
60
|
-
// Inject datasources into context for remote actions too
|
|
61
|
-
(ctx as DatasourceContext).datasources = initializedDatasources;
|
|
62
|
-
return handler.call(this, ctx);
|
|
63
|
-
};
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Type helper for services that use datasources
|
|
70
|
-
*/
|
|
71
|
-
export type ServiceWithDatasources<T extends DatasourceInstanceRegistry> = {
|
|
72
|
-
datasources: T;
|
|
73
|
-
};
|