@hemia/common 0.0.12 → 0.0.13

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.
@@ -10,6 +10,7 @@ const METADATA_KEYS = {
10
10
  COOKIES: 'cookies',
11
11
  GUARDS: 'hemia:guards',
12
12
  INTERCEPTORS: 'hemia:interceptors',
13
+ PIPES: 'hemia:pipes',
13
14
  CUSTOM: 'hemia:custom',
14
15
  SERIALIZE: 'hemia:serialize',
15
16
  ROLES: 'hemia:roles',
@@ -73,10 +74,15 @@ const Options = createMethodDecorator(HttpMethod.OPTIONS);
73
74
  const Head = createMethodDecorator(HttpMethod.HEAD);
74
75
 
75
76
  const createParamDecorator = (type) => {
76
- return (data) => {
77
+ return (data, ...pipes) => {
77
78
  return (target, propertyKey, parameterIndex) => {
78
79
  const existingParams = Reflect.getMetadata(METADATA_KEYS.PARAMS, target, propertyKey || '') || [];
79
- existingParams.push({ index: parameterIndex, type, data });
80
+ existingParams.push({
81
+ index: parameterIndex,
82
+ type,
83
+ data,
84
+ pipes: pipes.length > 0 ? pipes : undefined
85
+ });
80
86
  Reflect.defineMetadata(METADATA_KEYS.PARAMS, existingParams, target, propertyKey || '');
81
87
  };
82
88
  };
@@ -230,13 +236,12 @@ function extendArrayMetadata(key, metadata, target) {
230
236
  */
231
237
  function UseGuards(...guards) {
232
238
  return (target, key, descriptor) => {
233
- guards.filter(guard => guard && (typeof guard === 'function' || typeof guard === 'object'));
239
+ const validGuards = guards.filter(guard => guard && (typeof guard === 'function' || typeof guard === 'object'));
234
240
  if (descriptor) {
235
- extendArrayMetadata(METADATA_KEYS.GUARDS, guards, descriptor.value);
241
+ extendArrayMetadata(METADATA_KEYS.GUARDS, validGuards, descriptor.value);
236
242
  return descriptor;
237
243
  }
238
- extendArrayMetadata(METADATA_KEYS.GUARDS, guards, target);
239
- return target;
244
+ extendArrayMetadata(METADATA_KEYS.GUARDS, validGuards, target);
240
245
  };
241
246
  }
242
247
 
@@ -252,7 +257,21 @@ function UseInterceptors(...interceptors) {
252
257
  return descriptor;
253
258
  }
254
259
  extendArrayMetadata(METADATA_KEYS.INTERCEPTORS, validInterceptors, target);
255
- return target;
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Decorador que vincula Pipes al controlador o método.
265
+ * @param pipes Una lista de clases Pipe o instancias de Pipe.
266
+ */
267
+ function UsePipes(...pipes) {
268
+ return (target, key, descriptor) => {
269
+ const validPipes = pipes.filter(pipe => pipe && (typeof pipe === 'function' || typeof pipe === 'object'));
270
+ if (descriptor) {
271
+ extendArrayMetadata(METADATA_KEYS.PIPES, validPipes, descriptor.value);
272
+ return descriptor;
273
+ }
274
+ extendArrayMetadata(METADATA_KEYS.PIPES, validPipes, target);
256
275
  };
257
276
  }
258
277
 
@@ -384,25 +403,6 @@ function Transform(transformer) {
384
403
  };
385
404
  }
386
405
 
387
- class ApiResponse {
388
- static success(data, message = 'OK', status = 200) {
389
- return {
390
- success: true,
391
- message,
392
- data,
393
- status,
394
- };
395
- }
396
- static error(message, error = null, status = 500) {
397
- return {
398
- success: false,
399
- message,
400
- error,
401
- status,
402
- };
403
- }
404
- }
405
-
406
406
  class HttpError extends Error {
407
407
  constructor(message, statusCode, error) {
408
408
  super(message);
@@ -699,4 +699,243 @@ class InfraDataDeserializationError extends InfrastructureError {
699
699
  }
700
700
  }
701
701
 
702
- export { ApiResponse, BackupError, BadRequestError, Body, BusinessRuleViolationError, ConfigurationError, ConflictError, ConnectionError, Controller, ControllerRegistry, Cookies, Custom, CustomHttpError, DataConflictError, DataIntegrityError, DataMigrationError, DataNotFoundError, DataValidationError, Delete, DependencyError, DomainError, DuplicateEntityError, EntityNotFoundError, File, Files, ForbiddenError, GatewayTimeoutError, Get, Head, Header, Headers, Host, HttpError, HttpErrorWithDetails, HttpMethod, IndexingError, InfraAuthenticationError, InfraAuthorizationError, InfraCacheConnectionError, InfraConfigurationError, InfraDataDeserializationError, InfraDataSerializationError, InfraDatabaseConnectionError, InfraExternalServiceError, InfraMessageQueueError, InfraNetworkError, InfraServiceUnavailableError, InfraTimeoutError, InfrastructureError, InternalServerError, Ip, Locale, METADATA_KEYS, ManualRegister, Module, Next, NotFoundError, OperationNotAllowedError, Options, Param, ParamType, Patch, PersistenceError, Post, Put, Query, QueryExecutionError, Redirect, Repository, Req, ReqAuth, ReqContext, ReqPermissions, ReqUser, Request, Res, ResourceLimitError, ResourceLimitExceededError, Response, RestoreError, SchemaMismatchError, Serialize, Service, ServiceUnavailableError, Session, SetMetadata, TimeoutError, TransactionError, Transform, UnauthorizedError, UnprocessableEntityError, UseGuards, UseInterceptors, Validate, ValidationError, isRedirectResponse };
702
+ /**
703
+ * Pipe de validación genérico que valida objetos contra un esquema de validación.
704
+ * Este es un ejemplo básico - en producción se usaría con librerías como class-validator o zod.
705
+ */
706
+ class ValidationPipe {
707
+ async transform(value, metadata) {
708
+ if (value === undefined || value === null) {
709
+ throw new HttpError('Validation failed: value is required', 400);
710
+ }
711
+ // Aquí iría la lógica de validación real (ej: con class-validator)
712
+ // Por ahora solo verifica que el valor exista
713
+ return value;
714
+ }
715
+ }
716
+
717
+ /**
718
+ * Pipe que convierte un valor a número entero.
719
+ * Lanza una excepción si el valor no puede ser convertido.
720
+ */
721
+ class ParseIntPipe {
722
+ transform(value, metadata) {
723
+ const val = parseInt(value, 10);
724
+ if (isNaN(val)) {
725
+ throw new HttpError(`Validation failed: "${value}" is not an integer`, 400);
726
+ }
727
+ return val;
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Pipe que convierte un valor a número de punto flotante.
733
+ * Lanza una excepción si el valor no puede ser convertido.
734
+ */
735
+ class ParseFloatPipe {
736
+ transform(value, metadata) {
737
+ const val = parseFloat(value);
738
+ if (isNaN(val)) {
739
+ throw new HttpError(`Validation failed: "${value}" is not a valid number`, 400);
740
+ }
741
+ return val;
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Pipe que convierte un valor a booleano.
747
+ * Acepta: true, false, 1, 0, 'true', 'false', '1', '0'
748
+ */
749
+ class ParseBoolPipe {
750
+ transform(value, metadata) {
751
+ if (typeof value === 'boolean') {
752
+ return value;
753
+ }
754
+ const normalizedValue = String(value).toLowerCase();
755
+ if (normalizedValue === 'true' || normalizedValue === '1') {
756
+ return true;
757
+ }
758
+ if (normalizedValue === 'false' || normalizedValue === '0') {
759
+ return false;
760
+ }
761
+ throw new HttpError(`Validation failed: "${value}" is not a boolean value`, 400);
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Pipe que convierte un valor a array.
767
+ * Puede parsear strings separados por comas o validar que el valor ya sea un array.
768
+ */
769
+ class ParseArrayPipe {
770
+ constructor(options) {
771
+ this.options = options;
772
+ }
773
+ async transform(value, metadata) {
774
+ if (!value) {
775
+ if (this.options?.optional) {
776
+ return [];
777
+ }
778
+ throw new HttpError('Validation failed: array is required', 400);
779
+ }
780
+ let array;
781
+ if (Array.isArray(value)) {
782
+ array = value;
783
+ }
784
+ else if (typeof value === 'string') {
785
+ const separator = this.options?.separator || ',';
786
+ array = value.split(separator).map(item => item.trim());
787
+ }
788
+ else {
789
+ throw new HttpError(`Validation failed: "${value}" is not an array`, 400);
790
+ }
791
+ // Si hay un pipe para los items, aplicarlo a cada elemento
792
+ if (this.options?.items) {
793
+ const transformed = [];
794
+ for (const item of array) {
795
+ transformed.push(await this.options.items.transform(item, metadata));
796
+ }
797
+ return transformed;
798
+ }
799
+ return array;
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Pipe que valida que un valor sea un UUID válido.
805
+ * Soporta versiones 3, 4 y 5 de UUID.
806
+ */
807
+ class ParseUUIDPipe {
808
+ constructor(version) {
809
+ this.version = version;
810
+ this.uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
811
+ }
812
+ transform(value, metadata) {
813
+ if (!value || typeof value !== 'string') {
814
+ throw new HttpError('Validation failed: UUID is required', 400);
815
+ }
816
+ if (!this.uuidRegex.test(value)) {
817
+ throw new HttpError(`Validation failed: "${value}" is not a valid UUID`, 400);
818
+ }
819
+ if (this.version) {
820
+ const uuidVersion = value.charAt(14);
821
+ if (uuidVersion !== this.version) {
822
+ throw new HttpError(`Validation failed: expected UUID v${this.version}, got v${uuidVersion}`, 400);
823
+ }
824
+ }
825
+ return value;
826
+ }
827
+ }
828
+
829
+ /**
830
+ * Pipe que valida que un valor pertenezca a un enum específico.
831
+ */
832
+ class ParseEnumPipe {
833
+ constructor(enumType) {
834
+ this.enumType = enumType;
835
+ }
836
+ transform(value, metadata) {
837
+ const enumValues = Object.values(this.enumType);
838
+ if (!enumValues.includes(value)) {
839
+ const validValues = enumValues.join(', ');
840
+ throw new HttpError(`Validation failed: "${value}" is not a valid enum value. Valid values: ${validValues}`, 400);
841
+ }
842
+ return value;
843
+ }
844
+ }
845
+
846
+ /**
847
+ * Pipe que proporciona un valor por defecto si el valor de entrada es undefined o null.
848
+ */
849
+ class DefaultValuePipe {
850
+ constructor(defaultValue) {
851
+ this.defaultValue = defaultValue;
852
+ }
853
+ transform(value, metadata) {
854
+ if (value === undefined || value === null || value === '') {
855
+ return this.defaultValue;
856
+ }
857
+ return value;
858
+ }
859
+ }
860
+
861
+ /**
862
+ * Pipe que valida archivos subidos.
863
+ * Puede validar tipo MIME, tamaño máximo y campos requeridos.
864
+ */
865
+ class ParseFilePipe {
866
+ constructor(options) {
867
+ this.options = options;
868
+ }
869
+ transform(value, metadata) {
870
+ if (!value) {
871
+ throw new HttpError('Validation failed: file is required', 400);
872
+ }
873
+ // Validar tamaño
874
+ if (this.options?.maxSize && value.size > this.options.maxSize) {
875
+ const maxSizeMB = (this.options.maxSize / (1024 * 1024)).toFixed(2);
876
+ throw new HttpError(`Validation failed: file size exceeds maximum of ${maxSizeMB}MB`, 400);
877
+ }
878
+ // Validar tipo MIME
879
+ if (this.options?.allowedMimeTypes && value.mimetype) {
880
+ if (!this.options.allowedMimeTypes.includes(value.mimetype)) {
881
+ throw new HttpError(`Validation failed: file type "${value.mimetype}" is not allowed. Allowed types: ${this.options.allowedMimeTypes.join(', ')}`, 400);
882
+ }
883
+ }
884
+ // Validar campos requeridos
885
+ if (this.options?.requiredFields) {
886
+ for (const field of this.options.requiredFields) {
887
+ if (!value[field]) {
888
+ throw new HttpError(`Validation failed: file is missing required field "${field}"`, 400);
889
+ }
890
+ }
891
+ }
892
+ return value;
893
+ }
894
+ }
895
+
896
+ /**
897
+ * Pipe que convierte un valor a Date.
898
+ * Acepta timestamps, strings de fecha ISO, o cualquier formato parseable por Date.
899
+ */
900
+ class ParseDatePipe {
901
+ transform(value, metadata) {
902
+ if (!value) {
903
+ throw new HttpError('Validation failed: date is required', 400);
904
+ }
905
+ let date;
906
+ if (typeof value === 'number') {
907
+ date = new Date(value);
908
+ }
909
+ else if (typeof value === 'string') {
910
+ date = new Date(value);
911
+ }
912
+ else {
913
+ throw new HttpError(`Validation failed: "${value}" cannot be converted to Date`, 400);
914
+ }
915
+ if (isNaN(date.getTime())) {
916
+ throw new HttpError(`Validation failed: "${value}" is not a valid date`, 400);
917
+ }
918
+ return date;
919
+ }
920
+ }
921
+
922
+ class ApiResponse {
923
+ static success(data, message = 'OK', status = 200) {
924
+ return {
925
+ success: true,
926
+ message,
927
+ data,
928
+ status,
929
+ };
930
+ }
931
+ static error(message, error = null, status = 500) {
932
+ return {
933
+ success: false,
934
+ message,
935
+ error,
936
+ status,
937
+ };
938
+ }
939
+ }
940
+
941
+ export { ApiResponse, BackupError, BadRequestError, Body, BusinessRuleViolationError, ConfigurationError, ConflictError, ConnectionError, Controller, ControllerRegistry, Cookies, Custom, CustomHttpError, DataConflictError, DataIntegrityError, DataMigrationError, DataNotFoundError, DataValidationError, DefaultValuePipe, Delete, DependencyError, DomainError, DuplicateEntityError, EntityNotFoundError, File, Files, ForbiddenError, GatewayTimeoutError, Get, Head, Header, Headers, Host, HttpError, HttpErrorWithDetails, HttpMethod, IndexingError, InfraAuthenticationError, InfraAuthorizationError, InfraCacheConnectionError, InfraConfigurationError, InfraDataDeserializationError, InfraDataSerializationError, InfraDatabaseConnectionError, InfraExternalServiceError, InfraMessageQueueError, InfraNetworkError, InfraServiceUnavailableError, InfraTimeoutError, InfrastructureError, InternalServerError, Ip, Locale, METADATA_KEYS, ManualRegister, Module, Next, NotFoundError, OperationNotAllowedError, Options, Param, ParamType, ParseArrayPipe, ParseBoolPipe, ParseDatePipe, ParseEnumPipe, ParseFilePipe, ParseFloatPipe, ParseIntPipe, ParseUUIDPipe, Patch, PersistenceError, Post, Put, Query, QueryExecutionError, Redirect, Repository, Req, ReqAuth, ReqContext, ReqPermissions, ReqUser, Request, Res, ResourceLimitError, ResourceLimitExceededError, Response, RestoreError, SchemaMismatchError, Serialize, Service, ServiceUnavailableError, Session, SetMetadata, TimeoutError, TransactionError, Transform, UnauthorizedError, UnprocessableEntityError, UseGuards, UseInterceptors, UsePipes, Validate, ValidationError, ValidationPipe, isRedirectResponse };
@@ -12,6 +12,7 @@ const METADATA_KEYS = {
12
12
  COOKIES: 'cookies',
13
13
  GUARDS: 'hemia:guards',
14
14
  INTERCEPTORS: 'hemia:interceptors',
15
+ PIPES: 'hemia:pipes',
15
16
  CUSTOM: 'hemia:custom',
16
17
  SERIALIZE: 'hemia:serialize',
17
18
  ROLES: 'hemia:roles',
@@ -75,10 +76,15 @@ const Options = createMethodDecorator(exports.HttpMethod.OPTIONS);
75
76
  const Head = createMethodDecorator(exports.HttpMethod.HEAD);
76
77
 
77
78
  const createParamDecorator = (type) => {
78
- return (data) => {
79
+ return (data, ...pipes) => {
79
80
  return (target, propertyKey, parameterIndex) => {
80
81
  const existingParams = Reflect.getMetadata(METADATA_KEYS.PARAMS, target, propertyKey || '') || [];
81
- existingParams.push({ index: parameterIndex, type, data });
82
+ existingParams.push({
83
+ index: parameterIndex,
84
+ type,
85
+ data,
86
+ pipes: pipes.length > 0 ? pipes : undefined
87
+ });
82
88
  Reflect.defineMetadata(METADATA_KEYS.PARAMS, existingParams, target, propertyKey || '');
83
89
  };
84
90
  };
@@ -232,13 +238,12 @@ function extendArrayMetadata(key, metadata, target) {
232
238
  */
233
239
  function UseGuards(...guards) {
234
240
  return (target, key, descriptor) => {
235
- guards.filter(guard => guard && (typeof guard === 'function' || typeof guard === 'object'));
241
+ const validGuards = guards.filter(guard => guard && (typeof guard === 'function' || typeof guard === 'object'));
236
242
  if (descriptor) {
237
- extendArrayMetadata(METADATA_KEYS.GUARDS, guards, descriptor.value);
243
+ extendArrayMetadata(METADATA_KEYS.GUARDS, validGuards, descriptor.value);
238
244
  return descriptor;
239
245
  }
240
- extendArrayMetadata(METADATA_KEYS.GUARDS, guards, target);
241
- return target;
246
+ extendArrayMetadata(METADATA_KEYS.GUARDS, validGuards, target);
242
247
  };
243
248
  }
244
249
 
@@ -254,7 +259,21 @@ function UseInterceptors(...interceptors) {
254
259
  return descriptor;
255
260
  }
256
261
  extendArrayMetadata(METADATA_KEYS.INTERCEPTORS, validInterceptors, target);
257
- return target;
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Decorador que vincula Pipes al controlador o método.
267
+ * @param pipes Una lista de clases Pipe o instancias de Pipe.
268
+ */
269
+ function UsePipes(...pipes) {
270
+ return (target, key, descriptor) => {
271
+ const validPipes = pipes.filter(pipe => pipe && (typeof pipe === 'function' || typeof pipe === 'object'));
272
+ if (descriptor) {
273
+ extendArrayMetadata(METADATA_KEYS.PIPES, validPipes, descriptor.value);
274
+ return descriptor;
275
+ }
276
+ extendArrayMetadata(METADATA_KEYS.PIPES, validPipes, target);
258
277
  };
259
278
  }
260
279
 
@@ -386,25 +405,6 @@ function Transform(transformer) {
386
405
  };
387
406
  }
388
407
 
389
- class ApiResponse {
390
- static success(data, message = 'OK', status = 200) {
391
- return {
392
- success: true,
393
- message,
394
- data,
395
- status,
396
- };
397
- }
398
- static error(message, error = null, status = 500) {
399
- return {
400
- success: false,
401
- message,
402
- error,
403
- status,
404
- };
405
- }
406
- }
407
-
408
408
  class HttpError extends Error {
409
409
  constructor(message, statusCode, error) {
410
410
  super(message);
@@ -701,6 +701,245 @@ class InfraDataDeserializationError extends InfrastructureError {
701
701
  }
702
702
  }
703
703
 
704
+ /**
705
+ * Pipe de validación genérico que valida objetos contra un esquema de validación.
706
+ * Este es un ejemplo básico - en producción se usaría con librerías como class-validator o zod.
707
+ */
708
+ class ValidationPipe {
709
+ async transform(value, metadata) {
710
+ if (value === undefined || value === null) {
711
+ throw new HttpError('Validation failed: value is required', 400);
712
+ }
713
+ // Aquí iría la lógica de validación real (ej: con class-validator)
714
+ // Por ahora solo verifica que el valor exista
715
+ return value;
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Pipe que convierte un valor a número entero.
721
+ * Lanza una excepción si el valor no puede ser convertido.
722
+ */
723
+ class ParseIntPipe {
724
+ transform(value, metadata) {
725
+ const val = parseInt(value, 10);
726
+ if (isNaN(val)) {
727
+ throw new HttpError(`Validation failed: "${value}" is not an integer`, 400);
728
+ }
729
+ return val;
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Pipe que convierte un valor a número de punto flotante.
735
+ * Lanza una excepción si el valor no puede ser convertido.
736
+ */
737
+ class ParseFloatPipe {
738
+ transform(value, metadata) {
739
+ const val = parseFloat(value);
740
+ if (isNaN(val)) {
741
+ throw new HttpError(`Validation failed: "${value}" is not a valid number`, 400);
742
+ }
743
+ return val;
744
+ }
745
+ }
746
+
747
+ /**
748
+ * Pipe que convierte un valor a booleano.
749
+ * Acepta: true, false, 1, 0, 'true', 'false', '1', '0'
750
+ */
751
+ class ParseBoolPipe {
752
+ transform(value, metadata) {
753
+ if (typeof value === 'boolean') {
754
+ return value;
755
+ }
756
+ const normalizedValue = String(value).toLowerCase();
757
+ if (normalizedValue === 'true' || normalizedValue === '1') {
758
+ return true;
759
+ }
760
+ if (normalizedValue === 'false' || normalizedValue === '0') {
761
+ return false;
762
+ }
763
+ throw new HttpError(`Validation failed: "${value}" is not a boolean value`, 400);
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Pipe que convierte un valor a array.
769
+ * Puede parsear strings separados por comas o validar que el valor ya sea un array.
770
+ */
771
+ class ParseArrayPipe {
772
+ constructor(options) {
773
+ this.options = options;
774
+ }
775
+ async transform(value, metadata) {
776
+ if (!value) {
777
+ if (this.options?.optional) {
778
+ return [];
779
+ }
780
+ throw new HttpError('Validation failed: array is required', 400);
781
+ }
782
+ let array;
783
+ if (Array.isArray(value)) {
784
+ array = value;
785
+ }
786
+ else if (typeof value === 'string') {
787
+ const separator = this.options?.separator || ',';
788
+ array = value.split(separator).map(item => item.trim());
789
+ }
790
+ else {
791
+ throw new HttpError(`Validation failed: "${value}" is not an array`, 400);
792
+ }
793
+ // Si hay un pipe para los items, aplicarlo a cada elemento
794
+ if (this.options?.items) {
795
+ const transformed = [];
796
+ for (const item of array) {
797
+ transformed.push(await this.options.items.transform(item, metadata));
798
+ }
799
+ return transformed;
800
+ }
801
+ return array;
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Pipe que valida que un valor sea un UUID válido.
807
+ * Soporta versiones 3, 4 y 5 de UUID.
808
+ */
809
+ class ParseUUIDPipe {
810
+ constructor(version) {
811
+ this.version = version;
812
+ this.uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
813
+ }
814
+ transform(value, metadata) {
815
+ if (!value || typeof value !== 'string') {
816
+ throw new HttpError('Validation failed: UUID is required', 400);
817
+ }
818
+ if (!this.uuidRegex.test(value)) {
819
+ throw new HttpError(`Validation failed: "${value}" is not a valid UUID`, 400);
820
+ }
821
+ if (this.version) {
822
+ const uuidVersion = value.charAt(14);
823
+ if (uuidVersion !== this.version) {
824
+ throw new HttpError(`Validation failed: expected UUID v${this.version}, got v${uuidVersion}`, 400);
825
+ }
826
+ }
827
+ return value;
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Pipe que valida que un valor pertenezca a un enum específico.
833
+ */
834
+ class ParseEnumPipe {
835
+ constructor(enumType) {
836
+ this.enumType = enumType;
837
+ }
838
+ transform(value, metadata) {
839
+ const enumValues = Object.values(this.enumType);
840
+ if (!enumValues.includes(value)) {
841
+ const validValues = enumValues.join(', ');
842
+ throw new HttpError(`Validation failed: "${value}" is not a valid enum value. Valid values: ${validValues}`, 400);
843
+ }
844
+ return value;
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Pipe que proporciona un valor por defecto si el valor de entrada es undefined o null.
850
+ */
851
+ class DefaultValuePipe {
852
+ constructor(defaultValue) {
853
+ this.defaultValue = defaultValue;
854
+ }
855
+ transform(value, metadata) {
856
+ if (value === undefined || value === null || value === '') {
857
+ return this.defaultValue;
858
+ }
859
+ return value;
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Pipe que valida archivos subidos.
865
+ * Puede validar tipo MIME, tamaño máximo y campos requeridos.
866
+ */
867
+ class ParseFilePipe {
868
+ constructor(options) {
869
+ this.options = options;
870
+ }
871
+ transform(value, metadata) {
872
+ if (!value) {
873
+ throw new HttpError('Validation failed: file is required', 400);
874
+ }
875
+ // Validar tamaño
876
+ if (this.options?.maxSize && value.size > this.options.maxSize) {
877
+ const maxSizeMB = (this.options.maxSize / (1024 * 1024)).toFixed(2);
878
+ throw new HttpError(`Validation failed: file size exceeds maximum of ${maxSizeMB}MB`, 400);
879
+ }
880
+ // Validar tipo MIME
881
+ if (this.options?.allowedMimeTypes && value.mimetype) {
882
+ if (!this.options.allowedMimeTypes.includes(value.mimetype)) {
883
+ throw new HttpError(`Validation failed: file type "${value.mimetype}" is not allowed. Allowed types: ${this.options.allowedMimeTypes.join(', ')}`, 400);
884
+ }
885
+ }
886
+ // Validar campos requeridos
887
+ if (this.options?.requiredFields) {
888
+ for (const field of this.options.requiredFields) {
889
+ if (!value[field]) {
890
+ throw new HttpError(`Validation failed: file is missing required field "${field}"`, 400);
891
+ }
892
+ }
893
+ }
894
+ return value;
895
+ }
896
+ }
897
+
898
+ /**
899
+ * Pipe que convierte un valor a Date.
900
+ * Acepta timestamps, strings de fecha ISO, o cualquier formato parseable por Date.
901
+ */
902
+ class ParseDatePipe {
903
+ transform(value, metadata) {
904
+ if (!value) {
905
+ throw new HttpError('Validation failed: date is required', 400);
906
+ }
907
+ let date;
908
+ if (typeof value === 'number') {
909
+ date = new Date(value);
910
+ }
911
+ else if (typeof value === 'string') {
912
+ date = new Date(value);
913
+ }
914
+ else {
915
+ throw new HttpError(`Validation failed: "${value}" cannot be converted to Date`, 400);
916
+ }
917
+ if (isNaN(date.getTime())) {
918
+ throw new HttpError(`Validation failed: "${value}" is not a valid date`, 400);
919
+ }
920
+ return date;
921
+ }
922
+ }
923
+
924
+ class ApiResponse {
925
+ static success(data, message = 'OK', status = 200) {
926
+ return {
927
+ success: true,
928
+ message,
929
+ data,
930
+ status,
931
+ };
932
+ }
933
+ static error(message, error = null, status = 500) {
934
+ return {
935
+ success: false,
936
+ message,
937
+ error,
938
+ status,
939
+ };
940
+ }
941
+ }
942
+
704
943
  exports.ApiResponse = ApiResponse;
705
944
  exports.BackupError = BackupError;
706
945
  exports.BadRequestError = BadRequestError;
@@ -719,6 +958,7 @@ exports.DataIntegrityError = DataIntegrityError;
719
958
  exports.DataMigrationError = DataMigrationError;
720
959
  exports.DataNotFoundError = DataNotFoundError;
721
960
  exports.DataValidationError = DataValidationError;
961
+ exports.DefaultValuePipe = DefaultValuePipe;
722
962
  exports.Delete = Delete;
723
963
  exports.DependencyError = DependencyError;
724
964
  exports.DomainError = DomainError;
@@ -760,6 +1000,14 @@ exports.NotFoundError = NotFoundError;
760
1000
  exports.OperationNotAllowedError = OperationNotAllowedError;
761
1001
  exports.Options = Options;
762
1002
  exports.Param = Param;
1003
+ exports.ParseArrayPipe = ParseArrayPipe;
1004
+ exports.ParseBoolPipe = ParseBoolPipe;
1005
+ exports.ParseDatePipe = ParseDatePipe;
1006
+ exports.ParseEnumPipe = ParseEnumPipe;
1007
+ exports.ParseFilePipe = ParseFilePipe;
1008
+ exports.ParseFloatPipe = ParseFloatPipe;
1009
+ exports.ParseIntPipe = ParseIntPipe;
1010
+ exports.ParseUUIDPipe = ParseUUIDPipe;
763
1011
  exports.Patch = Patch;
764
1012
  exports.PersistenceError = PersistenceError;
765
1013
  exports.Post = Post;
@@ -792,6 +1040,8 @@ exports.UnauthorizedError = UnauthorizedError;
792
1040
  exports.UnprocessableEntityError = UnprocessableEntityError;
793
1041
  exports.UseGuards = UseGuards;
794
1042
  exports.UseInterceptors = UseInterceptors;
1043
+ exports.UsePipes = UsePipes;
795
1044
  exports.Validate = Validate;
796
1045
  exports.ValidationError = ValidationError;
1046
+ exports.ValidationPipe = ValidationPipe;
797
1047
  exports.isRedirectResponse = isRedirectResponse;
@@ -1,5 +1,6 @@
1
1
  export * from "./use-guards.decorator";
2
2
  export * from "./use-interceptors.decorator";
3
+ export * from "./use-pipes.decorator";
3
4
  export * from "./set-metadata.decorator";
4
5
  export * from "./serialize.decorator";
5
6
  export * from "./module.decorator";
@@ -4,4 +4,4 @@ import { CanActivate, Type } from '../../interfaces';
4
4
  * Decorador que vincula Guards al controlador o método.
5
5
  * @param guards Una lista de clases Guard o instancias de Guard.
6
6
  */
7
- export declare function UseGuards(...guards: (Type<CanActivate> | CanActivate | Function)[]): (target: object | Function, key?: string | symbol, descriptor?: PropertyDescriptor) => object;
7
+ export declare function UseGuards(...guards: (Type<CanActivate> | CanActivate | Function)[]): (target: object | Function, key?: string | symbol, descriptor?: PropertyDescriptor) => PropertyDescriptor | undefined;
@@ -4,4 +4,4 @@ import { HemiaInterceptor, Type } from '../../interfaces';
4
4
  * Decorador que vincula Interceptores al controlador o método.
5
5
  * @param interceptors Una lista de clases Interceptor o instancias.
6
6
  */
7
- export declare function UseInterceptors(...interceptors: (Type<HemiaInterceptor> | HemiaInterceptor | Function)[]): (target: object | Function, key?: string | symbol, descriptor?: PropertyDescriptor) => object;
7
+ export declare function UseInterceptors(...interceptors: (Type<HemiaInterceptor> | HemiaInterceptor | Function)[]): (target: object | Function, key?: string | symbol, descriptor?: PropertyDescriptor) => PropertyDescriptor | undefined;
@@ -0,0 +1,7 @@
1
+ import 'reflect-metadata';
2
+ import { PipeTransform, Type } from '../../interfaces';
3
+ /**
4
+ * Decorador que vincula Pipes al controlador o método.
5
+ * @param pipes Una lista de clases Pipe o instancias de Pipe.
6
+ */
7
+ export declare function UsePipes(...pipes: (Type<PipeTransform> | PipeTransform | Function)[]): (target: object | Function, key?: string | symbol, descriptor?: PropertyDescriptor) => PropertyDescriptor | undefined;
@@ -1,121 +1,122 @@
1
1
  import 'reflect-metadata';
2
+ import { PipeTransform, PType } from '../../interfaces';
2
3
  /**
3
4
  * Inyecta el objeto Request completo.
4
5
  * @example
5
6
  * async handler(@Request() req) {}
6
7
  */
7
- export declare const Request: (data?: string) => ParameterDecorator;
8
+ export declare const Request: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
8
9
  /**
9
10
  * Alias para Request.
10
11
  * @example
11
12
  * async handler(@Req() req) {}
12
13
  */
13
- export declare const Req: (data?: string) => ParameterDecorator;
14
+ export declare const Req: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
14
15
  /**
15
16
  * Inyecta el objeto Response.
16
17
  * @example
17
18
  * async handler(@Response() res) {}
18
19
  */
19
- export declare const Response: (data?: string) => ParameterDecorator;
20
+ export declare const Response: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
20
21
  /**
21
22
  * Alias para Response.
22
23
  * @example
23
24
  * async handler(@Res() res) {}
24
25
  */
25
- export declare const Res: (data?: string) => ParameterDecorator;
26
+ export declare const Res: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
26
27
  /**
27
28
  * Inyecta el objeto Next para middlewares.
28
29
  * @example
29
30
  * async handler(@Next() next) {}
30
31
  */
31
- export declare const Next: (data?: string) => ParameterDecorator;
32
+ export declare const Next: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
32
33
  /**
33
34
  * Inyecta la sesión del usuario.
34
35
  * @example
35
36
  * async handler(@Session() session) {}
36
37
  */
37
- export declare const Session: (data?: string) => ParameterDecorator;
38
+ export declare const Session: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
38
39
  /**
39
40
  * Inyecta la IP del cliente.
40
41
  * @example
41
42
  * async handler(@Ip() ip) {}
42
43
  */
43
- export declare const Ip: (data?: string) => ParameterDecorator;
44
+ export declare const Ip: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
44
45
  /**
45
46
  * Inyecta el host de la solicitud.
46
47
  * @example
47
48
  * async handler(@Host() host) {}
48
49
  */
49
- export declare const Host: (data?: string) => ParameterDecorator;
50
+ export declare const Host: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
50
51
  /**
51
52
  * Inyecta el cuerpo de la solicitud.
52
53
  * @example
53
54
  * async handler(@Body() body) {}
54
55
  */
55
- export declare const Body: (data?: string) => ParameterDecorator;
56
+ export declare const Body: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
56
57
  /**
57
58
  * Inyecta los parámetros de consulta (query).
58
59
  * @example
59
60
  * async handler(@Query('search') search) {}
60
61
  */
61
- export declare const Query: (data?: string) => ParameterDecorator;
62
+ export declare const Query: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
62
63
  /**
63
64
  * Inyecta los parámetros de la URL.
64
65
  * @example
65
66
  * async handler(@Param('id') id) {}
66
67
  */
67
- export declare const Param: (data?: string) => ParameterDecorator;
68
+ export declare const Param: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
68
69
  /**
69
70
  * Inyecta los encabezados de la solicitud.
70
71
  * @example
71
72
  * async handler(@Headers('authorization') auth) {}
72
73
  */
73
- export declare const Headers: (data?: string) => ParameterDecorator;
74
+ export declare const Headers: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
74
75
  /**
75
76
  * Inyecta los cookies de la solicitud.
76
77
  * @example
77
78
  * async handler(@Cookies('token') token) {}
78
79
  */
79
- export declare const Cookies: (data?: string) => ParameterDecorator;
80
+ export declare const Cookies: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
80
81
  /**
81
82
  * Inyecta todos los archivos enviados en la solicitud.
82
83
  * @example
83
84
  * async handler(@Files() files) {}
84
85
  */
85
- export declare const Files: (data?: string) => ParameterDecorator;
86
+ export declare const Files: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
86
87
  /**
87
88
  * Inyecta un archivo específico de la solicitud.
88
89
  * @example
89
90
  * async handler(@File('avatar') avatar) {}
90
91
  */
91
- export declare const File: (data?: string) => ParameterDecorator;
92
+ export declare const File: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
92
93
  /**
93
94
  * Inyecta el locale del usuario.
94
95
  * @example
95
96
  * async handler(@Locale() locale) {}
96
97
  */
97
- export declare const Locale: (data?: string) => ParameterDecorator;
98
+ export declare const Locale: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
98
99
  /**
99
100
  * Inyecta datos de autenticación.
100
101
  * @example
101
102
  * async handler(@ReqAuth() auth) {}
102
103
  */
103
- export declare const ReqAuth: (data?: string) => ParameterDecorator;
104
+ export declare const ReqAuth: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
104
105
  /**
105
106
  * Inyecta el usuario autenticado.
106
107
  * @example
107
108
  * async handler(@ReqUser() user) {}
108
109
  */
109
- export declare const ReqUser: (data?: string) => ParameterDecorator;
110
+ export declare const ReqUser: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
110
111
  /**
111
112
  * Inyecta los permisos del usuario.
112
113
  * @example
113
114
  * async handler(@ReqPermissions() permissions) {}
114
115
  */
115
- export declare const ReqPermissions: (data?: string) => ParameterDecorator;
116
+ export declare const ReqPermissions: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
116
117
  /**
117
118
  * Inyecta el contexto de la solicitud.
118
119
  * @example
119
120
  * async handler(@ReqContext() context) {}
120
121
  */
121
- export declare const ReqContext: (data?: string) => ParameterDecorator;
122
+ export declare const ReqContext: (data?: string, ...pipes: (PType<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
@@ -4,3 +4,4 @@ export * from "./core";
4
4
  export * from "./custom";
5
5
  export * from "./validation";
6
6
  export * from "./transform";
7
+ export * from "./pipes";
@@ -7,6 +7,7 @@ export declare const METADATA_KEYS: {
7
7
  readonly COOKIES: "cookies";
8
8
  readonly GUARDS: "hemia:guards";
9
9
  readonly INTERCEPTORS: "hemia:interceptors";
10
+ readonly PIPES: "hemia:pipes";
10
11
  readonly CUSTOM: "hemia:custom";
11
12
  readonly SERIALIZE: "hemia:serialize";
12
13
  readonly ROLES: "hemia:roles";
@@ -0,0 +1,9 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe que proporciona un valor por defecto si el valor de entrada es undefined o null.
4
+ */
5
+ export declare class DefaultValuePipe<T = any> implements PipeTransform<T, T> {
6
+ private readonly defaultValue;
7
+ constructor(defaultValue: T);
8
+ transform(value: T, metadata?: ArgumentMetadata): T;
9
+ }
@@ -0,0 +1,10 @@
1
+ export * from './validation.pipe';
2
+ export * from './parse-int.pipe';
3
+ export * from './parse-float.pipe';
4
+ export * from './parse-bool.pipe';
5
+ export * from './parse-array.pipe';
6
+ export * from './parse-uuid.pipe';
7
+ export * from './parse-enum.pipe';
8
+ export * from './default-value.pipe';
9
+ export * from './parse-file.pipe';
10
+ export * from './parse-date.pipe';
@@ -0,0 +1,15 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ export interface ParseArrayOptions {
3
+ items?: PipeTransform;
4
+ separator?: string;
5
+ optional?: boolean;
6
+ }
7
+ /**
8
+ * Pipe que convierte un valor a array.
9
+ * Puede parsear strings separados por comas o validar que el valor ya sea un array.
10
+ */
11
+ export declare class ParseArrayPipe implements PipeTransform {
12
+ private readonly options?;
13
+ constructor(options?: ParseArrayOptions | undefined);
14
+ transform(value: any, metadata?: ArgumentMetadata): Promise<any[]>;
15
+ }
@@ -0,0 +1,8 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe que convierte un valor a booleano.
4
+ * Acepta: true, false, 1, 0, 'true', 'false', '1', '0'
5
+ */
6
+ export declare class ParseBoolPipe implements PipeTransform<string | boolean, boolean> {
7
+ transform(value: string | boolean, metadata?: ArgumentMetadata): boolean;
8
+ }
@@ -0,0 +1,8 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe que convierte un valor a Date.
4
+ * Acepta timestamps, strings de fecha ISO, o cualquier formato parseable por Date.
5
+ */
6
+ export declare class ParseDatePipe implements PipeTransform<string | number, Date> {
7
+ transform(value: string | number, metadata?: ArgumentMetadata): Date;
8
+ }
@@ -0,0 +1,9 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe que valida que un valor pertenezca a un enum específico.
4
+ */
5
+ export declare class ParseEnumPipe<T extends Record<string, any>> implements PipeTransform<string, T[keyof T]> {
6
+ private readonly enumType;
7
+ constructor(enumType: T);
8
+ transform(value: string, metadata?: ArgumentMetadata): T[keyof T];
9
+ }
@@ -0,0 +1,15 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ export interface ParseFileOptions {
3
+ maxSize?: number;
4
+ allowedMimeTypes?: string[];
5
+ requiredFields?: string[];
6
+ }
7
+ /**
8
+ * Pipe que valida archivos subidos.
9
+ * Puede validar tipo MIME, tamaño máximo y campos requeridos.
10
+ */
11
+ export declare class ParseFilePipe implements PipeTransform {
12
+ private readonly options?;
13
+ constructor(options?: ParseFileOptions | undefined);
14
+ transform(value: any, metadata?: ArgumentMetadata): any;
15
+ }
@@ -0,0 +1,8 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe que convierte un valor a número de punto flotante.
4
+ * Lanza una excepción si el valor no puede ser convertido.
5
+ */
6
+ export declare class ParseFloatPipe implements PipeTransform<string, number> {
7
+ transform(value: string, metadata?: ArgumentMetadata): number;
8
+ }
@@ -0,0 +1,8 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe que convierte un valor a número entero.
4
+ * Lanza una excepción si el valor no puede ser convertido.
5
+ */
6
+ export declare class ParseIntPipe implements PipeTransform<string, number> {
7
+ transform(value: string, metadata?: ArgumentMetadata): number;
8
+ }
@@ -0,0 +1,11 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe que valida que un valor sea un UUID válido.
4
+ * Soporta versiones 3, 4 y 5 de UUID.
5
+ */
6
+ export declare class ParseUUIDPipe implements PipeTransform<string, string> {
7
+ private readonly version?;
8
+ private readonly uuidRegex;
9
+ constructor(version?: "3" | "4" | "5" | undefined);
10
+ transform(value: string, metadata?: ArgumentMetadata): string;
11
+ }
@@ -0,0 +1,8 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../../interfaces';
2
+ /**
3
+ * Pipe de validación genérico que valida objetos contra un esquema de validación.
4
+ * Este es un ejemplo básico - en producción se usaría con librerías como class-validator o zod.
5
+ */
6
+ export declare class ValidationPipe implements PipeTransform {
7
+ transform(value: any, metadata?: ArgumentMetadata): Promise<any>;
8
+ }
@@ -7,6 +7,7 @@ export * from "./features/arguments-host.interface";
7
7
  export * from './type.interface';
8
8
  export * from './handler.interface';
9
9
  export * from './interceptor.interface';
10
+ export * from './pipe.interface';
10
11
  export * from './configuration/redis-configuration.interface';
11
12
  export * from './configuration/mongo-db-configuration.interface';
12
13
  export * from './configuration/aws-s3-configuration.interface';
@@ -1,8 +1,10 @@
1
1
  import { ParamType } from "../enums/param-type.enum";
2
+ import { PipeTransform, PType } from "./pipe.interface";
2
3
  export interface ParamMetadata {
3
4
  index: number;
4
5
  type: ParamType;
5
6
  data?: string;
7
+ pipes?: (PType<PipeTransform> | PipeTransform)[];
6
8
  }
7
9
  export interface HeaderMetadata {
8
10
  name: string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Interface defining the contract for pipes.
3
+ * Pipes have two typical use cases:
4
+ * - transformation: transform input data to the desired form
5
+ * - validation: evaluate input data and if valid, pass it through unchanged; otherwise, throw an exception
6
+ */
7
+ export interface PipeTransform<T = any, R = any> {
8
+ /**
9
+ * Method to implement a custom pipe. Called with two parameters
10
+ * @param value the currently processed input argument
11
+ * @param metadata optional metadata about the current argument
12
+ */
13
+ transform(value: T, metadata?: ArgumentMetadata): R | Promise<R>;
14
+ }
15
+ /**
16
+ * Metadata about the argument being processed by the pipe
17
+ */
18
+ export interface ArgumentMetadata {
19
+ /**
20
+ * Indicates whether argument is a body, query, param, or custom parameter
21
+ */
22
+ type?: 'body' | 'query' | 'param' | 'custom';
23
+ /**
24
+ * Metatype of the argument (e.g., String, Number)
25
+ */
26
+ metatype?: PType<any>;
27
+ /**
28
+ * String value passed to the decorator (e.g., for @Param('id'), data would be 'id')
29
+ */
30
+ data?: string;
31
+ }
32
+ export interface PType<T = any> {
33
+ new (...args: any[]): T;
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hemia/common",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Paquete común para proyectos de Hemia",
5
5
  "main": "dist/hemia-common.js",
6
6
  "module": "dist/hemia-common.esm.js",