I'm implementing cursor pagination to create an infinite scroll of posts, building upon URQL's simple pagination example from the docs. It works fine, I get the posts I want with all the information I want... until the cache is invalidated by setting info.partial
to true
in my cursorPagination()
function. Then I get project: null
.
I wonder if there's something special I have to do to cache the results coming from the field resolver (dataloader)? I have other relations besides post.project
which are behaving as expected so it must be something specific to this field resolver... it's the only one that is nullable, maybe it has something to do with that fact, but why would it return the value initially and then lose it when info.partial
is set to true
?
Here's my code, let me know if there is something more I should share from it to shed light on the problem. Thanks in advance.
createProjectLoader.ts
import DataLoader from "dataloader";
import { Project } from "../entities/Project";
export const createProjectLoader = () => new DataLoader<number, Project>(async projectIds => {
const projects = await Project.findByIds(projectIds as number[]);
const projectIdToProject: Record<number, Project> = {};
projects.forEach(p => {
projectIdToProject[p.id] = p;
});
return projectIds.map((projectId) => projectIdToProject[projectId])
})
resolvers/post.ts
@FieldResolver(() => Project, { nullable: true })
project(
@Root() post: Post,
@Ctx() { projectLoader }: MyContext
) {
return post.projectId ? projectLoader.load(post.projectId) : undefined
}
...
@Query(() => PaginatedPosts)
async posts(
@Arg('limit', () => Int) limit: number,
@Arg('cursor', () => String, { nullable: true }) cursor: string | null,
): Promise<PaginatedPosts> {
const realLimit = Math.min(20, limit);
const realLimitPlusOne = realLimit + 1;
const replacements: any[] = [realLimitPlusOne];
if (cursor) {
replacements.push(new Date(parseInt(cursor)));
}
const posts = await getConnection().query(`
select p.*
from post p
${cursor ? `where p."createdAt" < $2` : ""}
order by p."createdAt" DESC
limit $1
`,
replacements
)
return {
posts: posts.slice(0, realLimit),
hasMore: posts.length === realLimitPlusOne
};
}
createUrqlClient.ts
const cursorPagination = (): Resolver => {
return (_parent, fieldArgs, cache, info) => {
const { parentKey: entityKey, fieldName } = info;
const allFields = cache.inspectFields(entityKey);
const fieldInfos = allFields.filter((info) => info.fieldName === fieldName);
const size = fieldInfos.length;
if (size === 0) {
return undefined
}
const fieldKey = `${fieldName}(${stringifyVariables(fieldArgs)})`
const isItInTheCache = cache.resolve(
cache.resolveFieldByKey(entityKey, fieldKey) as string,
'posts'
);
info.partial = !isItInTheCache;
let hasMore = true;
const results: string[] = [];
fieldInfos.forEach(fi => {
const key = cache.resolveFieldByKey(entityKey, fi.fieldKey) as string;
const data = cache.resolve(key, 'posts') as string[];
const _hasMore = cache.resolve(key, 'hasMore');
if (!_hasMore) {
hasMore = _hasMore as boolean;
}
results.push(...data);
})
return {
__typename: 'PaginatedPosts',
hasMore,
posts: results
}
}
}