@unito/integration-debugger
Version:
The Unito Integration Debugger
265 lines (264 loc) • 11.4 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Instance = exports.ValidationFailed = void 0;
exports.create = create;
const integration_api_1 = require("@unito/integration-api");
const ajv_1 = __importDefault(require("ajv"));
const ajv_formats_1 = __importDefault(require("ajv-formats"));
const fs = __importStar(require("fs"));
const path_1 = __importDefault(require("path"));
/**
* This error is raised when a payload is not compliant with the Integration API specification.
*/
class ValidationFailed extends Error {
payload;
details;
constructor(payload, details) {
super('The payload is not compliant with the Integration API specification');
this.payload = payload;
this.details = details;
}
}
exports.ValidationFailed = ValidationFailed;
async function create() {
const directory = `${path_1.default.dirname(require.resolve('@unito/integration-api/package.json'))}/dist/schemas`;
const files = await fs.promises.readdir(directory);
const jsonFiles = files.filter(filename => filename.endsWith('.json'));
const schemas = await Promise.all(jsonFiles.map(filename => fs.promises.readFile(`${directory}/${filename}`, 'utf8').then(content => JSON.parse(content))));
return new Instance(schemas);
}
/**
* The Integration API specification validator.
*
* Part of the specification define types that payloads must comply to.
* Those types are available in our @unito/integration-api NPM package.
*/
class Instance {
spec;
schemas;
constructor(schemas) {
this.spec = new ajv_1.default({ schemas, allErrors: true });
this.schemas = schemas;
// Why "unito" keyword.
//
// AJV supports defining [user-defined keywords](https://ajv.js.org/keywords.html#define-keyword-with-validate-function)
// in our JSON Schema files that we can then apply custom validation to it.
//
// In our case, we want to perform additional unito-specific validation.
this.spec.addKeyword({
keyword: 'unito',
validate: (_schema, _relation) => {
const errors = this.validateForUnito();
return errors.length <= 0;
},
});
// Add standard formats
(0, ajv_formats_1.default)(this.spec);
// Add custom formats
this.spec.addFormat(integration_api_1.FieldValueTypes.DATE_RANGE, (value) => this.validateRange('date', value));
this.spec.addFormat(integration_api_1.FieldValueTypes.DATETIME_RANGE, (value) => this.validateRange('date-time', value));
}
/**
* Validate the shape of the payload.
*
* For example, if the payload is supposed to be an "Item",
* we validate that it complies to our "Item" JSON Schema type.
*/
validateShape(payload, schema) {
const validate = this.spec.getSchema(`https://unito.io/integration_api/${schema}.schema.json`);
if (!validate) {
throw new Error(`Schema ${schema} not found'`);
}
return this.performValidation(validate, payload, `Does not respect the specification for the schema '${schema}'.`);
}
/**
* Validate the content of an Item.
*
* The content of an Item is specific to an integration.
*
* For example, if the item is supposed to be a "task",
* which is a specific "item" of some integration,
* we validate that it complies to the provided "task" schema.
*/
validateItem(item, options) {
// Happens for the root.
if (options.schemaPath === undefined || options.fields === undefined) {
return { errors: [], warnings: [] };
}
const itemFieldsSchema = {
type: 'object',
additionalProperties: true,
required: Array.from(new Set(options.requiredFields ?? options.fields?.map(field => field.name) ?? [])),
properties: this.toJsonFields(options.fields ?? [], options.partial ?? false),
};
const validate = this.spec.compile(itemFieldsSchema);
return this.performValidation(validate, item.fields ?? {}, options.detailedMessage ?? `Does not respect the schema '${options.schemaPath}'.`);
}
toJsonFields(fields, partial) {
const properties = {};
for (const field of fields) {
const jsonField = {
type: 'string',
nullable: field.nullable ?? true,
};
switch (field.type) {
case integration_api_1.FieldValueTypes.RICH_TEXT_MARKDOWN:
case integration_api_1.FieldValueTypes.RICH_TEXT_HTML:
jsonField.type = 'string';
break;
case integration_api_1.FieldValueTypes.BOOLEAN:
jsonField.type = 'boolean';
break;
case integration_api_1.FieldValueTypes.DATE:
jsonField.type = 'string';
jsonField.format = 'date';
break;
case integration_api_1.FieldValueTypes.DATETIME:
jsonField.type = 'string';
jsonField.format = 'date-time';
break;
// Custom type that we added to the Validator and the Generator.
case integration_api_1.FieldValueTypes.DATE_RANGE:
jsonField.type = 'string';
jsonField.format = integration_api_1.FieldValueTypes.DATE_RANGE;
break;
// Custom type that we added to the Validator and the Generator.
case integration_api_1.FieldValueTypes.DATETIME_RANGE:
jsonField.type = 'string';
jsonField.format = integration_api_1.FieldValueTypes.DATETIME_RANGE;
break;
case integration_api_1.FieldValueTypes.EMAIL:
jsonField.type = 'string';
jsonField.format = 'email';
break;
case integration_api_1.FieldValueTypes.URL:
jsonField.type = 'string';
jsonField.format = 'uri-reference';
break;
case integration_api_1.FieldValueTypes.NUMBER:
jsonField.type = 'number';
break;
case integration_api_1.FieldValueTypes.INTEGER:
jsonField.type = 'integer';
break;
case integration_api_1.FieldValueTypes.DURATION:
jsonField.type = 'integer';
jsonField.minimum = 0;
break;
case integration_api_1.FieldValueTypes.OBJECT:
jsonField.type = 'object';
jsonField.additionalProperties = false;
jsonField.properties = this.toJsonFields(field.fields ?? [], partial);
jsonField.required = partial ? [] : (field.fields ?? []).map(field => field.name);
break;
case integration_api_1.FieldValueTypes.REFERENCE:
Object.assign(jsonField, this.spec.getSchema('https://unito.io/integration_api/itemSummary.schema.json')?.schema ?? {});
delete jsonField.$id;
break;
case integration_api_1.FieldValueTypes.BLOB:
Object.assign(jsonField, this.spec.getSchema('https://unito.io/integration_api/blobSummary.schema.json')?.schema ?? {});
delete jsonField.$id;
break;
}
if (field.isArray) {
properties[field.name] = {
type: field.nullable ? ['array', 'null'] : 'array',
items: jsonField,
};
}
else {
properties[field.name] = jsonField;
}
}
return properties;
}
/**
* Populate the validation result with errors, warnings, a message and the validity of the payload
*/
performValidation(validate, payload, detailedMessage) {
validate(payload);
const errors = [];
const warnings = [];
for (const errorDetail of validate.errors ?? []) {
const error = {
keyword: errorDetail.keyword,
detailedMessage,
message: errorDetail.message,
schemaPath: errorDetail.schemaPath,
instancePath: errorDetail.instancePath,
params: errorDetail.params,
};
if (errorDetail.params.isWarning) {
warnings.push(error);
}
else {
errors.push(error);
}
}
// Ajv does not populate `validate.errors` whenever the schema is valid,
// that's why we use the filtered array of errors to consider whether it's valid or not.
return { errors, warnings };
}
validateRange(format, value) {
const [start, end, ...rest] = value.split('/');
if ((!start && !end) || rest.length || !value.includes('/')) {
return false;
}
const schema = {
type: 'string',
format: format,
};
return (!start || this.spec.validate(schema, start)) && (!end || this.spec.validate(schema, end));
}
/**
* Validate Unito specific requirements of an item's fields.
*/
validateForUnito() {
const errors = [];
//
// Add errors & warnings to AJV validator.
//
const validator = this.spec.getKeyword('unito');
if (validator.validate) {
validator.validate.errors = errors;
}
return errors;
}
}
exports.Instance = Instance;
;