Skip to content

Commit

Permalink
Add rootSchema and ajvOptions options (#196)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
CalebUsadi and sindresorhus authored Dec 7, 2024
1 parent 8fdcdd7 commit 2819caa
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 9 deletions.
51 changes: 48 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ Type: `object`

[JSON Schema](https://json-schema.org) to validate your config data.

Under the hood, the JSON Schema validator [ajv](https://ajv.js.org/json-schema.html) is used to validate your config. We use [JSON Schema draft-2020-12](https://json-schema.org/draft/2020-12/release-notes) and support all validation keywords and formats.

You should define your schema as an object where each key is the name of your data's property and each value is a JSON schema used to validate that property. See more [here](https://json-schema.org/understanding-json-schema/reference/object.html#properties).
This will be the [`properties`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) object of the JSON schema. That is, define `schema` as an object where each key is the name of your data's property and each value is a JSON schema used to validate that property.

Example:

Expand Down Expand Up @@ -100,6 +98,53 @@ config.set('foo', '1');

**Note:** The `default` value will be overwritten by the `defaults` option if set.

#### rootSchema

Type: `object`

Top-level properties for the schema, excluding `properties` field.

Example:

```js
import Conf from 'conf';

const store = new Conf({
projectName: 'foo',
schema: { /**/ },
rootSchema: {
additionalProperties: false
}
});
```

#### ajvOptions

Type: `object`

[Options passed to AJV](https://ajv.js.org/options.html).

Under the hood, the JSON Schema validator [ajv](https://ajv.js.org/json-schema.html) is used to validate your config. We use [JSON Schema draft-2020-12](https://json-schema.org/draft/2020-12/release-notes) and support all validation keywords and formats.

**Note:** By default, `allErrors` and `useDefaults` are both set to `true`, but can be overridden.

Example:

```js
import Conf from 'conf';

const store = new Conf({
projectName: 'foo',
schema: { /**/ },
rootSchema: {
additionalProperties: false
},
ajvOptions: {
removeAdditional: true
}
});
```

#### migrations

Type: `object`
Expand Down
8 changes: 5 additions & 3 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,25 +89,27 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown

this.#options = options;

if (options.schema) {
if (typeof options.schema !== 'object') {
if (options.schema ?? options.ajvOptions ?? options.rootSchema) {
if (options.schema && typeof options.schema !== 'object') {
throw new TypeError('The `schema` option must be an object.');
}

const ajv = new Ajv({
allErrors: true,
useDefaults: true,
...options.ajvOptions,
});
ajvFormats(ajv);

const schema: JSONSchema = {
...options.rootSchema,
type: 'object',
properties: options.schema,
};

this.#validator = ajv.compile(schema);

for (const [key, value] of Object.entries(options.schema) as any) { // TODO: Remove the `as any`.
for (const [key, value] of Object.entries(options.schema ?? {}) as any) { // TODO: Remove the `as any`.
if (value?.default) {
this.#defaultValues[key as keyof T] = value.default; // eslint-disable-line @typescript-eslint/no-unsafe-assignment
}
Expand Down
50 changes: 47 additions & 3 deletions source/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {type JSONSchema as TypedJSONSchema} from 'json-schema-typed';
// eslint-disable unicorn/import-index
import type {CurrentOptions as AjvOptions} from 'ajv/dist/core.js';
import type Conf from './index.js';

export type Options<T extends Record<string, any>> = {
Expand All @@ -13,9 +14,7 @@ export type Options<T extends Record<string, any>> = {
/**
[JSON Schema](https://json-schema.org) to validate your config data.
Under the hood, the JSON Schema validator [ajv](https://github.com/epoberezkin/ajv) is used to validate your config. We use [JSON Schema draft-07](https://json-schema.org/latest/json-schema-validation.html) and support all [validation keywords](https://github.com/epoberezkin/ajv/blob/master/KEYWORDS.md) and [formats](https://github.com/epoberezkin/ajv#formats).
You should define your schema as an object where each key is the name of your data's property and each value is a JSON schema used to validate that property. See more [here](https://json-schema.org/understanding-json-schema/reference/object.html#properties).
This will be the [`properties`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) object of the JSON schema. That is, define `schema` as an object where each key is the name of your data's property and each value is a JSON schema used to validate that property.
@example
```
Expand Down Expand Up @@ -50,6 +49,49 @@ export type Options<T extends Record<string, any>> = {
*/
schema?: Schema<T>;

/**
Top-level properties for the schema, excluding `properties` field.
@example
```
import Conf from 'conf';
const store = new Conf({
projectName: 'foo',
schema: {},
rootSchema: {
additionalProperties: false
}
});
```
*/
rootSchema?: Omit<TypedJSONSchema, 'properties'>;

/**
[Options passed to AJV](https://ajv.js.org/options.html).
Under the hood, the JSON Schema validator [ajv](https://ajv.js.org/json-schema.html) is used to validate your config. We use [JSON Schema draft-2020-12](https://json-schema.org/draft/2020-12/release-notes) and support all validation keywords and formats.
**Note:** By default, `allErrors` and `useDefaults` are both set to `true`, but can be overridden.
@example
```
import Conf from 'conf';
const store = new Conf({
projectName: 'foo',
schema: {},
rootSchema: {
additionalProperties: false
},
ajvOptions: {
removeAdditional: true
}
});
```
*/
ajvOptions?: AjvOptions;

/**
Name of the config file (without extension).
Expand Down Expand Up @@ -259,3 +301,5 @@ export type OnDidChangeCallback<T> = (newValue?: T, oldValue?: T) => void;
export type OnDidAnyChangeCallback<T> = (newValue?: Readonly<T>, oldValue?: Readonly<T>) => void;

export type Unsubscribe = () => void;

export type {CurrentOptions as AjvOptions} from 'ajv/dist/core.js';
26 changes: 26 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,32 @@ test('schema - validate Conf default', t => {
}, {message: 'Config schema violation: `foo` must be string'});
});

test('schema - validate rootSchema', t => {
t.throws(() => {
const config = new Conf({
cwd: temporaryDirectory(),
rootSchema: {
additionalProperties: false,
},
});
config.set('foo', 'bar');
}, {message: 'Config schema violation: `` must NOT have additional properties'});
});

test('AJV - validate AJV options', t => {
const config = new Conf({
cwd: temporaryDirectory(),
ajvOptions: {
removeAdditional: true,
},
rootSchema: {
additionalProperties: false,
},
});
config.set('foo', 'bar');
t.is(config.get('foo'), undefined);
});

test('.get() - without dot notation', t => {
t.is(t.context.configWithoutDotNotation.get('foo'), undefined);
t.is(t.context.configWithoutDotNotation.get('foo', '🐴'), '🐴');
Expand Down

0 comments on commit 2819caa

Please sign in to comment.