UNPKG

@salesforce/agents

Version:

Client side APIs for working with Salesforce agents

189 lines 7.6 kB
"use strict"; /* * Copyright (c) 2024, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MaybeMock = void 0; const node_path_1 = require("node:path"); const node_fs_1 = require("node:fs"); const promises_1 = require("node:fs/promises"); const core_1 = require("@salesforce/core"); const kit_1 = require("@salesforce/kit"); const nock_1 = __importDefault(require("nock")); /** * If the `SF_MOCK_DIR` environment variable is set, resolve to an absolute path * and ensure the directory exits, then return the path. * * NOTE: THIS SHOULD BE MOVED TO SOME OTHER LIBRARY LIKE `@salesforce/kit`. * * @returns the absolute path to an existing directory used for mocking behavior */ const getMockDir = () => { const mockDir = kit_1.env.getString('SF_MOCK_DIR'); if (mockDir) { let mockDirStat; try { mockDirStat = (0, node_fs_1.statSync)((0, node_path_1.resolve)(mockDir)); } catch (err) { throw core_1.SfError.create({ name: 'InvalidMockDir', message: `SF_MOCK_DIR [${mockDir}] not found`, cause: err, actions: [ "If you're trying to mock agent behavior you must create the mock directory and add expected mock files to it.", ], }); } if (!mockDirStat.isDirectory()) { throw core_1.SfError.create({ name: 'InvalidMockDir', message: `SF_MOCK_DIR [${mockDir}] is not a directory`, actions: [ "If you're trying to mock agent behavior you must create the mock directory and add expected mock files to it.", ], }); } return mockDir; } }; async function readJson(path) { return JSON.parse(await (0, promises_1.readFile)(path, 'utf-8')); } async function readPlainText(path) { return (0, promises_1.readFile)(path, 'utf-8'); } async function readDirectory(path) { const files = await (0, promises_1.readdir)(path); const promises = files.map((file) => { if (file.endsWith('.json')) { return readJson((0, node_path_1.join)(path, file)); } else { return readPlainText((0, node_path_1.join)(path, file)); } }); return (await Promise.all(promises)).filter((r) => !!r); } async function readResponses(mockDir, url, logger) { const mockResponseName = url.replace(/\//g, '_').replace(/:/g, '_').replace(/^_/, '').split('?')[0]; const mockResponsePath = (0, node_path_1.join)(mockDir, mockResponseName); // Try all possibilities for the mock response file const responses = (await Promise.all([ readJson(`${mockResponsePath}.json`) .then((r) => { logger.debug(`Found JSON mock file: ${mockResponsePath}.json`); return r; }) .catch(() => undefined), readPlainText(mockResponsePath) .then((r) => { logger.debug(`Found plain text mock file: ${mockResponsePath}`); return r; }) .catch(() => undefined), readDirectory(mockResponsePath) .then((r) => { logger.debug(`Found directory of mock files: ${mockResponsePath}`); return r; }) .catch(() => undefined), ])) .filter((r) => !!r) .flat(); if (responses.length === 0) { throw core_1.SfError.create({ name: 'MissingMockFile', message: `SF_MOCK_DIR [${mockDir}] must contain a spec file with name ${mockResponsePath} or ${mockResponsePath}.json`, }); } logger.debug(`Using responses: ${responses.map((r) => JSON.stringify(r)).join(', ')}`); return responses; } /** * A class to act as an in-between the library's request, and the orgs response * * if `SF_MOCK_DIR` is set it will read from the directory, resolving files as API responses with nock * * if it is NOT set, it will hit the endpoint and use real server responses */ class MaybeMock { connection; mockDir = getMockDir(); scopes = new Map(); logger; constructor(connection) { this.connection = connection; this.logger = core_1.Logger.childFromRoot(this.constructor.name); } /** * Will either use mocked responses, or the real server response, as the library/APIs become more feature complete, * there will be fewer mocks and more real responses * * @param {"GET" | "POST" | "DELETE"} method * @param {string} url * @param {nock.RequestBodyMatcher} body * @returns {Promise<T>} */ async request(method, url, body = {}, headers = {}) { if (this.mockDir) { this.logger.debug(`Mocking ${method} request to ${url} using ${this.mockDir}`); const responses = await readResponses(this.mockDir, url, this.logger); const baseUrl = this.connection.baseUrl(); const scope = this.scopes.get(baseUrl) ?? (0, nock_1.default)(baseUrl); // Look up status code to determine if it's successful or not // Be have to assert this is a number because AgentTester has a status that is non-numeric const getCode = (response) => typeof response === 'object' && 'status' in response && typeof response.status === 'number' ? response.status : 200; // This is a hack to work with SFAP endpoints url = url.replace('https://api.salesforce.com', ''); this.scopes.set(baseUrl, scope); switch (method) { case 'GET': for (const response of responses) { scope.get(url).reply(getCode(response), response); } break; case 'POST': for (const response of responses) { scope.post(url, body).reply(getCode(response), response); } break; case 'DELETE': for (const response of responses) { scope.delete(url).reply(getCode(response), response); } break; } } this.logger.debug(`Making ${method} request to ${url}`); switch (method) { case 'GET': return this.connection.requestGet(url, { retry: { maxRetries: 3 } }); case 'POST': if (!body) { throw core_1.SfError.create({ name: 'InvalidBody', message: 'POST requests must include a body', }); } return this.connection.requestPost(url, body, { retry: { maxRetries: 3 } }); case 'DELETE': // We use .request() rather than .requestDelete() so that we can pass in the headers return this.connection.request({ method: 'DELETE', url, headers, }, { retry: { maxRetries: 3 } }); } } } exports.MaybeMock = MaybeMock; //# sourceMappingURL=maybe-mock.js.map