Merge pull request #14152 from Budibase/BUDI-8428/row-action-crud
Row action CRUD endpoints
This commit is contained in:
commit
3411411c3f
|
@ -4,8 +4,9 @@ import { Ctx } from "@budibase/types"
|
||||||
function validate(
|
function validate(
|
||||||
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
||||||
property: string,
|
property: string,
|
||||||
opts: { errorPrefix: string } = { errorPrefix: `Invalid ${property}` }
|
opts?: { errorPrefix?: string; allowUnknown?: boolean }
|
||||||
) {
|
) {
|
||||||
|
const errorPrefix = opts?.errorPrefix ?? `Invalid ${property}`
|
||||||
// Return a Koa middleware function
|
// Return a Koa middleware function
|
||||||
return (ctx: Ctx, next: any) => {
|
return (ctx: Ctx, next: any) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
|
@ -28,10 +29,12 @@ function validate(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = schema.validate(params)
|
const { error } = schema.validate(params, {
|
||||||
|
allowUnknown: opts?.allowUnknown,
|
||||||
|
})
|
||||||
if (error) {
|
if (error) {
|
||||||
let message = error.message
|
let message = error.message
|
||||||
if (opts.errorPrefix) {
|
if (errorPrefix) {
|
||||||
message = `Invalid ${property} - ${message}`
|
message = `Invalid ${property} - ${message}`
|
||||||
}
|
}
|
||||||
ctx.throw(400, message)
|
ctx.throw(400, message)
|
||||||
|
@ -42,7 +45,7 @@ function validate(
|
||||||
|
|
||||||
export function body(
|
export function body(
|
||||||
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
||||||
opts?: { errorPrefix: string }
|
opts?: { errorPrefix?: string; allowUnknown?: boolean }
|
||||||
) {
|
) {
|
||||||
return validate(schema, "body", opts)
|
return validate(schema, "body", opts)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {
|
||||||
|
CreateRowActionRequest,
|
||||||
|
Ctx,
|
||||||
|
RowActionResponse,
|
||||||
|
RowActionsResponse,
|
||||||
|
UpdateRowActionRequest,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
|
async function getTable(ctx: Ctx) {
|
||||||
|
const { tableId } = ctx.params
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
if (!table) {
|
||||||
|
ctx.throw(404)
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function find(ctx: Ctx<void, RowActionsResponse>) {
|
||||||
|
const table = await getTable(ctx)
|
||||||
|
|
||||||
|
if (!(await sdk.rowActions.docExists(table._id!))) {
|
||||||
|
ctx.body = {
|
||||||
|
actions: {},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { actions } = await sdk.rowActions.get(table._id!)
|
||||||
|
const result: RowActionsResponse = {
|
||||||
|
actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
|
||||||
|
(acc, [key, action]) => ({
|
||||||
|
...acc,
|
||||||
|
[key]: { id: key, tableId: table._id!, ...action },
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ctx.body = result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(
|
||||||
|
ctx: Ctx<CreateRowActionRequest, RowActionResponse>
|
||||||
|
) {
|
||||||
|
const table = await getTable(ctx)
|
||||||
|
|
||||||
|
const createdAction = await sdk.rowActions.create(table._id!, {
|
||||||
|
name: ctx.request.body.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
tableId: table._id!,
|
||||||
|
...createdAction,
|
||||||
|
}
|
||||||
|
ctx.status = 201
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(
|
||||||
|
ctx: Ctx<UpdateRowActionRequest, RowActionResponse>
|
||||||
|
) {
|
||||||
|
const table = await getTable(ctx)
|
||||||
|
const { actionId } = ctx.params
|
||||||
|
|
||||||
|
const actions = await sdk.rowActions.update(table._id!, actionId, {
|
||||||
|
name: ctx.request.body.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
tableId: table._id!,
|
||||||
|
...actions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(ctx: Ctx<void, void>) {
|
||||||
|
const table = await getTable(ctx)
|
||||||
|
const { actionId } = ctx.params
|
||||||
|
|
||||||
|
await sdk.rowActions.remove(table._id!, actionId)
|
||||||
|
ctx.status = 204
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./crud"
|
||||||
|
export * from "./run"
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function run() {
|
||||||
|
throw new Error("Function not implemented.")
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import opsRoutes from "./ops"
|
||||||
import debugRoutes from "./debug"
|
import debugRoutes from "./debug"
|
||||||
import Router from "@koa/router"
|
import Router from "@koa/router"
|
||||||
import { api as pro } from "@budibase/pro"
|
import { api as pro } from "@budibase/pro"
|
||||||
|
import rowActionRoutes from "./rowAction"
|
||||||
|
|
||||||
export { default as staticRoutes } from "./static"
|
export { default as staticRoutes } from "./static"
|
||||||
export { default as publicRoutes } from "./public"
|
export { default as publicRoutes } from "./public"
|
||||||
|
@ -65,6 +66,7 @@ export const mainRoutes: Router[] = [
|
||||||
opsRoutes,
|
opsRoutes,
|
||||||
debugRoutes,
|
debugRoutes,
|
||||||
environmentVariableRoutes,
|
environmentVariableRoutes,
|
||||||
|
rowActionRoutes,
|
||||||
// these need to be handled last as they still use /api/:tableId
|
// these need to be handled last as they still use /api/:tableId
|
||||||
// this could be breaking as koa may recognise other routes as this
|
// this could be breaking as koa may recognise other routes as this
|
||||||
tableRoutes,
|
tableRoutes,
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import Router from "@koa/router"
|
||||||
|
import * as rowActionController from "../controllers/rowAction"
|
||||||
|
import { authorizedResource } from "../../middleware/authorized"
|
||||||
|
|
||||||
|
import { middleware, permissions } from "@budibase/backend-core"
|
||||||
|
import Joi from "joi"
|
||||||
|
|
||||||
|
const { PermissionLevel, PermissionType } = permissions
|
||||||
|
|
||||||
|
export function rowActionValidator() {
|
||||||
|
return middleware.joiValidator.body(
|
||||||
|
Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
{ allowUnknown: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const router: Router = new Router()
|
||||||
|
|
||||||
|
// CRUD endpoints
|
||||||
|
router
|
||||||
|
.get(
|
||||||
|
"/api/tables/:tableId/actions",
|
||||||
|
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||||
|
rowActionController.find
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/api/tables/:tableId/actions",
|
||||||
|
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||||
|
rowActionValidator(),
|
||||||
|
rowActionController.create
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"/api/tables/:tableId/actions/:actionId",
|
||||||
|
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||||
|
rowActionValidator(),
|
||||||
|
rowActionController.update
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/api/tables/:tableId/actions/:actionId",
|
||||||
|
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||||
|
rowActionController.remove
|
||||||
|
)
|
||||||
|
|
||||||
|
// Other endpoints
|
||||||
|
.post(
|
||||||
|
"/api/tables/:tableId/actions/:actionId/run",
|
||||||
|
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||||
|
rowActionController.run
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
|
@ -0,0 +1,298 @@
|
||||||
|
import _ from "lodash"
|
||||||
|
import tk from "timekeeper"
|
||||||
|
|
||||||
|
import { CreateRowActionRequest, RowActionResponse } from "@budibase/types"
|
||||||
|
import * as setup from "./utilities"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
|
describe("/rowsActions", () => {
|
||||||
|
const config = setup.getConfig()
|
||||||
|
|
||||||
|
let tableId: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
tk.freeze(new Date())
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const table = await config.api.table.save(setup.structures.basicTable())
|
||||||
|
tableId = table._id!
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
const createRowAction = config.api.rowAction.save
|
||||||
|
|
||||||
|
function createRowActionRequest(): CreateRowActionRequest {
|
||||||
|
return {
|
||||||
|
name: generator.string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRowActionRequests(count: number): CreateRowActionRequest[] {
|
||||||
|
return generator
|
||||||
|
.unique(() => generator.string(), count)
|
||||||
|
.map(name => ({ name }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function unauthorisedTests() {
|
||||||
|
it("returns unauthorised (401) for unauthenticated requests", async () => {
|
||||||
|
await createRowAction(
|
||||||
|
tableId,
|
||||||
|
createRowActionRequest(),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
body: {
|
||||||
|
message: "Session not authenticated",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ publicUser: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns forbidden (403) for non-builder users", async () => {
|
||||||
|
const user = await config.createUser({
|
||||||
|
builder: {},
|
||||||
|
})
|
||||||
|
await config.withUser(user, async () => {
|
||||||
|
await createRowAction(generator.guid(), createRowActionRequest(), {
|
||||||
|
status: 403,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects (404) for a non-existing table", async () => {
|
||||||
|
await createRowAction(generator.guid(), createRowActionRequest(), {
|
||||||
|
status: 404,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
unauthorisedTests()
|
||||||
|
|
||||||
|
it("creates new row actions for tables without existing actions", async () => {
|
||||||
|
const rowAction = createRowActionRequest()
|
||||||
|
const res = await createRowAction(tableId, rowAction, {
|
||||||
|
status: 201,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
id: expect.stringMatching(/^row_action_\w+/),
|
||||||
|
tableId: tableId,
|
||||||
|
...rowAction,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||||
|
actions: {
|
||||||
|
[res.id]: {
|
||||||
|
...rowAction,
|
||||||
|
id: res.id,
|
||||||
|
tableId: tableId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can create multiple row actions for the same table", async () => {
|
||||||
|
const rowActions = createRowActionRequests(3)
|
||||||
|
const responses: RowActionResponse[] = []
|
||||||
|
for (const action of rowActions) {
|
||||||
|
responses.push(await createRowAction(tableId, action))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||||
|
actions: {
|
||||||
|
[responses[0].id]: { ...rowActions[0], id: responses[0].id, tableId },
|
||||||
|
[responses[1].id]: { ...rowActions[1], id: responses[1].id, tableId },
|
||||||
|
[responses[2].id]: { ...rowActions[2], id: responses[2].id, tableId },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects with bad request when creating with no name", async () => {
|
||||||
|
const rowAction: CreateRowActionRequest = {
|
||||||
|
name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
await createRowAction(tableId, rowAction, {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
message: 'Invalid body - "name" is not allowed to be empty',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ignores not valid row action data", async () => {
|
||||||
|
const rowAction = createRowActionRequest()
|
||||||
|
const dirtyRowAction = {
|
||||||
|
...rowAction,
|
||||||
|
id: generator.guid(),
|
||||||
|
valueToIgnore: generator.string(),
|
||||||
|
}
|
||||||
|
const res = await createRowAction(tableId, dirtyRowAction, {
|
||||||
|
status: 201,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
tableId,
|
||||||
|
...rowAction,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||||
|
actions: {
|
||||||
|
[res.id]: {
|
||||||
|
id: res.id,
|
||||||
|
tableId: tableId,
|
||||||
|
...rowAction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("find", () => {
|
||||||
|
unauthorisedTests()
|
||||||
|
|
||||||
|
it("returns only the actions for the requested table", async () => {
|
||||||
|
const rowActions: RowActionResponse[] = []
|
||||||
|
for (const action of createRowActionRequests(3)) {
|
||||||
|
rowActions.push(await createRowAction(tableId, action))
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherTable = await config.api.table.save(
|
||||||
|
setup.structures.basicTable()
|
||||||
|
)
|
||||||
|
await createRowAction(otherTable._id!, createRowActionRequest())
|
||||||
|
|
||||||
|
const response = await config.api.rowAction.find(tableId)
|
||||||
|
expect(response).toEqual({
|
||||||
|
actions: {
|
||||||
|
[rowActions[0].id]: expect.any(Object),
|
||||||
|
[rowActions[1].id]: expect.any(Object),
|
||||||
|
[rowActions[2].id]: expect.any(Object),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns empty for tables without row actions", async () => {
|
||||||
|
const response = await config.api.rowAction.find(tableId)
|
||||||
|
expect(response).toEqual({
|
||||||
|
actions: {},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
unauthorisedTests()
|
||||||
|
|
||||||
|
it("can update existing actions", async () => {
|
||||||
|
for (const rowAction of createRowActionRequests(3)) {
|
||||||
|
await createRowAction(tableId, rowAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = await config.api.rowAction.find(tableId)
|
||||||
|
|
||||||
|
const [actionId, actionData] = _.sample(
|
||||||
|
Object.entries(persisted.actions)
|
||||||
|
)!
|
||||||
|
|
||||||
|
const updatedName = generator.string()
|
||||||
|
|
||||||
|
const res = await config.api.rowAction.update(tableId, actionId, {
|
||||||
|
...actionData,
|
||||||
|
name: updatedName,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
id: actionId,
|
||||||
|
tableId,
|
||||||
|
name: updatedName,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await config.api.rowAction.find(tableId)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
actions: expect.objectContaining({
|
||||||
|
[actionId]: {
|
||||||
|
...actionData,
|
||||||
|
name: updatedName,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws Bad Request when trying to update by a non-existing id", async () => {
|
||||||
|
await createRowAction(tableId, createRowActionRequest())
|
||||||
|
|
||||||
|
await config.api.rowAction.update(
|
||||||
|
tableId,
|
||||||
|
generator.guid(),
|
||||||
|
createRowActionRequest(),
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws Bad Request when trying to update by a via another table id", async () => {
|
||||||
|
const otherTable = await config.api.table.save(
|
||||||
|
setup.structures.basicTable()
|
||||||
|
)
|
||||||
|
await createRowAction(otherTable._id!, createRowActionRequest())
|
||||||
|
|
||||||
|
const action = await createRowAction(tableId, createRowActionRequest())
|
||||||
|
await config.api.rowAction.update(
|
||||||
|
otherTable._id!,
|
||||||
|
action.id,
|
||||||
|
createRowActionRequest(),
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("delete", () => {
|
||||||
|
unauthorisedTests()
|
||||||
|
|
||||||
|
it("can delete existing actions", async () => {
|
||||||
|
const actions: RowActionResponse[] = []
|
||||||
|
for (const rowAction of createRowActionRequests(3)) {
|
||||||
|
actions.push(await createRowAction(tableId, rowAction))
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionToDelete = _.sample(actions)!
|
||||||
|
|
||||||
|
await config.api.rowAction.delete(tableId, actionToDelete.id, {
|
||||||
|
status: 204,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await config.api.rowAction.find(tableId)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
actions: actions
|
||||||
|
.filter(a => a.id !== actionToDelete.id)
|
||||||
|
.reduce((acc, c) => ({ ...acc, [c.id]: expect.any(Object) }), {}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws Bad Request when trying to delete by a non-existing id", async () => {
|
||||||
|
await createRowAction(tableId, createRowActionRequest())
|
||||||
|
|
||||||
|
await config.api.rowAction.delete(tableId, generator.guid(), {
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws Bad Request when trying to delete by a via another table id", async () => {
|
||||||
|
const otherTable = await config.api.table.save(
|
||||||
|
setup.structures.basicTable()
|
||||||
|
)
|
||||||
|
await createRowAction(otherTable._id!, createRowActionRequest())
|
||||||
|
|
||||||
|
const action = await createRowAction(tableId, createRowActionRequest())
|
||||||
|
await config.api.rowAction.delete(otherTable._id!, action.id, {
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -349,3 +349,11 @@ export function isRelationshipColumn(
|
||||||
): column is RelationshipFieldMetadata {
|
): column is RelationshipFieldMetadata {
|
||||||
return column.type === FieldType.LINK
|
return column.type === FieldType.LINK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new row actions ID.
|
||||||
|
* @returns The new row actions ID which the row actions doc can be stored under.
|
||||||
|
*/
|
||||||
|
export function generateRowActionsID(tableId: string) {
|
||||||
|
return `${DocumentType.ROW_ACTIONS}${SEPARATOR}${tableId}`
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { context, HTTPError, utils } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
import { generateRowActionsID } from "../../db/utils"
|
||||||
|
import {
|
||||||
|
SEPARATOR,
|
||||||
|
TableRowActions,
|
||||||
|
VirtualDocumentType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export async function create(tableId: string, rowAction: { name: string }) {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const rowActionsId = generateRowActionsID(tableId)
|
||||||
|
let doc: TableRowActions
|
||||||
|
try {
|
||||||
|
doc = await db.get<TableRowActions>(rowActionsId)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.status !== 404) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
doc = { _id: rowActionsId, actions: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}`
|
||||||
|
doc.actions[newId] = rowAction
|
||||||
|
await db.put(doc)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newId,
|
||||||
|
...rowAction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(tableId: string) {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const rowActionsId = generateRowActionsID(tableId)
|
||||||
|
return await db.get<TableRowActions>(rowActionsId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function docExists(tableId: string) {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const rowActionsId = generateRowActionsID(tableId)
|
||||||
|
const result = await db.exists(rowActionsId)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(
|
||||||
|
tableId: string,
|
||||||
|
rowActionId: string,
|
||||||
|
rowAction: { name: string }
|
||||||
|
) {
|
||||||
|
const actionsDoc = await get(tableId)
|
||||||
|
|
||||||
|
if (!actionsDoc.actions[rowActionId]) {
|
||||||
|
throw new HTTPError(
|
||||||
|
`Row action '${rowActionId}' not found in '${tableId}'`,
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actionsDoc.actions[rowActionId] = rowAction
|
||||||
|
|
||||||
|
const db = context.getAppDB()
|
||||||
|
await db.put(actionsDoc)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: rowActionId,
|
||||||
|
...rowAction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(tableId: string, rowActionId: string) {
|
||||||
|
const actionsDoc = await get(tableId)
|
||||||
|
|
||||||
|
if (!actionsDoc.actions[rowActionId]) {
|
||||||
|
throw new HTTPError(
|
||||||
|
`Row action '${rowActionId}' not found in '${tableId}'`,
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete actionsDoc.actions[rowActionId]
|
||||||
|
|
||||||
|
const db = context.getAppDB()
|
||||||
|
await db.put(actionsDoc)
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { default as users } from "./users"
|
||||||
import { default as plugins } from "./plugins"
|
import { default as plugins } from "./plugins"
|
||||||
import * as views from "./app/views"
|
import * as views from "./app/views"
|
||||||
import * as permissions from "./app/permissions"
|
import * as permissions from "./app/permissions"
|
||||||
|
import * as rowActions from "./app/rowActions"
|
||||||
|
|
||||||
const sdk = {
|
const sdk = {
|
||||||
backups,
|
backups,
|
||||||
|
@ -24,6 +25,7 @@ const sdk = {
|
||||||
views,
|
views,
|
||||||
permissions,
|
permissions,
|
||||||
links,
|
links,
|
||||||
|
rowActions,
|
||||||
}
|
}
|
||||||
|
|
||||||
// default export for TS
|
// default export for TS
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { UserAPI } from "./user"
|
||||||
import { QueryAPI } from "./query"
|
import { QueryAPI } from "./query"
|
||||||
import { RoleAPI } from "./role"
|
import { RoleAPI } from "./role"
|
||||||
import { TemplateAPI } from "./template"
|
import { TemplateAPI } from "./template"
|
||||||
|
import { RowActionAPI } from "./rowAction"
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
table: TableAPI
|
table: TableAPI
|
||||||
|
@ -29,6 +30,7 @@ export default class API {
|
||||||
query: QueryAPI
|
query: QueryAPI
|
||||||
roles: RoleAPI
|
roles: RoleAPI
|
||||||
templates: TemplateAPI
|
templates: TemplateAPI
|
||||||
|
rowAction: RowActionAPI
|
||||||
|
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
this.table = new TableAPI(config)
|
this.table = new TableAPI(config)
|
||||||
|
@ -45,5 +47,6 @@ export default class API {
|
||||||
this.query = new QueryAPI(config)
|
this.query = new QueryAPI(config)
|
||||||
this.roles = new RoleAPI(config)
|
this.roles = new RoleAPI(config)
|
||||||
this.templates = new TemplateAPI(config)
|
this.templates = new TemplateAPI(config)
|
||||||
|
this.rowAction = new RowActionAPI(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import {
|
||||||
|
CreateRowActionRequest,
|
||||||
|
RowActionResponse,
|
||||||
|
RowActionsResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
|
export class RowActionAPI extends TestAPI {
|
||||||
|
save = async (
|
||||||
|
tableId: string,
|
||||||
|
rowAction: CreateRowActionRequest,
|
||||||
|
expectations?: Expectations,
|
||||||
|
config?: { publicUser?: boolean }
|
||||||
|
) => {
|
||||||
|
return await this._post<RowActionResponse>(
|
||||||
|
`/api/tables/${tableId}/actions`,
|
||||||
|
{
|
||||||
|
body: rowAction,
|
||||||
|
expectations: {
|
||||||
|
...expectations,
|
||||||
|
status: expectations?.status || 201,
|
||||||
|
},
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
find = async (
|
||||||
|
tableId: string,
|
||||||
|
expectations?: Expectations,
|
||||||
|
config?: { publicUser?: boolean }
|
||||||
|
) => {
|
||||||
|
return await this._get<RowActionsResponse>(
|
||||||
|
`/api/tables/${tableId}/actions`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
update = async (
|
||||||
|
tableId: string,
|
||||||
|
rowActionId: string,
|
||||||
|
rowAction: CreateRowActionRequest,
|
||||||
|
expectations?: Expectations,
|
||||||
|
config?: { publicUser?: boolean }
|
||||||
|
) => {
|
||||||
|
return await this._put<RowActionResponse>(
|
||||||
|
`/api/tables/${tableId}/actions/${rowActionId}`,
|
||||||
|
{
|
||||||
|
body: rowAction,
|
||||||
|
expectations,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (
|
||||||
|
tableId: string,
|
||||||
|
rowActionId: string,
|
||||||
|
expectations?: Expectations,
|
||||||
|
config?: { publicUser?: boolean }
|
||||||
|
) => {
|
||||||
|
return await this._delete<RowActionResponse>(
|
||||||
|
`/api/tables/${tableId}/actions/${rowActionId}`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,3 +7,4 @@ export * from "./table"
|
||||||
export * from "./permission"
|
export * from "./permission"
|
||||||
export * from "./attachment"
|
export * from "./attachment"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
|
export * from "./rowAction"
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
interface RowActionData {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
export interface CreateRowActionRequest extends RowActionData {}
|
||||||
|
export interface UpdateRowActionRequest extends RowActionData {}
|
||||||
|
|
||||||
|
export interface RowActionResponse extends RowActionData {
|
||||||
|
id: string
|
||||||
|
tableId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RowActionsResponse {
|
||||||
|
actions: Record<string, RowActionResponse>
|
||||||
|
}
|
|
@ -16,3 +16,4 @@ export * from "./links"
|
||||||
export * from "./component"
|
export * from "./component"
|
||||||
export * from "./sqlite"
|
export * from "./sqlite"
|
||||||
export * from "./snippet"
|
export * from "./snippet"
|
||||||
|
export * from "./rowAction"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Document } from "../document"
|
||||||
|
|
||||||
|
export interface TableRowActions extends Document {
|
||||||
|
_id: string
|
||||||
|
actions: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ export enum DocumentType {
|
||||||
AUDIT_LOG = "al",
|
AUDIT_LOG = "al",
|
||||||
APP_MIGRATION_METADATA = "_design/migrations",
|
APP_MIGRATION_METADATA = "_design/migrations",
|
||||||
SCIM_LOG = "scimlog",
|
SCIM_LOG = "scimlog",
|
||||||
|
ROW_ACTIONS = "ra",
|
||||||
}
|
}
|
||||||
|
|
||||||
// these are the core documents that make up the data, design
|
// these are the core documents that make up the data, design
|
||||||
|
@ -68,6 +69,7 @@ export enum InternalTable {
|
||||||
// documents or enriched into existence as part of get requests
|
// documents or enriched into existence as part of get requests
|
||||||
export enum VirtualDocumentType {
|
export enum VirtualDocumentType {
|
||||||
VIEW = "view",
|
VIEW = "view",
|
||||||
|
ROW_ACTION = "row_action",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Document {
|
export interface Document {
|
||||||
|
|
Loading…
Reference in New Issue