Merge branch 'master' of github.com:budibase/budibase into budi-7754-make-our-helm-chart-work-out-of-the-box
This commit is contained in:
commit
2fb9748979
|
@ -1,24 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo ${TARGETBUILD} > /buildtarget.txt
|
|
||||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
|
||||||
# Azure AppService uses /home for persistent data & SSH on port 2222
|
|
||||||
DATA_DIR="${DATA_DIR:-/home}"
|
|
||||||
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
|
|
||||||
mkdir -p $DATA_DIR/{search,minio,couch}
|
|
||||||
mkdir -p $DATA_DIR/couch/{dbs,views}
|
|
||||||
chown -R couchdb:couchdb $DATA_DIR/couch/
|
|
||||||
apt update
|
|
||||||
apt-get install -y openssh-server
|
|
||||||
echo "root:Docker!" | chpasswd
|
|
||||||
mkdir -p /tmp
|
|
||||||
chmod +x /tmp/ssh_setup.sh \
|
|
||||||
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
|
|
||||||
cp /etc/sshd_config /etc/ssh/sshd_config
|
|
||||||
/etc/init.d/ssh restart
|
|
||||||
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
|
|
||||||
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
|
|
||||||
else
|
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
|
||||||
fi
|
|
|
@ -25,7 +25,7 @@ if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) -
|
||||||
healthy=false
|
healthy=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then
|
if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; then
|
||||||
echo 'ERROR: CouchDB is not running';
|
echo 'ERROR: CouchDB is not running';
|
||||||
healthy=false
|
healthy=false
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -2,7 +2,7 @@ import env from "../environment"
|
||||||
import * as eventHelpers from "./events"
|
import * as eventHelpers from "./events"
|
||||||
import * as accountSdk from "../accounts"
|
import * as accountSdk from "../accounts"
|
||||||
import * as cache from "../cache"
|
import * as cache from "../cache"
|
||||||
import { getGlobalDB, getIdentity, getTenantId } from "../context"
|
import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
|
||||||
import * as dbUtils from "../db"
|
import * as dbUtils from "../db"
|
||||||
import { EmailUnavailableError, HTTPError } from "../errors"
|
import { EmailUnavailableError, HTTPError } from "../errors"
|
||||||
import * as platform from "../platform"
|
import * as platform from "../platform"
|
||||||
|
@ -10,12 +10,10 @@ import * as sessions from "../security/sessions"
|
||||||
import * as usersCore from "./users"
|
import * as usersCore from "./users"
|
||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
AllDocsResponse,
|
|
||||||
BulkUserCreated,
|
BulkUserCreated,
|
||||||
BulkUserDeleted,
|
BulkUserDeleted,
|
||||||
isSSOAccount,
|
isSSOAccount,
|
||||||
isSSOUser,
|
isSSOUser,
|
||||||
RowResponse,
|
|
||||||
SaveUserOpts,
|
SaveUserOpts,
|
||||||
User,
|
User,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
|
@ -487,6 +485,37 @@ export class UserDB {
|
||||||
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async createAdminUser(
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
tenantId: string,
|
||||||
|
opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean }
|
||||||
|
) {
|
||||||
|
const user: User = {
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
roles: {},
|
||||||
|
builder: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
|
tenantId,
|
||||||
|
}
|
||||||
|
if (opts?.ssoId) {
|
||||||
|
user.ssoId = opts.ssoId
|
||||||
|
}
|
||||||
|
// always bust checklist beforehand, if an error occurs but can proceed, don't get
|
||||||
|
// stuck in a cycle
|
||||||
|
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
||||||
|
return await UserDB.save(user, {
|
||||||
|
hashPassword: opts?.hashPassword,
|
||||||
|
requirePassword: opts?.requirePassword,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
static async getGroups(groupIds: string[]) {
|
static async getGroups(groupIds: string[]) {
|
||||||
return await this.groups.getBulk(groupIds)
|
return await this.groups.getBulk(groupIds)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ function removeUserPassword(users: User | User[]) {
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isSupportedUserSearch = (query: SearchQuery) => {
|
export function isSupportedUserSearch(query: SearchQuery) {
|
||||||
const allowed = [
|
const allowed = [
|
||||||
{ op: SearchQueryOperators.STRING, key: "email" },
|
{ op: SearchQueryOperators.STRING, key: "email" },
|
||||||
{ op: SearchQueryOperators.EQUAL, key: "_id" },
|
{ op: SearchQueryOperators.EQUAL, key: "_id" },
|
||||||
|
@ -68,10 +68,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkGetGlobalUsersById = async (
|
export async function bulkGetGlobalUsersById(
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
opts?: GetOpts
|
opts?: GetOpts
|
||||||
) => {
|
) {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
let users = (
|
let users = (
|
||||||
await db.allDocs({
|
await db.allDocs({
|
||||||
|
@ -85,7 +85,7 @@ export const bulkGetGlobalUsersById = async (
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAllUserIds = async () => {
|
export async function getAllUserIds() {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
const startKey = `${DocumentType.USER}${SEPARATOR}`
|
const startKey = `${DocumentType.USER}${SEPARATOR}`
|
||||||
const response = await db.allDocs({
|
const response = await db.allDocs({
|
||||||
|
@ -95,7 +95,7 @@ export const getAllUserIds = async () => {
|
||||||
return response.rows.map(row => row.id)
|
return response.rows.map(row => row.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
export async function bulkUpdateGlobalUsers(users: User[]) {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
return (await db.bulkDocs(users)) as BulkDocsResponse
|
return (await db.bulkDocs(users)) as BulkDocsResponse
|
||||||
}
|
}
|
||||||
|
@ -113,10 +113,10 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
||||||
* Given an email address this will use a view to search through
|
* Given an email address this will use a view to search through
|
||||||
* all the users to find one with this email address.
|
* all the users to find one with this email address.
|
||||||
*/
|
*/
|
||||||
export const getGlobalUserByEmail = async (
|
export async function getGlobalUserByEmail(
|
||||||
email: String,
|
email: String,
|
||||||
opts?: GetOpts
|
opts?: GetOpts
|
||||||
): Promise<User | undefined> => {
|
): Promise<User | undefined> {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
|
@ -139,11 +139,23 @@ export const getGlobalUserByEmail = async (
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchGlobalUsersByApp = async (
|
export async function doesUserExist(email: string) {
|
||||||
|
try {
|
||||||
|
const user = await getGlobalUserByEmail(email)
|
||||||
|
if (Array.isArray(user) || user != null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchGlobalUsersByApp(
|
||||||
appId: any,
|
appId: any,
|
||||||
opts: DatabaseQueryOpts,
|
opts: DatabaseQueryOpts,
|
||||||
getOpts?: GetOpts
|
getOpts?: GetOpts
|
||||||
) => {
|
) {
|
||||||
if (typeof appId !== "string") {
|
if (typeof appId !== "string") {
|
||||||
throw new Error("Must provide a string based app ID")
|
throw new Error("Must provide a string based app ID")
|
||||||
}
|
}
|
||||||
|
@ -167,10 +179,10 @@ export const searchGlobalUsersByApp = async (
|
||||||
Return any user who potentially has access to the application
|
Return any user who potentially has access to the application
|
||||||
Admins, developers and app users with the explicitly role.
|
Admins, developers and app users with the explicitly role.
|
||||||
*/
|
*/
|
||||||
export const searchGlobalUsersByAppAccess = async (
|
export async function searchGlobalUsersByAppAccess(
|
||||||
appId: any,
|
appId: any,
|
||||||
opts?: { limit?: number }
|
opts?: { limit?: number }
|
||||||
) => {
|
) {
|
||||||
const roleSelector = `roles.${appId}`
|
const roleSelector = `roles.${appId}`
|
||||||
|
|
||||||
let orQuery: any[] = [
|
let orQuery: any[] = [
|
||||||
|
@ -205,7 +217,7 @@ export const searchGlobalUsersByAppAccess = async (
|
||||||
return resp.rows
|
return resp.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
export function getGlobalUserByAppPage(appId: string, user: User) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -215,11 +227,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
/**
|
/**
|
||||||
* Performs a starts with search on the global email view.
|
* Performs a starts with search on the global email view.
|
||||||
*/
|
*/
|
||||||
export const searchGlobalUsersByEmail = async (
|
export async function searchGlobalUsersByEmail(
|
||||||
email: string | unknown,
|
email: string | unknown,
|
||||||
opts: any,
|
opts: any,
|
||||||
getOpts?: GetOpts
|
getOpts?: GetOpts
|
||||||
) => {
|
) {
|
||||||
if (typeof email !== "string") {
|
if (typeof email !== "string") {
|
||||||
throw new Error("Must provide a string to search by")
|
throw new Error("Must provide a string to search by")
|
||||||
}
|
}
|
||||||
|
@ -242,12 +254,12 @@ export const searchGlobalUsersByEmail = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_LIMIT = 8
|
const PAGE_LIMIT = 8
|
||||||
export const paginatedUsers = async ({
|
export async function paginatedUsers({
|
||||||
bookmark,
|
bookmark,
|
||||||
query,
|
query,
|
||||||
appId,
|
appId,
|
||||||
limit,
|
limit,
|
||||||
}: SearchUsersRequest = {}) => {
|
}: SearchUsersRequest = {}) {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
const pageSize = limit ?? PAGE_LIMIT
|
const pageSize = limit ?? PAGE_LIMIT
|
||||||
const pageLimit = pageSize + 1
|
const pageLimit = pageSize + 1
|
||||||
|
|
|
@ -228,7 +228,12 @@ export const getContextProviderComponents = (
|
||||||
/**
|
/**
|
||||||
* Gets all data provider components above a component.
|
* Gets all data provider components above a component.
|
||||||
*/
|
*/
|
||||||
export const getActionProviderComponents = (asset, componentId, actionType) => {
|
export const getActionProviders = (
|
||||||
|
asset,
|
||||||
|
componentId,
|
||||||
|
actionType,
|
||||||
|
options = { includeSelf: false }
|
||||||
|
) => {
|
||||||
if (!asset || !componentId) {
|
if (!asset || !componentId) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
|
||||||
// Get the component tree leading up to this component, ignoring the component
|
// Get the component tree leading up to this component, ignoring the component
|
||||||
// itself
|
// itself
|
||||||
const path = findComponentPath(asset.props, componentId)
|
const path = findComponentPath(asset.props, componentId)
|
||||||
|
if (!options?.includeSelf) {
|
||||||
path.pop()
|
path.pop()
|
||||||
|
}
|
||||||
|
|
||||||
// Filter by only data provider components
|
// Find matching contexts and generate bindings
|
||||||
return path.filter(component => {
|
let providers = []
|
||||||
|
path.forEach(component => {
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
return def?.actions?.includes(actionType)
|
const actions = (def?.actions || []).map(action => {
|
||||||
|
return typeof action === "string" ? { type: action } : action
|
||||||
})
|
})
|
||||||
|
const action = actions.find(x => x.type === actionType)
|
||||||
|
if (action) {
|
||||||
|
let runtimeBinding = component._id
|
||||||
|
if (action.suffix) {
|
||||||
|
runtimeBinding += `-${action.suffix}`
|
||||||
|
}
|
||||||
|
providers.push({
|
||||||
|
readableBinding: component._instanceName,
|
||||||
|
runtimeBinding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return providers
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviders } from "builderStore/dataBinding"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ChangeFormStep"
|
"ChangeFormStep",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
|
@ -46,8 +48,8 @@
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
<Label small>Step</Label>
|
<Label small>Step</Label>
|
||||||
<Select bind:value={parameters.type} options={typeOptions} />
|
<Select bind:value={parameters.type} options={typeOptions} />
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviders } from "builderStore/dataBinding"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ClearForm"
|
"ClearForm",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -17,8 +19,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,27 +2,20 @@
|
||||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import { tables, viewsV2 } from "stores/backend"
|
import { tables, viewsV2 } from "stores/backend"
|
||||||
import {
|
import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
|
||||||
getContextProviderComponents,
|
|
||||||
getSchemaForDatasourcePlus,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import SaveFields from "./SaveFields.svelte"
|
import SaveFields from "./SaveFields.svelte"
|
||||||
|
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: formComponents = getContextProviderComponents(
|
$: providerOptions = getDatasourceLikeProviders({
|
||||||
$currentAsset,
|
asset: $currentAsset,
|
||||||
$store.selectedComponentId,
|
componentId: $store.selectedComponentId,
|
||||||
"form"
|
nested,
|
||||||
)
|
})
|
||||||
$: schemaComponents = getContextProviderComponents(
|
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||||
$currentAsset,
|
|
||||||
$store.selectedComponentId,
|
|
||||||
"schema"
|
|
||||||
)
|
|
||||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
|
||||||
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
|
|
||||||
$: tableOptions = $tables.list.map(table => ({
|
$: tableOptions = $tables.list.map(table => ({
|
||||||
label: table.name,
|
label: table.name,
|
||||||
resourceId: table._id,
|
resourceId: table._id,
|
||||||
|
@ -33,44 +26,8 @@
|
||||||
}))
|
}))
|
||||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||||
|
|
||||||
// Gets a context definition of a certain type from a component definition
|
const getSchemaFields = resourceId => {
|
||||||
const extractComponentContext = (component, contextType) => {
|
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||||
const def = store.actions.components.getDefinition(component?._component)
|
|
||||||
if (!def) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
||||||
return contexts.find(context => context?.type === contextType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets options for valid context keys which provide valid data to submit
|
|
||||||
const getProviderOptions = (formComponents, schemaComponents) => {
|
|
||||||
const formContexts = formComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "form"),
|
|
||||||
}))
|
|
||||||
const schemaContexts = schemaComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "schema"),
|
|
||||||
}))
|
|
||||||
const allContexts = formContexts.concat(schemaContexts)
|
|
||||||
|
|
||||||
return allContexts.map(({ component, context }) => {
|
|
||||||
let runtimeBinding = component._id
|
|
||||||
if (context.suffix) {
|
|
||||||
runtimeBinding += `-${context.suffix}`
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: component._instanceName,
|
|
||||||
value: runtimeBinding,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchemaFields = (asset, tableId) => {
|
|
||||||
const { schema } = getSchemaForDatasourcePlus(tableId)
|
|
||||||
delete schema._id
|
|
||||||
delete schema._rev
|
|
||||||
return Object.values(schema || {})
|
return Object.values(schema || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviders } from "builderStore/dataBinding"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"RefreshDatasource"
|
"RefreshDatasource",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -17,8 +19,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,29 +2,19 @@
|
||||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import { tables, viewsV2 } from "stores/backend"
|
import { tables, viewsV2 } from "stores/backend"
|
||||||
import {
|
import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
|
||||||
getContextProviderComponents,
|
|
||||||
getSchemaForDatasourcePlus,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import SaveFields from "./SaveFields.svelte"
|
import SaveFields from "./SaveFields.svelte"
|
||||||
|
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let nested
|
export let nested
|
||||||
|
|
||||||
$: formComponents = getContextProviderComponents(
|
$: providerOptions = getDatasourceLikeProviders({
|
||||||
$currentAsset,
|
asset: $currentAsset,
|
||||||
$store.selectedComponentId,
|
componentId: $store.selectedComponentId,
|
||||||
"form",
|
nested,
|
||||||
{ includeSelf: nested }
|
})
|
||||||
)
|
|
||||||
$: schemaComponents = getContextProviderComponents(
|
|
||||||
$currentAsset,
|
|
||||||
$store.selectedComponentId,
|
|
||||||
"schema",
|
|
||||||
{ includeSelf: nested }
|
|
||||||
)
|
|
||||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
|
||||||
$: schemaFields = getSchemaFields(parameters?.tableId)
|
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||||
$: tableOptions = $tables.list.map(table => ({
|
$: tableOptions = $tables.list.map(table => ({
|
||||||
label: table.name,
|
label: table.name,
|
||||||
|
@ -36,40 +26,6 @@
|
||||||
}))
|
}))
|
||||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||||
|
|
||||||
// Gets a context definition of a certain type from a component definition
|
|
||||||
const extractComponentContext = (component, contextType) => {
|
|
||||||
const def = store.actions.components.getDefinition(component?._component)
|
|
||||||
if (!def) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
|
||||||
return contexts.find(context => context?.type === contextType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets options for valid context keys which provide valid data to submit
|
|
||||||
const getProviderOptions = (formComponents, schemaComponents) => {
|
|
||||||
const formContexts = formComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "form"),
|
|
||||||
}))
|
|
||||||
const schemaContexts = schemaComponents.map(component => ({
|
|
||||||
component,
|
|
||||||
context: extractComponentContext(component, "schema"),
|
|
||||||
}))
|
|
||||||
const allContexts = formContexts.concat(schemaContexts)
|
|
||||||
|
|
||||||
return allContexts.map(({ component, context }) => {
|
|
||||||
let runtimeBinding = component._id
|
|
||||||
if (context.suffix) {
|
|
||||||
runtimeBinding += `-${context.suffix}`
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: component._instanceName,
|
|
||||||
value: runtimeBinding,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchemaFields = resourceId => {
|
const getSchemaFields = resourceId => {
|
||||||
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||||
return Object.values(schema || {})
|
return Object.values(schema || {})
|
||||||
|
|
|
@ -1,22 +1,36 @@
|
||||||
<script>
|
<script>
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { Label, Combobox, Select } from "@budibase/bbui"
|
import { Label, Combobox, Select } from "@budibase/bbui"
|
||||||
import {
|
import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
|
||||||
getActionProviderComponents,
|
|
||||||
buildFormSchema,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { findComponent } from "builderStore/componentUtils"
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
$: formComponent = getFormComponent(
|
||||||
|
$currentAsset.props,
|
||||||
|
parameters.componentId
|
||||||
|
)
|
||||||
$: formSchema = buildFormSchema(formComponent)
|
$: formSchema = buildFormSchema(formComponent)
|
||||||
$: fieldOptions = Object.keys(formSchema || {})
|
$: fieldOptions = Object.keys(formSchema || {})
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ScrollTo"
|
"ScrollTo",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getFormComponent = (asset, id) => {
|
||||||
|
let component = findComponent(asset, id)
|
||||||
|
if (component) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
// Check for block component IDs, and use the block itself instead
|
||||||
|
if (id?.includes("-")) {
|
||||||
|
return findComponent(asset, id.split("-")[0])
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
|
@ -24,8 +38,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
<Label small>Field</Label>
|
<Label small>Field</Label>
|
||||||
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
||||||
|
|
|
@ -3,14 +3,12 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import {
|
import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
|
||||||
getActionProviderComponents,
|
|
||||||
buildFormSchema,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { findComponent } from "builderStore/componentUtils"
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let nested
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{
|
{
|
||||||
|
@ -23,15 +21,31 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
$: formComponent = getFormComponent(
|
||||||
|
$currentAsset.props,
|
||||||
|
parameters.componentId
|
||||||
|
)
|
||||||
$: formSchema = buildFormSchema(formComponent)
|
$: formSchema = buildFormSchema(formComponent)
|
||||||
$: fieldOptions = Object.keys(formSchema || {})
|
$: fieldOptions = Object.keys(formSchema || {})
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ValidateForm"
|
"ValidateForm",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getFormComponent = (asset, id) => {
|
||||||
|
let component = findComponent(asset, id)
|
||||||
|
if (component) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
// Check for block component IDs, and use the block itself instead
|
||||||
|
if (id?.includes("-")) {
|
||||||
|
return findComponent(asset, id.split("-")[0])
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!parameters.type) {
|
if (!parameters.type) {
|
||||||
parameters.type = "set"
|
parameters.type = "set"
|
||||||
|
@ -44,8 +58,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
<Label small>Type</Label>
|
<Label small>Type</Label>
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label } from "@budibase/bbui"
|
||||||
import { currentAsset, store } from "builderStore"
|
import { currentAsset, store } from "builderStore"
|
||||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
import { getActionProviders } from "builderStore/dataBinding"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let nested
|
||||||
|
|
||||||
$: actionProviders = getActionProviderComponents(
|
$: actionProviders = getActionProviders(
|
||||||
$currentAsset,
|
$currentAsset,
|
||||||
$store.selectedComponentId,
|
$store.selectedComponentId,
|
||||||
"ValidateForm"
|
"ValidateForm",
|
||||||
|
{ includeSelf: nested }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -17,8 +19,8 @@
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.componentId}
|
bind:value={parameters.componentId}
|
||||||
options={actionProviders}
|
options={actionProviders}
|
||||||
getOptionLabel={x => x._instanceName}
|
getOptionLabel={x => x.readableBinding}
|
||||||
getOptionValue={x => x._id}
|
getOptionValue={x => x.runtimeBinding}
|
||||||
/>
|
/>
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { getContextProviderComponents } from "builderStore/dataBinding"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
|
// Generates bindings for all components that provider "datasource like"
|
||||||
|
// contexts. This includes "form" contexts and "schema" contexts. This is used
|
||||||
|
// by various button actions as candidates for whole "row" objects.
|
||||||
|
// Some examples are saving rows or duplicating rows.
|
||||||
|
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
|
||||||
|
// Get all form context providers
|
||||||
|
const formComponents = getContextProviderComponents(
|
||||||
|
asset,
|
||||||
|
componentId,
|
||||||
|
"form",
|
||||||
|
{ includeSelf: nested }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get all schema context providers
|
||||||
|
const schemaComponents = getContextProviderComponents(
|
||||||
|
asset,
|
||||||
|
componentId,
|
||||||
|
"schema",
|
||||||
|
{ includeSelf: nested }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate contexts for all form providers
|
||||||
|
const formContexts = formComponents.map(component => ({
|
||||||
|
component,
|
||||||
|
context: extractComponentContext(component, "form"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Generate contexts for all schema providers
|
||||||
|
const schemaContexts = schemaComponents.map(component => ({
|
||||||
|
component,
|
||||||
|
context: extractComponentContext(component, "schema"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Check for duplicate contexts by the same component. In this case, attempt
|
||||||
|
// to label contexts with their suffixes
|
||||||
|
schemaContexts.forEach(schemaContext => {
|
||||||
|
// Check if we have a form context for this component
|
||||||
|
const id = schemaContext.component._id
|
||||||
|
const existing = formContexts.find(x => x.component._id === id)
|
||||||
|
if (existing) {
|
||||||
|
if (existing.context.suffix) {
|
||||||
|
const suffix = capitalise(existing.context.suffix)
|
||||||
|
existing.readableSuffix = ` - ${suffix}`
|
||||||
|
}
|
||||||
|
if (schemaContext.context.suffix) {
|
||||||
|
const suffix = capitalise(schemaContext.context.suffix)
|
||||||
|
schemaContext.readableSuffix = ` - ${suffix}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate bindings for all contexts
|
||||||
|
const allContexts = formContexts.concat(schemaContexts)
|
||||||
|
return allContexts.map(({ component, context, readableSuffix }) => {
|
||||||
|
let readableBinding = component._instanceName
|
||||||
|
let runtimeBinding = component._id
|
||||||
|
if (context.suffix) {
|
||||||
|
runtimeBinding += `-${context.suffix}`
|
||||||
|
}
|
||||||
|
if (readableSuffix) {
|
||||||
|
readableBinding += readableSuffix
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: readableBinding,
|
||||||
|
value: runtimeBinding,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a context definition of a certain type from a component definition
|
||||||
|
const extractComponentContext = (component, contextType) => {
|
||||||
|
const def = store.actions.components.getDefinition(component?._component)
|
||||||
|
if (!def) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
|
return contexts.find(context => context?.type === contextType)
|
||||||
|
}
|
|
@ -6165,6 +6165,24 @@
|
||||||
"defaultValue": "spectrum--medium"
|
"defaultValue": "spectrum--medium"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "ValidateForm",
|
||||||
|
"suffix": "form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ClearForm",
|
||||||
|
"suffix": "form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UpdateFieldValue",
|
||||||
|
"suffix": "form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ScrollTo",
|
||||||
|
"suffix": "form"
|
||||||
|
}
|
||||||
|
],
|
||||||
"context": [
|
"context": [
|
||||||
{
|
{
|
||||||
"type": "form",
|
"type": "form",
|
||||||
|
@ -6420,7 +6438,8 @@
|
||||||
],
|
],
|
||||||
"context": {
|
"context": {
|
||||||
"type": "schema"
|
"type": "schema"
|
||||||
}
|
},
|
||||||
|
"actions": ["RefreshDatasource"]
|
||||||
},
|
},
|
||||||
"bbreferencefield": {
|
"bbreferencefield": {
|
||||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
import Block from "../Block.svelte"
|
import Block from "../Block.svelte"
|
||||||
|
|
||||||
export let buttons = []
|
export let buttons = []
|
||||||
export let direction
|
export let direction = "row"
|
||||||
export let hAlign
|
export let hAlign = "left"
|
||||||
export let vAlign
|
export let vAlign = "top"
|
||||||
export let gap = "S"
|
export let gap = "S"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,12 @@
|
||||||
builderStore,
|
builderStore,
|
||||||
notificationStore,
|
notificationStore,
|
||||||
enrichButtonActions,
|
enrichButtonActions,
|
||||||
|
ActionTypes,
|
||||||
|
createContextStore,
|
||||||
} = getContext("sdk")
|
} = getContext("sdk")
|
||||||
|
|
||||||
|
let grid
|
||||||
|
|
||||||
$: columnWhitelist = columns?.map(col => col.name)
|
$: columnWhitelist = columns?.map(col => col.name)
|
||||||
$: schemaOverrides = getSchemaOverrides(columns)
|
$: schemaOverrides = getSchemaOverrides(columns)
|
||||||
$: enrichedButtons = enrichButtons(buttons)
|
$: enrichedButtons = enrichButtons(buttons)
|
||||||
|
@ -53,11 +57,16 @@
|
||||||
text: settings.text,
|
text: settings.text,
|
||||||
type: settings.type,
|
type: settings.type,
|
||||||
onClick: async row => {
|
onClick: async row => {
|
||||||
// We add a fake context binding in here, which allows us to pretend
|
// Create a fake, ephemeral context to run the buttons actions with
|
||||||
// that the grid provides a "schema" binding - that lets us use the
|
const id = get(component).id
|
||||||
// clicked row in things like save row actions
|
const gridContext = createContextStore(context)
|
||||||
const enrichedContext = { ...get(context), [get(component).id]: row }
|
gridContext.actions.provideData(id, row)
|
||||||
const fn = enrichButtonActions(settings.onClick, enrichedContext)
|
gridContext.actions.provideAction(
|
||||||
|
id,
|
||||||
|
ActionTypes.RefreshDatasource,
|
||||||
|
() => grid?.getContext()?.rows.actions.refreshData()
|
||||||
|
)
|
||||||
|
const fn = enrichButtonActions(settings.onClick, get(gridContext))
|
||||||
return await fn?.({ row })
|
return await fn?.({ row })
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -69,6 +78,7 @@
|
||||||
class:in-builder={$builderStore.inBuilder}
|
class:in-builder={$builderStore.inBuilder}
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
|
bind:this={grid}
|
||||||
datasource={table}
|
datasource={table}
|
||||||
{API}
|
{API}
|
||||||
{stripeRows}
|
{stripeRows}
|
||||||
|
|
|
@ -45,13 +45,14 @@
|
||||||
// Register any "refresh datasource" actions with a singleton store
|
// Register any "refresh datasource" actions with a singleton store
|
||||||
// so we can easily refresh data at all levels for any datasource
|
// so we can easily refresh data at all levels for any datasource
|
||||||
if (type === ActionTypes.RefreshDatasource) {
|
if (type === ActionTypes.RefreshDatasource) {
|
||||||
const { dataSource } = metadata || {}
|
if (metadata?.dataSource) {
|
||||||
dataSourceStore.actions.registerDataSource(
|
dataSourceStore.actions.registerDataSource(
|
||||||
dataSource,
|
metadata.dataSource,
|
||||||
instanceId,
|
instanceId,
|
||||||
callback
|
callback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
confirmationStore,
|
confirmationStore,
|
||||||
roleStore,
|
roleStore,
|
||||||
stateStore,
|
stateStore,
|
||||||
|
createContextStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { styleable } from "utils/styleable"
|
import { styleable } from "utils/styleable"
|
||||||
import { linkable } from "utils/linkable"
|
import { linkable } from "utils/linkable"
|
||||||
|
@ -57,6 +58,7 @@ export default {
|
||||||
enrichButtonActions,
|
enrichButtonActions,
|
||||||
processStringSync,
|
processStringSync,
|
||||||
makePropSafe,
|
makePropSafe,
|
||||||
|
createContextStore,
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
Provider,
|
Provider,
|
||||||
|
|
|
@ -33,6 +33,7 @@ const environment = {
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
||||||
HTTP_MIGRATIONS: process.env.HTTP_MIGRATIONS,
|
HTTP_MIGRATIONS: process.env.HTTP_MIGRATIONS,
|
||||||
|
CLUSTER_MODE: process.env.CLUSTER_MODE,
|
||||||
API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC,
|
API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import env from "./environment"
|
import env from "./environment"
|
||||||
import * as redis from "./utilities/redis"
|
import * as redis from "./utilities/redis"
|
||||||
|
import { generateApiKey, getChecklist } from "./utilities/workerRequests"
|
||||||
import {
|
import {
|
||||||
createAdminUser,
|
events,
|
||||||
generateApiKey,
|
installation,
|
||||||
getChecklist,
|
logging,
|
||||||
} from "./utilities/workerRequests"
|
tenancy,
|
||||||
import { events, installation, logging, tenancy } from "@budibase/backend-core"
|
users,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { watch } from "./watch"
|
import { watch } from "./watch"
|
||||||
import * as automations from "./automations"
|
import * as automations from "./automations"
|
||||||
|
@ -58,7 +60,7 @@ export async function startup(app?: any, server?: any) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
STARTUP_RAN = true
|
STARTUP_RAN = true
|
||||||
if (server) {
|
if (server && !env.CLUSTER_MODE) {
|
||||||
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
||||||
env._set("PORT", server.address().port)
|
env._set("PORT", server.address().port)
|
||||||
}
|
}
|
||||||
|
@ -110,34 +112,37 @@ export async function startup(app?: any, server?: any) {
|
||||||
// check and create admin user if required
|
// check and create admin user if required
|
||||||
// this must be run after the api has been initialised due to
|
// this must be run after the api has been initialised due to
|
||||||
// the app user sync
|
// the app user sync
|
||||||
|
const bbAdminEmail = env.BB_ADMIN_USER_EMAIL,
|
||||||
|
bbAdminPassword = env.BB_ADMIN_USER_PASSWORD
|
||||||
if (
|
if (
|
||||||
env.SELF_HOSTED &&
|
env.SELF_HOSTED &&
|
||||||
!env.MULTI_TENANCY &&
|
!env.MULTI_TENANCY &&
|
||||||
env.BB_ADMIN_USER_EMAIL &&
|
bbAdminEmail &&
|
||||||
env.BB_ADMIN_USER_PASSWORD
|
bbAdminPassword
|
||||||
) {
|
) {
|
||||||
const checklist = await getChecklist()
|
|
||||||
if (!checklist?.adminUser?.checked) {
|
|
||||||
try {
|
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
const user = await createAdminUser(
|
await tenancy.doInTenant(tenantId, async () => {
|
||||||
env.BB_ADMIN_USER_EMAIL,
|
const exists = await users.doesUserExist(bbAdminEmail)
|
||||||
env.BB_ADMIN_USER_PASSWORD,
|
const checklist = await getChecklist()
|
||||||
tenantId
|
if (!checklist?.adminUser?.checked || !exists) {
|
||||||
|
try {
|
||||||
|
const user = await users.UserDB.createAdminUser(
|
||||||
|
bbAdminEmail,
|
||||||
|
bbAdminPassword,
|
||||||
|
tenantId,
|
||||||
|
{ hashPassword: true, requirePassword: true }
|
||||||
)
|
)
|
||||||
// Need to set up an API key for automated integration tests
|
// Need to set up an API key for automated integration tests
|
||||||
if (env.isTest()) {
|
if (env.isTest()) {
|
||||||
await generateApiKey(user._id)
|
await generateApiKey(user._id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log("Admin account automatically created for", bbAdminEmail)
|
||||||
"Admin account automatically created for",
|
|
||||||
env.BB_ADMIN_USER_EMAIL
|
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logging.logAlert("Error creating initial admin user. Exiting.", e)
|
logging.logAlert("Error creating initial admin user. Exiting.", e)
|
||||||
shutdown(server)
|
shutdown(server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,19 +155,9 @@ export async function readGlobalUser(ctx: Ctx): Promise<User> {
|
||||||
return checkResponse(response, "get user", { ctx })
|
return checkResponse(response, "get user", { ctx })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAdminUser(
|
export async function getChecklist(): Promise<{
|
||||||
email: string,
|
adminUser: { checked: boolean }
|
||||||
password: string,
|
}> {
|
||||||
tenantId: string
|
|
||||||
) {
|
|
||||||
const response = await fetch(
|
|
||||||
checkSlashesInUrl(env.WORKER_URL + "/api/global/users/init"),
|
|
||||||
request(undefined, { method: "POST", body: { email, password, tenantId } })
|
|
||||||
)
|
|
||||||
return checkResponse(response, "create admin user")
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getChecklist() {
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"),
|
checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"),
|
||||||
request(undefined, { method: "GET" })
|
request(undefined, { method: "GET" })
|
||||||
|
|
|
@ -120,28 +120,17 @@ export const adminUser = async (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const user: User = {
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
roles: {},
|
|
||||||
builder: {
|
|
||||||
global: true,
|
|
||||||
},
|
|
||||||
admin: {
|
|
||||||
global: true,
|
|
||||||
},
|
|
||||||
tenantId,
|
|
||||||
ssoId,
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
// always bust checklist beforehand, if an error occurs but can proceed, don't get
|
const finalUser = await userSdk.db.createAdminUser(
|
||||||
// stuck in a cycle
|
email,
|
||||||
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
password,
|
||||||
const finalUser = await userSdk.db.save(user, {
|
tenantId,
|
||||||
|
{
|
||||||
|
ssoId,
|
||||||
hashPassword,
|
hashPassword,
|
||||||
requirePassword,
|
requirePassword,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// events
|
// events
|
||||||
let account: CloudAccount | undefined
|
let account: CloudAccount | undefined
|
||||||
|
|
Loading…
Reference in New Issue