Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 39 additions & 38 deletions README.ko.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
7 changes: 7 additions & 0 deletions apps/docs/docs/decorators/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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` 또는 동적 비교를 위한 함수를 받습니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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` для динамического сравнения.
Expand Down
16 changes: 16 additions & 0 deletions apps/example/src/routers/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
IsUrl,
IsHexColor,
IsHexadecimal,
IsHash,
MinDate,
MaxDate,
With,
Expand Down Expand Up @@ -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
}
Comment thread
yuchem2 marked this conversation as resolved.

router.post('/is-hash', bindingCargo(IsHashExample), (req, res) => {
const cargo = getCargo<IsHashExample>(req)
res.json(cargo)
})

class MinDateExample {
@Body()
@MinDate(new Date('2000-01-01'))
Expand Down
1 change: 1 addition & 0 deletions packages/express-cargo/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ClassConstructor<T = any> = 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.
Expand Down
34 changes: 33 additions & 1 deletion packages/express-cargo/src/validator.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -502,6 +502,38 @@ export function IsHexadecimal(message?: cargoErrorMessage): TypedPropertyDecorat
}
}

const hashPatterns: Record<HashAlgorithm, RegExp> = {
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<string> {
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.
Expand Down
151 changes: 151 additions & 0 deletions packages/express-cargo/tests/validator/isHash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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)
})
})
Comment thread
yuchem2 marked this conversation as resolved.

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')
})
})
Loading