Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,19 @@ jobs:
- name: Install Dependencies
run: npm install --ignore-scripts
- name: Test
run: npm test
run: npm test
test-esm:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm install --ignore-scripts
- name: Test
run: npm run test-esm
26 changes: 13 additions & 13 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
export type EnvSchemaData = {
[key: string]: unknown;
};
export = envSchema;

export type EnvSchemaOpt = {
schema?: object;
data?: [EnvSchemaData, ...EnvSchemaData[]] | EnvSchemaData;
env?: boolean;
dotenv?: boolean | object;
};
declare function envSchema(
_opts?: envSchema.EnvSchemaOpt
): envSchema.PlainObject;

declare function loadAndValidateEnvironment(
_opts?: EnvSchemaOpt
): EnvSchemaData;
declare namespace envSchema {
type PlainObject = { [key: string]: any };

export default loadAndValidateEnvironment;
type EnvSchemaOpt = {
schema: object;
data?: PlainObject[] | PlainObject
env?: boolean;
dotenv?: boolean | PlainObject;
};
}
62 changes: 29 additions & 33 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
// @ts-check

'use strict'

const Ajv = require('ajv')
const ajv = new Ajv({ removeAdditional: true, useDefaults: true, coerceTypes: true })

class EnvSchemaError extends Error {
/** @param {import('ajv').ErrorObject[]} errors */
constructor (errors) {
const message = errors.map(e => e.message).join('\n')

super(message)

this.errors = errors
}
}

ajv.addKeyword('separator', {
type: 'string',
metaSchema: {
Expand All @@ -12,53 +25,38 @@ ajv.addKeyword('separator', {
modifying: true,
valid: true,
errors: false,
compile: (schema) => (data, dataPath, parentData, parentDataProperty) => {
compile: (schema) => (data, _dataPath, parentData, parentDataProperty) => {
parentData[parentDataProperty] = data === '' ? [] : data.split(schema)
return true
}
})

const optsSchema = {
type: 'object',
required: ['schema'],
properties: {
schema: { type: 'object', additionalProperties: true },
data: {
oneOf: [
{ type: 'array', items: { type: 'object' }, minItems: 1 },
{ type: 'object' }
],
default: {}
},
env: { type: 'boolean', default: true },
dotenv: { type: ['boolean', 'object'], default: false }
}
}
const optsSchemaValidator = ajv.compile(optsSchema)
const optsSchemaValidator = ajv.compile(require('./options.schema.json'))

/** @type {import('./index')} */
function loadAndValidateEnvironment (_opts) {
const opts = Object.assign({}, _opts)
const opts = { ..._opts }

if (opts.schema && opts.schema[Symbol.for('fluent-schema-object')]) {
opts.schema = opts.schema.valueOf()
}

const isOptionValid = optsSchemaValidator(opts)
if (!isOptionValid) {
const error = new Error(optsSchemaValidator.errors.map(e => e.message))
error.errors = optsSchemaValidator.errors
throw error
throw new EnvSchemaError(optsSchemaValidator.errors)
}

const schema = opts.schema
schema.additionalProperties = false

let data = opts.data
if (!Array.isArray(opts.data)) {
data = [data]
const schema = {
...opts.schema,
additionalProperties: false
}

if (opts.dotenv) {
require('dotenv').config(Object.assign({}, opts.dotenv))
const data = Array.isArray(opts.data) ? opts.data : [opts.data]

if (opts.dotenv === true) {
require('dotenv').config()
} else if (opts.dotenv) {
require('dotenv').config({ ...opts.dotenv })
}

if (opts.env) {
Expand All @@ -70,9 +68,7 @@ function loadAndValidateEnvironment (_opts) {

const valid = ajv.validate(schema, merge)
if (!valid) {
const error = new Error(ajv.errors.map(e => e.message).join('\n'))
error.errors = ajv.errors
throw error
throw new EnvSchemaError(ajv.errors)
}

return merge
Expand Down
22 changes: 22 additions & 0 deletions options.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"type": "object",
"required": ["schema"],
"properties": {
"schema": { "type": "object", "additionalProperties": true },
"data": {
"oneOf": [
{ "type": "array", "items": { "type": "object" }, "minItems": 1 },
{ "type": "object" }
],
"default": {}
},
"env": { "type": "boolean", "default": true },
"dotenv": {
"oneOf": [
{ "type": "boolean" },
{ "type": "object" }
],
"default": false
}
}
}
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "index.js",
"scripts": {
"test": "standard | snazzy && tap test/*.test.js && npm run typescript",
"typescript": "tsd"
"typescript": "json2ts --no-unknownAny -i options.schema.json -o test/types/options.schema.d.ts && tsd && tsc",
"test-esm": " tap test/*.test.mjs"
},
"repository": {
"type": "git",
Expand All @@ -27,12 +28,16 @@
},
"homepage": "https://github.com/fastify/env-schema#readme",
"devDependencies": {
"@types/node": "^14.11.8",
"@types/tap": "^14.10.1",
"fluent-schema": "^0.8.0",
"json-schema-to-typescript": "^9.1.1",
"pre-commit": "^1.2.2",
"snazzy": "^8.0.0",
"standard": "^14.0.2",
"tap": "^12.7.0",
"tsd": "^0.13.1"
"tsd": "^0.13.1",
"typescript": "^4.0.3"
},
"dependencies": {
"ajv": "^6.10.2",
Expand All @@ -44,6 +49,9 @@
"tap"
]
},
"engines": {
"node": ">=10.0.0"
},
"tsd": {
"directory": "test/types"
}
Expand Down
2 changes: 1 addition & 1 deletion test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ const tests = [
},
data: [],
isOk: false,
errorMessage: 'should NOT have fewer than 1 items,should be object,should match exactly one schema in oneOf'
errorMessage: 'should NOT have fewer than 1 items\nshould be object\nshould match exactly one schema in oneOf'
},
{
name: 'simple object - ok - with separator',
Expand Down
33 changes: 33 additions & 0 deletions test/esm.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @ts-check

import t from 'tap'
import envSync from '..'

t.test('esm-test', t => {
t.plan(1)

const schema = {
type: 'object',
required: ['PORT'],
properties: {
PORT: {
type: 'string',
default: 3000
}
}
}

const data = {
PORT: 'bar'
}

/** @type {import('..').PlainObject} */
const config = envSync({
schema,
data
})

t.sameStrict(config, {
PORT: 'bar'
})
})
31 changes: 31 additions & 0 deletions test/types/options.schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* tslint:disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/

export interface OptionsSchema {
schema: {
[k: string]: any;
};
data?:
| [
{
[k: string]: any;
},
...{
[k: string]: any;
}[]
]
| {
[k: string]: any;
};
env?: boolean;
dotenv?:
| boolean
| {
[k: string]: any;
};
[k: string]: any;
}
66 changes: 36 additions & 30 deletions test/types/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expectError, expectType } from "tsd";
import envSchema, { EnvSchemaData, EnvSchemaOpt } from "../..";
import { expectError, expectType, expectAssignable, expectNotAssignable } from "tsd";
import envSchema, { PlainObject, EnvSchemaOpt } from "../..";
import { OptionsSchema } from "./options.schema"

const schema = {
type: "object",
Expand All @@ -15,41 +16,46 @@ const data = {
foo: "bar",
};

expectType<EnvSchemaData>(envSchema());
expectType<PlainObject>(envSchema());

const emptyOpt: EnvSchemaOpt = {};
expectType<EnvSchemaOpt>(emptyOpt);
expectAssignable<EnvSchemaOpt>({ schema });
expectAssignable<EnvSchemaOpt>({ schema, data });

const optWithSchema: EnvSchemaOpt = {
expectAssignable<EnvSchemaOpt>({
schema,
};
expectType<EnvSchemaOpt>(optWithSchema);
data: [{}],
});

const optWithData: EnvSchemaOpt = {
data,
};
expectType<EnvSchemaOpt>(optWithData);
expectAssignable<EnvSchemaOpt>({
schema,
data: [{}, {}],
});

expectError<EnvSchemaOpt>({
data: [], // min 1 item
expectAssignable<EnvSchemaOpt>({
schema,
dotenv: true,
});

const optWithArrayData: EnvSchemaOpt = {
data: [{}],
};
expectType<EnvSchemaOpt>(optWithArrayData);
expectAssignable<EnvSchemaOpt>({
schema,
dotenv: { path: './foo/bar.env' },
});

const optWithMultipleItemArrayData: EnvSchemaOpt = {
data: [{}, {}],
};
expectType<EnvSchemaOpt>(optWithMultipleItemArrayData);
expectAssignable<EnvSchemaOpt>({
schema,
env: false,
});

const optWithDotEnvBoolean: EnvSchemaOpt = {
dotenv: true,
};
expectType<EnvSchemaOpt>(optWithDotEnvBoolean);
expectNotAssignable<EnvSchemaOpt>({
schema,
dotenv: 'foobar',
});

const optWithDotEnvOpt: EnvSchemaOpt = {
dotenv: true,
};
expectType<EnvSchemaOpt>(optWithDotEnvOpt);
expectError<EnvSchemaOpt>({});
expectError<EnvSchemaOpt>({
schema,
data: 'foo'
});

const assignableOpt: EnvSchemaOpt = { schema };
expectAssignable<OptionsSchema>(assignableOpt);
21 changes: 21 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"files": [
"index.js"
],
"include": [
"test/**/*.js",
"test/**/*.mjs"
],
"compilerOptions": {
/* Basic Options */
"target": "ES2016",
"module": "commonjs",
"allowJs": true,
"resolveJsonModule": true,
"noEmit": true,
"esModuleInterop": true,

/* Additional Checks */
"noUnusedLocals": true,
}
}