import { type CacheHint, maybeCacheControlFromInfo } from '@apollo/cache-control-types' import { type GraphQLResolveInfo } from 'graphql' import DataLoader from 'dataloader' import type { NextFieldType, IndividualFieldAccessControl, BaseListTypeInfo, BaseItem, FindManyArgsValue, KeystoneContext, GraphQLTypesForList, FieldReadItemAccessArgs, } from '../../../types' import { graphql } from '../../..' import { getOperationFieldAccess, getOperationAccess, getAccessFilters } from '../access-control' import type { ResolvedDBField, ResolvedRelationDBField } from '../resolve-relationships' import type { InitialisedList } from '../initialise-lists' import { type IdType, getDBFieldKeyForFieldOnMultiField } from '../utils' import { accessControlledFilter } from './resolvers' import * as queries from './resolvers' function getRelationVal ( dbField: ResolvedRelationDBField, id: IdType, foreignList: InitialisedList, context: KeystoneContext, info: GraphQLResolveInfo, fk: IdType | null | undefined ) { const oppositeDbField = foreignList.resolvedDbFields[dbField.field] if (oppositeDbField.kind !== 'relation') throw new Error('failed assert') if (dbField.mode === 'many') { const relationFilter = { [dbField.field]: oppositeDbField.mode === 'many' ? { some: { id } } : { id }, } return { findMany: async (args: FindManyArgsValue) => queries.findMany(args, foreignList, context, info, relationFilter), count: async ({ where }: { where: GraphQLTypesForList['where'] }) => queries.count({ where }, foreignList, context, info, relationFilter), } } else { return async () => { if (fk === null) { // If the foreign key is explicitly null, there's no need to anything else, // since we know the related item doesn't exist. return null } // for one-to-many relationships, the one side always owns the foreign key // so that means we have the id for the related item and we're fetching it by _its_ id. // for the a one-to-one relationship though, the id might be on the related item // so we need to fetch the related item by the id of the current item on the foreign key field const currentItemOwnsForeignKey = fk !== undefined return fetchRelatedItem(context)(foreignList)( currentItemOwnsForeignKey ? 'id' : `${dbField.field}Id` )(currentItemOwnsForeignKey ? fk : id) } } } function weakMemoize (cb: (arg: Arg) => Return) { const cache = new WeakMap() return (arg: Arg) => { if (!cache.has(arg)) { const result = cb(arg) cache.set(arg, result) } return cache.get(arg)! } } function memoize (cb: (arg: Arg) => Return) { const cache = new Map() return (arg: Arg) => { if (!cache.has(arg)) { const result = cb(arg) cache.set(arg, result) } return cache.get(arg)! } } const fetchRelatedItem = weakMemoize((context: KeystoneContext) => weakMemoize((foreignList: InitialisedList) => memoize((idFieldKey: string) => { const relatedItemLoader = new DataLoader( (keys: readonly IdType[]) => fetchRelatedItems(context, foreignList, idFieldKey, keys), { cache: false } ) return (id: IdType) => relatedItemLoader.load(id) }) ) ) async function fetchRelatedItems ( context: KeystoneContext, foreignList: InitialisedList, idFieldKey: string, toFetch: readonly IdType[] ) { const operationAccess = await getOperationAccess(foreignList, context, 'query') if (!operationAccess) { return toFetch.map(() => undefined) } const accessFilters = await getAccessFilters(foreignList, context, 'query') if (accessFilters === false) { return toFetch.map(() => undefined) } const toFetchUnique = Array.from(new Set(toFetch)) const resolvedWhere = await accessControlledFilter( foreignList, context, { [idFieldKey]: { in: toFetchUnique } }, accessFilters ) const results = await context.prisma[foreignList.listKey].findMany({ where: resolvedWhere }) const resultsById = new Map(results.map((x: any) => [x[idFieldKey], x])) return toFetch.map(id => resultsById.get(id)) } function getValueForDBField ( rootVal: BaseItem, dbField: ResolvedDBField, id: IdType, fieldPath: string, context: KeystoneContext, lists: Record, info: GraphQLResolveInfo ) { if (dbField.kind === 'multi') { return Object.fromEntries( Object.keys(dbField.fields).map(innerDBFieldKey => { const keyOnDbValue = getDBFieldKeyForFieldOnMultiField(fieldPath, innerDBFieldKey) return [innerDBFieldKey, rootVal[keyOnDbValue] as any] }) ) } if (dbField.kind === 'relation') { // If we're holding a foreign key value, let's take advantage of that. let fk: IdType | undefined if (dbField.mode === 'one' && dbField.foreignIdField.kind !== 'none') { fk = rootVal[`${fieldPath}Id`] as IdType } return getRelationVal(dbField, id, lists[dbField.list], context, info, fk) } else { return rootVal[fieldPath] as any } } export function outputTypeField ( output: NextFieldType['output'], dbField: ResolvedDBField, cacheHint: CacheHint | undefined, access: IndividualFieldAccessControl>, listKey: string, fieldKey: string, lists: Record ) { const list = lists[listKey] return graphql.field({ type: output.type, deprecationReason: output.deprecationReason, description: output.description, args: output.args, extensions: output.extensions, async resolve (rootVal: BaseItem, args, context, info) { const id = rootVal.id as IdType const fieldAccess = await getOperationFieldAccess(rootVal, list, fieldKey, context, 'read') if (!fieldAccess) return null // only static cache hints are supported at the field level until a use-case makes it clear what parameters a dynamic hint would take if (cacheHint && info) { maybeCacheControlFromInfo(info)?.setCacheHint(cacheHint) } const value = getValueForDBField(rootVal, dbField, id, fieldKey, context, lists, info) if (output.resolve) { return output.resolve({ value, item: rootVal }, args, context, info) } else { return value } }, }) }