@things-factory/setting-base
v4.3.767
Published
Module for adding setting option.
Keywords
Readme
Setting Base - Extensible Validation System
This 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.
Table of Contents
- Overview
- Architecture
- Getting Started
- Usage Guide
- Validator Types
- Examples
- Best Practices
- Error Handling
- Testing
- Troubleshooting
Overview
The validation system uses a registry pattern that allows each application to register their own validators for specific settings. This design:
- ✅ Keeps
setting-mutation.tsas 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-uicomponent
Architecture
Dependency Flow
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│
└──────────────────┘Key Components
setting-validator.ts: Contains the validator registry and validation logicsetting-mutation.ts: Common mutation functions that call validators before saving- Application validators: Each application registers its own validators
How It Works
- Module Load Phase: Applications register validators when their server code is imported
- Schema Build Phase: GraphQL schema is built with mutation resolvers
- Runtime Phase: When a setting is created/updated, the mutation function calls registered validators
- Validation Result: If validation fails, an error is thrown before saving; if it passes, the setting is saved normally
Package Dependencies
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) ✅Getting Started
Step 1: Create Validator Functions
Create a validator file in your application (e.g., packages/operato-wms/server/validators/setting-validators.ts):
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<SettingValidationResult> {
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<SettingValidationResult> {
// 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)
}Step 2: Register Validators at Module Load Time
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):
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-startevent - 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
setting-basedoesn't import application packages
Usage Guide
Registering Validators
Exact Name Matching
Register validators for specific setting names:
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:
// 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
)Complete Flow Example
Application Registers Validators:
// packages/operato-wms/server/index.ts import { registerOperatoWmsSettingValidators } from './validators/setting-validators' registerOperatoWmsSettingValidators() // Module loads → validators registeredServer Starts:
// shell/server/server.ts const builtSchema = await schema() // Schema built with SettingMutation resolverUser Makes Request:
// 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 }
Validator Types
SettingCreateValidator
type SettingCreateValidator = (
setting: NewSetting,
context: { domain: Domain; user: User }
) => Promise<SettingValidationResult> | SettingValidationResultSettingUpdateValidator
type SettingUpdateValidator = (
setting: Setting,
patch: SettingPatch,
context: { domain: Domain; user: User }
) => Promise<SettingValidationResult> | SettingValidationResultSettingValidationResult
interface SettingValidationResult {
valid: boolean
error?: string // Error message if validation fails
}Examples
Example 1: Validate JSON Format
async function validateStackingOptionLimit(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise<SettingValidationResult> {
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 }
}Example 2: Validate Boolean Setting
async function validateBooleanSetting(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise<SettingValidationResult> {
if (setting.value && setting.value !== 'true' && setting.value !== 'false') {
return {
valid: false,
error: `${setting.name} must be "true" or "false"`
}
}
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)
})Example 3: Validate Integer (No Decimals, No Negatives)
async function validateMinimumSealNumber(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise<SettingValidationResult> {
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 }
}Example 4: Validate with Database Check
import { getRepository } from 'typeorm'
import { SomeEntity } from './entities'
async function validateSettingWithDbCheck(
setting: NewSetting,
context: { domain: Domain; user: User }
): Promise<SettingValidationResult> {
// 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 }
}Best Practices
- Keep validators focused: Each validator should validate one specific aspect
- Return clear error messages: Help users understand what went wrong
- Use pattern matching: For settings that follow naming conventions
- Register at module load time: Ensure validators are registered before the application starts
- Handle async operations: Validators can be async if you need to check database or external services
- Skip validation when not needed: In update validators, check if the field being validated is actually being updated
Error Handling
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:
{
"errors": [
{
"message": "batch-picking-limit must be a number between 1 and 1000",
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}
]
}Testing
You can test validators independently:
// 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()
})Troubleshooting
Common Mistakes to Avoid
❌ DON'T register in bootstrap-module-start:
// WRONG - too late!
process.on('bootstrap-module-start', () => {
registerValidators() // Schema already built
})✅ DO register at module load:
// CORRECT - before schema build
import { registerValidators } from './validators'
registerValidators() // Module loads first❌ DON'T import application packages in setting-base:
// WRONG - creates circular dependency!
import { operatoWmsValidators } from '@things-factory/operato-wms'✅ DO let applications register themselves:
// CORRECT - applications register to shared registry
// setting-base just exports the registry
export { settingValidatorRegistry }Error: "Cannot read properties of undefined (reading 'registerCreateValidator')"
This error occurs when settingValidatorRegistry is undefined. Make sure:
- You've imported from the correct path:
@things-factory/setting-base - The
setting-basepackage has been rebuilt after adding the validator exports - Your application has been rebuilt to pick up the updated
setting-baseexports
Validators Not Being Called
If validators aren't being called:
- Verify validators are registered at module load time (not in bootstrap events)
- Check that the setting name matches exactly (case-sensitive)
- For pattern matching, verify the regex or function matches correctly
- Ensure the mutation is actually calling
settingValidatorRegistry.validateCreate()orvalidateUpdate()
Migration Guide
If you have existing validation logic in your application:
- Extract validation logic into validator functions
- Register validators at module load time in your server entry file
- Remove any custom validation from mutation hooks or middleware
- Test thoroughly to ensure validation still works
See Also
- Example validators:
packages/operato-wms/server/validators/setting-validators.tspackages/operato-hub/server/validators/setting-validators.ts
