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
41 changes: 41 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,47 @@ 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. Requires a `schema` option to be provided, and cannot contain a `properties` field.

Example:

```js
import Conf from 'conf';

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

#### ajvOptions
Type: `object`

Options to pass to AJV. Requires a `schema` option to be provided.

Example:

```js
import Conf from 'conf';

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

#### migrations

Type: `object`
Expand Down
4 changes: 4 additions & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
const ajv = new Ajv({
allErrors: true,
useDefaults: true,
...options.ajvOptions,
});
ajvFormats(ajv);

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

this.#validator = ajv.compile(schema);
Expand All @@ -112,6 +114,8 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
this.#defaultValues[key as keyof T] = value.default; // eslint-disable-line @typescript-eslint/no-unsafe-assignment
}
}
} else if (options.rootSchema ?? options.ajvOptions) {
throw new TypeError('`schema` option required to use `rootSchema` or `ajvOptions`.');
}

if (options.defaults) {
Expand Down
15 changes: 15 additions & 0 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} from 'ajv/dist/core.js';
import type Conf from './index.js';

export type Options<T extends Record<string, any>> = {
Expand Down Expand Up @@ -50,6 +51,20 @@ export type Options<T extends Record<string, any>> = {
*/
schema?: Schema<T>;

/**
Top-level properties for the schema.

Requires a `schema` option to be provided, and cannot contain a `properties` field.
*/
rootSchema?: Omit<TypedJSONSchema, 'properties'>;

/**
Options to pass to AJV.

Requires a `schema` option to be provided.
CalebUsadi marked this conversation as resolved.
Show resolved Hide resolved
*/
CalebUsadi marked this conversation as resolved.
Show resolved Hide resolved
ajvOptions?: CurrentOptions;

/**
Name of the config file (without extension).

Expand Down
37 changes: 37 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,43 @@ test('schema - validate Conf default', t => {
}, {message: 'Config schema violation: `foo` must be string'});
});

test('schema - rootSchema without schema', t => {
t.throws(() => {
new Conf({
cwd: temporaryDirectory(),
rootSchema: {},
});
}, {message: '`schema` option required to use `rootSchema` or `ajvOptions`.'});
});

test('schema - validate rootSchema', t => {
t.throws(() => {
const config = new Conf({
cwd: temporaryDirectory(),
schema: {},
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(),
schema: {},
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