@sprucelabs/mercury-client
Version:
The simple way to interact with the Spruce Experience Platform
351 lines (350 loc) • 13.9 kB
JavaScript
"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,
]);
}