@martel/calyx 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ import { ClientProxy } from './client-proxy.ts';
2
+ import { Observable } from 'rxjs';
3
+
4
+ export class ClientTcp extends ClientProxy {
5
+ private socket: any;
6
+ private readonly host: string;
7
+ private readonly port: number;
8
+ private messageId = 0;
9
+ private pendingRequests = new Map<string, { next: (val: any) => void; error: (err: any) => void; complete: () => void }>();
10
+ private socketPromise: Promise<any> | null = null;
11
+ private buffer = '';
12
+
13
+ constructor(options: { host?: string; port?: number }) {
14
+ super();
15
+ this.host = options.host ?? '127.0.0.1';
16
+ this.port = options.port ?? 3000;
17
+ }
18
+
19
+ private async getSocket(): Promise<any> {
20
+ if (this.socket) return this.socket;
21
+ if (this.socketPromise) return this.socketPromise;
22
+
23
+ this.socketPromise = (async () => {
24
+ const self = this;
25
+ const socket = await Bun.connect({
26
+ hostname: this.host,
27
+ port: this.port,
28
+ socket: {
29
+ data(socket, data) {
30
+ self.handleData(data);
31
+ },
32
+ error(socket, error) {
33
+ self.handleError(error);
34
+ },
35
+ close(socket) {
36
+ self.socket = null;
37
+ self.socketPromise = null;
38
+ }
39
+ }
40
+ });
41
+ this.socket = socket;
42
+ return socket;
43
+ })();
44
+
45
+ return this.socketPromise;
46
+ }
47
+
48
+ private handleData(data: Uint8Array) {
49
+ this.buffer += new TextDecoder().decode(data);
50
+ let boundary = this.buffer.indexOf('\n');
51
+ while (boundary !== -1) {
52
+ const messageStr = this.buffer.substring(0, boundary).trim();
53
+ this.buffer = this.buffer.substring(boundary + 1);
54
+
55
+ if (messageStr) {
56
+ try {
57
+ const parsed = JSON.parse(messageStr);
58
+ const { id, response, error, isDisposed } = parsed;
59
+
60
+ const pending = this.pendingRequests.get(id);
61
+ if (pending) {
62
+ if (error) {
63
+ pending.error(new Error(error));
64
+ this.pendingRequests.delete(id);
65
+ } else if (isDisposed) {
66
+ pending.complete();
67
+ this.pendingRequests.delete(id);
68
+ } else {
69
+ pending.next(response);
70
+ }
71
+ }
72
+ } catch (err) {
73
+ // ignore parsing errors for partial or malformed frames
74
+ }
75
+ }
76
+ boundary = this.buffer.indexOf('\n');
77
+ }
78
+ }
79
+
80
+ private handleError(error: any) {
81
+ for (const pending of this.pendingRequests.values()) {
82
+ pending.error(error);
83
+ }
84
+ this.pendingRequests.clear();
85
+ }
86
+
87
+ send<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult> {
88
+ return new Observable<TResult>((observer) => {
89
+ this.messageId++;
90
+ const id = String(this.messageId);
91
+ this.pendingRequests.set(id, observer);
92
+
93
+ this.getSocket().then((socket) => {
94
+ const packet = JSON.stringify({ pattern, data, id }) + '\n';
95
+ socket.write(packet);
96
+ }).catch((err) => {
97
+ observer.error(err);
98
+ });
99
+
100
+ return () => {
101
+ this.pendingRequests.delete(id);
102
+ };
103
+ });
104
+ }
105
+
106
+ emit<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult> {
107
+ return new Observable<TResult>((observer) => {
108
+ this.getSocket().then((socket) => {
109
+ const packet = JSON.stringify({ pattern, data }) + '\n';
110
+ socket.write(packet);
111
+ observer.next(undefined as any);
112
+ observer.complete();
113
+ }).catch((err) => {
114
+ observer.error(err);
115
+ });
116
+ });
117
+ }
118
+
119
+ close() {
120
+ if (this.socket) {
121
+ this.socket.end();
122
+ this.socket = null;
123
+ }
124
+ this.socketPromise = null;
125
+ this.pendingRequests.clear();
126
+ }
127
+ }
@@ -0,0 +1,19 @@
1
+ import 'reflect-metadata';
2
+
3
+ export function MessagePattern(pattern: any): MethodDecorator {
4
+ return (target, propertyKey) => {
5
+ const constructor = target.constructor;
6
+ const existing = Reflect.getOwnMetadata('calyx:message_pattern', constructor) || [];
7
+ existing.push({ pattern, propertyKey });
8
+ Reflect.defineMetadata('calyx:message_pattern', existing, constructor);
9
+ };
10
+ }
11
+
12
+ export function EventPattern(pattern: any): MethodDecorator {
13
+ return (target, propertyKey) => {
14
+ const constructor = target.constructor;
15
+ const existing = Reflect.getOwnMetadata('calyx:event_pattern', constructor) || [];
16
+ existing.push({ pattern, propertyKey });
17
+ Reflect.defineMetadata('calyx:event_pattern', existing, constructor);
18
+ };
19
+ }
@@ -0,0 +1,6 @@
1
+ export * from './interfaces.ts';
2
+ export * from './client-proxy.ts';
3
+ export * from './client-tcp.ts';
4
+ export * from './decorators.ts';
5
+ export * from './server-tcp.ts';
6
+ export * from './microservice.ts';
@@ -0,0 +1,11 @@
1
+ export enum Transport {
2
+ TCP = 0,
3
+ }
4
+
5
+ export interface MicroserviceOptions {
6
+ transport?: Transport;
7
+ options?: {
8
+ host?: string;
9
+ port?: number;
10
+ };
11
+ }
@@ -0,0 +1,114 @@
1
+ import { CalyxContainer } from '../core/container.ts';
2
+ import { ServerTcp } from './server-tcp.ts';
3
+ import { MicroserviceOptions, Transport } from './interfaces.ts';
4
+
5
+ export class CalyxMicroservice {
6
+ private readonly container = new CalyxContainer();
7
+ private readonly server: ServerTcp;
8
+ private isListening = false;
9
+ private cleanupListeners: (() => void)[] = [];
10
+ private readonly globalGuards: any[] = [];
11
+ private readonly globalInterceptors: any[] = [];
12
+
13
+ constructor(private readonly rootModule: any, options: MicroserviceOptions = {}) {
14
+ const transport = options.transport ?? Transport.TCP;
15
+ if (transport !== Transport.TCP) {
16
+ throw new Error(`Calyx microservice: Transport algorithm "${transport}" not supported`);
17
+ }
18
+ this.server = new ServerTcp(options.options);
19
+ }
20
+
21
+ useGlobalGuards(...guards: any[]) {
22
+ this.globalGuards.push(...guards);
23
+ return this;
24
+ }
25
+
26
+ useGlobalInterceptors(...interceptors: any[]) {
27
+ this.globalInterceptors.push(...interceptors);
28
+ return this;
29
+ }
30
+
31
+ async listen(): Promise<any> {
32
+ if (this.isListening) return;
33
+
34
+ this.container.bootstrap(this.rootModule);
35
+ this.server.registerHandlers(this.container, this.globalGuards, this.globalInterceptors);
36
+
37
+ const hostInfo = await this.server.listen();
38
+ this.isListening = true;
39
+
40
+ await this.runOnModuleInit();
41
+ await this.runOnApplicationBootstrap();
42
+
43
+ return hostInfo;
44
+ }
45
+
46
+ async close() {
47
+ if (!this.isListening) return;
48
+
49
+ await this.runShutdownHooks();
50
+
51
+ for (const cleanup of this.cleanupListeners) {
52
+ cleanup();
53
+ }
54
+ this.cleanupListeners = [];
55
+
56
+ this.server.close();
57
+ this.isListening = false;
58
+ }
59
+
60
+ enableShutdownHooks(signals: string[] = ['SIGTERM', 'SIGINT']) {
61
+ const handler = async (signal: string) => {
62
+ await this.close();
63
+ process.exit(0);
64
+ };
65
+
66
+ for (const signal of signals) {
67
+ const listener = () => handler(signal);
68
+ process.on(signal as any, listener);
69
+ this.cleanupListeners.push(() => {
70
+ process.off(signal as any, listener);
71
+ });
72
+ }
73
+ }
74
+
75
+ private async runOnModuleInit() {
76
+ const instances = this.container.getProviderAndControllerInstances();
77
+ for (const instance of instances) {
78
+ if (instance && typeof instance.onModuleInit === 'function') {
79
+ await instance.onModuleInit();
80
+ }
81
+ }
82
+ }
83
+
84
+ private async runOnApplicationBootstrap() {
85
+ const instances = this.container.getProviderAndControllerInstances();
86
+ for (const instance of instances) {
87
+ if (instance && typeof instance.onApplicationBootstrap === 'function') {
88
+ await instance.onApplicationBootstrap();
89
+ }
90
+ }
91
+ }
92
+
93
+ private async runShutdownHooks() {
94
+ const instances = this.container.getProviderAndControllerInstances();
95
+
96
+ for (const instance of instances) {
97
+ if (instance && typeof instance.onModuleDestroy === 'function') {
98
+ await instance.onModuleDestroy();
99
+ }
100
+ }
101
+
102
+ for (const instance of instances) {
103
+ if (instance && typeof instance.beforeApplicationShutdown === 'function') {
104
+ await instance.beforeApplicationShutdown();
105
+ }
106
+ }
107
+
108
+ for (const instance of instances) {
109
+ if (instance && typeof instance.onApplicationShutdown === 'function') {
110
+ await instance.onApplicationShutdown();
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,210 @@
1
+ import { CalyxContainer } from '../core/container.ts';
2
+ import { METADATA_KEYS } from '../core/metadata.ts';
3
+ import { CalyxExecutionContext } from '../lifecycle/context.ts';
4
+
5
+ export class ServerTcp {
6
+ private server: any;
7
+ private readonly host: string;
8
+ private readonly port: number;
9
+ private readonly messageHandlers = new Map<string, {
10
+ instance: any;
11
+ propertyKey: string | symbol;
12
+ guards: any[];
13
+ interceptors: any[];
14
+ controllerClass: any;
15
+ }>();
16
+ private readonly eventHandlers = new Map<string, { instance: any; propertyKey: string | symbol }>();
17
+
18
+ constructor(options: { host?: string; port?: number } = {}) {
19
+ this.host = options.host ?? '127.0.0.1';
20
+ this.port = options.port ?? 3000;
21
+ }
22
+
23
+ registerHandlers(
24
+ container: CalyxContainer,
25
+ globalGuards: any[] = [],
26
+ globalInterceptors: any[] = []
27
+ ) {
28
+ const instances = container.getProviderAndControllerInstances();
29
+ for (const instance of instances) {
30
+ if (!instance || !instance.constructor) continue;
31
+
32
+ const msgPatterns: { pattern: any; propertyKey: string | symbol }[] =
33
+ Reflect.getMetadata('calyx:message_pattern', instance.constructor) || [];
34
+ for (const handler of msgPatterns) {
35
+ const patternStr = typeof handler.pattern === 'string' ? handler.pattern : JSON.stringify(handler.pattern);
36
+
37
+ const classGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor) || [];
38
+ const methodGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor.prototype, handler.propertyKey) || [];
39
+ const guards = this.compileLifecycleItems(container, [...globalGuards, ...classGuards, ...methodGuards]);
40
+
41
+ const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor) || [];
42
+ const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, handler.propertyKey) || [];
43
+ const interceptors = this.compileLifecycleItems(container, [...globalInterceptors, ...classInterceptors, ...methodInterceptors]);
44
+
45
+ this.messageHandlers.set(patternStr, {
46
+ instance,
47
+ propertyKey: handler.propertyKey,
48
+ guards,
49
+ interceptors,
50
+ controllerClass: instance.constructor,
51
+ });
52
+ }
53
+
54
+ const eventPatterns: { pattern: any; propertyKey: string | symbol }[] =
55
+ Reflect.getMetadata('calyx:event_pattern', instance.constructor) || [];
56
+ for (const handler of eventPatterns) {
57
+ const patternStr = typeof handler.pattern === 'string' ? handler.pattern : JSON.stringify(handler.pattern);
58
+ this.eventHandlers.set(patternStr, { instance, propertyKey: handler.propertyKey });
59
+ }
60
+ }
61
+ }
62
+
63
+ private compileLifecycleItems(
64
+ container: CalyxContainer,
65
+ items: any[]
66
+ ): any[] {
67
+ return items.map((item) => {
68
+ if (typeof item === 'function') {
69
+ const proto = item.prototype;
70
+ const hasLifecycle = proto && (proto.canActivate || proto.intercept);
71
+ if (hasLifecycle) {
72
+ try {
73
+ return { instance: container.resolveTokenGlobally(item) };
74
+ } catch {
75
+ return { instance: new item() };
76
+ }
77
+ }
78
+ return { instance: { canActivate: item, intercept: item } };
79
+ }
80
+ return { instance: item };
81
+ });
82
+ }
83
+
84
+ listen(): Promise<any> {
85
+ const self = this;
86
+ return new Promise((resolve, reject) => {
87
+ try {
88
+ this.server = Bun.listen({
89
+ hostname: this.host,
90
+ port: this.port,
91
+ socket: {
92
+ data(socket, data) {
93
+ self.handleSocketData(socket, data);
94
+ },
95
+ error(socket, err) {
96
+ console.error(`Microservice socket error:`, err);
97
+ }
98
+ }
99
+ });
100
+ resolve(this.server);
101
+ } catch (err) {
102
+ reject(err);
103
+ }
104
+ });
105
+ }
106
+
107
+ private handleSocketData(socket: any, data: Uint8Array) {
108
+ if (!socket.data) {
109
+ socket.data = { buffer: '' };
110
+ }
111
+
112
+ socket.data.buffer += new TextDecoder().decode(data);
113
+ let boundary = socket.data.buffer.indexOf('\n');
114
+ while (boundary !== -1) {
115
+ const messageStr = socket.data.buffer.substring(0, boundary).trim();
116
+ socket.data.buffer = socket.data.buffer.substring(boundary + 1);
117
+
118
+ if (messageStr) {
119
+ this.handleMessage(socket, messageStr);
120
+ }
121
+ boundary = socket.data.buffer.indexOf('\n');
122
+ }
123
+ }
124
+
125
+ private async handleMessage(socket: any, messageStr: string) {
126
+ try {
127
+ const parsed = JSON.parse(messageStr);
128
+ const { pattern, data, id } = parsed;
129
+ const patternStr = typeof pattern === 'string' ? pattern : JSON.stringify(pattern);
130
+
131
+ if (id !== undefined) {
132
+ const handler = this.messageHandlers.get(patternStr);
133
+ if (!handler) {
134
+ socket.write(JSON.stringify({ id, error: `Handler for pattern "${patternStr}" not found` }) + '\n');
135
+ return;
136
+ }
137
+
138
+ const context = new CalyxExecutionContext();
139
+ context.resetContextRpc(socket, data, handler.controllerClass, handler.instance[handler.propertyKey]);
140
+
141
+ try {
142
+ for (const guard of handler.guards) {
143
+ const canActive = await guard.instance.canActivate(context);
144
+ if (!canActive) {
145
+ throw new Error('Forbidden resource');
146
+ }
147
+ }
148
+
149
+ const nextCall = {
150
+ handle: async () => {
151
+ return handler.instance[handler.propertyKey](data);
152
+ }
153
+ };
154
+
155
+ let chain = nextCall.handle;
156
+ for (let i = handler.interceptors.length - 1; i >= 0; i--) {
157
+ const interceptor = handler.interceptors[i];
158
+ const currentChain = chain;
159
+ chain = async () => {
160
+ return interceptor.instance.intercept(context, {
161
+ handle: () => currentChain()
162
+ });
163
+ };
164
+ }
165
+
166
+ const result = await chain();
167
+
168
+ if (result && typeof result.subscribe === 'function') {
169
+ result.subscribe({
170
+ next: (val: any) => {
171
+ socket.write(JSON.stringify({ id, response: val }) + '\n');
172
+ },
173
+ error: (err: any) => {
174
+ socket.write(JSON.stringify({ id, error: err.message }) + '\n');
175
+ },
176
+ complete: () => {
177
+ socket.write(JSON.stringify({ id, isDisposed: true }) + '\n');
178
+ }
179
+ });
180
+ } else {
181
+ socket.write(JSON.stringify({ id, response: result }) + '\n');
182
+ socket.write(JSON.stringify({ id, isDisposed: true }) + '\n');
183
+ }
184
+ } catch (err: any) {
185
+ socket.write(JSON.stringify({ id, error: err.message }) + '\n');
186
+ } finally {
187
+ context.clearContext();
188
+ }
189
+ } else {
190
+ const handler = this.eventHandlers.get(patternStr);
191
+ if (handler) {
192
+ try {
193
+ handler.instance[handler.propertyKey](data);
194
+ } catch (err) {
195
+ console.error(`Error executing EventPattern handler for pattern "${patternStr}":`, err);
196
+ }
197
+ }
198
+ }
199
+ } catch {
200
+ // ignore
201
+ }
202
+ }
203
+
204
+ close() {
205
+ if (this.server) {
206
+ this.server.stop();
207
+ this.server = null;
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,124 @@
1
+ import 'reflect-metadata';
2
+ import { ValidationRule } from './decorators.ts';
3
+
4
+ export class ValidationCompiler {
5
+ private static readonly compiledValidators = new Map<any, (obj: any) => string[] | null>();
6
+
7
+ static compile(dtoClass: any): (obj: any) => string[] | null {
8
+ if (this.compiledValidators.has(dtoClass)) {
9
+ return this.compiledValidators.get(dtoClass)!;
10
+ }
11
+
12
+ const rules: ValidationRule[] = Reflect.getMetadata('calyx:validation_rules', dtoClass) || [];
13
+
14
+ const rulesByProp = new Map<string, ValidationRule[]>();
15
+ for (const rule of rules) {
16
+ let list = rulesByProp.get(rule.propertyKey);
17
+ if (!list) {
18
+ list = [];
19
+ rulesByProp.set(rule.propertyKey, list);
20
+ }
21
+ list.push(rule);
22
+ }
23
+
24
+ const codeParts: string[] = ['const errors = [];'];
25
+
26
+ for (const [prop, propRules] of rulesByProp.entries()) {
27
+ const isOptional = propRules.some((r) => r.type === 'optional');
28
+
29
+ const propCode: string[] = [];
30
+ for (const rule of propRules) {
31
+ if (rule.type === 'optional') continue;
32
+
33
+ if (rule.type === 'string') {
34
+ propCode.push(`if (typeof obj.${prop} !== 'string') errors.push('${prop} must be a string');`);
35
+ } else if (rule.type === 'number') {
36
+ propCode.push(`if (typeof obj.${prop} !== 'number' || isNaN(obj.${prop})) errors.push('${prop} must be a number');`);
37
+ } else if (rule.type === 'email') {
38
+ propCode.push(
39
+ `if (typeof obj.${prop} !== 'string' || !obj.${prop}.includes('@')) errors.push('${prop} must be a valid email');`
40
+ );
41
+ }
42
+ }
43
+
44
+ if (isOptional) {
45
+ codeParts.push(`if (obj.${prop} !== undefined && obj.${prop} !== null) {
46
+ ${propCode.join('\n')}
47
+ }`);
48
+ } else {
49
+ codeParts.push(`if (obj.${prop} === undefined || obj.${prop} === null) {
50
+ errors.push('${prop} should not be empty');
51
+ } else {
52
+ ${propCode.join('\n')}
53
+ }`);
54
+ }
55
+ }
56
+
57
+ codeParts.push('return errors.length > 0 ? errors : null;');
58
+
59
+ const fnBody = codeParts.join('\n');
60
+ try {
61
+ const validator = new Function('obj', fnBody) as (obj: any) => string[] | null;
62
+ this.compiledValidators.set(dtoClass, validator);
63
+ return validator;
64
+ } catch (err) {
65
+ console.error('Failed to compile validation JIT for class:', dtoClass.name || dtoClass);
66
+ console.error('Code body:', fnBody);
67
+ throw err;
68
+ }
69
+ }
70
+ }
71
+
72
+ export class SerializationCompiler {
73
+ private static readonly compiledSerializers = new Map<any, (obj: any) => string>();
74
+
75
+ static compile(dtoClass: any): (obj: any) => string {
76
+ if (this.compiledSerializers.has(dtoClass)) {
77
+ return this.compiledSerializers.get(dtoClass)!;
78
+ }
79
+
80
+ const excludes: Set<string> = Reflect.getMetadata('calyx:exclude_properties', dtoClass) || new Set();
81
+
82
+ const rules: ValidationRule[] = Reflect.getMetadata('calyx:validation_rules', dtoClass) || [];
83
+ const exposedKeys = new Set(rules.map((r) => r.propertyKey));
84
+
85
+ const exposes: Set<string> = Reflect.getMetadata('calyx:expose_properties', dtoClass) || new Set();
86
+ for (const exp of exposes) {
87
+ exposedKeys.add(exp);
88
+ }
89
+
90
+ const keys = Array.from(exposedKeys).filter((k) => !excludes.has(k));
91
+
92
+ const jsonParts: string[] = [];
93
+ for (const key of keys) {
94
+ const propRules = rules.filter((r) => r.propertyKey === key);
95
+ const isNumber = propRules.some((r) => r.type === 'number');
96
+ const isString = propRules.some((r) => r.type === 'string');
97
+
98
+ if (isNumber) {
99
+ jsonParts.push(`"${key}":\${obj.${key} === undefined || obj.${key} === null ? null : obj.${key}}`);
100
+ } else if (isString) {
101
+ jsonParts.push(
102
+ `"${key}":\${obj.${key} === undefined || obj.${key} === null ? null : JSON.stringify(obj.${key})}`
103
+ );
104
+ } else {
105
+ jsonParts.push(
106
+ `"${key}":\${obj.${key} === undefined || obj.${key} === null ? null : JSON.stringify(obj.${key})}`
107
+ );
108
+ }
109
+ }
110
+
111
+ const fnBody = `
112
+ return \`{${jsonParts.join(',')}}\`;
113
+ `;
114
+
115
+ try {
116
+ const serializer = new Function('obj', fnBody) as (obj: any) => string;
117
+ this.compiledSerializers.set(dtoClass, serializer);
118
+ return serializer;
119
+ } catch (err) {
120
+ console.error('Failed to compile JIT response serializer for class:', dtoClass.name || dtoClass);
121
+ throw err;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,47 @@
1
+ import 'reflect-metadata';
2
+
3
+ export interface ValidationRule {
4
+ type: string;
5
+ propertyKey: string;
6
+ args?: any[];
7
+ }
8
+
9
+ function registerValidationRule(type: string, target: any, propertyKey: string, args?: any[]) {
10
+ const existing: ValidationRule[] = Reflect.getOwnMetadata('calyx:validation_rules', target.constructor) || [];
11
+ existing.push({ type, propertyKey, args });
12
+ Reflect.defineMetadata('calyx:validation_rules', existing, target.constructor);
13
+ }
14
+
15
+ export function IsString(): PropertyDecorator {
16
+ return (target, propertyKey) => registerValidationRule('string', target, String(propertyKey));
17
+ }
18
+
19
+ export function IsNumber(): PropertyDecorator {
20
+ return (target, propertyKey) => registerValidationRule('number', target, String(propertyKey));
21
+ }
22
+
23
+ export function IsOptional(): PropertyDecorator {
24
+ return (target, propertyKey) => registerValidationRule('optional', target, String(propertyKey));
25
+ }
26
+
27
+ export function IsEmail(): PropertyDecorator {
28
+ return (target, propertyKey) => registerValidationRule('email', target, String(propertyKey));
29
+ }
30
+
31
+ export function Expose(): PropertyDecorator {
32
+ return (target, propertyKey) => {
33
+ const constructor = target.constructor;
34
+ const existing = Reflect.getOwnMetadata('calyx:expose_properties', constructor) || new Set();
35
+ existing.add(String(propertyKey));
36
+ Reflect.defineMetadata('calyx:expose_properties', existing, constructor);
37
+ };
38
+ }
39
+
40
+ export function Exclude(): PropertyDecorator {
41
+ return (target, propertyKey) => {
42
+ const constructor = target.constructor;
43
+ const existing = Reflect.getOwnMetadata('calyx:exclude_properties', constructor) || new Set();
44
+ existing.add(String(propertyKey));
45
+ Reflect.defineMetadata('calyx:exclude_properties', existing, constructor);
46
+ };
47
+ }
@@ -0,0 +1,3 @@
1
+ export * from './decorators.ts';
2
+ export * from './compiler.ts';
3
+ export * from './pipe.ts';
@@ -0,0 +1,31 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../lifecycle/interfaces.ts';
2
+ import { Injectable } from '../core/decorators.ts';
3
+ import { HttpException } from '../http/exceptions.ts';
4
+ import { ValidationCompiler } from './compiler.ts';
5
+
6
+ @Injectable()
7
+ export class ValidationPipe implements PipeTransform {
8
+ async transform(value: any, metadata: ArgumentMetadata) {
9
+ const metatype = metadata.metatype;
10
+ if (!metatype || this.toValidate(metatype)) {
11
+ return value;
12
+ }
13
+
14
+ const validate = ValidationCompiler.compile(metatype);
15
+ const errors = validate(value);
16
+ if (errors) {
17
+ throw new HttpException({
18
+ statusCode: 400,
19
+ message: 'Validation failed',
20
+ errors,
21
+ }, 400);
22
+ }
23
+
24
+ return value;
25
+ }
26
+
27
+ private toValidate(metatype: Function): boolean {
28
+ const types: Function[] = [String, Boolean, Number, Array, Object];
29
+ return types.includes(metatype);
30
+ }
31
+ }