input-spec
Version:
Zero-dependency TypeScript implementation of the Dynamic Input Field Specification Protocol with framework integration support
636 lines (485 loc) • 17.3 kB
Markdown
<div align="center">
# input-spec (TypeScript SDK)
Declarative, backend-owned field & validation specifications for frontends.
<strong>FR:</strong> Définissez les champs et règles côté serveur et consommez-les côté client sans duplication.
Zero runtime dependencies • Fully typed • Framework agnostic
[Docs](https://cyfko.github.io/input-spec/typescript/) · [API](https://cyfko.github.io/input-spec/typescript/API) · [Intégration](https://cyfko.github.io/input-spec/typescript/FRAMEWORK_INTEGRATION) · [Architecture](https://cyfko.github.io/input-spec/typescript/ARCHITECTURE)
</div>
## 1. Install
```bash
npm install input-spec
```
Node >= 16. No runtime deps.
## 2. Frontend Quick Start
```typescript
import { FieldValidator, InputFieldSpec } from 'input-spec';
const spec: InputFieldSpec = await fetch('/api/form-fields/email').then(r => r.json());
const validator = new FieldValidator();
const result = await validator.validate(spec, 'user@example.com', 'email');
if (!result.isValid) console.log(result.errors.map(e => e.message));
```
## 3. Backend Generation
```typescript
import { InputFieldSpec, ConstraintDescriptor } from 'input-spec';
function buildEmail(tier: 'basic' | 'premium'): InputFieldSpec {
const constraints: ConstraintDescriptor[] = [
{ name: 'email', pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$', errorMessage: 'Invalid email' }
];
constraints.push({
name: 'maxLength',
max: tier === 'premium' ? 200 : 50,
errorMessage: `Email too long (max ${tier === 'premium' ? 200 : 50} chars)`
});
return {
displayName: 'Email Address',
dataType: 'STRING',
expectMultipleValues: false,
required: true,
constraints
};
}
```
## 4. Dynamic Values (Autocomplete)
```typescript
import { ValuesResolver, FetchHttpClient, MemoryCacheProvider, createDefaultValuesEndpoint } from 'input-spec';
const resolver = new ValuesResolver(new FetchHttpClient(), new MemoryCacheProvider());
const endpoint = createDefaultValuesEndpoint('https://api.example.com/countries');
const result = await resolver.resolveValues(endpoint, { search: 'fr', page: 1, limit: 10 });
console.log(result.values);
```
## 5. Core Concepts
| Name | Description |
|------|-------------|
| InputFieldSpec | Full declarative field specification |
| ConstraintDescriptor | Ordered validation rule descriptor |
| ValuesEndpoint | Dynamic values contract (search + pagination) |
| FieldValidator | Executes validation (single constraint or all) |
| ValuesResolver | Fetch + cache orchestration for dynamic values |
| MemoryCacheProvider | In‑memory TTL cache |
## 6. API Snapshot
```typescript
interface InputFieldSpec {
displayName: string;
description?: string;
dataType: 'STRING' | 'NUMBER' | 'DATE' | 'BOOLEAN';
expectMultipleValues: boolean;
required: boolean;
constraints: ConstraintDescriptor[];
valuesEndpoint?: ValuesEndpoint;
}
```
Full reference: `./docs/API.md`.
Migration: A helper `migrateV1Spec` converts legacy (enumValues + composite min/max/pattern) to v2 atomic form; review output (see Migration section in API docs).
Coercion (optional library-only): Disabled by default; enable via `new FieldValidator({ coercion: { coerce: true } })` or per-field `coercion` block to accept numeric / boolean strings or epoch dates. Protocol wire format remains unchanged.
## 7. Example Patterns
Validate specific constraint:
```typescript
await validator.validate(spec, 'bad@', 'email');
```
Validate all:
```typescript
await validator.validate(spec, 'good@example.com');
```
Array field:
```typescript
await validator.validate(arraySpec, ['a','b']);
```
## 8. Design Principles
1. Backend is source of truth
2. Ordered constraint execution
3. Zero runtime dependencies
4. Extensible via injected HTTP/cache
5. Serializable specs for testing
## 9. Project Layout
```
src/
types/ # Interfaces & type guards
validation/ # Validation engine
client/ # HTTP + cache + resolver
__tests__/ # Jest tests
```
## 10. Scripts
| Task | Command |
|------|---------|
| Build | `npm run build` |
| Test | `npm test` |
| Lint | `npm run lint` |
| Types | `npm run type-check` |
## 11. Publishing (Maintainers)
```bash
npm run build && npm test
npm publish --dry-run
npm publish --access public
```
## 12. Contributing
1. Fork & branch
2. Add tests
3. Ensure green build
4. Open PR
## 13. License
MIT (see `LICENSE`).
## 14. Integrity
| Item | Value |
|------|-------|
| Package | input-spec |
| Protocol constant | PROTOCOL_VERSION |
| Library constant | LIBRARY_VERSION |
| Runtime deps | 0 |
## 15. Resources
- Protocol Specification: `../../PROTOCOL_SPECIFICATION.md`
- Root README: `../../README.md`
- Docs: `./docs/`
## 🌐 Real-World Scenarios
### Scenario 1: Multi-Tenant SaaS Application
**Frontend Team**: Different validation rules per client, all handled automatically!
```typescript
// Your component automatically adapts to each client's rules
const loadUserForm = async (clientId: string) => {
const fields = await fetch(`/api/clients/${clientId}/form-fields/user`)
.then(r => r.json());
// Client A might require 2FA, Client B might not
// Client A might have different email domains allowed
// Your frontend doesn't care - it just renders what backend sends!
return fields;
};
```
**Backend Team**: Complete control over client-specific business rules
```typescript
// Your API dynamically generates rules based on client configuration
app.get('/api/clients/:clientId/form-fields/user', async (req, res) => {
const client = await getClientConfig(req.params.clientId);
const emailField: InputFieldSpec = {
displayName: "Email Address",
dataType: "STRING",
required: true,
constraints: [
{ name: "email", pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", errorMessage: "Invalid email format" }
]
};
// Client-specific email domain restrictions
if (client.restrictEmailDomains) {
emailField.constraints.push({
name: "allowedDomains",
pattern: `^[^@]+@(${client.allowedDomains.join('|')})$`,
errorMessage: `Email must be from: ${client.allowedDomains.join(', ')}`
});
}
res.json({ fields: [emailField] });
});
```
### Scenario 2: Smart Product Search with Live Data
**Frontend Team**: Rich autocomplete without hardcoding product lists
```typescript
// Product search that adapts to inventory and user permissions
const productField = await fetch(`/api/form-fields/product?userRole=${userRole}`)
.then(r => r.json());
// Backend controls:
// - Which products user can see
// - Search parameters
// - Pagination settings
// - Validation rules
const searchProducts = async (query: string) => {
if (productField.valuesEndpoint) {
const response = await fetch(
`${productField.valuesEndpoint.url}?${productField.valuesEndpoint.searchParam}=${query}`
);
return response.json();
}
};
```
**Backend Team**: Dynamic product visibility and search logic
```typescript
app.get('/api/form-fields/product', async (req, res) => {
const userRole = req.query.userRole;
let searchEndpoint = '/api/products/search';
let constraints: ConstraintDescriptor[] = [
{ name: "required", errorMessage: "Please select a product" }
];
// Admin users can see all products including discontinued
if (userRole === 'admin') {
searchEndpoint += '?includeDiscontinued=true';
}
// Sales users have minimum quantity requirements
if (userRole === 'sales') {
constraints.push({
name: "minQuantity",
min: 10,
errorMessage: "Sales orders minimum 10 units"
});
}
const productFieldSpec: InputFieldSpec = {
displayName: "Product",
dataType: "STRING",
required: true,
constraints,
valuesEndpoint: {
url: searchEndpoint,
searchParam: "q",
pageParam: "page",
minSearchLength: 2
}
};
res.json(productFieldSpec);
});
```
### Scenario 3: Dynamic Form Generation
**Frontend Team**: Build entire forms from backend configuration
```typescript
// Generate complete registration form from backend
const buildRegistrationForm = async (country: string, userType: string) => {
const formConfig = await fetch(`/api/forms/registration?country=${country}&userType=${userType}`)
.then(r => r.json());
// Backend controls:
// - Which fields are required per country
// - Validation rules (phone formats, postal codes)
// - Field order and grouping
// - Conditional field visibility
const formElements = formConfig.fields.map(fieldSpec =>
createFormField(fieldSpec) // Your UI component factory
);
return formElements;
};
```
**Backend Team**: Country and role-specific form logic
```typescript
app.get('/api/forms/registration', async (req, res) => {
const { country, userType } = req.query;
const countryConfig = await getCountryConfig(country);
const fields: InputFieldSpec[] = [
// Email - universal
{
displayName: "Email",
dataType: "STRING",
required: true,
constraints: [
{ name: "email", pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", errorMessage: "Invalid email format" },
{ name: "maxLength", max: 100, errorMessage: "Email too long (max 100 chars)" }
]
}
];
// Phone field with country-specific validation
fields.push({
displayName: "Phone Number",
dataType: "STRING",
required: countryConfig.phoneRequired,
constraints: [
{
name: "phoneFormat",
pattern: countryConfig.phonePattern,
errorMessage: `Invalid phone format for ${country}`
}
]
});
// Business users get additional fields
if (userType === 'business') {
fields.push({
displayName: "Tax ID",
dataType: "STRING",
required: true,
constraints: [
{
name: "taxIdFormat",
pattern: countryConfig.taxIdPattern,
errorMessage: `Invalid tax ID format for ${country}`
}
]
});
}
res.json({ fields });
});
```
## 🎯 Key Features
### ✨ **Zero Dependencies**
- Pure TypeScript implementation
- No external runtime dependencies
- Works in browser and Node.js
- Small bundle size (34.7 KB)
### 🔧 **Enhanced API Design**
- **Required field at top-level**: `required: boolean` moved from constraints for better ergonomics
- **Ordered constraint execution**: Constraints execute in array order for predictable behavior
- **Named constraints**: Each constraint has a `name` property for better identification and debugging
### � **Framework Integration**
- **Angular HttpClient**: Seamless integration with interceptors and dependency injection
- **Axios Support**: Custom Axios instances with existing configurations preserved
- **Custom HTTP Clients**: Configurable fetch-based client with interceptors and error handling
- **Zero Breaking Changes**: Existing HTTP infrastructure remains intact
## 📦 Feature Summary
- **Zero Runtime Dependencies**: Pure TypeScript implementation
- **Framework Integration**: Angular, React, Vue, Vanilla JS support
- **HTTP Client Injection**: Preserves existing interceptors and configurations
- **Type Safety**: Complete TypeScript type definitions
- **Validation Engine**: Comprehensive field validation
- **HTTP Client**: Pluggable HTTP client with caching
- **Dependency Injection**: Clean architecture with IoC
- **Extensive Testing**: 58 tests with 96%+ coverage
## 🏗️ Architecture
### Separation of Concerns
```
src/
├── types/ # Pure TypeScript interfaces (zero dependencies)
├── validation/ # Business logic validation engine
├── client/ # Infrastructure (HTTP, caching, resolution)
└── __tests__/ # Comprehensive test suite
```
### Design Patterns
- **Dependency Injection**: Constructor injection with interfaces
- **Strategy Pattern**: Pluggable HTTP clients and cache providers
- **Factory Pattern**: Simplified object creation
- **Template Method**: Validation algorithms
### Dynamic Values Resolution
```typescript
import {
ValuesResolver,
FetchHttpClient,
MemoryCacheProvider,
createDefaultValuesEndpoint
} from './src';
// Setup with dependency injection
const resolver = new ValuesResolver(
new FetchHttpClient(),
new MemoryCacheProvider()
);
// Configure endpoint
const endpoint = createDefaultValuesEndpoint('https://api.example.com/countries');
// Resolve values with caching and pagination
const result = await resolver.resolveValues(endpoint, {
search: 'france',
page: 1,
limit: 10
});
```
### Zero-Dependency Architecture
```typescript
// No external dependencies at runtime!
import { MemoryCacheProvider, FetchHttpClient } from './src';
const cache = new MemoryCacheProvider(); // Uses native Map
const client = new FetchHttpClient(); // Uses native fetch
```
## 🧪 Testing
### Run Tests
```bash
# All tests
npm test
# Watch mode
npm run test:watch
# Coverage report
npm run test:coverage
```
### Test Results
- **58 tests** pass with **100% success rate**
- **Types Module**: 100% coverage
- **Validation Module**: 96% coverage
- **Client Module**: 86% coverage
- **Integration Tests**: End-to-end scenarios with new v2.0 structure
## 🔨 Build & Development
### Available Scripts
```bash
npm run build # Build distribution files (CJS, ESM, types)
npm run dev # Build in watch mode
npm run test # Run test suite
npm run lint # ESLint checking
npm run format # Prettier formatting
npm run type-check # TypeScript type checking
```
### Build Output
```
dist/
├── index.js # CommonJS build
├── index.mjs # ES Module build
├── index.d.ts # TypeScript declarations (CJS)
└── index.d.mts # TypeScript declarations (ESM)
```
## 📋 API Reference (Snapshot)
### Core Types
- `InputFieldSpec` - Complete field specification
- `ConstraintDescriptor` - Validation rules
- `ValidationResult` - Validation outcome
- `ValuesEndpoint` - Dynamic values configuration
### Classes
- `FieldValidator` - Main validation engine
- `ValuesResolver` - Value resolution orchestrator
- `FetchHttpClient` - HTTP client implementation
- `MemoryCacheProvider` - In-memory caching
### Interfaces
- `HttpClient` - HTTP client abstraction
- `CacheProvider` - Cache provider abstraction
## 🎯 Design Principles
### Zero Dependencies
- **Runtime**: No external dependencies
- **Build**: Only development dependencies (TypeScript, Jest, etc.)
- **Browser**: Uses native fetch, Map, etc.
### Type Safety
- **Compile-time**: Full TypeScript strict mode
- **Runtime**: Type guards for external data
- **API**: Strongly typed interfaces
### Testability
- **Dependency Injection**: Easy mocking and testing
- **Interface Segregation**: Focused, testable interfaces
- **Pure Functions**: Predictable, testable logic
## 📚 Documentation
- [Usage Guide](./docs/USAGE_GUIDE.md) - Getting started and common patterns
- [API Reference](./docs/API.md) - Complete API documentation with examples
- [Framework Integration](./docs/FRAMEWORK_INTEGRATION.md) - Angular, React, Vue integration examples
- [Performance Guide](./docs/PERFORMANCE.md) - Optimization strategies and benchmarks
- [Architecture Guide](./docs/ARCHITECTURE.md) - Design decisions and patterns
- [Migration Guide](./MIGRATION.md) - Migrating from v1.x to v2.0
- [Release Notes](./RELEASE_NOTES.md) - What's new in v2.0
## 🤝 Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Add tests for your changes
4. Ensure all tests pass (`npm test`)
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request
## 📄 License
MIT License - see LICENSE file for details.
## 🔗 Related
## 📦 Publishing (Maintainers)
```bash
# Build & test
npm run build && npm test
# Dry run publish
npm publish --dry-run
# Publish (ensure version not already published)
npm publish --access public
```
## ✅ Integrity Notes
- Package name: `input-spec`
- Protocol version constant exported as `PROTOCOL_VERSION`
- Library version exported as `LIBRARY_VERSION`
- No runtime dependencies; only dev tooling (TypeScript, Jest, tsup)
If something is unclear or you need a French version complète du README, ouvrez une issue. 🙌
- [Protocol Specification](../../PROTOCOL_SPECIFICATION.md)
- [Project Root](../../README.md)