integreat
Version:
Node.js integration layer
275 lines • 11.7 kB
JavaScript
import pLimit from 'p-limit';
import ms from 'ms';
import { createMetaForSubAction } from '../utils/action.js';
import { setOrigin, createErrorResponse } from '../utils/response.js';
import { isTypedData, isNotNullOrUndefined } from '../utils/is.js';
import { ensureArray } from '../utils/array.js';
import castDate from '../schema/castFns/date.js';
import castNumber from '../schema/castFns/number.js';
const createGetMetaAction = (targetService, type, metaKey, meta) => ({
type: 'GET_META',
payload: { type, keys: 'lastSyncedAt', metaKey, targetService },
meta,
});
const createSetMetaAction = (lastSyncedAt, targetService, type, metaKey, meta) => ({
type: 'SET_META',
payload: { type, meta: { lastSyncedAt }, metaKey, targetService },
meta,
});
const createGetAction = ({ service: targetService, action = 'GET', ...params }, meta) => ({
type: action,
payload: { targetService, ...params },
meta,
});
const createSetAction = (data, { service: targetService, action = 'SET', ...params }, doQueueSet, meta) => ({
type: action,
payload: { data, targetService, ...params },
meta: { ...meta, queue: doQueueSet },
});
async function setData(dispatch, data, { alwaysSet = false, maxPerSet, setMember, ...params }, doQueueSet, meta) {
let index = data.length === 0 && alwaysSet ? -1 : 0;
const maxCount = setMember
? 1
: castNumber(maxPerSet) || Number.MAX_SAFE_INTEGER;
while (index < data.length) {
const response = await dispatch(createSetAction(setMember ? data[index] : data.slice(index, index + maxCount), params, doQueueSet, meta));
if (!response?.status || !['ok', 'queued'].includes(response.status)) {
const progressMessage = index > 0 ? `, but the first ${index} items where set successfully` : '';
return {
status: response?.status || 'error',
error: `SYNC: Setting data failed${progressMessage}. ${response?.error || ''}`.trim(),
};
}
index += maxCount;
}
return data.length > 0 || alwaysSet
? { status: 'ok' }
: { status: 'noaction', warning: 'SYNC: No data to set' };
}
const setDatePropIf = (date, prop) => date ? { [prop]: castDate(date) || undefined } : {};
async function getLastSyncedAt(dispatch, service, type, metaKey, meta) {
const response = await dispatch(createGetMetaAction(service, type, metaKey, meta));
if (response.status !== 'ok') {
throw new Error(`Could not fetch last synced date for service '${service}': [${response.status}] ${response.error}`);
}
return (castDate(response.data?.meta.lastSyncedAt) ||
undefined);
}
function setCounterPart(params, dateSet) {
const after = params[`${dateSet}After`];
const since = params[`${dateSet}Since`];
const until = params[`${dateSet}Until`];
const before = params[`${dateSet}Before`];
const nextParams = {};
if (after) {
nextParams[`${dateSet}Since`] = new Date(after.getTime() + 1);
}
else if (since) {
nextParams[`${dateSet}After`] = new Date(since.getTime() - 1);
}
if (until) {
nextParams[`${dateSet}Before`] = new Date(until.getTime() + 1);
}
else if (before) {
nextParams[`${dateSet}Until`] = new Date(before.getTime() - 1);
}
return nextParams;
}
const setDatesAndType = (dispatch, payload, meta) => async function setUpdatedDatesAndType(params) {
const type = payload.type;
const { retrieve, updatedAfter, updatedSince, updatedUntil, updatedBefore, createdAfter, createdSince, createdUntil, createdBefore, metaKey, } = payload;
const nextParams = {
...setDatePropIf(updatedAfter, 'updatedAfter'),
...setDatePropIf(updatedSince, 'updatedSince'),
...setDatePropIf(updatedUntil, 'updatedUntil'),
...setDatePropIf(updatedBefore, 'updatedBefore'),
...setDatePropIf(createdAfter, 'createdAfter'),
...setDatePropIf(createdSince, 'createdSince'),
...setDatePropIf(createdUntil, 'createdUntil'),
...setDatePropIf(createdBefore, 'createdBefore'),
type,
...params,
};
if (retrieve === 'updated' &&
params.service &&
!updatedAfter &&
!updatedSince) {
nextParams.updatedAfter = await getLastSyncedAt(dispatch, params.service, type, metaKey, meta);
}
else if (retrieve === 'created' &&
params.service &&
!createdAfter &&
!createdSince) {
nextParams.createdAfter = await getLastSyncedAt(dispatch, params.service, type, metaKey, meta);
}
return {
...nextParams,
...setCounterPart(nextParams, 'created'),
...setCounterPart(nextParams, 'updated'),
};
};
const setMetaFromParams = (dispatch, { payload: { type, metaKey } }, meta, datesFromData, gottenDataDate) => async function setMetaFromParams({ service, updatedUntil, createdUntil }, index) {
if (service) {
let lastSyncedAt = datesFromData[index] || updatedUntil || createdUntil || gottenDataDate;
if (lastSyncedAt > gottenDataDate) {
lastSyncedAt = gottenDataDate;
}
return dispatch(createSetMetaAction(lastSyncedAt, service, type, metaKey, meta));
}
return { status: 'noaction', warning: 'SYNC: No service to set meta for' };
};
const paramsAsObject = (params) => typeof params === 'string' ? { service: params } : params;
const generateFromParams = async (dispatch, payload, meta) => Promise.all(ensureArray(payload.from)
.map(paramsAsObject)
.filter(isNotNullOrUndefined)
.map(setDatesAndType(dispatch, payload, meta))
.map((p) => pLimit(1)(() => p)));
function generateSetDates(fromParams, params, dateSet) {
const until = castDate(params[`${dateSet}Until`]);
const before = castDate(params[`${dateSet}Before`]);
const oldestAfter = fromParams
.map((params) => params[`${dateSet}After`])
.sort()[0];
return {
...(oldestAfter
? {
[`${dateSet}After`]: oldestAfter,
[`${dateSet}Since`]: new Date(oldestAfter.getTime() + 1),
}
: {}),
...(until
? {
[`${dateSet}Until`]: until,
[`${dateSet}Before`]: new Date(until.getTime() + 1),
}
: {}),
...(before
? {
[`${dateSet}Before`]: before,
[`${dateSet}Until`]: new Date(before.getTime() - 1),
}
: {}),
};
}
function generateToParams(fromParams, payload) {
const { to, maxPerSet, setMember, alwaysSet, type } = payload;
return {
type: type,
alwaysSet,
maxPerSet,
setMember,
...generateSetDates(fromParams, payload, 'updated'),
...generateSetDates(fromParams, payload, 'created'),
...paramsAsObject(to),
};
}
async function extractActionParams({ payload }, meta, dispatch) {
if (!payload.type) {
return [[], undefined];
}
const fromParams = await generateFromParams(dispatch, payload, meta);
return [fromParams, generateToParams(fromParams, payload)];
}
const dateFieldFromRetrieve = (retrieve) => retrieve === 'created' ? 'createdAt' : 'updatedAt';
function sortByItemDate(retrieve) {
const dateField = dateFieldFromRetrieve(retrieve);
return ({ [dateField]: a }, { [dateField]: b }) => {
const dateA = a ? new Date(a).getTime() : undefined;
const dateB = b ? new Date(b).getTime() : undefined;
return dateA && dateB ? dateA - dateB : dateA ? -1 : 1;
};
}
const withinDateRange = (updatedAfter, updatedUntil) => (data) => (!updatedAfter || (!!data.updatedAt && data.updatedAt > updatedAfter)) &&
(!updatedUntil || (!!data.updatedAt && data.updatedAt <= updatedUntil));
async function retrieveDataFromOneService(dispatch, params, doFilterData, meta) {
const { updatedAfter, updatedUntil } = params;
const response = await dispatch(createGetAction(params, meta));
if (response.status === 'noaction') {
return [];
}
else if (response.status !== 'ok') {
throw new Error(response.error);
}
const data = ensureArray(response.data).filter(isTypedData);
return doFilterData && (updatedAfter || updatedUntil)
? data.filter(withinDateRange(updatedAfter, updatedUntil))
: data;
}
const msFromDelta = (delta) => delta === 'now'
? 0
: delta[0] === '+'
? ms(delta.slice(1))
: delta[0] === '-'
? ms(delta)
: undefined;
function generateUntilDate(date) {
if (typeof date === 'string') {
const delta = msFromDelta(date);
if (typeof delta === 'number') {
return new Date(Date.now() + delta);
}
}
return date;
}
const prepareInputParams = (action) => ({
...action,
payload: {
...action.payload,
updatedUntil: generateUntilDate(action.payload.updatedUntil),
createdUntil: generateUntilDate(action.payload.createdUntil),
retrieve: action.payload.retrieve ?? 'all',
},
});
const extractItemDate = (retrieve) => (item) => (retrieve === 'created'
? item?.createdAt && new Date(item.createdAt)
: item?.updatedAt && new Date(item.updatedAt)) || undefined;
const fetchDataFromService = (fromParams, doFilterData, dispatch, meta) => Promise.all(fromParams.map((params) => retrieveDataFromOneService(dispatch, params, doFilterData, meta)));
const extractLastSyncedAtDates = (dataFromServices, retrieve) => dataFromServices.map((data) => data
.map(extractItemDate(retrieve))
.reduce((lastDate, date) => !lastDate || (date && date > lastDate) ? date : lastDate, undefined));
export default async function syncHandler(inputAction, { dispatch, setProgress }) {
setProgress(0);
const action = prepareInputParams(inputAction);
const { payload: { retrieve, setLastSyncedAtFromData = false, doFilterData = true, doQueueSet = true, }, } = action;
const meta = createMetaForSubAction(action.meta);
let fromParams, toParams;
try {
;
[fromParams, toParams] = await extractActionParams(action, meta, dispatch);
}
catch (error) {
return createErrorResponse(`Failed to prepare params for SYNC: ${error instanceof Error ? error.message : String(error)}`, 'handler:SYNC');
}
if (fromParams.length === 0 || !toParams) {
return createErrorResponse('SYNC: `type`, `to`, and `from` parameters are required', 'handler:SYNC', 'badrequest');
}
let data;
let datesFromData = [];
setProgress(0.1);
try {
const dataFromServices = await fetchDataFromService(fromParams, doFilterData, dispatch, meta);
data = dataFromServices.flat().sort(sortByItemDate(retrieve));
if (setLastSyncedAtFromData) {
datesFromData = extractLastSyncedAtDates(dataFromServices, retrieve);
}
}
catch (error) {
return createErrorResponse(`SYNC: Could not get data. ${error.message}`, 'handler:SYNC');
}
const gottenDataDate = new Date();
setProgress(0.5);
const response = await setData(dispatch, data, toParams, doQueueSet, meta);
if (response.status === 'noaction') {
return setOrigin(response, 'handler:SYNC');
}
else if (response.status !== 'ok') {
return createErrorResponse(response?.error, 'handler:SYNC', response?.status || 'error');
}
setProgress(0.9);
if (retrieve === 'updated' || retrieve === 'created') {
await Promise.all(fromParams.map(setMetaFromParams(dispatch, action, meta, datesFromData, gottenDataDate)));
}
setProgress(1);
return { status: 'ok' };
}
//# sourceMappingURL=sync.js.map