1
0

chore: scaffold node/ts/vitest project with health endpoint

This commit is contained in:
2026-08-29 14:43:44 +02:00
parent 52b2cbd6c5
commit 01ad6fbb40
12 changed files with 3020 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
server/dist/
web/dist/
data/
*.log
.DS_Store

2870
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "record-shop",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": { "node": ">=20" },
"scripts": {
"dev:server": "tsx watch server/src/index.ts",
"dev": "npm run dev:server",
"test": "vitest run",
"test:watch": "vitest",
"build:server": "tsc -p server/tsconfig.build.json",
"build": "npm run build:server",
"start": "node server/dist/index.js",
"typecheck": "tsc -p server/tsconfig.json --noEmit"
},
"dependencies": {
"@fastify/cookie": "^11.0.0",
"@fastify/static": "^8.0.0",
"argon2": "^0.41.1",
"better-sqlite3": "^13.0.3",
"fastify": "^5.0.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.11",
"@types/node": "^22.5.0",
"tsx": "^4.16.0",
"typescript": "^5.5.4",
"vitest": "^3.0.0"
}
}

26
server/src/app.ts Normal file
View File

@@ -0,0 +1,26 @@
import Fastify, { FastifyInstance } from 'fastify'
import Database from 'better-sqlite3'
import type { Config } from './config.js'
declare module 'fastify' {
interface FastifyInstance {
db: Database.Database
config: Config
fetchImpl: typeof fetch
}
}
export interface AppOptions {
db: Database.Database
config: Config
fetchImpl?: typeof fetch
}
export async function buildApp(opts: AppOptions): Promise<FastifyInstance> {
const app = Fastify({ logger: false })
app.decorate('db', opts.db)
app.decorate('config', opts.config)
app.decorate('fetchImpl', opts.fetchImpl ?? fetch)
app.get('/api/health', async () => ({ ok: true }))
return app
}

7
server/src/config.ts Normal file
View File

@@ -0,0 +1,7 @@
export interface Config {
dataDir: string
artworkDir: string
dbPath: string
port: number
sessionSecret: string
}

10
server/src/db.ts Normal file
View File

@@ -0,0 +1,10 @@
import Database from 'better-sqlite3'
export type DB = Database.Database
export function openDatabase(path: string): DB {
const db = new Database(path)
db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = ON')
return db
}

15
server/src/index.ts Normal file
View File

@@ -0,0 +1,15 @@
import { buildApp } from './app.js'
import { openDatabase } from './db.js'
import type { Config } from './config.js'
const config: Config = {
dataDir: process.env.DATA_DIR ?? 'data',
artworkDir: (process.env.DATA_DIR ?? 'data') + '/artwork-cache',
dbPath: (process.env.DATA_DIR ?? 'data') + '/record-shop.db',
port: Number(process.env.PORT ?? 3000),
sessionSecret: 'placeholder-replaced-in-task-3',
}
const db = openDatabase(config.dbPath)
const app = await buildApp({ db, config })
await app.listen({ port: config.port, host: '0.0.0.0' })

23
server/test/app.test.ts Normal file
View File

@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest'
import { buildApp } from '../src/app.js'
import { openDatabase } from '../src/db.js'
import type { Config } from '../src/config.js'
const config: Config = {
dataDir: ':memory:',
artworkDir: ':memory:',
dbPath: ':memory:',
port: 0,
sessionSecret: 'test-secret-test-secret-test-secret-1234',
}
describe('GET /api/health', () => {
it('returns ok', async () => {
const db = openDatabase(':memory:')
const app = await buildApp({ db, config })
const res = await app.inject({ method: 'GET', url: '/api/health' })
expect(res.statusCode).toBe(200)
expect(res.json()).toEqual({ ok: true })
await app.close()
})
})

View File

@@ -0,0 +1,5 @@
{
"extends": "../tsconfig.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*"]
}

5
server/tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": "../tsconfig.json",
"compilerOptions": { "noEmit": true },
"include": ["src/**/*", "test/**/*"]
}

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"types": ["node"]
}
}

7
vitest.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['server/test/**/*.test.ts'],
},
})