import type { Optional } from '@speckle/shared' import type { FieldMergeFunction } from '@apollo/client/core' import { get, has } from 'lodash-es' interface AbstractCollection { __typename: T totalCount: number cursor: string | null items: Record[] } interface MergeSettings { /** * Set to false if you want to merge incoming items without checking * for duplicates. Usually you don't want to do this as you can introduce duplicates this way. * Defaults to true */ checkIdentity: boolean /** * Optionally change the prop that should be used to compare * equality between items * Defaults to '__ref', which is the prop added by Apollo that contains the globally unique ID of the object */ identityProp: string } const prepareMergeSettings = ( settings: Optional> ): MergeSettings => ({ checkIdentity: true, identityProp: '__ref', ...(settings || {}) }) /** * Build an Apollo merge function for a field that returns an array of identifiable objects */ export function buildArrayMergeFunction( settings?: Partial ): FieldMergeFunction[], Record[]> { const { checkIdentity, identityProp } = prepareMergeSettings(settings) return (existing, incoming) => { let finalItems: Record[] if (checkIdentity) { finalItems = [...(existing || [])] for (const newItem of incoming || []) { if ( finalItems.findIndex( (item) => item[identityProp] === newItem[identityProp] ) === -1 ) { finalItems.push(newItem) } } } else { finalItems = [...(existing || []), ...(incoming || [])] } return finalItems } } /** * Build an Apollo merge function for a field that returns a collection like AbstractCollection */ export function buildAbstractCollectionMergeFunction( typeName: T, settings?: Partial ): FieldMergeFunction>, AbstractCollection> { const { checkIdentity, identityProp } = prepareMergeSettings(settings) const resolveCursor = ( args: Record | null ): string | null | undefined => { if (!args) return undefined if (has(args, 'cursor')) return get(args, 'cursor') as string | null if (has(args, 'input.cursor')) return get(args, 'input.cursor') as string | null if (has(args, 'filter.cursor')) return get(args, 'filter.cursor') as string | null return undefined } const merge: FieldMergeFunction< Optional>, AbstractCollection > = (existing, incoming, options) => { // If incoming data was requested w/ cursor: null, its the very first page and should replace whatever existed before const cursor = resolveCursor(options.args) const shouldReplace = cursor === null let finalItems: Record[] | undefined = undefined if (existing?.items || incoming?.items) { const existingItems = existing?.items || [] const incomingItems = incoming?.items || [] if (shouldReplace) { finalItems = [...incomingItems] } else { if (checkIdentity) { finalItems = [...existingItems] for (const newItem of incomingItems) { if ( finalItems.findIndex( (item) => item[identityProp] === newItem[identityProp] ) === -1 ) { finalItems.push(newItem) } } } else { finalItems = [...existingItems, ...incomingItems] } } } return { ...(incoming || {}), ...(finalItems ? { items: finalItems } : {}), __typename: incoming?.__typename || existing?.__typename || typeName } } return merge } /** * Merge function that just takes incoming data and overrides all of old data with it * Useful for array fields w/o pagination, where a new array response is supposed to replace * the entire old one */ export const incomingOverwritesExistingMergeFunction: FieldMergeFunction = ( _existing: unknown, incoming: unknown ) => incoming export const mergeAsObjectsFunction: FieldMergeFunction = (existing, incoming) => ({ ...existing, ...incoming })