graphql-mandatory-validator
Version:
A GraphQL schema validator for mandatory fields with default values and composite type validation
335 lines (267 loc) • 8.53 kB
Markdown
# graphql-mandatory-validator
A Node.js package for validating GraphQL schema files to ensure mandatory fields have appropriate default values. This tool helps maintain schema consistency across multiple repositories and prevents breaking changes.
## Features
- ✅ Validates mandatory GraphQL fields have default values
- 🏗️ Validates mandatory composite types have at least one mandatory field
- 🎯 **NEW**: Validates mandatory enum fields have default values
- 🔧 Configurable scalar type defaults
- 📁 Works with standard `src/type-defs` directory structure
- 🚀 Can validate staged files (git pre-commit) or entire projects
- 🎨 Colored terminal output with detailed error messages
- 📦 Available as both CLI tool and programmatic API
- 🔄 Supports multiple repositories with consistent structure
## Installation
### Global Installation (CLI usage)
```bash
npm install -g graphql-mandatory-validator
```
### Local Installation (Project dependency)
```bash
npm install --save-dev graphql-mandatory-validator
```
## CLI Usage
### Basic Usage
```bash
# Validate staged .graphql files (for pre-commit hooks)
graphql-validator
# Validate all .graphql files in the project
graphql-validator --mode all
```
### Advanced Options
```bash
# Use custom base directory
graphql-validator --base-dir schema/definitions
# Validate specific project root
graphql-validator --mode all --project-root /path/to/project
# Disable colored output
graphql-validator --no-color
# Don't exit process on validation failure
graphql-validator --no-exit
```
### Help
```bash
graphql-validator --help
```
## Programmatic API
### Basic Usage
```typescript
import { GraphQLValidator } from 'graphql-mandatory-validator';
const validator = new GraphQLValidator();
// Validate staged files
const result = await validator.validateStagedFiles();
// Validate entire project
const result = await validator.validateProject();
console.log(`Validation ${result.success ? 'passed' : 'failed'}`);
console.log(`Files checked: ${result.filesChecked}`);
console.log(`Errors found: ${result.errors.length}`);
```
### Advanced Configuration
```typescript
import { GraphQLValidator } from 'graphql-mandatory-validator';
const validator = new GraphQLValidator({
baseDir: 'custom/graphql/path',
colorOutput: false,
exitOnError: false,
scalarDefaults: {
String: '@defaultValue(value: "")',
Int: '@defaultValue(value: 0)',
Float: '@defaultValue(value: 0.0)',
Boolean: '@defaultValue(value: false)',
ID: '@defaultValue(value: "")',
// Add custom scalar types
DateTime: '@defaultValue(value: "1970-01-01T00:00:00Z")',
}
});
const result = await validator.run('all', '/path/to/project');
```
## Validation Rules
### 1. Scalar Type Default Values
The validator enforces the following default values for GraphQL scalar types:
| GraphQL Type | Required Default Value |
|--------------|------------------------|
| `String!` | `@defaultValue(value: "")` |
| `Int!` | `@defaultValue(value: 0)` |
| `Float!` | `@defaultValue(value: 0.0)` |
| `Boolean!` | `@defaultValue(value: false)` |
| `ID!` | `@defaultValue(value: "")` |
### 2. Composite Type Validation
The validator ensures that if a composite type is used as a mandatory field, the composite type itself must contain at least one mandatory field.
**Why this rule?** If a composite type has no mandatory fields, making it mandatory doesn't provide any meaningful constraint - it could be an empty object and still satisfy the schema.
### 3. Enum Type Validation
**NEW**: The validator ensures that mandatory enum fields have appropriate default values.
**Default Value Format**: `@defaultValue(value: "ENUM_VALUE")`
**Recommended Values**:
- Use `"UNKNOWN"` if available in the enum
- Otherwise, use any valid enum value
**Why this rule?** Mandatory enum fields without defaults can break existing queries when new enum types are introduced.
## Example GraphQL Schema
### Scalar Type Validation
#### ❌ Invalid (will fail validation)
```graphql
type User {
id: ID!
name: String!
age: Int!
isActive: Boolean!
}
```
#### ✅ Valid (passes validation)
```graphql
type User {
id: ID! @defaultValue(value: "")
name: String! @defaultValue(value: "")
age: Int! @defaultValue(value: 0)
isActive: Boolean! @defaultValue(value: false)
}
```
### Composite Type Validation
#### ❌ Invalid (will fail validation)
```graphql
type User {
id: ID! @defaultValue(value: "")
profile: Profile! # ERROR: Profile has no mandatory fields
}
type Profile {
bio: String # Optional field
avatar: String # Optional field
website: String # Optional field
}
```
**Error**: `Mandatory composite field "profile" of type "Profile!" references a type with no mandatory fields. Type "Profile" should have at least one mandatory field.`
#### ✅ Valid (passes validation)
```graphql
# Option 1: Add mandatory field to composite type
type User {
id: ID! @defaultValue(value: "")
profile: Profile!
}
type Profile {
userId: ID! @defaultValue(value: "") # Now has mandatory field
bio: String
avatar: String
website: String
}
# Option 2: Make composite field optional
type User {
id: ID! @defaultValue(value: "")
profile: Profile # Made optional (no !)
}
type Profile {
bio: String
avatar: String
website: String
}
```
### Enum Type Validation
#### ❌ Invalid (will fail validation)
```graphql
type Order {
id: ID! @defaultValue(value: "")
status: OrderStatus! # ERROR: Missing default value
priority: Priority! # ERROR: Missing default value
}
```
**Error**: `Mandatory field "status" of type "OrderStatus!" must have a default value. For enum types, use @defaultValue(value: "UNKNOWN") or @defaultValue(value: "<enum_value>") with one of the enum values.`
#### ✅ Valid (passes validation)
```graphql
# Option 1: Using UNKNOWN (recommended)
type Order {
id: ID! @defaultValue(value: "")
status: OrderStatus! @defaultValue(value: "UNKNOWN")
priority: Priority! @defaultValue(value: "UNKNOWN")
}
# Option 2: Using specific enum values
type Task {
id: ID! @defaultValue(value: "")
status: TaskStatus! @defaultValue(value: "PENDING")
priority: Priority! @defaultValue(value: "MEDIUM")
}
# Option 3: Mixed approach
type Product {
id: ID! @defaultValue(value: "")
category: Category! @defaultValue(value: "UNKNOWN") # Has UNKNOWN
status: ProductStatus! @defaultValue(value: "DRAFT") # No UNKNOWN, use first value
}
```
## Git Pre-commit Hook Integration
### Using Husky
1. Install husky:
```bash
npm install --save-dev husky
npx husky install
```
2. Add pre-commit hook:
```bash
npx husky add .husky/pre-commit "graphql-validator"
```
### Using pre-commit (Python)
Add to your `.pre-commit-config.yaml`:
```yaml
repos:
- repo: local
hooks:
- id: graphql-validator
name: GraphQL Schema Validator
entry: graphql-validator
language: node
files: \\.graphql$
pass_filenames: false
```
## Package.json Scripts
Add to your `package.json`:
```json
{
"scripts": {
"validate-graphql": "graphql-validator --mode all",
"validate-graphql:staged": "graphql-validator"
}
}
```
## Configuration Options
### Constructor Options
```typescript
interface ValidationOptions {
baseDir?: string; // Default: 'src/type-defs'
scalarDefaults?: Record<string, string>;
colorOutput?: boolean; // Default: true
exitOnError?: boolean; // Default: true
}
```
### ValidationResult
```typescript
interface ValidationResult {
success: boolean;
errors: ValidationError[];
filesChecked: number;
}
interface ValidationError {
file: string;
line: number;
fieldName: string;
fieldType: string;
expectedDefault: string;
message: string;
}
```
## Repository Structure Requirements
This package assumes your GraphQL files are located in:
```
your-project/
├── src/
│ └── type-defs/
│ ├── schema1.graphql
│ ├── schema2.graphql
│ └── ...
└── package.json
```
You can customize the base directory using the `baseDir` option.
## License
MIT
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## Support
For issues and questions, please open an issue on the GitHub repository.