Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempting to pass in a query to a paginate function with proper types #197

Open
schwartzmj opened this issue May 30, 2023 · 5 comments
Open

Comments

@schwartzmj
Copy link

schwartzmj commented May 30, 2023

Edit: Does something like the runPaginatedQuery wrapper function at the end of this post make more sense?

I'm playing around with turning something like the following into a function called e.g. paginateQuery. Essentially the idea being that you'd build up a query with whatever filters you want, pass it into the paginateQuery function, and get paginated data out of it.

Essentially everything under const queryBuilder = q('*').filter(filter); would be in its own function, so then I could re-use this for other queries.

I'm not exactly a TypeScript guru, so the biggest problem I'm running into is How would I write a function that accepts a groqd array query? (while also passing through the proper types of course). It appears that a lot of the types I may need are not exported from the groqd package.

I've played around with a few different ways to accomplish this and am a bit stuck.

If there's a simpler way to do this that I'm not thinking of, I'm all ears. Thank you in advance for any ideas you have!

// inside a `getPosts` function ...
let filter = '_type == "post"';

if (search) {
  params.search = search.toLowerCase();
  filter += ' && title match "*" + lower($search) + "*"';
}

if (slug) {
  params.slug = slug;
  filter += ' && category->slug.current == $slug';
}
const queryBuilder = q('*').filter(filter);

 // Ideally this one line replaces everything below
 // const paginatedQuery = paginateQuery(queryBuilder, pageIndex, pageSize)

const totalCountQuery = q(`count(${queryBuilder.query})`);

const result = await runQuery(
  q('').grab({
	  posts: queryBuilder
		  .order(`publishDate desc`)
		  .slice(pageIndex * PAGE_SIZE, pageIndex * PAGE_SIZE + PAGE_SIZE - 1)
		  .grab(POST_PREVIEW),
	  totalCount: totalCountQuery
  }),
  params
);


const posts = result.posts;
const totalCount = result.totalCount as number;

return createPaginationDto({
  items: posts,
  totalCount,
  page,
  pageSize: PAGE_SIZE
});

 // ... end `getPosts` function



// createPaginationDto func for reference
export function createPaginationDto<T>({
	items,
	totalCount,
	page,
	pageSize
}: {
	items: T[];
	totalCount: number;
	page: number;
	pageSize: number;
}): PaginationDto<T> {
	const totalPages = Math.ceil(totalCount / pageSize);
	return {
		items,
		page,
		pageSize,
		totalPages,
		totalCount,
		hasPrevious: page > 1,
		hasNext: page < totalPages,
		from: (page - 1) * pageSize + 1,
		to: (page - 1) * pageSize + items.length
	};
}

Edit: I also started going down the route of creating a new runQuery function but had to move on to something else for a bit. Testing quickly, it seems to work but obviously I'm losing my type safety.

export const runQuery = makeSafeQueryRunner((query, params: Record<string, unknown> = {}) =>
	sanityClient.fetch(query, params)
);

export const runPaginatedQuery = async (
	query: any,
	params: Record<string, unknown> = {},
	{ pageIndex, pageSize }: { pageIndex: number; pageSize: number }
) => {
	const totalCountQuery = q(`count(${query.query})`);
	const { items, totalCount } = await runQuery(
		q('').grab({
			items: query.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize - 1),
			totalCount: totalCountQuery
		}),
		params
	);
	return createPaginationDto({
		items,
		totalCount: totalCount as number,
		page: pageIndex + 1,
		pageSize
	});
};
@littlemilkstudio
Copy link
Contributor

👋 hey @schwartzmj . There's a couple ways to do this. Either approach you have could work with a couple updates. It's hard to comment on which of the two might be better, because I think that's dependent on the broader architecture of your application.

As for this question:

How would I write a function that accepts a groqd array query?

You can extract out some helpful types for this through typescripts typeof operator. basically can do something like this:

// This isn't currently exposed through groqd,
// but you can extract it like this
const unknownArrayQuery = q('').filter('')
type UnknownArrayQuery = typeof unknownArrayQuery

In groqd's current state, we don't assign much of a type to anything until the .grab() call. So you can compose a function like this and avoid generics entirely while still getting proper types:

import { q, type InferType } from 'groqd'

const unknownArrayQuery = q('').filter('')
type UnknownArrayQuery = typeof unknownArrayQuery

function someQueryOperation(query: UnknownArrayQuery){
  return query.slice(0, 2).grab({
    foo: q.string(),
    bar: q.number()
  })
}

const someQuery = q('*').filterByType('post')
type SomeQuery = InferType<typeof someQuery>
// ^? unknown[]

const alteredQuery = someQueryOperation(someQuery)
type AlteredQuery = InferType<typeof alteredQuery>
// ^? { foo: string; bar: number }[]

I created a paginate() function in this arcade example that should hopefully mirror your initial solutions close enough to give you what you need to get goin.

@schwartzmj
Copy link
Author

schwartzmj commented Jul 21, 2023

@littlemilkstudio Thank you for taking the time to help!

Extracting types makes sense. The only issue I'm running into with the arcade example is that the "grab" and resulting type is hardcoded in the paginate function. I'd like to have a re-usable paginate function with a return value more like this:

type PaginationDto<T> = {
	items: T[];
	page: number;
	pageSize: number;
	totalPages: number;
	totalCount: number;
	hasPrevious: boolean;
	hasNext: boolean;
	from: number;
	to: number;
};

It seems like I need a generic like ArrayQuery<FromSelection<T>> (instead of UnknownArrayQuery) in order to properly type the items value.

@littlemilkstudio
Copy link
Contributor

littlemilkstudio commented Jul 24, 2023

@schwartzmj Ahh I see. yeaup, would something like this work?

import { q, type Selection } from "groqd";

const unknownArrayQuery = q('').filter('')
type UnknownArrayQuery = typeof unknownArrayQuery

function paginate<T extends Selection>(query: UnknownArrayQuery, selection: T, pageIndex: number, pageSize: number){
  return q('').grab({
    posts: query
      .grab(selection)
      .slice(pageIndex * pageSize, pageIndex * pageSize + pageSize - 1),

    totalCount: [`count(${query.query})`, q.number()]
  })
}

@schwartzmj
Copy link
Author

@schwartzmj Ahh I see. yeaup, would something like this work?

import { q, type Selection } from "groqd";

const unknownArrayQuery = q('').filter('')
type UnknownArrayQuery = typeof unknownArrayQuery

function paginate<T extends Selection>(query: UnknownArrayQuery, selection: T, pageIndex: number, pageSize: number){
  return q('').grab({
    posts: query
      .grab(selection)
      .slice(pageIndex * pageSize, pageIndex * pageSize + pageSize - 1),

    totalCount: [`count(${query.query})`, q.number()]
  })
}

That might be it! I'll have to do a little refactoring but will test it out. Thank you!

@littlemilkstudio
Copy link
Contributor

Sounds good. I think there could be more resources/references around passing queries to functions & relevant types. I'll be sure to keep this example in mind. Thank you for posting!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants