Merge branch 'develop' of github.com:Budibase/budibase into url-context
This commit is contained in:
commit
ec94aaa90b
2
LICENSE
2
LICENSE
|
@ -1,3 +1,5 @@
|
||||||
|
Copyright 2019-2021, Budibase Ltd
|
||||||
|
|
||||||
Each Budibase package has its own license:
|
Each Budibase package has its own license:
|
||||||
|
|
||||||
builder: AGPLv3
|
builder: AGPLv3
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright 2019-2021, Budibase Ltd
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
const rimraf = require("rimraf")
|
const rimraf = require("rimraf")
|
||||||
const { join, resolve } = require("path")
|
const { join, resolve } = require("path")
|
||||||
// const run = require("../../cli/src/commands/run/runHandler")
|
|
||||||
const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase")
|
const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase")
|
||||||
|
|
||||||
const homedir = join(require("os").homedir(), ".budibase")
|
const homedir = join(require("os").homedir(), ".budibase")
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.58.4",
|
"@budibase/bbui": "^1.58.5",
|
||||||
"@budibase/client": "^0.7.8",
|
"@budibase/client": "^0.7.8",
|
||||||
"@budibase/colorpicker": "1.0.1",
|
"@budibase/colorpicker": "1.0.1",
|
||||||
"@budibase/string-templates": "^0.7.8",
|
"@budibase/string-templates": "^0.7.8",
|
||||||
|
|
|
@ -235,7 +235,9 @@ export const getSchemaForDatasource = (datasource, isForm = false) => {
|
||||||
schema = {}
|
schema = {}
|
||||||
const params = table.parameters || []
|
const params = table.parameters || []
|
||||||
params.forEach(param => {
|
params.forEach(param => {
|
||||||
schema[param.name] = { ...param, type: "string" }
|
if (param?.name) {
|
||||||
|
schema[param.name] = { ...param, type: "string" }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
schema = cloneDeep(table.schema)
|
schema = cloneDeep(table.schema)
|
||||||
|
|
|
@ -30,6 +30,7 @@ export const getBackendUiStore = () => {
|
||||||
const queries = await queriesResponse.json()
|
const queries = await queriesResponse.json()
|
||||||
const integrationsResponse = await api.get("/api/integrations")
|
const integrationsResponse = await api.get("/api/integrations")
|
||||||
const integrations = await integrationsResponse.json()
|
const integrations = await integrationsResponse.json()
|
||||||
|
const permissionLevels = await store.actions.permissions.fetchLevels()
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedDatabase = db
|
state.selectedDatabase = db
|
||||||
|
@ -37,6 +38,7 @@ export const getBackendUiStore = () => {
|
||||||
state.datasources = datasources
|
state.datasources = datasources
|
||||||
state.queries = queries
|
state.queries = queries
|
||||||
state.integrations = integrations
|
state.integrations = integrations
|
||||||
|
state.permissionLevels = permissionLevels
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -232,7 +234,7 @@ export const getBackendUiStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveField: ({ originalName, field, primaryDisplay = false }) => {
|
saveField: ({ originalName, field, primaryDisplay = false, indexes }) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// delete the original if renaming
|
// delete the original if renaming
|
||||||
// need to handle if the column had no name, empty string
|
// need to handle if the column had no name, empty string
|
||||||
|
@ -249,6 +251,10 @@ export const getBackendUiStore = () => {
|
||||||
state.draftTable.primaryDisplay = field.name
|
state.draftTable.primaryDisplay = field.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (indexes) {
|
||||||
|
state.draftTable.indexes = indexes
|
||||||
|
}
|
||||||
|
|
||||||
state.draftTable.schema[field.name] = cloneDeep(field)
|
state.draftTable.schema[field.name] = cloneDeep(field)
|
||||||
store.actions.tables.save(state.draftTable)
|
store.actions.tables.save(state.draftTable)
|
||||||
return state
|
return state
|
||||||
|
@ -324,6 +330,25 @@ export const getBackendUiStore = () => {
|
||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
permissions: {
|
||||||
|
fetchLevels: async () => {
|
||||||
|
const response = await api.get("/api/permission/levels")
|
||||||
|
const json = await response.json()
|
||||||
|
return json
|
||||||
|
},
|
||||||
|
forResource: async resourceId => {
|
||||||
|
const response = await api.get(`/api/permission/${resourceId}`)
|
||||||
|
const json = await response.json()
|
||||||
|
return json
|
||||||
|
},
|
||||||
|
save: async ({ role, resource, level }) => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/api/permission/${role}/${resource}/${level}`
|
||||||
|
)
|
||||||
|
const json = await response.json()
|
||||||
|
return json
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return store
|
return store
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
makeMainForm,
|
makeMainForm,
|
||||||
makeTitleContainer,
|
makeTitleContainer,
|
||||||
makeSaveButton,
|
makeSaveButton,
|
||||||
makeTableFormComponents,
|
makeDatasourceFormComponents,
|
||||||
} from "./utils/commonComponents"
|
} from "./utils/commonComponents"
|
||||||
|
|
||||||
export default function(tables) {
|
export default function(tables) {
|
||||||
|
@ -51,7 +51,8 @@ const createScreen = table => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add all form fields from this schema to the field group
|
// Add all form fields from this schema to the field group
|
||||||
makeTableFormComponents(table._id).forEach(component => {
|
const datasource = { type: "table", tableId: table._id }
|
||||||
|
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||||
fieldGroup.addChild(component)
|
fieldGroup.addChild(component)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ import {
|
||||||
makeBreadcrumbContainer,
|
makeBreadcrumbContainer,
|
||||||
makeTitleContainer,
|
makeTitleContainer,
|
||||||
makeSaveButton,
|
makeSaveButton,
|
||||||
makeTableFormComponents,
|
|
||||||
makeMainForm,
|
makeMainForm,
|
||||||
spectrumColor,
|
spectrumColor,
|
||||||
|
makeDatasourceFormComponents,
|
||||||
} from "./utils/commonComponents"
|
} from "./utils/commonComponents"
|
||||||
|
|
||||||
export default function(tables) {
|
export default function(tables) {
|
||||||
|
@ -109,7 +109,8 @@ const createScreen = table => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add all form fields from this schema to the field group
|
// Add all form fields from this schema to the field group
|
||||||
makeTableFormComponents(table._id).forEach(component => {
|
const datasource = { type: "table", tableId: table._id }
|
||||||
|
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||||
fieldGroup.addChild(component)
|
fieldGroup.addChild(component)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { Component } from "./Component"
|
import { Component } from "./Component"
|
||||||
import { rowListUrl } from "../rowListScreen"
|
import { rowListUrl } from "../rowListScreen"
|
||||||
import { backendUiStore } from "builderStore"
|
import { getSchemaForDatasource } from "../../../dataBinding"
|
||||||
|
|
||||||
export function spectrumColor(number) {
|
export function spectrumColor(number) {
|
||||||
// Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found
|
// Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found
|
||||||
|
@ -174,37 +173,15 @@ const fieldTypeToComponentMap = {
|
||||||
link: "relationshipfield",
|
link: "relationshipfield",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeTableFormComponents(tableId) {
|
|
||||||
const tables = get(backendUiStore).tables
|
|
||||||
const schema = tables.find(table => table._id === tableId)?.schema ?? {}
|
|
||||||
return makeSchemaFormComponents(schema)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeQueryFormComponents(queryId) {
|
|
||||||
const queries = get(backendUiStore).queries
|
|
||||||
const params = queries.find(query => query._id === queryId)?.parameters ?? []
|
|
||||||
let schema = {}
|
|
||||||
params.forEach(param => {
|
|
||||||
schema[param.name] = { ...param, type: "string" }
|
|
||||||
})
|
|
||||||
return makeSchemaFormComponents(schema)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeDatasourceFormComponents(datasource) {
|
export function makeDatasourceFormComponents(datasource) {
|
||||||
if (!datasource) {
|
const { schema } = getSchemaForDatasource(datasource, true)
|
||||||
return []
|
|
||||||
}
|
|
||||||
return datasource.type === "table"
|
|
||||||
? makeTableFormComponents(datasource.tableId)
|
|
||||||
: makeQueryFormComponents(datasource._id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSchemaFormComponents(schema) {
|
|
||||||
let components = []
|
let components = []
|
||||||
let fields = Object.keys(schema || {})
|
let fields = Object.keys(schema || {})
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
const fieldSchema = schema[field]
|
const fieldSchema = schema[field]
|
||||||
const componentType = fieldTypeToComponentMap[fieldSchema.type]
|
const fieldType =
|
||||||
|
typeof fieldSchema === "object" ? fieldSchema.type : fieldSchema
|
||||||
|
const componentType = fieldTypeToComponentMap[fieldType]
|
||||||
const fullComponentType = `@budibase/standard-components/${componentType}`
|
const fullComponentType = `@budibase/standard-components/${componentType}`
|
||||||
if (componentType) {
|
if (componentType) {
|
||||||
const component = new Component(fullComponentType)
|
const component = new Component(fullComponentType)
|
||||||
|
@ -214,10 +191,10 @@ function makeSchemaFormComponents(schema) {
|
||||||
label: field,
|
label: field,
|
||||||
placeholder: field,
|
placeholder: field,
|
||||||
})
|
})
|
||||||
if (fieldSchema.type === "options") {
|
if (fieldType === "options") {
|
||||||
component.customProps({ placeholder: "Choose an option " })
|
component.customProps({ placeholder: "Choose an option " })
|
||||||
}
|
}
|
||||||
if (fieldSchema.type === "boolean") {
|
if (fieldType === "boolean") {
|
||||||
component.customProps({ text: field, label: "" })
|
component.customProps({ text: field, label: "" })
|
||||||
}
|
}
|
||||||
components.push(component)
|
components.push(component)
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||||
import ExportButton from "./buttons/ExportButton.svelte"
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||||
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
import * as api from "./api"
|
import * as api from "./api"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
title={isUsersTable ? 'Create New User' : 'Create New Row'}
|
title={isUsersTable ? 'Create New User' : 'Create New Row'}
|
||||||
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
|
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
|
||||||
<CreateViewButton />
|
<CreateViewButton />
|
||||||
|
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
|
||||||
<ExportButton view={tableView} />
|
<ExportButton view={tableView} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isUsersTable}
|
{#if isUsersTable}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import GroupByButton from "./buttons/GroupByButton.svelte"
|
import GroupByButton from "./buttons/GroupByButton.svelte"
|
||||||
import FilterButton from "./buttons/FilterButton.svelte"
|
import FilterButton from "./buttons/FilterButton.svelte"
|
||||||
import ExportButton from "./buttons/ExportButton.svelte"
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
|
|
||||||
export let view = {}
|
export let view = {}
|
||||||
|
|
||||||
|
@ -53,5 +54,6 @@
|
||||||
{#if view.calculation}
|
{#if view.calculation}
|
||||||
<GroupByButton {view} />
|
<GroupByButton {view} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<ManageAccessButton resourceId={decodeURI(name)} />
|
||||||
<ExportButton {view} />
|
<ExportButton {view} />
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { TextButton, Icon, Popover } from "@budibase/bbui"
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
import ManageAccessPopover from "../popovers/ManageAccessPopover.svelte"
|
||||||
|
|
||||||
|
export let resourceId
|
||||||
|
|
||||||
|
let anchor
|
||||||
|
let dropdown
|
||||||
|
let levels
|
||||||
|
let permissions
|
||||||
|
|
||||||
|
async function openDropdown() {
|
||||||
|
permissions = await backendUiStore.actions.permissions.forResource(
|
||||||
|
resourceId
|
||||||
|
)
|
||||||
|
levels = await backendUiStore.actions.permissions.fetchLevels()
|
||||||
|
dropdown.show()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={anchor}>
|
||||||
|
<TextButton text small on:click={openDropdown}>
|
||||||
|
<i class="ri-lock-line" />
|
||||||
|
Manage Access
|
||||||
|
</TextButton>
|
||||||
|
</div>
|
||||||
|
<Popover bind:this={dropdown} {anchor} align="left">
|
||||||
|
<ManageAccessPopover
|
||||||
|
{resourceId}
|
||||||
|
{levels}
|
||||||
|
{permissions}
|
||||||
|
onClosed={dropdown.hide} />
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
i {
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
|
import {
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
TextButton,
|
||||||
|
Select,
|
||||||
|
Toggle,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
|
@ -24,6 +31,7 @@
|
||||||
let primaryDisplay =
|
let primaryDisplay =
|
||||||
$backendUiStore.selectedTable.primaryDisplay == null ||
|
$backendUiStore.selectedTable.primaryDisplay == null ||
|
||||||
$backendUiStore.selectedTable.primaryDisplay === field.name
|
$backendUiStore.selectedTable.primaryDisplay === field.name
|
||||||
|
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let deletion
|
let deletion
|
||||||
|
|
||||||
|
@ -41,6 +49,7 @@
|
||||||
originalName,
|
originalName,
|
||||||
field,
|
field,
|
||||||
primaryDisplay,
|
primaryDisplay,
|
||||||
|
indexes,
|
||||||
})
|
})
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
@ -79,6 +88,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangePrimaryIndex(e) {
|
||||||
|
indexes = e.target.checked ? [field.name] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeSecondaryIndex(e) {
|
||||||
|
if (e.target.checked) {
|
||||||
|
indexes[1] = field.name
|
||||||
|
} else {
|
||||||
|
indexes = indexes.slice(0, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
confirmDeleteDialog.show()
|
confirmDeleteDialog.show()
|
||||||
deletion = true
|
deletion = true
|
||||||
|
@ -120,6 +141,20 @@
|
||||||
on:change={onChangePrimaryDisplay}
|
on:change={onChangePrimaryDisplay}
|
||||||
thin
|
thin
|
||||||
text="Use as table display column" />
|
text="Use as table display column" />
|
||||||
|
|
||||||
|
<Label grey small>Search Indexes</Label>
|
||||||
|
<Toggle
|
||||||
|
checked={indexes[0] === field.name}
|
||||||
|
disabled={indexes[1] === field.name}
|
||||||
|
on:change={onChangePrimaryIndex}
|
||||||
|
thin
|
||||||
|
text="Primary" />
|
||||||
|
<Toggle
|
||||||
|
checked={indexes[1] === field.name}
|
||||||
|
disabled={!indexes[0] || indexes[0] === field.name}
|
||||||
|
on:change={onChangeSecondaryIndex}
|
||||||
|
thin
|
||||||
|
text="Secondary" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if field.type === 'string'}
|
{#if field.type === 'string'}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
|
||||||
let permissions = []
|
let basePermissions = []
|
||||||
let selectedRole = {}
|
let selectedRole = {}
|
||||||
let errors = []
|
let errors = []
|
||||||
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
||||||
|
@ -16,9 +16,9 @@
|
||||||
)
|
)
|
||||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||||
|
|
||||||
const fetchPermissions = async () => {
|
const fetchBasePermissions = async () => {
|
||||||
const permissionsResponse = await api.get("/api/permissions")
|
const permissionsResponse = await api.get("/api/permission/builtin")
|
||||||
permissions = await permissionsResponse.json()
|
basePermissions = await permissionsResponse.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changes the selected role
|
// Changes the selected role
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(fetchPermissions)
|
onMount(fetchBasePermissions)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -121,11 +121,11 @@
|
||||||
<Select
|
<Select
|
||||||
thin
|
thin
|
||||||
secondary
|
secondary
|
||||||
label="Permissions"
|
label="Base Permissions"
|
||||||
bind:value={selectedRole.permissionId}>
|
bind:value={selectedRole.permissionId}>
|
||||||
<option value="">Choose permissions</option>
|
<option value="">Choose permissions</option>
|
||||||
{#each permissions as permission}
|
{#each basePermissions as basePerm}
|
||||||
<option value={permission._id}>{permission.name}</option>
|
<option value={basePerm._id}>{basePerm.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
import { Button, Label, Input, Select, Spacer } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let resourceId
|
||||||
|
export let permissions
|
||||||
|
export let onClosed
|
||||||
|
|
||||||
|
async function changePermission(level, role) {
|
||||||
|
await backendUiStore.actions.permissions.save({
|
||||||
|
level,
|
||||||
|
role,
|
||||||
|
resource: resourceId,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show updated permissions in UI: REMOVE
|
||||||
|
permissions = await backendUiStore.actions.permissions.forResource(
|
||||||
|
resourceId
|
||||||
|
)
|
||||||
|
notifier.success("Updated permissions.")
|
||||||
|
// TODO: update permissions
|
||||||
|
// permissions[]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="popover">
|
||||||
|
<h5>Who Can Access This Data?</h5>
|
||||||
|
<div class="note">
|
||||||
|
<Label extraSmall grey>Specify the minimum access level role for this data.</Label>
|
||||||
|
</div>
|
||||||
|
<Spacer large />
|
||||||
|
<div class="row">
|
||||||
|
<Label extraSmall grey>Level</Label>
|
||||||
|
<Label extraSmall grey>Role</Label>
|
||||||
|
{#each Object.keys(permissions) as level}
|
||||||
|
<Input secondary thin value={level} disabled={true} />
|
||||||
|
<Select
|
||||||
|
secondary
|
||||||
|
thin
|
||||||
|
value={permissions[level]}
|
||||||
|
on:change={e => changePermission(level, e.target.value)}>
|
||||||
|
{#each $backendUiStore.roles as role}
|
||||||
|
<option value={role._id}>{role.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer large />
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popover {
|
||||||
|
display: grid;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: var(--spacing-s) 0 var(--spacing-m) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
right: 2px;
|
right: 2px;
|
||||||
top: 26px;
|
top: 5px;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"datagrid",
|
"datagrid",
|
||||||
"list",
|
"list",
|
||||||
"button",
|
"button",
|
||||||
|
"search",
|
||||||
{
|
{
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"icon": "ri-file-edit-line",
|
"icon": "ri-file-edit-line",
|
||||||
|
|
|
@ -46,6 +46,11 @@
|
||||||
innerVal = value.target.value
|
innerVal = value.target.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "number") {
|
||||||
|
innerVal = parseInt(innerVal)
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof innerVal === "string") {
|
if (typeof innerVal === "string") {
|
||||||
onChange(replaceBindings(innerVal))
|
onChange(replaceBindings(innerVal))
|
||||||
} else {
|
} else {
|
||||||
|
@ -72,6 +77,7 @@
|
||||||
value={safeValue}
|
value={safeValue}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
{type}
|
||||||
{...props}
|
{...props}
|
||||||
name={key} />
|
name={key} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -92,3 +92,11 @@ export const HostingTypes = {
|
||||||
CLOUD: "cloud",
|
CLOUD: "cloud",
|
||||||
SELF: "self",
|
SELF: "self",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Roles = {
|
||||||
|
ADMIN: "ADMIN",
|
||||||
|
POWER: "POWER",
|
||||||
|
BASIC: "BASIC",
|
||||||
|
PUBLIC: "PUBLIC",
|
||||||
|
BUILDER: "BUILDER",
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,8 @@
|
||||||
Mozilla Public License Version 2.0
|
Mozilla Public License Version 2.0
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
|
Copyright 2019-2021, Budibase Ltd
|
||||||
|
|
||||||
1. Definitions
|
1. Definitions
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -16,3 +16,19 @@ export const fetchTableData = async tableId => {
|
||||||
const rows = await API.get({ url: `/api/${tableId}/rows` })
|
const rows = await API.get({ url: `/api/${tableId}/rows` })
|
||||||
return await enrichRows(rows, tableId)
|
return await enrichRows(rows, tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a mango query against an internal table
|
||||||
|
* @param {String} tableId - id of the table to search
|
||||||
|
* @param {Object} search - Mango Compliant search object
|
||||||
|
*/
|
||||||
|
export const searchTableData = async ({ tableId, search, pagination }) => {
|
||||||
|
const rows = await API.post({
|
||||||
|
url: `/api/${tableId}/rows/search`,
|
||||||
|
body: {
|
||||||
|
query: search,
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
||||||
|
|
|
@ -11,17 +11,13 @@
|
||||||
// Clone and create new data context for this component tree
|
// Clone and create new data context for this component tree
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const newContext = createContextStore()
|
const newContext = createContextStore(context)
|
||||||
setContext("context", newContext)
|
setContext("context", newContext)
|
||||||
|
|
||||||
let initiated = false
|
|
||||||
$: providerKey = key || $component.id
|
$: providerKey = key || $component.id
|
||||||
|
|
||||||
// Add data context
|
// Add data context
|
||||||
$: {
|
$: newContext.actions.provideData(providerKey, data)
|
||||||
newContext.actions.provideData(providerKey, $context, data)
|
|
||||||
initiated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instance ID is unique to each instance of a provider
|
// Instance ID is unique to each instance of a provider
|
||||||
let instanceId
|
let instanceId
|
||||||
|
@ -56,6 +52,4 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if initiated}
|
<slot />
|
||||||
<slot />
|
|
||||||
{/if}
|
|
||||||
|
|
|
@ -1,20 +1,27 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
|
|
||||||
export const createContextStore = () => {
|
export const createContextStore = oldContext => {
|
||||||
const store = writable({})
|
const newContext = writable({})
|
||||||
|
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
||||||
|
const totalContext = derived(contexts, $contexts => {
|
||||||
|
return $contexts.reduce((total, context) => ({ ...total, ...context }), {})
|
||||||
|
})
|
||||||
|
|
||||||
// Adds a data context layer to the tree
|
// Adds a data context layer to the tree
|
||||||
const provideData = (providerId, context, data) => {
|
const provideData = (providerId, data) => {
|
||||||
let newData = { ...context }
|
if (!providerId || data === undefined) {
|
||||||
if (providerId && data !== undefined) {
|
return
|
||||||
newData[providerId] = data
|
}
|
||||||
|
newContext.update(state => {
|
||||||
|
state[providerId] = data
|
||||||
|
|
||||||
// Keep track of the closest component ID so we can later hydrate a "data" prop.
|
// Keep track of the closest component ID so we can later hydrate a "data" prop.
|
||||||
// This is only required for legacy bindings that used "data" rather than a
|
// This is only required for legacy bindings that used "data" rather than a
|
||||||
// component ID.
|
// component ID.
|
||||||
newData.closestComponentId = providerId
|
state.closestComponentId = providerId
|
||||||
}
|
|
||||||
store.set(newData)
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds an action context layer to the tree
|
// Adds an action context layer to the tree
|
||||||
|
@ -22,14 +29,14 @@ export const createContextStore = () => {
|
||||||
if (!providerId || !actionType) {
|
if (!providerId || !actionType) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store.update(state => {
|
newContext.update(state => {
|
||||||
state[`${providerId}_${actionType}`] = callback
|
state[`${providerId}_${actionType}`] = callback
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: totalContext.subscribe,
|
||||||
actions: { provideData, provideAction },
|
actions: { provideData, provideAction },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
|
import { notificationStore } from "./notification"
|
||||||
|
|
||||||
export const createDatasourceStore = () => {
|
export const createDatasourceStore = () => {
|
||||||
const store = writable([])
|
const store = writable([])
|
||||||
|
@ -66,6 +67,9 @@ export const createDatasourceStore = () => {
|
||||||
const relatedInstances = get(store).filter(instance => {
|
const relatedInstances = get(store).filter(instance => {
|
||||||
return instance.datasourceId === datasourceId
|
return instance.datasourceId === datasourceId
|
||||||
})
|
})
|
||||||
|
if (relatedInstances?.length) {
|
||||||
|
notificationStore.blockNotifications(1000)
|
||||||
|
}
|
||||||
relatedInstances?.forEach(instance => {
|
relatedInstances?.forEach(instance => {
|
||||||
instance.refresh()
|
instance.refresh()
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,13 +5,22 @@ const NOTIFICATION_TIMEOUT = 3000
|
||||||
|
|
||||||
const createNotificationStore = () => {
|
const createNotificationStore = () => {
|
||||||
const _notifications = writable([])
|
const _notifications = writable([])
|
||||||
|
let block = false
|
||||||
|
|
||||||
const send = (message, type = "default") => {
|
const send = (message, type = "default") => {
|
||||||
|
if (block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
_notifications.update(state => {
|
_notifications.update(state => {
|
||||||
return [...state, { id: generate(), type, message }]
|
return [...state, { id: generate(), type, message }]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockNotifications = (timeout = 1000) => {
|
||||||
|
block = true
|
||||||
|
setTimeout(() => (block = false), timeout)
|
||||||
|
}
|
||||||
|
|
||||||
const notifications = derived(_notifications, ($_notifications, set) => {
|
const notifications = derived(_notifications, ($_notifications, set) => {
|
||||||
set($_notifications)
|
set($_notifications)
|
||||||
if ($_notifications.length > 0) {
|
if ($_notifications.length > 0) {
|
||||||
|
@ -36,6 +45,7 @@ const createNotificationStore = () => {
|
||||||
warning: msg => send(msg, "warning"),
|
warning: msg => send(msg, "warning"),
|
||||||
info: msg => send(msg, "info"),
|
info: msg => send(msg, "info"),
|
||||||
success: msg => send(msg, "success"),
|
success: msg => send(msg, "success"),
|
||||||
|
blockNotifications,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright 2019-2021, Budibase Ltd
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
"pino-pretty": "4.0.0",
|
"pino-pretty": "4.0.0",
|
||||||
"pouchdb": "7.2.1",
|
"pouchdb": "7.2.1",
|
||||||
"pouchdb-all-dbs": "1.0.2",
|
"pouchdb-all-dbs": "1.0.2",
|
||||||
|
"pouchdb-find": "^7.2.2",
|
||||||
"pouchdb-replication-stream": "1.2.9",
|
"pouchdb-replication-stream": "1.2.9",
|
||||||
"sanitize-s3-objectkey": "0.0.1",
|
"sanitize-s3-objectkey": "0.0.1",
|
||||||
"server-destroy": "1.0.1",
|
"server-destroy": "1.0.1",
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports.fetchInfo = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.save = async ctx => {
|
exports.save = async ctx => {
|
||||||
|
console.trace("DID A SAVE!")
|
||||||
const db = new CouchDB(BUILDER_CONFIG_DB)
|
const db = new CouchDB(BUILDER_CONFIG_DB)
|
||||||
const { type } = ctx.request.body
|
const { type } = ctx.request.body
|
||||||
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {
|
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {
|
||||||
|
|
|
@ -1,6 +1,187 @@
|
||||||
const { BUILTIN_PERMISSIONS } = require("../../utilities/security/permissions")
|
const {
|
||||||
|
BUILTIN_PERMISSIONS,
|
||||||
|
PermissionLevels,
|
||||||
|
isPermissionLevelHigherThanRead,
|
||||||
|
higherPermission,
|
||||||
|
} = require("../../utilities/security/permissions")
|
||||||
|
const {
|
||||||
|
isBuiltin,
|
||||||
|
getDBRoleID,
|
||||||
|
getExternalRoleID,
|
||||||
|
BUILTIN_ROLES,
|
||||||
|
} = require("../../utilities/security/roles")
|
||||||
|
const { getRoleParams } = require("../../db/utils")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
const {
|
||||||
|
CURRENTLY_SUPPORTED_LEVELS,
|
||||||
|
getBasePermissions,
|
||||||
|
} = require("../../utilities/security/utilities")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
const PermissionUpdateType = {
|
||||||
// TODO: need to build out custom permissions
|
REMOVE: "remove",
|
||||||
|
ADD: "add",
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
||||||
|
|
||||||
|
// quick function to perform a bit of weird logic, make sure fetch calls
|
||||||
|
// always say a write role also has read permission
|
||||||
|
function fetchLevelPerms(permissions, level, roleId) {
|
||||||
|
if (!permissions) {
|
||||||
|
permissions = {}
|
||||||
|
}
|
||||||
|
permissions[level] = roleId
|
||||||
|
if (
|
||||||
|
isPermissionLevelHigherThanRead(level) &&
|
||||||
|
!permissions[PermissionLevels.READ]
|
||||||
|
) {
|
||||||
|
permissions[PermissionLevels.READ] = roleId
|
||||||
|
}
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility function to stop this repetition - permissions always stored under roles
|
||||||
|
async function getAllDBRoles(db) {
|
||||||
|
const body = await db.allDocs(
|
||||||
|
getRoleParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return body.rows.map(row => row.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePermissionOnRole(
|
||||||
|
appId,
|
||||||
|
{ roleId, resourceId, level },
|
||||||
|
updateType
|
||||||
|
) {
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
const remove = updateType === PermissionUpdateType.REMOVE
|
||||||
|
const isABuiltin = isBuiltin(roleId)
|
||||||
|
const dbRoleId = getDBRoleID(roleId)
|
||||||
|
const dbRoles = await getAllDBRoles(db)
|
||||||
|
const docUpdates = []
|
||||||
|
|
||||||
|
// the permission is for a built in, make sure it exists
|
||||||
|
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
|
||||||
|
const builtin = cloneDeep(BUILTIN_ROLES[roleId])
|
||||||
|
builtin._id = getDBRoleID(builtin._id)
|
||||||
|
dbRoles.push(builtin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now try to find any roles which need updated, e.g. removing the
|
||||||
|
// resource from another role and then adding to the new role
|
||||||
|
for (let role of dbRoles) {
|
||||||
|
let updated = false
|
||||||
|
const rolePermissions = role.permissions ? role.permissions : {}
|
||||||
|
// handle the removal/updating the role which has this permission first
|
||||||
|
// the updating (role._id !== dbRoleId) is required because a resource/level can
|
||||||
|
// only be permitted in a single role (this reduces hierarchy confusion and simplifies
|
||||||
|
// the general UI for this, rather than needing to show everywhere it is used)
|
||||||
|
if (
|
||||||
|
(role._id !== dbRoleId || remove) &&
|
||||||
|
rolePermissions[resourceId] === level
|
||||||
|
) {
|
||||||
|
delete rolePermissions[resourceId]
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
// handle the adding, we're on the correct role, at it to this
|
||||||
|
if (!remove && role._id === dbRoleId) {
|
||||||
|
rolePermissions[resourceId] = higherPermission(
|
||||||
|
rolePermissions[resourceId],
|
||||||
|
level
|
||||||
|
)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
// handle the update, add it to bulk docs to perform at end
|
||||||
|
if (updated) {
|
||||||
|
role.permissions = rolePermissions
|
||||||
|
docUpdates.push(role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await db.bulkDocs(docUpdates)
|
||||||
|
return response.map(resp => {
|
||||||
|
resp._id = getExternalRoleID(resp.id)
|
||||||
|
delete resp.id
|
||||||
|
return resp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetchBuiltin = function(ctx) {
|
||||||
ctx.body = Object.values(BUILTIN_PERMISSIONS)
|
ctx.body = Object.values(BUILTIN_PERMISSIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.fetchLevels = function(ctx) {
|
||||||
|
// for now only provide the read/write perms externally
|
||||||
|
ctx.body = SUPPORTED_LEVELS
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.fetch = async function(ctx) {
|
||||||
|
const db = new CouchDB(ctx.appId)
|
||||||
|
const roles = await getAllDBRoles(db)
|
||||||
|
let permissions = {}
|
||||||
|
// create an object with structure role ID -> resource ID -> level
|
||||||
|
for (let role of roles) {
|
||||||
|
if (!role.permissions) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const roleId = getExternalRoleID(role._id)
|
||||||
|
for (let [resource, level] of Object.entries(role.permissions)) {
|
||||||
|
permissions[resource] = fetchLevelPerms(
|
||||||
|
permissions[resource],
|
||||||
|
level,
|
||||||
|
roleId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// apply the base permissions
|
||||||
|
const finalPermissions = {}
|
||||||
|
for (let [resource, permission] of Object.entries(permissions)) {
|
||||||
|
const basePerms = getBasePermissions(resource)
|
||||||
|
finalPermissions[resource] = Object.assign(basePerms, permission)
|
||||||
|
}
|
||||||
|
ctx.body = finalPermissions
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getResourcePerms = async function(ctx) {
|
||||||
|
const resourceId = ctx.params.resourceId
|
||||||
|
const db = new CouchDB(ctx.appId)
|
||||||
|
const body = await db.allDocs(
|
||||||
|
getRoleParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const roles = body.rows.map(row => row.doc)
|
||||||
|
let permissions = {}
|
||||||
|
for (let level of SUPPORTED_LEVELS) {
|
||||||
|
// update the various roleIds in the resource permissions
|
||||||
|
for (let role of roles) {
|
||||||
|
if (role.permissions && role.permissions[resourceId] === level) {
|
||||||
|
permissions = fetchLevelPerms(
|
||||||
|
permissions,
|
||||||
|
level,
|
||||||
|
getExternalRoleID(role._id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.body = Object.assign(getBasePermissions(resourceId), permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addPermission = async function(ctx) {
|
||||||
|
ctx.body = await updatePermissionOnRole(
|
||||||
|
ctx.appId,
|
||||||
|
ctx.params,
|
||||||
|
PermissionUpdateType.ADD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.removePermission = async function(ctx) {
|
||||||
|
ctx.body = await updatePermissionOnRole(
|
||||||
|
ctx.appId,
|
||||||
|
ctx.params,
|
||||||
|
PermissionUpdateType.REMOVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const {
|
const {
|
||||||
BUILTIN_ROLES,
|
BUILTIN_ROLES,
|
||||||
|
BUILTIN_ROLE_IDS,
|
||||||
Role,
|
Role,
|
||||||
getRole,
|
getRole,
|
||||||
|
isBuiltin,
|
||||||
|
getExternalRoleID,
|
||||||
} = require("../../utilities/security/roles")
|
} = require("../../utilities/security/roles")
|
||||||
const {
|
const {
|
||||||
generateRoleID,
|
generateRoleID,
|
||||||
|
@ -16,6 +19,14 @@ const UpdateRolesOptions = {
|
||||||
REMOVED: "removed",
|
REMOVED: "removed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exclude internal roles like builder
|
||||||
|
const EXTERNAL_BUILTIN_ROLE_IDS = [
|
||||||
|
BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
BUILTIN_ROLE_IDS.POWER,
|
||||||
|
BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
BUILTIN_ROLE_IDS.PUBLIC,
|
||||||
|
]
|
||||||
|
|
||||||
async function updateRolesOnUserTable(db, roleId, updateOption) {
|
async function updateRolesOnUserTable(db, roleId, updateOption) {
|
||||||
const table = await db.get(ViewNames.USERS)
|
const table = await db.get(ViewNames.USERS)
|
||||||
const schema = table.schema
|
const schema = table.schema
|
||||||
|
@ -46,16 +57,24 @@ exports.fetch = async function(ctx) {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const customRoles = body.rows.map(row => row.doc)
|
let roles = body.rows.map(row => row.doc)
|
||||||
|
|
||||||
// exclude internal roles like builder
|
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||||
const staticRoles = [
|
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
||||||
BUILTIN_ROLES.ADMIN,
|
const builtinRole = BUILTIN_ROLES[builtinRoleId]
|
||||||
BUILTIN_ROLES.POWER,
|
const dbBuiltin = roles.filter(
|
||||||
BUILTIN_ROLES.BASIC,
|
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
|
||||||
BUILTIN_ROLES.PUBLIC,
|
)[0]
|
||||||
]
|
if (dbBuiltin == null) {
|
||||||
ctx.body = [...staticRoles, ...customRoles]
|
roles.push(builtinRole)
|
||||||
|
} else {
|
||||||
|
// remove role and all back after combining with the builtin
|
||||||
|
roles = roles.filter(role => role._id !== dbBuiltin._id)
|
||||||
|
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
|
||||||
|
roles.push(Object.assign(builtinRole, dbBuiltin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.body = roles
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
|
@ -67,6 +86,8 @@ exports.save = async function(ctx) {
|
||||||
let { _id, name, inherits, permissionId } = ctx.request.body
|
let { _id, name, inherits, permissionId } = ctx.request.body
|
||||||
if (!_id) {
|
if (!_id) {
|
||||||
_id = generateRoleID()
|
_id = generateRoleID()
|
||||||
|
} else if (isBuiltin(_id)) {
|
||||||
|
ctx.throw(400, "Cannot update builtin roles.")
|
||||||
}
|
}
|
||||||
const role = new Role(_id, name)
|
const role = new Role(_id, name)
|
||||||
.addPermission(permissionId)
|
.addPermission(permissionId)
|
||||||
|
@ -84,6 +105,9 @@ exports.save = async function(ctx) {
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.appId)
|
const db = new CouchDB(ctx.user.appId)
|
||||||
const roleId = ctx.params.roleId
|
const roleId = ctx.params.roleId
|
||||||
|
if (isBuiltin(roleId)) {
|
||||||
|
ctx.throw(400, "Cannot delete builtin role.")
|
||||||
|
}
|
||||||
// first check no users actively attached to role
|
// first check no users actively attached to role
|
||||||
const users = (
|
const users = (
|
||||||
await db.allDocs(
|
await db.allDocs(
|
||||||
|
@ -94,7 +118,7 @@ exports.destroy = async function(ctx) {
|
||||||
).rows.map(row => row.doc)
|
).rows.map(row => row.doc)
|
||||||
const usersWithRole = users.filter(user => user.roleId === roleId)
|
const usersWithRole = users.filter(user => user.roleId === roleId)
|
||||||
if (usersWithRole.length !== 0) {
|
if (usersWithRole.length !== 0) {
|
||||||
ctx.throw("Cannot delete role when it is in use.")
|
ctx.throw(400, "Cannot delete role when it is in use.")
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.remove(roleId, ctx.params.rev)
|
await db.remove(roleId, ctx.params.rev)
|
||||||
|
|
|
@ -54,7 +54,7 @@ async function findRow(db, appId, tableId, rowId) {
|
||||||
exports.patch = async function(ctx) {
|
exports.patch = async function(ctx) {
|
||||||
const appId = ctx.user.appId
|
const appId = ctx.user.appId
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
let row = await db.get(ctx.params.id)
|
let row = await db.get(ctx.params.rowId)
|
||||||
const table = await db.get(row.tableId)
|
const table = await db.get(row.tableId)
|
||||||
const patchfields = ctx.request.body
|
const patchfields = ctx.request.body
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ exports.save = async function(ctx) {
|
||||||
// if the row obj had an _id then it will have been retrieved
|
// if the row obj had an _id then it will have been retrieved
|
||||||
const existingRow = ctx.preExisting
|
const existingRow = ctx.preExisting
|
||||||
if (existingRow) {
|
if (existingRow) {
|
||||||
ctx.params.id = row._id
|
ctx.params.rowId = row._id
|
||||||
await exports.patch(ctx)
|
await exports.patch(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -229,6 +229,38 @@ exports.fetchView = async function(ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.search = async function(ctx) {
|
||||||
|
const appId = ctx.user.appId
|
||||||
|
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
pagination: { pageSize = 10, page },
|
||||||
|
} = ctx.request.body
|
||||||
|
|
||||||
|
query.tableId = ctx.params.tableId
|
||||||
|
|
||||||
|
const response = await db.find({
|
||||||
|
selector: query,
|
||||||
|
limit: pageSize,
|
||||||
|
skip: pageSize * page,
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = response.docs
|
||||||
|
|
||||||
|
// delete passwords from users
|
||||||
|
if (query.tableId === ViewNames.USERS) {
|
||||||
|
for (let row of rows) {
|
||||||
|
delete row.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = await db.get(ctx.params.tableId)
|
||||||
|
|
||||||
|
ctx.body = await enrichRows(appId, table, rows)
|
||||||
|
}
|
||||||
|
|
||||||
exports.fetchTableRows = async function(ctx) {
|
exports.fetchTableRows = async function(ctx) {
|
||||||
const appId = ctx.user.appId
|
const appId = ctx.user.appId
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
|
|
|
@ -7,6 +7,7 @@ const {
|
||||||
generateTableID,
|
generateTableID,
|
||||||
generateRowID,
|
generateRowID,
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
|
const { isEqual } = require("lodash/fp")
|
||||||
|
|
||||||
async function checkForColumnUpdates(db, oldTable, updatedTable) {
|
async function checkForColumnUpdates(db, oldTable, updatedTable) {
|
||||||
let updatedRows
|
let updatedRows
|
||||||
|
@ -128,6 +129,46 @@ exports.save = async function(ctx) {
|
||||||
const result = await db.post(tableToSave)
|
const result = await db.post(tableToSave)
|
||||||
tableToSave._rev = result.rev
|
tableToSave._rev = result.rev
|
||||||
|
|
||||||
|
// create relevant search indexes
|
||||||
|
if (tableToSave.indexes && tableToSave.indexes.length > 0) {
|
||||||
|
const currentIndexes = await db.getIndexes()
|
||||||
|
const indexName = `search:${result.id}`
|
||||||
|
|
||||||
|
const existingIndex = currentIndexes.indexes.find(
|
||||||
|
existing => existing.name === indexName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingIndex) {
|
||||||
|
const currentFields = existingIndex.def.fields.map(
|
||||||
|
field => Object.keys(field)[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
// if index fields have changed, delete the original index
|
||||||
|
if (!isEqual(currentFields, tableToSave.indexes)) {
|
||||||
|
await db.deleteIndex(existingIndex)
|
||||||
|
// create/recreate the index with fields
|
||||||
|
await db.createIndex({
|
||||||
|
index: {
|
||||||
|
fields: tableToSave.indexes,
|
||||||
|
name: indexName,
|
||||||
|
ddoc: "search_ddoc",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// create/recreate the index with fields
|
||||||
|
await db.createIndex({
|
||||||
|
index: {
|
||||||
|
fields: tableToSave.indexes,
|
||||||
|
name: indexName,
|
||||||
|
ddoc: "search_ddoc",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emitTable(`table:save`, appId, tableToSave)
|
ctx.eventEmitter.emitTable(`table:save`, appId, tableToSave)
|
||||||
|
|
||||||
|
@ -171,6 +212,15 @@ exports.destroy = async function(ctx) {
|
||||||
// don't remove the table itself until very end
|
// don't remove the table itself until very end
|
||||||
await db.remove(tableToDelete)
|
await db.remove(tableToDelete)
|
||||||
|
|
||||||
|
// remove table search index
|
||||||
|
const currentIndexes = await db.getIndexes()
|
||||||
|
const existingIndex = currentIndexes.indexes.find(
|
||||||
|
existing => existing.name === `search:${ctx.params.tableId}`
|
||||||
|
)
|
||||||
|
if (existingIndex) {
|
||||||
|
await db.deleteIndex(existingIndex)
|
||||||
|
}
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emitTable(`table:delete`, appId, tableToDelete)
|
ctx.eventEmitter.emitTable(`table:delete`, appId, tableToDelete)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
|
|
@ -47,6 +47,7 @@ router.use(async (ctx, next) => {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
status: ctx.status,
|
status: ctx.status,
|
||||||
}
|
}
|
||||||
|
console.trace(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ const {
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
} = require("../../utilities/security/permissions")
|
} = require("../../utilities/security/permissions")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
|
const { bodyResource, paramResource } = require("../../middleware/resourceId")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -64,9 +65,15 @@ router
|
||||||
controller.getDefinitionList
|
controller.getDefinitionList
|
||||||
)
|
)
|
||||||
.get("/api/automations", authorized(BUILDER), controller.fetch)
|
.get("/api/automations", authorized(BUILDER), controller.fetch)
|
||||||
.get("/api/automations/:id", authorized(BUILDER), controller.find)
|
.get(
|
||||||
|
"/api/automations/:id",
|
||||||
|
paramResource("id"),
|
||||||
|
authorized(BUILDER),
|
||||||
|
controller.find
|
||||||
|
)
|
||||||
.put(
|
.put(
|
||||||
"/api/automations",
|
"/api/automations",
|
||||||
|
bodyResource("_id"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
generateValidator(true),
|
generateValidator(true),
|
||||||
controller.update
|
controller.update
|
||||||
|
@ -79,9 +86,15 @@ router
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/automations/:id/trigger",
|
"/api/automations/:id/trigger",
|
||||||
|
paramResource("id"),
|
||||||
authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
|
authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
|
||||||
controller.trigger
|
controller.trigger
|
||||||
)
|
)
|
||||||
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)
|
.delete(
|
||||||
|
"/api/automations/:id/:rev",
|
||||||
|
paramResource("id"),
|
||||||
|
authorized(BUILDER),
|
||||||
|
controller.destroy
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,10 +1,47 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/permission")
|
const controller = require("../controllers/permission")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const { BUILDER } = require("../../utilities/security/permissions")
|
const {
|
||||||
|
BUILDER,
|
||||||
|
PermissionLevels,
|
||||||
|
} = require("../../utilities/security/permissions")
|
||||||
|
const Joi = require("joi")
|
||||||
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.get("/api/permissions", authorized(BUILDER), controller.fetch)
|
function generateValidator() {
|
||||||
|
const permLevelArray = Object.values(PermissionLevels)
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.params(Joi.object({
|
||||||
|
level: Joi.string().valid(...permLevelArray).required(),
|
||||||
|
resourceId: Joi.string(),
|
||||||
|
roleId: Joi.string(),
|
||||||
|
}).unknown(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
router
|
||||||
|
.get("/api/permission/builtin", authorized(BUILDER), controller.fetchBuiltin)
|
||||||
|
.get("/api/permission/levels", authorized(BUILDER), controller.fetchLevels)
|
||||||
|
.get("/api/permission", authorized(BUILDER), controller.fetch)
|
||||||
|
.get(
|
||||||
|
"/api/permission/:resourceId",
|
||||||
|
authorized(BUILDER),
|
||||||
|
controller.getResourcePerms
|
||||||
|
)
|
||||||
|
// adding a specific role/level for the resource overrides the underlying access control
|
||||||
|
.post(
|
||||||
|
"/api/permission/:roleId/:resourceId/:level",
|
||||||
|
authorized(BUILDER),
|
||||||
|
generateValidator(),
|
||||||
|
controller.addPermission
|
||||||
|
)
|
||||||
|
// deleting the level defaults it back the underlying access control for the resource
|
||||||
|
.delete(
|
||||||
|
"/api/permission/:roleId/:resourceId/:level",
|
||||||
|
authorized(BUILDER),
|
||||||
|
generateValidator(),
|
||||||
|
controller.removePermission
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -8,6 +8,11 @@ const {
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
} = require("../../utilities/security/permissions")
|
} = require("../../utilities/security/permissions")
|
||||||
const joiValidator = require("../../middleware/joi-validator")
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
|
const {
|
||||||
|
bodyResource,
|
||||||
|
bodySubResource,
|
||||||
|
paramResource,
|
||||||
|
} = require("../../middleware/resourceId")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -43,12 +48,14 @@ router
|
||||||
.get("/api/queries", authorized(BUILDER), queryController.fetch)
|
.get("/api/queries", authorized(BUILDER), queryController.fetch)
|
||||||
.post(
|
.post(
|
||||||
"/api/queries",
|
"/api/queries",
|
||||||
|
bodySubResource("datasourceId", "_id"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
generateQueryValidation(),
|
generateQueryValidation(),
|
||||||
queryController.save
|
queryController.save
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/queries/preview",
|
"/api/queries/preview",
|
||||||
|
bodyResource("datasourceId"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
generateQueryPreviewValidation(),
|
generateQueryPreviewValidation(),
|
||||||
queryController.preview
|
queryController.preview
|
||||||
|
@ -60,11 +67,13 @@ router
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/queries/:queryId",
|
"/api/queries/:queryId",
|
||||||
|
paramResource("queryId"),
|
||||||
authorized(PermissionTypes.QUERY, PermissionLevels.WRITE),
|
authorized(PermissionTypes.QUERY, PermissionLevels.WRITE),
|
||||||
queryController.execute
|
queryController.execute
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/queries/:queryId/:revId",
|
"/api/queries/:queryId/:revId",
|
||||||
|
paramResource("queryId"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
queryController.destroy
|
queryController.destroy
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/role")
|
const controller = require("../controllers/role")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const { BUILDER } = require("../../utilities/security/permissions")
|
const {
|
||||||
|
BUILDER,
|
||||||
|
PermissionLevels,
|
||||||
|
} = require("../../utilities/security/permissions")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
const joiValidator = require("../../middleware/joi-validator")
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
const {
|
const {
|
||||||
|
@ -11,12 +14,17 @@ const {
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
function generateValidator() {
|
function generateValidator() {
|
||||||
|
const permLevelArray = Object.values(PermissionLevels)
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
_id: Joi.string().optional(),
|
_id: Joi.string().optional(),
|
||||||
_rev: Joi.string().optional(),
|
_rev: Joi.string().optional(),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
|
// this is the base permission ID (for now a built in)
|
||||||
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(),
|
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(),
|
||||||
|
permissions: Joi.object()
|
||||||
|
.pattern(/.*/, [Joi.string().valid(...permLevelArray)])
|
||||||
|
.optional(),
|
||||||
inherits: Joi.string().optional(),
|
inherits: Joi.string().optional(),
|
||||||
}).unknown(true))
|
}).unknown(true))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,10 @@ const Router = require("@koa/router")
|
||||||
const rowController = require("../controllers/row")
|
const rowController = require("../controllers/row")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const usage = require("../../middleware/usageQuota")
|
const usage = require("../../middleware/usageQuota")
|
||||||
|
const {
|
||||||
|
paramResource,
|
||||||
|
paramSubResource,
|
||||||
|
} = require("../../middleware/resourceId")
|
||||||
const {
|
const {
|
||||||
PermissionLevels,
|
PermissionLevels,
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
|
@ -12,37 +16,50 @@ const router = Router()
|
||||||
router
|
router
|
||||||
.get(
|
.get(
|
||||||
"/api/:tableId/:rowId/enrich",
|
"/api/:tableId/:rowId/enrich",
|
||||||
|
paramSubResource("tableId", "rowId"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
||||||
rowController.fetchEnrichedRow
|
rowController.fetchEnrichedRow
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/:tableId/rows",
|
"/api/:tableId/rows",
|
||||||
|
paramResource("tableId"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
||||||
rowController.fetchTableRows
|
rowController.fetchTableRows
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/:tableId/rows/:rowId",
|
"/api/:tableId/rows/:rowId",
|
||||||
|
paramSubResource("tableId", "rowId"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
||||||
rowController.find
|
rowController.find
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/:tableId/rows",
|
"/api/:tableId/rows",
|
||||||
|
paramResource("tableId"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
||||||
usage,
|
usage,
|
||||||
rowController.save
|
rowController.save
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/api/:tableId/rows/search",
|
||||||
|
paramResource("tableId"),
|
||||||
|
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
||||||
|
rowController.search
|
||||||
|
)
|
||||||
.patch(
|
.patch(
|
||||||
"/api/:tableId/rows/:id",
|
"/api/:tableId/rows/:rowId",
|
||||||
|
paramSubResource("tableId", "rowId"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
||||||
rowController.patch
|
rowController.patch
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/:tableId/rows/validate",
|
"/api/:tableId/rows/validate",
|
||||||
|
paramResource("tableId"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
||||||
rowController.validate
|
rowController.validate
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/:tableId/rows/:rowId/:revId",
|
"/api/:tableId/rows/:rowId/:revId",
|
||||||
|
paramSubResource("tableId", "rowId"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
||||||
usage,
|
usage,
|
||||||
rowController.destroy
|
rowController.destroy
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const tableController = require("../controllers/table")
|
const tableController = require("../controllers/table")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
|
const { paramResource, bodyResource } = require("../../middleware/resourceId")
|
||||||
const {
|
const {
|
||||||
BUILDER,
|
BUILDER,
|
||||||
PermissionLevels,
|
PermissionLevels,
|
||||||
|
@ -13,10 +14,17 @@ router
|
||||||
.get("/api/tables", authorized(BUILDER), tableController.fetch)
|
.get("/api/tables", authorized(BUILDER), tableController.fetch)
|
||||||
.get(
|
.get(
|
||||||
"/api/tables/:id",
|
"/api/tables/:id",
|
||||||
|
paramResource("id"),
|
||||||
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
||||||
tableController.find
|
tableController.find
|
||||||
)
|
)
|
||||||
.post("/api/tables", authorized(BUILDER), tableController.save)
|
.post(
|
||||||
|
"/api/tables",
|
||||||
|
// allows control over updating a table
|
||||||
|
bodyResource("_id"),
|
||||||
|
authorized(BUILDER),
|
||||||
|
tableController.save
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/tables/csv/validate",
|
"/api/tables/csv/validate",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
|
@ -24,6 +32,7 @@ router
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/tables/:tableId/:revId",
|
"/api/tables/:tableId/:revId",
|
||||||
|
paramResource("tableId"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
tableController.destroy
|
tableController.destroy
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,9 @@ const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
|
||||||
const packageJson = require("../../../../package")
|
const packageJson = require("../../../../package")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
|
const {
|
||||||
|
BUILTIN_PERMISSION_IDS,
|
||||||
|
} = require("../../../utilities/security/permissions")
|
||||||
|
|
||||||
const TEST_CLIENT_ID = "test-client-id"
|
const TEST_CLIENT_ID = "test-client-id"
|
||||||
|
|
||||||
|
@ -37,6 +40,17 @@ exports.defaultHeaders = appId => {
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.publicHeaders = appId => {
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/json",
|
||||||
|
}
|
||||||
|
if (appId) {
|
||||||
|
headers["x-budibase-app-id"] = appId
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
exports.BASE_TABLE = {
|
exports.BASE_TABLE = {
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
type: "table",
|
type: "table",
|
||||||
|
@ -70,6 +84,56 @@ exports.createTable = async (request, appId, table, removeId = true) => {
|
||||||
return res.body
|
return res.body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.makeBasicRow = tableId => {
|
||||||
|
return {
|
||||||
|
name: "Test Contact",
|
||||||
|
description: "original description",
|
||||||
|
status: "new",
|
||||||
|
tableId: tableId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createRow = async (request, appId, tableId, row = null) => {
|
||||||
|
row = row || exports.makeBasicRow(tableId)
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/${tableId}/rows`)
|
||||||
|
.send(row)
|
||||||
|
.set(exports.defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createRole = async (request, appId) => {
|
||||||
|
const roleBody = {
|
||||||
|
name: "NewRole",
|
||||||
|
inherits: BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY,
|
||||||
|
}
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/roles`)
|
||||||
|
.send(roleBody)
|
||||||
|
.set(exports.defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addPermission = async (
|
||||||
|
request,
|
||||||
|
appId,
|
||||||
|
role,
|
||||||
|
resource,
|
||||||
|
level = "read"
|
||||||
|
) => {
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/permission/${role}/${resource}/${level}`)
|
||||||
|
.set(exports.defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
exports.createLinkedTable = async (request, appId) => {
|
exports.createLinkedTable = async (request, appId) => {
|
||||||
// get the ID to link to
|
// get the ID to link to
|
||||||
const table = await exports.createTable(request, appId)
|
const table = await exports.createTable(request, appId)
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
const {
|
||||||
|
createApplication,
|
||||||
|
createTable,
|
||||||
|
createRow,
|
||||||
|
supertest,
|
||||||
|
defaultHeaders,
|
||||||
|
addPermission,
|
||||||
|
publicHeaders,
|
||||||
|
makeBasicRow,
|
||||||
|
} = require("./couchTestUtils")
|
||||||
|
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
|
||||||
|
|
||||||
|
const HIGHER_ROLE_ID = BUILTIN_ROLE_IDS.BASIC
|
||||||
|
const STD_ROLE_ID = BUILTIN_ROLE_IDS.PUBLIC
|
||||||
|
|
||||||
|
describe("/permission", () => {
|
||||||
|
let server
|
||||||
|
let request
|
||||||
|
let appId
|
||||||
|
let table
|
||||||
|
let perms
|
||||||
|
let row
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ request, server } = await supertest())
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
server.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
let app = await createApplication(request)
|
||||||
|
appId = app.instance._id
|
||||||
|
table = await createTable(request, appId)
|
||||||
|
perms = await addPermission(request, appId, STD_ROLE_ID, table._id)
|
||||||
|
row = await createRow(request, appId, table._id)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getTablePermissions() {
|
||||||
|
return request
|
||||||
|
.get(`/api/permission/${table._id}`)
|
||||||
|
.set(defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("levels", () => {
|
||||||
|
it("should be able to get levels", async () => {
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/permission/levels`)
|
||||||
|
.set(defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body).toBeDefined()
|
||||||
|
expect(res.body.length).toEqual(2)
|
||||||
|
expect(res.body).toContain("read")
|
||||||
|
expect(res.body).toContain("write")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("add", () => {
|
||||||
|
it("should be able to add permission to a role for the table", async () => {
|
||||||
|
expect(perms.length).toEqual(1)
|
||||||
|
expect(perms[0]._id).toEqual(`${STD_ROLE_ID}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get the resource permissions", async () => {
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/permission/${table._id}`)
|
||||||
|
.set(defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||||
|
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get resource permissions with multiple roles", async () => {
|
||||||
|
perms = await addPermission(request, appId, HIGHER_ROLE_ID, table._id, "write")
|
||||||
|
const res = await getTablePermissions()
|
||||||
|
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||||
|
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
|
const allRes = await request
|
||||||
|
.get(`/api/permission`)
|
||||||
|
.set(defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
|
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("remove", () => {
|
||||||
|
it("should be able to remove the permission", async () => {
|
||||||
|
const res = await request
|
||||||
|
.delete(`/api/permission/${STD_ROLE_ID}/${table._id}/read`)
|
||||||
|
.set(defaultHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body[0]._id).toEqual(STD_ROLE_ID)
|
||||||
|
const permsRes = await getTablePermissions()
|
||||||
|
expect(permsRes.body[STD_ROLE_ID]).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("check public user allowed", () => {
|
||||||
|
it("should be able to read the row", async () => {
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/${table._id}/rows`)
|
||||||
|
.set(publicHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body[0]._id).toEqual(row._id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't allow writing from a public user", async () => {
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/${table._id}/rows`)
|
||||||
|
.send(makeBasicRow(table._id))
|
||||||
|
.set(publicHeaders(appId))
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(403)
|
||||||
|
expect(res.status).toEqual(403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,7 +1,5 @@
|
||||||
const {
|
const {
|
||||||
createApplication,
|
createApplication,
|
||||||
createTable,
|
|
||||||
createView,
|
|
||||||
supertest,
|
supertest,
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
} = require("./couchTestUtils")
|
} = require("./couchTestUtils")
|
||||||
|
@ -20,8 +18,6 @@ describe("/roles", () => {
|
||||||
let server
|
let server
|
||||||
let request
|
let request
|
||||||
let appId
|
let appId
|
||||||
let table
|
|
||||||
let view
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;({ request, server } = await supertest())
|
;({ request, server } = await supertest())
|
||||||
|
@ -34,8 +30,6 @@ describe("/roles", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
let app = await createApplication(request)
|
let app = await createApplication(request)
|
||||||
appId = app.instance._id
|
appId = app.instance._id
|
||||||
table = await createTable(request, appId)
|
|
||||||
view = await createView(request, appId, table._id)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
|
|
|
@ -5,6 +5,7 @@ const {
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
createLinkedTable,
|
createLinkedTable,
|
||||||
createAttachmentTable,
|
createAttachmentTable,
|
||||||
|
makeBasicRow,
|
||||||
} = require("./couchTestUtils");
|
} = require("./couchTestUtils");
|
||||||
const { enrichRows } = require("../../../utilities")
|
const { enrichRows } = require("../../../utilities")
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
|
@ -30,12 +31,7 @@ describe("/rows", () => {
|
||||||
app = await createApplication(request)
|
app = await createApplication(request)
|
||||||
appId = app.instance._id
|
appId = app.instance._id
|
||||||
table = await createTable(request, appId)
|
table = await createTable(request, appId)
|
||||||
row = {
|
row = makeBasicRow(table._id)
|
||||||
name: "Test Contact",
|
|
||||||
description: "original description",
|
|
||||||
status: "new",
|
|
||||||
tableId: table._id
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const createRow = async r =>
|
const createRow = async r =>
|
||||||
|
|
|
@ -2,6 +2,7 @@ const Router = require("@koa/router")
|
||||||
const viewController = require("../controllers/view")
|
const viewController = require("../controllers/view")
|
||||||
const rowController = require("../controllers/row")
|
const rowController = require("../controllers/row")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
|
const { paramResource } = require("../../middleware/resourceId")
|
||||||
const {
|
const {
|
||||||
BUILDER,
|
BUILDER,
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
|
@ -15,12 +16,14 @@ router
|
||||||
.get("/api/views/export", authorized(BUILDER), viewController.exportView)
|
.get("/api/views/export", authorized(BUILDER), viewController.exportView)
|
||||||
.get(
|
.get(
|
||||||
"/api/views/:viewName",
|
"/api/views/:viewName",
|
||||||
|
paramResource("viewName"),
|
||||||
authorized(PermissionTypes.VIEW, PermissionLevels.READ),
|
authorized(PermissionTypes.VIEW, PermissionLevels.READ),
|
||||||
rowController.fetchView
|
rowController.fetchView
|
||||||
)
|
)
|
||||||
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/views/:viewName",
|
"/api/views/:viewName",
|
||||||
|
paramResource("viewName"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
usage,
|
usage,
|
||||||
viewController.destroy
|
viewController.destroy
|
||||||
|
|
|
@ -14,7 +14,15 @@ const selfhost = require("./selfhost")
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
|
|
||||||
// set up top level koa middleware
|
// set up top level koa middleware
|
||||||
app.use(koaBody({ multipart: true }))
|
app.use(
|
||||||
|
koaBody({
|
||||||
|
multipart: true,
|
||||||
|
formLimit: "10mb",
|
||||||
|
jsonLimit: "10mb",
|
||||||
|
textLimit: "10mb",
|
||||||
|
enableTypes: ["json", "form", "text"],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
logger({
|
logger({
|
||||||
|
|
|
@ -2,12 +2,14 @@ const PouchDB = require("pouchdb")
|
||||||
const replicationStream = require("pouchdb-replication-stream")
|
const replicationStream = require("pouchdb-replication-stream")
|
||||||
const allDbs = require("pouchdb-all-dbs")
|
const allDbs = require("pouchdb-all-dbs")
|
||||||
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
||||||
|
const find = require("pouchdb-find")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
|
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
|
||||||
const isInMemory = env.NODE_ENV === "jest"
|
const isInMemory = env.NODE_ENV === "jest"
|
||||||
|
|
||||||
PouchDB.plugin(replicationStream.plugin)
|
PouchDB.plugin(replicationStream.plugin)
|
||||||
|
PouchDB.plugin(find)
|
||||||
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
|
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
|
||||||
|
|
||||||
let POUCH_DB_DEFAULTS = {
|
let POUCH_DB_DEFAULTS = {
|
||||||
|
|
|
@ -170,8 +170,8 @@ exports.getAppParams = (appId = null, otherProps = {}) => {
|
||||||
* Generates a new role ID.
|
* Generates a new role ID.
|
||||||
* @returns {string} The new role ID which the role doc can be stored under.
|
* @returns {string} The new role ID which the role doc can be stored under.
|
||||||
*/
|
*/
|
||||||
exports.generateRoleID = () => {
|
exports.generateRoleID = id => {
|
||||||
return `${DocumentTypes.ROLE}${SEPARATOR}${newid()}`
|
return `${DocumentTypes.ROLE}${SEPARATOR}${id || newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
const {
|
const {
|
||||||
BUILTIN_ROLE_IDS,
|
BUILTIN_ROLE_IDS,
|
||||||
getUserPermissionIds,
|
getUserPermissions,
|
||||||
} = require("../utilities/security/roles")
|
} = require("../utilities/security/roles")
|
||||||
const {
|
const {
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
doesHavePermission,
|
doesHaveResourcePermission,
|
||||||
|
doesHaveBasePermission,
|
||||||
} = require("../utilities/security/permissions")
|
} = require("../utilities/security/permissions")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const { isAPIKeyValid } = require("../utilities/security/apikey")
|
const { isAPIKeyValid } = require("../utilities/security/apikey")
|
||||||
|
@ -14,6 +15,10 @@ const ADMIN_ROLES = [BUILTIN_ROLE_IDS.ADMIN, BUILTIN_ROLE_IDS.BUILDER]
|
||||||
|
|
||||||
const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|"))
|
const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|"))
|
||||||
|
|
||||||
|
function hasResource(ctx) {
|
||||||
|
return ctx.resourceId != null
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
// webhooks can pass locally
|
// webhooks can pass locally
|
||||||
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
|
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
|
||||||
|
@ -38,25 +43,39 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
// don't expose builder endpoints in the cloud
|
// don't expose builder endpoints in the cloud
|
||||||
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
|
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
|
||||||
|
|
||||||
if (!ctx.auth.authenticated) {
|
|
||||||
ctx.throw(403, "Session not authenticated")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.user) {
|
if (!ctx.user) {
|
||||||
ctx.throw(403, "User not found")
|
ctx.throw(403, "No user info found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = ctx.user.role
|
const role = ctx.user.role
|
||||||
const permissions = await getUserPermissionIds(ctx.appId, role._id)
|
const { basePermissions, permissions } = await getUserPermissions(
|
||||||
if (ADMIN_ROLES.indexOf(role._id) !== -1) {
|
ctx.appId,
|
||||||
return next()
|
role._id
|
||||||
}
|
)
|
||||||
|
const isAdmin = ADMIN_ROLES.indexOf(role._id) !== -1
|
||||||
|
const isAuthed = ctx.auth.authenticated
|
||||||
|
|
||||||
if (permType === PermissionTypes.BUILDER) {
|
// this may need to change in the future, right now only admins
|
||||||
|
// can have access to builder features, this is hard coded into
|
||||||
|
// our rules
|
||||||
|
if (isAdmin && isAuthed) {
|
||||||
|
return next()
|
||||||
|
} else if (permType === PermissionTypes.BUILDER) {
|
||||||
ctx.throw(403, "Not Authorized")
|
ctx.throw(403, "Not Authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doesHavePermission(permType, permLevel, permissions)) {
|
if (
|
||||||
|
hasResource(ctx) &&
|
||||||
|
doesHaveResourcePermission(permissions, permLevel, ctx)
|
||||||
|
) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthed) {
|
||||||
|
ctx.throw(403, "Session not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doesHaveBasePermission(permType, permLevel, basePermissions)) {
|
||||||
ctx.throw(403, "User does not have permission")
|
ctx.throw(403, "User does not have permission")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,3 +22,7 @@ function validate(schema, property) {
|
||||||
module.exports.body = schema => {
|
module.exports.body = schema => {
|
||||||
return validate(schema, "body")
|
return validate(schema, "body")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.params = schema => {
|
||||||
|
return validate(schema, "params")
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
class ResourceIdGetter {
|
||||||
|
constructor(ctxProperty) {
|
||||||
|
this.parameter = ctxProperty
|
||||||
|
this.main = null
|
||||||
|
this.sub = null
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
mainResource(field) {
|
||||||
|
this.main = field
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
subResource(field) {
|
||||||
|
this.sub = field
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
const parameter = this.parameter,
|
||||||
|
main = this.main,
|
||||||
|
sub = this.sub
|
||||||
|
return (ctx, next) => {
|
||||||
|
const request = ctx.request[parameter] || ctx[parameter]
|
||||||
|
if (request == null) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
if (main != null && request[main]) {
|
||||||
|
ctx.resourceId = request[main]
|
||||||
|
}
|
||||||
|
if (sub != null && request[sub]) {
|
||||||
|
ctx.subResourceId = request[sub]
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.paramResource = main => {
|
||||||
|
return new ResourceIdGetter("params").mainResource(main).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.paramSubResource = (main, sub) => {
|
||||||
|
return new ResourceIdGetter("params")
|
||||||
|
.mainResource(main)
|
||||||
|
.subResource(sub)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.bodyResource = main => {
|
||||||
|
return new ResourceIdGetter("body").mainResource(main).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.bodySubResource = (main, sub) => {
|
||||||
|
return new ResourceIdGetter("body")
|
||||||
|
.mainResource(main)
|
||||||
|
.subResource(sub)
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ const PermissionLevels = {
|
||||||
ADMIN: "admin",
|
ADMIN: "admin",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// these are the global types, that govern the underlying default behaviour
|
||||||
const PermissionTypes = {
|
const PermissionTypes = {
|
||||||
TABLE: "table",
|
TABLE: "table",
|
||||||
USER: "user",
|
USER: "user",
|
||||||
|
@ -22,6 +23,22 @@ function Permission(type, level) {
|
||||||
this.type = type
|
this.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function levelToNumber(perm) {
|
||||||
|
switch (perm) {
|
||||||
|
// not everything has execute privileges
|
||||||
|
case PermissionLevels.EXECUTE:
|
||||||
|
return 0
|
||||||
|
case PermissionLevels.READ:
|
||||||
|
return 1
|
||||||
|
case PermissionLevels.WRITE:
|
||||||
|
return 2
|
||||||
|
case PermissionLevels.ADMIN:
|
||||||
|
return 3
|
||||||
|
default:
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the specified permission level for the user return the levels they are allowed to carry out.
|
* Given the specified permission level for the user return the levels they are allowed to carry out.
|
||||||
* @param {string} userPermLevel The permission level of the user.
|
* @param {string} userPermLevel The permission level of the user.
|
||||||
|
@ -29,12 +46,11 @@ function Permission(type, level) {
|
||||||
*/
|
*/
|
||||||
function getAllowedLevels(userPermLevel) {
|
function getAllowedLevels(userPermLevel) {
|
||||||
switch (userPermLevel) {
|
switch (userPermLevel) {
|
||||||
case PermissionLevels.READ:
|
|
||||||
return [PermissionLevels.READ]
|
|
||||||
case PermissionLevels.WRITE:
|
|
||||||
return [PermissionLevels.READ, PermissionLevels.WRITE]
|
|
||||||
case PermissionLevels.EXECUTE:
|
case PermissionLevels.EXECUTE:
|
||||||
return [PermissionLevels.EXECUTE]
|
return [PermissionLevels.EXECUTE]
|
||||||
|
case PermissionLevels.READ:
|
||||||
|
return [PermissionLevels.EXECUTE, PermissionLevels.READ]
|
||||||
|
case PermissionLevels.WRITE:
|
||||||
case PermissionLevels.ADMIN:
|
case PermissionLevels.ADMIN:
|
||||||
return [
|
return [
|
||||||
PermissionLevels.READ,
|
PermissionLevels.READ,
|
||||||
|
@ -47,6 +63,7 @@ function getAllowedLevels(userPermLevel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.BUILTIN_PERMISSION_IDS = {
|
exports.BUILTIN_PERMISSION_IDS = {
|
||||||
|
PUBLIC: "public",
|
||||||
READ_ONLY: "read_only",
|
READ_ONLY: "read_only",
|
||||||
WRITE: "write",
|
WRITE: "write",
|
||||||
ADMIN: "admin",
|
ADMIN: "admin",
|
||||||
|
@ -54,6 +71,13 @@ exports.BUILTIN_PERMISSION_IDS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.BUILTIN_PERMISSIONS = {
|
exports.BUILTIN_PERMISSIONS = {
|
||||||
|
PUBLIC: {
|
||||||
|
_id: exports.BUILTIN_PERMISSION_IDS.PUBLIC,
|
||||||
|
name: "Public",
|
||||||
|
permissions: [
|
||||||
|
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE),
|
||||||
|
],
|
||||||
|
},
|
||||||
READ_ONLY: {
|
READ_ONLY: {
|
||||||
_id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY,
|
_id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY,
|
||||||
name: "Read only",
|
name: "Read only",
|
||||||
|
@ -97,7 +121,40 @@ exports.BUILTIN_PERMISSIONS = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.doesHavePermission = (permType, permLevel, permissionIds) => {
|
exports.getBuiltinPermissionByID = id => {
|
||||||
|
const perms = Object.values(exports.BUILTIN_PERMISSIONS)
|
||||||
|
return perms.find(perm => perm._id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.doesHaveResourcePermission = (
|
||||||
|
permissions,
|
||||||
|
permLevel,
|
||||||
|
{ resourceId, subResourceId }
|
||||||
|
) => {
|
||||||
|
// set foundSub to not subResourceId, incase there is no subResource
|
||||||
|
let foundMain = false,
|
||||||
|
foundSub = !subResourceId
|
||||||
|
for (let [resource, level] of Object.entries(permissions)) {
|
||||||
|
const levels = getAllowedLevels(level)
|
||||||
|
if (resource === resourceId && levels.indexOf(permLevel) !== -1) {
|
||||||
|
foundMain = true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
subResourceId &&
|
||||||
|
resource === subResourceId &&
|
||||||
|
levels.indexOf(permLevel) !== -1
|
||||||
|
) {
|
||||||
|
foundSub = true
|
||||||
|
}
|
||||||
|
// this will escape if foundMain only when no sub resource
|
||||||
|
if (foundMain && foundSub) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundMain && foundSub
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
|
||||||
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
|
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
|
||||||
let permissions = flatten(
|
let permissions = flatten(
|
||||||
builtins
|
builtins
|
||||||
|
@ -115,6 +172,14 @@ exports.doesHavePermission = (permType, permLevel, permissionIds) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.higherPermission = (perm1, perm2) => {
|
||||||
|
return levelToNumber(perm1) > levelToNumber(perm2) ? perm1 : perm2
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isPermissionLevelHigherThanRead = level => {
|
||||||
|
return levelToNumber(level) > 1
|
||||||
|
}
|
||||||
|
|
||||||
// utility as a lot of things need simply the builder permission
|
// utility as a lot of things need simply the builder permission
|
||||||
exports.BUILDER = PermissionTypes.BUILDER
|
exports.BUILDER = PermissionTypes.BUILDER
|
||||||
exports.PermissionTypes = PermissionTypes
|
exports.PermissionTypes = PermissionTypes
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
const { BUILTIN_PERMISSION_IDS, higherPermission } = require("./permissions")
|
||||||
|
const { generateRoleID, DocumentTypes, SEPARATOR } = require("../../db/utils")
|
||||||
|
|
||||||
const BUILTIN_IDS = {
|
const BUILTIN_IDS = {
|
||||||
ADMIN: "ADMIN",
|
ADMIN: "ADMIN",
|
||||||
|
@ -36,7 +37,7 @@ exports.BUILTIN_ROLES = {
|
||||||
.addPermission(BUILTIN_PERMISSION_IDS.WRITE)
|
.addPermission(BUILTIN_PERMISSION_IDS.WRITE)
|
||||||
.addInheritance(BUILTIN_IDS.PUBLIC),
|
.addInheritance(BUILTIN_IDS.PUBLIC),
|
||||||
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission(
|
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission(
|
||||||
BUILTIN_PERMISSION_IDS.READ_ONLY
|
BUILTIN_PERMISSION_IDS.PUBLIC
|
||||||
),
|
),
|
||||||
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission(
|
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission(
|
||||||
BUILTIN_PERMISSION_IDS.ADMIN
|
BUILTIN_PERMISSION_IDS.ADMIN
|
||||||
|
@ -44,15 +45,50 @@ exports.BUILTIN_ROLES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
||||||
level => level._id
|
role => role._id
|
||||||
)
|
)
|
||||||
|
|
||||||
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
||||||
level => level.name
|
role => role.name
|
||||||
)
|
)
|
||||||
|
|
||||||
function isBuiltin(role) {
|
function isBuiltin(role) {
|
||||||
return exports.BUILTIN_ROLE_ID_ARRAY.indexOf(role) !== -1
|
return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works through the inheritance ranks to see how far up the builtin stack this ID is.
|
||||||
|
*/
|
||||||
|
function builtinRoleToNumber(id) {
|
||||||
|
const MAX = Object.values(BUILTIN_IDS).length + 1
|
||||||
|
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
|
||||||
|
return MAX
|
||||||
|
}
|
||||||
|
let role = exports.BUILTIN_ROLES[id],
|
||||||
|
count = 0
|
||||||
|
do {
|
||||||
|
if (!role) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
role = exports.BUILTIN_ROLES[role.inherits]
|
||||||
|
count++
|
||||||
|
} while (role !== null)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whichever builtin roleID is lower.
|
||||||
|
*/
|
||||||
|
exports.lowerBuiltinRoleID = (roleId1, roleId2) => {
|
||||||
|
if (!roleId1) {
|
||||||
|
return roleId2
|
||||||
|
}
|
||||||
|
if (!roleId2) {
|
||||||
|
return roleId1
|
||||||
|
}
|
||||||
|
return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2)
|
||||||
|
? roleId2
|
||||||
|
: roleId1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,14 +102,25 @@ exports.getRole = async (appId, roleId) => {
|
||||||
if (!roleId) {
|
if (!roleId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let role
|
let role = {}
|
||||||
|
// built in roles mostly come from the in-code implementation,
|
||||||
|
// but can be extended by a doc stored about them (e.g. permissions)
|
||||||
if (isBuiltin(roleId)) {
|
if (isBuiltin(roleId)) {
|
||||||
role = cloneDeep(
|
role = cloneDeep(
|
||||||
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
|
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
|
try {
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
role = await db.get(roleId)
|
const dbRole = await db.get(exports.getDBRoleID(roleId))
|
||||||
|
role = Object.assign(role, dbRole)
|
||||||
|
// finalise the ID
|
||||||
|
role._id = exports.getExternalRoleID(role._id)
|
||||||
|
} catch (err) {
|
||||||
|
// only throw an error if there is no role at all
|
||||||
|
if (Object.keys(role).length === 0) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
|
@ -118,14 +165,26 @@ exports.getUserRoleHierarchy = async (appId, userRoleId) => {
|
||||||
* Get all of the user permissions which could be found across the role hierarchy
|
* Get all of the user permissions which could be found across the role hierarchy
|
||||||
* @param appId The ID of the application from which roles should be obtained.
|
* @param appId The ID of the application from which roles should be obtained.
|
||||||
* @param userRoleId The user's role ID, this can be found in their access token.
|
* @param userRoleId The user's role ID, this can be found in their access token.
|
||||||
* @returns {Promise<string[]>} A list of permission IDs these should all be unique.
|
* @returns {Promise<{basePermissions: string[], permissions: Object}>} the base
|
||||||
|
* permission IDs as well as any custom resource permissions.
|
||||||
*/
|
*/
|
||||||
exports.getUserPermissionIds = async (appId, userRoleId) => {
|
exports.getUserPermissions = async (appId, userRoleId) => {
|
||||||
return [
|
const rolesHierarchy = await getAllUserRoles(appId, userRoleId)
|
||||||
...new Set(
|
const basePermissions = [
|
||||||
(await getAllUserRoles(appId, userRoleId)).map(role => role.permissionId)
|
...new Set(rolesHierarchy.map(role => role.permissionId)),
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
const permissions = {}
|
||||||
|
for (let role of rolesHierarchy) {
|
||||||
|
if (role.permissions) {
|
||||||
|
for (let [resource, level] of Object.entries(role.permissions)) {
|
||||||
|
permissions[resource] = higherPermission(permissions[resource], level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
basePermissions,
|
||||||
|
permissions,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccessController {
|
class AccessController {
|
||||||
|
@ -177,6 +236,27 @@ class AccessController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions).
|
||||||
|
*/
|
||||||
|
exports.getDBRoleID = roleId => {
|
||||||
|
if (roleId.startsWith(DocumentTypes.ROLE)) {
|
||||||
|
return roleId
|
||||||
|
}
|
||||||
|
return generateRoleID(roleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the "role_" from builtin role IDs that have been written to the DB (for permissions).
|
||||||
|
*/
|
||||||
|
exports.getExternalRoleID = roleId => {
|
||||||
|
// for built in roles we want to remove the DB role ID element (role_)
|
||||||
|
if (roleId.startsWith(DocumentTypes.ROLE) && isBuiltin(roleId)) {
|
||||||
|
return roleId.split(`${DocumentTypes.ROLE}${SEPARATOR}`)[1]
|
||||||
|
}
|
||||||
|
return roleId
|
||||||
|
}
|
||||||
|
|
||||||
exports.AccessController = AccessController
|
exports.AccessController = AccessController
|
||||||
exports.BUILTIN_ROLE_IDS = BUILTIN_IDS
|
exports.BUILTIN_ROLE_IDS = BUILTIN_IDS
|
||||||
exports.isBuiltin = isBuiltin
|
exports.isBuiltin = isBuiltin
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
const {
|
||||||
|
PermissionLevels,
|
||||||
|
PermissionTypes,
|
||||||
|
getBuiltinPermissionByID,
|
||||||
|
isPermissionLevelHigherThanRead,
|
||||||
|
} = require("../../utilities/security/permissions")
|
||||||
|
const {
|
||||||
|
lowerBuiltinRoleID,
|
||||||
|
BUILTIN_ROLES,
|
||||||
|
} = require("../../utilities/security/roles")
|
||||||
|
const { DocumentTypes } = require("../../db/utils")
|
||||||
|
|
||||||
|
const CURRENTLY_SUPPORTED_LEVELS = [
|
||||||
|
PermissionLevels.WRITE,
|
||||||
|
PermissionLevels.READ,
|
||||||
|
]
|
||||||
|
|
||||||
|
exports.getPermissionType = resourceId => {
|
||||||
|
const docType = Object.values(DocumentTypes).filter(docType =>
|
||||||
|
resourceId.startsWith(docType)
|
||||||
|
)[0]
|
||||||
|
switch (docType) {
|
||||||
|
case DocumentTypes.TABLE:
|
||||||
|
case DocumentTypes.ROW:
|
||||||
|
return PermissionTypes.TABLE
|
||||||
|
case DocumentTypes.AUTOMATION:
|
||||||
|
return PermissionTypes.AUTOMATION
|
||||||
|
case DocumentTypes.WEBHOOK:
|
||||||
|
return PermissionTypes.WEBHOOK
|
||||||
|
case DocumentTypes.QUERY:
|
||||||
|
case DocumentTypes.DATASOURCE:
|
||||||
|
return PermissionTypes.QUERY
|
||||||
|
default:
|
||||||
|
// views don't have an ID, will end up here
|
||||||
|
return PermissionTypes.VIEW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* works out the basic permissions based on builtin roles for a resource, using its ID
|
||||||
|
* @param resourceId
|
||||||
|
* @returns {{}}
|
||||||
|
*/
|
||||||
|
exports.getBasePermissions = resourceId => {
|
||||||
|
const type = exports.getPermissionType(resourceId)
|
||||||
|
const permissions = {}
|
||||||
|
for (let [roleId, role] of Object.entries(BUILTIN_ROLES)) {
|
||||||
|
if (!role.permissionId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const perms = getBuiltinPermissionByID(role.permissionId)
|
||||||
|
const typedPermission = perms.permissions.find(perm => perm.type === type)
|
||||||
|
if (
|
||||||
|
typedPermission &&
|
||||||
|
CURRENTLY_SUPPORTED_LEVELS.indexOf(typedPermission.level) !== -1
|
||||||
|
) {
|
||||||
|
const level = typedPermission.level
|
||||||
|
permissions[level] = lowerBuiltinRoleID(permissions[level], roleId)
|
||||||
|
if (isPermissionLevelHigherThanRead(level)) {
|
||||||
|
permissions[PermissionLevels.READ] = lowerBuiltinRoleID(
|
||||||
|
permissions[PermissionLevels.READ],
|
||||||
|
roleId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.CURRENTLY_SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
|
@ -3219,6 +3219,13 @@ fd-slicer@~1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pend "~1.2.0"
|
pend "~1.2.0"
|
||||||
|
|
||||||
|
fetch-cookie@0.10.1:
|
||||||
|
version "0.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.10.1.tgz#5ea88f3d36950543c87997c27ae2aeafb4b5c4d4"
|
||||||
|
integrity sha512-beB+VEd4cNeVG1PY+ee74+PkuCQnik78pgLi5Ah/7qdUfov8IctU0vLUbBT8/10Ma5GMBeI4wtxhGrEfKNYs2g==
|
||||||
|
dependencies:
|
||||||
|
tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0"
|
||||||
|
|
||||||
fetch-cookie@0.7.3:
|
fetch-cookie@0.7.3:
|
||||||
version "0.7.3"
|
version "0.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.7.3.tgz#b8d023f421dd2b2f4a0eca9cd7318a967ed4eed8"
|
resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.7.3.tgz#b8d023f421dd2b2f4a0eca9cd7318a967ed4eed8"
|
||||||
|
@ -6498,6 +6505,20 @@ pouch-stream@^0.4.0:
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
readable-stream "^1.0.27-1"
|
readable-stream "^1.0.27-1"
|
||||||
|
|
||||||
|
pouchdb-abstract-mapreduce@7.2.2:
|
||||||
|
version "7.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-7.2.2.tgz#dd1b10a83f8d24361dce9aaaab054614b39f766f"
|
||||||
|
integrity sha512-7HWN/2yV2JkwMnGnlp84lGvFtnm0Q55NiBUdbBcaT810+clCGKvhssBCrXnmwShD1SXTwT83aszsgiSfW+SnBA==
|
||||||
|
dependencies:
|
||||||
|
pouchdb-binary-utils "7.2.2"
|
||||||
|
pouchdb-collate "7.2.2"
|
||||||
|
pouchdb-collections "7.2.2"
|
||||||
|
pouchdb-errors "7.2.2"
|
||||||
|
pouchdb-fetch "7.2.2"
|
||||||
|
pouchdb-mapreduce-utils "7.2.2"
|
||||||
|
pouchdb-md5 "7.2.2"
|
||||||
|
pouchdb-utils "7.2.2"
|
||||||
|
|
||||||
pouchdb-adapter-leveldb-core@7.2.2:
|
pouchdb-adapter-leveldb-core@7.2.2:
|
||||||
version "7.2.2"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.2.tgz#e0aa6a476e2607d7ae89f4a803c9fba6e6d05a8a"
|
resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.2.tgz#e0aa6a476e2607d7ae89f4a803c9fba6e6d05a8a"
|
||||||
|
@ -6557,6 +6578,11 @@ pouchdb-binary-utils@7.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-from "1.1.1"
|
buffer-from "1.1.1"
|
||||||
|
|
||||||
|
pouchdb-collate@7.2.2:
|
||||||
|
version "7.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-collate/-/pouchdb-collate-7.2.2.tgz#fc261f5ef837c437e3445fb0abc3f125d982c37c"
|
||||||
|
integrity sha512-/SMY9GGasslknivWlCVwXMRMnQ8myKHs4WryQ5535nq1Wj/ehpqWloMwxEQGvZE1Sda3LOm7/5HwLTcB8Our+w==
|
||||||
|
|
||||||
pouchdb-collections@7.2.2:
|
pouchdb-collections@7.2.2:
|
||||||
version "7.2.2"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-7.2.2.tgz#aeed77f33322429e3f59d59ea233b48ff0e68572"
|
resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-7.2.2.tgz#aeed77f33322429e3f59d59ea233b48ff0e68572"
|
||||||
|
@ -6569,6 +6595,28 @@ pouchdb-errors@7.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits "2.0.4"
|
inherits "2.0.4"
|
||||||
|
|
||||||
|
pouchdb-fetch@7.2.2:
|
||||||
|
version "7.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-fetch/-/pouchdb-fetch-7.2.2.tgz#492791236d60c899d7e9973f9aca0d7b9cc02230"
|
||||||
|
integrity sha512-lUHmaG6U3zjdMkh8Vob9GvEiRGwJfXKE02aZfjiVQgew+9SLkuOxNw3y2q4d1B6mBd273y1k2Lm0IAziRNxQnA==
|
||||||
|
dependencies:
|
||||||
|
abort-controller "3.0.0"
|
||||||
|
fetch-cookie "0.10.1"
|
||||||
|
node-fetch "2.6.0"
|
||||||
|
|
||||||
|
pouchdb-find@^7.2.2:
|
||||||
|
version "7.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-find/-/pouchdb-find-7.2.2.tgz#1227afdd761812d508fe0794b3e904518a721089"
|
||||||
|
integrity sha512-BmFeFVQ0kHmDehvJxNZl9OmIztCjPlZlVSdpijuFbk/Fi1EFPU1BAv3kLC+6DhZuOqU/BCoaUBY9sn66pPY2ag==
|
||||||
|
dependencies:
|
||||||
|
pouchdb-abstract-mapreduce "7.2.2"
|
||||||
|
pouchdb-collate "7.2.2"
|
||||||
|
pouchdb-errors "7.2.2"
|
||||||
|
pouchdb-fetch "7.2.2"
|
||||||
|
pouchdb-md5 "7.2.2"
|
||||||
|
pouchdb-selector-core "7.2.2"
|
||||||
|
pouchdb-utils "7.2.2"
|
||||||
|
|
||||||
pouchdb-json@7.2.2:
|
pouchdb-json@7.2.2:
|
||||||
version "7.2.2"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-json/-/pouchdb-json-7.2.2.tgz#b939be24b91a7322e9a24b8880a6e21514ec5e1f"
|
resolved "https://registry.yarnpkg.com/pouchdb-json/-/pouchdb-json-7.2.2.tgz#b939be24b91a7322e9a24b8880a6e21514ec5e1f"
|
||||||
|
@ -6576,6 +6624,16 @@ pouchdb-json@7.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
vuvuzela "1.0.3"
|
vuvuzela "1.0.3"
|
||||||
|
|
||||||
|
pouchdb-mapreduce-utils@7.2.2:
|
||||||
|
version "7.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-mapreduce-utils/-/pouchdb-mapreduce-utils-7.2.2.tgz#13a46a3cc2a3f3b8e24861da26966904f2963146"
|
||||||
|
integrity sha512-rAllb73hIkU8rU2LJNbzlcj91KuulpwQu804/F6xF3fhZKC/4JQMClahk+N/+VATkpmLxp1zWmvmgdlwVU4HtQ==
|
||||||
|
dependencies:
|
||||||
|
argsarray "0.0.1"
|
||||||
|
inherits "2.0.4"
|
||||||
|
pouchdb-collections "7.2.2"
|
||||||
|
pouchdb-utils "7.2.2"
|
||||||
|
|
||||||
pouchdb-md5@7.2.2:
|
pouchdb-md5@7.2.2:
|
||||||
version "7.2.2"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-md5/-/pouchdb-md5-7.2.2.tgz#415401acc5a844112d765bd1fb4e5d9f38fb0838"
|
resolved "https://registry.yarnpkg.com/pouchdb-md5/-/pouchdb-md5-7.2.2.tgz#415401acc5a844112d765bd1fb4e5d9f38fb0838"
|
||||||
|
@ -6616,6 +6674,14 @@ pouchdb-replication-stream@1.2.9:
|
||||||
pouchdb-promise "^6.0.4"
|
pouchdb-promise "^6.0.4"
|
||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
|
|
||||||
|
pouchdb-selector-core@7.2.2:
|
||||||
|
version "7.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-7.2.2.tgz#264d7436a8c8ac3801f39960e79875ef7f3879a0"
|
||||||
|
integrity sha512-XYKCNv9oiNmSXV5+CgR9pkEkTFqxQGWplnVhO3W9P154H08lU0ZoNH02+uf+NjZ2kjse7Q1fxV4r401LEcGMMg==
|
||||||
|
dependencies:
|
||||||
|
pouchdb-collate "7.2.2"
|
||||||
|
pouchdb-utils "7.2.2"
|
||||||
|
|
||||||
pouchdb-utils@7.2.2:
|
pouchdb-utils@7.2.2:
|
||||||
version "7.2.2"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.2.tgz#c17c4788f1d052b0daf4ef8797bbc4aaa3945aa4"
|
resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.2.tgz#c17c4788f1d052b0daf4ef8797bbc4aaa3945aa4"
|
||||||
|
@ -6724,7 +6790,7 @@ pseudomap@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||||
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
||||||
|
|
||||||
psl@^1.1.28:
|
psl@^1.1.28, psl@^1.1.33:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
||||||
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
|
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
|
||||||
|
@ -8090,6 +8156,15 @@ tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.4.3, tough-cookie@~2.5
|
||||||
psl "^1.1.28"
|
psl "^1.1.28"
|
||||||
punycode "^2.1.1"
|
punycode "^2.1.1"
|
||||||
|
|
||||||
|
"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
|
||||||
|
integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==
|
||||||
|
dependencies:
|
||||||
|
psl "^1.1.33"
|
||||||
|
punycode "^2.1.1"
|
||||||
|
universalify "^0.1.2"
|
||||||
|
|
||||||
tr46@^1.0.1:
|
tr46@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
|
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
|
||||||
|
@ -8217,7 +8292,7 @@ unique-string@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
crypto-random-string "^2.0.0"
|
crypto-random-string "^2.0.0"
|
||||||
|
|
||||||
universalify@^0.1.0:
|
universalify@^0.1.0, universalify@^0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
Mozilla Public License Version 2.0
|
Mozilla Public License Version 2.0
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
|
Copyright 2019-2021, Budibase Ltd
|
||||||
|
|
||||||
1. Definitions
|
1. Definitions
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,45 @@
|
||||||
"type": "datasource",
|
"type": "datasource",
|
||||||
"label": "Data",
|
"label": "Data",
|
||||||
"key": "datasource"
|
"key": "datasource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Empty Text",
|
||||||
|
"key": "noRowsMessage",
|
||||||
|
"defaultValue": "No rows found."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"name": "Search",
|
||||||
|
"description": "A searchable list of items.",
|
||||||
|
"icon": "ri-search-line",
|
||||||
|
"styleable": true,
|
||||||
|
"hasChildren": true,
|
||||||
|
"dataProvider": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"label": "Table",
|
||||||
|
"key": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "multifield",
|
||||||
|
"label": "Columns",
|
||||||
|
"key": "columns",
|
||||||
|
"dependsOn": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Rows/Page",
|
||||||
|
"defaultValue": 25,
|
||||||
|
"key": "pageSize"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Empty Text",
|
||||||
|
"key": "noRowsMessage",
|
||||||
|
"defaultValue": "No rows found."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
|
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
|
||||||
"@budibase/bbui": "^1.58.4",
|
"@budibase/bbui": "^1.58.5",
|
||||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
"@spectrum-css/actionbutton": "^1.0.0-beta.1",
|
"@spectrum-css/actionbutton": "^1.0.0-beta.1",
|
||||||
"@spectrum-css/button": "^3.0.0-beta.6",
|
"@spectrum-css/button": "^3.0.0-beta.6",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable, linkable } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export const className = ""
|
export const className = ""
|
||||||
|
@ -38,8 +38,11 @@
|
||||||
<h2 class="heading">{heading}</h2>
|
<h2 class="heading">{heading}</h2>
|
||||||
<h4 class="text">{description}</h4>
|
<h4 class="text">{description}</h4>
|
||||||
<a
|
<a
|
||||||
|
use:linkable
|
||||||
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
|
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
|
||||||
href={linkUrl}>{linkText}</a>
|
href={linkUrl}>
|
||||||
|
{linkText}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
export let datasource = []
|
export let datasource
|
||||||
|
export let noRowsMessage
|
||||||
|
|
||||||
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
||||||
"sdk"
|
"sdk"
|
||||||
|
@ -29,10 +30,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Provider {actions}>
|
<Provider {actions}>
|
||||||
{#if rows.length > 0}
|
<div use:styleable={$component.styles}>
|
||||||
<div use:styleable={$component.styles}>
|
{#if rows.length > 0}
|
||||||
{#if $component.children === 0 && $builderStore.inBuilder}
|
{#if $component.children === 0 && $builderStore.inBuilder}
|
||||||
<p>Add some components too</p>
|
<p><i class="ri-image-line" />Add some components to display.</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each rows as row}
|
{#each rows as row}
|
||||||
<Provider data={row}>
|
<Provider data={row}>
|
||||||
|
@ -40,20 +41,26 @@
|
||||||
</Provider>
|
</Provider>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{:else if loaded && noRowsMessage}
|
||||||
{:else if loaded && $builderStore.inBuilder}
|
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
|
||||||
<div use:styleable={$component.styles}>
|
{/if}
|
||||||
<p>Feed me some data</p>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
|
margin: 0 var(--spacing-m);
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
color: var(--grey-6);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #f5f5f5;
|
}
|
||||||
border: #ccc 1px solid;
|
p i {
|
||||||
padding: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--grey-5);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -30,8 +30,15 @@
|
||||||
await authStore.actions.logIn({ email, password })
|
await authStore.actions.logIn({ email, password })
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeydown(evt) {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
login()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
<div class="root" use:styleable={$component.styles}>
|
<div class="root" use:styleable={$component.styles}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if logo}
|
{#if logo}
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
Toggle,
|
||||||
|
Input,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
|
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
||||||
|
"sdk"
|
||||||
|
)
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
|
export let table
|
||||||
|
export let columns = []
|
||||||
|
export let pageSize
|
||||||
|
export let noRowsMessage
|
||||||
|
|
||||||
|
let rows = []
|
||||||
|
let loaded = false
|
||||||
|
let search = {}
|
||||||
|
let tableDefinition
|
||||||
|
let schema
|
||||||
|
|
||||||
|
// pagination
|
||||||
|
let page = 0
|
||||||
|
|
||||||
|
$: fetchData(table, page)
|
||||||
|
// omit empty strings
|
||||||
|
$: parsedSearch = Object.keys(search).reduce(
|
||||||
|
(acc, next) =>
|
||||||
|
search[next] === "" ? acc : { ...acc, [next]: search[next] },
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
$: actions = [
|
||||||
|
{
|
||||||
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
callback: () => fetchData(table, page),
|
||||||
|
metadata: { datasource: { type: "table", tableId: table } },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function fetchData(table, page) {
|
||||||
|
if (table) {
|
||||||
|
const tableDef = await API.fetchTableDefinition(table)
|
||||||
|
schema = tableDef.schema
|
||||||
|
rows = await API.searchTableData({
|
||||||
|
tableId: table,
|
||||||
|
search: parsedSearch,
|
||||||
|
pagination: {
|
||||||
|
pageSize,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousPage() {
|
||||||
|
page -= 1
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Provider {actions}>
|
||||||
|
<div use:styleable={$component.styles}>
|
||||||
|
<div class="query-builder">
|
||||||
|
{#if schema}
|
||||||
|
{#each columns as field}
|
||||||
|
<div class="form-field">
|
||||||
|
<Label extraSmall grey>{schema[field].name}</Label>
|
||||||
|
{#if schema[field].type === 'options'}
|
||||||
|
<Select secondary bind:value={search[field]}>
|
||||||
|
<option value="">Choose an option</option>
|
||||||
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
|
<option>{opt}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
{:else if schema[field].type === 'datetime'}
|
||||||
|
<DatePicker bind:value={search[field]} />
|
||||||
|
{:else if schema[field].type === 'boolean'}
|
||||||
|
<Toggle text={schema[field].name} bind:checked={search[field]} />
|
||||||
|
{:else if schema[field].type === 'number'}
|
||||||
|
<Input type="number" bind:value={search[field]} />
|
||||||
|
{:else if schema[field].type === 'string'}
|
||||||
|
<Input bind:value={search[field]} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
<div class="actions">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
search = {}
|
||||||
|
page = 0
|
||||||
|
}}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
on:click={() => {
|
||||||
|
page = 0
|
||||||
|
fetchData(table, page)
|
||||||
|
}}>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if loaded}
|
||||||
|
{#if rows.length > 0}
|
||||||
|
{#if $component.children === 0 && $builderStore.inBuilder}
|
||||||
|
<p><i class="ri-image-line" />Add some components to display.</p>
|
||||||
|
{:else}
|
||||||
|
{#each rows as row}
|
||||||
|
<Provider data={row}>
|
||||||
|
<slot />
|
||||||
|
</Provider>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{:else if noRowsMessage}
|
||||||
|
<p><i class="ri-search-2-line" />{noRowsMessage}</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<div class="pagination">
|
||||||
|
{#if page > 0}
|
||||||
|
<Button primary on:click={previousPage}>Back</Button>
|
||||||
|
{/if}
|
||||||
|
{#if rows.length === pageSize}
|
||||||
|
<Button primary on:click={nextPage}>Next</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Provider>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
p {
|
||||||
|
margin: 0 var(--spacing-m);
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
color: var(--grey-6);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
p i {
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--grey-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-builder {
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
|
justify-content: flex-end;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -49,3 +49,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Checkbox {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import "flatpickr/dist/flatpickr.css"
|
import "flatpickr/dist/flatpickr.css"
|
||||||
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||||
|
import { generateID } from "../helpers"
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
export let label
|
export let label
|
||||||
|
@ -14,8 +15,9 @@
|
||||||
let open = false
|
let open = false
|
||||||
let flatpickr
|
let flatpickr
|
||||||
|
|
||||||
|
$: flatpickrId = `${$fieldState?.id}-${generateID()}-wrapper`
|
||||||
$: flatpickrOptions = {
|
$: flatpickrOptions = {
|
||||||
element: `#${$fieldState?.id}-wrapper`,
|
element: `#${flatpickrId}`,
|
||||||
enableTime: enableTime || false,
|
enableTime: enableTime || false,
|
||||||
altInput: true,
|
altInput: true,
|
||||||
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
|
@ -46,9 +48,7 @@
|
||||||
// duplicate input field.
|
// duplicate input field.
|
||||||
// We need to blur both because the focus styling does not get properly
|
// We need to blur both because the focus styling does not get properly
|
||||||
// applied.
|
// applied.
|
||||||
const els = document.querySelectorAll(
|
const els = document.querySelectorAll(`#${flatpickrId} input`)
|
||||||
`#${$fieldState.fieldId}-wrapper input`
|
|
||||||
)
|
|
||||||
els.forEach(el => el.blur())
|
els.forEach(el => el.blur())
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -62,9 +62,9 @@
|
||||||
on:close={onClose}
|
on:close={onClose}
|
||||||
options={flatpickrOptions}
|
options={flatpickrOptions}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
element={`#${$fieldState.fieldId}-wrapper`}>
|
element={`#${flatpickrId}`}>
|
||||||
<div
|
<div
|
||||||
id={`${$fieldState.fieldId}-wrapper`}
|
id={flatpickrId}
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-invalid={!$fieldState.valid}
|
aria-invalid={!$fieldState.valid}
|
||||||
class:is-invalid={!$fieldState.valid}
|
class:is-invalid={!$fieldState.valid}
|
||||||
|
@ -124,14 +124,11 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.flatpickr {
|
.flatpickr {
|
||||||
width: var(
|
width: 100%;
|
||||||
--spectrum-alias-single-line-width,
|
|
||||||
var(--spectrum-global-dimension-size-2400)
|
|
||||||
);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.flatpickr .spectrum-Textfield {
|
.flatpickr .spectrum-Textfield {
|
||||||
width: auto;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -61,8 +61,13 @@
|
||||||
</FieldGroupFallback>
|
</FieldGroupFallback>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.spectrum-Form-itemField {
|
.spectrum-Form-itemField {
|
||||||
width: 360px;
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
@ -73,4 +78,9 @@
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
margin-top: var(--spectrum-global-dimension-size-75);
|
margin-top: var(--spectrum-global-dimension-size-75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-FieldLabel--right,
|
||||||
|
.spectrum-FieldLabel--left {
|
||||||
|
padding-right: var(--spectrum-global-dimension-size-200);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,10 +8,20 @@
|
||||||
setContext("fieldGroup", { labelPosition })
|
setContext("fieldGroup", { labelPosition })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
<div class="wrapper" use:styleable={$component.styles}>
|
||||||
<div
|
<div
|
||||||
class="spectrum-Form"
|
class="spectrum-Form"
|
||||||
class:spectrum-Form--labelsAbove={labelPosition === 'above'}>
|
class:spectrum-Form--labelsAbove={labelPosition === 'above'}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.spectrum-Form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -168,5 +168,6 @@
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -65,4 +65,7 @@
|
||||||
div :global(.ql-snow .ql-formats:after) {
|
div :global(.ql-snow .ql-formats:after) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
div :global(.ql-editor p) {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -100,7 +100,10 @@
|
||||||
}
|
}
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
width: var(--spectrum-global-dimension-size-2400);
|
width: 100%;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
.spectrum-Picker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -58,3 +58,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -30,5 +30,6 @@ export { default as embed } from "./Embed.svelte"
|
||||||
export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
||||||
export { default as cardstat } from "./CardStat.svelte"
|
export { default as cardstat } from "./CardStat.svelte"
|
||||||
export { default as icon } from "./Icon.svelte"
|
export { default as icon } from "./Icon.svelte"
|
||||||
|
export { default as search } from "./Search.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
|
|
|
@ -44,10 +44,10 @@
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/bbui@^1.58.4":
|
"@budibase/bbui@^1.58.5":
|
||||||
version "1.58.4"
|
version "1.58.5"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.4.tgz#a74d66b3dd715b0a9861a0f86bc0b863fd8f1d44"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.5.tgz#c9ce712941760825c7774a1de77594e989db4561"
|
||||||
integrity sha512-1oEVt7zMREM594CAUIXqOtiuP4Sx4FbfgPBHTZ+t4RhFfbFqvU7yyakqPZM2LhTAmO5Rfa+c+dfFLh+y1++KaA==
|
integrity sha512-0j1I7BetJ2GzB1BXKyvvlkuFphLmADJh2U/Ihubwxx5qUDY8REoVzLgAB4c24zt0CGVTF9VMmOoMLd0zD0QwdQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it "^12.0.2"
|
markdown-it "^12.0.2"
|
||||||
quill "^1.3.7"
|
quill "^1.3.7"
|
||||||
|
|
Loading…
Reference in New Issue