Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rootSchema and ajvOptions options #196

Merged
merged 12 commits into from
Dec 7, 2024
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
}
});
```
*/
CalebUsadi marked this conversation as resolved.
Show resolved Hide resolved
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
Loading