Skip to content

Commit

Permalink
Save questions to test to avoid re-query database in each test (#52)
Browse files Browse the repository at this point in the history
* docs(readme): remove `db:generate`

* fix(db): drizzle push

* feat: save questions to test to avoid re-query database in each test
  • Loading branch information
jsun969 authored Oct 20, 2023
1 parent 42b2c7f commit d824956
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 78 deletions.
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@
pnpm i
```

2. Add `.env` file
2. Pull `.env` file from vercel

1. Copy `.env.example` and rename it to `.env`
2. Edit `.env`.

3. Initialize database
```bash
vercel env pull .env
```

> **Note**
> If your database is not empty, just run `pnpm run db:generate` to generate types
3. (Optional) Initialize database

```bash
pnpm run db:push
Expand Down
120 changes: 120 additions & 0 deletions app/_game/helpers/getQuestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { desc, eq, inArray } from 'drizzle-orm'

import { db } from '~/db'
import { fetchQuestionCount } from '~/db/queries'
import { questions, questionsToTests, tests } from '~/db/schema'
import { env } from '~/env.mjs'
import dayjs from '~/lib/dayjs'
import { Random } from '~/lib/random'

const generateRandomSeed = ({ now }: { now: Date }) => {
const nowDayjs = dayjs(now)
const dateString = nowDayjs.format('YYYYMMDD')
const idFlag = String(
Math.floor(nowDayjs.hour() / env.NEXT_PUBLIC_REFRESH_INTERVAL_HOURS)
).padStart(2, '0')
return dateString + idFlag
}

const generateRandomArray = ({
length,
max,
seed,
}: {
length: number
max: number
seed: string
}) => {
const random = new Random(Number(seed))
const randomArray: number[] = []
const getRandomNumber = (): number => {
const randomNumber = random.range(0, max)
// Random array should be unrepeated when `length` is not bigger than `max`
if (length <= max) {
if (randomArray.includes(randomNumber)) {
return getRandomNumber()
}
}
return randomNumber
}
for (let index = 0; index < length; index++) {
randomArray.push(getRandomNumber())
}
return randomArray
}

const generateRandomQuestions = async ({
seed,
testId,
}: {
seed: string
testId: number
}) => {
// Generate random indexes
const questionTotal = await fetchQuestionCount()
const randomIndexes = generateRandomArray({
length: env.NEXT_PUBLIC_QUESTIONS_PER_CHALLENGE,
max: Math.min(env.NEXT_PUBLIC_ACTIVE_QUESTIONS_LIMIT, questionTotal),
seed,
})
// Indexes -> Question IDs
const activeQuestionsIds = (
await db
.select({ id: questions.id })
.from(questions)
.orderBy(desc(questions.id))
.limit(env.NEXT_PUBLIC_ACTIVE_QUESTIONS_LIMIT - 1)
).map(({ id }) => id)
const randomIds = randomIndexes.map(
(index) => activeQuestionsIds[index] || -1
)
// Save IDs to test
await db
.insert(tests)
.values({ id: testId })
.onDuplicateKeyUpdate({ set: { id: testId } })
await db
.delete(questionsToTests)
.where(inArray(questionsToTests.questionId, randomIds))
await db
.insert(questionsToTests)
.values(randomIds.map((questionId) => ({ questionId, testId })))
// Get image from question IDs
const images = (
await db
.select({ image: questions.image })
.from(questions)
.where(inArray(questions.id, randomIds))
).map(({ image }) => image)
return images
}

const getQuestionFromTest = async ({ testId }: { testId: number }) => {
const questionRows = await db
.select()
.from(questions)
.innerJoin(questionsToTests, eq(questionsToTests.questionId, questions.id))
.where(eq(questionsToTests.testId, testId))
return questionRows.map(({ questions: { image } }) => image)
}

type GetQuestionsParams = {
length: number
now: Date
testId: number
}
export const getQuestions = async ({
length,
now,
testId,
}: GetQuestionsParams) => {
const existedTestQuestions = await getQuestionFromTest({ testId })
if (existedTestQuestions.length >= length) {
return existedTestQuestions.slice(0, length)
}
const generatedQuestions = await generateRandomQuestions({
seed: generateRandomSeed({ now }),
testId,
})
return generatedQuestions.slice(0, length)
}
79 changes: 16 additions & 63 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { currentUser } from '@clerk/nextjs'
import { desc, eq, inArray } from 'drizzle-orm'
import { desc, eq } from 'drizzle-orm'

import { db } from '~/db'
import { fetchQuestionCount } from '~/db/queries'
import { questions, userScores } from '~/db/schema'
import { userScores } from '~/db/schema'
import { env } from '~/env.mjs'
import dayjs from '~/lib/dayjs'
import { Random } from '~/lib/random'

import { Game } from './_game/Game'
import { filterUser } from './_game/helpers/filterUser'
import { getNextTestStartTime } from './_game/helpers/getNextTestStartTime'
import { getQuestions } from './_game/helpers/getQuestions'
import { getTestId } from './_game/helpers/getTestId'

const fetchUser = async () => {
Expand All @@ -22,61 +20,13 @@ const fetchUser = async () => {
}
}

const getTestSeed = (now: Date) => {
const nowDayjs = dayjs(now)
const dateString = nowDayjs.format('YYYYMMDD')
const idFlag = String(
Math.floor(nowDayjs.hour() / env.NEXT_PUBLIC_REFRESH_INTERVAL_HOURS)
).padStart(2, '0')
return dateString + idFlag
}

const generateRandomArray = (length: number, max: number, seed: string) => {
const random = new Random(Number(seed))
const randomArray: number[] = []
const getRandomNumber = (): number => {
const randomNumber = random.range(0, max)
// Random array should be unrepeated when `length` is not bigger than `max`
if (length <= max) {
if (randomArray.includes(randomNumber)) {
return getRandomNumber()
}
}
return randomNumber
}
for (let index = 0; index < length; index++) {
randomArray.push(getRandomNumber())
}
return randomArray
}

const fetchRandomQuestions = async (isTrial: boolean, seed: string) => {
const questionCount = await fetchQuestionCount()
const randomIndexes = generateRandomArray(
isTrial ? 1 : env.NEXT_PUBLIC_QUESTIONS_PER_CHALLENGE,
Math.min(env.NEXT_PUBLIC_ACTIVE_QUESTIONS_LIMIT, questionCount),
seed
)
const activeQuestionsIds = (
await db
.select({ id: questions.id })
.from(questions)
.orderBy(desc(questions.id))
.limit(env.NEXT_PUBLIC_ACTIVE_QUESTIONS_LIMIT - 1)
).map(({ id }) => id)
const randomIds = randomIndexes.map(
(index) => activeQuestionsIds[index] || -1
)
const images = (
await db
.select({ image: questions.image })
.from(questions)
.where(inArray(questions.id, randomIds))
).map(({ image }) => image)
return images
}

const fetchUserScoreInCurrentTest = async (userId: string, testId: number) => {
const fetchUserScoreInCurrentTest = async ({
userId,
testId,
}: {
userId: string
testId: number
}) => {
const [latestUserScoreRow] = await db
.select()
.from(userScores)
Expand All @@ -93,7 +43,6 @@ const fetchUserScoreInCurrentTest = async (userId: string, testId: number) => {
export default async function Home() {
const now = new Date()

const testSeed = getTestSeed(now)
const testId = getTestId({ date: now })
const nextTestStartTime = getNextTestStartTime({
now,
Expand All @@ -102,12 +51,16 @@ export default async function Home() {

const user = await fetchUser()
const userScoreInCurrentTest = user
? await fetchUserScoreInCurrentTest(user.userId, testId)
? await fetchUserScoreInCurrentTest({ userId: user.userId, testId })
: null

const images = userScoreInCurrentTest
? []
: await fetchRandomQuestions(user === null, testSeed)
: await getQuestions({
length: user === null ? 1 : env.NEXT_PUBLIC_QUESTIONS_PER_CHALLENGE,
testId,
now,
})

return (
<Game
Expand Down
3 changes: 3 additions & 0 deletions db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { drizzle } from 'drizzle-orm/planetscale-serverless'

import { env } from '~/env.mjs'

import * as schema from './schema'

const connection = connect({
host: env.DATABASE_HOST,
username: env.DATABASE_USERNAME,
Expand All @@ -11,4 +13,5 @@ const connection = connect({

export const db = drizzle(connection, {
logger: process.env.NODE_ENV === 'development',
schema,
})
35 changes: 35 additions & 0 deletions db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { relations } from 'drizzle-orm'
import {
boolean,
int,
mysqlTable,
primaryKey,
serial,
timestamp,
varchar,
Expand All @@ -14,6 +16,39 @@ export const questions = mysqlTable('questions', {
createdAt: timestamp('created_at').defaultNow().notNull(),
})

export const questionsRelations = relations(questions, ({ many }) => ({
questionsToTests: many(questionsToTests),
}))

export const tests = mysqlTable('tests', { id: int('id').primaryKey() })

export const testsRelations = relations(tests, ({ many }) => ({
questionsToTests: many(questionsToTests),
}))

export const questionsToTests = mysqlTable(
'questions_to_tests',
{
questionId: int('question_id').notNull(),
testId: int('test_id').notNull(),
},
(t) => ({ pk: primaryKey(t.questionId, t.testId) })
)

export const questionsToTestsRelations = relations(
questionsToTests,
({ one }) => ({
question: one(questions, {
fields: [questionsToTests.questionId],
references: [questions.id],
}),
test: one(tests, {
fields: [questionsToTests.testId],
references: [tests.id],
}),
})
)

export const userScores = mysqlTable('userScores', {
id: serial('id').primaryKey(),
userId: varchar('user_id', { length: 191 }).notNull(),
Expand Down
2 changes: 2 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dotenv/config'

import type { Config } from 'drizzle-kit'

export default {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dependencies": {
"@clerk/nextjs": "^4.25.3",
"@headlessui/react": "^1.7.16",
"@planetscale/database": "^1.10.0",
"@planetscale/database": "^1.11.0",
"@t3-oss/env-nextjs": "^0.6.0",
"@tremor/react": "^3.6.0",
"@upstash/redis": "^1.23.4",
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d824956

Please sign in to comment.