Conversion from svelte params to pagination query to search params

This commit is contained in:
2022-03-29 21:22:22 +02:00
parent 5f1c7e9804
commit 9846fab54c
10 changed files with 586 additions and 33 deletions

View File

@ -0,0 +1,98 @@
import { range } from 'ramda'
import { describe, expect, test } from 'vitest'
import { filterByPropContains, dropAndTake, filterAndCount } from './pagination'
describe('pagination', () => {
test('does not drop any items by default', () => {
const items = range(0, 100)
expect(dropAndTake({})(items)).toHaveLength(100)
})
test('limits out exact number of items', () => {
const items = range(0, 100)
expect(dropAndTake({ limit: 10 })(items)).toHaveLength(10)
expect(dropAndTake({ limit: 10 })(items)[0]).toBe(0)
expect(dropAndTake({ limit: 10 })(items)[9]).toBe(9)
})
test('offset is skipping a number of items from the front', () => {
const items = range(0, 100)
expect(dropAndTake({ offset: 10 })(items)).toHaveLength(90)
expect(dropAndTake({ offset: 10 })(items)[0]).toBe(10)
})
test('is able to combine limit and offset', () => {
const items = range(0, 100)
expect(dropAndTake({ offset: 10, limit: 10 })(items)).toHaveLength(10)
expect(dropAndTake({ offset: 10, limit: 10 })(items)[0]).toBe(10)
expect(dropAndTake({ offset: 10, limit: 10 })(items)[9]).toBe(19)
})
test('is able to filter by a field', () => {
const items = [
{
id: 1,
prop: ['yes'],
},
{
id: 2,
prop: ['yes', 'no'],
},
]
expect(filterByPropContains({ prop: 'no' })(items)).toHaveLength(1)
expect(filterByPropContains({ prop: 'no' })(items)[0].id).toBe(2)
expect(filterByPropContains({ prop: 'yes' })(items)[0].id).toBe(1)
expect(filterByPropContains({ prop: 'yes' })(items)).toHaveLength(2)
})
describe('is able to combine limit and offset while filtering by field', () => {
const items = [
{
id: 1,
prop: ['yes'],
},
{
id: 2,
prop: ['yes', 'no'],
},
{
id: 3,
prop: ['yes', 'no'],
},
]
test('combine all parameters', () => {
const result = filterAndCount({
offset: 1,
limit: 1,
filters: { prop: 'no' },
})(items)
expect(result.totalCount).toBe(2)
expect(result.items[0].id).toBe(3)
})
test('with 0 offset', () => {
const result = filterAndCount({
offset: 0,
limit: 1,
filters: { prop: 'no' },
})(items)
expect(result.totalCount).toBe(2)
expect(result.items[0].id).toBe(2)
})
test('without filter', () => {
const result = filterAndCount({ offset: 1, limit: 1 })(items)
expect(result.totalCount).toBe(3)
expect(result.items[0].id).toBe(2)
})
test('without any params', () => {
const result = filterAndCount({})(items)
expect(result.totalCount).toBe(3)
expect(result.items.length).toEqual(result.totalCount)
})
})
})

View File

@ -0,0 +1,44 @@
import { identity } from 'ramda'
import { flow, A } from '@mobily/ts-belt'
const { drop, take } = A
export interface PaginationQuery {
offset?: number
limit?: number
filters?: Record<string, string>
}
export interface PaginationResult<ItemType> {
items: ItemType[]
totalCount: number
}
export function dropAndTake<Item>({ offset = 0, limit = Infinity }) {
return flow(drop<Item>(offset), take<Item>(limit))
}
export function filterByPropContains<Item>(filters: Record<string, string>) {
return function (items: Item[]) {
return items.filter((item) => {
return Object.entries(filters).every(([fieldName, value]) =>
item[fieldName].includes(value)
)
})
}
}
export function filterAndCount<Item>({
filters,
...dropTakeParams
}: PaginationQuery) {
return function (items: Item[]) {
const filterFunction = filters
? filterByPropContains<Item>(filters)
: identity
const filteredItems = filterFunction(items)
return {
items: dropAndTake<Item>(dropTakeParams)(filteredItems),
totalCount: filteredItems.length,
}
}
}

View File

@ -0,0 +1,64 @@
import { describe, test, expect } from 'vitest'
import {
getPaginationQueryFromSearchParams,
getPaginationSearchParams,
parseParams,
} from './searchParams'
describe('convert search params', () => {
test('drop take params are not taken as filters', () => {
expect(
getPaginationQueryFromSearchParams(
new URLSearchParams('offset=2&limit=5')
)
).toEqual({ offset: 2, limit: 5 })
})
test('return empty paginationQuery if ', () => {
expect(getPaginationQueryFromSearchParams(new URLSearchParams(''))).toEqual(
{}
)
})
test('other than drop take params are moved to filters ', () => {
expect(
getPaginationQueryFromSearchParams(new URLSearchParams('tag=news'))
).toEqual({ filters: { tag: 'news' } })
})
test('offset and filter combined', () => {
expect(
getPaginationQueryFromSearchParams(
new URLSearchParams('offset=3&tag=news')
)
).toEqual({ offset: 3, filters: { tag: 'news' } })
})
})
describe('get search params', () => {
test('parse params', () => {
const params = 'tags/News/page/1'
expect(parseParams(params)).toEqual({ tags: 'News', page: '1' })
})
test('should parse values into searchParams for first page', () => {
const params = 'tags/News/page/1'
expect(getPaginationSearchParams(7, params).toString()).toEqual(
'limit=7&offset=0&tags=News'
)
})
test('should parse values into searchParams for third page', () => {
const params = 'tags/News/page/3'
expect(getPaginationSearchParams(7, params).toString()).toEqual(
'limit=7&offset=14&tags=News'
)
})
test('should return first page without any params specified', () => {
const params = ''
expect(getPaginationSearchParams(7, params).toString()).toEqual(
'limit=7&offset=0'
)
})
})

View File

@ -0,0 +1,46 @@
import { splitEvery } from 'ramda'
import type { PaginationQuery } from './pagination'
export function getPaginationQueryFromSearchParams(
searchParams: URLSearchParams
) {
return Array.from(searchParams).reduce<PaginationQuery>(
(acc, [key, value]) => {
const isDropTake = ['offset', 'limit'].includes(key)
if (isDropTake) {
return {
...acc,
[key]: Number(value),
}
}
return {
...acc,
filters: {
...acc.filters,
[key]: value,
},
}
},
{}
)
}
export function parseParams(params: string) {
const splittedParams = params.split('/')
if (splittedParams.length % 2 !== 0) {
return []
}
const splits = splitEvery(2, splittedParams)
return Object.fromEntries(splits)
}
/**
* Convert svelte `load` params into a `URLSearchParams` so they can be used to fetch endpoints with pagination queries
*/
export function getPaginationSearchParams(pageSize: number, params: string) {
const { page = 1, ...filters } = parseParams(params)
const offset = pageSize * (page - 1)
const limit = pageSize
return new URLSearchParams({ limit, offset, ...filters })
}

View File

@ -3,11 +3,11 @@
import type { LoadInput, LoadOutput } from '@sveltejs/kit/types/page'
export async function load({ fetch, url }: LoadInput): Promise<LoadOutput> {
const blogPostsResponse = await fetch(`/blog/articles`)
const blogPostsResponse = await fetch(`/blog/articles?limit=5`)
const blogPostsContent = await blogPostsResponse.json()
return {
props: {
latestPosts: take(5, blogPostsContent.posts),
latestPosts: blogPostsContent.posts.items,
// TODO Check if not bugged FIXME
segment: '',
},

View File

@ -1,10 +1,15 @@
<script lang="ts" context="module">
import { getPaginationSearchParams } from '$lib/pagination/searchParams'
/**
* @type {import('@sveltejs/kit').Load}
*/
export async function load({ fetch }) {
const articleResponse = await fetch(`/blog/articles`)
.then((r) => r.json())
export async function load({ fetch, params }) {
console.log('params', params)
const searchParams = getPaginationSearchParams(7, params.params)
console.log('searchpprsm', searchParams)
const articleResponse = await fetch(
`/blog/articles?${searchParams.toString()}`
).then((r) => r.json())
return { props: { posts: articleResponse.posts } }
}
@ -14,8 +19,9 @@
import ArticleFooter from '../../components/blog/ArticleFooter.svelte'
import { postListClass, seeAllClass } from './index.css'
import type { PostContent } from './_content'
import type { PaginationResult } from '$lib/pagination/pagination'
export let posts: PostContent[]
export let posts: PaginationResult<PostContent>
export let tagQuery: string
</script>
@ -23,7 +29,7 @@
<title>My blog @michalvankodev</title>
</svelte:head>
{#if posts.length === 0}
{#if posts.items.length === 0}
<p class="no-posts">You've found void in the space.</p>
{:else}
<h1>
@ -40,7 +46,7 @@
{/if}
{/if}
<ul class="post-list {postListClass}">
{#each posts as post}
{#each posts.items as post}
<li>
<article>
<header>

View File

@ -1,11 +1,18 @@
import { readdir, readFile } from 'fs'
import { promisify } from 'util'
import { basename } from 'path'
import { pipe, partial, prop, sortBy, reverse, filter } from 'ramda'
import { pipe, prop, sortBy, reverse, filter } from 'ramda'
import fm from 'front-matter'
import marked from 'marked'
import {
filterAndCount,
type PaginationQuery,
} from '$lib/pagination/pagination'
const { NODE_ENV } = process.env
// TODO remove ramda and migrate to ts-belt
// TODO Pagination component for routing
// TODO Tag filtering visualization
export interface PostAttributes {
layout: string
@ -22,7 +29,7 @@ export interface PostContent extends PostAttributes {
published: boolean
}
export async function getBlogListing(tag?: string) {
export async function getBlogListing(paginationQuery: PaginationQuery) {
const files = await promisify(readdir)(`_posts/blog/`, 'utf-8')
const filteredFiles = filterDevelopmentFiles(files)
@ -48,17 +55,11 @@ export async function getBlogListing(tag?: string) {
}
})
)
const filteredContents = pipe<
PostContent[],
PostContent[],
PostContent[],
PostContent[],
PostContent[]
>(
sortBy(prop('date')),
reverse,
const filteredContents = pipe(
sortBy<PostContent>(prop('date')),
(items) => reverse(items),
filter<typeof contents[0]>((article) => article.published),
partial(filterByTag, [tag])
filterAndCount(paginationQuery)
)(contents)
return filteredContents
@ -69,9 +70,3 @@ function filterDevelopmentFiles(files: string[]) {
? files
: files.filter((file) => !file.startsWith('dev-'))
}
function filterByTag(tag: string | undefined, contents: PostContent[]) {
return tag
? contents.filter((content) => content.tags.includes(tag))
: contents
}

View File

@ -1,11 +1,10 @@
import { getPaginationQueryFromSearchParams } from '$lib/pagination/searchParams'
import { getBlogListing } from './_content'
export async function get({ url: { searchParams } }) {
console.log('bloglistingparams', searchParams)
const paginationQuery = getPaginationQueryFromSearchParams(searchParams)
const filteredContents = await getBlogListing(paginationQuery)
//Regexp for getting an optional tag and a page from the params
const tag = undefined
const filteredContents = await getBlogListing(tag)
return {
status: 200,
body: {