UNPKG

@unito/integration-debugger

Version:

The Unito Integration Debugger

265 lines (264 loc) 11.4 kB
"use strict"; 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;