Merge remote-tracking branch 'origin/feature/app-list-actions' into feature/app-favourites

This commit is contained in:
Dean 2024-03-11 14:41:07 +00:00
commit 1cd20781fb
6 changed files with 115 additions and 118 deletions

View File

@ -1,5 +1,5 @@
{
"version": "2.21.5",
"version": "2.21.6",
"npmClient": "yarn",
"packages": [
"packages/*",

View File

@ -39,7 +39,7 @@
let integration
let schemaType
let autoSchema = {}
let schema = {}
let nestedSchemaFields = {}
let rows = []
let keys = {}
@ -52,6 +52,8 @@
schemaType = integration.query[query.queryVerb].type
newQuery = cloneDeep(query)
// init schema from the query if one already exists
schema = newQuery.schema
// Set the location where the query code will be written to an empty string so that it doesn't
// get changed from undefined -> "" by the input, breaking our unsaved changes checks
newQuery.fields[schemaType] ??= ""
@ -86,12 +88,7 @@
nestedSchemaFields = response.nestedSchemaFields
if (Object.keys(newQuery.schema).length === 0) {
// Assign this to a variable instead of directly to the newQuery.schema so that a user
// can change the table they're querying and have the schema update until they first
// edit it
autoSchema = response.schema
}
schema = response.schema
rows = response.rows
notifications.success("Query executed successfully")
@ -118,10 +115,7 @@
loading = true
const response = await queries.save(newQuery.datasourceId, {
...newQuery,
schema:
Object.keys(newQuery.schema).length === 0
? autoSchema
: newQuery.schema,
schema,
nestedSchemaFields,
})
@ -320,12 +314,10 @@
<QueryViewerSidePanel
onClose={() => (showSidePanel = false)}
onSchemaChange={newSchema => {
newQuery.schema = newSchema
schema = newSchema
}}
{rows}
schema={Object.keys(newQuery.schema).length === 0
? autoSchema
: newQuery.schema}
{schema}
/>
</div>
</div>

View File

@ -701,7 +701,6 @@ export async function duplicateApp(
}
ctx.body = {
message: "app duplicated",
duplicateAppId: newApplication?.appId,
sourceAppId,
}

View File

@ -34,6 +34,96 @@ describe("/applications", () => {
jest.clearAllMocks()
})
// These need to go first for the app totals to make sense
describe("permissions", () => {
it("should only return apps a user has access to", async () => {
let user = await config.createUser({
builder: { global: false },
admin: { global: false },
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(0)
})
user = await config.globalUser({
...user,
builder: {
apps: [config.getProdAppId()],
},
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(1)
})
})
it("should only return apps a user has access to through a custom role", async () => {
let user = await config.createUser({
builder: { global: false },
admin: { global: false },
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(0)
})
const role = await config.api.roles.save({
name: "Test",
inherits: "PUBLIC",
permissionId: "read_only",
version: "name",
})
user = await config.globalUser({
...user,
roles: {
[config.getProdAppId()]: role.name,
},
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(1)
})
})
it("should only return apps a user has access to through a custom role on a group", async () => {
let user = await config.createUser({
builder: { global: false },
admin: { global: false },
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(0)
})
const roleName = uuid.v4().replace(/-/g, "")
const role = await config.api.roles.save({
name: roleName,
inherits: "PUBLIC",
permissionId: "read_only",
version: "name",
})
const group = await config.createGroup(role._id!)
user = await config.globalUser({
...user,
userGroups: [group._id!],
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(1)
})
})
})
describe("create", () => {
it("creates empty app", async () => {
const app = await config.api.application.create({ name: utils.newid() })
@ -96,13 +186,16 @@ describe("/applications", () => {
})
it("should reject with a known name", async () => {
await config.api.application.create({ name: app.name }, { status: 400 })
await config.api.application.create(
{ name: app.name },
{ body: { message: "App name is already in use." }, status: 400 }
)
})
it("should reject with a known url", async () => {
await config.api.application.create(
{ name: "made up", url: app?.url! },
{ status: 400 }
{ body: { message: "App URL is already in use." }, status: 400 }
)
})
})
@ -279,10 +372,9 @@ describe("/applications", () => {
name: app.name,
url: "/known-name",
},
{ status: 400 }
{ body: { message: "App name is already in use." }, status: 400 }
)
expect(resp.message).toEqual("App name is already in use.")
expect(events.app.duplicated).not.toBeCalled()
})
it("should reject with a known url", async () => {
@ -292,10 +384,9 @@ describe("/applications", () => {
name: "this is fine",
url: app.url,
},
{ status: 400 }
{ body: { message: "App URL is already in use." }, status: 400 }
)
expect(resp.message).toEqual("App URL is already in use.")
expect(events.app.duplicated).not.toBeCalled()
})
})
@ -319,93 +410,4 @@ describe("/applications", () => {
expect(devLogs.data.length).toBe(0)
})
})
describe("permissions", () => {
it("should only return apps a user has access to", async () => {
let user = await config.createUser({
builder: { global: false },
admin: { global: false },
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(0)
})
user = await config.globalUser({
...user,
builder: {
apps: [config.getProdAppId()],
},
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(1)
})
})
it("should only return apps a user has access to through a custom role", async () => {
let user = await config.createUser({
builder: { global: false },
admin: { global: false },
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(0)
})
const role = await config.api.roles.save({
name: "Test",
inherits: "PUBLIC",
permissionId: "read_only",
version: "name",
})
user = await config.globalUser({
...user,
roles: {
[config.getProdAppId()]: role.name,
},
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(1)
})
})
it.only("should only return apps a user has access to through a custom role on a group", async () => {
let user = await config.createUser({
builder: { global: false },
admin: { global: false },
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(0)
})
const roleName = uuid.v4().replace(/-/g, "")
const role = await config.api.roles.save({
name: roleName,
inherits: "PUBLIC",
permissionId: "read_only",
version: "name",
})
const group = await config.createGroup(role._id!)
user = await config.globalUser({
...user,
userGroups: [group._id!],
})
await config.withUser(user, async () => {
const apps = await config.api.application.fetch()
expect(apps).toHaveLength(1)
})
})
})
})

View File

@ -15,7 +15,7 @@ import { context, cache, auth } from "@budibase/backend-core"
import { getGlobalIDFromUserMetadataID } from "../db/utils"
import sdk from "../sdk"
import { cloneDeep } from "lodash/fp"
import { Datasource, Query, SourceName } from "@budibase/types"
import { Datasource, Query, SourceName, Row } from "@budibase/types"
import { isSQL } from "../integrations/utils"
import { interpolateSQL } from "../integrations/queries/sql"
@ -115,7 +115,7 @@ class QueryRunner {
}
let output = threadUtils.formatResponse(await integration[queryVerb](query))
let rows = output,
let rows = output as Row[],
info = undefined,
extra = undefined,
pagination = undefined
@ -170,7 +170,12 @@ class QueryRunner {
}
// get all the potential fields in the schema
let keys = rows.flatMap(Object.keys)
const keysSet: Set<string> = new Set()
rows.forEach(row => {
const keys = Object.keys(row)
keys.forEach(key => keysSet.add(key))
})
const keys: string[] = [...keysSet]
if (integration.end) {
integration.end()

View File

@ -22,7 +22,6 @@ export interface DuplicateAppRequest {
export interface DuplicateAppResponse {
duplicateAppId: string
sourceAppId: string
message: string
}
export interface FetchAppDefinitionResponse {