feat(types): type-register-rest-routes (#10171)

This commit is contained in:
Grandalf
2026-07-28 14:51:36 +02:00
committed by GitHub
parent dd58fed217
commit 4636fcf820
9 changed files with 122 additions and 76 deletions

View File

@@ -32,7 +32,8 @@
"devDependencies": {
"@eslint/js": "^9.19.0",
"typescript": "~5.6.3",
"typescript-eslint": "^8.61.0"
"typescript-eslint": "^8.61.0",
"@types/express": "^5.0.0"
},
"scripts": {
"build": "tsc",

View File

@@ -5,3 +5,4 @@ export * from './xo-app.mjs'
export * from './lib/xen-orchestra-xapi.mjs'
export * from './lib/xen-orchestra-acl.mjs'
export * from './lib/complex-matcher.mjs'
export * from './lib/rest-api.mjs'

View File

@@ -0,0 +1,102 @@
import type { NextFunction, Request, Response } from 'express'
import type { VatesTask } from './vates-task.mjs'
import type { XoApp } from '../xo-app.mjs'
export type SecurityName = '*' | 'token' | 'basic' | 'none'
export type FieldDefinition =
| {
type: 'string'
example?: string
optional?: boolean
}
| {
type: 'boolean'
example?: boolean
optional?: boolean
}
| {
type: 'number'
example?: number
optional?: boolean
}
| {
type: 'enum'
enum: string[]
example?: string
optional?: boolean
}
| {
type: 'object'
fields: Record<string, FieldDefinition>
optional?: boolean
}
| {
type: 'array'
items: FieldDefinition
example?: unknown[]
optional?: boolean
}
export type ParamFieldDefinition = Exclude<
FieldDefinition,
{ type: 'boolean' } | { type: 'object' } | { type: 'array' }
>
export type QueryFieldDefinition = Exclude<FieldDefinition, { type: 'object' } | { type: 'array' }>
type RestApi = object & { xoApp: XoApp }
export type CreateAction = <Result>(
callback: (task: VatesTask) => MaybePromise<Result>,
opts: {
sync?: boolean
statusCode?: number
taskProperties: { name: string; [key: string]: unknown }
}
) => Promise<Result | undefined>
export interface BaseRouteDefinition<Middleware> {
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
endpoint: string
description?: string
tags?: string[]
params?: Record<string, ParamFieldDefinition>
query?: Record<string, QueryFieldDefinition>
body?: Record<string, FieldDefinition>
responses?: Array<{
status: number
description: string
schema?: Record<string, FieldDefinition>
}>
middlewares?: Middleware[]
scope?: 'acl'
callback: (params: {
req: Request
res: Response
next: NextFunction
restApi: RestApi
createAction: CreateAction
}) => MaybePromise<unknown>
security?: SecurityName
}
export type MiddlewareDescriptor =
| { name: 'json' | 'urlencoded' | 'text' | 'raw'; options?: Record<string, unknown> }
| { name: 'acl'; acls: LooseAclEntry | LooseAclEntry[] }
type MaybePromise<T> = T | Promise<T>
/**
* Loosely-typed mirror of {@link https://github.com/vatesfr/xen-orchestra/blob/master/@xen-orchestra/rest-api/src/middlewares/acl.middleware.mts | AclEntry}
* from `@xen-orchestra/rest-api/src/middlewares/acl.middleware.mts`.
*/
export type LooseAclEntry = {
resource: string
action?: string | ((opts: { req: object; restApi: RestApi }) => string | undefined)
actions?: string[] | ((opts: { req: object; restApi: RestApi }) => string[])
objectId?: string | ((opts: { req: object; restApi: RestApi }) => string)
objectIds?: string[] | ((opts: { req: object; restApi: RestApi }) => string[])
object?: object | ((opts: { req: object; restApi: RestApi }) => MaybePromise<object> | undefined)
objects?: object[] | ((opts: { req: object; restApi: RestApi }) => MaybePromise<object[]> | undefined)
getObject?: (opts: { restApi: RestApi }) => (id: string) => MaybePromise<object>
}
export type PluginRestRouteDefinition = BaseRouteDefinition<MiddlewareDescriptor>

View File

@@ -1,6 +1,5 @@
import { EventEmitter } from 'node:stream'
import type {
AnyXoBackupJob,
AnyXoJob,
AnyXoLog,
XapiXoRecord,
@@ -21,6 +20,7 @@ import type {
XoVmBackupArchive,
} from './xo.mjs'
import { VatesTask } from './lib/vates-task.mjs'
import type { PluginRestRouteDefinition } from './lib/rest-api.mjs'
import {
Xapi,
XapiHostStats,
@@ -195,6 +195,7 @@ export type XoApp = {
params?: any
}
) => () => void // eslint-disable-line @typescript-eslint/no-explicit-any
registerRestRoutes: (routes: PluginRestRouteDefinition[], base?: string) => () => void
authenticateUser: (
credentials: { token?: string; username?: string; password?: string },
userData?: { ip?: string },

View File

@@ -1,6 +1,6 @@
import type { Readable } from 'node:stream'
import type { Request, Response } from 'express'
import type { SecurityName } from '../middlewares/authentication.middleware.mjs'
import type { SecurityName } from '@vates/types'
export type MaybePromise<T> = T | Promise<T>

View File

@@ -9,11 +9,9 @@ import type { AuthenticatedRequest } from '../helpers/helper.type.mjs'
import { iocContainer } from '../ioc/ioc.mjs'
import { RestApi } from '../rest-api/rest-api.mjs'
import { ACL_MIDDLEWARE_NAME } from './acl.middleware.mjs'
import type { SecurityName } from '@vates/types'
const log = createLogger('xo:rest-api:authentication')
export type SecurityName = '*' | 'token' | 'basic' | 'none'
// TSOA spec require this function to be async
export async function expressAuthentication(req: AuthenticatedRequest, securityName: SecurityName, scopes: 'acl'[]) {
if (securityName === 'none') {

View File

@@ -17,11 +17,11 @@ import { iocContainer } from '../ioc/ioc.mjs'
import { RestApi } from '../rest-api/rest-api.mjs'
import {
CONTENT_TYPE_BY_MIDDLEWARE_NAME,
type CreateAction,
type FieldDefinition,
type MiddlewareDescriptor,
type RouteDefinition,
} from './types.mjs'
import type { CreateAction } from '@vates/types'
const log = createLogger('xo:rest-api:external-router')

View File

@@ -1,18 +1,16 @@
import type { SecurityName } from '../middlewares/authentication.middleware.mjs'
import type { AclEntry } from '../middlewares/acl.middleware.mjs'
import type { NextFunction, Request, Response } from 'express'
import type { MaybePromise } from '../helpers/helper.type.mjs'
import type { RestApi } from '../rest-api/rest-api.mjs'
import type { VatesTask } from '@vates/types/lib/vates/task'
import type {
BaseRouteDefinition,
FieldDefinition,
ParamFieldDefinition,
QueryFieldDefinition,
CreateAction,
} from '@vates/types'
import type { NextFunction, Request, Response } from 'express'
export type CreateAction = <CbType>(
cb: (task: VatesTask) => MaybePromise<CbType>,
options: {
sync?: boolean
statusCode?: number
taskProperties: { name: string; [key: string]: unknown }
}
) => Promise<CbType | undefined>
export type { FieldDefinition, ParamFieldDefinition, QueryFieldDefinition }
// Maps middleware descriptor names to their OpenAPI content type
export const CONTENT_TYPE_BY_MIDDLEWARE_NAME: Record<string, string> = {
@@ -26,62 +24,7 @@ export type MiddlewareDescriptor =
| { name: 'json' | 'urlencoded' | 'text' | 'raw'; options?: Record<string, unknown> }
| { name: 'acl'; acls: AclEntry | AclEntry[] }
export type FieldDefinition =
| {
type: 'string'
example?: string
optional?: boolean
}
| {
type: 'boolean'
example?: boolean
optional?: boolean
}
| {
type: 'number'
example?: number
optional?: boolean
}
| {
type: 'enum'
enum: string[]
example?: string
optional?: boolean
}
| {
type: 'object'
fields: Record<string, FieldDefinition>
optional?: boolean
}
| {
type: 'array'
items: FieldDefinition
example?: unknown[]
optional?: boolean
}
export type ParamFieldDefinition = Exclude<
FieldDefinition,
{ type: 'boolean' } | { type: 'object' } | { type: 'array' }
>
export type QueryFieldDefinition = Exclude<FieldDefinition, { type: 'object' } | { type: 'array' }>
export interface RouteDefinition {
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
endpoint: string
description?: string
tags?: string[]
params?: Record<string, ParamFieldDefinition>
query?: Record<string, QueryFieldDefinition>
body?: Record<string, FieldDefinition>
responses?: Array<{
status: number
description: string
schema?: Record<string, FieldDefinition>
}>
middlewares?: MiddlewareDescriptor[]
scope?: 'acl'
export type RouteDefinition = Omit<BaseRouteDefinition<MiddlewareDescriptor>, 'callback'> & {
callback: (params: {
req: Request
res: Response
@@ -89,5 +32,4 @@ export interface RouteDefinition {
restApi: RestApi
createAction: CreateAction
}) => MaybePromise<unknown>
security?: SecurityName
}

View File

@@ -125,7 +125,8 @@ export function defineRemoteResource<
string,
{
count: number
evictionTimeout?: number
evictionTimeout?: NodeJS.Timeout // TODO : Fixme , the actual type in browser is number, but NodeJS.Timeout in nodejs.
// Due to transitive types of vates/types, the type is NodeJS.Timeout.
pause: VoidFunction
resume: VoidFunction
isPaused: boolean