Skip to content

Commit

Permalink
Merge pull request #29 from trayio/jon-urry/CSP-2182/add-schema-gener…
Browse files Browse the repository at this point in the history
…ator-to-connector-utils

[CSP-2182] add schema generator to connector utils
  • Loading branch information
jonurry authored Mar 27, 2020
2 parents e354e6f + 5e8d682 commit 10d79dd
Show file tree
Hide file tree
Showing 27 changed files with 7,921 additions and 4,325 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.vscode
/coverage
/node_modules
/coverage
81 changes: 69 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ for arrays).</p>
<dt><a href="#validatePaginationRange">validatePaginationRange(value, validation)</a></dt>
<dd><p>Helper for validating user pagination input for a given range.</p>
</dd>
<dt><a href="#generateInputSchema">generateInputSchema({ schema, keys, operation = 'schema' })</a></dt>
<dd><p>Helper for generating an operation input schema.</p>
</dd>
</dl>

<a name="GenericError"></a>
Expand Down Expand Up @@ -373,21 +376,75 @@ Helper for validating user pagination input for a given range.
| validation.maxRange | <code>Integer</code> \| <code>String</code> | The maximum range specified by the API. |
| validation.inputName | <code>String</code> | The name of the input the range is associated with. |

## generateInputSchema({ schema, keys, operation = 'schema' })

Helper for generating an operation input schema.

Will log to the console if:

- a requested key does not exist, or
- `type` or `description` keys are missing

Will not log to the console if requested key does not exist, but is overridden with at least a type and description.

- @param {Object} schema The full connector schema definition.
- @param {Object} keys The keys that you wish to extract from the schema with any override values.
- @param {String} operation The name of the connector operation that you are generating the schema for.
- This will be used as the root of the object path when logging validation issues.
- @return {object} A copy of the requested schema elements.
\*/

**Kind**: global function

| Param | Type | Description |
| --------- | ------------------- | --------------------------------------------------------------------------- |
| schema | <code>Object</code> | The full connector schema definition. |
| keys | <code>Object</code> | The keys that you wish to extract from the schema with any override values. |
| operation | <code>String</code> | The name of the connector operation that you are generating the schema for. |

For more information on how to use the schema generator, please see [schema-generation.md](./schema-generation.md).

**Example**

```js
validatePaginationRange(50, {
minRange: 1,
maxRange: 100,
inputName: 'page size',
generateInputSchema({
operation: 'operationName',
schema: fullSchema,
keys: {
full_schema_key_1: {},
full_schema_key_2: {},
full_schema_key_3: {},
},
});
// no error thrown as pagination is within range

validatePaginationRange(101, {
minRange: 1,
maxRange: 100,
inputName: 'page size',
/**
* `fullSchema` is the complete schema definition for the connector
* `full_schema_key_1` is extracted from the full schema without modification
* `full_schema_key_2` is extracted from the full schema without modification
* `full_schema_key_3` is extracted from the full schema without modification
*/

generateInputSchema({
operation: 'operationName',
schema: fullSchema,
keys: {
full_schema_key_1: {},
full_schema_key_2: {
required: true,
description: 'Override key values.',
default: 'value',
},
new_key: {
type: 'string',
description: 'New date key, not in full schema.',
format: 'datetime',
date_mask: 'X',
},
},
});
// will throw a UserInputError as the pageSize is outside the range
// Error message returned: 'The page size must be between 1 - 100.'
/**
* `fullSchema` is the complete schema definition for the connector
* `full_schema_key_1` is extracted from the full schema without modification
* `full_schema_key_2` is extracted from the full schema and extended/overridden with extra keys and values
* `new_key` is not in the full schema but it's full keys and values are supplied
*/
```
Binary file added img/node-dev-log.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/test-runner-log-error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/test-runner-log-warning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
143 changes: 143 additions & 0 deletions lib/generateInputSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint-disable no-console */
const _ = require('lodash');
const logger = require('./internal/logger');

const MISSING_KEYS_MESSAGE =
'There are missing schema keys that should be provided:';

const flattenAndCompact = ({ array }) => _.flattenDeep(_.compact(array));

const logIssuesToConsole = ({ issues }) => {
if (issues.some(error => error.missing === 'type')) {
logger.log('error', MISSING_KEYS_MESSAGE);
} else {
logger.log('warn', MISSING_KEYS_MESSAGE);
}
logger.log(
'table',
issues.map(error => ({
key: error.key,
[error.missing]: 'missing',
})),
['key', 'type', 'description'],
);
};

const shouldInvokeIteratee = ({ value, key }) =>
// ignore lookups, object properties and oneOf arrays
!['lookup', 'properties'].includes(key) && !Array.isArray(value.oneOf);

const shouldParseChildren = ({ key }) => !['lookup'].includes(key);

/**
* Deep recursive iteration through a full schema object definition.
* Returns a flat array of objects specifying the missing keys.
* Validation rules are specified in the iteratee.
*/
const deepValidateSchema = ({ collection, iteratee, path = 'schema' }) => {
const recursiveArray = ({ col, fn, oPath }) => {
return Array.isArray(col)
? col.map(element =>
deepValidateSchema({
collection: element,
iteratee: fn,
path: oPath,
}),
)
: [];
};

const issues = [];

issues.push(recursiveArray({ col: collection, fn: iteratee, oPath: path }));

if (_.isPlainObject(collection)) {
_.forEach(collection, (value, key) => {
issues.push(
recursiveArray({
col: value,
fn: iteratee,
oPath: `${path}.${key}`,
}),
);

if (_.isPlainObject(value)) {
if (shouldInvokeIteratee({ value, key })) {
issues.push(
iteratee({ element: value, key: `${path}.${key}` }),
);
}
if (shouldParseChildren({ key })) {
issues.push(
deepValidateSchema({
collection: value,
iteratee,
path: `${path}.${key}`,
}),
);
}
}
});
}

return flattenAndCompact({ array: issues });
};

// Schema elements must include 'type' and 'description' keys.
const checkForIncompleteSchemaElements = ({ element, key }) => {
const keys = Object.keys(element);
const incompleteSchemaElements = [];
if (_.isPlainObject(element)) {
if (!keys.includes('type')) {
incompleteSchemaElements.push({ key, missing: 'type' });
}
if (!keys.includes('description')) {
incompleteSchemaElements.push({ key, missing: 'description' });
}
}
return incompleteSchemaElements;
};

/**
* Generates an operation input schema.
* Will log to the console if a requested key does not exist.
* Will not log to the console if requested key does not exist,
* but is overridden with at least a type and description.
*
* @param {Object} schema The full connector schema definition.
* @param {Object} keys The keys that you wish to extract from the schema with any override values.
* @param {String} operation The name of the connector operation that you are generating the schema for.
* This will be used as the root of the object path when logging validation issues.
* @return {object} A copy of the requested schema elements.
*/

const generateInputSchema = ({ schema, keys, operation = 'schema' }) => {
// map the required input parameters to their individual schemas
// and override with any additionally provided values
const mappedSchema = _.map(keys, (value, key) => ({
[key]: { ...schema[key], ...value },
}));

// find incomplete schema definitions
const incompleteSchemaErrors = deepValidateSchema({
collection: mappedSchema,
iteratee: checkForIncompleteSchemaElements,
path: operation,
});

// Log issues for missing schema definitions to console
if (incompleteSchemaErrors.length > 0) {
logIssuesToConsole({ issues: incompleteSchemaErrors });
}

// combine the individual schemas to a single operation schema
const combinedSchema = mappedSchema.reduce(
(acc, curr) => ({ ...acc, ...curr }),
{},
);

// deep clone the schema so that only copies of schema elements are returned
return _.cloneDeep(combinedSchema);
};

module.exports = generateInputSchema;
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const deepMapKeys = require('./deepMapKeys');
const removeEmptyObjects = require('./removeEmptyObjects');
const lookup = require('./lookup');
const validatePaginationRange = require('./validatePaginationRange');
const generateInputSchema = require('./generateInputSchema');
// const xmlFormatter = require('./xmlFormatter');
const { mustachedDDL, DDL } = require('./ddl');
const {
Expand Down Expand Up @@ -66,6 +67,7 @@ module.exports = {
mustachedDDL,
DDL,
validatePaginationRange,
generateInputSchema,
// Commenting for initial release pending functionality to add paths to treatAsArray
// xml: {
// xmlFormatter,
Expand Down
8 changes: 8 additions & 0 deletions lib/internal/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* wrapper for the console function
* allows us to log to the console within the utils library
* easy to stub or mock logging in tests without affecting production logging
*/

// eslint-disable-next-line no-console
exports.log = (level, ...toLog) => console[level].apply(this, toLog);
Loading

0 comments on commit 10d79dd

Please sign in to comment.