Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
1 change: 1 addition & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ app.listen(3000)
| `@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` |
| `@With(fieldName: string)` | 데코레이터가 적용된 필드에 값이 있을 경우, 지정된 대상 필드 (fieldName)도 반드시 값을 가져야 함을 검증하여, 두 필드 간의 필수적인 의존 관계를 설정합니다. | `@With('price') discountRate?: number` |
| `@Without(fieldName: string)` | 데코레이터가 선언된 필드에 값이 있을 경우, 지정된 타겟 필드(fieldName)는 반드시 값이 없어야 함을 검증하여 상호 배타적 관계를 설정합니다. | `@Without('isGuest') password?: string` |

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Full guide and API reference:
| `@Uuid(version?, message?)` | Validates that the field is a valid UUID, optionally restricted to a specific version (v1, v3, v4, or v5). | `@Uuid('v4') requestId!: string` |
| `@Alpha(message?: string)` | Validates that the field contains alphabetic characters (A–Z, a–z) only. | @Alpha() firstName!: string |
| `@Alphanumeric(message?: string)` | Validates that the field contains alphanumeric characters (A-Z, a-z, 0-9) only. | `@Alphanumeric() productCode!: string` |
| `@IsUppercase(message?: string)` | Validates that the field contains only uppercase characters. | `@IsUppercase() countryCode!: string` |
| `@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` |
| `@Without(fieldName: string)` | Validates that if the decorated field has a value, the specified target field (fieldName) must NOT have a value, establishing a mutually exclusive relationship. | `@Without('isGuest') password?: string` |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ Validiert, dass das dekorierte Feld nur alphanumerische Zeichen enthält (englis

- **`message`** (optional): Die Fehlermeldung, die angezeigt wird, wenn die Validierung fehlschlägt. Wenn weggelassen, wird eine Standardmeldung verwendet.

---

### `@IsUppercase(message?: string)`

Validiert, dass das dekorierte Feld nur Großbuchstaben enthält.

- **`message`** (optional): Die Fehlermeldung, die angezeigt wird, wenn die Validierung fehlschlägt. Wenn weggelassen, wird eine Standardmeldung verwendet.

---

### `@With(fieldName: string, message?: string)`

Validiert, dass, wenn das dekorierte Feld einen Wert hat, das angegebene Zielfeld (fieldName) ebenfalls einen Wert haben muss, wodurch eine zwingende Abhängigkeit zwischen den beiden Feldern hergestellt wird.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ Valide que le champ décoré contient uniquement des caractères alphanumérique

- **`message`** (optionnel) : Le message d'erreur à afficher lorsque la validation échoue. S'il est omis, un message par défaut sera utilisé.

---

### `@IsUppercase(message?: string)`

Valide que le champ décoré contient uniquement des caractères en majuscules.

- **`message`** (optionnel) : Le message d'erreur à afficher lorsque la validation échoue. S'il est omis, un message par défaut sera utilisé.

---

### `@With(fieldName: string, message?: string)`

Valide que si le champ décoré a une valeur, le champ cible spécifié (fieldName) doit également avoir une valeur, établissant une dépendance obligatoire entre les deux champs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ title: 유효성 검사 데코레이터

---

### `@IsUppercase(message?: string)`

데코레이터가 적용된 필드에 대문자만 포함되어 있는지 검증합니다.

- **`message`** (선택 사항): 검증 실패 시 표시할 메시지. 생략하면 기본 메시지가 사용됩니다.

---

### `@With(fieldName: string, message?: string)`

데코레이터가 적용된 속성이 값을 가질 경우, 지정된 대상 속성 또한 반드시 값을 가지고 있어야 함을 검증하여 두 속성 간에 필수적인 의존 관계를 설정합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ Express-Cargo использует декораторы для валидаци

- **`message`** (необязательно): Сообщение об ошибке, которое будет отображаться при сбое валидации. Если опущено, будет использоваться сообщение по умолчанию.

---

### `@IsUppercase(message?: string)`

Проверяет, что декорированное поле содержит только символы верхнего регистра.

- **`message`** (необязательно): Сообщение об ошибке, которое будет отображаться при сбое валидации. Если опущено, будет использоваться сообщение по умолчанию.

---

### `@With(fieldName: string, message?: string)`

Проверяет, что если декорированное поле имеет значение, указанное целевое поле (fieldName) также должно иметь значение, устанавливая обязательную зависимость между двумя полями.
Expand Down
22 changes: 22 additions & 0 deletions apps/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,28 @@ curl -X POST 'http://localhost:3000/alphanumeric' \
```
---

### @IsUppercase

```typescript
class IsUppercaseExample {
@Body()
@IsUppercase()
text!: string
}

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

```shell
curl -X POST 'http://localhost:3000/is-uppercase' \
-H 'Content-Type: application/json' \
-d '{ "text": "HELLO" }'
```
---

### @With

```typescript
Expand Down
12 changes: 12 additions & 0 deletions apps/example/src/routers/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
Alpha,
Uuid,
Alphanumeric,
IsUppercase,
With,
Without,
Enum,
Expand Down Expand Up @@ -271,6 +272,17 @@ router.post('/alphanumeric', bindingCargo(AlphanumericExample), (req, res) => {
res.json(cargo)
})

class IsUppercaseExample {
@Body()
@IsUppercase()
text!: string
}

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

class WithExample {
@Body()
limit!: number
Expand Down
19 changes: 19 additions & 0 deletions packages/express-cargo/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,25 @@ export function Alphanumeric(message?: cargoErrorMessage): TypedPropertyDecorato
}
}

/**
* Checks if the string contains only uppercase characters.
* @param message - Optional custom error message.
*/
export function IsUppercase(message?: cargoErrorMessage): TypedPropertyDecorator<string> {
return (target, propertyKey): void => {
addValidator(
target,
propertyKey,
new ValidatorRule(
propertyKey,
'isUppercase',
(value: unknown) => typeof value === 'string' && value === value.toUpperCase(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 구현은 value.toUpperCase()를 호출하여 새로운 문자열을 할당합니다. 이 방식은 매우 긴 문자열을 검증할 때 미미한 성능 저하를 유발할 수 있습니다.

Alpha, Alphanumeric와 같은 다른 검증기와의 일관성을 위해, 그리고 잠재적인 성능 향상을 위해 정규식을 사용하는 것을 제안합니다.

Suggested change
(value: unknown) => typeof value === 'string' && value === value.toUpperCase(),
(value: unknown) => typeof value === 'string' && !/[a-z]/.test(value),
References
  1. 유사한 기능을 하는 검증기들은 일관된 방식으로 구현하는 것이 좋습니다. 또한, 불필요한 문자열 할당을 피해 성능을 개선할 수 있습니다.

message || `${String(propertyKey)} should be uppercase`,
),
)
}
}

/**
* Validates that if the decorated property has a value, the specified field must also be present.
* @param fieldName - The name of the required field.
Expand Down
71 changes: 71 additions & 0 deletions packages/express-cargo/tests/validator/isUppercase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CargoFieldError, IsUppercase } from '../../src'
import { CargoClassMetadata } from '../../src/metadata'

describe('isUppercase decorator', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

테스트 코드의 가독성과 유지보수성을 높이기 위해, 여러 it 블록에서 반복되는 isUppercaseRule을 가져오는 로직을 describe 블록 상단으로 옮기는 것을 제안합니다. 이렇게 하면 중복 코드를 줄이고 각 테스트의 의도를 더 명확하게 파악할 수 있습니다.

예를 들어, 다음과 같이 리팩토링할 수 있습니다:

describe('isUppercase decorator', () => {
    class Sample {
        @IsUppercase()
        uppercaseValue!: string

        noValidatorValue!: string
    }

    const classMeta = new CargoClassMetadata(Sample.prototype);
    const meta = classMeta.getFieldMetadata('uppercaseValue');
    const isUppercaseRule = meta.getValidators()?.find(v => v.type === 'isUppercase');

    it('should have isUppercase validator', () => {
        expect(isUppercaseRule).toBeDefined();
        expect(isUppercaseRule?.message).toBe('uppercaseValue should be uppercase');
    });

    it('should pass for all-uppercase strings', () => {
        expect(isUppercaseRule?.validate('HELLO')).toBeNull();
        // ...
    });

    // ... other tests using isUppercaseRule
});
References
  1. 코드는 항상 읽기 쉽고 이해하기 쉬워야 합니다. 테스트 코드에서 반복되는 로직을 줄이면 가독성과 유지보수성이 향상됩니다. (link)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

IsUppercase 유효성 검사기의 견고성을 높이기 위해 유니코드 문자에 대한 테스트 케이스를 추가하는 것을 권장합니다. 현재 테스트는 ASCII 문자만 다루고 있어, 다국어 환경에서의 동작을 보장하기 어렵습니다.

아래와 같은 테스트 케이스를 추가하여 악센트가 있는 문자나 다른 언어의 대소문자를 올바르게 처리하는지 확인할 수 있습니다.

it('should handle unicode characters correctly', () => {
    // The following will pass with the suggested implementation change
    expect(isUppercaseRule?.validate('ÄÖÜ')).toBeNull(); // Uppercase with accents
    expect(isUppercaseRule?.validate('ÄöÜ')).toBeInstanceOf(CargoFieldError); // Mixed case with accents
    expect(isUppercaseRule?.validate('ПРИВЕТ')).toBeNull(); // Uppercase Cyrillic
    expect(isUppercaseRule?.validate('Привет')).toBeInstanceOf(CargoFieldError); // Mixed case Cyrillic
    expect(isUppercaseRule?.validate('你好')).toBeNull(); // Non-cased characters should pass
});

class Sample {
@IsUppercase()
uppercaseValue!: string

noValidatorValue!: string
}

const classMeta = new CargoClassMetadata(Sample.prototype)

it('should have isUppercase validator', () => {
const meta = classMeta.getFieldMetadata('uppercaseValue')
const isUppercaseRule = meta.getValidators()?.find(v => v.type === 'isUppercase')

expect(isUppercaseRule).toBeDefined()
expect(isUppercaseRule?.message).toBe('uppercaseValue should be uppercase')
})

it('should pass for all-uppercase strings', () => {
const meta = classMeta.getFieldMetadata('uppercaseValue')
const isUppercaseRule = meta.getValidators()?.find(v => v.type === 'isUppercase')

expect(isUppercaseRule?.validate('HELLO')).toBeNull()
expect(isUppercaseRule?.validate('HELLO WORLD')).toBeNull()
expect(isUppercaseRule?.validate('HELLO123')).toBeNull()
expect(isUppercaseRule?.validate('')).toBeNull()
})

it('should fail for strings with lowercase characters', () => {
const meta = classMeta.getFieldMetadata('uppercaseValue')
const isUppercaseRule = meta.getValidators()?.find(v => v.type === 'isUppercase')

expect(isUppercaseRule?.validate('hello')).toBeInstanceOf(CargoFieldError)
expect(isUppercaseRule?.validate('Hello')).toBeInstanceOf(CargoFieldError)
expect(isUppercaseRule?.validate('helloWorld')).toBeInstanceOf(CargoFieldError)
})

it('should fail for non-string values', () => {
const meta = classMeta.getFieldMetadata('uppercaseValue')
const isUppercaseRule = meta.getValidators()?.find(v => v.type === 'isUppercase')

expect(isUppercaseRule?.validate(null)).toBeInstanceOf(CargoFieldError)
expect(isUppercaseRule?.validate(undefined)).toBeInstanceOf(CargoFieldError)
expect(isUppercaseRule?.validate(123)).toBeInstanceOf(CargoFieldError)
})

it('should not have isUppercase validator on undecorated field', () => {
const meta = classMeta.getFieldMetadata('noValidatorValue')
const isUppercaseRule = meta.getValidators()?.find(v => v.type === 'isUppercase')

expect(isUppercaseRule).toBeUndefined()
})

it('should support custom error message', () => {
class CustomMessage {
@IsUppercase('custom error')
value!: string
}

const customMeta = new CargoClassMetadata(CustomMessage.prototype)
const meta = customMeta.getFieldMetadata('value')
const rule = meta.getValidators()?.find(v => v.type === 'isUppercase')

const error = rule?.validate('lower')
expect(error).toBeInstanceOf(CargoFieldError)
expect(error?.message).toBe('custom error')
})
})
Loading