feat: add OpenAPI support and in-process cron scheduler

- Install @elysia/openapi to auto-generate API docs at /api/openapi
- Install @elysia/cron for in-process scheduled jobs (orphan detection at 2am, storage cleanup at 3am)
- Add Scalar UI docs with 14 tag groups and Bearer JWT security scheme
- Add route-level detail annotations (tags, summaries) to all 30+ route files
- Upgrade crud-builder to accept optional tags config
- Keep HTTP cron endpoint as manual/admin fallback
This commit is contained in:
2026-05-14 21:43:33 +08:00
parent 20f010cef1
commit fbfc43832e
37 changed files with 161 additions and 32 deletions
+26 -1
View File
@@ -60,6 +60,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
primaryKey?: string
paramName?: string
softDelete?: boolean
tags?: string[]
},
) {
const {
@@ -71,6 +72,7 @@ export function createCrudRoutes<T extends AnyPgTable>(
primaryKey = "id",
paramName = primaryKey,
softDelete = false,
tags,
} = config
const columns = getTableColumns(table) as Record<string, PgColumn>
@@ -80,7 +82,10 @@ export function createCrudRoutes<T extends AnyPgTable>(
throw new Error(`Primary key column "${primaryKey}" not found on table`)
}
const routes = new Elysia({ prefix })
const routes = new Elysia({
prefix,
...(tags ? { detail: { tags } } : {}),
})
// ── LIST ──────────────────────────────────────────────────────────
routes.get(
@@ -157,6 +162,10 @@ export function createCrudRoutes<T extends AnyPgTable>(
// Dynamic filter fields are too varied for static TypeBox,
// so we allow any string keys with filter_ prefix
}),
detail: {
summary: `List ${name}s`,
description: `Returns a paginated list of ${name}s with optional search and filtering.`,
},
},
)
@@ -183,6 +192,10 @@ export function createCrudRoutes<T extends AnyPgTable>(
params: t.Object({
[paramName]: t.String(),
}),
detail: {
summary: `Get ${name} by ID`,
description: `Returns a single ${name} by its unique identifier.`,
},
},
)
@@ -211,6 +224,10 @@ export function createCrudRoutes<T extends AnyPgTable>(
},
{
body: t.Record(t.String(), t.Any()),
detail: {
summary: `Create ${name}`,
description: `Creates a new ${name}. Requires authentication.`,
},
},
)
@@ -256,6 +273,10 @@ export function createCrudRoutes<T extends AnyPgTable>(
[paramName]: t.String(),
}),
body: t.Record(t.String(), t.Any()),
detail: {
summary: `Update ${name}`,
description: `Updates an existing ${name} by ID. Requires authentication.`,
},
},
)
@@ -303,6 +324,10 @@ export function createCrudRoutes<T extends AnyPgTable>(
params: t.Object({
[paramName]: t.String(),
}),
detail: {
summary: `Delete ${name}`,
description: `Deletes a ${name} by ID. Requires admin or contributor role.`,
},
},
)