Merge pull request #15755 from Budibase/BUDI-9127/edit-delete-configs

Implement edit and delete config endpoints
This commit is contained in:
Adria Navarro 2025-03-18 20:52:19 +01:00 committed by GitHub
commit e4eb09b203
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 230 additions and 19 deletions

View File

@ -1,3 +1,3 @@
import { CreateOAuth2ConfigRequest } from "@budibase/types"
import { UpsertOAuth2ConfigRequest } from "@budibase/types"
export interface CreateOAuth2Config extends CreateOAuth2ConfigRequest {}
export interface CreateOAuth2Config extends UpsertOAuth2ConfigRequest {}

View File

@ -1,16 +1,16 @@
import {
FetchOAuth2ConfigsResponse,
CreateOAuth2ConfigResponse,
OAuth2ConfigResponse,
CreateOAuth2ConfigRequest,
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse,
} from "@budibase/types"
import { BaseAPIClient } from "./types"
export interface OAuth2Endpoints {
fetch: () => Promise<OAuth2ConfigResponse[]>
create: (
config: CreateOAuth2ConfigRequest
) => Promise<CreateOAuth2ConfigResponse>
config: UpsertOAuth2ConfigRequest
) => Promise<UpsertOAuth2ConfigResponse>
}
export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
@ -33,8 +33,8 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
*/
create: async config => {
return await API.post<
CreateOAuth2ConfigRequest,
CreateOAuth2ConfigResponse
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse
>({
url: `/api/oauth2`,
body: {

View File

@ -1,6 +1,6 @@
import {
CreateOAuth2ConfigRequest,
CreateOAuth2ConfigResponse,
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse,
Ctx,
FetchOAuth2ConfigsResponse,
OAuth2Config,
@ -22,7 +22,7 @@ export async function fetch(ctx: Ctx<void, FetchOAuth2ConfigsResponse>) {
}
export async function create(
ctx: Ctx<CreateOAuth2ConfigRequest, CreateOAuth2ConfigResponse>
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
) {
const { body } = ctx.request
const newConfig: RequiredKeys<Omit<OAuth2Config, "id">> = {
@ -36,3 +36,28 @@ export async function create(
ctx.status = 201
ctx.body = { config }
}
export async function edit(
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
) {
const { body } = ctx.request
const toUpdate: RequiredKeys<OAuth2Config> = {
id: ctx.params.id,
name: body.name,
url: body.url,
clientId: ctx.clientId,
clientSecret: ctx.clientSecret,
}
const config = await sdk.oauth2.update(toUpdate)
ctx.body = { config }
}
export async function remove(
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
) {
const configToRemove = ctx.params.id
await sdk.oauth2.remove(configToRemove)
ctx.status = 204
}

View File

@ -1,8 +1,22 @@
import Router from "@koa/router"
import { PermissionType } from "@budibase/types"
import { middleware } from "@budibase/backend-core"
import authorized from "../../middleware/authorized"
import * as controller from "../controllers/oauth2"
import Joi from "joi"
function oAuth2ConfigValidator() {
return middleware.joiValidator.body(
Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
clientId: Joi.string().required(),
clientSecret: Joi.string().required(),
}),
{ allowUnknown: false }
)
}
const router: Router = new Router()
@ -10,7 +24,19 @@ router.get("/api/oauth2", authorized(PermissionType.BUILDER), controller.fetch)
router.post(
"/api/oauth2",
authorized(PermissionType.BUILDER),
oAuth2ConfigValidator(),
controller.create
)
router.put(
"/api/oauth2/:id",
authorized(PermissionType.BUILDER),
oAuth2ConfigValidator(),
controller.edit
)
router.delete(
"/api/oauth2/:id",
authorized(PermissionType.BUILDER),
controller.remove
)
export default router

View File

@ -1,11 +1,16 @@
import { CreateOAuth2ConfigRequest, VirtualDocumentType } from "@budibase/types"
import {
OAuth2Config,
UpsertOAuth2ConfigRequest,
VirtualDocumentType,
} from "@budibase/types"
import * as setup from "./utilities"
import { generator } from "@budibase/backend-core/tests"
import _ from "lodash/fp"
describe("/oauth2", () => {
let config = setup.getConfig()
function makeOAuth2Config(): CreateOAuth2ConfigRequest {
function makeOAuth2Config(): UpsertOAuth2ConfigRequest {
return {
name: generator.guid(),
url: generator.url(),
@ -92,4 +97,96 @@ describe("/oauth2", () => {
])
})
})
describe("update", () => {
let existingConfigs: OAuth2Config[] = []
beforeEach(async () => {
existingConfigs = []
for (let i = 0; i < 10; i++) {
const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id })
}
})
it("can update an existing configuration", async () => {
const { id: configId, ...configData } = _.sample(existingConfigs)!
await config.api.oauth2.update(configId, {
...configData,
name: "updated name",
})
const response = await config.api.oauth2.fetch()
expect(response.configs).toHaveLength(existingConfigs.length)
expect(response.configs).toEqual(
expect.arrayContaining([
{
id: configId,
name: "updated name",
url: configData.url,
},
])
)
})
it("throw if config not found", async () => {
await config.api.oauth2.update("unexisting", makeOAuth2Config(), {
status: 404,
body: { message: "OAuth2 config with id 'unexisting' not found." },
})
})
it("throws if trying to use an existing name", async () => {
const [config1, config2] = _.sampleSize(2, existingConfigs)
const { id: configId, ...configData } = config1
await config.api.oauth2.update(
configId,
{
...configData,
name: config2.name,
},
{
status: 400,
body: {
message: `OAuth2 config with name '${config2.name}' is already taken.`,
},
}
)
})
})
describe("delete", () => {
let existingConfigs: OAuth2Config[] = []
beforeEach(async () => {
existingConfigs = []
for (let i = 0; i < 5; i++) {
const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id })
}
})
it("can delete an existing configuration", async () => {
const { id: configId } = _.sample(existingConfigs)!
await config.api.oauth2.delete(configId, { status: 204 })
const response = await config.api.oauth2.fetch()
expect(response.configs).toHaveLength(existingConfigs.length - 1)
expect(response.configs.find(c => c.id === configId)).toBeUndefined()
})
it("throw if config not found", async () => {
await config.api.oauth2.delete("unexisting", {
status: 404,
body: { message: "OAuth2 config with id 'unexisting' not found." },
})
})
})
})

View File

@ -48,3 +48,49 @@ export async function get(id: string): Promise<OAuth2Config | undefined> {
const doc = await getDocument()
return doc?.configs?.[id]
}
export async function update(config: OAuth2Config): Promise<OAuth2Config> {
const db = context.getAppDB()
const doc: OAuth2Configs = (await getDocument(db)) ?? {
_id: DocumentType.OAUTH2_CONFIG,
configs: {},
}
if (!doc.configs[config.id]) {
throw new HTTPError(`OAuth2 config with id '${config.id}' not found.`, 404)
}
if (
Object.values(doc.configs).find(
c => c.name === config.name && c.id !== config.id
)
) {
throw new HTTPError(
`OAuth2 config with name '${config.name}' is already taken.`,
400
)
}
doc.configs[config.id] = {
...config,
}
await db.put(doc)
return doc.configs[config.id]
}
export async function remove(configId: string): Promise<void> {
const db = context.getAppDB()
const doc: OAuth2Configs = (await getDocument(db)) ?? {
_id: DocumentType.OAUTH2_CONFIG,
configs: {},
}
if (!doc.configs[configId]) {
throw new HTTPError(`OAuth2 config with id '${configId}' not found.`, 404)
}
delete doc.configs[configId]
await db.put(doc)
}

View File

@ -1,6 +1,6 @@
import {
CreateOAuth2ConfigRequest,
CreateOAuth2ConfigResponse,
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse,
FetchOAuth2ConfigsResponse,
} from "@budibase/types"
import { Expectations, TestAPI } from "./base"
@ -13,10 +13,10 @@ export class OAuth2API extends TestAPI {
}
create = async (
body: CreateOAuth2ConfigRequest,
body: UpsertOAuth2ConfigRequest,
expectations?: Expectations
) => {
return await this._post<CreateOAuth2ConfigResponse>("/api/oauth2", {
return await this._post<UpsertOAuth2ConfigResponse>("/api/oauth2", {
body,
expectations: {
status: expectations?.status ?? 201,
@ -24,4 +24,21 @@ export class OAuth2API extends TestAPI {
},
})
}
update = async (
id: string,
body: UpsertOAuth2ConfigRequest,
expectations?: Expectations
) => {
return await this._put<UpsertOAuth2ConfigResponse>(`/api/oauth2/${id}`, {
body,
expectations,
})
}
delete = async (id: string, expectations?: Expectations) => {
return await this._delete<void>(`/api/oauth2/${id}`, {
expectations,
})
}
}

View File

@ -7,13 +7,13 @@ export interface FetchOAuth2ConfigsResponse {
configs: OAuth2ConfigResponse[]
}
export interface CreateOAuth2ConfigRequest {
export interface UpsertOAuth2ConfigRequest {
name: string
url: string
clientId: string
clientSecret: string
}
export interface CreateOAuth2ConfigResponse {
export interface UpsertOAuth2ConfigResponse {
config: OAuth2ConfigResponse
}