Module for adding setting option.
npm install @things-factory/setting-baseThis package provides a common setting management system with an extensible validation framework that allows each application (operato-wms, operato-hub, etc.) to register their own validators for specific settings without modifying the core mutation functions.
- Overview
- Architecture
- Getting Started
- Usage Guide
- Validator Types
- Examples
- Best Practices
- Error Handling
- Testing
- Troubleshooting
The validation system uses a registry pattern that allows each application to register their own validators for specific settings. This design:
- ✅ Keeps setting-mutation.ts as a common function
- ✅ Avoids circular dependencies
- ✅ Allows applications to add custom validation logic
- ✅ Supports both exact name matching and pattern matching
- ✅ Works seamlessly with the shared setting-ui component
The validator system is designed to avoid circular dependencies:
```
┌─────────────────┐
│ setting-base │ (Common package - no app dependencies)
│ │
│ - Registry │
│ - Mutations │
└────────┬────────┘
│ exports registry
│
▼
┌─────────────────┐ ┌─────────────────┐
│ operato-wms │ │ operato-hub │
│ │ │ │
│ - Validators │ │ - Validators │
│ - Registers │ │ - Registers │
└────────┬────────┘ └────────┬────────┘
│ │
│ imports registry │ imports registry
│ │
└────────┬───────────────┘
│
▼
┌─────────────────┐
│ setting-ui │
│ │
│ - UI Component │
│ - Uses mutations│
└──────────────────┘
- setting-validator.ts: Contains the validator registry and validation logic
- setting-mutation.ts: Common mutation functions that call validators before saving
- Application validators: Each application registers its own validators
1. Module Load Phase: Applications register validators when their server code is imported
2. Schema Build Phase: GraphQL schema is built with mutation resolvers
3. Runtime Phase: When a setting is created/updated, the mutation function calls registered validators
4. Validation Result: If validation fails, an error is thrown before saving; if it passes, the setting is saved normally
`
setting-base:
- No dependencies on applications ✅
- Only depends on: auth-base, code-base
operato-wms:
- Depends on: setting-base ✅
- Registers validators at module load ✅
operato-hub:
- Depends on: setting-base ✅
- Registers validators at module load ✅
setting-ui:
- Depends on: setting-base ✅
- Uses mutations (which call validators) ✅
`
Create a validator file in your application (e.g., packages/operato-wms/server/validators/setting-validators.ts):
`typescript
import { settingValidatorRegistry, SettingValidationResult } from '@things-factory/setting-base'
import { Domain } from '@things-factory/shell'
import { User } from '@things-factory/auth-base'
import { NewSetting, SettingPatch } from '@things-factory/setting-base'
import { Setting } from '@things-factory/setting-base'
// Validator for creating a setting
async function validateBatchPickingLimit(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise
if (!setting.value) {
return { valid: false, error: 'batch-picking-limit value is required' }
}
const limit = parseInt(setting.value, 10)
if (isNaN(limit) || limit < 1 || limit > 1000) {
return {
valid: false,
error: 'batch-picking-limit must be a number between 1 and 1000'
}
}
return { valid: true }
}
// Validator for updating a setting
async function validateBatchPickingLimitUpdate(
existingSetting: Setting,
patch: SettingPatch,
context: { domain: Domain; user: User }
): Promise
// Only validate if value is being updated
if (patch.value === undefined) {
return { valid: true }
}
return await validateBatchPickingLimit({ ...existingSetting, value: patch.value } as NewSetting, context)
}
// Register function
export function registerOperatoWmsSettingValidators(): void {
settingValidatorRegistry.registerCreateValidator('batch-picking-limit', validateBatchPickingLimit)
settingValidatorRegistry.registerUpdateValidator('batch-picking-limit', validateBatchPickingLimitUpdate)
}
`
IMPORTANT: Validators MUST be registered at module load time (when your application's server code is imported), NOT in bootstrap events. This ensures validators are registered before the GraphQL schema is built.
In your application's server entry file (e.g., packages/operato-wms/server/index.ts):
`typescript
export * from './graphql'
export * from './migrations'
export * from './entities'
import './routes'
// Register setting validators at module load time (before schema is built)
// This ensures validators are available when mutations are called
import { registerOperatoWmsSettingValidators } from './validators/setting-validators'
registerOperatoWmsSettingValidators()
`
Why module load time?
- The GraphQL schema is built during server startup, before bootstrap-module-start eventsetting-base
- Validators are called at runtime when mutations execute
- Registering at module load ensures validators are ready before any mutations are called
- No circular dependencies because doesn't import application packages
#### Exact Name Matching
Register validators for specific setting names:
`typescript`
settingValidatorRegistry.registerCreateValidator('batch-picking-limit', validateBatchPickingLimit)
settingValidatorRegistry.registerUpdateValidator('batch-picking-limit', validateBatchPickingLimitUpdate)
#### Pattern Matching
For settings that follow a naming pattern, you can use pattern matching:
`typescript
// Register validators using regex pattern
settingValidatorRegistry.registerPatternValidator(
/^rule-for-.*$/,
validateLocationSortingRule,
validateLocationSortingRule
)
// Or use a function for more complex matching
settingValidatorRegistry.registerPatternValidator(
(name: string) => name.startsWith('enable-') || name.startsWith('disable-'),
validateBooleanSetting,
validateBooleanSetting
)
`
1. Application Registers Validators:
`typescript`
// packages/operato-wms/server/index.ts
import { registerOperatoWmsSettingValidators } from './validators/setting-validators'
registerOperatoWmsSettingValidators() // Module loads → validators registered
2. Server Starts:
`typescript`
// shell/server/server.ts
const builtSchema = await schema() // Schema built with SettingMutation resolver
3. User Makes Request:
`typescript`
// User calls: mutation { createSetting(setting: {...}) }
// setting-base/server/service/setting/setting-mutation.ts
async createSetting(...) {
await settingValidatorRegistry.validateCreate(setting, { domain, user })
// ↑ Calls registered validators from operato-wms
}
`typescript`
type SettingCreateValidator = (
setting: NewSetting,
context: { domain: Domain; user: User }
) => Promise
`typescript`
type SettingUpdateValidator = (
setting: Setting,
patch: SettingPatch,
context: { domain: Domain; user: User }
) => Promise
`typescript`
interface SettingValidationResult {
valid: boolean
error?: string // Error message if validation fails
}
`typescript
async function validateStackingOptionLimit(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise
if (!setting.value) {
return { valid: false, error: 'stacking-option-limit value is required' }
}
try {
const parsed = JSON.parse(setting.value)
if (typeof parsed.Min !== 'number' || typeof parsed.Max !== 'number') {
return {
valid: false,
error: 'stacking-option-limit must have Min and Max as numbers'
}
}
if (parsed.Min < 0 || parsed.Max < parsed.Min) {
return {
valid: false,
error: 'Min must be >= 0 and Max must be >= Min'
}
}
} catch (e) {
return {
valid: false,
error: 'stacking-option-limit must be valid JSON: { "Min": 1, "Max": 1 }'
}
}
return { valid: true }
}
`
`typescript${setting.name} must be "true" or "false"
async function validateBooleanSetting(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise
if (setting.value && setting.value !== 'true' && setting.value !== 'false') {
return {
valid: false,
error:
}
}
return { valid: true }
}
// Register for multiple boolean settings
const booleanSettings = ['enable-bin-picking', 'enable-product-scanning', 'enable-beta-feature']
booleanSettings.forEach(settingName => {
settingValidatorRegistry.registerCreateValidator(settingName, validateBooleanSetting)
settingValidatorRegistry.registerUpdateValidator(settingName, validateBooleanSetting)
})
`
`typescript
async function validateMinimumSealNumber(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise
if (!setting.value) {
return { valid: false, error: 'minimum-seal-number value is required' }
}
// Check if value contains decimal point
if (setting.value.includes('.')) {
return {
valid: false,
error: 'minimum-seal-number must be an integer (no decimals allowed)'
}
}
const number = parseInt(setting.value, 10)
// Check if parsing resulted in NaN
if (isNaN(number)) {
return {
valid: false,
error: 'minimum-seal-number must be a valid number'
}
}
// Check if the parsed value matches the original string (to catch cases like "123abc")
if (number.toString() !== setting.value.trim()) {
return {
valid: false,
error: 'minimum-seal-number must be a valid integer'
}
}
// Check if negative
if (number < 0) {
return {
valid: false,
error: 'minimum-seal-number must be 0 or above (negative numbers not allowed)'
}
}
return { valid: true }
}
`
`typescript
import { getRepository } from 'typeorm'
import { SomeEntity } from './entities'
async function validateSettingWithDbCheck(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise
// Example: Check if referenced entity exists
if (setting.name === 'referenced-entity-id') {
const entity = await getRepository(SomeEntity).findOne({
where: { id: setting.value, domain: context.domain }
})
if (!entity) {
return {
valid: false,
error: Referenced entity not found: ${setting.value}
}
}
}
return { valid: true }
}
`
1. Keep validators focused: Each validator should validate one specific aspect
2. Return clear error messages: Help users understand what went wrong
3. Use pattern matching: For settings that follow naming conventions
4. Register at module load time: Ensure validators are registered before the application starts
5. Handle async operations: Validators can be async if you need to check database or external services
6. Skip validation when not needed: In update validators, check if the field being validated is actually being updated
When validation fails, the mutation will throw an error with the message from SettingValidationResult.error. This error will be caught by GraphQL and returned to the client.
Example error response:
`json`
{
"errors": [
{
"message": "batch-picking-limit must be a number between 1 and 1000",
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}
]
}
You can test validators independently:
`typescript
// In operato-wms tests
import { settingValidatorRegistry } from '@things-factory/setting-base'
import { registerOperatoWmsSettingValidators } from '../validators/setting-validators'
beforeAll(() => {
registerOperatoWmsSettingValidators()
})
test('validates batch-picking-limit', async () => {
const result = await settingValidatorRegistry.validateCreate(
{ name: 'batch-picking-limit', value: '50', category: 'id-rule' },
{ domain: mockDomain, user: mockUser }
)
// No error means validation passed
expect(result).toBeUndefined()
})
`
❌ DON'T register in bootstrap-module-start:
`typescript`
// WRONG - too late!
process.on('bootstrap-module-start', () => {
registerValidators() // Schema already built
})
✅ DO register at module load:
`typescript`
// CORRECT - before schema build
import { registerValidators } from './validators'
registerValidators() // Module loads first
❌ DON'T import application packages in setting-base:
`typescript`
// WRONG - creates circular dependency!
import { operatoWmsValidators } from '@things-factory/operato-wms'
✅ DO let applications register themselves:
`typescript`
// CORRECT - applications register to shared registry
// setting-base just exports the registry
export { settingValidatorRegistry }
This error occurs when settingValidatorRegistry is undefined. Make sure:
1. You've imported from the correct path: @things-factory/setting-basesetting-base
2. The package has been rebuilt after adding the validator exportssetting-base
3. Your application has been rebuilt to pick up the updated exports
If validators aren't being called:
1. Verify validators are registered at module load time (not in bootstrap events)
2. Check that the setting name matches exactly (case-sensitive)
3. For pattern matching, verify the regex or function matches correctly
4. Ensure the mutation is actually calling settingValidatorRegistry.validateCreate() or validateUpdate()
If you have existing validation logic in your application:
1. Extract validation logic into validator functions
2. Register validators at module load time in your server entry file
3. Remove any custom validation from mutation hooks or middleware
4. Test thoroughly to ensure validation still works
- Example validators:
- packages/operato-wms/server/validators/setting-validators.tspackages/operato-hub/server/validators/setting-validators.ts`
-