chore: initial commit
This commit is contained in:
110
src/tools/database/__test__/paginateModel.test.ts
Normal file
110
src/tools/database/__test__/paginateModel.test.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { Sequelize } from 'sequelize-typescript'
|
||||
import sqlite3 from 'sqlite3'
|
||||
import { open, Database } from 'sqlite'
|
||||
|
||||
import { paginateModel } from '../paginateModel'
|
||||
import { BadRequestError } from '../../errors/BadRequestError'
|
||||
import PostTest from './utils/PostTest'
|
||||
import { createPosts } from './utils/createPosts'
|
||||
|
||||
let sqlite: Database | undefined
|
||||
let sequelize: Sequelize | undefined
|
||||
|
||||
describe('/tools/database/paginateModel', () => {
|
||||
beforeAll(async () => {
|
||||
sqlite = await open({
|
||||
filename: ':memory:',
|
||||
driver: sqlite3.Database
|
||||
})
|
||||
sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
logging: false,
|
||||
models: [PostTest]
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await sequelize?.sync({ force: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await sqlite?.close()
|
||||
await sequelize?.close()
|
||||
})
|
||||
|
||||
it('fetch a certain amount of rows', async () => {
|
||||
const numberOfPosts = 21
|
||||
await createPosts(numberOfPosts)
|
||||
const result = await paginateModel({ Model: PostTest })
|
||||
expect(result.hasMore).toBeTruthy()
|
||||
expect(result.rows.length).toEqual(20)
|
||||
expect(result.totalItems).toEqual(numberOfPosts)
|
||||
})
|
||||
|
||||
it('fetch less than 20 itemsPerPage', async () => {
|
||||
const numberOfPosts = 15
|
||||
await createPosts(numberOfPosts)
|
||||
const result = await paginateModel({ Model: PostTest })
|
||||
expect(result.hasMore).toBeFalsy()
|
||||
expect(result.rows.length).toEqual(numberOfPosts)
|
||||
expect(result.totalItems).toEqual(numberOfPosts)
|
||||
})
|
||||
|
||||
it('fetch more than 20 itemsPerPage', async () => {
|
||||
const numberOfPosts = 30
|
||||
const itemsPerPage = '25'
|
||||
await createPosts(numberOfPosts)
|
||||
const result = await paginateModel({
|
||||
Model: PostTest,
|
||||
queryOptions: { itemsPerPage }
|
||||
})
|
||||
expect(result.hasMore).toBeTruthy()
|
||||
expect(result.rows.length).toEqual(parseInt(itemsPerPage))
|
||||
expect(result.totalItems).toEqual(numberOfPosts)
|
||||
expect(result.itemsPerPage).toEqual(Number(itemsPerPage))
|
||||
})
|
||||
|
||||
it('throws "BadRequestError" if "itemsPerPage" is more than 100', async () => {
|
||||
const numberOfPosts = 10
|
||||
const itemsPerPage = '101'
|
||||
await createPosts(numberOfPosts)
|
||||
await expect(
|
||||
paginateModel({ Model: PostTest, queryOptions: { itemsPerPage } })
|
||||
).rejects.toThrow(BadRequestError)
|
||||
})
|
||||
|
||||
it('goes to the next page', async () => {
|
||||
let page = 1
|
||||
const numberOfPosts = 100
|
||||
const itemsPerPage = '30'
|
||||
const itemsPerPageInt = parseInt(itemsPerPage)
|
||||
await createPosts(numberOfPosts)
|
||||
const result1 = await paginateModel({
|
||||
Model: PostTest,
|
||||
queryOptions: { itemsPerPage, page: page.toString() },
|
||||
findOptions: {
|
||||
order: [['id', 'ASC']]
|
||||
}
|
||||
})
|
||||
page += 1
|
||||
expect(result1.hasMore).toBeTruthy()
|
||||
expect(result1.rows[itemsPerPageInt - 1].title).toEqual(
|
||||
`title-${itemsPerPage}`
|
||||
)
|
||||
expect(result1.totalItems).toEqual(numberOfPosts)
|
||||
const result2 = await paginateModel({
|
||||
Model: PostTest,
|
||||
queryOptions: { itemsPerPage, page: page.toString() },
|
||||
findOptions: {
|
||||
order: [['id', 'ASC']]
|
||||
}
|
||||
})
|
||||
expect(result2.page).toEqual(page)
|
||||
expect(result2.hasMore).toBeTruthy()
|
||||
expect(result2.rows[itemsPerPageInt - 1].title).toEqual(
|
||||
`title-${itemsPerPageInt * 2}`
|
||||
)
|
||||
expect(result2.totalItems).toEqual(numberOfPosts)
|
||||
})
|
||||
})
|
10
src/tools/database/__test__/utils/PostTest.ts
Normal file
10
src/tools/database/__test__/utils/PostTest.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Table, Model, Column, DataType } from 'sequelize-typescript'
|
||||
|
||||
@Table
|
||||
export default class PostTest extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false
|
||||
})
|
||||
title!: string
|
||||
}
|
9
src/tools/database/__test__/utils/createPosts.ts
Normal file
9
src/tools/database/__test__/utils/createPosts.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import PostTest from './PostTest'
|
||||
|
||||
export const createPosts = async (
|
||||
numberOfPostsToCreate: number
|
||||
): Promise<void> => {
|
||||
for (let index = 1; index <= numberOfPostsToCreate; index++) {
|
||||
await PostTest.create({ title: `title-${index}` })
|
||||
}
|
||||
}
|
46
src/tools/database/paginateModel.ts
Normal file
46
src/tools/database/paginateModel.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { FindOptions, Model } from 'sequelize/types'
|
||||
|
||||
import { BadRequestError } from '../errors/BadRequestError'
|
||||
import { parseIntOrDefaultValue } from '../utils/parseIntOrDefaultValue'
|
||||
|
||||
interface PaginateModelOptions<M extends Model> {
|
||||
findOptions?: FindOptions
|
||||
queryOptions?: {
|
||||
page?: string
|
||||
itemsPerPage?: string
|
||||
}
|
||||
Model: typeof Model & (new () => M)
|
||||
}
|
||||
|
||||
/** Allows to make a pagination system on a Sequelize model instance */
|
||||
export const paginateModel = async <M extends Model<any, any>>(
|
||||
options: PaginateModelOptions<M>
|
||||
): Promise<{
|
||||
totalItems: number
|
||||
hasMore: boolean
|
||||
page: number
|
||||
itemsPerPage: number
|
||||
rows: M[]
|
||||
}> => {
|
||||
const {
|
||||
findOptions = {
|
||||
order: [['createdAt', 'DESC']]
|
||||
},
|
||||
queryOptions,
|
||||
Model
|
||||
} = options
|
||||
const page = parseIntOrDefaultValue(queryOptions?.page, 1)
|
||||
const itemsPerPage = parseIntOrDefaultValue(queryOptions?.itemsPerPage, 20)
|
||||
if (itemsPerPage > 100) {
|
||||
throw new BadRequestError('"itemsPerPage" should be less than 100')
|
||||
}
|
||||
const offset = (page - 1) * itemsPerPage
|
||||
const result = await Model.findAndCountAll<M>({
|
||||
limit: itemsPerPage,
|
||||
offset,
|
||||
...findOptions
|
||||
})
|
||||
const { count, rows } = result
|
||||
const hasMore = page * itemsPerPage < count
|
||||
return { page, itemsPerPage, totalItems: count, hasMore, rows }
|
||||
}
|
29
src/tools/database/sequelize.ts
Normal file
29
src/tools/database/sequelize.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import path from 'path'
|
||||
import { Sequelize } from 'sequelize-typescript'
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
host: process.env.DATABASE_HOST,
|
||||
database: process.env.DATABASE_NAME,
|
||||
dialect: process.env.DATABASE_DIALECT,
|
||||
storage: process.env.DATABASE_DIALECT === 'sqlite' ? ':memory:' : undefined,
|
||||
username: process.env.DATABASE_USER,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
port: parseInt(process.env.DATABASE_PORT ?? '3306', 10),
|
||||
models: [path.join(__dirname, '..', '..', 'models')],
|
||||
retry: {
|
||||
max: 10,
|
||||
match: [
|
||||
/ConnectionError/,
|
||||
/SequelizeConnectionError/,
|
||||
/SequelizeConnectionRefusedError/,
|
||||
/SequelizeHostNotFoundError/,
|
||||
/SequelizeHostNotReachableError/,
|
||||
/SequelizeInvalidConnectionError/,
|
||||
/SequelizeConnectionTimedOutError/,
|
||||
/SequelizeConnectionAcquireTimeoutError/,
|
||||
/Connection terminated unexpectedly/
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
export { sequelize }
|
Reference in New Issue
Block a user