-
-
{item.label || item.field}
+ >
+
+
+ {item.field}
+
+
+
{readableText}
@@ -53,4 +81,20 @@
.list-item-body {
justify-content: space-between;
}
+ .type-icon {
+ display: flex;
+ gap: var(--spacing-m);
+ margin: var(--spacing-xl);
+ margin-bottom: 0px;
+ height: var(--spectrum-alias-item-height-m);
+ padding: 0px var(--spectrum-alias-item-padding-m);
+ border-width: var(--spectrum-actionbutton-border-size);
+ border-radius: var(--spectrum-alias-border-radius-regular);
+ border: 1px solid
+ var(
+ --spectrum-actionbutton-m-border-color,
+ var(--spectrum-alias-border-color)
+ );
+ align-items: center;
+ }
diff --git a/packages/builder/src/components/integration/RestQueryViewer.svelte b/packages/builder/src/components/integration/RestQueryViewer.svelte
index 254f65fcaf..e6913b0953 100644
--- a/packages/builder/src/components/integration/RestQueryViewer.svelte
+++ b/packages/builder/src/components/integration/RestQueryViewer.svelte
@@ -196,8 +196,36 @@
}
}
+ const validateQuery = async () => {
+ const forbiddenBindings = /{{\s?user(\.(\w|\$)*\s?|\s?)}}/g
+ const bindingError = new Error(
+ "'user' is a protected binding and cannot be used"
+ )
+
+ if (forbiddenBindings.test(url)) {
+ throw bindingError
+ }
+
+ if (forbiddenBindings.test(query.fields.requestBody ?? "")) {
+ throw bindingError
+ }
+
+ Object.values(requestBindings).forEach(bindingValue => {
+ if (forbiddenBindings.test(bindingValue)) {
+ throw bindingError
+ }
+ })
+
+ Object.values(query.fields.headers).forEach(headerValue => {
+ if (forbiddenBindings.test(headerValue)) {
+ throw bindingError
+ }
+ })
+ }
+
async function runQuery() {
try {
+ await validateQuery()
response = await queries.preview(buildQuery())
if (response.rows.length === 0) {
notifications.info("Request did not return any data")
diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
index a7d9584330..f9a40b09a6 100644
--- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
@@ -516,6 +516,13 @@
}
return null
}
+
+ const parseRole = user => {
+ if (user.isAdminOrGlobalBuilder) {
+ return Constants.Roles.CREATOR
+ }
+ return user.role
+ }
@@ -725,7 +732,7 @@
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte
index 17eadb99bd..affa115ca2 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte
@@ -91,7 +91,12 @@
/>
{/if}
{#if section == "styles"}
-
+
{
+ const getSections = (instance, definition, isScreen, tag) => {
const settings = definition?.settings ?? []
- const generalSettings = settings.filter(setting => !setting.section)
- const customSections = settings.filter(setting => setting.section)
+ const generalSettings = settings.filter(
+ setting => !setting.section && setting.tag === tag
+ )
+ const customSections = settings.filter(
+ setting => setting.section && setting.tag === tag
+ )
let sections = [
- {
- name: "General",
- settings: generalSettings,
- },
+ ...(generalSettings?.length
+ ? [
+ {
+ name: "General",
+ settings: generalSettings,
+ },
+ ]
+ : []),
...(customSections || []),
]
@@ -132,7 +146,7 @@
- {:else if idx === 0 && section.name === "General" && componentDefinition.info}
+ {:else if idx === 0 && section.name === "General" && componentDefinition?.info && !tag}
{/if}
{/each}
-{#if componentDefinition?.block}
+{#if componentDefinition?.block && !tag}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte
index 444ded7e1f..def1fcf24b 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte
@@ -1,10 +1,12 @@
+
+
+
{#if styles?.length > 0}
{#each styles as style}
+ import BlockComponent from "../BlockComponent.svelte"
+ import Block from "../Block.svelte"
+
+ export let buttons = []
+ export let direction
+ export let hAlign
+ export let vAlign
+ export let gap = "S"
+
+
+
+
+ {#each buttons as { text, type, quiet, disabled, onClick, size }}
+
+ {/each}
+
+
diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js
index 060c15a857..97df3741e1 100644
--- a/packages/client/src/components/app/index.js
+++ b/packages/client/src/components/app/index.js
@@ -19,6 +19,7 @@ export { default as dataprovider } from "./DataProvider.svelte"
export { default as divider } from "./Divider.svelte"
export { default as screenslot } from "./ScreenSlot.svelte"
export { default as button } from "./Button.svelte"
+export { default as buttongroup } from "./ButtonGroup.svelte"
export { default as repeater } from "./Repeater.svelte"
export { default as text } from "./Text.svelte"
export { default as layout } from "./Layout.svelte"
diff --git a/packages/pro b/packages/pro
index 044bec6447..5ed0ee2aca 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit 044bec6447066b215932d6726c437e7ec5a9e42e
+Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e
diff --git a/packages/server/Dockerfile.v2 b/packages/server/Dockerfile.v2
index 881c21299e..f737570fcd 100644
--- a/packages/server/Dockerfile.v2
+++ b/packages/server/Dockerfile.v2
@@ -44,7 +44,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates
COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh package.json
-RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
+RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
COPY packages/string-templates .
@@ -57,7 +57,7 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh package.json
-RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true \
+RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \
# Remove unneeded data from file system to reduce image size
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
diff --git a/packages/server/src/api/controllers/role.ts b/packages/server/src/api/controllers/role.ts
index ed23009706..3697bbe925 100644
--- a/packages/server/src/api/controllers/role.ts
+++ b/packages/server/src/api/controllers/role.ts
@@ -1,4 +1,10 @@
-import { context, db as dbCore, events, roles } from "@budibase/backend-core"
+import {
+ context,
+ db as dbCore,
+ events,
+ roles,
+ Header,
+} from "@budibase/backend-core"
import { getUserMetadataParams, InternalTables } from "../../db/utils"
import { Database, Role, UserCtx, UserRoles } from "@budibase/types"
import { sdk as sharedSdk } from "@budibase/shared-core"
@@ -143,4 +149,20 @@ export async function accessible(ctx: UserCtx) {
} else {
ctx.body = await roles.getUserRoleIdHierarchy(roleId!)
}
+
+ // If a custom role is provided in the header, filter out higher level roles
+ const roleHeader = ctx.header?.[Header.PREVIEW_ROLE] as string
+ if (roleHeader && !Object.keys(roles.BUILTIN_ROLE_IDS).includes(roleHeader)) {
+ const inherits = (await roles.getRole(roleHeader))?.inherits
+ const orderedRoles = ctx.body.reverse()
+ let filteredRoles = [roleHeader]
+ for (let role of orderedRoles) {
+ filteredRoles = [role, ...filteredRoles]
+ if (role === inherits) {
+ break
+ }
+ }
+ filteredRoles.pop()
+ ctx.body = [roleHeader, ...filteredRoles]
+ }
}
diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts
index c29cb65eac..516bfd20c6 100644
--- a/packages/server/src/api/routes/row.ts
+++ b/packages/server/src/api/routes/row.ts
@@ -11,128 +11,24 @@ const { PermissionType, PermissionLevel } = permissions
const router: Router = new Router()
router
- /**
- * @api {get} /api/:sourceId/:rowId/enrich Get an enriched row
- * @apiName Get an enriched row
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This API is only useful when dealing with rows that have relationships.
- * Normally when a row is a returned from the API relationships will only have the structure
- * `{ primaryDisplay: "name", _id: ... }` but this call will return the full related rows
- * for each relationship instead.
- *
- * @apiParam {string} rowId The ID of the row which is to be retrieved and enriched.
- *
- * @apiSuccess {object} row The response body will be the enriched row.
- */
.get(
"/api/:sourceId/:rowId/enrich",
paramSubResource("sourceId", "rowId"),
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.fetchEnrichedRow
)
- /**
- * @api {get} /api/:sourceId/rows Get all rows in a table
- * @apiName Get all rows in a table
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This is a deprecated endpoint that should not be used anymore, instead use the search endpoint.
- * This endpoint gets all of the rows within the specified table - it is not heavily used
- * due to its lack of support for pagination. With SQL tables this will retrieve up to a limit and then
- * will simply stop.
- *
- * @apiParam {string} sourceId The ID of the table to retrieve all rows within.
- *
- * @apiSuccess {object[]} rows The response body will be an array of all rows found.
- */
.get(
"/api/:sourceId/rows",
paramResource("sourceId"),
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.fetch
)
- /**
- * @api {get} /api/:sourceId/rows/:rowId Retrieve a single row
- * @apiName Retrieve a single row
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This endpoint retrieves only the specified row. If you wish to retrieve
- * a row by anything other than its _id field, use the search endpoint.
- *
- * @apiParam {string} sourceId The ID of the table to retrieve a row from.
- * @apiParam {string} rowId The ID of the row to retrieve.
- *
- * @apiSuccess {object} body The response body will be the row that was found.
- */
.get(
"/api/:sourceId/rows/:rowId",
paramSubResource("sourceId", "rowId"),
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.find
)
- /**
- * @api {post} /api/:sourceId/search Search for rows in a table
- * @apiName Search for rows in a table
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This is the primary method of accessing rows in Budibase, the data provider
- * and data UI in the builder are built atop this. All filtering, sorting and pagination is
- * handled through this, for internal and external (datasource plus, e.g. SQL) tables.
- *
- * @apiParam {string} sourceId The ID of the table to retrieve rows from.
- *
- * @apiParam (Body) {boolean} [paginate] If pagination is required then this should be set to true,
- * defaults to false.
- * @apiParam (Body) {object} [query] This contains a set of filters which should be applied, if none
- * specified then the request will be unfiltered. An example with all of the possible query
- * options has been supplied below.
- * @apiParam (Body) {number} [limit] This sets a limit for the number of rows that will be returned,
- * this will be implemented at the database level if supported for performance reasons. This
- * is useful when paginating to set exactly how many rows per page.
- * @apiParam (Body) {string} [bookmark] If pagination is enabled then a bookmark will be returned
- * with each successful search request, this should be supplied back to get the next page.
- * @apiParam (Body) {object} [sort] If sort is desired this should contain the name of the column to
- * sort on.
- * @apiParam (Body) {string} [sortOrder] If sort is enabled then this can be either "descending" or
- * "ascending" as required.
- * @apiParam (Body) {string} [sortType] If sort is enabled then you must specify the type of search
- * being used, either "string" or "number". This is only used for internal tables.
- *
- * @apiParamExample {json} Example:
- * {
- * "tableId": "ta_70260ff0b85c467ca74364aefc46f26d",
- * "query": {
- * "string": {},
- * "fuzzy": {},
- * "range": {
- * "columnName": {
- * "high": 20,
- * "low": 10,
- * }
- * },
- * "equal": {
- * "columnName": "someValue"
- * },
- * "notEqual": {},
- * "empty": {},
- * "notEmpty": {},
- * "oneOf": {
- * "columnName": ["value"]
- * }
- * },
- * "limit": 10,
- * "sort": "name",
- * "sortOrder": "descending",
- * "sortType": "string",
- * "paginate": true
- * }
- *
- * @apiSuccess {object[]} rows An array of rows that was found based on the supplied parameters.
- * @apiSuccess {boolean} hasNextPage If pagination was enabled then this specifies whether or
- * not there is another page after this request.
- * @apiSuccess {string} bookmark The bookmark to be sent with the next request to get the next
- * page.
- */
.post(
"/api/:sourceId/search",
internalSearchValidator(),
@@ -148,30 +44,6 @@ router
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.search
)
- /**
- * @api {post} /api/:sourceId/rows Creates a new row
- * @apiName Creates a new row
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This API will create a new row based on the supplied body. If the
- * body includes an "_id" field then it will update an existing row if the field
- * links to one. Please note that "_id", "_rev" and "tableId" are fields that are
- * already used by Budibase tables and cannot be used for columns.
- *
- * @apiParam {string} sourceId The ID of the table to save a row to.
- *
- * @apiParam (Body) {string} [_id] If the row exists already then an ID for the row must be provided.
- * @apiParam (Body) {string} [_rev] If working with an existing row for an internal table its revision
- * must also be provided.
- * @apiParam (Body) {string} tableId The ID of the table should also be specified in the row body itself.
- * @apiParam (Body) {any} [any] Any field supplied in the body will be assessed to see if it matches
- * a column in the specified table. All other fields will be dropped and not stored.
- *
- * @apiSuccess {string} _id The ID of the row that was just saved, if it was just created this
- * is the rows new ID.
- * @apiSuccess {string} [_rev] If saving to an internal table a revision will also be returned.
- * @apiSuccess {object} body The contents of the row that was saved will be returned as well.
- */
.post(
"/api/:sourceId/rows",
paramResource("sourceId"),
@@ -179,14 +51,6 @@ router
trimViewRowInfo,
rowController.save
)
- /**
- * @api {patch} /api/:sourceId/rows Updates a row
- * @apiName Update a row
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This endpoint is identical to the row creation endpoint but instead it will
- * error if an _id isn't provided, it will only function for existing rows.
- */
.patch(
"/api/:sourceId/rows",
paramResource("sourceId"),
@@ -194,52 +58,12 @@ router
trimViewRowInfo,
rowController.patch
)
- /**
- * @api {post} /api/:sourceId/rows/validate Validate inputs for a row
- * @apiName Validate inputs for a row
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription When attempting to save a row you may want to check if the row is valid
- * given the table schema, this will iterate through all the constraints on the table and
- * check if the request body is valid.
- *
- * @apiParam {string} sourceId The ID of the table the row is to be validated for.
- *
- * @apiParam (Body) {any} [any] Any fields provided in the request body will be tested
- * against the table schema and constraints.
- *
- * @apiSuccess {boolean} valid If inputs provided are acceptable within the table schema this
- * will be true, if it is not then then errors property will be populated.
- * @apiSuccess {object} [errors] A key value map of information about fields on the input
- * which do not match the table schema. The key name will be the column names that have breached
- * the schema.
- */
.post(
"/api/:sourceId/rows/validate",
paramResource("sourceId"),
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
rowController.validate
)
- /**
- * @api {delete} /api/:sourceId/rows Delete rows
- * @apiName Delete rows
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This endpoint can delete a single row, or delete them in a bulk
- * fashion.
- *
- * @apiParam {string} sourceId The ID of the table the row is to be deleted from.
- *
- * @apiParam (Body) {object[]} [rows] If bulk deletion is desired then provide the rows in this
- * key of the request body that are to be deleted.
- * @apiParam (Body) {string} [_id] If deleting a single row then provide its ID in this field.
- * @apiParam (Body) {string} [_rev] If deleting a single row from an internal table then provide its
- * revision here.
- *
- * @apiSuccess {object[]|object} body If deleting bulk then the response body will be an array
- * of the deleted rows, if deleting a single row then the body will contain a "row" property which
- * is the deleted row.
- */
.delete(
"/api/:sourceId/rows",
paramResource("sourceId"),
@@ -247,20 +71,6 @@ router
trimViewRowInfo,
rowController.destroy
)
-
- /**
- * @api {post} /api/:sourceId/rows/exportRows Export Rows
- * @apiName Export rows
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This API can export a number of provided rows
- *
- * @apiParam {string} sourceId The ID of the table the row is to be deleted from.
- *
- * @apiParam (Body) {object[]} [rows] The row IDs which are to be exported
- *
- * @apiSuccess {object[]|object}
- */
.post(
"/api/:sourceId/rows/exportRows",
paramResource("sourceId"),
diff --git a/packages/server/src/api/routes/table.ts b/packages/server/src/api/routes/table.ts
index 7ffa5acb3e..0172d9844d 100644
--- a/packages/server/src/api/routes/table.ts
+++ b/packages/server/src/api/routes/table.ts
@@ -9,99 +9,13 @@ const { BUILDER, PermissionLevel, PermissionType } = permissions
const router: Router = new Router()
router
- /**
- * @api {get} /api/tables Fetch all tables
- * @apiName Fetch all tables
- * @apiGroup tables
- * @apiPermission table read access
- * @apiDescription This endpoint retrieves all of the tables which have been created in
- * an app. This includes all of the external and internal tables; to tell the difference
- * between these look for the "type" property on each table, either being "internal" or "external".
- *
- * @apiSuccess {object[]} body The response body will be the list of tables that was found - as
- * this does not take any parameters the only error scenario is no access.
- */
.get("/api/tables", authorized(BUILDER), tableController.fetch)
- /**
- * @api {get} /api/tables/:id Fetch a single table
- * @apiName Fetch a single table
- * @apiGroup tables
- * @apiPermission table read access
- * @apiDescription Retrieves a single table this could be be internal or external based on
- * the provided table ID.
- *
- * @apiParam {string} id The ID of the table which is to be retrieved.
- *
- * @apiSuccess {object[]} body The response body will be the table that was found.
- */
.get(
"/api/tables/:tableId",
paramResource("tableId"),
authorized(PermissionType.TABLE, PermissionLevel.READ, { schema: true }),
tableController.find
)
- /**
- * @api {post} /api/tables Save a table
- * @apiName Save a table
- * @apiGroup tables
- * @apiPermission builder
- * @apiDescription Create or update a table with this endpoint, this will function for both internal
- * external tables.
- *
- * @apiParam (Body) {string} [_id] If updating an existing table then the ID of the table must be specified.
- * @apiParam (Body) {string} [_rev] If updating an existing internal table then the revision must also be specified.
- * @apiParam (Body) {string} type] This should either be "internal" or "external" depending on the table type -
- * this will default to internal.
- * @apiParam (Body) {string} [sourceId] If creating an external table then this should be set to the datasource ID. If
- * building an internal table this does not need to be set, although it will be returned as "bb_internal".
- * @apiParam (Body) {string} name The name of the table, this will be used in the UI. To rename the table simply
- * supply the table structure to this endpoint with the name changed.
- * @apiParam (Body) {object} schema A key value object which has all of the columns in the table as the keys in this
- * object. For each column a "type" and "constraints" must be specified, with some types requiring further information.
- * More information about the schema structure can be found in the Typescript definitions.
- * @apiParam (Body) {string} [primaryDisplay] The name of the column which should be used when displaying rows
- * from this table as relationships.
- * @apiParam (Body) {object[]} [indexes] Specifies the search indexes - this is deprecated behaviour with the introduction
- * of lucene indexes. This functionality is only available for internal tables.
- * @apiParam (Body) {object} [_rename] If a column is to be renamed then the "old" column name should be set in this
- * structure, and the "updated", new column name should also be supplied. The schema should also be updated, this field
- * lets the server know that a field hasn't just been deleted, that the data has moved to a new name, this will fix
- * the rows in the table. This functionality is only available for internal tables.
- * @apiParam (Body) {object[]} [rows] When creating a table using a compatible data source, an array of objects to be imported into the new table can be provided.
- *
- * @apiParamExample {json} Example:
- * {
- * "_id": "ta_05541307fa0f4044abee071ca2a82119",
- * "_rev": "10-0fbe4e78f69b255d79f1017e2eeef807",
- * "type": "internal",
- * "views": {},
- * "name": "tableName",
- * "schema": {
- * "column": {
- * "type": "string",
- * "constraints": {
- * "type": "string",
- * "length": {
- * "maximum": null
- * },
- * "presence": false
- * },
- * "name": "column"
- * },
- * },
- * "primaryDisplay": "column",
- * "indexes": [],
- * "sourceId": "bb_internal",
- * "_rename": {
- * "old": "columnName",
- * "updated": "newColumnName",
- * },
- * "rows": []
- * }
- *
- * @apiSuccess {object} table The response body will contain the table structure after being cleaned up and
- * saved to the database.
- */
.post(
"/api/tables",
// allows control over updating a table
@@ -125,41 +39,12 @@ router
authorized(BUILDER),
tableController.validateExistingTableImport
)
- /**
- * @api {post} /api/tables/:tableId/:revId Delete a table
- * @apiName Delete a table
- * @apiGroup tables
- * @apiPermission builder
- * @apiDescription This endpoint will delete a table and all of its associated data, for this reason it is
- * quite dangerous - it will work for internal and external tables.
- *
- * @apiParam {string} tableId The ID of the table which is to be deleted.
- * @apiParam {string} [revId] If deleting an internal table then the revision must also be supplied (_rev), for
- * external tables this can simply be set to anything, e.g. "external".
- *
- * @apiSuccess {string} message A message stating that the table was deleted successfully.
- */
.delete(
"/api/tables/:tableId/:revId",
paramResource("tableId"),
authorized(BUILDER),
tableController.destroy
)
- /**
- * @api {post} /api/tables/:tableId/:revId Import CSV to existing table
- * @apiName Import CSV to existing table
- * @apiGroup tables
- * @apiPermission builder
- * @apiDescription This endpoint will import data to existing tables, internal or external. It is used in combination
- * with the CSV validation endpoint. Take the output of the CSV validation endpoint and pass it to this endpoint to
- * import the data; please note this will only import fields that already exist on the table/match the type.
- *
- * @apiParam {string} tableId The ID of the table which the data should be imported to.
- *
- * @apiParam (Body) {object[]} rows An array of objects representing the rows to be imported, key-value pairs not matching the table schema will be ignored.
- *
- * @apiSuccess {string} message A message stating that the data was imported successfully.
- */
.post(
"/api/tables/:tableId/import",
paramResource("tableId"),
diff --git a/packages/server/src/api/routes/tests/role.spec.js b/packages/server/src/api/routes/tests/role.spec.js
index c8e383d5ed..d133a69d64 100644
--- a/packages/server/src/api/routes/tests/role.spec.js
+++ b/packages/server/src/api/routes/tests/role.spec.js
@@ -158,5 +158,25 @@ describe("/roles", () => {
expect(res.body.length).toBe(1)
expect(res.body[0]).toBe("PUBLIC")
})
+
+ it("should not fetch higher level accessible roles when a custom role header is provided", async () => {
+ await createRole({
+ name: `CUSTOM_ROLE`,
+ inherits: roles.BUILTIN_ROLE_IDS.BASIC,
+ permissionId: permissions.BuiltinPermissionID.READ_ONLY,
+ version: "name",
+ })
+ const res = await request
+ .get("/api/roles/accessible")
+ .set({
+ ...config.defaultHeaders(),
+ "x-budibase-role": "CUSTOM_ROLE"
+ })
+ .expect(200)
+ expect(res.body.length).toBe(3)
+ expect(res.body[0]).toBe("CUSTOM_ROLE")
+ expect(res.body[1]).toBe("BASIC")
+ expect(res.body[2]).toBe("PUBLIC")
+ })
})
})
diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js
index ff6d7aba1d..4076f4879c 100644
--- a/packages/server/src/api/routes/tests/routing.spec.js
+++ b/packages/server/src/api/routes/tests/routing.spec.js
@@ -1,5 +1,5 @@
const setup = require("./utilities")
-const { basicScreen } = setup.structures
+const { basicScreen, powerScreen } = setup.structures
const { checkBuilderEndpoint, runInProd } = require("./utilities/TestFunctions")
const { roles } = require("@budibase/backend-core")
const { BUILTIN_ROLE_IDS } = roles
@@ -12,19 +12,14 @@ const route = "/test"
describe("/routing", () => {
let request = setup.getRequest()
let config = setup.getConfig()
- let screen, screen2
+ let basic, power
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
- screen = basicScreen()
- screen.routing.route = route
- screen = await config.createScreen(screen)
- screen2 = basicScreen()
- screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER
- screen2.routing.route = route
- screen2 = await config.createScreen(screen2)
+ basic = await config.createScreen(basicScreen(route))
+ power = await config.createScreen(powerScreen(route))
await config.publish()
})
@@ -61,8 +56,8 @@ describe("/routing", () => {
expect(res.body.routes[route]).toEqual({
subpaths: {
[route]: {
- screenId: screen._id,
- roleId: screen.routing.roleId
+ screenId: basic._id,
+ roleId: basic.routing.roleId
}
}
})
@@ -80,8 +75,8 @@ describe("/routing", () => {
expect(res.body.routes[route]).toEqual({
subpaths: {
[route]: {
- screenId: screen2._id,
- roleId: screen2.routing.roleId
+ screenId: power._id,
+ roleId: power.routing.roleId
}
}
})
@@ -101,8 +96,8 @@ describe("/routing", () => {
expect(res.body.routes).toBeDefined()
expect(res.body.routes[route].subpaths[route]).toBeDefined()
const subpath = res.body.routes[route].subpaths[route]
- expect(subpath.screens[screen2.routing.roleId]).toEqual(screen2._id)
- expect(subpath.screens[screen.routing.roleId]).toEqual(screen._id)
+ expect(subpath.screens[power.routing.roleId]).toEqual(power._id)
+ expect(subpath.screens[basic.routing.roleId]).toEqual(basic._id)
})
it("make sure it is a builder only endpoint", async () => {
diff --git a/packages/server/src/constants/screens.ts b/packages/server/src/constants/screens.ts
index 23e36a65b8..6c88b0f957 100644
--- a/packages/server/src/constants/screens.ts
+++ b/packages/server/src/constants/screens.ts
@@ -1,7 +1,15 @@
import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
-export function createHomeScreen() {
+export function createHomeScreen(
+ config: {
+ roleId: string
+ route: string
+ } = {
+ roleId: roles.BUILTIN_ROLE_IDS.BASIC,
+ route: "/",
+ }
+) {
return {
description: "",
url: "",
@@ -40,8 +48,8 @@ export function createHomeScreen() {
gap: "M",
},
routing: {
- route: "/",
- roleId: roles.BUILTIN_ROLE_IDS.BASIC,
+ route: config.route,
+ roleId: config.roleId,
},
name: "home-screen",
}
diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts
index d3e92ea34d..6d236062a8 100644
--- a/packages/server/src/tests/utilities/structures.ts
+++ b/packages/server/src/tests/utilities/structures.ts
@@ -20,6 +20,7 @@ import {
SourceName,
Table,
} from "@budibase/types"
+const { BUILTIN_ROLE_IDS } = roles
export function basicTable(): Table {
return {
@@ -322,8 +323,22 @@ export function basicUser(role: string) {
}
}
-export function basicScreen() {
- return createHomeScreen()
+export function basicScreen(route: string = "/") {
+ return createHomeScreen({
+ roleId: BUILTIN_ROLE_IDS.BASIC,
+ route,
+ })
+}
+
+export function powerScreen(route: string = "/") {
+ return createHomeScreen({
+ roleId: BUILTIN_ROLE_IDS.POWER,
+ route,
+ })
+}
+
+export function customScreen(config: { roleId: string; route: string }) {
+ return createHomeScreen(config)
}
export function basicLayout() {
diff --git a/packages/worker/Dockerfile.v2 b/packages/worker/Dockerfile.v2
index a8be432827..4706ca155a 100644
--- a/packages/worker/Dockerfile.v2
+++ b/packages/worker/Dockerfile.v2
@@ -19,7 +19,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates
COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh package.json
-RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
+RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
COPY packages/string-templates .
@@ -30,7 +30,7 @@ RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-te
RUN ../scripts/removeWorkspaceDependencies.sh package.json
-RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
+RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
# Remove unneeded data from file system to reduce image size
RUN apk del .gyp \
&& yarn cache clean
diff --git a/packages/worker/src/api/routes/global/tests/groups.spec.ts b/packages/worker/src/api/routes/global/tests/groups.spec.ts
index afeaae952c..8f0739a812 100644
--- a/packages/worker/src/api/routes/global/tests/groups.spec.ts
+++ b/packages/worker/src/api/routes/global/tests/groups.spec.ts
@@ -1,7 +1,7 @@
import { events } from "@budibase/backend-core"
import { generator } from "@budibase/backend-core/tests"
import { structures, TestConfiguration, mocks } from "../../../../tests"
-import { UserGroup } from "@budibase/types"
+import { User, UserGroup } from "@budibase/types"
mocks.licenses.useGroups()
@@ -231,4 +231,39 @@ describe("/api/global/groups", () => {
})
})
})
+
+ describe("with global builder role", () => {
+ let builder: User
+ let group: UserGroup
+
+ beforeAll(async () => {
+ builder = await config.createUser({
+ builder: { global: true },
+ admin: { global: false },
+ })
+ await config.createSession(builder)
+
+ let resp = await config.api.groups.saveGroup(
+ structures.groups.UserGroup()
+ )
+ group = resp.body as UserGroup
+ })
+
+ it("find should return 200", async () => {
+ await config.withUser(builder, async () => {
+ await config.api.groups.searchUsers(group._id!, {
+ emailSearch: `user1`,
+ })
+ })
+ })
+
+ it("update should return 200", async () => {
+ await config.withUser(builder, async () => {
+ await config.api.groups.updateGroupUsers(group._id!, {
+ add: [builder._id!],
+ remove: [],
+ })
+ })
+ })
+ })
})
diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts
index 7e9792c9e3..d4fcbeebd6 100644
--- a/packages/worker/src/tests/TestConfiguration.ts
+++ b/packages/worker/src/tests/TestConfiguration.ts
@@ -190,6 +190,16 @@ class TestConfiguration {
}
}
+ async withUser(user: User, f: () => Promise) {
+ const oldUser = this.user
+ this.user = user
+ try {
+ await f()
+ } finally {
+ this.user = oldUser
+ }
+ }
+
authHeaders(user: User) {
const authToken: AuthToken = {
userId: user._id!,
@@ -257,9 +267,10 @@ class TestConfiguration {
})
}
- async createUser(user?: User) {
- if (!user) {
- user = structures.users.user()
+ async createUser(opts?: Partial) {
+ let user = structures.users.user()
+ if (user) {
+ user = { ...user, ...opts }
}
const response = await this._req(user, null, controllers.users.save)
const body = response as SaveUserResponse
diff --git a/packages/worker/src/tests/structures/groups.ts b/packages/worker/src/tests/structures/groups.ts
index b0d6bb8fc0..d39dd74eb8 100644
--- a/packages/worker/src/tests/structures/groups.ts
+++ b/packages/worker/src/tests/structures/groups.ts
@@ -1,8 +1,8 @@
import { generator } from "@budibase/backend-core/tests"
import { db } from "@budibase/backend-core"
-import { UserGroupRoles } from "@budibase/types"
+import { UserGroup as UserGroupType, UserGroupRoles } from "@budibase/types"
-export const UserGroup = () => {
+export function UserGroup(): UserGroupType {
const appsCount = generator.integer({ min: 0, max: 3 })
const roles = Array.from({ length: appsCount }).reduce(
(p: UserGroupRoles, v) => {
@@ -14,13 +14,11 @@ export const UserGroup = () => {
{}
)
- let group = {
- apps: [],
+ return {
color: generator.color(),
icon: generator.word(),
name: generator.word(),
roles: roles,
users: [],
}
- return group
}
diff --git a/scripts/updateWorkspaceVersions.V2.sh b/scripts/updateWorkspaceVersions.V2.sh
new file mode 100755
index 0000000000..634bcbcfb0
--- /dev/null
+++ b/scripts/updateWorkspaceVersions.V2.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+version=$1
+echo "Setting version $version"
+yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
+echo "Updating dependencies"
+node scripts/syncLocalDependencies.js $version
+echo "Syncing yarn workspace"
+yarn