Documentationcurrent version
Help us improve the docs by fixing typos and proposing enhancements.

Nikita

Schema definitions

The configuration schema validates the properties provided to and returned by an action.

Schema definition is optional but all the actions available in Nikita define a schema. It is used for validation but it also provides additional functionalities such as default values and coercion.

An action can partially or fully inherit from the properties of other actions. Nikita unifies the declaration of schemas using the definitions metadata property.

When the action properties don't conform with the schema, the action is rejected with the NIKITA_SCHEMA_VALIDATION_CONFIG error.

Schema leverages the JSON Schema specification. Literally, it is a JavaScript object with validation keywords. Internally, the Ajv library is used.

Basic schema definition

To define a schema, use to the definitions metadata. For example, when registering an action:

nikita
// Registering an action with schema
.registry.register('my_action', {
  metadata: {
    definitions: {      'config': {        type: 'object',        properties: {          'my_config': {            type: 'string',            default: 'my value',            description: 'My configuration property.'          }        }      }
    }
  },
  handler: ({config}) => {
    // Print config
    console.info(config)
  }
})
// Call an action
.my_action()
// Prints:
// { my_config: 'my value' }

The above example defines the configuration property my_config. It is of type string, the default value is my value and it has a description.

It is also possible to provide the definition metadata when calling the action. The same action as above could be written as:

nikita
// Call an action
.call({
  $definitions: {    'config': {      type: 'object',      properties: {        'my_config': {          type: 'string',          default: 'my value',          description: 'My configuration property.'        }      }    }
  }
}, ({config}) => {
  // Print config
  console.info(config)
})
// Prints:
// { my_config: 'my value' }

Required property

To define a property as required, use the required keyword and pass an array of strings with property names:

nikita
// Registering an action with a required property
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_config': {            type: 'string'          }        },
        required: ['my_config']      }
    }
  },
  handler: ({config}) => {
    // Print config
    console.info(config)
  }
})
// Calling `my_action` with `my_config` fulfills
.my_action({my_config: 'my value'})
// Prints:
// { my_config: 'my value' }
// Calling `my_action` without `my_config` rejects an error
.my_action()
// Catch error
.catch(err => {
  // Print the error message
  console.info(err.message)
})
// Prints:
// NIKITA_SCHEMA_VALIDATION_CONFIG: one error was found in the configuration of action `my_action`: #/definitions/config/required config should have required property 'my_config'.

To define a property as conditionally required property, use the if/then/else properties. The following example defines the my_config property required only when the value of my_flag is true:

nikita
// Registering an action with a conditionally required property
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_flag': {            type: 'boolean',          },          'my_config': {
            type: 'string'
          }
        },
        if: {properties: {'my_flag': {const: true}}},        then: {required: ['my_config']}      }
    }
  },
  handler: ({config}) => {
    // Print config
    console.info(config)
  }
})
// Calling `my_action` with `my_flag: false` fulfills
.my_action({
  my_flag: false
})
// Prints:
// { my_flag: false }
// Calling `my_action` with `my_config` defined fulfills
.my_action({
  my_config: 'my value'
})
// Prints:
// { my_config: 'my value' }
// Calling `my_action` without `my_flag: true` rejects an error
.my_action({my_flag: true})
// Catch error
.catch(err => {
  // Print the error message
  console.info(err.message)
})
// Prints:
// NIKITA_SCHEMA_VALIDATION_CONFIG: multiple errors were found in the configuration of action `my_action`: #/definitions/config/if config should match "then" schema, failingKeyword is "then"; #/definitions/config/then/required config should have required property 'my_config'.

Coercing data types

Type coercion is about changing a value from one data type to another. For example, an integer could be converted to a string with numeric characters.

Based on the schema definitions, the action arguments are automatically converted to the targeted types before executing the action handler. In this example, the my_config configuration is defined as a string. If a user call the action with an integer, it is coerced to a string:

nikita
// Registering an action
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_config': {
            type: 'string'          }
        }
      }
    }
  },
  handler: ({config}) => {
    // Print the config type
    console.info(typeof config.my_config)
  }
})
// Pass a number to `my_config`
.my_action({
  my_config: 100})
// Prints:
// string

Follow the Ajv documentation to learn all the possible type coercions.

Multiple data types

Multiple types can be defined by setting the type property as an array:

nikita
nikita
// Registering an action with multiple data types properties
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_config': {
            type: ['string', 'number']          }
        },
      }
    }
  },
  handler: ({config}) => {
    // Print the config type
    console.info(typeof config.my_config)
  }
})
// Calling `my_action` passing a number to `my_config` prints "number"
.my_action({my_config: 10})
// Calling `my_action` passing a string to `my_config` prints "string"
.my_action({my_config: 'my value'})

Be careful when using the alternative oneOf keyword. Because coercion is activated, the rule will fail if the value is compatible with multiple types. For example, the following declaration will fail if the property can be converted to both a string and a number:

{
  ...
  "my_config": {
    "oneOf": [
      {"type": "string"},
      {"type": "number"},
    ]
  }
}

Refers the Ajv documentation to learn more.

Referencing internal properties

To reference a property defined in another action, use the $ref keyword.

To reference a property of the current action, use the $ref keyword in a combination with definitions:

nikita
// Registering an action with referenced properties
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_config': {
            $ref: '#/definitions/my_referenced_config'          }
        }
      },
      'my_referenced_config': {        type: 'string'      }    }
  },
  handler: ({config}) => {
    // Print the config type
    console.info(typeof config.my_config)
  }
})
// Calling `my_action` prints "string", because the number type coerced to string.
.my_action({my_config: 10})

Referencing external properties

To reference a property in an external action, Nikita introduces two discovery mechanisms.

The module:// prefix search for the action exported in the location defined after the prefix. It uses the Node.js module discovery algorithm.

nikita
// Registering an action with referenced properties
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_config': {            $ref: 'module://@nikitajs/core/lib/actions/fs/base/readdir#/definitions/config/properties/target'          }        }
      }
    }
  },
  handler: ({config}) => {
    // Print the config type
    console.info(typeof config.my_config)
  }
})
// Calling `my_action` prints "string", because the number type coerced to string.
.my_action({my_config: 10})

The registry:// prefix search for an action present in the registry:

nikita
// Registering an action
.registry.register(['my', 'first', 'action'], {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_config': {            type: 'string'          }        }
      }
    }
  }
})
// Registering an action with referenced properties
.registry.register(['my', 'second', 'action'], {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          // Referencing via registry
          'my_config': {            $ref: 'registry://my/first/action#/definitions/config/properties/my_config'          }        }
      }
    }
  },
  handler: ({config}) => {
    // Print the config type
    console.info(typeof config.my_config)
  }
})
// Calling `my.second.action` prints "string", because the number type coerced to string.
.my.second.action({my_config: 10})

Pattern properties

An object with dynamic keys is validated with the patternProperties keyword. The value of this keyword is a map where keys are regular expressions and the values are JSON Schemas.

In this example, all the keys starting with my_ must be of type string:

nikita
// Registering an action with the pattern property
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        patternProperties: {          'my_.*': {            type: 'string'          }        }      }
    }
  },
  handler: ({config}) => {
    // Print the config type
    console.info(typeof config.my_config)
  }
})
// Calling `my_action` prints "string", because the number type coerced to string.
.my_action({my_config: 10})

Disallowing additional properties

By default, not all properties must be defined in the schema. Additional properties are not evaluated and are passed as-is. To enforce the schema definition of every property, use the additionalProperties keyword with the false value.

The following example disallows passing any properties other than my_config:

nikita
// Registering an action disallowing additional properties
.registry.register('my_action', {
  metadata: {
    definitions: {
      'config': {
        type: 'object',
        properties: {
          'my_config': {
            type: 'string'
          }
        },
        // Disallow additional properties
        additionalProperties: false      }
    }
  }
})
// Calling `my_action` with the `my_another_config` config will not be fulfilled
.my_action({my_another_config: 10})
// Catch error
.catch(err => {
  // Print error message
  console.info(err.message)
})
// Prints:
// NIKITA_SCHEMA_VALIDATION_CONFIG: one error was found in the configuration of action `my_action`: #/definitions/config/additionalProperties config should NOT have additional properties, additionalProperty is "my_another_config".
Edit on GitHub
Navigate
About

Nikita is an open source project hosted on GitHub and developed by Adaltas.