integreat
Version:
Node.js integration layer
297 lines • 13.9 kB
JavaScript
import pLimit from 'p-limit';
import { ensureArray, unwrapArrayOfOne } from '../utils/array.js';
import { isObject, isOkResponse, isOkStatus } from '../utils/is.js';
import xor from '../utils/xor.js';
import { setDataOnActionPayload, setResponseOnAction, setOriginOnAction, setErrorOnAction, setMetaOnAction, } from '../utils/action.js';
import { combineResponses, setOrigin } from '../utils/response.js';
import validateFilters from '../utils/validateFilters.js';
import prepareValidator from '../utils/validation.js';
import { populateActionAfterMutation } from '../utils/mutationHelpers.js';
export const breakSymbol = Symbol('break');
export const statusFromResponses = (responses) => responses.reduce((status, response) => status === response.status || !status ? response.status : 'error', undefined);
const adjustPrevalidationResponse = ({ break: _, message, ...response }) => message
? isOkResponse(response)
? { ...response, warning: message }
: { ...response, error: message }
: response;
const setOkStatusOnErrorResponse = ({ error, origin, ...response } = {}) => ({
...response,
status: 'ok',
...(error ? { warning: error } : {}),
});
const ensureOkResponse = (response) => isOkResponse(response) ? response : setOkStatusOnErrorResponse(response);
const removeError = ({ error, ...response }) => response;
const adjustNoActionResponse = (response) => response.status === 'noaction' && response.error
? removeError({ ...response, warning: response.error })
: response;
const adjustValidationResponse = (responses, response) => responses.length > 0
? combineResponses(responses.map(adjustNoActionResponse))
: response
? ensureOkResponse(response)
: null;
const addModify = (mutation) => isObject(mutation) ? { $modify: true, ...mutation } : mutation;
const putMutationInPipeline = (mutation, useMagic) => Array.isArray(mutation)
? ['$action', ...mutation.map(addModify)]
: useMagic
? ['$action', { '.': '.', ...mutation }]
: ['$action', addModify(mutation)];
const putMutationInPipelineForCondition = (conditionObject) => ({
...conditionObject,
condition: Array.isArray(conditionObject.condition)
? ['$action', ...conditionObject.condition]
: conditionObject.condition
? ['$action', conditionObject.condition]
: null,
});
const getThisResponse = (actionResponses) => actionResponses.$action?.response;
const getPreviousResponse = (actionResponses, prevStepId) => (prevStepId ? actionResponses[prevStepId]?.response : undefined);
function createConditionsValidator(conditions, mapTransform, mapOptions, isPreconditions, failOnErrorInPostconditions, prevStepId) {
const breakByDefault = !isPreconditions && failOnErrorInPostconditions;
if (Array.isArray(conditions)) {
const defaultFailStatus = isPreconditions ? 'noaction' : 'error';
const validator = prepareValidator(conditions, mapTransform, mapOptions, defaultFailStatus, breakByDefault);
return async function validate(actionResponses) {
const [responses, doBreak] = await validator(actionResponses);
const stepResponse = getThisResponse(actionResponses);
const response = adjustValidationResponse(responses, stepResponse);
return [response, doBreak];
};
}
else if (isObject(conditions)) {
const validator = validateFilters(conditions, true);
return async function validate(actionResponses) {
const responses = validator(actionResponses);
const doBreak = responses.some(({ break: doBreak = breakByDefault }) => doBreak);
return responses.length > 0
? [adjustPrevalidationResponse(combineResponses(responses)), doBreak]
: [null, false];
};
}
else if (xor(isPreconditions, failOnErrorInPostconditions)) {
return async function validatePrevWasOk(actionResponses) {
const response = failOnErrorInPostconditions
? getThisResponse(actionResponses)
: getPreviousResponse(actionResponses, prevStepId);
return response && !isOkResponse(response)
? [response, true]
: [null, false];
};
}
else {
return async function validateAlwaysOk(_actionResponses) {
return [null, false];
};
}
}
export function createPreconditionsValidator(preconditions, validationFilters, mapTransform, mapOptions, failOnErrorInPostconditions, prevStepId) {
const conditions = preconditions || validationFilters;
return createConditionsValidator(conditions, mapTransform, mapOptions, true, failOnErrorInPostconditions, prevStepId);
}
function createPostconditionsValidator(conditions, mapTransform, mapOptions, failOnErrorInPostconditions) {
if (Array.isArray(conditions)) {
conditions = conditions.map(putMutationInPipelineForCondition);
}
return createConditionsValidator(conditions, mapTransform, mapOptions, false, failOnErrorInPostconditions);
}
export function getLastJobWithResponse(steps, actionResponses) {
for (let i = steps.length - 1; i > -1; i--) {
const step = steps.at(i);
const actionResponse = actionResponses[step.id] || {};
if (actionResponse.response?.status) {
return actionResponse.response;
}
}
return undefined;
}
const setResponseStatus = (response = {}) => ({
...response,
status: response.status ? response.status : response.error ? 'error' : 'ok',
});
async function mutateAction(action, mutator, actionsWithResponses) {
if (mutator) {
const responsesIncludingAction = {
...actionsWithResponses,
$action: action,
};
return (await mutator(responsesIncludingAction));
}
else {
return action;
}
}
export async function mutateResponse(action, responses, origin, postmutator, prefixOrigin = false) {
const { response: mutatedResponse } = await mutateAction(action, postmutator, responses);
return setOriginOnAction(populateActionAfterMutation(action, setResponseOnAction(action, setResponseStatus(mutatedResponse))), origin, prefixOrigin);
}
function responseFromSteps(actionResponses) {
const errorResponses = Object.values(actionResponses)
.map(({ response }) => response)
.filter((response) => !isOkResponse(response) || !!response?.warning);
if (errorResponses.length === 0) {
return { response: { status: 'ok' } };
}
else {
const status = statusFromResponses(errorResponses);
return {
response: {
status: isOkStatus(status) ? 'ok' : status,
responses: errorResponses,
},
};
}
}
function generateIterateResponse(action, responses) {
const actionsWithResponses = Object.values(responses);
const errorResponses = actionsWithResponses
.map(({ response }) => response)
.filter((response) => !!response && !isOkResponse(response));
const status = errorResponses.length === 0 ? 'ok' : 'error';
const error = errorResponses
.map((response) => response ? `[${response.status}] ${response.error}` : undefined)
.filter(Boolean)
.join(' | ');
const data = actionsWithResponses.flatMap(({ response }) => response?.data);
return setResponseOnAction(action, {
status,
data,
...(error && { error, responses: errorResponses }),
});
}
export const prepareMutation = (pipeline, mapTransform, mapOptions, useMagic = false) => mapTransform(putMutationInPipeline(pipeline, useMagic), mapOptions);
function getIterateMutator(step, mapTransform, mapOptions) {
const pipeline = step.iterate || step.iteratePath;
if (pipeline) {
return mapTransform(pipeline, mapOptions);
}
else {
return undefined;
}
}
export function getPrevStepId(index, steps) {
const prevStep = index > 0 ? steps[index - 1] : undefined;
return Array.isArray(prevStep)
? prevStep
.map((step) => step?.id)
.filter(Boolean)
.join(':')
: prevStep?.id;
}
async function runOneAction(action, dispatch) {
try {
return setResponseOnAction(action, await dispatch(action));
}
catch (error) {
return setErrorOnAction(action, error);
}
}
export default class Step {
id;
#action;
#subSteps;
#validatePreconditions = async () => [null, false];
#validatePostconditions = async () => [null, false];
#premutator;
#postmutator;
#iterateMutator;
#iterateConcurrency;
#isJob;
constructor(stepDef, mapTransform, mapOptions, failOnErrorInPostconditions = false, prevStepId, isJob = false) {
this.#isJob = isJob;
const def = unwrapArrayOfOne(stepDef);
if (Array.isArray(def)) {
this.id = def.map((step) => step.id).join(':');
this.#subSteps = def.map((step, index, steps) => new Step(step, mapTransform, mapOptions, failOnErrorInPostconditions, getPrevStepId(index, steps)));
}
else {
this.id = def.id;
this.#validatePreconditions = createPreconditionsValidator(def.preconditions, def.conditions, mapTransform, mapOptions, failOnErrorInPostconditions, prevStepId);
this.#validatePostconditions = createPostconditionsValidator(def.postconditions, mapTransform, mapOptions, failOnErrorInPostconditions);
this.#action = def.action;
const premutation = def.premutation || def.mutation;
const postmutation = def.postmutation || def.responseMutation;
this.#premutator = premutation
? prepareMutation(premutation, mapTransform, mapOptions, !!def.mutation)
: undefined;
this.#postmutator = postmutation
? prepareMutation(postmutation, mapTransform, mapOptions, !!def.responseMutation)
: undefined;
this.#iterateMutator = getIterateMutator(def, mapTransform, mapOptions);
this.#iterateConcurrency = def.iterateConcurrency;
}
}
async runIteration(actionResponses, dispatch, meta) {
if (!this.#iterateMutator || !this.#action) {
return undefined;
}
const action = setMetaOnAction(this.#action, meta);
const items = ensureArray(await this.#iterateMutator(actionResponses));
const actions = items.map((item) => setDataOnActionPayload(action, item));
const limit = pLimit(this.#iterateConcurrency ?? 1);
return Object.fromEntries((await Promise.all(actions.map((action, index) => limit(async () => runOneAction(await mutateAction(setMetaOnAction(action, {
...action.meta,
stepId: `${this.id}_${index}`,
}), this.#premutator, actionResponses), dispatch))))).map((response, index) => [
`${this.id}_${index}`,
setOriginOnAction(response, `${this.id}_${index}`),
]));
}
async runAction(actionResponses, dispatch, meta) {
if (!this.#action) {
return {};
}
const action = setMetaOnAction(this.#action, meta);
const iterateResponses = await this.runIteration(actionResponses, dispatch, meta);
const responseAction = iterateResponses
? generateIterateResponse(action, iterateResponses)
: await runOneAction(await mutateAction(this.#isJob
? action
: setMetaOnAction(action, { ...meta, stepId: this.id }), this.#premutator, actionResponses), dispatch);
if (this.#isJob) {
actionResponses.action.response = responseAction.response;
}
return {
...iterateResponses,
[this.id]: await mutateResponse(responseAction, { ...actionResponses, ...iterateResponses }, this.id, this.#postmutator, true),
};
}
async runSubSteps(actionResponses, dispatch, meta) {
if (!this.#subSteps) {
return {};
}
const arrayOfResponses = await Promise.all(this.#subSteps.map((step) => step.run({ ...meta, stepId: step.id }, actionResponses, dispatch)));
const doBreak = arrayOfResponses.some((response) => response[breakSymbol]);
const responsesObj = Object.fromEntries(arrayOfResponses.flatMap((responses) => Object.entries(responses)));
const thisStep = responseFromSteps(responsesObj);
return thisStep
? { ...responsesObj, [this.id]: thisStep, [breakSymbol]: doBreak }
: { ...responsesObj, [breakSymbol]: doBreak };
}
async run(meta, actionResponses, dispatch) {
const [preconditionsResponse, doPreBreak] = await this.#validatePreconditions(actionResponses);
if (preconditionsResponse || doPreBreak) {
return {
[this.id]: {
response: setOrigin(preconditionsResponse || {}, this.id),
},
[breakSymbol]: doPreBreak,
};
}
const responseObject = this.#action
? await this.runAction(actionResponses, dispatch, meta)
: await this.runSubSteps(actionResponses, dispatch, meta);
const thisActionResponse = responseObject[this.id];
const [postconditionsResponse, doPostBreak] = await this.#validatePostconditions({
...actionResponses,
$action: thisActionResponse,
});
if (postconditionsResponse || doPostBreak) {
return {
...responseObject,
[this.id]: setResponseOnAction(thisActionResponse, setOrigin(postconditionsResponse || {}, this.id)),
[breakSymbol]: doPostBreak,
};
}
return responseObject;
}
}
//# sourceMappingURL=Step.js.map