integreat
Version:
Node.js integration layer
270 lines • 14.2 kB
JavaScript
import debugLib from 'debug';
import Endpoint from './Endpoint.js';
import { sendToTransporter } from './utils/send.js';
import { dispatchIncoming, authenticateCallback } from './utils/incoming.js';
import { resolveAuth, resolveIncomingAuth } from './utils/resolveAuth.js';
import { castPayload, castResponse, getCastFn } from './utils/cast.js';
import { setErrorOnAction, setOriginOnAction, setOptionsOnAction, setResponseOnAction, } from '../utils/action.js';
import { createErrorResponse, setOrigin } from '../utils/response.js';
import { prepareOptions, mergeOptions } from './utils/options.js';
import Connection from './Connection.js';
import { lookupById, lookupByIds } from '../utils/indexUtils.js';
import * as authorizeData from './utils/authData.js';
import authorizeAction, { isAuthorizedAction } from './utils/authAction.js';
import { composeMiddleware } from '../utils/composeMiddleware.js';
const debug = debugLib('great');
const areWeMissingAdapters = (defs, adapters) => Array.isArray(defs) && defs.length !== adapters.length;
const createServiceAdapterError = (serviceId) => `Service '${serviceId}' references one or more unknown adapters`;
const createEndpointAdapterError = (endpoint, serviceId) => `${endpoint.id ? `Endpoint '${endpoint.id}'` : 'An endpoint'} on service '${serviceId}' references one or more unknown adapters`;
const shouldSetTargetService = (action, flag) => flag && action.meta?.options?.doSetTargetService !== false;
const setTargetService = (action, targetService) => ({
...action,
payload: { ...action.payload, targetService },
});
const removeSetTargetServiceFlagFromOptions = ({ doSetTargetService, ...options }) => options;
const removeSetTargetServiceFlag = (action) => typeof action.meta?.options?.doSetTargetService === 'boolean'
? {
...action,
meta: {
...action.meta,
options: removeSetTargetServiceFlagFromOptions(action.meta.options),
},
}
: action;
function setUpEndpoint(serviceId, adapters, resolveOutgoingAuth, resolveIncomingAuth, serviceOutgoingAuth, serviceIncomingAuth, serviceOptions, mapTransform, mapOptions, mutation, serviceAdapters) {
return (endpointDef) => {
const endpointAdapters = lookupByIds(endpointDef.adapters, adapters);
if (areWeMissingAdapters(endpointDef.adapters, endpointAdapters)) {
throw new TypeError(createEndpointAdapterError(endpointDef, serviceId));
}
const outgoingAuth = resolveOutgoingAuth(endpointDef.auth) || serviceOutgoingAuth;
const incomingAuth = resolveIncomingAuth(endpointDef.auth) || serviceIncomingAuth;
const options = endpointDef.options
? mergeOptions(serviceOptions, prepareOptions(endpointDef.options))
: serviceOptions;
return new Endpoint(endpointDef, serviceId, options, mapTransform, mapOptions, mutation, serviceAdapters, endpointAdapters, outgoingAuth, incomingAuth);
};
}
const preserveMetaProps = (action, { id, cid, gid, ident } = {}) => ({
...action,
meta: { id, cid, gid, ident, ...action.meta },
});
export default class Service {
id;
meta;
isListening;
#schemas;
#options;
#endpoints;
#transporter;
#outgoingAuth;
#incomingAuth;
#shouldCompleteIdent;
#connection;
#authorizeDataFromService;
#authorizeDataToService;
#middleware;
#emit;
constructor({ id: serviceId, transporter: transporterId, adapters: adapterDefs = [], auth, meta, options = {}, mutation, endpoints: endpointDefs = [], }, { transporters, adapters = {}, authenticators = {}, auths, schemas, mapTransform, mapOptions = {}, middleware = [], identConfig, emit = () => undefined, }) {
if (typeof serviceId !== 'string' || serviceId === '') {
throw new TypeError(`Can't create service without an id.`);
}
this.id = serviceId;
this.meta = meta;
this.#emit = emit;
this.isListening = false;
this.#schemas = schemas;
const transporter = lookupById(transporterId, transporters);
if (!transporter) {
throw new TypeError(`Service '${serviceId}' references unknown transporter '${transporterId}'`);
}
this.#transporter = transporter;
const outgoingAuthResolver = resolveAuth(authenticators, auths);
const incomingAuthResolver = resolveIncomingAuth(authenticators, auths);
this.#outgoingAuth = outgoingAuthResolver(auth);
this.#incomingAuth = incomingAuthResolver(auth);
this.#shouldCompleteIdent = identConfig?.completeIdent ?? false;
this.#authorizeDataFromService = authorizeData.fromService(schemas);
this.#authorizeDataToService = authorizeData.toService(schemas);
const serviceAdapters = lookupByIds(adapterDefs, adapters);
if (areWeMissingAdapters(serviceAdapters, adapterDefs)) {
throw new TypeError(createServiceAdapterError(serviceId));
}
const serviceOptions = prepareOptions(options);
this.#options = serviceOptions.transporter;
this.#endpoints = Endpoint.sortAndPrepare(endpointDefs).map(setUpEndpoint(serviceId, adapters, outgoingAuthResolver, incomingAuthResolver, this.#outgoingAuth, this.#incomingAuth, serviceOptions, mapTransform, mapOptions, mutation, serviceAdapters));
this.#middleware =
middleware.length > 0 ? composeMiddleware(...middleware) : (fn) => fn;
this.#connection = new Connection(this.#transporter, serviceOptions.transporter, emit);
}
async endpointFromAction(action, isIncoming = false) {
return await Endpoint.findMatchingEndpoint(this.#endpoints, action, isIncoming);
}
async preflightAction(action, endpoint, dispatch) {
const outgoingAuth = endpoint.outgoingAuth;
let preparedAction = authorizeAction(this.#schemas, !!outgoingAuth)(action);
if (preparedAction.response?.status) {
return preparedAction;
}
const validateResponse = await endpoint.validateAction(preparedAction);
if (validateResponse) {
return setResponseOnAction(action, validateResponse);
}
if (endpoint.options?.transporter.authInData && outgoingAuth) {
await outgoingAuth.authenticate(preparedAction, dispatch);
preparedAction = outgoingAuth.applyToAction(preparedAction, this.#transporter);
if (preparedAction.response?.status) {
return setResponseOnAction(action, preparedAction.response);
}
}
return preparedAction;
}
async mutateRequest(action, endpoint) {
const castFn = getCastFn(this.#schemas, action.payload.type);
const actionWithOptions = setOptionsOnAction(action, endpoint);
const castedAction = castPayload(actionWithOptions, endpoint, castFn);
const authorizedAction = this.#authorizeDataToService(castedAction, endpoint.allowRawRequest);
let mutatedAction;
try {
mutatedAction = await endpoint.mutate(authorizedAction, true);
}
catch (error) {
return setErrorOnAction(action, `Error while mutating request: ${error instanceof Error ? error.message : String(error)}`, 'mutate:request');
}
return setOriginOnAction(mutatedAction, 'mutate:request', false);
}
async mutateIncomingRequest(action, endpoint) {
let mutated;
try {
mutated = preserveMetaProps(await endpoint.mutate(action, false), action.meta);
}
catch (error) {
return setErrorOnAction(action, `Error while mutating incoming request: ${error instanceof Error ? error.message : String(error)}`, 'mutate:request:incoming');
}
const castFn = getCastFn(this.#schemas, mutated.payload.type);
const casted = castPayload(mutated, endpoint, castFn);
const withOrigin = setOriginOnAction(casted, 'mutate:request:incoming');
return this.#authorizeDataToService(withOrigin, endpoint.allowRawRequest);
}
async mutateResponse(action, endpoint) {
const actionWithOptions = setOptionsOnAction(action, endpoint);
let mutated;
try {
mutated = await endpoint.mutate(actionWithOptions, false);
}
catch (error) {
return {
...action.response,
...createErrorResponse(`Error while mutating response: ${error instanceof Error ? error.message : String(error)}`, 'mutate:response'),
};
}
const castFn = getCastFn(this.#schemas, mutated.payload.type);
const casted = castResponse(mutated, endpoint, castFn);
const withOrigin = setOriginOnAction(casted, 'mutate:response', false);
const { response } = this.#authorizeDataFromService(withOrigin, endpoint.allowRawResponse);
return response || { status: undefined };
}
async mutateIncomingResponse(action, endpoint) {
const castFn = getCastFn(this.#schemas, action.payload.type);
const castedAction = castResponse(action, endpoint, castFn);
const authorizedAction = this.#authorizeDataFromService(castedAction, endpoint.allowRawResponse ?? true);
let mutatedAction;
try {
mutatedAction = await endpoint.mutate(authorizedAction, true);
}
catch (error) {
return {
...action.response,
...createErrorResponse(`Error while mutating response: ${error instanceof Error ? error.message : String(error)}`, 'mutate:response:incoming'),
};
}
const { response } = setOriginOnAction(mutatedAction, 'mutate:response:incoming', false);
return response || { status: undefined };
}
async send(action, endpoint, dispatch, doSetTargetService = true) {
if (action.response?.status) {
return action.response;
}
if (!this.#connection) {
return createErrorResponse(`Service '${this.id}' has no open connection`, `service:${this.id}`);
}
if (!isAuthorizedAction(action)) {
return createErrorResponse('Action has not been authorized by Integreat', `internal:service:${this.id}`, 'autherror');
}
const outgoingAuth = endpoint?.outgoingAuth;
if (outgoingAuth && !action.meta?.auth) {
await outgoingAuth.authenticate(action, dispatch);
action = outgoingAuth.applyToAction(action, this.#transporter);
if (action.response?.status) {
return setOrigin(action.response, `service:${this.id}`, true);
}
}
if (shouldSetTargetService(action, doSetTargetService)) {
action = setTargetService(action, this.id);
}
action = removeSetTargetServiceFlag(action);
return setOrigin(await this.#middleware(sendToTransporter(this.#transporter, this.#connection, this.id))(action), `middleware:service:${this.id}`);
}
async listen(dispatch) {
debug('Set up service listening ...');
if (!this.#connection) {
debug(`Service '${this.id}' has no open connection`);
return createErrorResponse(`Service '${this.id}' has no open connection`, `service:${this.id}`);
}
if (typeof this.#transporter.listen !== 'function') {
debug('Transporter has no listen method');
return createErrorResponse('Transporter has no listen method', `service:${this.id}`, 'noaction');
}
if (typeof this.#transporter.shouldListen === 'function' &&
!this.#transporter.shouldListen(this.#options)) {
debug('Transporter is not configured to listen');
return createErrorResponse('Transporter is not configured to listen', `service:${this.id}`, 'noaction');
}
if (this.#outgoingAuth &&
!(await this.#outgoingAuth.authenticate(null, dispatch))) {
debug('Could not authenticate');
return setOrigin(this.#outgoingAuth.getResponseFromAuth(), `service:${this.id}`, true);
}
if (!(await this.#connection.connect(this.#outgoingAuth?.getAuthObject(this.#transporter, null)))) {
debug(`Could not listen to '${this.id}' service. Failed to connect`);
return createErrorResponse(`Could not listen to '${this.id}' service. Failed to connect`, `service:${this.id}`);
}
debug('Calling transporter listen() ...');
const listenResponse = await this.#transporter.listen(dispatchIncoming(dispatch, this.#middleware, this.id), this.#connection.object, authenticateCallback(this, dispatch, this.#shouldCompleteIdent, this.#incomingAuth), this.#emit);
this.isListening = listenResponse.status === 'ok';
return listenResponse;
}
async stopListening() {
debug(`Stop listening to service '${this.id}'`);
if (typeof this.#transporter.stopListening === 'function') {
if (this.#connection) {
this.#transporter.stopListening(this.#connection.object);
this.isListening = false;
debug('Stopped');
return { status: 'ok' };
}
else {
debug('No connection to stop listening to');
return createErrorResponse(`Service '${this.id}' does not have an open connection`, `service:${this.id}`, 'noaction');
}
}
else {
debug(`Service '${this.id}' has no \`stopListening()\` method`);
return createErrorResponse(`Service '${this.id}' only allows stopping listening by closing the connection`, `service:${this.id}`, 'noaction');
}
}
async close() {
debug(`Close service '${this.id}'`);
if (this.#connection) {
await this.#transporter.disconnect(this.#connection.object);
this.#connection = null;
debug('Closed');
}
else {
debug('No connection to disconnect');
}
this.isListening = false;
return { status: 'ok' };
}
}
//# sourceMappingURL=Service.js.map