Skip to content

Commit e151146

Browse files
authored
feat: Add startup process for importing questions (#15)
1 parent 799e68a commit e151146

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

app/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export interface Question {
55
is_locked: boolean
66
}
77

8+
export type InputQuestion = Omit<Question, 'id' | 'is_locked'>
9+
810
export interface Answer {
911
id: string
1012
question_id: string

server/utils/storage.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ import { join } from 'path'
33
import { createId } from '@paralleldrive/cuid2'
44
import { lock } from 'proper-lockfile'
55
import type { H3Event } from 'h3'
6-
import type { Question, Results, Answer } from '~/types'
6+
import type { Question, Results, Answer, InputQuestion } from '~/types'
77

88
const DATA_DIR = join(process.cwd(), 'data')
99
const QUESTIONS_FILE = join(DATA_DIR, 'questions.json')
1010
const ANSWERS_FILE = join(DATA_DIR, 'answers.json')
1111
const ADMIN_FILE = join(DATA_DIR, 'admin.json')
12+
const PREDEFINED_QUESTIONS_FILE = join(DATA_DIR, 'predefined-questions.json')
13+
const PROCESSING_FILE = `${PREDEFINED_QUESTIONS_FILE}.processing`
1214

15+
// Type guard to check for Node.js errors
16+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
17+
return error instanceof Error && 'code' in error
18+
}
1319
// Initialize storage with runtime config
1420
async function initStorage(event?: H3Event) {
1521
try {
@@ -23,6 +29,87 @@ async function initStorage(event?: H3Event) {
2329
await fs.writeFile(QUESTIONS_FILE, JSON.stringify([]))
2430
}
2531

32+
// Process predefined questions atomically
33+
try {
34+
// Step 1: Rename the file to mark it as being processed.
35+
await fs.rename(PREDEFINED_QUESTIONS_FILE, PROCESSING_FILE)
36+
37+
// Step 2: Read and validate the processing file.
38+
const predefinedData = await fs.readFile(PROCESSING_FILE, 'utf-8')
39+
let predefinedQuestions: InputQuestion[]
40+
41+
try {
42+
predefinedQuestions = JSON.parse(predefinedData)
43+
}
44+
catch (parseError: unknown) {
45+
logger_error('Malformed JSON in processing file:', parseError)
46+
// Leave the .processing file for manual inspection.
47+
return
48+
}
49+
50+
if (!Array.isArray(predefinedQuestions)) {
51+
logger_error('Processing file must contain a JSON array.')
52+
return
53+
}
54+
55+
for (const q of predefinedQuestions) {
56+
if (typeof q.question_text !== 'string' || q.question_text.trim() === '') {
57+
logger_error('Invalid question_text in processing file:', q)
58+
return
59+
}
60+
if (!Array.isArray(q.answer_options) || q.answer_options.length === 0) {
61+
logger_error('Invalid answer_options in processing file:', q)
62+
return
63+
}
64+
}
65+
66+
// Step 3: Atomically update the questions file.
67+
if (predefinedQuestions.length > 0) {
68+
const release = await lock(QUESTIONS_FILE)
69+
try {
70+
const questionsData = await fs.readFile(QUESTIONS_FILE, 'utf-8')
71+
const existingQuestions: Question[] = JSON.parse(questionsData)
72+
const existingQuestionTexts = new Set(existingQuestions.map(q => q.question_text))
73+
74+
const newQuestions: Question[] = []
75+
for (const q of predefinedQuestions) {
76+
if (!existingQuestionTexts.has(q.question_text)) {
77+
existingQuestionTexts.add(q.question_text) // Add to set to prevent duplicates within the batch
78+
newQuestions.push({
79+
...q,
80+
id: createId(),
81+
is_locked: false
82+
})
83+
}
84+
}
85+
86+
if (newQuestions.length > 0) {
87+
const allQuestions = [...existingQuestions, ...newQuestions]
88+
await fs.writeFile(QUESTIONS_FILE, JSON.stringify(allQuestions, null, 2))
89+
logger(`${newQuestions.length} new predefined questions loaded successfully.`)
90+
}
91+
else {
92+
logger('No new predefined questions to load.')
93+
}
94+
}
95+
finally {
96+
await release()
97+
}
98+
}
99+
100+
// Step 4: Remove the processing file on success.
101+
await fs.unlink(PROCESSING_FILE)
102+
}
103+
catch (error: unknown) {
104+
if (isNodeError(error) && error.code === 'ENOENT') {
105+
// This is fine, no predefined questions file to process.
106+
}
107+
else {
108+
logger_error('Error processing predefined questions:', error)
109+
// If an error occurred, the .processing file is left for manual review.
110+
}
111+
}
112+
26113
// Initialize answers file
27114
try {
28115
await fs.access(ANSWERS_FILE)

0 commit comments

Comments
 (0)