src/handlers/sync.ts
File `sync.ts` has 502 lines of code (exceeds 250 allowed). Consider refactoring.import pLimit from 'p-limit'import ms from 'ms'import type { Action, Payload, Response, HandlerDispatch, Meta, TypedData, Params, ActionHandlerResources,} from '../types.js'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' type RetrieveOptions = 'all' | 'updated' | 'created' interface ActionParams extends Record<string, unknown> { type: string | string[] service?: string action?: string updatedAfter?: Date updatedUntil?: Date updatedSince?: Date updatedBefore?: Date createdAfter?: Date createdUntil?: Date createdSince?: Date createdBefore?: Date} interface SyncParams extends Payload { from?: string | Partial<ActionParams> | (string | Partial<ActionParams>)[] to?: string | Partial<ActionParams> updatedAfter?: Date updatedUntil?: Date updatedSince?: Date updatedBefore?: Date createdAfter?: Date createdUntil?: Date createdSince?: Date createdBefore?: Date retrieve?: RetrieveOptions doFilterData?: boolean doQueueSet?: boolean metaKey?: string alwaysSet?: boolean setLastSyncedAtFromData?: boolean maxPerSet?: number setMember?: boolean} interface MetaData { meta: { lastSyncedAt?: Date }} const createGetMetaAction = ( targetService: string, type?: string | string[], metaKey?: string, meta?: Meta,) => ({ type: 'GET_META', payload: { type, keys: 'lastSyncedAt', metaKey, targetService }, meta,}) const createSetMetaAction = ( lastSyncedAt: Date, targetService: string, type?: string | string[], metaKey?: string, meta?: Meta,) => ({ type: 'SET_META', payload: { type, meta: { lastSyncedAt }, metaKey, targetService }, meta,}) const createGetAction = ( { service: targetService, action = 'GET', ...params }: ActionParams, meta?: Meta,) => ({ type: action, payload: { targetService, ...params }, meta,}) const createSetAction = ( data: unknown, { service: targetService, action = 'SET', ...params }: ActionParams, doQueueSet: boolean, meta?: Meta,): Action => ({ type: action, payload: { data, targetService, ...params }, meta: { ...meta, queue: doQueueSet },}) Function `setData` has a Cognitive Complexity of 13 (exceeds 5 allowed). Consider refactoring.async function setData( dispatch: HandlerDispatch, data: TypedData[], { alwaysSet = false, maxPerSet, setMember, ...params }: ActionParams, doQueueSet: boolean, meta?: Meta,): Promise<Response> { let index = data.length === 0 && alwaysSet ? -1 : 0 // Trick to always dispatch SET for `alwaysSet` const maxCount = setMember ? 1 // `setMember` is true, so we only want to set one item at a time : castNumber(maxPerSet) || Number.MAX_SAFE_INTEGER while (index < data.length) { const response = await dispatch( createSetAction( setMember ? data[index] : data.slice(index, index + maxCount), // eslint-disable-line security/detect-object-injection 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: string | Date | undefined, prop: string) => date ? { [prop]: castDate(date) || undefined } : {} async function getLastSyncedAt( dispatch: HandlerDispatch, service: string, type: string | string[], metaKey?: string, meta?: 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 as MetaData | undefined)?.meta.lastSyncedAt) || undefined )} function setCounterPart(params: ActionParams, dateSet: 'updated' | 'created') { const after = params[`${dateSet}After`] const since = params[`${dateSet}Since`] const until = params[`${dateSet}Until`] const before = params[`${dateSet}Before`] const nextParams: Partial<ActionParams> = {}Similar blocks of code found in 2 locations. Consider refactoring. if (after) { nextParams[`${dateSet}Since`] = new Date(after.getTime() + 1) } else if (since) { nextParams[`${dateSet}After`] = new Date(since.getTime() - 1) }Similar blocks of code found in 2 locations. Consider refactoring. 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: HandlerDispatch, payload: Payload, meta?: Meta,) =>Function `setUpdatedDatesAndType` has 57 lines of code (exceeds 25 allowed). Consider refactoring. async function setUpdatedDatesAndType(params: Partial<ActionParams>) { const type = payload.type as string | string[] // We know it's not undefined const { retrieve, updatedAfter, updatedSince, updatedUntil, updatedBefore, createdAfter, createdSince, createdUntil, createdBefore, metaKey, } = payload as SyncParams const nextParams: ActionParams = { ...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, } // Fetch lastSyncedAt from meta when needed, and use as updatedAfter 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, ) } // Make sure the "counterpart" dates are set return { ...nextParams, ...setCounterPart(nextParams, 'created'), ...setCounterPart(nextParams, 'updated'), } } Function `setMetaFromParams` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.const setMetaFromParams = ( dispatch: HandlerDispatch, { payload: { type, metaKey } }: Action, meta: Meta, datesFromData: (Date | undefined)[], gottenDataDate: Date,) => async function setMetaFromParams( { service, updatedUntil, createdUntil }: ActionParams, index: number, ) { if (service) { let lastSyncedAt = // eslint-disable-next-line security/detect-object-injection datesFromData[index] || updatedUntil || createdUntil || gottenDataDate if (lastSyncedAt > gottenDataDate) { lastSyncedAt = gottenDataDate } return dispatch( createSetMetaAction( lastSyncedAt, service, type, metaKey as string | undefined, meta, ), ) } return { status: 'noaction', warning: 'SYNC: No service to set meta for' } } const paramsAsObject = (params?: string | Partial<ActionParams>) => typeof params === 'string' ? { service: params } : params const generateFromParams = async ( dispatch: HandlerDispatch, payload: Payload, meta: Meta,) => Promise.all( ensureArray((payload as SyncParams).from) .map(paramsAsObject) .filter(isNotNullOrUndefined) .map(setDatesAndType(dispatch, payload, meta)) .map((p) => pLimit(1)(() => p)), // Run one promise at a time ) function generateSetDates( fromParams: ActionParams[], params: Params, dateSet: 'updated' | 'created',) { const until = castDate(params[`${dateSet}Until`]) const before = castDate(params[`${dateSet}Before`]) const oldestAfter = fromParams .map((params) => params[`${dateSet}After`]) .sort()[0] return {Similar blocks of code found in 3 locations. Consider refactoring. ...(oldestAfter ? { [`${dateSet}After`]: oldestAfter, [`${dateSet}Since`]: new Date(oldestAfter.getTime() + 1), } : {}),Similar blocks of code found in 3 locations. Consider refactoring. ...(until ? { [`${dateSet}Until`]: until, [`${dateSet}Before`]: new Date(until.getTime() + 1), } : {}),Similar blocks of code found in 3 locations. Consider refactoring. ...(before ? { [`${dateSet}Before`]: before, [`${dateSet}Until`]: new Date(before.getTime() - 1), } : {}), }} function generateToParams( fromParams: ActionParams[], payload: Payload,): ActionParams { const { to, maxPerSet, setMember, alwaysSet, type }: SyncParams = payload return { type: type as string | string[], // We know it's not undefined alwaysSet, maxPerSet, setMember, ...generateSetDates(fromParams, payload, 'updated'), ...generateSetDates(fromParams, payload, 'created'), ...paramsAsObject(to), }} async function extractActionParams( { payload }: Action, meta: Meta, dispatch: HandlerDispatch,): Promise<[ActionParams[], ActionParams | undefined]> { // Require a type if (!payload.type) { return [[], undefined] } // Make from an array of params objects and fetch updatedAfter or createdAfter // from meta when needed const fromParams = await generateFromParams(dispatch, payload, meta) return [fromParams, generateToParams(fromParams, payload)]} const dateFieldFromRetrieve = (retrieve?: RetrieveOptions) => retrieve === 'created' ? 'createdAt' : 'updatedAt' function sortByItemDate(retrieve?: RetrieveOptions) { const dateField = dateFieldFromRetrieve(retrieve) return ({ [dateField]: a }: TypedData, { [dateField]: b }: TypedData) => { 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?: Date, updatedUntil?: Date) => (data: TypedData) => (!updatedAfter || (!!data.updatedAt && data.updatedAt > updatedAfter)) && (!updatedUntil || (!!data.updatedAt && data.updatedAt <= updatedUntil)) async function retrieveDataFromOneService( dispatch: HandlerDispatch, params: ActionParams, doFilterData: boolean, meta?: Meta,) { const { updatedAfter, updatedUntil } = params // Fetch data from service const response = await dispatch(createGetAction(params, meta)) // Throw is not successfull if (response.status !== 'ok') { throw new Error(response.error) } // Return array of data filtered with updatedAt within date range const data = ensureArray(response.data).filter(isTypedData) return doFilterData && (updatedAfter || updatedUntil) ? data.filter(withinDateRange(updatedAfter, updatedUntil)) : data} Function `msFromDelta` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.const msFromDelta = (delta: string) => delta === 'now' ? 0 : delta[0] === '+' ? ms(delta.slice(1)) : delta[0] === '-' ? ms(delta) : undefined function generateUntilDate(date: unknown) { if (typeof date === 'string') { const delta = msFromDelta(date) if (typeof delta === 'number') { return new Date(Date.now() + delta) } } return date} const prepareInputParams = (action: Action<SyncParams>) => ({ ...action, payload: { ...action.payload, updatedUntil: generateUntilDate(action.payload.updatedUntil), createdUntil: generateUntilDate(action.payload.createdUntil), retrieve: action.payload.retrieve ?? 'all', },}) const extractItemDate = (retrieve?: RetrieveOptions) => (item?: TypedData) => (retrieve === 'created' ? item?.createdAt && new Date(item.createdAt) : item?.updatedAt && new Date(item.updatedAt)) || undefined const fetchDataFromService = ( fromParams: ActionParams[], doFilterData: boolean, dispatch: HandlerDispatch, meta: Meta,) => Promise.all( fromParams.map((params) => retrieveDataFromOneService(dispatch, params, doFilterData, meta), ), ) const extractLastSyncedAtDates = ( dataFromServices: TypedData[][], retrieve?: RetrieveOptions,) => dataFromServices.map((data) => data .map(extractItemDate(retrieve)) .reduce( (lastDate, date) => !lastDate || (date && date > lastDate) ? date : lastDate, undefined, ), ) /** * Handler for SYNC action, to sync data from one service to another. * * `retrieve` indicates which items to retrieve. The default is `all`, which * will retrieve all items from the `get` endpoint(s). Set `retrieve` to * `updated` to retrieve only items that are updated after the `lastSyncedAt` * date for the `from` service(s). This is done by passing the `lastSyncedAt` * date as a parameter named `updatedAfter` to the `get` endpoint(s), and by * filtering away any items received with `updatedAt` earlier than * `lastSyncedAt`. You may also set `retrieve` to `created` and have the same * effect, but whith `createdAfter` and `createdAt`. * * The `lastSyncedAt` metadata will be set on the `from` service when items * are retrieved and updated. By default it will be set to the `updatedUntil` or * `createdUntil` date, or `Date.now()` if `updatedUntil` or `createdUntil` are * not given. When `setLastSyncedAtFromData` is true, the latest `updatedAt` or * `createdAt` from the data will be used for each service. */Function `syncHandler` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.export default async function syncHandler( inputAction: Action, { dispatch, setProgress }: ActionHandlerResources,): Promise<Response> { 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: TypedData[] let datesFromData: (Date | undefined)[] = [] 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 as 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') {Avoid too many `return` statements within this function. 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) Avoid too many `return` statements within this function. return { status: 'ok' }}