Skip to content

Commit

Permalink
Merge pull request #476 from mocks-server/refactor/typescript-migration
Browse files Browse the repository at this point in the history
Release 5.0.0-beta.3
  • Loading branch information
javierbrea authored May 25, 2023
2 parents 2c8d55f + 11aedaa commit 7c5a711
Show file tree
Hide file tree
Showing 43 changed files with 936 additions and 172 deletions.
2 changes: 2 additions & 0 deletions packages/config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- fix: Do not return root namespace in config.namespaces
- feat: Throw an error when creating namespace if it already exists, as creating an option does
- feat: Make itemsType property in options of type 'array' not mandatory
- feat: Support nullable in options of type 'array' and 'object'
- feat: Add `unknown` type to options. They support any type and are not validated

## [1.4.0] - 2022-09-01

Expand Down
2 changes: 1 addition & 1 deletion packages/config/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {

// The glob patterns Jest uses to detect test files
testMatch: ["<rootDir>/test/**/*.spec.js"],
// testMatch: ["<rootDir>/test/**/getValidationSchema.spec.js"],
// testMatch: ["<rootDir>/test/**/validate.spec.js"],

// The test environment that will be used for testing
testEnvironment: "node",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mocks-server/config",
"version": "2.0.0-beta.1",
"version": "2.0.0-beta.2",
"description": "Modular configuration provider. Read it from file, environment and arguments",
"keywords": [
"configuration",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sonar.organization=mocks-server
sonar.projectKey=mocks-server_main_config
sonar.projectName=config
sonar.projectVersion=2.0.0-beta.1
sonar.projectVersion=2.0.0-beta.2

sonar.javascript.file.suffixes=.js
sonar.sourceEncoding=UTF-8
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export const Config: ConfigConstructor = class Config implements ConfigInterface
return getValidationSchema({
namespaces: this._namespaces,
allowAdditionalProperties,
removeCustomProperties: true,
});
}

Expand Down
6 changes: 2 additions & 4 deletions packages/config/src/Option.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EventEmitter from "events";

import deepMerge from "deepmerge";
import { isUndefined, isEqual } from "lodash";
import { isUndefined, isEqual, isNull } from "lodash";

import type { UnknownObject } from "./Common.types";
import { addEventListener, CHANGE } from "./Events";
Expand All @@ -12,8 +12,6 @@ import type {
SetMethodOptions,
GetOptionValueTypeFromDefinition,
GetOptionTypeFromDefinition,
OptionInterfaceOfType,
OptionDefinition,
} from "./Option.types";
import { typeIsArray, typeIsObject, optionIsObject, avoidArraysMerge } from "./Typing";
import { validateOptionAndThrow, validateValueTypeAndThrow } from "./Validation";
Expand Down Expand Up @@ -93,7 +91,7 @@ export class Option<T extends OptionDefinitionGeneric, TypeOfValue = void>
private _clone(
value: GetOptionValueTypeFromDefinition<T, TypeOfValue>
): GetOptionValueTypeFromDefinition<T, TypeOfValue> {
if (isUndefined(value)) {
if (isUndefined(value) || (this._nullable === true && isNull(value))) {
return value;
}
if (typeIsArray(this._type)) {
Expand Down
154 changes: 108 additions & 46 deletions packages/config/src/Validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ import {
NULL_TYPE,
UNKNOWN_TYPE,
} from "./Typing";
import type { ConfigValidationResult, GetValidationSchemaOptions } from "./Validation.types";
import type {
ConfigValidationResult,
GetValidationSchemaOptions,
JSONSchema7WithUnknown,
} from "./Validation.types";

const ajv = new Ajv({ allErrors: true, allowUnionTypes: true });

const UNKNOWN_TYPE_SCHEMA = [BOOLEAN_TYPE, NUMBER_TYPE, STRING_TYPE, OBJECT_TYPE, ARRAY_TYPE];
ajv.addKeyword({
keyword: "isUnknown",
compile: (schema: boolean) => () => schema === true,
errors: false,
});

type AnySingleValue = unknown;
type AnyArrayValue = unknown[];
Expand All @@ -39,7 +47,7 @@ function enforceDefaultTypeSchema({
type: OptionType;
itemsType?: OptionItemsType;
nullable?: boolean;
}): JSONSchema7 {
}): JSONSchema7WithUnknown {
const properties: { [key: string]: JSONSchema7 } = {
name: { type: STRING_TYPE as JSONSchema7TypeName },
type: { enum: [type] },
Expand All @@ -58,24 +66,24 @@ function enforceDefaultTypeSchema({

const defaultProperty: JSONSchema7 = {};

const allowedType = type === UNKNOWN_TYPE ? UNKNOWN_TYPE_SCHEMA : type;

if (nullable) {
defaultProperty.type = Array.isArray(allowedType)
? ([...allowedType, NULL_TYPE] as JSONSchema7TypeName[])
: [allowedType as JSONSchema7TypeName, NULL_TYPE as JSONSchema7TypeName];
if (type !== UNKNOWN_TYPE) {
defaultProperty.type = [type as JSONSchema7TypeName, NULL_TYPE as JSONSchema7TypeName];
}
properties.nullable = { enum: [true] };
} else {
defaultProperty.type = allowedType as JSONSchema7TypeName;
if (type !== UNKNOWN_TYPE) {
defaultProperty.type = type as JSONSchema7TypeName;
}
}

if (itemsType) {
const allowedItemsType = itemsType === UNKNOWN_TYPE ? UNKNOWN_TYPE_SCHEMA : itemsType;
properties.itemsType = { enum: [itemsType] };
defaultProperty.items = {
type: allowedItemsType as JSONSchema7TypeName,
};

if (itemsType !== UNKNOWN_TYPE) {
defaultProperty.items = {
type: itemsType as JSONSchema7TypeName,
};
}
schema.required = ["name", "type", "nullable", "itemsType"];
}

Expand All @@ -85,7 +93,7 @@ function enforceDefaultTypeSchema({
return schema;
}

const optionSchema: JSONSchema7 = {
const optionSchema: JSONSchema7WithUnknown = {
type: OBJECT_TYPE as JSONSchema7TypeName,
oneOf: [
enforceDefaultTypeSchema({ type: NUMBER_TYPE }),
Expand All @@ -95,14 +103,21 @@ const optionSchema: JSONSchema7 = {
enforceDefaultTypeSchema({ type: BOOLEAN_TYPE }),
enforceDefaultTypeSchema({ type: BOOLEAN_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: OBJECT_TYPE }),
enforceDefaultTypeSchema({ type: OBJECT_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: UNKNOWN_TYPE }),
enforceDefaultTypeSchema({ type: UNKNOWN_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: NUMBER_TYPE }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: NUMBER_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: STRING_TYPE }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: STRING_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: BOOLEAN_TYPE }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: BOOLEAN_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: OBJECT_TYPE }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: OBJECT_TYPE, nullable: true }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: UNKNOWN_TYPE }),
enforceDefaultTypeSchema({ type: ARRAY_TYPE, itemsType: UNKNOWN_TYPE, nullable: true }),
],
};

Expand All @@ -112,7 +127,7 @@ function emptySchema({
allowAdditionalProperties,
}: {
allowAdditionalProperties: boolean;
}): JSONSchema7 {
}): JSONSchema7WithUnknown {
return {
type: OBJECT_TYPE as JSONSchema7TypeName,
properties: {},
Expand Down Expand Up @@ -220,31 +235,49 @@ function addNamespaceSchema(
{
rootSchema,
allowAdditionalProperties,
}: { rootSchema?: JSONSchema7; allowAdditionalProperties: boolean }
): JSONSchema7 {
removeCustomProperties,
}: {
rootSchema?: JSONSchema7;
allowAdditionalProperties: boolean;
removeCustomProperties?: boolean;
}
): JSONSchema7WithUnknown {
const initialSchema = rootSchema || emptySchema({ allowAdditionalProperties });
const schema = namespace.options.reduce(
(currentSchema: JSONSchema7, option: OptionInterfaceGeneric) => {
const properties: { [key: string]: JSONSchema7 } = {};
const allowedType = option.type === UNKNOWN_TYPE ? UNKNOWN_TYPE_SCHEMA : option.type;
if (option.nullable) {
properties[option.name] = {
type: Array.isArray(allowedType)
? ([...allowedType, NULL_TYPE] as JSONSchema7TypeName[])
: [allowedType as JSONSchema7TypeName, NULL_TYPE as JSONSchema7TypeName],
};
(currentSchema: JSONSchema7WithUnknown, option: OptionInterfaceGeneric) => {
const properties: { [key: string]: JSONSchema7WithUnknown } = {};
if (option.type !== UNKNOWN_TYPE) {
if (option.nullable) {
properties[option.name] = {
type: [option.type, NULL_TYPE] as JSONSchema7TypeName[],
};
} else {
properties[option.name] = {
type: option.type as JSONSchema7TypeName,
};
}
} else {
properties[option.name] = {
type: allowedType as JSONSchema7TypeName,
};
if (!removeCustomProperties) {
properties[option.name] = {
isUnknown: true,
};
} else {
properties[option.name] = {};
}
}

if (optionIsArray(option)) {
const allowedItemsType =
option.itemsType === UNKNOWN_TYPE ? UNKNOWN_TYPE_SCHEMA : option.itemsType;
properties[option.name].items = {
type: allowedItemsType,
} as JSONSchema7Definition;
if (option.itemsType !== UNKNOWN_TYPE) {
properties[option.name].items = {
type: option.itemsType,
} as JSONSchema7Definition;
} else {
if (!removeCustomProperties) {
properties[option.name].items = {
isUnknown: true,
};
}
}
}
currentSchema.properties = {
...currentSchema.properties,
Expand All @@ -257,6 +290,7 @@ function addNamespaceSchema(
addNamespacesSchema(namespace.namespaces, {
rootSchema: initialSchema,
allowAdditionalProperties,
removeCustomProperties,
});
return schema;
}
Expand All @@ -266,16 +300,26 @@ function addNamespacesSchema(
{
rootSchema,
allowAdditionalProperties,
}: { rootSchema: JSONSchema7; allowAdditionalProperties: boolean }
): JSONSchema7 {
removeCustomProperties,
}: {
rootSchema: JSONSchema7;
allowAdditionalProperties: boolean;
removeCustomProperties?: boolean;
}
): JSONSchema7WithUnknown {
return namespaces.reduce((currentSchema: JSONSchema7, namespace: ConfigNamespaceInterface) => {
const properties: { [key: string]: JSONSchema7 } = {};
const properties: { [key: string]: JSONSchema7WithUnknown } = {};
if (!namespace.isRoot) {
properties[namespace.name] = addNamespaceSchema(namespace, {
allowAdditionalProperties,
removeCustomProperties,
});
} else {
addNamespaceSchema(namespace, { rootSchema: currentSchema, allowAdditionalProperties });
addNamespaceSchema(namespace, {
rootSchema: currentSchema,
allowAdditionalProperties,
removeCustomProperties,
});
}
currentSchema.properties = {
...currentSchema.properties,
Expand All @@ -288,13 +332,16 @@ function addNamespacesSchema(
function getConfigValidationSchema({
namespaces,
allowAdditionalProperties,
removeCustomProperties,
}: {
namespaces: ConfigNamespaceInterface[];
allowAdditionalProperties: boolean;
}): JSONSchema7 {
removeCustomProperties?: boolean;
}): JSONSchema7WithUnknown {
return addNamespacesSchema(namespaces, {
rootSchema: emptySchema({ allowAdditionalProperties }),
allowAdditionalProperties,
removeCustomProperties,
});
}

Expand All @@ -303,11 +350,16 @@ export function validateConfigAndThrow(
{
namespaces,
allowAdditionalProperties,
}: { namespaces: ConfigNamespaceInterface[]; allowAdditionalProperties: boolean }
removeCustomProperties,
}: {
namespaces: ConfigNamespaceInterface[];
allowAdditionalProperties: boolean;
removeCustomProperties?: boolean;
}
): void | never {
validateSchemaAndThrow(
config,
getConfigValidationSchema({ namespaces, allowAdditionalProperties })
getConfigValidationSchema({ namespaces, allowAdditionalProperties, removeCustomProperties })
);
}

Expand All @@ -316,19 +368,29 @@ export function validateConfig(
{
namespaces,
allowAdditionalProperties,
}: { namespaces: ConfigNamespaceInterface[]; allowAdditionalProperties: boolean }
removeCustomProperties,
}: {
namespaces: ConfigNamespaceInterface[];
allowAdditionalProperties: boolean;
removeCustomProperties?: boolean;
}
): ConfigValidationResult {
return validateSchema(
config,
getConfigValidationSchema({ namespaces, allowAdditionalProperties })
getConfigValidationSchema({ namespaces, allowAdditionalProperties, removeCustomProperties })
);
}

export function getValidationSchema({
namespaces,
allowAdditionalProperties,
}: GetValidationSchemaOptions): JSONSchema7 {
return getConfigValidationSchema({ namespaces, allowAdditionalProperties });
removeCustomProperties,
}: GetValidationSchemaOptions): JSONSchema7WithUnknown {
return getConfigValidationSchema({
namespaces,
allowAdditionalProperties,
removeCustomProperties,
});
}

export function validateOptionAndThrow(option: OptionDefinitionGeneric): void | never {
Expand Down
Loading

0 comments on commit 7c5a711

Please sign in to comment.