From 366a43bfdb3e6892ad779b7ae6b5a2a05bb8eee8 Mon Sep 17 00:00:00 2001 From: mathieuRA Date: Wed, 26 Feb 2025 17:38:13 +0100 Subject: [PATCH] feat(@xen-orchestra/rest-api): expose get servers and get server --- @vates/types/src/xo.mts | 20 ++++++- @xen-orchestra/rest-api/.USAGE.md | 1 + @xen-orchestra/rest-api/README.md | 1 + .../src/abstract-classes/base-controller.mts | 26 ++++++++++ .../abstract-classes/xapi-xo-controller.mts | 18 ++----- .../src/abstract-classes/xo-controller.mts | 47 +++++++++++++++++ .../src/helpers/object-wrapper.helper.mts | 4 +- .../rest-api/src/rest-api/rest-api.mts | 8 +++ .../rest-api/src/rest-api/rest-api.type.mts | 4 +- .../src/servers/server.controller.mts | 52 +++++++++++++++++++ packages/xo-server/src/xo-mixins/rest-api.mjs | 10 +--- 11 files changed, 163 insertions(+), 28 deletions(-) create mode 100644 @xen-orchestra/rest-api/src/abstract-classes/base-controller.mts create mode 100644 @xen-orchestra/rest-api/src/abstract-classes/xo-controller.mts create mode 100644 @xen-orchestra/rest-api/src/servers/server.controller.mts diff --git a/@vates/types/src/xo.mts b/@vates/types/src/xo.mts index 0e8aa2139f9..8485689edd3 100644 --- a/@vates/types/src/xo.mts +++ b/@vates/types/src/xo.mts @@ -109,6 +109,22 @@ export type XoPool = { type: 'pool' } +export type XoServer = { + allowUnauthorized: boolean + enabled: boolean + error?: Record + host: string + httpProxy?: string + id: Branded<'server'> + label?: string + poolId?: XoPool['id'] + poolNameDescription?: string + poolNameLabel?: string + readOnly: boolean + status: 'connected' | 'disconnected' | 'connecting' + username: string +} + export type XoSr = { id: Branded<'SR'> type: 'SR' @@ -200,4 +216,6 @@ export type XapiXoRecord = | XoVmTemplate | XoVtpm -export type XoRecord = XapiXoRecord | XoGroup | XoUser +export type NonXapiXoRecord = XoGroup | XoServer | XoUser + +export type XoRecord = XapiXoRecord | NonXapiXoRecord diff --git a/@xen-orchestra/rest-api/.USAGE.md b/@xen-orchestra/rest-api/.USAGE.md index 3278f62031a..0a0695db70c 100644 --- a/@xen-orchestra/rest-api/.USAGE.md +++ b/@xen-orchestra/rest-api/.USAGE.md @@ -10,6 +10,7 @@ The REST API is based on the `TSOA` framework and therefore we use decorators a @Routes('foo') @Security('*') @Response(401) +@Tags('foo') @provide(Foo) class Foo extends Controller {} ``` diff --git a/@xen-orchestra/rest-api/README.md b/@xen-orchestra/rest-api/README.md index f2c74377071..94e759b3922 100644 --- a/@xen-orchestra/rest-api/README.md +++ b/@xen-orchestra/rest-api/README.md @@ -28,6 +28,7 @@ The REST API is based on the `TSOA` framework and therefore we use decorators a @Routes('foo') @Security('*') @Response(401) +@Tags('foo') @provide(Foo) class Foo extends Controller {} ``` diff --git a/@xen-orchestra/rest-api/src/abstract-classes/base-controller.mts b/@xen-orchestra/rest-api/src/abstract-classes/base-controller.mts new file mode 100644 index 00000000000..e7756cdf4d0 --- /dev/null +++ b/@xen-orchestra/rest-api/src/abstract-classes/base-controller.mts @@ -0,0 +1,26 @@ +import { Controller } from 'tsoa' +import { Request } from 'express' +import { XoRecord } from '@vates/types' + +import { RestApi } from '../rest-api/rest-api.mjs' +import { makeObjectMapper } from '../helpers/object-wrapper.helper.mjs' +import type { WithHref } from '../helpers/helper.type.mjs' + +export abstract class BaseController extends Controller { + abstract getObjects(): IsSync extends false ? Promise> : Record + abstract getObject(id: T['id']): IsSync extends false ? Promise : T + + restApi: RestApi + + constructor(restApi: RestApi) { + super() + this.restApi = restApi + } + + sendObjects(objects: T[], req: Request): string[] | WithHref[] | WithHref>[] { + const mapper = makeObjectMapper(req) + const mappedObjects = objects.map(mapper) as string[] | WithHref[] | WithHref>[] + + return mappedObjects + } +} diff --git a/@xen-orchestra/rest-api/src/abstract-classes/xapi-xo-controller.mts b/@xen-orchestra/rest-api/src/abstract-classes/xapi-xo-controller.mts index 3a93dd89e53..9edabf78ebb 100644 --- a/@xen-orchestra/rest-api/src/abstract-classes/xapi-xo-controller.mts +++ b/@xen-orchestra/rest-api/src/abstract-classes/xapi-xo-controller.mts @@ -1,20 +1,15 @@ import * as CM from 'complex-matcher' -import { Controller } from 'tsoa' -import { Request } from 'express' import type { XapiXoRecord } from '@vates/types/xo' import { RestApi } from '../rest-api/rest-api.mjs' -import { makeObjectMapper } from '../helpers/object-wrapper.helper.mjs' -import type { WithHref } from '../helpers/helper.type.mjs' +import { BaseController } from './base-controller.mjs' -export abstract class XapiXoController extends Controller { +export abstract class XapiXoController extends BaseController { #type: T['type'] - restApi: RestApi constructor(type: T['type'], restApi: RestApi) { - super() + super(restApi) this.#type = type - this.restApi = restApi } getObjects({ filter, limit }: { filter?: string; limit?: number } = {}): Record { @@ -27,11 +22,4 @@ export abstract class XapiXoController extends Controlle getObject(id: T['id']): T { return this.restApi.getObject(id, this.#type) } - - sendObjects(objects: T[], req: Request): string[] | WithHref[] | WithHref>[] { - const mapper = makeObjectMapper(req) - const mappedObjects = objects.map(mapper) as string[] | WithHref[] | WithHref>[] - - return mappedObjects - } } diff --git a/@xen-orchestra/rest-api/src/abstract-classes/xo-controller.mts b/@xen-orchestra/rest-api/src/abstract-classes/xo-controller.mts new file mode 100644 index 00000000000..b147263f9d1 --- /dev/null +++ b/@xen-orchestra/rest-api/src/abstract-classes/xo-controller.mts @@ -0,0 +1,47 @@ +import * as CM from 'complex-matcher' +import type { NonXapiXoRecord } from '@vates/types/xo' + +import { BaseController } from './base-controller.mjs' + +import { RestApi } from '../rest-api/rest-api.mjs' + +export abstract class XoController extends BaseController { + abstract _abstractGetObjects(): Promise + abstract _abstractGetObject(id: T['id']): Promise + + constructor(restApi: RestApi) { + super(restApi) + } + + async getObjects({ + filter, + limit, + }: { + filter?: string + limit?: number + } = {}): Promise> { + const _limit = limit ?? Infinity + let objects = await this._abstractGetObjects() + + if (filter !== undefined) { + const predicate = CM.parse(filter).createPredicate() + objects = objects.filter(predicate) + } + + if (_limit < objects.length) { + objects.length = _limit + } + + const objectById = {} as Record + + objects.forEach(obj => { + objectById[obj.id] = obj + }) + + return objectById + } + + async getObject(id: T['id']): Promise { + return this._abstractGetObject(id) + } +} diff --git a/@xen-orchestra/rest-api/src/helpers/object-wrapper.helper.mts b/@xen-orchestra/rest-api/src/helpers/object-wrapper.helper.mts index 3d28e8d52ed..53482b6bbb5 100644 --- a/@xen-orchestra/rest-api/src/helpers/object-wrapper.helper.mts +++ b/@xen-orchestra/rest-api/src/helpers/object-wrapper.helper.mts @@ -1,13 +1,13 @@ import path from 'node:path' import pick from 'lodash/pick.js' import { Request } from 'express' -import type { XapiXoRecord } from '@vates/types' +import type { XoRecord } from '@vates/types' import type { WithHref } from './helper.type.mjs' const { join } = path.posix -export function makeObjectMapper(req: Request, path = req.path) { +export function makeObjectMapper(req: Request, path = req.path) { const makeUrl = ({ id }: T) => join(baseUrl, path, typeof id === 'number' ? String(id) : id) let objectMapper: (object: T) => string | WithHref> | WithHref diff --git a/@xen-orchestra/rest-api/src/rest-api/rest-api.mts b/@xen-orchestra/rest-api/src/rest-api/rest-api.mts index f8317e1faae..9fe5825d59e 100644 --- a/@xen-orchestra/rest-api/src/rest-api/rest-api.mts +++ b/@xen-orchestra/rest-api/src/rest-api/rest-api.mts @@ -13,6 +13,10 @@ export class RestApi { return this.#xoApp.authenticateUser(...args) } + getAllXenServers() { + return this.#xoApp.getAllXenServers() + } + getObject(id: T['id'], type: T['type']) { return this.#xoApp.getObject(id, type) } @@ -21,6 +25,10 @@ export class RestApi { return this.#xoApp.getObjectsByType(type, opts) } + getXenServer(...args: Parameters) { + return this.#xoApp.getXenServer(...args) + } + runWithApiContext(...args: Parameters) { return this.#xoApp.runWithApiContext(...args) } diff --git a/@xen-orchestra/rest-api/src/rest-api/rest-api.type.mts b/@xen-orchestra/rest-api/src/rest-api/rest-api.type.mts index 43f98b9a1b5..db2cc901391 100644 --- a/@xen-orchestra/rest-api/src/rest-api/rest-api.type.mts +++ b/@xen-orchestra/rest-api/src/rest-api/rest-api.type.mts @@ -1,4 +1,4 @@ -import type { XoUser, XapiXoRecord } from '@vates/types/xo' +import type { XoUser, XapiXoRecord, XoServer } from '@vates/types/xo' export type XoApp = { authenticateUser: ( @@ -6,10 +6,12 @@ export type XoApp = { userData?: { ip?: string }, opts?: { bypassOtp?: boolean } ) => Promise<{ bypassOtp: boolean; expiration: number; user: XoUser }> + getAllXenServers(): Promise getObject: (id: T['id'], type: T['type']) => T getObjectsByType: ( type: T['type'], opts?: { filter?: string; limit?: number } ) => Record + getXenServer(id: XoServer['id']): Promise runWithApiContext: (user: XoUser, fn: () => void) => Promise } diff --git a/@xen-orchestra/rest-api/src/servers/server.controller.mts b/@xen-orchestra/rest-api/src/servers/server.controller.mts new file mode 100644 index 00000000000..d5c2731f9c1 --- /dev/null +++ b/@xen-orchestra/rest-api/src/servers/server.controller.mts @@ -0,0 +1,52 @@ +import { Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa' +import { Request as ExRequest } from 'express' +import { inject } from 'inversify' +import { provide } from 'inversify-binding-decorators' +import type { XoServer } from '@vates/types' + +import { RestApi } from '../rest-api/rest-api.mjs' +import type { Unbrand, WithHref } from '../helpers/helper.type.mjs' +import { XoController } from '../abstract-classes/xo-controller.mjs' + +@Route('servers') +@Security('*') +@Response(401) +@Tags('servers') +@provide(ServerController) +export class ServerController extends XoController { + // --- abstract methods + _abstractGetObjects(): Promise { + return this.restApi.getAllXenServers() + } + _abstractGetObject(id: XoServer['id']): Promise { + return this.restApi.getXenServer(id) + } + + constructor(@inject(RestApi) restApi: RestApi) { + super(restApi) + } + + /** + * @example fields "status,uuid" + * @example filter "status:/^connected$/" + * @example limit 42 + */ + @Get('') + async getServers( + @Request() req: ExRequest, + @Query() fields?: string, + @Query() filter?: string, + @Query() limit?: number + ): Promise>[] | WithHref>>[]> { + const servers = Object.values(await this.getObjects({ filter, limit })) + return this.sendObjects(servers, req) + } + + /** + * @example id "f07ab729-c0e8-721c-45ec-f11276377030" + */ + @Get('{id}') + getServer(@Path() id: string) { + return this.getObject(id as XoServer['id']) + } +} diff --git a/packages/xo-server/src/xo-mixins/rest-api.mjs b/packages/xo-server/src/xo-mixins/rest-api.mjs index 476a8e4d764..4295411aaa2 100644 --- a/packages/xo-server/src/xo-mixins/rest-api.mjs +++ b/packages/xo-server/src/xo-mixins/rest-api.mjs @@ -605,7 +605,7 @@ export default class RestApi { const collections = { __proto__: null } // add migrated collections to maintain their discoverability - const swaggerEndpoints = ['docs', 'vms'] + const swaggerEndpoints = ['docs', 'vms', 'servers'] const withParams = (fn, paramsSchema) => { fn.params = paramsSchema @@ -982,14 +982,6 @@ export default class RestApi { return stream[Symbol.asyncIterator]() }, } - collections.servers = { - getObject(id) { - return app.getXenServer(id) - }, - async getObjects(filter, limit) { - return handleArray(await app.getAllXenServers(), filter, limit) - }, - } collections.users = { getObject(id) { return app.getUser(id).then(getUserPublicProperties)