prisma-no-offset
v1.1.5
Published
This package is a lightweight utility package that helps you conveniently use no-offset paging when using prisma orm.
Downloads
28
Maintainers
Readme
prisma-no-offset
Supports both no-offset paging for ascending and descending order.
Notice
Built for developers using prisma and nestjs. However, even without using nestjs, examples can be applied to most backend/full stack frameworks.
Contents
Why I made this?
- Prisma has the grammar of
cursor
, but you have to use the codeskip: 1
together to use it. - But this code skips the first data even when there is no lastId,
- So
skip: lastId ? 1 : 0, ...(lastId && {cursor: { id: lastId }})
- it will work normally only when these codes are added.
- Because of this inconvenience, a library was created to allow simple curser-based paging.
//before - prisma provides
findMany({
take: 10,
skip: lastId ? 1 : 0,
...(lastId && { cursor: { id: lastId } }),
});
//after - use prisma-no-offset
findMany({
where: ltLastIdCondition(lastId),
take: 10,
});
intro
- This package is a lightweight utility package that helps you conveniently use no-offset paging(called cursor based, infinite scroll or keyset pagination) when using prisma orm.
- The package contains a lastId constant to be used as a query string and a default value constant for lastId,
- a conditional query function(usually called ltLastId) that filters data with an id less than the last id in descending order,
- a conditional query function(usally called gtLastId) that filters data with an id greater than the last id in ascending order,
- a function that extracts the last id from the current query data,
- and a code that casts it as a string type when the bigint type is serialized as json.
- Because of the bigint type, the package will be available starting in es2020.
- If you check the actual code in index.ts, you can see the same annotation as the document.
- If you are using ascending sort, look at the
gtLastIdCondition
document. - If you are using descending sort, look at the
ltLastIdCondition
document. - 이 패키지는 prisma orm에서 편리하게 no-offset 페이징(무한 스크롤, 커서 기반 페이징이라 불리기도 한다)을 사용할 수 있도록 도와주는 유틸 패키지입니다.
- 이 패키지는 쿼리스트링으로 사용될 lastId 상수와 lastId의 기본값 상수,
- 내림차순 정렬에서 사용되는 last id보다 작은 id를 가진 데이터를 필터링하는 조건절 쿼리 함수와
- 오름차순 정렬에서 사용되는 last id보다 큰 id를 가진 데이터를 필터링 하는 조건절 쿼리 함수와
- 조회한 데이터에서 last id를 추출하는 함수,
- 마지막으로 bigint타입이 json으로 직렬화 될 때, 문자타입(string)으로 자동 캐스팅 시켜주는 코드가 들어있습니다.
- bigint타입 때문에 es2020부터 해당 패키지를 사용하실 수 있습니다.
- index.ts 파일의 실제코드를 확인하시면 문서와 똑같은 주석을 확인하실 수 있습니다.
docs: ENG
Bigint to string in Json serialization
- This code requires definition only.
- When converted to json, it is cast and returned as a string.
ltLastIdCondition - less than last id filtering fuction
- You must use this function when sorting data in descending order.
- This function is a utility function used to create a clause in the no offset paging based on the descending order of id.
- The id must be set to the bigint type.
- Returns an empty query if the lastId is null, 0 or less than 0(less than & euqal == lte). prisma ignores this empty query.
- If a normal lastId comes in, this function looks for id less than lastId.
- When you use this utility function in a single condition, you just need to call the function.
- However, when using multiple conditions, it is desirable to filter the necessary conditions and then call the function as the last condition.
- Because if there's a lot of data in the database and the current page is a relatively recent page,
- Calling this function first is not at all efficient because all data that meet conditions less than id are filtered first.
//single condition
where: ltLastIdCondition(lastId)
//multiple condition
where: { AND: [{ column: agrs }, ltLastIdCondition(lastId)], },
gtLastIdCondition - greater than last id filtering fuction
- You must use this function when sorting data in ascending order.
- This function is a utility function used to create a clause in the no offset paging based on the ascending order of id.
- The id must be set to the bigint type.
- Returns an empty query if the lastId is null, 0 or less than 0(less than & euqal == lte). prisma ignores this empty query.
- If a normal lastId comes in, this function looks for id less than lastId.
- When you use this utility function in a single condition, you just need to call the function.
- However, when using multiple conditions, it is desirable to filter the necessary conditions and then call the function as the last condition.
- Because if there's a lot of data in the database and the current page is a relatively recent page,
- Calling this function first is not at all efficient because all data that meet conditions greater than id are filtered first.
//single condition
where: gtLastIdCondition(lastId)
//multiple condition
where: { AND: [{ column: agrs }, gtLastIdCondition(lastId)], },
findLastIdOrDefault - find last id in found data
- This function finds and returns the lastId among the currently searched data.
- The result value of this function is used to insert the last id into the paging metadata.
- It is not mandatory to use this function, but you can use it if you want to insert the last id of the data you currently inquired into the metadata.
- If an empty array comes in as a parameter, return 0 as the last id.
- You can put any type of data as a parameter, which means it can be used in any domain.
metadata: { lastId: findLastIdOrDefault(posts) },
docs: KOR
Bigint json 직렬화시 string으로 캐스팅 코드
- 이 코드는 정의만 필요로 합니다. 직접 사용할 일은 없을 겁니다.
- json으로 직렬화 시 문자열로 캐스팅되어 리턴합니다. 클라이언트에게 bigint타입을 리턴시 string으로 리턴됩니다.
ltLastIdCondition - last id보다 작은 id 필터링 함수
- 내림차순 정렬을 사용하고 있다면 반드시 이 함수를 사용해야합니다.
- 이 함수는 id 내림차순 기반의 No offset 페이징의 where절을 만드는데 사용하는 유틸함수입니다.
- id는 반드시 bigint 타입으로 구성해야합니다.
- lastId가 null, 0 혹은 0보다 작을경우 빈 쿼리를 리턴합니다. prisma는 이 빈쿼리를 무시합니다.
- 정상적인 lastId가 들어온다면, lastId 보다 작은 id를 기준으로 찾도록 합니다.
- 이 유틸함수를 단일 조건에서 사용할 때는 큰 문제없이 함수를 호출하면됩니다.
- 그러나 여러 조건에서 사용할 때에는 필요한 조건들을 필터링한 후, 맨 마지막 조건으로 함수를 호출하는 것이 바람직합니다.
- 왜냐하면 많은 양의 데이터가 존재할 경우 현재 페이지가 비교적 최근 페이지라면,
- 이 함수를 먼저 호출하게되면 id보다 작은 조건에 부합하는 모든 데이터가 먼저 필터링 되기 때문에 전혀 효율적이지 않습니다.
//단일 조건
where: ltLastIdCondition(lastId)
//다중 조건
where: { AND: [{ column: agrs }, ltLastIdCondition(lastId)], },
gtLastIdCondition - last id보다 큰 id 필터링 함수
- 오름차순 정렬을 사용하고 있다면 반드시 이 함수를 사용해야합니다.
- 이 함수는 id 오름차순 기반의 No offset 페이징의 where절을 만드는데 사용하는 유틸함수입니다.
- id는 반드시 bigint 타입으로 구성해야합니다.
- lastId가 null, 0 혹은 0보다 작을경우 빈 쿼리를 리턴합니다. prisma는 이 빈쿼리를 무시합니다.
- 정상적인 lastId가 들어온다면, lastId 보다 큰 id를 기준으로 찾도록 합니다.
- 이 유틸함수를 단일 조건에서 사용할 때는 큰 문제없이 함수를 호출하면됩니다.
- 그러나 여러 조건에서 사용할 때에는 필요한 조건들을 필터링한 후, 맨 마지막 조건으로 함수를 호출하는 것이 바람직합니다.
- 왜냐하면 많은 양의 데이터가 존재할 경우 현재 페이지가 비교적 최근 페이지라면,
- 이 함수를 먼저 호출하게되면 id보다 큰 조건에 부합하는 모든 데이터가 먼저 필터링 되기 때문에 전혀 효율적이지 않습니다.
//단일 조건
where: gtLastIdCondition(lastId)
//다중 조건
where: { AND: [{ column: agrs }, gtLastIdCondition(lastId)], },
findLastIdOrDefault - 조회한 데이터에서 last id를 찾는 함수
- 이 함수는 현재 조회한 데이터 중 lastId를 찾아서 리턴하는 함수입니다.
- 이 함수의 결과값은 페이징 meta data에 last id를 삽입하는데 사용됩니다.
- 이 함수를 사용하는 것은 필수는 아니나, 현재 조회한 데이터 중 last id를 meta data에 삽입하고 싶은 경우 사용하면 됩니다.
- 만약 빈 배열이 매개변수로 들어왔다면 last id로 0을 리턴합니다.
- 매개변수로는 어떤 타입의 데이터라도 넣을 수 있습니다. 즉 모든 도메인에서 사용가능합니다.
metadata: { lastId: findLastIdOrDefault(posts) },
Example - ENG
Dto/VO
- Please make and use the vo yourself.
- Note!! : Unlike class, interface is removed at runtime.
- Therefore, it is not appropriate with a request dto.
- However, I used it because there was no problem with a vo(response dto).
- Use the dto/vo according to your preference, but use the structure recommended by your company and official document.
- Also, if you want to unify the dto/vo structure, use the same class as the request dto.
//PostPage.ts
export interface PostPage {
readonly id: bigint;
readonly title: string;
readonly writer_id: string;
readonly created_date: Date;
}
//PostOptimizedPageVo.ts
export interface PostOptimizedPageVo {
readonly postPages: PostPage[];
readonly metadata: {
readonly lastId: bigint;
};
}
Repository
- Single Condition & descending order
async findAllOptimizedPostPage(
lastId: bigint,
): Promise<PostOptimizedPageVo> {
//call ltLastIdCondition function
const lastIdCondition = ltLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
where: lastIdCondition,
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'desc' },
take: PostRepoConstant.PAGE_SIZE, //The limit page size must be specified. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//I inserted the last id in the metadata.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
- Multiple Condition & descending order
async findOptimizedPostPageByWriterId(
writerId: string,
lastId: bigint,
): Promise<PostOptimizedPageVo> {
const lastIdCondition = ltLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
//Multiple conditions use and queries.
//As described in the description, ltLastId is most efficient to use as the last condition.
where: {
AND: [{ writer_id: writerId }, lastIdCondition],
},
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'desc' },
take: PostRepoConstant.PAGE_SIZE, //The limit page size must be specified. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//I inserted the last id in the metadata.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
- Single Condition & ascending order
async findAllOptimizedPostPage(
lastId: bigint,
): Promise<PostOptimizedPageVo> {
//call gtLastIdCondition function
const lastIdCondition = gtLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
where: lastIdCondition,
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'asc' },
take: PostRepoConstant.PAGE_SIZE, //The limit page size must be specified. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//I inserted the last id in the metadata.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
- Multiple Condition & ascending order
async findOptimizedPostPageByWriterId(
writerId: string,
lastId: bigint,
): Promise<PostOptimizedPageVo> {
const lastIdCondition = gtLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
//Multiple conditions use and queries.
//As described in the description, gtLastId is most efficient to use as the last condition.
where: {
AND: [{ writer_id: writerId }, lastIdCondition],
},
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'asc' },
take: PostRepoConstant.PAGE_SIZE, //The limit page size must be specified. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//I inserted the last id in the metadata.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
- In case you receive a query string optionally, it is also available in the optional query string.
- Same as usual, the only difference is to declare optional in the parameter.
async findAllOptimizedPostPageOptionalQueryString(
lastId?: bigint,
): Promise<PostOptimizedPageVo> {
//call ltLastIdCondition function
const lastIdCondition = ltLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
where: lastIdCondition,
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'desc' },
take: PostRepoConstant.PAGE_SIZE, //The limit page size must be specified. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//I inserted the last id in the metadata.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
Service
async getAllOptimizedPostPage(lastId: bigint) {
return await this.postRepository.findAllOptimizedPostPage(lastId);
}
async getAllOptimizedPostPageWithOptionalQueryString(lastId?: bigint) {
return await this.postRepository.findAllOptimizedPostPageOptionalQueryString(lastId);
}
async getOptimizedPostPageByWriterId(writerId: string, lastId: bigint) {
return await this.postRepository.findOptimizedPostPageByWriterId(
writerId,
lastId,
);
}
Controller
- You must receive a query string named 'last-id'.
- For the first page, the client does not need to use lastId in the query string.
- Instead, set 0 as the default value for last id. In this package, last id=0 means the first page.
- In case the default value provided by the library is not used in the constant, but is entered as optional, it is a better code style to receive explicit parameters in case there is no value than receiving implicit parameters.
- In the restful design, api is expressed in lowercase and '-' is specified if necessary.
- Therefore, it is correct to express the query string in 'last-id' rather than 'lastId'.
- However, if you are expressing last id in a different way depending on company rules or personal circumstances, use a self-made constant rather than the one provided by the library.
// uri : /posts?last-id={lastId}
@Get('posts')
async allPosts(
//LAST_ID = last-id
//DEFAULT_LAST_ID = BigInt(0)
@Query(LAST_ID) lastId: bigint = DEFAULT_LAST_ID,
) {
return this.postService.getAllOptimizedPostPage(lastId);
}
//optional query string
// uri : /posts-optional?last-id={lastId}
@Get('posts-optional')
async allPosts(
//LAST_ID = last-id, that is optional value
@Query(LAST_ID) lastId?: bigint,
) {
return this.postService.getAllOptimizedPostPageOptionalQueryString(lastId);
}
// uri : /posts?writer-id={writerId}&last-id={lastId}
@Get('posts')
async myPosts(
@Query('writer-id') writerId: string,
//LAST_ID = last-id
//DEFAULT_LAST_ID = BigInt(0)
@Query(LAST_ID) lastId: bigint = DEFAULT_LAST_ID,
) {
return await this.postService.getOptimizedPostPageByWriterId(
writerId,
lastId,
);
}
Example - KOR
Dto/VO
- 본인이 직접 만든 dto/vo를 사용하길 바랍니다.
- Note!! : interface는 class와 달리 런타임단계에서 제거됩니다.
- 따라서 request dto로는 적절하지 않습니다.
- 그러나 필자가 볼때 vo(response dto)로는 문제가 없어서 사용하였습니다.
- dto/vo는 기호에 맞게 사용하되, 본인의 회사와 공식문서에서 권장하는 구조를 사용하시기 바랍니다.
- 또한 dto/vo 구조의 통일을 원한다면 request dto와 동일하게 class를 사용하시기 바랍니다.
//PostPage.ts
export interface PostPage {
readonly id: bigint;
readonly title: string;
readonly writer_id: string;
readonly created_date: Date;
}
//PostOptimizedPageVo.ts
export interface PostOptimizedPageVo {
readonly postPages: PostPage[];
readonly metadata: {
readonly lastId: bigint;
};
}
Repository
- 단일 조건 & 내림차순 정렬
async findAllOptimizedPostPage(
lastId: bigint,
): Promise<PostOptimizedPageVo> {
//ltLastIdCondition 함수 호출
const lastIdCondition = ltLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
where: lastIdCondition,
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'desc' },
take: PostRepoConstant.PAGE_SIZE, //리밋 페이지 사이즈는 기호에 맞게 반드시 정의하시기 바랍니다. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//필자의 경우 last id를 meta data에 삽입하였습니다.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
- 다중 조건 & 내림차순 정렬
async findOptimizedPostPageByWriterId(
writerId: string,
lastId: bigint,
): Promise<PostOptimizedPageVo> {
const lastIdCondition = ltLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
//다중 조건에서는 and 쿼리를 이용합니다.
//설명에서 기술했듯이 ltLastId는 맨 마지막 조건으로 사용하는 것이 가장 효율적입니다.
where: {
AND: [{ writer_id: writerId }, lastIdCondition],
},
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'desc' },
take: PostRepoConstant.PAGE_SIZE, //리밋 페이지 사이즈는 기호에 맞게 반드시 정의하시기 바랍니다. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//필자의 경우 last id를 meta data에 삽입하였습니다.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
- 단일 조건 & 오름차순 정렬
async findAllOptimizedPostPage(
lastId: bigint,
): Promise<PostOptimizedPageVo> {
//gtLastIdCondition 함수 호출
const lastIdCondition = gtLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
where: lastIdCondition,
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'asc' },
take: PostRepoConstant.PAGE_SIZE, //리밋 페이지 사이즈는 기호에 맞게 반드시 정의하시기 바랍니다. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//필자의 경우 last id를 meta data에 삽입하였습니다.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
- 다중 조건 & 오름차순 정렬
async findOptimizedPostPageByWriterId(
writerId: string,
lastId: bigint,
): Promise<PostOptimizedPageVo> {
const lastIdCondition = gtLastIdCondition(lastId);
const posts: PostPage[] = await this.prisma.post.findMany({
//다중 조건에서는 and 쿼리를 이용합니다.
//설명에서 기술했듯이 gtLastId는 맨 마지막 조건으로 사용하는 것이 가장 효율적입니다.
where: {
AND: [{ writer_id: writerId }, lastIdCondition],
},
select: { id: true, title: true, writer_id: true, created_date: true },
orderBy: { id: 'asc' },
take: PostRepoConstant.PAGE_SIZE, //리밋 페이지 사이즈는 기호에 맞게 반드시 정의하시기 바랍니다. PostRepoConstant.PAGE_SIZE = 10
});
return {
postPages: posts,
//필자의 경우 last id를 meta data에 삽입하였습니다.
metadata: { lastId: findLastIdOrDefault(posts) },
};
}
Service
- optional 파라미터를 허용합니다. optional의 경우
?
만 붙여주면 optional로 사용할 수 있습니다.
async getAllOptimizedPostPage(lastId: bigint) {
return await this.postRepository.findAllOptimizedPostPage(lastId);
}
async getAllOptimizedPostPageWithOptionalQueryString(lastId?: bigint) {
return await this.postRepository.findAllOptimizedPostPageOptionalQueryString(lastId);
}
async getOptimizedPostPageByWriterId(writerId: string, lastId: bigint) {
return await this.postRepository.findOptimizedPostPageByWriterId(
writerId,
lastId,
);
}
Controller
- lastId 쿼리스트링을 반드시 받아야합니다.
- 첫번째 페이지의 경우 클라이언트는 lastId를 쿼리스트링에 사용하지 않아도 됩니다.
- 대신 last id의 기본값으로 0을 설정해줍니다. 이 패키지에서 last id=0의 의미는 첫번째 페이지라는 의미를 지닙니다.
- 라이브러리에서 제공하는 상수를 사용하지 않고, optional 쿼리 스트링을 입력받을 수 있습니다.
- 그러나 암묵적인 매개변수보다는 명시적인 default value를 선언하는 것이 보다 좋은 코드 스타일입니다.
- 따라서 라이브러리에서는 optional 쿼리스트링 보다는 명시적인 default value를 추천합니다.
- restful한 설계에서는 소문자로 api를 표현하며, 필요한 경우
-
를 사용하도록 명시되있습니다. - 이에 따라 last id의 쿼리스트링은 'lastId'로 표현하는 것보다 'last-id'로 표현하는 것이 올바릅니다.
- 다만 사내 규칙이나 개인적인 상황에 따라 다른 방법으로 last id를 표현하는 경우, 라이브러리에서 제공하는 상수가 아닌 직접 제작한 상수를 사용하십시오.
// uri : /posts
@Get('posts')
async allPosts(
//LAST_ID = last-id
//DEFAULT_LAST_ID = BigInt(0)
@Query(LAST_ID) lastId: bigint = DEFAULT_LAST_ID,
) {
return this.postService.getAllOptimizedPostPage(lastId);
}
//optional 쿼리 스트링 사용 예제
// uri : /posts-optional?last-id={lastId}
@Get('posts-optional')
async allPosts(
//LAST_ID = lastId
@Query(LAST_ID) lastId?: bigint
) {
return this.postService.getAllOptimizedPostPageOptionalQueryString(lastId);
}
// uri : /posts?writer-id={writerId}&last-id={lastId}
@Get('posts')
async myPosts(
@Query('writer-id') writerId: string,
//LAST_ID = last-id
//DEFAULT_LAST_ID = BigInt(0)
@Query(LAST_ID) lastId: bigint = DEFAULT_LAST_ID,
) {
return await this.postService.getOptimizedPostPageByWriterId(
writerId,
lastId,
);
}