UNPKG

integreat

Version:
275 lines 11.7 kB
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