UNPKG

@sprucelabs/mercury-client

Version:

The simple way to interact with the Spruce Experience Platform

351 lines (350 loc) • 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const mercury_event_emitter_1 = require("@sprucelabs/mercury-event-emitter"); const spruce_event_utils_1 = require("@sprucelabs/spruce-event-utils"); const just_clone_1 = __importDefault(require("just-clone")); const SpruceError_1 = __importDefault(require("../errors/SpruceError")); const MercurySocketIoClient_1 = require("./MercurySocketIoClient"); const MutableContractClient_1 = __importDefault(require("./MutableContractClient")); const statusChangePayloadSchema_1 = require("./statusChangePayloadSchema"); class InternalEmitter extends mercury_event_emitter_1.AbstractEventEmitter { doesHandleEvent(eventName) { try { spruce_event_utils_1.eventContractUtil.getSignatureByName(this.eventContract, eventName); return true; } catch { return false; } } validateEmitPayload(schema, actualPayload, eventName) { const payload = { ...actualPayload }; delete payload.source; return super.validateEmitPayload(schema, payload, eventName); } mixinOnlyUniqueSignatures(contract) { const fqens = Object.keys(contract.eventSignatures); for (const fqen of fqens) { if (!this.eventContract.eventSignatures[fqen]) { this.eventContract.eventSignatures[fqen] = contract.eventSignatures[fqen]; } } } overrideSignatures(contract) { const fqens = Object.keys(contract.eventSignatures); for (const fqen of fqens) { this.eventContract.eventSignatures[fqen] = contract.eventSignatures[fqen]; } } getContract() { return this.eventContract; } setContract(contract) { this.eventContract = contract; } } class MercuryTestClient extends MutableContractClient_1.default { get eventContract() { return MercuryTestClient.emitter.getContract(); } set eventContract(contract) { } static setShouldCheckPermissionsOnLocalEvents(should) { this.shouldCheckPermissionsOnLocalEvents = should; } static setNamespacesThatMustBeHandledLocally(namespaces) { this.namespacesThatHaveToBeHandledLocally = namespaces; } static getNamespacesThatMustBeHandledLocally() { return this.namespacesThatHaveToBeHandledLocally; } constructor(options) { const contract = options.eventContract; super({ ...options, eventContract: contract }); this._isConnected = false; this.isConnectedToApi = false; this.shouldHandleAuthenticateLocallyIfListenerSet = true; this.shouldWaitForDelayedConnectIfAuthing = true; if (!MercuryTestClient.emitter) { MercuryTestClient.getInternalEmitter(contract); } else if (contract) { MercuryTestClient.emitter.overrideSignatures(contract); } } /** @ts-ignore */ static getInternalEmitter(contract) { if (!MercuryTestClient.emitter) { MercuryTestClient.emitter = new InternalEmitter({ eventSignatures: {}, }); } const mixed = mixinConnectionEvents(contract); MercuryTestClient.emitter.mixinOnlyUniqueSignatures(mixed); /** @ts-ignore */ return MercuryTestClient.emitter; } async off(eventName, cb) { await MercuryTestClient.emitter?.off(eventName, cb); if (MercuryTestClient.emitter?.listenCount(eventName) === 0) { return super.off(eventName); } else { return 1; } } static mixinContract(contract) { MutableContractClient_1.default.mixinContract(contract); MercuryTestClient.emitter.mixinContract(contract); } mixinContract(contract) { MutableContractClient_1.default.mixinContract(contract); MercuryTestClient.emitter.mixinContract(contract); } doesHandleEvent(eventName) { return (super.doesHandleEvent(eventName) || MercuryTestClient.emitter?.doesHandleEvent(eventName)); } async on(...args) { //@ts-ignore return MercuryTestClient.emitter.on(...args); } async emit(...args) { const fqen = args[0]; try { if (this.shouldHandleEventLocally(fqen)) { return this.handleEventLocally(args); } else { if (MercuryTestClient.shouldRequireLocalListeners && fqen !== 'connection-status-change') { throw new SpruceError_1.default({ code: 'MUST_HANDLE_LOCALLY', fqen, friendlyMessage: `You need to listen to, fake a response to '${fqen}', or boot your skill. Try 'spruce create.listener', 'eventFaker.on('${fqen}')', or 'await this.bootSkill()'!`, }); } await this.connectIfNotConnected(fqen); //@ts-ignore const results = await super.emit(...args); const firstError = results.responses?.[0]?.errors?.[0]; if (firstError && firstError.options?.code === 'INVALID_EVENT_NAME') { firstError.message = `Event not found! Make sure you are booting your skill in your test with \`await this.bootSkill()\`. If you haven't, you'll need to create a listener with \`spruce create.listener\`.\n\nOriginal Error:\n\n${firstError.options.friendlyMessage}`; } return results; } } catch (err) { if (err.options?.code === 'INVALID_EVENT_NAME') { err.message = `${err.message} Double check it's spelled correctly (types are passing) and that you've run \`spruce create.event\` to create the event.`; } throw err; } } shouldHandleEventLocally(fqen) { const emitter = MercuryTestClient.emitter; if (!this.shouldHandleAuthenticateLocallyIfListenerSet && fqen === MercurySocketIoClient_1.authenticateFqen) { return false; } if (fqen === 'connection-status-change') { return true; } const { eventNamespace } = spruce_event_utils_1.eventNameUtil.split(fqen); if (eventNamespace && MercuryTestClient.namespacesThatHaveToBeHandledLocally.indexOf(eventNamespace) > -1) { return true; } return emitter.listenCount(fqen) > 0; } async handleEventLocally(args) { const emitter = MercuryTestClient.emitter; const fqen = args[0]; const payload = args[1]; if (!MercuryTestClient.emitter.doesHandleEvent(fqen)) { throw new SpruceError_1.default({ code: 'MUST_CREATE_EVENT', fqen, }); } if (MercuryTestClient.shouldRequireLocalListeners !== false && MercuryTestClient.emitter.listenCount(fqen) === 0) { if (fqen === 'connection-status-change') { return { responses: [], totalContracts: 0, totalErrors: 0, totalResponses: 0, }; } throw new SpruceError_1.default({ code: 'MUST_HANDLE_LOCALLY', fqen, }); } this.assertValidEmitTargetAndPayload(fqen, payload); let { argsWithSource } = this.buildSource(args); const contract = emitter.getContract(); const sig = spruce_event_utils_1.eventContractUtil.getSignatureByName(contract, fqen); const { eventNamespace } = spruce_event_utils_1.eventNameUtil.split(fqen); if (eventNamespace) { this.assertValidEventSignature(sig, fqen); } if (sig.emitPermissionContract && eventNamespace) { const doesHonor = await this.optionallyCheckPermissions(args, sig.emitPermissionContract.id, fqen); if (typeof doesHonor !== 'boolean') { return doesHonor; } if (!doesHonor) { return { totalContracts: 1, totalErrors: 1, totalResponses: 1, responses: [ { errors: [ new SpruceError_1.default({ code: 'UNAUTHORIZED_ACCESS', fqen, action: 'emit', target: args[1] ?? {}, permissionContractId: sig.emitPermissionContract.id, }), ], }, ], }; } } //@ts-ignore const results = (await emitter.emit(...argsWithSource)); return (0, just_clone_1.default)(results); } assertValidEventSignature(sig, fqen) { if (!sig.isGlobal && !sig.emitPayloadSchema?.fields?.target) { throw new SpruceError_1.default({ code: 'INVALID_EVENT_SIGNATURE', fqen, instructions: 'Oh no! You have to either create an event using `spruce create.event`, set your event to global (event.options.ts, which requires special permissions) or add a target that includes an organizationId or locationId. Choose wisely!', }); } } async optionallyCheckPermissions(args, permissionContractId, fqen) { if (!MercuryTestClient.shouldCheckPermissionsOnLocalEvents) { return true; } let { target } = args[1] ?? {}; const permTarget = {}; if (target?.organizationId) { permTarget.organizationId = target.organizationId; } if (target?.locationId) { throw new Error('checking permissions against a location is not supported. Add to mercury-workspace -> mercury-client'); } const { eventNamespace } = spruce_event_utils_1.eventNameUtil.split(fqen); const results = await this.emit('does-honor-permission-contract::v2020_12_25', { target: permTarget, payload: { id: `${eventNamespace}.${permissionContractId}`, }, }); if (results.totalErrors > 0) { return results; } const { doesHonor } = spruce_event_utils_1.eventResponseUtil.getFirstResponseOrThrow(results); return doesHonor; } buildSource(args) { let source = { ...args[1]?.source, }; if (this.auth?.person) { source.personId = this.auth.person.id; } if (this.auth?.skill) { source.skillId = this.auth.skill.id; } if (args[0] !== 'authenticate::v2020_12_25' && !source.proxyToken && this.getProxyToken()) { source.proxyToken = this.getProxyToken(); } const argsWithSource = [...args]; if (typeof argsWithSource[1] !== 'function' && Object.keys(source).length > 0) { argsWithSource[1] = { ...argsWithSource[1], source, }; } return { source, argsWithSource: (0, just_clone_1.default)(argsWithSource) }; } async connectIfNotConnected(fqen) { if (!this.isConnectedToApi) { this.isConnectedToApi = true; this.connectPromise = this.delayedConnectAndAuth(fqen); } if (!this.shouldWaitForDelayedConnectIfAuthing && fqen === MercurySocketIoClient_1.authenticateFqen) { return; } await this.connectPromise; } async delayedConnectAndAuth(fqen) { await super.connect(); if (this.lastAuthOptions && fqen !== MercurySocketIoClient_1.authenticateFqen) { this.authPromise = undefined; this.shouldHandleAuthenticateLocallyIfListenerSet = false; this.shouldWaitForDelayedConnectIfAuthing = false; await this.authenticate(this.lastAuthOptions); } } async connect() { if (this._isConnected) { await super.connect(); } this._isConnected = true; } isConnected() { return this._isConnected; } async disconnect() { if (this.isConnectedToApi) { await super.disconnect(); } this._isConnected = false; } static reset() { MutableContractClient_1.default.reset(); //@ts-ignore MercuryTestClient.emitter = undefined; //@ts-ignore MercuryTestClient.emitter = MercuryTestClient.getInternalEmitter({ eventSignatures: {}, }); } getIsTestClient() { return true; } static setShouldRequireLocalListeners(shouldRequire) { this.shouldRequireLocalListeners = shouldRequire; } static getShouldRequireLocalListeners() { return this.shouldRequireLocalListeners; } } MercuryTestClient.shouldCheckPermissionsOnLocalEvents = false; MercuryTestClient.namespacesThatHaveToBeHandledLocally = []; MercuryTestClient.shouldRequireLocalListeners = true; exports.default = MercuryTestClient; function mixinConnectionEvents(contract) { return spruce_event_utils_1.eventContractUtil.unifyContracts([ contract ?? { eventSignatures: {} }, statusChangePayloadSchema_1.connectionStatusContract, ]); }