Merge remote-tracking branch 'origin/feature/app-list-actions' into feature/app-favourites
This commit is contained in:
commit
1cd20781fb
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.21.5",
|
"version": "2.21.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
let integration
|
let integration
|
||||||
let schemaType
|
let schemaType
|
||||||
|
|
||||||
let autoSchema = {}
|
let schema = {}
|
||||||
let nestedSchemaFields = {}
|
let nestedSchemaFields = {}
|
||||||
let rows = []
|
let rows = []
|
||||||
let keys = {}
|
let keys = {}
|
||||||
|
@ -52,6 +52,8 @@
|
||||||
schemaType = integration.query[query.queryVerb].type
|
schemaType = integration.query[query.queryVerb].type
|
||||||
|
|
||||||
newQuery = cloneDeep(query)
|
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
|
// 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
|
// get changed from undefined -> "" by the input, breaking our unsaved changes checks
|
||||||
newQuery.fields[schemaType] ??= ""
|
newQuery.fields[schemaType] ??= ""
|
||||||
|
@ -86,12 +88,7 @@
|
||||||
|
|
||||||
nestedSchemaFields = response.nestedSchemaFields
|
nestedSchemaFields = response.nestedSchemaFields
|
||||||
|
|
||||||
if (Object.keys(newQuery.schema).length === 0) {
|
schema = response.schema
|
||||||
// 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
|
|
||||||
}
|
|
||||||
rows = response.rows
|
rows = response.rows
|
||||||
|
|
||||||
notifications.success("Query executed successfully")
|
notifications.success("Query executed successfully")
|
||||||
|
@ -118,10 +115,7 @@
|
||||||
loading = true
|
loading = true
|
||||||
const response = await queries.save(newQuery.datasourceId, {
|
const response = await queries.save(newQuery.datasourceId, {
|
||||||
...newQuery,
|
...newQuery,
|
||||||
schema:
|
schema,
|
||||||
Object.keys(newQuery.schema).length === 0
|
|
||||||
? autoSchema
|
|
||||||
: newQuery.schema,
|
|
||||||
nestedSchemaFields,
|
nestedSchemaFields,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -320,12 +314,10 @@
|
||||||
<QueryViewerSidePanel
|
<QueryViewerSidePanel
|
||||||
onClose={() => (showSidePanel = false)}
|
onClose={() => (showSidePanel = false)}
|
||||||
onSchemaChange={newSchema => {
|
onSchemaChange={newSchema => {
|
||||||
newQuery.schema = newSchema
|
schema = newSchema
|
||||||
}}
|
}}
|
||||||
{rows}
|
{rows}
|
||||||
schema={Object.keys(newQuery.schema).length === 0
|
{schema}
|
||||||
? autoSchema
|
|
||||||
: newQuery.schema}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -701,7 +701,6 @@ export async function duplicateApp(
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "app duplicated",
|
|
||||||
duplicateAppId: newApplication?.appId,
|
duplicateAppId: newApplication?.appId,
|
||||||
sourceAppId,
|
sourceAppId,
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,96 @@ describe("/applications", () => {
|
||||||
jest.clearAllMocks()
|
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", () => {
|
describe("create", () => {
|
||||||
it("creates empty app", async () => {
|
it("creates empty app", async () => {
|
||||||
const app = await config.api.application.create({ name: utils.newid() })
|
const app = await config.api.application.create({ name: utils.newid() })
|
||||||
|
@ -96,13 +186,16 @@ describe("/applications", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reject with a known name", async () => {
|
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 () => {
|
it("should reject with a known url", async () => {
|
||||||
await config.api.application.create(
|
await config.api.application.create(
|
||||||
{ name: "made up", url: app?.url! },
|
{ 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,
|
name: app.name,
|
||||||
url: "/known-name",
|
url: "/known-name",
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ body: { message: "App name is already in use." }, status: 400 }
|
||||||
)
|
)
|
||||||
|
expect(events.app.duplicated).not.toBeCalled()
|
||||||
expect(resp.message).toEqual("App name is already in use.")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reject with a known url", async () => {
|
it("should reject with a known url", async () => {
|
||||||
|
@ -292,10 +384,9 @@ describe("/applications", () => {
|
||||||
name: "this is fine",
|
name: "this is fine",
|
||||||
url: app.url,
|
url: app.url,
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ body: { message: "App URL is already in use." }, status: 400 }
|
||||||
)
|
)
|
||||||
|
expect(events.app.duplicated).not.toBeCalled()
|
||||||
expect(resp.message).toEqual("App URL is already in use.")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -319,93 +410,4 @@ describe("/applications", () => {
|
||||||
expect(devLogs.data.length).toBe(0)
|
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { context, cache, auth } from "@budibase/backend-core"
|
||||||
import { getGlobalIDFromUserMetadataID } from "../db/utils"
|
import { getGlobalIDFromUserMetadataID } from "../db/utils"
|
||||||
import sdk from "../sdk"
|
import sdk from "../sdk"
|
||||||
import { cloneDeep } from "lodash/fp"
|
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 { isSQL } from "../integrations/utils"
|
||||||
import { interpolateSQL } from "../integrations/queries/sql"
|
import { interpolateSQL } from "../integrations/queries/sql"
|
||||||
|
@ -115,7 +115,7 @@ class QueryRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = threadUtils.formatResponse(await integration[queryVerb](query))
|
let output = threadUtils.formatResponse(await integration[queryVerb](query))
|
||||||
let rows = output,
|
let rows = output as Row[],
|
||||||
info = undefined,
|
info = undefined,
|
||||||
extra = undefined,
|
extra = undefined,
|
||||||
pagination = undefined
|
pagination = undefined
|
||||||
|
@ -170,7 +170,12 @@ class QueryRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all the potential fields in the schema
|
// 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) {
|
if (integration.end) {
|
||||||
integration.end()
|
integration.end()
|
||||||
|
|
|
@ -22,7 +22,6 @@ export interface DuplicateAppRequest {
|
||||||
export interface DuplicateAppResponse {
|
export interface DuplicateAppResponse {
|
||||||
duplicateAppId: string
|
duplicateAppId: string
|
||||||
sourceAppId: string
|
sourceAppId: string
|
||||||
message: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchAppDefinitionResponse {
|
export interface FetchAppDefinitionResponse {
|
||||||
|
|
Loading…
Reference in New Issue