npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@things-factory/setting-base

v4.3.767

Published

Module for adding setting option.

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

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

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 logic
  • setting-mutation.ts: Common mutation functions that call validators before saving
  • Application validators: Each application registers its own validators

How It Works

  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

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-start event
  • 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-base doesn'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

  1. Application Registers Validators:

    // packages/operato-wms/server/index.ts
    import { registerOperatoWmsSettingValidators } from './validators/setting-validators'
    registerOperatoWmsSettingValidators() // Module loads → validators registered
  2. Server Starts:

    // shell/server/server.ts
    const builtSchema = await schema() // Schema built with SettingMutation resolver
  3. User 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> | SettingValidationResult

SettingUpdateValidator

type SettingUpdateValidator = (
  setting: Setting,
  patch: SettingPatch,
  context: { domain: Domain; user: User }
) => Promise<SettingValidationResult> | SettingValidationResult

SettingValidationResult

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

  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

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:

  1. You've imported from the correct path: @things-factory/setting-base
  2. The setting-base package has been rebuilt after adding the validator exports
  3. Your application has been rebuilt to pick up the updated setting-base exports

Validators Not Being Called

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()

Migration Guide

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

See Also

  • Example validators:
    • packages/operato-wms/server/validators/setting-validators.ts
    • packages/operato-hub/server/validators/setting-validators.ts