import { difference, isDefined } from 'remeda' import { ZodIssueCode } from 'zod' import { SuperRefinement } from 'zod/lib/types' import { getId, Id, WithId } from '../../../generic/models/Id' import { Mapper } from '../../Mapper' import { parallelMap } from '../../promise' import { ToString } from '../../string' export const oneToMany = ($parent: string, $child: string, getParents: Mapper, getChildren: Mapper, getParentId: Mapper, getChildId: Mapper, getChildParentId: Mapper): SuperRefinement => (database, ctx) => { const parents = getParents(database) const children = getChildren(database) // const getParentById = (database: Database, parentId: ParentId) => parents.find(p => getParentId(p) === parentId) // const getChildren = todo() // const childrenWithoutParents = getChildrenWithoutParents({ iDatabase: { getChildren, getParentById } }) for (const child of children) { const parentId = getChildParentId(child) const parent = parents.find(p => getParentId(p) === parentId) if (!parent) { ctx.addIssue({ code: ZodIssueCode.custom, message: `${$child} #${getChildId(child)} is linked to ${$parent} #${parentId}, but ${$parent} #${parentId} does not exist`, params: { child, }, }) } } } export const oneToManyArray = ($parent: string, $child: string, getParents: Mapper, getChildren: Mapper, getParentId: Mapper, getChildId: Mapper, getChildParentIds: Mapper): SuperRefinement => (database, ctx) => { const parents = getParents(database) const children = getChildren(database) for (const child of children) { const parentIdsAll = getChildParentIds(child) const parentIdsWithoutParent = parentIdsAll.filter(parentId => !parents.find(p => getParentId(p) === parentId)) if (parentIdsWithoutParent.length) { ctx.addIssue({ code: ZodIssueCode.custom, message: `${$child} #${getChildId(child)} is linked to ${$parent} ${parentIdsWithoutParent.map(parentId => `#${parentId}`)}, but these parents do not exist`, params: { child, parentIdsWithoutParent, }, }) } } } export const oneToManyWithId = ($parent: string, $child: string, getParents: Mapper, getChildren: Mapper, getChildParentId: Mapper): SuperRefinement => { return oneToMany($parent, $child, getParents, getChildren, getId, getId, getChildParentId) } export const oneToManyArrayWithId = ($parent: string, $child: string, getParents: Mapper, getChildren: Mapper, getChildParentIds: Mapper): SuperRefinement => { return oneToManyArray($parent, $child, getParents, getChildren, getId, getId, getChildParentIds) } export const oneToManySimple = ($source: string, $target: string, getSourceIds: (value: Val) => Id[], getTargetIds: (value: Val) => Id[]): SuperRefinement => (value, ctx) => { const diff = getOneToManyDiff(getSourceIds, getTargetIds)(value) if (diff.length) { ctx.addIssue({ code: ZodIssueCode.custom, message: 'Some values are present in target but not present in source', params: { diff, }, }) } } export const getOneToManyDiff = (getSourceIds: (value: Val) => Id[], getTargetIds: (value: Val) => Id[]) => (value: Val) => { const sourceIds = getSourceIds(value) const targetIds = getTargetIds(value) return difference(targetIds, sourceIds) } export const getChildrenWithoutParents = ( getChildren: (database: Database) => Promise, getParentById: (database: Database, parentId: ParentId) => Promise, getChildParentId: (child: Child) => ParentId, ) => async (database: Database) => { const children = await getChildren(database) const results = await parallelMap(children, async (child) => { const parentId = getChildParentId(child) const parent = await getParentById(database, parentId) return parent ? undefined : child }) return results.filter(isDefined) } export interface OneToManyError { $parent: string $child: string parentId: ParentId childId: ChildId } export const getOneToManyErrors = ( $parent: string, $child: string, getChildren: (database: Database) => Promise, getParentById: (database: Database, parentId: ParentId) => Promise, getChildParentId: (child: Child) => ParentId, getChildId: (child: Child) => ChildId ) => async (database: Database): Promise[]> => { const children = await getChildrenWithoutParents(getChildren, getParentById, getChildParentId)(database) return children.map(child => ({ $parent, $child, parentId: getChildParentId(child), childId: getChildId(child), })) } export const getOneToManyErrorsWithId = ( $parent: string, $child: string, getParents: (database: Database) => Promise, getChildren: (database: Database) => Promise, ) => async (database: Database) => { const parents = await getParents(database) const getParentById = async (database: Database, parentId: Id) => parents.find(parent => parent.id === parentId) return getOneToManyErrors($parent, $child, getChildren, getParentById, getId, getId) } const toStringOneToManyError = (toStringChildId: ToString, toStringParentId: ToString) => ({ $child, $parent, childId, parentId }: OneToManyError) => { const $parentId = toStringParentId(parentId) const $childId = toStringChildId(childId) return `${$child} #${$childId} is linked to ${$parent} #${$parentId}, but ${$parent} #${$parentId} does not exist` }