npm package: @sindika/validation
Description: Sindika Validation Library for Typescript and React
Author: Willy Achmat Fauzi
Latest version: 0.9.0

# Nedo React Common Validation

A comprehensive TypeScript validation library designed for React applications, following modern best practices and sindika standards for error handling and validation.
This library is comply with [`standard-schema`](https://github.com/standard-schema/standard-schema) version 1.

## Features

- 🎯 **Comprehensive Coverage**: Supports all major validation types including strings, numbers, dates, files, images, IP addresses, phone numbers, location data, and monetary values
- 🏗️ **Modern TypeScript**: Built with TypeScript for full type safety and IntelliSense support
- 🎨 **Modular Design**: Import only the validators you need
- 📋 **Standardized Error Codes**: Sindika-standard error codes with detailed context
- 🔧 **Extensible**: Easy to extend with custom validators
- ⚡ **Performance**: Lightweight and optimized for production use
- 🧪 **Well Tested**: Comprehensive test coverage

## Installation

```bash
npm install @sindika/validation
```

Or with yarn:

```bash
yarn add @sindika/validation
```

## Quick Start

```typescript
import sv from "@sindika/validation";
import { string, number, object } from "@sindika/validation";

const userSchema = sv
  .object()
  .shape({
    email: sv.string().email().required().compile(),
    age: sv.number().min(18).max(100).required().compile(),
    name: sv.string().min(2).max(50).required().compile(),
  })
  .compile();

// ========== or ==========

// Create validation schema with compile() method
const userSchema = object()
  .shape({
    email: string().email().required().compile(),
    age: number().min(18).max(100).required().compile(),
    name: string().min(2).max(50).required().compile(),
  })
  .compile();

// Validate data
const result = userSchema["~standard"].validate({
  email: "user@example.com",
  age: 25,
  name: "John Doe",
});

if (!result.success) {
  console.log(result.issues); // Array of validation issues with error codes
}
```

## Supported Validation Rules

### Generic Validation Rules

| Code          | Validator             | Description                                             |
| ------------- | --------------------- | ------------------------------------------------------- |
| `VAL-GEN-001` | `required()`          | Validates that the field is not null, empty, or missing |
| `VAL-GEN-002` | `minLength(min)`      | Checks minimum character length                         |
| `VAL-GEN-003` | `maxLength(max)`      | Checks maximum character length                         |
| `VAL-GEN-004` | `exactLength(length)` | Ensures exact character length                          |
| `VAL-GEN-005` | `unique()`            | Validates uniqueness within context                     |
| `VAL-GEN-006` | `pattern(regex)`      | Uses regex pattern matching                             |

### UUID Validation Rules: `string()`

| Code          | Validator | Description                |
| ------------- | --------- | -------------------------- |
| `VAL-UID-001` | `uuid()`  | Validates UUID/GUID format |

### String Validation Rules: `string()`

| Code          | Validator                | Description                             |
| ------------- | ------------------------ | --------------------------------------- |
| `VAL-STR-000` | -                        | Invalid String                          |
| `VAL-STR-001` | `noWhitespace()`         | No whitespace characters allowed        |
| `VAL-STR-002` | `numericString()`        | Only numeric characters                 |
| `VAL-STR-003` | `startsWith(prefix)`     | Must start with specified prefix        |
| `VAL-STR-004` | `endsWith(suffix)`       | Must end with specified suffix          |
| `VAL-STR-005` | `includes(substring)`    | Must contain specified substring        |
| `VAL-STR-006` | `notContains(forbidden)` | Cannot contain forbidden substring      |
| `VAL-STR-007` | `alpha()`                | Only alphabetic characters              |
| `VAL-STR-008` | `noSpecialChars()`       | No special characters                   |
| `VAL-STR-009` | `alphaSpaceQuote()`      | Letters, spaces, and single quotes only |
| `VAL-STR-010` | `alphaNumeric()`         | Letters and numbers only                |
| `VAL-STR-011` | `upperCase()`            | Must be uppercase                       |
| `VAL-STR-012` | `lowerCase()`            | Must be lowercase                       |
| `VAL-STR-013` | `titleCase()`            | Must be title case                      |
| `VAL-STR-014` | `snakeCase()`            | Must be snake_case format               |
| `VAL-STR-015` | `kebabCase()`            | Must be kebab-case format               |
| `VAL-STR-016` | `camelCase()`            | Must be camelCase format                |
| `VAL-STR-017` | `pascalCase()`           | Must be PascalCase format               |

### Numeric Validation Rules: `number()`

| Code          | Validator              | Description                |
| ------------- | ---------------------- | -------------------------- |
| `VAL-NUM-000` | -                      | Invalid Number             |
| `VAL-NUM-001` | `min(value)`           | Minimum value constraint   |
| `VAL-NUM-002` | `max(value)`           | Maximum value constraint   |
| `VAL-NUM-003` | `range(min, max)`      | Value within range         |
| `VAL-NUM-004` | `decimalPlaces(max)`   | Maximum decimal places     |
| `VAL-NUM-005` | `positive()`           | Must be positive           |
| `VAL-NUM-006` | `negative()`           | Must be negative           |
| `VAL-NUM-007` | `integer()`            | Must be whole number       |
| `VAL-NUM-008` | `nonZero()`            | Cannot be zero             |
| `VAL-NUM-009` | `even()`               | Must be even number        |
| `VAL-NUM-010` | `odd()`                | Must be odd number         |
| `VAL-NUM-011` | `divisibleBy(divisor)` | Must be divisible by value |

### Date/DateTime Validation Rules: `date()`

| Code          | Validator                         | Description           |
| ------------- | --------------------------------- | --------------------- |
| `VAL-DAT-000` | -                                 | invalid Date          |
| `VAL-DAT-001` | `minDate(date)` / `maxDate(date)` | Date range validation |
| `VAL-DAT-039` | `leapYear()`                      | Must be leap year     |
| `VAL-DAT-040` | `customHoliday()`                 | Holiday validation    |

### File Validation Rules: `file()`

| Code          | Validator                  | Description          |
| ------------- | -------------------------- | -------------------- |
| `VAL-FIL-001` | `fileType(types)`          | Allowed file types   |
| `VAL-FIL-002` | `maxFileSize(size)`        | Maximum file size    |
| `VAL-FIL-003` | `minFileSize(size)`        | Minimum file size    |
| `VAL-FIL-004` | `mimeType(types)`          | MIME type validation |
| `VAL-FIL-005` | `fileNamePattern(pattern)` | File name pattern    |
| `VAL-FIL-006` | `fileNameLength(config)`   | File name length     |

### Image Validation Rules: `file()`

| Code          | Validator                 | Description                  |
| ------------- | ------------------------- | ---------------------------- |
| `VAL-IMA-001` | `imageType(types)`        | Allowed image types          |
| `VAL-IMA-002` | `maxImageSize(size)`      | Maximum image file size      |
| `VAL-IMA-003` | `minImageSize(size)`      | Minimum image file size      |
| `VAL-IMA-004` | `imageDimensions(config)` | Image resolution constraints |
| `VAL-IMA-005` | `imageDimensions(config)` | Minimum resolution           |
| `VAL-IMA-006` | `imageDimensions(config)` | Exact resolution             |
| `VAL-IMA-007` | `aspectRatio(ratio)`      | Aspect ratio validation      |
| `VAL-IMA-008` | `mimeType(types)`         | Image MIME type              |
| `VAL-IMA-009` | `imageDimensions(config)` | Exact dimensions             |
| `VAL-IMA-010` | `squareImage()`           | Square image constraint      |
| `VAL-IMA-011` | `portraitImage()`         | Portrait orientation         |

### IP Address Validation Rules: `string()`

| Code          | Validator       | Description        |
| ------------- | --------------- | ------------------ |
| `VAL-IPA-001` | `privateIP()`   | Private IP address |
| `VAL-IPA-002` | `publicIP()`    | Public IP address  |
| `VAL-IPA-003` | `ipv4()`        | IPv4 format        |
| `VAL-IPA-004` | `ipv6()`        | IPv6 format        |
| `VAL-IPA-005` | `reservedIP()`  | Reserved IP ranges |
| `VAL-IPA-006` | `loopbackIP()`  | Loopback address   |
| `VAL-IPA-007` | `multicastIP()` | Multicast address  |

### Phone Validation Rules: `string()`

| Code          | Validator              | Description           |
| ------------- | ---------------------- | --------------------- |
| `VAL-PHN-001` | `phoneNumber(config)`  | Phone number format   |
| `VAL-PHN-002` | `internationalPhone()` | Requires country code |
| `VAL-PHN-003` | `countryCode(codes)`   | Valid country code    |

### Location Validation Rules: `string()`

| Code          | Validator             | Description          |
| ------------- | --------------------- | -------------------- |
| `VAL-LOC-001` | `latitude(config)`    | Latitude range       |
| `VAL-LOC-002` | `longitude(config)`   | Longitude range      |
| `VAL-LOC-003` | `coordinates(config)` | Coordinate precision |

### Email Validation Rules: `string()`

| Code            | Validator | Description   |
| --------------- | --------- | ------------- |
| `VAL-EMAIL-000` | `email()` | Invalid Email |

### Array Validation Rules: `array()`

| Code          | Validator | Description   |
| ------------- | --------- | ------------- |
| `VAL-ARR-000` | -         | Invalid Array |

### Object Validation Rules: `object()`

| Code          | Validator | Description    |
| ------------- | --------- | -------------- |
| `VAL-OBJ-000` | -         | Invalid Object |

### Money Validation Rules

| Code          | Type       | Validator             | Description             |
| ------------- | ---------- | --------------------- | ----------------------- |
| `VAL-MON-001` | `number()` | `moneyAmount(config)` | Minimum amount          |
| `VAL-MON-002` | `number()` | `moneyAmount(config)` | Maximum amount          |
| `VAL-MON-003` | `string()` | `moneyFormat(config)` | Currency format         |
| `VAL-MON-004` | `string()` | `currencyCode(codes)` | Valid currency code     |
| `VAL-MON-005` | `number()` | `decimalPlaces(max)`  | Currency decimal places |
| `VAL-MON-006` | `number()` | `positiveAmount()`    | Positive amount         |
| `VAL-MON-007` | `number()` | `nonZeroAmount()`     | Non-zero amount         |

## Usage Examples

### Basic String Validation

```typescript
import { string } from "@sindika/validation";

// Email validation
const emailValidator = string().email().compile();
const result = emailValidator["~standard"].validate("user@example.com");

// Chain multiple validations
const nameValidator = string().min(2).max(50).alphaSpaceQuote().compile();

// Case validation
const usernameValidator = string().camelCase().min(3).max(20).compile();
```

### Number Validation

```typescript
import { number } from "@sindika/validation";

// Age validation
const ageValidator = number().min(0).max(150).integer().compile();

// Price validation
const priceValidator = number()
  .strictPositive()
  .decimalPlaces(2)
  .max(99999.99)
  .compile();

// Even number validation
const evenValidator = number().even().strictPositive().compile();
```

### Object Schema Validation

```typescript
import { string, number, object } from "@sindika/validation";

// Create a complete user validation schema
const userSchema = object()
  .shape({
    email: string().email().required().compile(),
    age: number().min(18).max(65).integer().required().compile(),
    name: string().min(2).max(50).required().compile(),
    username: string().camelCase().min(3).max(20).optional().compile(),
  })
  .compile();

// Validate user data
const userData = {
  email: "john@example.com",
  age: 30,
  name: "John Doe",
  username: "johnDoe",
};

const result = userSchema["~standard"].validate(userData);
if (!result.success) {
  console.log(result.issues);
}
```

### Advanced String Validation

```typescript
import { string } from "@sindika/validation";

// Complex text validation
const complexTextValidator = string()
  .trim()
  .min(5)
  .max(100)
  .noWhitespace()
  .alphaNumeric()
  .compile();

// Case-specific validators
const camelCaseValidator = string().camelCase().compile();
const snakeCaseValidator = string().snakeCase().compile();
const kebabCaseValidator = string().kebabCase().compile();

// Content validators
const restrictedContentValidator = string()
  .notContains("banned")
  .startsWith("prefix_")
  .endsWith("_suffix")
  .compile();
```

### Number Validation with Constraints

```typescript
import { number } from "@sindika/validation";

// Mathematical constraints
const evenPositiveValidator = number().even().strictPositive().compile();

const oddInRangeValidator = number().odd().range(1, 100).compile();

const decimalValidator = number()
  .decimalPlaces(2)
  .range(0.01, 999.99)
  .compile();

const divisibilityValidator = number().divisibleBy(5).compile();
```

### Schema Compilation with Field Names

```typescript
import { string, number, object } from "@sindika/validation";

// Individual field validators with field context
const emailValidator = string().email().compile("email");
const ageValidator = number().min(18).max(100).compile("age");

// Object schema with field context
const userSchema = object()
  .shape({
    email: string().email().required().compile(),
    age: number().min(18).max(100).required().compile(),
  })
  .compile("user");

const emailResult = emailValidator["~standard"].validate("invalid-email");
// Error will include field context: "The field email must be a valid email address"
```

### Conditional and Optional Validation

```typescript
import { string, number, object } from "@sindika/validation";

// Optional fields with defaults
const profileSchema = object()
  .shape({
    name: string().required().compile(),
    nickname: string().optional().compile(),
    age: number().default(18).compile(),
    bio: string().max(500).nullable().compile(),
  })
  .compile();

// Validate with optional and default values
const profileData = {
  name: "John Doe",
  // nickname is optional
  // age will use default value of 18
  bio: null, // nullable field
};

const result = profileSchema["~standard"].validate(profileData);
```

### Money and Currency Validation

```typescript
import { string, number } from "@sindika/validation";

// Currency format validation
const usdValidator = string().usdFormat().compile();
const eurValidator = string().eurFormat().compile();

// Currency code validation
const currencyCodeValidator = string()
  .currencyCode(["USD", "EUR", "GBP"])
  .compile();

// Amount validation
const priceValidator = number()
  .positiveAmount()
  .amountRange(0.01, 99999.99)
  .compile();

// Money amount with decimal constraints
const moneyValidator = number()
  .moneyAmount({
    minAmount: 0.01,
    maxAmount: 999999.99,
    maxDecimalPlaces: 2,
    positive: true,
  })
  .compile();

// Usage
const usdResult = usdValidator["~standard"].validate("$123.45");
const priceResult = priceValidator["~standard"].validate(29.99);
```

### Phone Number Validation

```typescript
import { string } from "@sindika/validation";

// International phone number
const internationalPhoneValidator = string().internationalPhone().compile();

// National phone number
const nationalPhoneValidator = string().nationalPhone().compile();

// Custom phone validation
const phoneValidator = string()
  .phoneNumber({
    format: "international",
    requireCountryCode: true,
  })
  .compile();

// Country code validation
const countryCodeValidator = string()
  .countryCode(["+1", "+44", "+49"])
  .compile();

// Usage
const phoneResult =
  internationalPhoneValidator["~standard"].validate("+1234567890");
```

### IP Address Validation

```typescript
import { string } from "@sindika/validation";

// IPv4 validation
const ipv4Validator = string().ipv4().compile();

// IPv6 validation
const ipv6Validator = string().ipv6().compile();

// IP type validation
const privateIPValidator = string().privateIP().compile();
const publicIPValidator = string().publicIP().compile();
const loopbackValidator = string().loopbackIP().compile();

// Usage
const ipResult = ipv4Validator["~standard"].validate("192.168.1.1");
```

### Location and Coordinates Validation

```typescript
import { number, string } from "@sindika/validation";

// Latitude validation
const latitudeValidator = number()
  .latitude({
    minLatitude: -90,
    maxLatitude: 90,
    precision: 6,
  })
  .compile();

// Longitude validation
const longitudeValidator = number()
  .longitude({
    minLongitude: -180,
    maxLongitude: 180,
    precision: 6,
  })
  .compile();

// Coordinate string validation (e.g., "40.7128,-74.0060")
const coordinateStringValidator = string()
  .coordinates({
    minLatitude: -90,
    maxLatitude: 90,
    minLongitude: -180,
    maxLongitude: 180,
  })
  .compile();

// Usage
const latResult = latitudeValidator["~standard"].validate(40.7128);
const coordResult =
  coordinateStringValidator["~standard"].validate("40.7128,-74.0060");
```

### File and Image Validation

```typescript
import { file } from "@sindika/validation";

// Basic file validation
const fileValidator = file()
  .fileType(["pdf", "doc", "docx"])
  .maxFileSize(5 * 1024 * 1024) // 5MB
  .compile();

// Image validation
const imageValidator = file()
  .imageType(["jpg", "jpeg", "png", "gif"])
  .maxImageSize(2 * 1024 * 1024) // 2MB
  .imageDimensions({
    minWidth: 100,
    minHeight: 100,
    maxWidth: 2000,
    maxHeight: 2000,
  })
  .compile();

// Specific image constraints
const avatarValidator = file()
  .imageType(["jpg", "jpeg", "png"])
  .squareImage()
  .maxImageSize(1024 * 1024) // 1MB
  .compile();

// Profile image with aspect ratio
const profileImageValidator = file()
  .imageType(["jpg", "jpeg", "png"])
  .aspectRatio(16 / 9, 0.1) // 16:9 with 10% tolerance
  .maxImageSize(3 * 1024 * 1024)
  .compile();

// Usage
const testFile = new File(["content"], "document.pdf", {
  type: "application/pdf",
});
const fileResult = fileValidator["~standard"].validate(testFile);
```

## Error Handling

All validators return a `ValidationResult` object:

```typescript
interface ValidationResult {
  success: boolean;
  issues: ValidationIssue[];
}

interface ValidationIssue {
  code: ErrorCode;
  path?: (string | number)[];
  context?: { fieldName?: string } & Record<string, unknown>;
}
```

Example error handling (simplified):

```typescript
const result = validator["~standard"].validate(value);

if (!result.success) {
  result.issues.forEach((issue) => {
    console.log(`Error ${issue.code}:`, issue.context);

    switch (issue.code) {
      case "VAL-STR-001":
        console.log("Remove whitespace characters");
        break;
      case "VAL-NUM-005":
        console.log("Enter a positive number");
        break;
      // Handle other error codes...
    }
  });
}
```

## TypeScript Support

The library is built with TypeScript and provides full type safety:

```typescript
import { string, ValidationResult } from "@sindika/validation";

const validator = object()
  .shape({
    app: object()
      .shape({
        name: string().trim().min(1).max(50).compile(),
        version: string()
          .pattern(/^\d+\.\d+\.\d+$/)
          .compile(),
        environment: string()
          .pattern(/^(development|staging|production)$/)
          .compile(),
      })
      .compile(),
    features: array().shape(string().min(1).compile()).optional().compile(),
  })
  .compile();

// Typescript will infer to this type:

// const validator: ObjectSchema<InferObjectOutput<{
//   app: ObjectSchema<InferObjectOutput<{
//     name: StringSchema;
//     version: StringSchema;
//     environment: StringSchema;
//   }>>;
//   features: ArraySchema<...>;
// }>>
```

## Browser and Node.js Support

This library works in both browser and Node.js environments. Some validators (like image dimension validation) have enhanced functionality in browser environments but gracefully degrade in server environments.

## Contributing

1. Fork the repository
2. Create a feature branch
3. Add tests for new validators
4. Ensure all tests pass
5. Submit a pull request

## License

MIT License - see LICENSE file for details.

## Sindika Standards

This validation library follows internal Sindika standards for:

- Error code naming conventions (`VAL-XXX-###`)
- Error message formatting with field context
- Modular architecture for maintainability
- Comprehensive test coverage
- TypeScript-first development approach