diff --git a/Makefile b/Makefile index b89968a6..6005dba3 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ generate: .PHONY: test test: $(MAKE) -C go test + $(MAKE) -C js test .PHONY: build build: diff --git a/examples/js/ip-list.ts b/examples/js/ip-list.ts index 5fcc8131..2423881f 100644 --- a/examples/js/ip-list.ts +++ b/examples/js/ip-list.ts @@ -1,51 +1,16 @@ -import * as apiv2 from "@metal-stack/api/js/metalstack/api/v2/ip_pb"; -import { createClient } from "@connectrpc/connect"; -import { createConnectTransport } from "@connectrpc/connect-web"; - -import { Code, ConnectError, Interceptor } from "@connectrpc/connect"; - -class AuthInterceptor { - private authToken: string; - - constructor(authToken: string) { - this.authToken = authToken; - } - - interceptor: Interceptor = (next) => async (req) => { - if (!this.authToken) { - throw new ConnectError("Missing auth token", Code.Unauthenticated); - } - - req.header.append("Authorization", `Bearer ${this.authToken}`); - - try { - const res = await next(req); - return res; - } catch (e) { - if (e instanceof ConnectError && e.code === Code.Unauthenticated) { - // e.g. message: "token has expired" - console.error("unauthenticated", e); - } - throw e; - } - }; -} +import { newClient } from "../../js/client"; async function main() { const token = process.env["API_TOKEN"]; const project = process.env["PROJECT_ID"]; const baseUrl = process.env["METAL_APISERVER_URL"]; - const auth = new AuthInterceptor(token!); - const client = createClient( - apiv2.IPService, - createConnectTransport({ - baseUrl: baseUrl!, - interceptors: [auth.interceptor], - }), - ); + const client = newClient({ + baseUrl: baseUrl!, + token: token!, + }); - const listResp = await client.list({ project }); + const listResp = await client.apiv2().ip().list({ project }); for (const ip of listResp.ips) { console.log("ip", ip); diff --git a/examples/js/tsconfig.json b/examples/js/tsconfig.json new file mode 100644 index 00000000..e4420f06 --- /dev/null +++ b/examples/js/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "esnext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["./**/*.ts"] +} diff --git a/generate/Makefile b/generate/Makefile index aab1ce33..5a532789 100644 --- a/generate/Makefile +++ b/generate/Makefile @@ -3,5 +3,7 @@ generate: # we remove files explicitly to ensure files really get generated rm -f ../go/permissions/servicepermissions.go rm -f ../go/client/client.go + rm -f ../python/metalstack/client.py + rm -f ../ts/client/client.* go run ./generate.go \ No newline at end of file diff --git a/generate/generate.go b/generate/generate.go index 4fd7ebc6..babfbc73 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -4,7 +4,7 @@ import ( "bytes" "fmt" "go/format" - "html/template" + "text/template" "os" "path" "path/filepath" @@ -31,6 +31,8 @@ var ( servicePermissionsTpl string //go:embed go_client.tpl clientTpl string + //go:embed ts_client.tpl + tsClientTpl string //go:embed python_client.tpl pythonClientTpl string ) @@ -52,7 +54,7 @@ func main() { panic(err) } - err = writeTemplate("../go/permissions/servicepermissions.go", servicePermissionsTpl, perms) + err = writeTemplate("../go/permissions/servicepermissions.go", servicePermissionsTpl, perms, sprig.FuncMap()) if err != nil { panic(err) } @@ -64,7 +66,24 @@ func main() { panic(err) } - err = writeTemplate("../go/client/client.go", clientTpl, svcs) + funcs := sprig.FuncMap() + funcs["lowerFirst"] = func(s string) string { + if len(s) == 0 { + return s + } + // If the string is all uppercase (e.g. "IP", "VPN", "BMC"), lowercase the entire thing + if s == strings.ToUpper(s) { + return strings.ToLower(s) + } + return strings.ToLower(s[:1]) + s[1:] + } + + err = writeTemplate("../go/client/client.go", clientTpl, svcs, funcs) + if err != nil { + panic(err) + } + + err = writeTemplate("../js/client.ts", tsClientTpl, svcs, funcs) if err != nil { panic(err) } @@ -281,8 +300,8 @@ func svcs(root string) (map[string]api, error) { return result, nil } -func writeTemplate(dest, text string, data any) error { - t, err := template.New("").Funcs(sprig.FuncMap()).Parse(text) +func writeTemplate(dest, text string, data any, funcs template.FuncMap) error { + t, err := template.New("").Funcs(funcs).Parse(text) if err != nil { return err } @@ -292,15 +311,22 @@ func writeTemplate(dest, text string, data any) error { return err } - p, err := format.Source(buf.Bytes()) - if err != nil { - return err + var p []byte + if strings.HasSuffix(dest, ".go") { + var ferr error + p, ferr = format.Source(buf.Bytes()) + if ferr != nil { + return ferr + } + } else { + p = buf.Bytes() } fmt.Println("wrote " + dest) return os.WriteFile(dest, p, 0755) // nolint:gosec } + func writePythonTemplate(dest, text string, data any) error { t, err := template.New("").Funcs(sprig.FuncMap()).Parse(text) if err != nil { diff --git a/generate/ts_client.tpl b/generate/ts_client.tpl new file mode 100644 index 00000000..47a81fa8 --- /dev/null +++ b/generate/ts_client.tpl @@ -0,0 +1,106 @@ +// Code generated by generate_clients.go. DO NOT EDIT. + +import { createClient, Interceptor, Transport } from "@connectrpc/connect"; +import type { Client as ConnectClient } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-web"; +{{ range $name, $api := . }} +{{ range $svc := $api.Services }} +import { {{ $svc.Name }} as {{ $name | title }}{{ $svc.Name }} } from ".{{ $api.Path }}/{{ $svc.FileName | trimSuffix ".proto" }}_pb"; +{{ end }} +{{ end }} + +export interface ClientConfig { + baseUrl: string; + token?: string; + interceptors?: Interceptor[]; +} + +export interface Client { +{{ range $name, $api := . }} + {{ $name }}(): {{ $name | title }}; +{{ end }} +} + +{{ range $name, $api := . }} +export interface {{ $name | title }} { +{{ range $svc := $api.Services }} + {{ $svc.Name | trimSuffix "Service" | title | lowerFirst }}(): ConnectClient; +{{ end }} +} + +{{ end }} + +function authInterceptor(token: string): Interceptor { + return (next) => async (req) => { + req.header.set("Authorization", `Bearer ${token}`); + return await next(req); + }; +} + +function buildTransport(config: ClientConfig): Transport { + const interceptors: Interceptor[] = []; + + if (config.token) { + interceptors.push(authInterceptor(config.token)); + } + + if (config.interceptors) { + interceptors.push(...config.interceptors); + } + + return createConnectTransport({ + baseUrl: config.baseUrl, + interceptors: interceptors.length > 0 ? interceptors : undefined, + defaultTimeoutMs: 30_000, + }); +} + +export function newClient(config: ClientConfig): Client { + const transport = buildTransport(config); + return new ClientImpl(transport); +} + +class ClientImpl implements Client { + private transport: Transport; + +{{ range $name, $api := . }} + private _{{ $name }}?: {{ $name | title }}Impl; +{{ end }} + + constructor(transport: Transport) { + this.transport = transport; + } + +{{ range $name, $api := . }} + {{ $name }}(): {{ $name | title }} { + if (!this._{{ $name }}) { + this._{{ $name }} = new {{ $name | title }}Impl(this.transport); + } + return this._{{ $name }}; + } +{{ end }} +} + +{{ range $name, $api := . }} +class {{ $name | title }}Impl implements {{ $name | title }} { + private transport: Transport; + +{{ range $svc := $api.Services }} + private _{{ $svc.Name | trimSuffix "Service" | title | lowerFirst }}?: ConnectClient; +{{ end }} + + constructor(transport: Transport) { + this.transport = transport; + } + +{{ range $svc := $api.Services }} + {{ $svc.Name | trimSuffix "Service" | title | lowerFirst }}(): ConnectClient { + if (!this._{{ $svc.Name | trimSuffix "Service" | title | lowerFirst }}) { + this._{{ $svc.Name | trimSuffix "Service" | title | lowerFirst }} = createClient({{ $name | title }}{{ $svc.Name }}, this.transport); + } + return this._{{ $svc.Name | trimSuffix "Service" | title | lowerFirst }}; + } +{{ end }} +} + +{{ end }} diff --git a/js/Makefile b/js/Makefile index daab8154..7c5059fb 100644 --- a/js/Makefile +++ b/js/Makefile @@ -17,3 +17,7 @@ ifeq ($(CI),true) yq e -o=json ".version" package.json endif cd .. && bun run build + +.PHONY: test +test: + bun test ./client-test.ts diff --git a/js/client-test.ts b/js/client-test.ts new file mode 100644 index 00000000..bf8fa251 --- /dev/null +++ b/js/client-test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "bun:test"; +import { create } from "@bufbuild/protobuf"; +import type { DescMessage, MessageShape } from "@bufbuild/protobuf"; +import { + VersionServiceGetRequestSchema, + VersionServiceGetResponseSchema, + VersionService, +} from "./metalstack/api/v2/version_pb"; +import type { UnaryResponse } from "@connectrpc/connect"; +import { newClient } from "./client"; +import { newTestInterceptor } from "./test-interceptor"; + +function unaryResponse( + schema: Req, + message: MessageShape, +): UnaryResponse { + return { + stream: false as const, + message: message as any, + header: new Headers(), + trailer: new Headers(), + service: undefined as any, + method: undefined as any, + }; +} + +describe("client", () => { + it("works with test interceptor", async () => { + const c = newClient({ + baseUrl: "http://this-is-just-for-testing", + interceptors: [ + newTestInterceptor([ + { + wantRequest: create(VersionServiceGetRequestSchema, {}), + wantRequestSchema: VersionServiceGetRequestSchema, + wantResponse: () => + unaryResponse(VersionServiceGetRequestSchema, { + version: { version: "1.0", revision: "", gitSha1: "", buildDate: "" }, + }), + }, + ]), + ], + }); + + const v = await c.apiv2().version().get({}); + expect(v.version?.version).toBe("1.0"); + }); +}); diff --git a/js/client.d.ts b/js/client.d.ts new file mode 100644 index 00000000..1e868edf --- /dev/null +++ b/js/client.d.ts @@ -0,0 +1,97 @@ +import { Interceptor } from "@connectrpc/connect"; +import type { Client as ConnectClient } from "@connectrpc/connect"; +import { AuditService as Adminv2AuditService } from "./metalstack/admin/v2/audit_pb"; +import { ComponentService as Adminv2ComponentService } from "./metalstack/admin/v2/component_pb"; +import { FilesystemService as Adminv2FilesystemService } from "./metalstack/admin/v2/filesystem_pb"; +import { ImageService as Adminv2ImageService } from "./metalstack/admin/v2/image_pb"; +import { IPService as Adminv2IPService } from "./metalstack/admin/v2/ip_pb"; +import { MachineService as Adminv2MachineService } from "./metalstack/admin/v2/machine_pb"; +import { NetworkService as Adminv2NetworkService } from "./metalstack/admin/v2/network_pb"; +import { PartitionService as Adminv2PartitionService } from "./metalstack/admin/v2/partition_pb"; +import { ProjectService as Adminv2ProjectService } from "./metalstack/admin/v2/project_pb"; +import { SizeService as Adminv2SizeService } from "./metalstack/admin/v2/size_pb"; +import { SizeImageConstraintService as Adminv2SizeImageConstraintService } from "./metalstack/admin/v2/size_imageconstraint_pb"; +import { SizeReservationService as Adminv2SizeReservationService } from "./metalstack/admin/v2/size_reservation_pb"; +import { SwitchService as Adminv2SwitchService } from "./metalstack/admin/v2/switch_pb"; +import { TaskService as Adminv2TaskService } from "./metalstack/admin/v2/task_pb"; +import { TenantService as Adminv2TenantService } from "./metalstack/admin/v2/tenant_pb"; +import { TokenService as Adminv2TokenService } from "./metalstack/admin/v2/token_pb"; +import { VPNService as Adminv2VPNService } from "./metalstack/admin/v2/vpn_pb"; +import { AuditService as Apiv2AuditService } from "./metalstack/api/v2/audit_pb"; +import { FilesystemService as Apiv2FilesystemService } from "./metalstack/api/v2/filesystem_pb"; +import { HealthService as Apiv2HealthService } from "./metalstack/api/v2/health_pb"; +import { ImageService as Apiv2ImageService } from "./metalstack/api/v2/image_pb"; +import { IPService as Apiv2IPService } from "./metalstack/api/v2/ip_pb"; +import { MachineService as Apiv2MachineService } from "./metalstack/api/v2/machine_pb"; +import { MethodService as Apiv2MethodService } from "./metalstack/api/v2/method_pb"; +import { NetworkService as Apiv2NetworkService } from "./metalstack/api/v2/network_pb"; +import { PartitionService as Apiv2PartitionService } from "./metalstack/api/v2/partition_pb"; +import { ProjectService as Apiv2ProjectService } from "./metalstack/api/v2/project_pb"; +import { SizeService as Apiv2SizeService } from "./metalstack/api/v2/size_pb"; +import { SizeImageConstraintService as Apiv2SizeImageConstraintService } from "./metalstack/api/v2/size_imageconstraint_pb"; +import { SizeReservationService as Apiv2SizeReservationService } from "./metalstack/api/v2/size_reservation_pb"; +import { TenantService as Apiv2TenantService } from "./metalstack/api/v2/tenant_pb"; +import { TokenService as Apiv2TokenService } from "./metalstack/api/v2/token_pb"; +import { UserService as Apiv2UserService } from "./metalstack/api/v2/user_pb"; +import { VersionService as Apiv2VersionService } from "./metalstack/api/v2/version_pb"; +import { BMCService as Infrav2BMCService } from "./metalstack/infra/v2/bmc_pb"; +import { BootService as Infrav2BootService } from "./metalstack/infra/v2/boot_pb"; +import { ComponentService as Infrav2ComponentService } from "./metalstack/infra/v2/component_pb"; +import { EventService as Infrav2EventService } from "./metalstack/infra/v2/event_pb"; +import { SwitchService as Infrav2SwitchService } from "./metalstack/infra/v2/switch_pb"; +export interface ClientConfig { + baseUrl: string; + token?: string; + interceptors?: Interceptor[]; +} +export interface Client { + adminv2(): Adminv2; + apiv2(): Apiv2; + infrav2(): Infrav2; +} +export interface Adminv2 { + audit(): ConnectClient; + component(): ConnectClient; + filesystem(): ConnectClient; + image(): ConnectClient; + ip(): ConnectClient; + machine(): ConnectClient; + network(): ConnectClient; + partition(): ConnectClient; + project(): ConnectClient; + size(): ConnectClient; + sizeImageConstraint(): ConnectClient; + sizeReservation(): ConnectClient; + switch(): ConnectClient; + task(): ConnectClient; + tenant(): ConnectClient; + token(): ConnectClient; + vpn(): ConnectClient; +} +export interface Apiv2 { + audit(): ConnectClient; + filesystem(): ConnectClient; + health(): ConnectClient; + image(): ConnectClient; + ip(): ConnectClient; + machine(): ConnectClient; + method(): ConnectClient; + network(): ConnectClient; + partition(): ConnectClient; + project(): ConnectClient; + size(): ConnectClient; + sizeImageConstraint(): ConnectClient; + sizeReservation(): ConnectClient; + tenant(): ConnectClient; + token(): ConnectClient; + user(): ConnectClient; + version(): ConnectClient; +} +export interface Infrav2 { + bmc(): ConnectClient; + boot(): ConnectClient; + component(): ConnectClient; + event(): ConnectClient; + switch(): ConnectClient; +} +export declare function newClient(config: ClientConfig): Client; diff --git a/js/client.js b/js/client.js new file mode 100644 index 00000000..f04ea75e --- /dev/null +++ b/js/client.js @@ -0,0 +1,347 @@ +// Code generated by generate_clients.go. DO NOT EDIT. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { createClient } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-web"; +import { AuditService as Adminv2AuditService } from "./metalstack/admin/v2/audit_pb"; +import { ComponentService as Adminv2ComponentService } from "./metalstack/admin/v2/component_pb"; +import { FilesystemService as Adminv2FilesystemService } from "./metalstack/admin/v2/filesystem_pb"; +import { ImageService as Adminv2ImageService } from "./metalstack/admin/v2/image_pb"; +import { IPService as Adminv2IPService } from "./metalstack/admin/v2/ip_pb"; +import { MachineService as Adminv2MachineService } from "./metalstack/admin/v2/machine_pb"; +import { NetworkService as Adminv2NetworkService } from "./metalstack/admin/v2/network_pb"; +import { PartitionService as Adminv2PartitionService } from "./metalstack/admin/v2/partition_pb"; +import { ProjectService as Adminv2ProjectService } from "./metalstack/admin/v2/project_pb"; +import { SizeService as Adminv2SizeService } from "./metalstack/admin/v2/size_pb"; +import { SizeImageConstraintService as Adminv2SizeImageConstraintService } from "./metalstack/admin/v2/size_imageconstraint_pb"; +import { SizeReservationService as Adminv2SizeReservationService } from "./metalstack/admin/v2/size_reservation_pb"; +import { SwitchService as Adminv2SwitchService } from "./metalstack/admin/v2/switch_pb"; +import { TaskService as Adminv2TaskService } from "./metalstack/admin/v2/task_pb"; +import { TenantService as Adminv2TenantService } from "./metalstack/admin/v2/tenant_pb"; +import { TokenService as Adminv2TokenService } from "./metalstack/admin/v2/token_pb"; +import { VPNService as Adminv2VPNService } from "./metalstack/admin/v2/vpn_pb"; +import { AuditService as Apiv2AuditService } from "./metalstack/api/v2/audit_pb"; +import { FilesystemService as Apiv2FilesystemService } from "./metalstack/api/v2/filesystem_pb"; +import { HealthService as Apiv2HealthService } from "./metalstack/api/v2/health_pb"; +import { ImageService as Apiv2ImageService } from "./metalstack/api/v2/image_pb"; +import { IPService as Apiv2IPService } from "./metalstack/api/v2/ip_pb"; +import { MachineService as Apiv2MachineService } from "./metalstack/api/v2/machine_pb"; +import { MethodService as Apiv2MethodService } from "./metalstack/api/v2/method_pb"; +import { NetworkService as Apiv2NetworkService } from "./metalstack/api/v2/network_pb"; +import { PartitionService as Apiv2PartitionService } from "./metalstack/api/v2/partition_pb"; +import { ProjectService as Apiv2ProjectService } from "./metalstack/api/v2/project_pb"; +import { SizeService as Apiv2SizeService } from "./metalstack/api/v2/size_pb"; +import { SizeImageConstraintService as Apiv2SizeImageConstraintService } from "./metalstack/api/v2/size_imageconstraint_pb"; +import { SizeReservationService as Apiv2SizeReservationService } from "./metalstack/api/v2/size_reservation_pb"; +import { TenantService as Apiv2TenantService } from "./metalstack/api/v2/tenant_pb"; +import { TokenService as Apiv2TokenService } from "./metalstack/api/v2/token_pb"; +import { UserService as Apiv2UserService } from "./metalstack/api/v2/user_pb"; +import { VersionService as Apiv2VersionService } from "./metalstack/api/v2/version_pb"; +import { BMCService as Infrav2BMCService } from "./metalstack/infra/v2/bmc_pb"; +import { BootService as Infrav2BootService } from "./metalstack/infra/v2/boot_pb"; +import { ComponentService as Infrav2ComponentService } from "./metalstack/infra/v2/component_pb"; +import { EventService as Infrav2EventService } from "./metalstack/infra/v2/event_pb"; +import { SwitchService as Infrav2SwitchService } from "./metalstack/infra/v2/switch_pb"; +function authInterceptor(token) { + return (next) => (req) => __awaiter(this, void 0, void 0, function* () { + req.header.set("Authorization", `Bearer ${token}`); + return yield next(req); + }); +} +function buildTransport(config) { + const interceptors = []; + if (config.token) { + interceptors.push(authInterceptor(config.token)); + } + if (config.interceptors) { + interceptors.push(...config.interceptors); + } + return createConnectTransport({ + baseUrl: config.baseUrl, + interceptors: interceptors.length > 0 ? interceptors : undefined, + defaultTimeoutMs: 30000, + }); +} +export function newClient(config) { + const transport = buildTransport(config); + return new ClientImpl(transport); +} +class ClientImpl { + constructor(transport) { + this.transport = transport; + } + adminv2() { + if (!this._adminv2) { + this._adminv2 = new Adminv2Impl(this.transport); + } + return this._adminv2; + } + apiv2() { + if (!this._apiv2) { + this._apiv2 = new Apiv2Impl(this.transport); + } + return this._apiv2; + } + infrav2() { + if (!this._infrav2) { + this._infrav2 = new Infrav2Impl(this.transport); + } + return this._infrav2; + } +} +class Adminv2Impl { + constructor(transport) { + this.transport = transport; + } + audit() { + if (!this._audit) { + this._audit = createClient(Adminv2AuditService, this.transport); + } + return this._audit; + } + component() { + if (!this._component) { + this._component = createClient(Adminv2ComponentService, this.transport); + } + return this._component; + } + filesystem() { + if (!this._filesystem) { + this._filesystem = createClient(Adminv2FilesystemService, this.transport); + } + return this._filesystem; + } + image() { + if (!this._image) { + this._image = createClient(Adminv2ImageService, this.transport); + } + return this._image; + } + ip() { + if (!this._ip) { + this._ip = createClient(Adminv2IPService, this.transport); + } + return this._ip; + } + machine() { + if (!this._machine) { + this._machine = createClient(Adminv2MachineService, this.transport); + } + return this._machine; + } + network() { + if (!this._network) { + this._network = createClient(Adminv2NetworkService, this.transport); + } + return this._network; + } + partition() { + if (!this._partition) { + this._partition = createClient(Adminv2PartitionService, this.transport); + } + return this._partition; + } + project() { + if (!this._project) { + this._project = createClient(Adminv2ProjectService, this.transport); + } + return this._project; + } + size() { + if (!this._size) { + this._size = createClient(Adminv2SizeService, this.transport); + } + return this._size; + } + sizeImageConstraint() { + if (!this._sizeImageConstraint) { + this._sizeImageConstraint = createClient(Adminv2SizeImageConstraintService, this.transport); + } + return this._sizeImageConstraint; + } + sizeReservation() { + if (!this._sizeReservation) { + this._sizeReservation = createClient(Adminv2SizeReservationService, this.transport); + } + return this._sizeReservation; + } + switch() { + if (!this._switch) { + this._switch = createClient(Adminv2SwitchService, this.transport); + } + return this._switch; + } + task() { + if (!this._task) { + this._task = createClient(Adminv2TaskService, this.transport); + } + return this._task; + } + tenant() { + if (!this._tenant) { + this._tenant = createClient(Adminv2TenantService, this.transport); + } + return this._tenant; + } + token() { + if (!this._token) { + this._token = createClient(Adminv2TokenService, this.transport); + } + return this._token; + } + vpn() { + if (!this._vpn) { + this._vpn = createClient(Adminv2VPNService, this.transport); + } + return this._vpn; + } +} +class Apiv2Impl { + constructor(transport) { + this.transport = transport; + } + audit() { + if (!this._audit) { + this._audit = createClient(Apiv2AuditService, this.transport); + } + return this._audit; + } + filesystem() { + if (!this._filesystem) { + this._filesystem = createClient(Apiv2FilesystemService, this.transport); + } + return this._filesystem; + } + health() { + if (!this._health) { + this._health = createClient(Apiv2HealthService, this.transport); + } + return this._health; + } + image() { + if (!this._image) { + this._image = createClient(Apiv2ImageService, this.transport); + } + return this._image; + } + ip() { + if (!this._ip) { + this._ip = createClient(Apiv2IPService, this.transport); + } + return this._ip; + } + machine() { + if (!this._machine) { + this._machine = createClient(Apiv2MachineService, this.transport); + } + return this._machine; + } + method() { + if (!this._method) { + this._method = createClient(Apiv2MethodService, this.transport); + } + return this._method; + } + network() { + if (!this._network) { + this._network = createClient(Apiv2NetworkService, this.transport); + } + return this._network; + } + partition() { + if (!this._partition) { + this._partition = createClient(Apiv2PartitionService, this.transport); + } + return this._partition; + } + project() { + if (!this._project) { + this._project = createClient(Apiv2ProjectService, this.transport); + } + return this._project; + } + size() { + if (!this._size) { + this._size = createClient(Apiv2SizeService, this.transport); + } + return this._size; + } + sizeImageConstraint() { + if (!this._sizeImageConstraint) { + this._sizeImageConstraint = createClient(Apiv2SizeImageConstraintService, this.transport); + } + return this._sizeImageConstraint; + } + sizeReservation() { + if (!this._sizeReservation) { + this._sizeReservation = createClient(Apiv2SizeReservationService, this.transport); + } + return this._sizeReservation; + } + tenant() { + if (!this._tenant) { + this._tenant = createClient(Apiv2TenantService, this.transport); + } + return this._tenant; + } + token() { + if (!this._token) { + this._token = createClient(Apiv2TokenService, this.transport); + } + return this._token; + } + user() { + if (!this._user) { + this._user = createClient(Apiv2UserService, this.transport); + } + return this._user; + } + version() { + if (!this._version) { + this._version = createClient(Apiv2VersionService, this.transport); + } + return this._version; + } +} +class Infrav2Impl { + constructor(transport) { + this.transport = transport; + } + bmc() { + if (!this._bmc) { + this._bmc = createClient(Infrav2BMCService, this.transport); + } + return this._bmc; + } + boot() { + if (!this._boot) { + this._boot = createClient(Infrav2BootService, this.transport); + } + return this._boot; + } + component() { + if (!this._component) { + this._component = createClient(Infrav2ComponentService, this.transport); + } + return this._component; + } + event() { + if (!this._event) { + this._event = createClient(Infrav2EventService, this.transport); + } + return this._event; + } + switch() { + if (!this._switch) { + this._switch = createClient(Infrav2SwitchService, this.transport); + } + return this._switch; + } +} diff --git a/js/client.ts b/js/client.ts new file mode 100755 index 00000000..1327b1ed --- /dev/null +++ b/js/client.ts @@ -0,0 +1,662 @@ +// Code generated by generate_clients.go. DO NOT EDIT. + +import { createClient, Interceptor, Transport } from "@connectrpc/connect"; +import type { Client as ConnectClient } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-web"; + + +import { AuditService as Adminv2AuditService } from "./metalstack/admin/v2/audit_pb"; + +import { ComponentService as Adminv2ComponentService } from "./metalstack/admin/v2/component_pb"; + +import { FilesystemService as Adminv2FilesystemService } from "./metalstack/admin/v2/filesystem_pb"; + +import { ImageService as Adminv2ImageService } from "./metalstack/admin/v2/image_pb"; + +import { IPService as Adminv2IPService } from "./metalstack/admin/v2/ip_pb"; + +import { MachineService as Adminv2MachineService } from "./metalstack/admin/v2/machine_pb"; + +import { NetworkService as Adminv2NetworkService } from "./metalstack/admin/v2/network_pb"; + +import { PartitionService as Adminv2PartitionService } from "./metalstack/admin/v2/partition_pb"; + +import { ProjectService as Adminv2ProjectService } from "./metalstack/admin/v2/project_pb"; + +import { SizeService as Adminv2SizeService } from "./metalstack/admin/v2/size_pb"; + +import { SizeImageConstraintService as Adminv2SizeImageConstraintService } from "./metalstack/admin/v2/size_imageconstraint_pb"; + +import { SizeReservationService as Adminv2SizeReservationService } from "./metalstack/admin/v2/size_reservation_pb"; + +import { SwitchService as Adminv2SwitchService } from "./metalstack/admin/v2/switch_pb"; + +import { TaskService as Adminv2TaskService } from "./metalstack/admin/v2/task_pb"; + +import { TenantService as Adminv2TenantService } from "./metalstack/admin/v2/tenant_pb"; + +import { TokenService as Adminv2TokenService } from "./metalstack/admin/v2/token_pb"; + +import { VPNService as Adminv2VPNService } from "./metalstack/admin/v2/vpn_pb"; + + + +import { AuditService as Apiv2AuditService } from "./metalstack/api/v2/audit_pb"; + +import { FilesystemService as Apiv2FilesystemService } from "./metalstack/api/v2/filesystem_pb"; + +import { HealthService as Apiv2HealthService } from "./metalstack/api/v2/health_pb"; + +import { ImageService as Apiv2ImageService } from "./metalstack/api/v2/image_pb"; + +import { IPService as Apiv2IPService } from "./metalstack/api/v2/ip_pb"; + +import { MachineService as Apiv2MachineService } from "./metalstack/api/v2/machine_pb"; + +import { MethodService as Apiv2MethodService } from "./metalstack/api/v2/method_pb"; + +import { NetworkService as Apiv2NetworkService } from "./metalstack/api/v2/network_pb"; + +import { PartitionService as Apiv2PartitionService } from "./metalstack/api/v2/partition_pb"; + +import { ProjectService as Apiv2ProjectService } from "./metalstack/api/v2/project_pb"; + +import { SizeService as Apiv2SizeService } from "./metalstack/api/v2/size_pb"; + +import { SizeImageConstraintService as Apiv2SizeImageConstraintService } from "./metalstack/api/v2/size_imageconstraint_pb"; + +import { SizeReservationService as Apiv2SizeReservationService } from "./metalstack/api/v2/size_reservation_pb"; + +import { TenantService as Apiv2TenantService } from "./metalstack/api/v2/tenant_pb"; + +import { TokenService as Apiv2TokenService } from "./metalstack/api/v2/token_pb"; + +import { UserService as Apiv2UserService } from "./metalstack/api/v2/user_pb"; + +import { VersionService as Apiv2VersionService } from "./metalstack/api/v2/version_pb"; + + + +import { BMCService as Infrav2BMCService } from "./metalstack/infra/v2/bmc_pb"; + +import { BootService as Infrav2BootService } from "./metalstack/infra/v2/boot_pb"; + +import { ComponentService as Infrav2ComponentService } from "./metalstack/infra/v2/component_pb"; + +import { EventService as Infrav2EventService } from "./metalstack/infra/v2/event_pb"; + +import { SwitchService as Infrav2SwitchService } from "./metalstack/infra/v2/switch_pb"; + + + +export interface ClientConfig { + baseUrl: string; + token?: string; + interceptors?: Interceptor[]; +} + +export interface Client { + + adminv2(): Adminv2; + + apiv2(): Apiv2; + + infrav2(): Infrav2; + +} + + +export interface Adminv2 { + + audit(): ConnectClient; + + component(): ConnectClient; + + filesystem(): ConnectClient; + + image(): ConnectClient; + + ip(): ConnectClient; + + machine(): ConnectClient; + + network(): ConnectClient; + + partition(): ConnectClient; + + project(): ConnectClient; + + size(): ConnectClient; + + sizeImageConstraint(): ConnectClient; + + sizeReservation(): ConnectClient; + + switch(): ConnectClient; + + task(): ConnectClient; + + tenant(): ConnectClient; + + token(): ConnectClient; + + vpn(): ConnectClient; + +} + + +export interface Apiv2 { + + audit(): ConnectClient; + + filesystem(): ConnectClient; + + health(): ConnectClient; + + image(): ConnectClient; + + ip(): ConnectClient; + + machine(): ConnectClient; + + method(): ConnectClient; + + network(): ConnectClient; + + partition(): ConnectClient; + + project(): ConnectClient; + + size(): ConnectClient; + + sizeImageConstraint(): ConnectClient; + + sizeReservation(): ConnectClient; + + tenant(): ConnectClient; + + token(): ConnectClient; + + user(): ConnectClient; + + version(): ConnectClient; + +} + + +export interface Infrav2 { + + bmc(): ConnectClient; + + boot(): ConnectClient; + + component(): ConnectClient; + + event(): ConnectClient; + + switch(): ConnectClient; + +} + + + +function authInterceptor(token: string): Interceptor { + return (next) => async (req) => { + req.header.set("Authorization", `Bearer ${token}`); + return await next(req); + }; +} + +function buildTransport(config: ClientConfig): Transport { + const interceptors: Interceptor[] = []; + + if (config.token) { + interceptors.push(authInterceptor(config.token)); + } + + if (config.interceptors) { + interceptors.push(...config.interceptors); + } + + return createConnectTransport({ + baseUrl: config.baseUrl, + interceptors: interceptors.length > 0 ? interceptors : undefined, + defaultTimeoutMs: 30_000, + }); +} + +export function newClient(config: ClientConfig): Client { + const transport = buildTransport(config); + return new ClientImpl(transport); +} + +class ClientImpl implements Client { + private transport: Transport; + + + private _adminv2?: Adminv2Impl; + + private _apiv2?: Apiv2Impl; + + private _infrav2?: Infrav2Impl; + + + constructor(transport: Transport) { + this.transport = transport; + } + + + adminv2(): Adminv2 { + if (!this._adminv2) { + this._adminv2 = new Adminv2Impl(this.transport); + } + return this._adminv2; + } + + apiv2(): Apiv2 { + if (!this._apiv2) { + this._apiv2 = new Apiv2Impl(this.transport); + } + return this._apiv2; + } + + infrav2(): Infrav2 { + if (!this._infrav2) { + this._infrav2 = new Infrav2Impl(this.transport); + } + return this._infrav2; + } + +} + + +class Adminv2Impl implements Adminv2 { + private transport: Transport; + + + private _audit?: ConnectClient; + + private _component?: ConnectClient; + + private _filesystem?: ConnectClient; + + private _image?: ConnectClient; + + private _ip?: ConnectClient; + + private _machine?: ConnectClient; + + private _network?: ConnectClient; + + private _partition?: ConnectClient; + + private _project?: ConnectClient; + + private _size?: ConnectClient; + + private _sizeImageConstraint?: ConnectClient; + + private _sizeReservation?: ConnectClient; + + private _switch?: ConnectClient; + + private _task?: ConnectClient; + + private _tenant?: ConnectClient; + + private _token?: ConnectClient; + + private _vpn?: ConnectClient; + + + constructor(transport: Transport) { + this.transport = transport; + } + + + audit(): ConnectClient { + if (!this._audit) { + this._audit = createClient(Adminv2AuditService, this.transport); + } + return this._audit; + } + + component(): ConnectClient { + if (!this._component) { + this._component = createClient(Adminv2ComponentService, this.transport); + } + return this._component; + } + + filesystem(): ConnectClient { + if (!this._filesystem) { + this._filesystem = createClient(Adminv2FilesystemService, this.transport); + } + return this._filesystem; + } + + image(): ConnectClient { + if (!this._image) { + this._image = createClient(Adminv2ImageService, this.transport); + } + return this._image; + } + + ip(): ConnectClient { + if (!this._ip) { + this._ip = createClient(Adminv2IPService, this.transport); + } + return this._ip; + } + + machine(): ConnectClient { + if (!this._machine) { + this._machine = createClient(Adminv2MachineService, this.transport); + } + return this._machine; + } + + network(): ConnectClient { + if (!this._network) { + this._network = createClient(Adminv2NetworkService, this.transport); + } + return this._network; + } + + partition(): ConnectClient { + if (!this._partition) { + this._partition = createClient(Adminv2PartitionService, this.transport); + } + return this._partition; + } + + project(): ConnectClient { + if (!this._project) { + this._project = createClient(Adminv2ProjectService, this.transport); + } + return this._project; + } + + size(): ConnectClient { + if (!this._size) { + this._size = createClient(Adminv2SizeService, this.transport); + } + return this._size; + } + + sizeImageConstraint(): ConnectClient { + if (!this._sizeImageConstraint) { + this._sizeImageConstraint = createClient(Adminv2SizeImageConstraintService, this.transport); + } + return this._sizeImageConstraint; + } + + sizeReservation(): ConnectClient { + if (!this._sizeReservation) { + this._sizeReservation = createClient(Adminv2SizeReservationService, this.transport); + } + return this._sizeReservation; + } + + switch(): ConnectClient { + if (!this._switch) { + this._switch = createClient(Adminv2SwitchService, this.transport); + } + return this._switch; + } + + task(): ConnectClient { + if (!this._task) { + this._task = createClient(Adminv2TaskService, this.transport); + } + return this._task; + } + + tenant(): ConnectClient { + if (!this._tenant) { + this._tenant = createClient(Adminv2TenantService, this.transport); + } + return this._tenant; + } + + token(): ConnectClient { + if (!this._token) { + this._token = createClient(Adminv2TokenService, this.transport); + } + return this._token; + } + + vpn(): ConnectClient { + if (!this._vpn) { + this._vpn = createClient(Adminv2VPNService, this.transport); + } + return this._vpn; + } + +} + + +class Apiv2Impl implements Apiv2 { + private transport: Transport; + + + private _audit?: ConnectClient; + + private _filesystem?: ConnectClient; + + private _health?: ConnectClient; + + private _image?: ConnectClient; + + private _ip?: ConnectClient; + + private _machine?: ConnectClient; + + private _method?: ConnectClient; + + private _network?: ConnectClient; + + private _partition?: ConnectClient; + + private _project?: ConnectClient; + + private _size?: ConnectClient; + + private _sizeImageConstraint?: ConnectClient; + + private _sizeReservation?: ConnectClient; + + private _tenant?: ConnectClient; + + private _token?: ConnectClient; + + private _user?: ConnectClient; + + private _version?: ConnectClient; + + + constructor(transport: Transport) { + this.transport = transport; + } + + + audit(): ConnectClient { + if (!this._audit) { + this._audit = createClient(Apiv2AuditService, this.transport); + } + return this._audit; + } + + filesystem(): ConnectClient { + if (!this._filesystem) { + this._filesystem = createClient(Apiv2FilesystemService, this.transport); + } + return this._filesystem; + } + + health(): ConnectClient { + if (!this._health) { + this._health = createClient(Apiv2HealthService, this.transport); + } + return this._health; + } + + image(): ConnectClient { + if (!this._image) { + this._image = createClient(Apiv2ImageService, this.transport); + } + return this._image; + } + + ip(): ConnectClient { + if (!this._ip) { + this._ip = createClient(Apiv2IPService, this.transport); + } + return this._ip; + } + + machine(): ConnectClient { + if (!this._machine) { + this._machine = createClient(Apiv2MachineService, this.transport); + } + return this._machine; + } + + method(): ConnectClient { + if (!this._method) { + this._method = createClient(Apiv2MethodService, this.transport); + } + return this._method; + } + + network(): ConnectClient { + if (!this._network) { + this._network = createClient(Apiv2NetworkService, this.transport); + } + return this._network; + } + + partition(): ConnectClient { + if (!this._partition) { + this._partition = createClient(Apiv2PartitionService, this.transport); + } + return this._partition; + } + + project(): ConnectClient { + if (!this._project) { + this._project = createClient(Apiv2ProjectService, this.transport); + } + return this._project; + } + + size(): ConnectClient { + if (!this._size) { + this._size = createClient(Apiv2SizeService, this.transport); + } + return this._size; + } + + sizeImageConstraint(): ConnectClient { + if (!this._sizeImageConstraint) { + this._sizeImageConstraint = createClient(Apiv2SizeImageConstraintService, this.transport); + } + return this._sizeImageConstraint; + } + + sizeReservation(): ConnectClient { + if (!this._sizeReservation) { + this._sizeReservation = createClient(Apiv2SizeReservationService, this.transport); + } + return this._sizeReservation; + } + + tenant(): ConnectClient { + if (!this._tenant) { + this._tenant = createClient(Apiv2TenantService, this.transport); + } + return this._tenant; + } + + token(): ConnectClient { + if (!this._token) { + this._token = createClient(Apiv2TokenService, this.transport); + } + return this._token; + } + + user(): ConnectClient { + if (!this._user) { + this._user = createClient(Apiv2UserService, this.transport); + } + return this._user; + } + + version(): ConnectClient { + if (!this._version) { + this._version = createClient(Apiv2VersionService, this.transport); + } + return this._version; + } + +} + + +class Infrav2Impl implements Infrav2 { + private transport: Transport; + + + private _bmc?: ConnectClient; + + private _boot?: ConnectClient; + + private _component?: ConnectClient; + + private _event?: ConnectClient; + + private _switch?: ConnectClient; + + + constructor(transport: Transport) { + this.transport = transport; + } + + + bmc(): ConnectClient { + if (!this._bmc) { + this._bmc = createClient(Infrav2BMCService, this.transport); + } + return this._bmc; + } + + boot(): ConnectClient { + if (!this._boot) { + this._boot = createClient(Infrav2BootService, this.transport); + } + return this._boot; + } + + component(): ConnectClient { + if (!this._component) { + this._component = createClient(Infrav2ComponentService, this.transport); + } + return this._component; + } + + event(): ConnectClient { + if (!this._event) { + this._event = createClient(Infrav2EventService, this.transport); + } + return this._event; + } + + switch(): ConnectClient { + if (!this._switch) { + this._switch = createClient(Infrav2SwitchService, this.transport); + } + return this._switch; + } + +} + + diff --git a/js/test-interceptor.ts b/js/test-interceptor.ts new file mode 100644 index 00000000..877ca0fb --- /dev/null +++ b/js/test-interceptor.ts @@ -0,0 +1,49 @@ +import type { Interceptor, UnaryRequest, UnaryResponse } from "@connectrpc/connect"; +import { equals } from "@bufbuild/protobuf"; +import type { DescMessage, MessageShape } from "@bufbuild/protobuf"; + +export type ClientCall = { + wantRequest: MessageShape; + wantRequestSchema: Req; + wantResponse?: () => UnaryResponse; + wantError?: Error; +}; + +export function newTestInterceptor(calls: ClientCall[]): Interceptor { + let count = 0; + + return (next) => async (req) => { + const reqAny = req as UnaryRequest; + + const expected = calls[count]; + if (!expected) { + throw new Error( + `received an unexpected client call of type ${reqAny.method?.input?.typeName ?? "unknown"}`, + ); + } + + count++; + + if ( + !equals( + expected.wantRequestSchema, + reqAny.message, + expected.wantRequest as MessageShape, + ) + ) { + throw new Error( + `request mismatch for call #${count - 1}: got ${JSON.stringify(reqAny.message)}`, + ); + } + + if (expected.wantError) { + throw expected.wantError; + } + + if (expected.wantResponse) { + return expected.wantResponse(); + } + + throw new Error(`no wantResponse or wantError configured for call #${count - 1}`); + }; +}