diff --git a/README.ko.md b/README.ko.md index cec951a..774bf26 100644 --- a/README.ko.md +++ b/README.ko.md @@ -99,44 +99,45 @@ app.listen(3000) ### 검증 데코레이터 -| 데코레이터 | 설명 | 예시 | -|-------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| `@Optional()` | 값이 없는 경우 밸리데이션을 하지 않음 | `@Optional() value?: number` | -| `@Min(minimum: number)` | 숫자가 `minimum` 이상이어야 함 | `@Min(18) age!: number` | -| `@Max(maximum: number)` | 숫자가 `maximum` 이하이어야 함 | `@Max(100) score!: number` | -| `@Range(min: number, max: number)` | 숫자가 `min` 이상 `max` 이하 범위에 포함되어야 함 | `@Range(1, 5) rating!: number` | -| `@Contains(seed: string)` | 문자열이 `seed` 를 포함해야 함 | `@Contains('hello') greeting!: string` | -| `@Prefix(prefixText: string)` | 문자열이 `prefixText` 로 시작해야 함 | `@Prefix('IMG_') fileName!: string` | -| `@Suffix(suffixText: string)` | 문자열이 `suffixText` 로 끝나야 함 | `@Suffix('.jpg') fileName!: string` | -| `@Length(value: number)` | 문자열 길이가 정확히 `value` 여야 함 | `@Length(6) otp!: string` | -| `@MinLength(min: number)` | 문자열 길이가 `min` 이상이어야 함 | `@MinLength(8) password!: string` | -| `@MaxLength(max: number)` | 문자열 길이가 `max` 이하이어야 함 | `@MaxLength(20) username!: string` | -| `@Equal(value: any)` | 값이 `value` 와 정확히 일치해야 함 | `@Equal('production') env!: string` | -| `@NotEqual(value: any)` | 값이 `value` 와 달라야 함 | `@NotEqual('admin') role!: string` | -| `@IsTrue()` | 값이 `true` 여야 함 | `@IsTrue() acceptedTerms!: boolean` | -| `@IsFalse()` | 값이 `false` 여야 함 | `@IsFalse() blocked!: boolean` | -| `@OneOf(options: readonly any[])` | 값이 `options` 중 하나여야 함 | `@OneOf(['credit','debit'] as const) method!: 'credit' \| 'debit'` | -| `@ListContains(values: any[], comparator?: (expected, actual) => boolean)` | 배열이 지정된 모든 값을 포함해야 함. `comparator` 제공 시 모든 비교를 위임 | `@ListContains([1, 2]) nums!: number[]` | -| `@ListNotContains(values: any[], comparator?: (expected, actual) => boolean)` | 배열이 지정된 값을 포함하지 않아야 함. `comparator` 제공 시 모든 비교를 위임 | `@ListNotContains([1, 2]) nums!: number[]` | -| `@ListMaxSize(max: number, message?)` | 배열의 요소 수가 `max` 이하이어야 함 | `@ListMaxSize(5) tags!: string[]` | -| `@ListMinSize(min: number, message?)` | 배열의 요소 수가 `min` 이상이어야 함 | `@ListMinSize(5) tags!: string[]` | -| `@Enum(enumObj: object, message?)` | 값이 `enumObj`의 멤버여야 함 | `@Enum(UserRole) role!: UserRole` | -| `@Validate(validateFn, message?)` | 커스텀 검증 함수를 사용 | `@Validate(v => typeof v === 'string' && v.includes('@'), 'invalid email') email!: string` | -| `@Regexp(pattern: RegExp, message?)` | 문자열이 주어진 정규식을 만족해야 함 | `@Regexp(/^[0-9]+$/, 'digits only') phone!: string` | -| `@Email()` | 값이 이메일 형식이어야 함 | `@Email() email!: string` | -| `@Uuid(version?, message?)` | 값이 유효한 UUID 형식이어야 하며, 선택적으로 특정 버전(v1, v3, v4, v5)으로 제한할 수 있습니다. | `@Uuid('v4') requestId!: string` | -| `@Alpha(message?: string)` | 문자열에 알파벳(A–Z, a–z)만 포함되어야 합니다. | `@Alpha() firstName!: string ` | -| `@Alphanumeric(message?: string)` | 필드에 알파벳과 숫자(A-Z, a-z, 0-9)만 포함되어야 합니다. | `@Alphanumeric() productCode!: string` | -| `@IsUppercase(message?: string)` | 필드에 대문자만 포함되어야 합니다. | `@IsUppercase() countryCode!: string` | -| `@IsLowercase(message?: string)` | 필드에 소문자만 포함되어야 합니다. | `@IsLowercase() username!: string` | -| `@IsJwt(message?: string)` | 필드가 유효한 JSON Web Token(JWT)인지 검증합니다. Base64URL 문자로 이루어진 `header.payload.signature` 형식이어야 합니다. | `@IsJwt() accessToken!: string` | -| `@IsUrl(options?: IsUrlOptions, message?: string)` | 필드가 유효한 URL인지 검증합니다. 기본적으로 `http`, `https`, `ftp` 프로토콜을 허용하며, `options.protocols`로 변경할 수 있습니다. | `@IsUrl() website!: string` | -| `@IsHexColor(message?: string)` | 필드가 유효한 16진수 색상 코드인지 검증합니다. `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` 형식을 지원합니다(대소문자 구분 없음). | `@IsHexColor() color!: string` | -| `@IsHexadecimal(message?: string)` | 필드가 16진수 숫자인지 검증합니다. `0-9` 및 `a-f` 문자(대소문자 구분 없음)만 허용되며, `0x` 접두사도 허용됩니다. | `@IsHexadecimal() color!: string` | -| `@MinDate(min: Date \| (() => Date), message?: string)` | 필드가 주어진 최소 날짜와 같거나 이후인 `Date`인지 검증합니다. 고정 날짜 또는 동적 비교를 위한 함수를 받습니다. | `@MinDate(new Date('2000-01-01')) createdAt!: Date` | -| `@MaxDate(max: Date \| (() => Date), message?: string)` | 필드가 주어진 최대 날짜와 같거나 이전인 `Date`인지 검증합니다. 고정 날짜 또는 동적 비교를 위한 함수를 받습니다. | `@MaxDate(new Date('2099-12-31')) createdAt!: Date` | -| `@With(fieldName: string)` | 데코레이터가 적용된 필드에 값이 있을 경우, 지정된 대상 필드 (fieldName)도 반드시 값을 가져야 함을 검증하여, 두 필드 간의 필수적인 의존 관계를 설정합니다. | `@With('price') discountRate?: number` | -| `@Without(fieldName: string)` | 데코레이터가 선언된 필드에 값이 있을 경우, 지정된 타겟 필드(fieldName)는 반드시 값이 없어야 함을 검증하여 상호 배타적 관계를 설정합니다. | `@Without('isGuest') password?: string` | +| 데코레이터 | 설명 | 예시 | +|-------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| +| `@Optional()` | 값이 없는 경우 밸리데이션을 하지 않음 | `@Optional() value?: number` | +| `@Min(minimum: number)` | 숫자가 `minimum` 이상이어야 함 | `@Min(18) age!: number` | +| `@Max(maximum: number)` | 숫자가 `maximum` 이하이어야 함 | `@Max(100) score!: number` | +| `@Range(min: number, max: number)` | 숫자가 `min` 이상 `max` 이하 범위에 포함되어야 함 | `@Range(1, 5) rating!: number` | +| `@Contains(seed: string)` | 문자열이 `seed` 를 포함해야 함 | `@Contains('hello') greeting!: string` | +| `@Prefix(prefixText: string)` | 문자열이 `prefixText` 로 시작해야 함 | `@Prefix('IMG_') fileName!: string` | +| `@Suffix(suffixText: string)` | 문자열이 `suffixText` 로 끝나야 함 | `@Suffix('.jpg') fileName!: string` | +| `@Length(value: number)` | 문자열 길이가 정확히 `value` 여야 함 | `@Length(6) otp!: string` | +| `@MinLength(min: number)` | 문자열 길이가 `min` 이상이어야 함 | `@MinLength(8) password!: string` | +| `@MaxLength(max: number)` | 문자열 길이가 `max` 이하이어야 함 | `@MaxLength(20) username!: string` | +| `@Equal(value: any)` | 값이 `value` 와 정확히 일치해야 함 | `@Equal('production') env!: string` | +| `@NotEqual(value: any)` | 값이 `value` 와 달라야 함 | `@NotEqual('admin') role!: string` | +| `@IsTrue()` | 값이 `true` 여야 함 | `@IsTrue() acceptedTerms!: boolean` | +| `@IsFalse()` | 값이 `false` 여야 함 | `@IsFalse() blocked!: boolean` | +| `@OneOf(options: readonly any[])` | 값이 `options` 중 하나여야 함 | `@OneOf(['credit','debit'] as const) method!: 'credit' \| 'debit'` | +| `@ListContains(values: any[], comparator?: (expected, actual) => boolean)` | 배열이 지정된 모든 값을 포함해야 함. `comparator` 제공 시 모든 비교를 위임 | `@ListContains([1, 2]) nums!: number[]` | +| `@ListNotContains(values: any[], comparator?: (expected, actual) => boolean)` | 배열이 지정된 값을 포함하지 않아야 함. `comparator` 제공 시 모든 비교를 위임 | `@ListNotContains([1, 2]) nums!: number[]` | +| `@ListMaxSize(max: number, message?)` | 배열의 요소 수가 `max` 이하이어야 함 | `@ListMaxSize(5) tags!: string[]` | +| `@ListMinSize(min: number, message?)` | 배열의 요소 수가 `min` 이상이어야 함 | `@ListMinSize(5) tags!: string[]` | +| `@Enum(enumObj: object, message?)` | 값이 `enumObj`의 멤버여야 함 | `@Enum(UserRole) role!: UserRole` | +| `@Validate(validateFn, message?)` | 커스텀 검증 함수를 사용 | `@Validate(v => typeof v === 'string' && v.includes('@'), 'invalid email') email!: string` | +| `@Regexp(pattern: RegExp, message?)` | 문자열이 주어진 정규식을 만족해야 함 | `@Regexp(/^[0-9]+$/, 'digits only') phone!: string` | +| `@Email()` | 값이 이메일 형식이어야 함 | `@Email() email!: string` | +| `@Uuid(version?, message?)` | 값이 유효한 UUID 형식이어야 하며, 선택적으로 특정 버전(v1, v3, v4, v5)으로 제한할 수 있습니다. | `@Uuid('v4') requestId!: string` | +| `@Alpha(message?: string)` | 문자열에 알파벳(A–Z, a–z)만 포함되어야 합니다. | `@Alpha() firstName!: string ` | +| `@Alphanumeric(message?: string)` | 필드에 알파벳과 숫자(A-Z, a-z, 0-9)만 포함되어야 합니다. | `@Alphanumeric() productCode!: string` | +| `@IsUppercase(message?: string)` | 필드에 대문자만 포함되어야 합니다. | `@IsUppercase() countryCode!: string` | +| `@IsLowercase(message?: string)` | 필드에 소문자만 포함되어야 합니다. | `@IsLowercase() username!: string` | +| `@IsJwt(message?: string)` | 필드가 유효한 JSON Web Token(JWT)인지 검증합니다. Base64URL 문자로 이루어진 `header.payload.signature` 형식이어야 합니다. | `@IsJwt() accessToken!: string` | +| `@IsUrl(options?: IsUrlOptions, message?: string)` | 필드가 유효한 URL인지 검증합니다. 기본적으로 `http`, `https`, `ftp` 프로토콜을 허용하며, `options.protocols`로 변경할 수 있습니다. | `@IsUrl() website!: string` | +| `@IsHexColor(message?: string)` | 필드가 유효한 16진수 색상 코드인지 검증합니다. `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` 형식을 지원합니다(대소문자 구분 없음). | `@IsHexColor() color!: string` | +| `@IsHexadecimal(message?: string)` | 필드가 16진수 숫자인지 검증합니다. `0-9` 및 `a-f` 문자(대소문자 구분 없음)만 허용되며, `0x` 접두사도 허용됩니다. | `@IsHexadecimal() color!: string` | +| `@IsHash(algorithm: HashAlgorithm, message?: string)` | 필드가 주어진 해시 알고리즘에 대한 유효한 해시 문자열인지 검증합니다. 지원: `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `crc32`, `crc32b`. | `@IsHash('sha256') checksum!: string` | +| `@MinDate(min: Date \| (() => Date), message?: string)` | 필드가 주어진 최소 날짜와 같거나 이후인 `Date`인지 검증합니다. 고정 날짜 또는 동적 비교를 위한 함수를 받습니다. | `@MinDate(new Date('2000-01-01')) createdAt!: Date` | +| `@MaxDate(max: Date \| (() => Date), message?: string)` | 필드가 주어진 최대 날짜와 같거나 이전인 `Date`인지 검증합니다. 고정 날짜 또는 동적 비교를 위한 함수를 받습니다. | `@MaxDate(new Date('2099-12-31')) createdAt!: Date` | +| `@With(fieldName: string)` | 데코레이터가 적용된 필드에 값이 있을 경우, 지정된 대상 필드 (fieldName)도 반드시 값을 가져야 함을 검증하여, 두 필드 간의 필수적인 의존 관계를 설정합니다. | `@With('price') discountRate?: number` | +| `@Without(fieldName: string)` | 데코레이터가 선언된 필드에 값이 있을 경우, 지정된 타겟 필드(fieldName)는 반드시 값이 없어야 함을 검증하여 상호 배타적 관계를 설정합니다. | `@Without('isGuest') password?: string` | --- diff --git a/README.md b/README.md index 836becf..771f650 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ Full guide and API reference: | `@IsUrl(options?: IsUrlOptions, message?: string)` | Validates that the field is a valid URL. `http`, `https`, and `ftp` protocols are allowed by default. Use `options.protocols` to customize allowed protocols. | `@IsUrl() website!: string` | | `@IsHexColor(message?: string)` | Validates that the field is a valid hex color code. Supports `#RGB`, `#RGBA`, `#RRGGBB`, and `#RRGGBBAA` formats (case-insensitive). The `#` prefix is required. | `@IsHexColor() color!: string` | | `@IsHexadecimal(message?: string)` | Validates that the field is a hexadecimal number (characters `0-9` and `a-f`, case-insensitive). The `0x` prefix is also allowed. | `@IsHexadecimal() color!: string` | +| `@IsHash(algorithm: HashAlgorithm, message?: string)` | Validates that the field is a valid hash string for the given algorithm. Supported: `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `crc32`, `crc32b`. | `@IsHash('sha256') checksum!: string` | | `@MinDate(min: Date \| (() => Date), message?: string)` | Validates that the field is a `Date` on or after the given minimum date. Accepts a fixed date or a function for dynamic comparison. | `@MinDate(new Date('2000-01-01')) createdAt!: Date` | | `@MaxDate(max: Date \| (() => Date), message?: string)` | Validates that the field is a `Date` on or before the given maximum date. Accepts a fixed date or a function for dynamic comparison. | `@MaxDate(new Date('2099-12-31')) createdAt!: Date` | | `@With(fieldName: string)` | Validates that if the decorated field has a value, the specified target field (fieldName) must also have a value, establishing a mandatory dependency. | `@With('price') discountRate?: number` | diff --git a/apps/docs/docs/decorators/validators.md b/apps/docs/docs/decorators/validators.md index 4691492..a1deb5f 100644 --- a/apps/docs/docs/decorators/validators.md +++ b/apps/docs/docs/decorators/validators.md @@ -197,6 +197,13 @@ Validates that the decorated field is a hexadecimal number. The value must conta - **`message`** (optional): The error message to display when validation fails. If omitted, a default message will be used. +### `@IsHash(algorithm: HashAlgorithm, message?: string)` + +Validates that the decorated field is a valid hash string for the given algorithm. Supported algorithms: `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `crc32`, `crc32b`. The value must be a hexadecimal string with the exact length required by the algorithm. + +- **`algorithm`**: The hash algorithm to validate against. +- **`message`** (optional): The error message to display when validation fails. If omitted, a default message will be used. + ### `@MinDate(min: Date | (() => Date), message?: string)` Validates that the decorated field is a `Date` that is on or after the given minimum date. Accepts a fixed `Date` or a function that returns a `Date` for dynamic comparison. diff --git a/apps/docs/i18n/de/docusaurus-plugin-content-docs/current/decorators/validators.md b/apps/docs/i18n/de/docusaurus-plugin-content-docs/current/decorators/validators.md index b9941fa..62dc276 100644 --- a/apps/docs/i18n/de/docusaurus-plugin-content-docs/current/decorators/validators.md +++ b/apps/docs/i18n/de/docusaurus-plugin-content-docs/current/decorators/validators.md @@ -197,6 +197,13 @@ Validiert, dass das dekorierte Feld eine hexadezimale Zahl ist. Nur die Zeichen - **`message`** (optional): Die Fehlermeldung, die angezeigt wird, wenn die Validierung fehlschlägt. Wenn weggelassen, wird eine Standardmeldung verwendet. +### `@IsHash(algorithm: HashAlgorithm, message?: string)` + +Validiert, dass das dekorierte Feld ein gültiger Hash-String für den angegebenen Algorithmus ist. Unterstützte Algorithmen: `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `crc32`, `crc32b`. Der Wert muss ein hexadezimaler String mit der exakten Länge sein, die der Algorithmus erfordert. + +- **`algorithm`**: Der Hash-Algorithmus, gegen den validiert werden soll. +- **`message`** (optional): Die Fehlermeldung, die angezeigt wird, wenn die Validierung fehlschlägt. Wenn weggelassen, wird eine Standardmeldung verwendet. + ### `@MinDate(min: Date | (() => Date), message?: string)` Validiert, dass das dekorierte Feld ein `Date` ist, das gleich oder nach dem angegebenen Mindestdatum liegt. Akzeptiert ein festes `Date` oder eine Funktion, die ein `Date` zurückgibt. diff --git a/apps/docs/i18n/fr/docusaurus-plugin-content-docs/current/decorators/validators.md b/apps/docs/i18n/fr/docusaurus-plugin-content-docs/current/decorators/validators.md index 5233d07..88a955c 100644 --- a/apps/docs/i18n/fr/docusaurus-plugin-content-docs/current/decorators/validators.md +++ b/apps/docs/i18n/fr/docusaurus-plugin-content-docs/current/decorators/validators.md @@ -197,6 +197,13 @@ Valide que le champ décoré est un nombre hexadécimal. Seuls les caractères ` - **`message`** (optionnel) : Le message d'erreur à afficher lorsque la validation échoue. S'il est omis, un message par défaut sera utilisé. +### `@IsHash(algorithm: HashAlgorithm, message?: string)` + +Valide que le champ décoré est une chaîne de hachage valide pour l'algorithme donné. Algorithmes pris en charge : `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `crc32`, `crc32b`. La valeur doit être une chaîne hexadécimale avec la longueur exacte requise par l'algorithme. + +- **`algorithm`** : L'algorithme de hachage contre lequel valider. +- **`message`** (optionnel) : Le message d'erreur à afficher lorsque la validation échoue. S'il est omis, un message par défaut sera utilisé. + ### `@MinDate(min: Date | (() => Date), message?: string)` Valide que le champ décoré est un `Date` égal ou postérieur à la date minimale donnée. Accepte un `Date` fixe ou une fonction qui retourne un `Date` pour une comparaison dynamique. diff --git a/apps/docs/i18n/ko/docusaurus-plugin-content-docs/current/decorators/validators.md b/apps/docs/i18n/ko/docusaurus-plugin-content-docs/current/decorators/validators.md index 67704dc..5c74a75 100644 --- a/apps/docs/i18n/ko/docusaurus-plugin-content-docs/current/decorators/validators.md +++ b/apps/docs/i18n/ko/docusaurus-plugin-content-docs/current/decorators/validators.md @@ -241,6 +241,13 @@ title: 유효성 검사 데코레이터 - **`message`** (선택 사항): 검증 실패 시 표시할 메시지. 생략하면 기본 메시지가 사용됩니다. +### `@IsHash(algorithm: HashAlgorithm, message?: string)` + +데코레이터가 적용된 필드가 주어진 해시 알고리즘에 대한 유효한 해시 문자열인지 검증합니다. 지원 알고리즘: `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `crc32`, `crc32b`. 값은 알고리즘이 요구하는 정확한 길이의 16진수 문자열이어야 합니다. + +- **`algorithm`**: 검증할 해시 알고리즘. +- **`message`** (선택 사항): 검증 실패 시 표시할 메시지. 생략하면 기본 메시지가 사용됩니다. + ### `@MinDate(min: Date | (() => Date), message?: string)` 데코레이터가 적용된 필드가 주어진 최소 날짜와 같거나 이후인 `Date`인지 검증합니다. 고정 `Date` 또는 동적 비교를 위한 함수를 받습니다. diff --git a/apps/docs/i18n/ru/docusaurus-plugin-content-docs/current/decorators/validators.md b/apps/docs/i18n/ru/docusaurus-plugin-content-docs/current/decorators/validators.md index aa19a3a..b705e10 100644 --- a/apps/docs/i18n/ru/docusaurus-plugin-content-docs/current/decorators/validators.md +++ b/apps/docs/i18n/ru/docusaurus-plugin-content-docs/current/decorators/validators.md @@ -197,6 +197,13 @@ Express-Cargo использует декораторы для валидаци - **`message`** (необязательно): Сообщение об ошибке, которое будет отображаться при сбое валидации. Если опущено, будет использоваться сообщение по умолчанию. +### `@IsHash(algorithm: HashAlgorithm, message?: string)` + +Проверяет, что декорированное поле является допустимой хеш-строкой для указанного алгоритма. Поддерживаемые алгоритмы: `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `crc32`, `crc32b`. Значение должно быть шестнадцатеричной строкой точной длины, требуемой алгоритмом. + +- **`algorithm`**: Алгоритм хеширования для валидации. +- **`message`** (необязательно): Сообщение об ошибке, которое будет отображаться при сбое валидации. Если опущено, будет использоваться сообщение по умолчанию. + ### `@MinDate(min: Date | (() => Date), message?: string)` Проверяет, что декорированное поле является `Date`, равным или более поздним, чем заданная минимальная дата. Принимает фиксированный `Date` или функцию, возвращающую `Date` для динамического сравнения. diff --git a/apps/example/src/routers/validator.ts b/apps/example/src/routers/validator.ts index ec8c94b..4adb201 100644 --- a/apps/example/src/routers/validator.ts +++ b/apps/example/src/routers/validator.ts @@ -29,6 +29,7 @@ import { IsUrl, IsHexColor, IsHexadecimal, + IsHash, MinDate, MaxDate, With, @@ -358,6 +359,21 @@ router.post('/is-hexadecimal', bindingCargo(IsHexadecimalExample), (req, res) => res.json(cargo) }) +class IsHashExample { + @Body() + @IsHash('md5') + md5!: string + + @Body() + @IsHash('sha256') + sha256!: string +} + +router.post('/is-hash', bindingCargo(IsHashExample), (req, res) => { + const cargo = getCargo(req) + res.json(cargo) +}) + class MinDateExample { @Body() @MinDate(new Date('2000-01-01')) diff --git a/packages/express-cargo/src/types.ts b/packages/express-cargo/src/types.ts index 961b2d5..83906ef 100644 --- a/packages/express-cargo/src/types.ts +++ b/packages/express-cargo/src/types.ts @@ -19,6 +19,7 @@ export type ClassConstructor = new (...args: any[]) => T export type validArrayElementType = typeof String | typeof Number | typeof Boolean | typeof Date | ClassConstructor export type ArrayElementType = validArrayElementType | 'string' | 'number' | 'boolean' | 'date' export type UuidVersion = 'v1' | 'v3' | 'v4' | 'v5' | 'all' +export type HashAlgorithm = 'md5' | 'sha1' | 'sha256' | 'sha384' | 'sha512' | 'crc32' | 'crc32b' /** * Options for the `@IsUrl` decorator. diff --git a/packages/express-cargo/src/validator.ts b/packages/express-cargo/src/validator.ts index 9c28a30..2353f55 100644 --- a/packages/express-cargo/src/validator.ts +++ b/packages/express-cargo/src/validator.ts @@ -1,4 +1,4 @@ -import { ArrayComparator, cargoErrorMessage, EachValidatorRule, IsUrlOptions, TypedPropertyDecorator, UuidVersion, ValidatorRule } from './types' +import { ArrayComparator, cargoErrorMessage, EachValidatorRule, HashAlgorithm, IsUrlOptions, TypedPropertyDecorator, UuidVersion, ValidatorRule } from './types' import { CargoClassMetadata } from './metadata' import { isDeepEqual } from './utils' @@ -502,6 +502,38 @@ export function IsHexadecimal(message?: cargoErrorMessage): TypedPropertyDecorat } } +const hashPatterns: Record = { + md5: /^[0-9a-f]{32}$/i, + sha1: /^[0-9a-f]{40}$/i, + sha256: /^[0-9a-f]{64}$/i, + sha384: /^[0-9a-f]{96}$/i, + sha512: /^[0-9a-f]{128}$/i, + crc32: /^[0-9a-f]{8}$/i, + crc32b: /^[0-9a-f]{8}$/i, +} + +/** + * Checks if the string is a valid hash for the specified algorithm. + * Supported algorithms: md5, sha1, sha256, sha384, sha512, crc32, crc32b. + * @param algorithm - The hash algorithm to validate against. + * @param message - Optional custom error message. + */ +export function IsHash(algorithm: HashAlgorithm, message?: cargoErrorMessage): TypedPropertyDecorator { + const pattern = hashPatterns[algorithm] + return (target, propertyKey): void => { + addValidator( + target, + propertyKey, + new ValidatorRule( + propertyKey, + 'isHash', + (value: unknown) => typeof value === 'string' && pattern.test(value), + message || `${String(propertyKey)} should be a valid ${algorithm} hash`, + ), + ) + } +} + /** * Checks if the Date value is greater than or equal to the given minimum date. * @param min - The minimum allowed date, or a function that returns it. diff --git a/packages/express-cargo/tests/validator/isHash.test.ts b/packages/express-cargo/tests/validator/isHash.test.ts new file mode 100644 index 0000000..535d873 --- /dev/null +++ b/packages/express-cargo/tests/validator/isHash.test.ts @@ -0,0 +1,166 @@ +import { CargoFieldError, IsHash } from '../../src' +import { CargoClassMetadata } from '../../src/metadata' + +describe('isHash decorator', () => { + class Sample { + @IsHash('md5') + md5Hash!: string + + @IsHash('sha1') + sha1Hash!: string + + @IsHash('sha256') + sha256Hash!: string + + @IsHash('sha384') + sha384Hash!: string + + @IsHash('sha512') + sha512Hash!: string + + @IsHash('crc32') + crc32Hash!: string + + @IsHash('crc32b') + crc32bHash!: string + + noValidatorValue!: string + } + + const classMeta = new CargoClassMetadata(Sample.prototype) + + describe('md5', () => { + const meta = classMeta.getFieldMetadata('md5Hash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + it('should have isHash validator', () => { + expect(rule).toBeDefined() + expect(rule?.message).toBe('md5Hash should be a valid md5 hash') + }) + + it('should pass for valid md5 hash', () => { + expect(rule!.validate('d41d8cd98f00b204e9800998ecf8427e')).toBeNull() + expect(rule!.validate('D41D8CD98F00B204E9800998ECF8427E')).toBeNull() + }) + + it('should fail for invalid md5 hash', () => { + expect(rule!.validate('d41d8cd98f00b204e9800998ecf8427')).toBeInstanceOf(CargoFieldError) + expect(rule!.validate('d41d8cd98f00b204e9800998ecf8427ez')).toBeInstanceOf(CargoFieldError) + expect(rule!.validate('not-a-hash')).toBeInstanceOf(CargoFieldError) + expect(rule!.validate('')).toBeInstanceOf(CargoFieldError) + }) + }) + + describe('sha1', () => { + const meta = classMeta.getFieldMetadata('sha1Hash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + it('should pass for valid sha1 hash', () => { + expect(rule!.validate('da39a3ee5e6b4b0d3255bfef95601890afd80709')).toBeNull() + }) + + it('should fail for invalid sha1 hash', () => { + expect(rule!.validate('da39a3ee5e6b4b0d3255bfef95601890afd8070')).toBeInstanceOf(CargoFieldError) + expect(rule!.validate('not-a-hash')).toBeInstanceOf(CargoFieldError) + }) + }) + + describe('sha256', () => { + const meta = classMeta.getFieldMetadata('sha256Hash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + it('should pass for valid sha256 hash', () => { + expect(rule!.validate('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')).toBeNull() + }) + + it('should fail for invalid sha256 hash', () => { + expect(rule!.validate('e3b0c44298fc1c149afbf4c8996fb924')).toBeInstanceOf(CargoFieldError) + expect(rule!.validate('not-a-hash')).toBeInstanceOf(CargoFieldError) + }) + }) + + describe('sha384', () => { + const meta = classMeta.getFieldMetadata('sha384Hash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + it('should pass for valid sha384 hash', () => { + expect(rule!.validate('38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b')).toBeNull() + }) + + it('should fail for invalid sha384 hash', () => { + expect(rule!.validate('38b060a751ac96384cd9327eb1b1e36a')).toBeInstanceOf(CargoFieldError) + }) + }) + + describe('sha512', () => { + const meta = classMeta.getFieldMetadata('sha512Hash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + it('should pass for valid sha512 hash', () => { + expect(rule!.validate('cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e')).toBeNull() + }) + + it('should fail for invalid sha512 hash', () => { + expect(rule!.validate('cf83e1357eefb8bdf1542850d66d8007')).toBeInstanceOf(CargoFieldError) + }) + }) + + describe('crc32', () => { + const meta = classMeta.getFieldMetadata('crc32Hash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + it('should pass for valid crc32 hash', () => { + expect(rule!.validate('00000000')).toBeNull() + expect(rule!.validate('cbf43926')).toBeNull() + }) + + it('should fail for invalid crc32 hash', () => { + expect(rule!.validate('0000000')).toBeInstanceOf(CargoFieldError) + expect(rule!.validate('cbf439260')).toBeInstanceOf(CargoFieldError) + }) + }) + + describe('crc32b', () => { + const meta = classMeta.getFieldMetadata('crc32bHash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + it('should pass for valid crc32b hash', () => { + expect(rule!.validate('00000000')).toBeNull() + expect(rule!.validate('cbf43926')).toBeNull() + }) + + it('should fail for invalid crc32b hash', () => { + expect(rule!.validate('0000000')).toBeInstanceOf(CargoFieldError) + expect(rule!.validate('cbf439260')).toBeInstanceOf(CargoFieldError) + }) + }) + + it('should fail for non-string values', () => { + const meta = classMeta.getFieldMetadata('md5Hash') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + + expect(rule!.validate(null)).toBeInstanceOf(CargoFieldError) + expect(rule!.validate(undefined)).toBeInstanceOf(CargoFieldError) + expect(rule!.validate(123)).toBeInstanceOf(CargoFieldError) + }) + + it('should not have isHash validator on undecorated field', () => { + const meta = classMeta.getFieldMetadata('noValidatorValue') + const rule = meta.getValidators()?.find(v => v.type === 'isHash') + expect(rule).toBeUndefined() + }) + + it('should support custom error message', () => { + class CustomMessage { + @IsHash('sha256', 'custom error') + hash!: string + } + + const customMeta = new CargoClassMetadata(CustomMessage.prototype) + const rule = customMeta.getFieldMetadata('hash').getValidators()?.find(v => v.type === 'isHash') + + const error = rule?.validate('invalid') + expect(error).toBeInstanceOf(CargoFieldError) + expect(error?.message).toBe('custom error') + }) +}) \ No newline at end of file