UNPKG

@unito/integration-debugger

Version:

The Unito Integration Debugger

455 lines (454 loc) 20.2 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.ReadOperations = exports.Operation = void 0; exports.buildStepUniqueId = buildStepUniqueId; exports.buildSchemaUniqueId = buildSchemaUniqueId; exports.create = create; const integration_api_1 = require("@unito/integration-api"); const Validator = __importStar(require("./validator")); const headers_1 = __importDefault(require("../resources/headers")); var Operation; (function (Operation) { Operation["GetCollection"] = "getCollection"; Operation["GetItem"] = "getItem"; Operation["GetBlob"] = "getBlob"; Operation["CreateItem"] = "createItem"; Operation["UpdateItem"] = "updateItem"; Operation["DeleteItem"] = "deleteItem"; Operation["SubscribeWebhook"] = "subscribeWebhook"; Operation["UnsubscribeWebhook"] = "unsubscribeWebhook"; Operation["GetCredentialAccount"] = "getCredentialAccount"; })(Operation || (exports.Operation = Operation = {})); exports.ReadOperations = Object.freeze([ Operation.GetCollection, Operation.GetItem, Operation.GetCredentialAccount, Operation.GetBlob, ]); var ValidationType; (function (ValidationType) { ValidationType["SHAPE"] = "shape"; ValidationType["ITEM"] = "item"; })(ValidationType || (ValidationType = {})); class IntegrationCallFailed extends Error { payloadOut; code; constructor(message, code, payloadOut) { super(message); this.payloadOut = payloadOut; this.code = code; } } function buildStepUniqueId(stepOperation, stepPath) { return [stepOperation, stepPath].join('.'); } function buildSchemaUniqueId(stepPath, relationName, fieldName) { return [stepPath, relationName, fieldName] .filter(v => v) .join('/') .replaceAll('//', '/'); } async function create(crawlerOptions) { const validator = await Validator.create(); return new Instance(crawlerOptions, validator); } class Instance { proxyCall; validator; timeout; nextSteps; visitedRelationSchemas; visitedFieldSchemas; constructor(crawlerOptions, validator) { this.proxyCall = crawlerOptions.proxyCall; this.validator = validator; this.timeout = crawlerOptions.timeout; this.visitedRelationSchemas = new Map(); this.visitedFieldSchemas = new Map(); this.nextSteps = []; } async next() { // Fetch the next step to execute. const stepToExecute = this.nextSteps.shift(); if (!stepToExecute) { return undefined; } // Set the operation deadline header to 20 seconds from now, if not already set. // Do nothing if no headers is present, happens only during unit tests. if (this.timeout && stepToExecute.headersIn) { stepToExecute.headersIn[headers_1.default.OPERATION_DEADLINE] ??= (Math.floor(Date.now() / 1000) + this.timeout).toString(); } // Execute the step. const currentStep = await this.execute(stepToExecute); if (currentStep.errors.length > 0) { return { step: currentStep, discoveredSteps: [] }; } return { step: currentStep, discoveredSteps: this.discoverSteps(currentStep) }; } discoverSteps(currentStep) { const payloadOut = currentStep.payloadOut; const discoveredSteps = []; if (currentStep.operation === Operation.CreateItem) { const itemSummary = payloadOut; // Visit the created item. discoveredSteps.push({ operation: Operation.GetItem, path: itemSummary.path, requestSchema: undefined, schemaPath: currentStep.schemaPath, parentOperation: currentStep.operation, parentPath: currentStep.path, errors: [], warnings: [], }); } if (payloadOut.relations) { const item = payloadOut; for (const relation of item.relations) { const schemaPath = buildSchemaUniqueId(currentStep.path, relation.name); this.visitedRelationSchemas.set(schemaPath, relation.schema); this.visitedFieldSchemas.set(schemaPath, relation.schema.fields ?? []); // Visit a relation. discoveredSteps.push({ operation: Operation.GetCollection, path: relation.path, requestSchema: relation.requestSchema, schemaPath, parentOperation: currentStep.operation, parentPath: currentStep.path, errors: [], warnings: [], }); for (const field of relation.schema.fields ?? []) { // Visit a referenced collection. if (field.type === integration_api_1.FieldValueTypes.REFERENCE) { const referencePath = field.reference.path; const referenceFields = field.reference.schema === '__self' ? relation.schema.fields : field.reference.schema.fields; const schemaPath = buildSchemaUniqueId(currentStep.path, relation.name, field.name); this.visitedFieldSchemas.set(schemaPath, referenceFields ?? []); discoveredSteps.push({ operation: Operation.GetCollection, path: referencePath, requestSchema: undefined, schemaPath, parentOperation: currentStep.operation, parentPath: currentStep.path, errors: [], warnings: [], }); } } } const fields = this.visitedFieldSchemas.get(currentStep.schemaPath ?? '') ?? []; for (const field of fields) { // Visit a reference / an array of references. // TODO: find references in nested objects. if (field.type === integration_api_1.FieldValueTypes.REFERENCE && item.fields[field.name]) { const schemaPath = buildSchemaUniqueId(currentStep.schemaPath, field.name); const references = [item.fields[field.name]].flat(1); for (const reference of references) { discoveredSteps.push({ operation: Operation.GetItem, path: reference.path, requestSchema: undefined, schemaPath, parentOperation: currentStep.operation, parentPath: currentStep.path, errors: [], warnings: [], }); } } if (field.type === integration_api_1.FieldValueTypes.BLOB && item.fields[field.name]) { const blob = item.fields[field.name]; discoveredSteps.push({ operation: Operation.GetBlob, path: blob.path, requestSchema: undefined, schemaPath: '', parentOperation: currentStep.operation, parentPath: currentStep.path, errors: [], warnings: [], }); } } } else if (payloadOut.data) { const collection = payloadOut; // Visit an item. for (const entry of collection.data) { discoveredSteps.push({ operation: Operation.GetItem, path: entry.path, requestSchema: entry.requestSchema, schemaPath: currentStep.schemaPath, parentOperation: currentStep.operation, parentPath: currentStep.path, errors: [], warnings: [], }); } // Visit the next page. if (collection.info.nextPage) { discoveredSteps.push({ operation: Operation.GetCollection, path: collection.info.nextPage, requestSchema: undefined, schemaPath: currentStep.schemaPath, parentOperation: currentStep.operation, parentPath: currentStep.path, errors: [], warnings: [], }); } } return discoveredSteps; } prepend(step) { this.nextSteps.unshift(this.copyStep(step)); } append(step) { this.nextSteps.push(this.copyStep(step)); } remove(step) { const uniqueId = buildStepUniqueId(step.operation, step.path); let index; do { index = this.nextSteps.findIndex(s => buildStepUniqueId(s.operation, s.path) === uniqueId); if (index !== -1) { this.nextSteps.splice(index, 1); } } while (index !== -1); } remaining() { return this.nextSteps; } getRelationSchema(schemaPath) { return this.visitedRelationSchemas.get(schemaPath); } getFieldSchemas(schemaPath) { return this.visitedFieldSchemas.get(schemaPath); } async execute(incomingStep) { const step = JSON.parse(JSON.stringify(incomingStep)); try { const response = await this.proxyCall(step); step.headersOut = response.headers; step.payloadOut = response.payload; this.validateResponse(response, step); } catch (err) { if (err instanceof IntegrationCallFailed) { step.payloadOut = err.payloadOut; step.errors.push({ message: `${err.message} (${err.code})`, detailedMessage: undefined, keyword: 'call', instancePath: '/call', schemaPath: '', }); } else if (err instanceof Validator.ValidationFailed) { step.payloadOut = err.payload; step.errors.push(...(err.details ?? [])); } else { step.errors.push({ message: err.message ?? err, detailedMessage: undefined, keyword: 'unknown', instancePath: '/unknown', schemaPath: '', }); } } return step; } validateResponse(response, step) { if (response.status >= 200 && response.status <= 299) { const fields = this.visitedFieldSchemas.get(step.schemaPath ?? ''); if (step.operation === Operation.GetCollection) { this.validatePayload(step, response.payload, ValidationType.SHAPE, {}); for (const itemSummary of response.payload.data) { this.validatePayload(step, itemSummary, ValidationType.ITEM, { schemaPath: step.schemaPath, fields, partial: true, requiredFields: [], }); } this.validateStatus(step, response, integration_api_1.StatusCodes.OK, `A successful '${Operation.GetCollection}' operation must return a 200 OK`); } else if (step.operation === Operation.GetItem) { this.validatePayload(step, response.payload, ValidationType.SHAPE, {}); this.validatePayload(step, response.payload, ValidationType.ITEM, { schemaPath: step.schemaPath, fields, partial: false, requiredFields: fields?.map(field => field.name) ?? [], }); this.validateStatus(step, response, integration_api_1.StatusCodes.OK, `A successful '${Operation.GetItem}' operation must return a 200 OK`); } else if (step.operation === Operation.CreateItem) { this.validatePayload(step, response.payload, ValidationType.SHAPE, {}); this.validatePayload(step, response.payload, ValidationType.ITEM, { schemaPath: step.schemaPath, fields, partial: true, requiredFields: [], }); this.validateStatus(step, response, integration_api_1.StatusCodes.CREATED, `A successful '${Operation.CreateItem}' operation must return a 201 Created`); } else if (step.operation === Operation.UpdateItem) { this.validatePayload(step, response.payload, ValidationType.SHAPE, {}); this.validatePayload(step, response.payload, ValidationType.ITEM, { schemaPath: step.schemaPath, fields, partial: false, requiredFields: fields?.map(field => field.name) ?? [], }); this.validateStatus(step, response, integration_api_1.StatusCodes.OK, `A successful '${Operation.UpdateItem}' operation must return a 200 OK`); } else if (step.operation === Operation.DeleteItem) { this.validateStatus(step, response, integration_api_1.StatusCodes.NO_CONTENT, `A successful '${Operation.DeleteItem}' operation must return a 204 No Content`); } else if (step.operation === Operation.SubscribeWebhook) { this.validateStatus(step, response, integration_api_1.StatusCodes.NO_CONTENT, `A successful '${Operation.SubscribeWebhook}' operation must return a 204 No Content`); } else if (step.operation === Operation.UnsubscribeWebhook) { this.validateStatus(step, response, integration_api_1.StatusCodes.NO_CONTENT, `A successful '${Operation.UnsubscribeWebhook}' operation must return a 204 No Content`); } else if (step.operation === Operation.GetCredentialAccount) { this.validatePayload(step, response.payload, ValidationType.SHAPE, {}); this.validateStatus(step, response, integration_api_1.StatusCodes.OK, `A successful '${Operation.GetCredentialAccount}' operation must return a 200 OK`); } else if (step.operation === Operation.GetBlob) { // this.validatePayload(step, response.payload, ValidationType.SHAPE, {}); this.validateStatus(step, response, integration_api_1.StatusCodes.OK, `A successful '${Operation.GetBlob}' operation must return a 200 OK`); } else { this.validatePayload(step, response.payload, ValidationType.SHAPE, {}); this.validatePayload(step, response.payload, ValidationType.ITEM, { schemaPath: step.schemaPath, fields, partial: false, requiredFields: fields?.map(field => field.name) ?? [], }); } } else if (Object.values(integration_api_1.StatusCodes).includes(response.status)) { // Validate handled and known status code const validationResult = this.validator.validateShape(response.payload, 'error'); if (validationResult.errors.length) { throw new Validator.ValidationFailed(response.payload, validationResult.errors); } throw new IntegrationCallFailed('The integration returned an error', response.status, response.payload); } else { throw new IntegrationCallFailed('The integration returned an unsupported status code', response.status, response.payload); } } validatePayload(step, payload, validationType, validateItemOptions) { let validationResult; if (validationType === ValidationType.SHAPE) { const schema = this.getSchemaByOperation(step.operation); validationResult = this.validator.validateShape(payload, schema); } else if (validationType === ValidationType.ITEM) { validationResult = this.validator.validateItem(payload, validateItemOptions); } else { throw new Error(`Hey fellow developer, the ValidationType ${validationType} must be handled here.`); } if (validationResult.warnings.length > 0) { step.warnings = validationResult.warnings; } if (validationResult.errors.length > 0) { throw new Validator.ValidationFailed(payload, validationResult.errors); } } validateStatus(step, response, expected, errorMessage) { if (response.status !== expected) { const details = { keyword: 'call', message: 'The integration returned an unsupported status code', detailedMessage: errorMessage, instancePath: step.path, schemaPath: step.schemaPath ?? '', }; throw new Validator.ValidationFailed(response.payload, [details]); } } getSchemaByOperation(operation) { switch (operation) { case Operation.CreateItem: return 'itemSummary'; case Operation.GetCollection: return 'collection'; case Operation.GetCredentialAccount: return 'credentialAccount'; default: return 'item'; } } copyStep(step) { const stepCopy = { operation: step.operation, path: step.path, requestSchema: step.requestSchema, schemaPath: step.schemaPath, parentOperation: step.parentOperation, parentPath: step.parentPath, errors: [], warnings: [], }; if (step.payloadIn) { stepCopy.payloadIn = step.payloadIn; } if (step.headersIn) { stepCopy.headersIn = step.headersIn; } if (step.context) { stepCopy.context = { ...step.context }; } return stepCopy; } } exports.Instance = Instance;