mirror of
https://github.com/typicode/json-server.git
synced 2026-03-13 09:35:37 +08:00
171 lines
4.6 KiB
JavaScript
171 lines
4.6 KiB
JavaScript
import { dirname, isAbsolute, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { App } from '@tinyhttp/app'
|
|
import { cors } from '@tinyhttp/cors'
|
|
import { Eta } from 'eta'
|
|
import { Low } from 'lowdb'
|
|
import { json } from 'milliparsec'
|
|
import sirv from 'sirv'
|
|
|
|
import { parseWhere } from './parse-where.ts'
|
|
import type { Data } from './service.ts'
|
|
import { isItem, Service } from './service.ts'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const isProduction = process.env['NODE_ENV'] === 'production'
|
|
|
|
export type AppOptions = {
|
|
logger?: boolean
|
|
static?: string[]
|
|
}
|
|
|
|
const eta = new Eta({
|
|
views: join(__dirname, '../views'),
|
|
cache: isProduction,
|
|
})
|
|
|
|
const RESERVED_QUERY_KEYS = new Set(['_sort', '_page', '_per_page', '_embed', '_where'])
|
|
|
|
function parseListParams(req: any) {
|
|
const queryString = req.url.split('?')[1] ?? ''
|
|
const params = new URLSearchParams(queryString)
|
|
|
|
const filterParams = new URLSearchParams()
|
|
for (const [key, value] of params.entries()) {
|
|
if (!RESERVED_QUERY_KEYS.has(key)) {
|
|
filterParams.append(key, value)
|
|
}
|
|
}
|
|
|
|
let where = parseWhere(filterParams.toString())
|
|
const rawWhere = params.get('_where')
|
|
if (typeof rawWhere === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(rawWhere)
|
|
if (typeof parsed === 'object' && parsed !== null) {
|
|
where = parsed
|
|
}
|
|
} catch {
|
|
// Ignore invalid JSON and fallback to parsed query params
|
|
}
|
|
}
|
|
|
|
const pageRaw = params.get('_page')
|
|
const perPageRaw = params.get('_per_page')
|
|
const page = pageRaw === null ? undefined : Number.parseInt(pageRaw, 10)
|
|
const perPage = perPageRaw === null ? undefined : Number.parseInt(perPageRaw, 10)
|
|
|
|
return {
|
|
where,
|
|
sort: params.get('_sort') ?? undefined,
|
|
page: Number.isNaN(page) ? undefined : page,
|
|
perPage: Number.isNaN(perPage) ? undefined : perPage,
|
|
embed: req.query['_embed'],
|
|
}
|
|
}
|
|
|
|
function withBody(action: (name: string, body: Record<string, unknown>) => Promise<unknown>) {
|
|
return async (req: any, res: any, next: any) => {
|
|
const { name = '' } = req.params
|
|
if (!isItem(req.body)) {
|
|
res.status(400).json({ error: 'Body must be a JSON object' })
|
|
return
|
|
}
|
|
res.locals['data'] = await action(name, req.body)
|
|
next?.()
|
|
}
|
|
}
|
|
|
|
function withIdAndBody(
|
|
action: (name: string, id: string, body: Record<string, unknown>) => Promise<unknown>,
|
|
) {
|
|
return async (req: any, res: any, next: any) => {
|
|
const { name = '', id = '' } = req.params
|
|
if (!isItem(req.body)) {
|
|
res.status(400).json({ error: 'Body must be a JSON object' })
|
|
return
|
|
}
|
|
res.locals['data'] = await action(name, id, req.body)
|
|
next?.()
|
|
}
|
|
}
|
|
|
|
export function createApp(db: Low<Data>, options: AppOptions = {}) {
|
|
// Create service
|
|
const service = new Service(db)
|
|
|
|
// Create app
|
|
const app = new App()
|
|
|
|
// Static files
|
|
app.use(sirv('public', { dev: !isProduction }))
|
|
options.static
|
|
?.map((path) => (isAbsolute(path) ? path : join(process.cwd(), path)))
|
|
.forEach((dir) => app.use(sirv(dir, { dev: !isProduction })))
|
|
|
|
// CORS
|
|
app
|
|
.use((req, res, next) => {
|
|
return cors({
|
|
allowedHeaders: req.headers['access-control-request-headers']
|
|
?.split(',')
|
|
.map((h) => h.trim()),
|
|
})(req, res, next)
|
|
})
|
|
.options('*', cors())
|
|
|
|
// Body parser
|
|
app.use(json())
|
|
|
|
app.get('/', (_req, res) => res.send(eta.render('index.html', { data: db.data })))
|
|
|
|
app.get('/:name', (req, res, next) => {
|
|
const { name = '' } = req.params
|
|
const { where, sort, page, perPage, embed } = parseListParams(req)
|
|
|
|
res.locals['data'] = service.find(name, {
|
|
where,
|
|
sort,
|
|
page,
|
|
perPage,
|
|
embed,
|
|
})
|
|
next?.()
|
|
})
|
|
|
|
app.get('/:name/:id', (req, res, next) => {
|
|
const { name = '', id = '' } = req.params
|
|
res.locals['data'] = service.findById(name, id, req.query)
|
|
next?.()
|
|
})
|
|
|
|
app.post('/:name', withBody(service.create.bind(service)))
|
|
|
|
app.put('/:name', withBody(service.update.bind(service)))
|
|
|
|
app.put('/:name/:id', withIdAndBody(service.updateById.bind(service)))
|
|
|
|
app.patch('/:name', withBody(service.patch.bind(service)))
|
|
|
|
app.patch('/:name/:id', withIdAndBody(service.patchById.bind(service)))
|
|
|
|
app.delete('/:name/:id', async (req, res, next) => {
|
|
const { name = '', id = '' } = req.params
|
|
res.locals['data'] = await service.destroyById(name, id, req.query['_dependent'])
|
|
next?.()
|
|
})
|
|
|
|
app.use('/:name', (req, res) => {
|
|
const { data } = res.locals
|
|
if (data === undefined) {
|
|
res.sendStatus(404)
|
|
} else {
|
|
if (req.method === 'POST') res.status(201)
|
|
res.json(data)
|
|
}
|
|
})
|
|
|
|
return app
|
|
}
|