UNPKG

@elastic.io/component-commons-library

Version:
578 lines (577 loc) 24.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PlatformApiLogicClient = void 0; /* eslint-disable no-restricted-syntax */ const PlatformApiRestClient_1 = require("./PlatformApiRestClient"); async function sleep(amount) { await new Promise((r) => setTimeout(r, amount)); } const DEFAULT_PARALLEL_PLATFORM_API_CALLS = process.env.PARALLEL_PLATFORM_API_CALLS || 20; const DEFAULT_OBJECTS_PER_PAGE = process.env.DEFAULT_OBJECTS_PER_PAGE || 20; class PlatformApiLogicClient extends PlatformApiRestClient_1.PlatformApiRestClient { /** * Fetch all flows for a given workspace * @param {string} options.workspaceId Id of the workspace to search * @returns {Promise<[]>} An array of flows */ async fetchAllFlowsForWorkspace(options = {}) { const { objectsPerPage = DEFAULT_OBJECTS_PER_PAGE, parallelCalls = DEFAULT_PARALLEL_PLATFORM_API_CALLS, workspaceId, } = options; const results = []; const objectCountResponse = await this.makeRequest({ url: `/flows?workspace_id=${workspaceId}&page[size]=1`, method: 'GET', }); const objectCount = objectCountResponse.meta.total; const numPages = Math.ceil(objectCount / Number(objectsPerPage)); const pageRange = Array.from({ length: numPages }, (_X, i) => i + 1); for (const pageNumber of pageRange) { const pageResult = await this.makeRequest({ url: `/flows?workspace_id=${workspaceId}&page[size]=${objectsPerPage}&page[number]=${pageNumber}`, method: 'GET', }); const objectArray = pageResult.data; results.push(...objectArray); } return results; } /** * Fetch all credentials for a given workspace * @param {string} options.workspaceId * @returns {Promise<[{{ * credentialId: string, * credentialName: string, * componentId: string, * }}]>} */ async fetchAllCredentialsForWorkspace(options = {}) { const { workspaceId, } = options; const credentialsResponse = await this.makeRequest({ method: 'GET', url: `/credentials?workspace_id=${workspaceId}`, }); return credentialsResponse.data.map((credential) => ({ credentialId: credential.id, credentialName: credential.attributes.name.trim(), componentId: credential.relationships.component.data.id, })); } /** * Fetch all credentials for a given workspace * @param {string} options.workspaceId * @returns {Promise<[{{ * secretId: string, * secretName: string, * componentIds: string[], * }}]>} */ async fetchAllSecretsForWorkspace(options = {}) { const { workspaceId } = options; if (!workspaceId) throw new Error('workspaceId not provided, can\'t fetch secrets'); const secrets = await this.makeRequest({ method: 'GET', url: `/workspaces/${workspaceId}/secrets` }); const resp = []; // eslint-disable-next-line no-restricted-syntax for (const secret of secrets.data) { const secretId = secret.id; const secretName = secret.attributes.name.trim(); let componentIds = []; try { if (secret.relationships.component) componentIds.push(secret.relationships.component.data.id); if (secret.relationships.auth_client) { const clientId = secret.relationships.auth_client.data.id; const clientResponse = await this.makeRequest({ method: 'GET', url: `/auth-clients/${clientId}` }); componentIds = clientResponse.data.relationships.components.data.map((x) => x.id); } } catch (e) { this.emitter.logger.info(`Can't find related to secret component - ${e.message}`); } resp.push({ secretId, secretName, componentIds }); } return resp; } /** * Fetch secret by id for a given workspace * @param {string} options.secretId * @returns {Promise<{ * id: string, * type: string, * links: object, * attributes: object, * relationships: object, * }>} */ async fetchSecretById(options = {}) { const { workspaceId = process.env.ELASTICIO_WORKSPACE_ID, secretId } = options; if (!secretId) throw new Error('secretId not provided, can\'t fetch secret'); const secret = await this.makeRequest({ method: 'GET', url: `/workspaces/${workspaceId}/secrets/${secretId}` }); return secret.data; } /** * Refresh token by secret id for a given workspace * @param {string} options.secretId * @returns {Promise<{ * id: string, * type: string, * links: object, * attributes: object, * relationships: object, * }>} */ async refreshTokenBySecretId(options = {}) { const { workspaceId = process.env.ELASTICIO_WORKSPACE_ID, secretId } = options; if (!secretId) throw new Error('secretId not provided, can\'t fetch secret'); const secret = await this.makeRequest({ method: 'POST', url: `/workspaces/${workspaceId}/secrets/${secretId}/refresh` }); return secret.data; } /** * Fetch All Components Accessible From a Given Workspace * @param {string} options.contractId Contract ID * @returns {Promise<[{{ * componentId: string, * componentName: string, * componentDevTeam: string * }}]>} */ async fetchComponentsAccessibleFromContract(options = {}) { const { contractId, } = options; const componentsResponse = await this.makeRequest({ method: 'GET', url: `/components?contract_id=${contractId}`, }); return componentsResponse.data.map((component) => ({ componentId: component.id, componentName: component.attributes.name, componentDevTeam: component.attributes.team_name, })); } /* eslint-disable-next-line class-methods-use-this */ splitParallelization(maxParallelization, splitFactor) { const realSplitFactor = Math.max(Math.floor(maxParallelization / splitFactor), 1); const parallelizationPerTask = Math.max(Math.floor(maxParallelization / realSplitFactor), 1); return { realSplitFactor, parallelizationPerTask, }; } /** * Fetches a list of flows * @param.workspaceId {string} Optional Workspace ID to limit results * @returns {Promise<[{{ * flowDetails: object, * flowId: string, * flowName: string, * workspaceId: string, * workspaceName: string, * contractId: string, * contractName: string, * contractDetails: object, * workspaceDetails: object * }}]>} */ async fetchFlowList(options = {}) { const { parallelCalls = DEFAULT_PARALLEL_PLATFORM_API_CALLS, workspaceId, } = options; const { realSplitFactor, parallelizationPerTask, } = this.splitParallelization(parallelCalls, 2); let flows; const workspaces = await this.fetchWorkspaceList({}); if (!workspaceId) { const nonFlatFlows = []; for (const workspace of workspaces) { const flowsForWorkspace = await this.fetchAllFlowsForWorkspace({ parallelCalls: parallelizationPerTask, workspaceId: workspace.workspaceId, }); nonFlatFlows.push(flowsForWorkspace); } flows = nonFlatFlows.flat(); } else { flows = await this.fetchAllFlowsForWorkspace({ parallelCalls, workspaceId, }); } return flows.map((flow) => { const matchingWorkspaces = workspaces .filter((workspace) => workspace.workspaceId === flow.relationships.workspace.data.id); if (matchingWorkspaces.length !== 1) { throw new Error('Failed to find matching workspace'); } const matchingWorkspace = matchingWorkspaces[0]; return { flowDetails: flow, flowId: flow.id, flowName: flow.attributes.name, ...matchingWorkspace, }; }); } /** * Fetch a list of all workspaces across all contracts for a user * @param options * @returns {Promise<[{{ * workspaceId: string, * workspaceName: string, * contractId: string, * contractName: string, * contractDetails: object, * workspaceDetails: object * }}]>} */ async fetchWorkspaceList(options = {}) { if (!this.workspaceList) { const { objectsPerPage = DEFAULT_OBJECTS_PER_PAGE, parallelCalls = DEFAULT_PARALLEL_PLATFORM_API_CALLS, } = options; // See the following issues for context // https://github.com/elasticio/elasticio/issues/4238 // https://github.com/elasticio/elasticio/issues/4236 // https://github.com/elasticio/elasticio/issues/4242 if (this.usingTaskUser) { const workspaceRequest = await this.makeRequest({ method: 'GET', url: `/workspaces/${process.env.ELASTICIO_WORKSPACE_ID}`, }); const workspace = workspaceRequest.data; const contractId = workspace.relationships.contract.data.id; const contractRequest = await this.makeRequest({ method: 'GET', url: `/contracts/${contractId}`, }); const contract = contractRequest.data; this.workspaceList = [{ contractId, workspaceId: workspace.id, workspaceName: workspace.attributes.name, contractName: contract.attributes.name, contractDetails: contract, workspaceDetails: workspace, }]; return this.workspaceList; } const contractsRequest = await this.makeRequest({ method: 'GET', url: '/contracts?page[size]=1', }); const contractsCount = contractsRequest.meta.total; const contractsPageRange = Array.from({ length: contractsCount }, (_X, i) => i + 1); const nonFlatContracts = []; for (const pageNumber of contractsPageRange) { const multipleContractsRequest = await this.makeRequest({ method: 'GET', url: `/contracts?page[size]=${objectsPerPage}&page[number]=${pageNumber}`, }); nonFlatContracts.push(multipleContractsRequest.data); } const contracts = nonFlatContracts.flat(); const contractsDictionary = contracts.reduce((soFar, contract) => { /* eslint-disable-next-line no-param-reassign */ soFar[contract.id] = contract; return soFar; }, {}); const nonFlatWorkspaces = []; for (const contract of contracts) { const currentContractId = contract.id; const workspacesCountResponse = await this.makeRequest({ url: `workspaces?contract_id=${currentContractId}&page[size]=1`, method: 'GET', }); const objectCount = workspacesCountResponse.meta.total; const numPages = Math.ceil(objectCount / Number(objectsPerPage)); const pageRange = Array.from({ length: numPages }, (_X, i) => i + 1); for (const pageNumber of pageRange) { const queries = [ `contract_id=${contract.id}`, `page[size]=${objectsPerPage}`, `page[number]=${pageNumber}`, ]; const workspaceRequest = await this.makeRequest({ method: 'GET', url: `/workspaces?${queries.join('&')}`, }); nonFlatWorkspaces.push(workspaceRequest.data); } } const workspaces = nonFlatWorkspaces.flat(); this.workspaceList = workspaces.map((workspace) => { const contractId = workspace.relationships.contract.data.id; return { contractId, workspaceId: workspace.id, workspaceName: workspace.attributes.name, contractName: contractsDictionary[contractId].attributes.name, contractDetails: contractsDictionary[contractId], workspaceDetails: workspace, }; }); } return this.workspaceList; } /** * Given a set of unique criteria, find the workspace that matches * @param {{ * value: {} * }} workspaceUniqueCriteria * @returns {Promise<{{ * workspaceId: string, * workspaceName: string, * contractId: string, * contractName: string, * contractDetails: object, * workspaceDetails: object * }}>} */ async fetchWorkspaceId(workspaceUniqueCriteria) { const workspaces = await this.fetchWorkspaceList({}); const matchingWorkspaces = workspaces.filter((workspace) => Object .keys(workspaceUniqueCriteria.value) .every((key) => (key.includes('flow') ? true : workspaceUniqueCriteria.value[key] === workspace[key]))); if (matchingWorkspaces.length !== 1) { this.emitter.logger.trace('Found %d workspaces for criteria: %j, throwing error', matchingWorkspaces.length, workspaceUniqueCriteria); throw new Error(`Found ${matchingWorkspaces.length} workspaces for criteria: ${JSON.stringify(workspaceUniqueCriteria)}`); } return matchingWorkspaces[0]; } /** * Given a flow, remove the properties of the flow that are regularly changed * by the system such as last executed time * @param {{}} flow * @param {boolean} Should keep data samples in returned object * @returns {{}} A copy of the flow with these properties removed */ /* eslint-disable-next-line class-methods-use-this */ removeNonWritableProperties(flow, includeDataSamples) { const flowLevelPropertiesToSkip = [ 'created_at', 'current_status', 'last_stop_time', 'last_modified', 'last_start_time', 'status', 'updated_at', ]; const nodeLevelPropertiesToSkip = [ 'dynamic_metadata', 'dynamic_select_model', ]; if (!includeDataSamples) { nodeLevelPropertiesToSkip.push('selected_data_samples'); } const toReturn = JSON.parse(JSON.stringify(flow)); flowLevelPropertiesToSkip.forEach((flowLevelPropertyToSkip) => { delete toReturn[flowLevelPropertyToSkip]; }); toReturn.graph.nodes.forEach((node) => { nodeLevelPropertiesToSkip.forEach((nodeLevelPropertyToSkip) => { /* eslint-disable-next-line no-param-reassign */ delete node[nodeLevelPropertyToSkip]; }); }); return toReturn; } /** * Given a set of unique criteria for a flow, find the corresponding flow * TODO: Below implementation is correct but can be made more efficient for some cases * @param {{ * value: {} * }} Criteria which uniquely describes a flow * @returns {Promise<null|{{}}>} Returns null if there are no matches. * Otherwise returns the content of the flow. */ async fetchFlowId(flowUniqueCriteria) { const flows = await this.fetchFlowList({}); const matchingFlows = flows.filter((flow) => Object .keys(flowUniqueCriteria.value) .every((key) => flowUniqueCriteria.value[key] === flow[key])); if (matchingFlows.length === 0) { return null; } if (matchingFlows.length > 1) { this.emitter.logger.trace('Found more than 1 object for criteria: %j, throwing error', flowUniqueCriteria); throw new Error(`Found more than 1 object for criteria: ${JSON.stringify(flowUniqueCriteria)}`); } return matchingFlows[0].flowDetails; } async fetchFlowById(id) { const flow = await this.makeRequest({ method: 'GET', url: `/flows/${id}`, }); return flow.data; } // much faster and less load to API than fetchFlowId async fetchFlowByNameAndWorkspaceId(flowName, workspaceId) { const flowsForWS = await this.fetchAllFlowsForWorkspace({ workspaceId }); const matchingFlows = flowsForWS.filter((wsFlow) => wsFlow.attributes.name === flowName); if (matchingFlows.length !== 1) { throw new Error(`Found ${matchingFlows.length} matching flow instead of 1`); } return matchingFlows[0]; } /** * Given a flow, change the flow to a given state (running, stopped, etc) * and wait for that change to take effect * @param {{ * action: string, * desiredStatus: string, * flowId: string * }} - Info for the request * @returns {Promise<>} */ /* eslint-disable no-await-in-loop */ async changeFlowState(options = {}) { const { timeout = 90000, pollInterval = 1000, action, desiredStatus, flowId, } = options; const timeoutTime = Date.now() + timeout; // Make sure flow is not changing states // eslint-disable-next-line no-constant-condition while (true) { if (Date.now() > timeoutTime) { throw new Error(`Timeout in waiting for flow ${flowId} to ${action}`); } const flow = await this.makeRequest({ method: 'GET', url: `/flows/${flowId}`, }); if (flow.data.attributes.current_status === flow.data.attributes.status) { break; } await sleep(pollInterval); } await this.makeRequest({ method: 'POST', url: `/flows/${flowId}/${action}`, }); // eslint-disable-next-line no-constant-condition while (true) { if (Date.now() > timeoutTime) { throw new Error(`Timeout in waiting for flow ${flowId} to ${action}`); } const flow = await this.makeRequest({ method: 'GET', url: `/flows/${flowId}`, }); if (flow.data.attributes.current_status === desiredStatus) { break; } await sleep(pollInterval); } } /* eslint-enable no-await-in-loop */ async startFlow(flowId, options = {}) { return this.changeFlowState({ ...options, flowId, action: 'start', desiredStatus: 'active', }); } async stopFlow(flowId, options = {}) { return this.changeFlowState({ ...options, flowId, action: 'stop', desiredStatus: 'inactive', }); } async hydrateFlow(options = {}) { const { flow, includeDataSamples, removeNonWritableProperties, parallelCalls = DEFAULT_PARALLEL_PLATFORM_API_CALLS, } = options; // Enrich all data samples if (includeDataSamples) { const sampleIds = flow.attributes.graph.nodes .filter((node) => node.selected_data_samples) .map((node) => node.selected_data_samples) .flat(); const samples = []; for (const sampleId of sampleIds) { let sampleRequest; try { sampleRequest = await this.makeRequest({ method: 'GET', url: `/data-samples/${sampleId}`, }); } catch (e) { throw new Error(`Can't extract data sample with id: ${sampleId}. Error: ${e.message}`); } const sample = sampleRequest.data.attributes; samples.push({ sample, sampleId, }); } const sampleDictionary = samples.reduce((soFar, sample) => { /* eslint-disable-next-line no-param-reassign */ soFar[sample.sampleId] = sample.sample; return soFar; }, {}); flow.attributes.graph.nodes .filter((node) => node.selected_data_samples) .forEach((node) => { /* eslint-disable-next-line no-param-reassign */ node.selected_data_samples = node.selected_data_samples .map((sampleId) => sampleDictionary[sampleId]); }); } else { flow.attributes.graph.nodes .filter((node) => node.selected_data_samples) .forEach((node) => { /* eslint-disable-next-line no-param-reassign */ delete node.selected_data_samples; }); } // Remove all attributes that a regularly re-written by the platform if (removeNonWritableProperties) { flow.attributes = this.removeNonWritableProperties(flow.attributes, includeDataSamples); } // Enrich credential names const credentialsList = await this.fetchAllCredentialsForWorkspace({ workspaceId: flow.relationships.workspace.data.id, }); flow.attributes.graph.nodes.forEach((node) => { if (node.credentials_id) { const matchingCredentials = credentialsList .filter((credential) => credential.credentialId === node.credentials_id); if (matchingCredentials.length !== 1) { throw new Error('Expected a single matching credential'); } /* eslint-disable-next-line no-param-reassign */ node.credentials_id = { credentialId: matchingCredentials[0].credentialId, credentialName: matchingCredentials[0].credentialName, }; } }); const secretsList = await this.fetchAllSecretsForWorkspace({ workspaceId: flow.relationships.workspace.data.id, }); flow.attributes.graph.nodes.forEach((node) => { if (node.secret_id) { const matchingSecrets = secretsList.filter((secret) => secret.secretId === node.secret_id); if (matchingSecrets.length !== 1) throw new Error('Expected a single matching secret'); /* eslint-disable-next-line no-param-reassign */ node.secret_id = { secretId: matchingSecrets[0].secretId, secretName: matchingSecrets[0].secretName, }; } }); // Enrich command and component Id fields flow.attributes.graph.nodes.forEach((node) => { const commandParts = node.command.split(/[/:@]/); /* eslint-disable-next-line no-param-reassign */ node.command = { actionOrTrigger: commandParts[2], componentVersion: commandParts[3], }; /* eslint-disable-next-line no-param-reassign */ node.component_id = { componentId: node.component_id, componentDevTeam: commandParts[0], componentName: commandParts[1], }; }); return flow; } } exports.PlatformApiLogicClient = PlatformApiLogicClient;