Skip to content

Commit

Permalink
Merge pull request #1 from wappla/feature/improve-for-joined-tables
Browse files Browse the repository at this point in the history
Added typescript, replaced babel by swc and added support for column alias
  • Loading branch information
fromthemills authored May 3, 2024
2 parents 8bcb4b6 + ec2ceb6 commit 4cc57c6
Show file tree
Hide file tree
Showing 19 changed files with 5,767 additions and 4,591 deletions.
4 changes: 0 additions & 4 deletions babel.config.js

This file was deleted.

File renamed without changes.
2 changes: 1 addition & 1 deletion fixup
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ cat >dist/esm/package.json <<!EOF
{
"type": "module"
}
!EOF
!EOF
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
coverageDirectory: 'coverage',
globalSetup: './src/testingSetup.js',
globalSetup: './src/testingSetup.ts',
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
}
9,904 changes: 5,485 additions & 4,419 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dashdot/cursors",
"version": "0.3.0",
"version": "0.4.0",
"description": "A simple SQL and Mongo implementation of cursor based pagination",
"license": "MIT",
"files": [
Expand All @@ -15,31 +15,33 @@
},
"scripts": {
"lint": "eslint .",
"test": "jest",
"test:coverage": "jest --coverage",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"benchmark": "node -r esm benchmark.js",
"codecov": "codecov",
"build": "rm -rf dist/* && tsc -p tsconfig-esm.json && tsc -p tsconfig-cjs.json && ./fixup",
"prepare": "npm run build"
},
"dependencies": {},
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.16.10",
"@babel/preset-env": "^7.16.11",
"@dashdot/eslint-config-base": "^0.3.0",
"@dashdot/factory": "^0.1.4",
"@faker-js/faker": "^6.0.0-alpha.5",
"@dashdot/factory": "^0.2.2",
"@faker-js/faker": "^6.3.1",
"@swc/jest": "^0.2.36",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.7",
"@types/pg": "^8.11.5",
"benchmark": "^2.1.4",
"dotenv": "^16.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.0.0",
"esm": "^3.2.25",
"jest": "^27.4.7",
"jest": "^29.7.0",
"knex": "^1.0.2",
"pg": "^8.7.3",
"pg": "^8.11.5",
"thenby": "^1.3.4",
"ts-jest": "^29.1.2",
"typescript": "^4.5.5"
},
"eslintConfig": {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,19 @@ test('if \'cursorToSqlQuery\' return correct values when no cursor is provided.'
]
expect(query).toEqual(expectedResult)
})

test('if \'cursorToSqlQuery\' return correct values using joined column.', async () => {
const taskId = 1
const userId = 5
const taskName = 'task'
const joinedTaskUser = { taskId, userId, 'tasks.name': taskName }
const orderBy = { 'tasks.name': ASC}
const cursor = createCursor(joinedTaskUser, orderBy)
const query = cursorToSqlQuery(cursor, orderBy)
const expectedResult = [
'"tasks.name" > $1',
[taskName],
'"tasks.name" ASC',
]
expect(query).toEqual(expectedResult)
})
120 changes: 83 additions & 37 deletions src/__tests__/postgres.test.js → src/__tests__/postgres.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,30 @@
/* eslint-disable no-await-in-loop */
import by from 'thenby'
import faker from '@faker-js/faker'
import by from 'thenby'
import { Knex } from 'knex'
import {
ASC,
DESC,
createCursor,
cursorToSqlQuery,
ASC, DESC, createCursor, cursorToSqlQuery
} from '../index'
import {
createTestDatabase,
destroyTestDatabase,
KnexFactory,
getKnexConnection
getKnexConnection,
} from '../testing'
import UserFactory from '../factories/UserFactory'
import { TABLE_TASKS, TASK_STATUS_COMPLETED, TASK_STATUS_PENDING, tasks } from '../factories/TaskFactory'
import faker from '@faker-js/faker'

const TABLE_USERS = 'users'
const TOTAL_USERS = 10
const DATABASE_NAME = 'testing-postgres'
const TOTAL_USERS = 2
const TASKS_PER_USER = 2

// We use a random word so when the tests run in parallel they don't collide.
const DATABASE_NAME = faker.random.word()

const users = () => {
const knex = getKnexConnection()
const knex = getKnexConnection(DATABASE_NAME)
return knex(TABLE_USERS)
}

class UserFactory extends KnexFactory {
static get table() {
return users
}

static async make() {
return {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.unique(faker.internet.email),
}
}
}

const createSchema = async (knex) => {
const createSchema = async (knex: Knex | null) => {
if (knex == null) {
return
}
Expand All @@ -49,16 +36,26 @@ const createSchema = async (knex) => {
table.string('email').notNullable()
table.string('firstName')
table.string('lastName')
table.unique('email')
table.unique(['email'])
})
await knex.schema.createTable(TABLE_TASKS, (table) => {
table.increments().primary()
table.datetime('createdAt').defaultTo(now)
table.datetime('updatedAt').defaultTo(now)
table.string('name').notNullable()
table.enum('status', [TASK_STATUS_PENDING, TASK_STATUS_COMPLETED])
table.integer('userId').unsigned().notNullable()
table.foreign('userId').references(`${TABLE_USERS}.id`)
})
}

let knex = null
let knex = null as Knex | null

beforeAll(async () => {
knex = await createTestDatabase(DATABASE_NAME)
await createSchema(knex)
await UserFactory.create(TOTAL_USERS)
const users = await UserFactory.create(TOTAL_USERS)
await Promise.all(users.map(async (user) => await user.createTasks(TASKS_PER_USER)))
})

afterAll(async () => {
Expand All @@ -67,7 +64,7 @@ afterAll(async () => {

test('if simple ascending order by finds the correct users.', async () => {
const allUsers = await users().where({})
const sortedUsers = allUsers.sort(by('id', 'asc'))
const sortedUsers = allUsers.sort(by.firstBy('id', 'asc'))
const orderBy = {
id: ASC,
}
Expand All @@ -89,7 +86,7 @@ test('if simple ascending order by finds the correct users.', async () => {

test('if simple descending order by finds the correct users.', async () => {
const allUsers = await users().where({})
const sortedUsers = allUsers.sort(by('id', 'desc'))
const sortedUsers = allUsers.sort(by.firstBy('id', 'desc'))
const orderBy = {
id: DESC,
}
Expand All @@ -112,9 +109,7 @@ test('if simple descending order by finds the correct users.', async () => {
test('if advanced order by finds the correct users.', async () => {
const allUsers = await users().where({})
const sortedUsers = allUsers.sort(
by('lastName', 'desc')
.thenBy('firstName', 'asc')
.thenBy('id', 'desc')
by.firstBy('lastName', 'desc').thenBy('firstName', 'asc').thenBy('id', 'desc')
)
const orderBy = {
lastName: DESC,
Expand All @@ -139,12 +134,12 @@ test('if advanced order by finds the correct users.', async () => {

test('if no cursor sorts users correctly.', async () => {
const allUsers = await users().where({})
const sortedUsers = allUsers.sort(by('id', 'desc'))
const sortedUsers = allUsers.sort(by.firstBy('id', 'desc'))
const orderBy = {
id: DESC,
}
let nextUser = sortedUsers[0]
let cursor = null
let cursor: string | null = null
for (let i = 0; i <= TOTAL_USERS; i += 1) {
const [query, bindings, orderQuery] = cursorToSqlQuery(cursor, orderBy)
const knexQuery = query.replace(/\$\w+/g, '?')
Expand All @@ -159,3 +154,54 @@ test('if no cursor sorts users correctly.', async () => {
}
}
})

test('If joined table is sorted correctly', async () => {
const allTasks = await tasks(DATABASE_NAME)
.select(
`${TABLE_TASKS}.id as taskId`,
`${TABLE_TASKS}.createdAt`,
`${TABLE_TASKS}.name`,
`${TABLE_USERS}.id as userId`,
)
.leftJoin(TABLE_USERS, `${TABLE_TASKS}.userId`, `${TABLE_USERS}.id`)
const sortedTasks = allTasks.sort(by.firstBy('createdAt', 'asc').thenBy('userId', 'asc'))

const TOTAL_USER_TASKS = TOTAL_USERS * TASKS_PER_USER
expect(sortedTasks.length).toEqual(TOTAL_USER_TASKS)

const orderBy = {
createdAt: {
direction: ASC,
column: `${TABLE_TASKS}"."createdAt`,
},
userId: {
direction: ASC,
column: `${TABLE_USERS}"."id`,
},
}

let cursor: string | null = null
let nextTask = sortedTasks[0]
for (let i = 0; i <= TOTAL_USER_TASKS; i += 1) {
const [whereQuery, whereValues, orderQuery] = cursorToSqlQuery(cursor, orderBy)
const knexQuery = whereQuery.replace(/\$\w+/g, '?')
nextTask = await tasks()
.select(
`${TABLE_TASKS}.id as taskId`,
`${TABLE_TASKS}.createdAt`,
`${TABLE_TASKS}.name`,
`${TABLE_USERS}.id as userId`,
)
.leftJoin(TABLE_USERS, `${TABLE_TASKS}.userId`, `${TABLE_USERS}.id`)
.whereRaw(knexQuery, whereValues)
.orderByRaw(orderQuery)
.first()
if (i < TOTAL_USER_TASKS) {
expect(nextTask).toBeDefined()
expect(nextTask.name).toEqual(sortedTasks[i].name)
expect(nextTask.taskId).toEqual(sortedTasks[i].taskId)
expect(nextTask.createdAt).toEqual(sortedTasks[i].createdAt)
cursor = createCursor(nextTask, orderBy)
}
}
})
50 changes: 0 additions & 50 deletions src/benchmarking.js

This file was deleted.

29 changes: 29 additions & 0 deletions src/benchmarking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Knex } from 'knex'
import {
createTestDatabase,
destroyTestDatabase,
} from './testing'
import UserFactory, { TABLE_USERS } from 'factories/UserFactory'

const TOTAL_USERS = 2000

export const createBenchmarkDatabase = async (databaseName: string) => {
const knex = await createTestDatabase(databaseName)
if (knex) {
const now = knex.fn.now()
await knex.schema.createTable(TABLE_USERS, (table) => {
table.increments().primary()
table.datetime('createdAt').defaultTo(now)
table.datetime('updatedAt').defaultTo(now)
table.string('email').notNullable()
table.string('firstName')
table.string('lastName')
table.unique(['email'])
})
await UserFactory.create(TOTAL_USERS)
}
}

export const destroyBenchmarkDatabase = async (knex: Knex) => {
await destroyTestDatabase(knex)
}
26 changes: 26 additions & 0 deletions src/factories/TaskFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import faker from '@faker-js/faker'
import { KnexFactory, getKnexConnection } from '../testing'

export const TABLE_TASKS = 'tasks'

export const TASK_STATUS_PENDING = 'pending'
export const TASK_STATUS_COMPLETED = 'completed'

export const tasks = (databaseName?: string) => {
const knex = getKnexConnection(databaseName)
return knex(TABLE_TASKS)
}

export default class TaskFactory extends KnexFactory {
static get table() {
return tasks
}

static async make() {
return {
name: faker.unique(faker.name.jobTitle),
status: faker.helpers.arrayElement([TASK_STATUS_PENDING, TASK_STATUS_COMPLETED]),
createdAt: faker.date.past(),
}
}
}
Loading

0 comments on commit 4cc57c6

Please sign in to comment.