- This component doesn't have any additional settings.
+
This component doesn't have any additional settings.
+ {/if}
+ {#if componentDefinition?.info}
+
+ {@html componentDefinition?.info}
{/if}
@@ -185,7 +188,7 @@
height: 100%;
gap: var(--spacing-s);
}
- .empty {
+ .text {
font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spacing-m);
color: var(--grey-6);
diff --git a/packages/builder/src/global.css b/packages/builder/src/global.css
index 726f422a48..ed80f10b1c 100644
--- a/packages/builder/src/global.css
+++ b/packages/builder/src/global.css
@@ -6,10 +6,6 @@ html, body {
min-height: 100%;
}
-.spectrum--light {
- --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-75);
-}
-
body {
--background: var(--spectrum-alias-background-color-primary);
--background-alt: var(--spectrum-alias-background-color-secondary);
diff --git a/packages/builder/src/helpers/fetchData.js b/packages/builder/src/helpers/fetchData.js
new file mode 100644
index 0000000000..65061f6b6a
--- /dev/null
+++ b/packages/builder/src/helpers/fetchData.js
@@ -0,0 +1,20 @@
+import { writable } from "svelte/store"
+import api from "builderStore/api"
+
+export default function (url) {
+ const store = writable({ status: "LOADING", data: {}, error: {} })
+
+ async function get() {
+ store.update(u => ({ ...u, status: "LOADING" }))
+ try {
+ const response = await api.get(url)
+ store.set({ data: await response.json(), status: "SUCCESS" })
+ } catch (e) {
+ store.set({ data: {}, error: e, status: "ERROR" })
+ }
+ }
+
+ get()
+
+ return { subscribe: store.subscribe, refresh: get }
+}
diff --git a/packages/builder/src/helpers.js b/packages/builder/src/helpers/helpers.js
similarity index 100%
rename from packages/builder/src/helpers.js
rename to packages/builder/src/helpers/helpers.js
diff --git a/packages/builder/src/helpers/index.js b/packages/builder/src/helpers/index.js
new file mode 100644
index 0000000000..2c09cc19ca
--- /dev/null
+++ b/packages/builder/src/helpers/index.js
@@ -0,0 +1,9 @@
+export { default as fetchData } from "./fetchData"
+export {
+ buildStyle,
+ convertCamel,
+ pipe,
+ capitalise,
+ get_name,
+ get_capitalised_name,
+} from "./helpers"
diff --git a/packages/builder/src/helpers/validation/index.js b/packages/builder/src/helpers/validation/index.js
new file mode 100644
index 0000000000..5e9cfcca79
--- /dev/null
+++ b/packages/builder/src/helpers/validation/index.js
@@ -0,0 +1,2 @@
+export { emailValidator, requiredValidator } from "./validators"
+export { createValidationStore } from "./validation"
diff --git a/packages/builder/src/helpers/validation/validation.js b/packages/builder/src/helpers/validation/validation.js
new file mode 100644
index 0000000000..8d80d720a1
--- /dev/null
+++ b/packages/builder/src/helpers/validation/validation.js
@@ -0,0 +1,23 @@
+import { writable, derived } from "svelte/store"
+
+export function createValidationStore(initialValue, ...validators) {
+ let touched = false
+
+ const value = writable(initialValue || "")
+ const error = derived(value, $v => validate($v, validators))
+ const touchedStore = derived(value, () => {
+ if (!touched) {
+ touched = true
+ return false
+ }
+ return touched
+ })
+
+ return [value, error, touchedStore]
+}
+
+function validate(value, validators) {
+ const failing = validators.find(v => v(value) !== true)
+
+ return failing && failing(value)
+}
diff --git a/packages/builder/src/helpers/validation/validators.js b/packages/builder/src/helpers/validation/validators.js
new file mode 100644
index 0000000000..036487fd50
--- /dev/null
+++ b/packages/builder/src/helpers/validation/validators.js
@@ -0,0 +1,16 @@
+export function emailValidator(value) {
+ return (
+ (value &&
+ !!value.match(
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ )) ||
+ "Please enter a valid email"
+ )
+}
+
+export function requiredValidator(value) {
+ return (
+ (value !== undefined && value !== null && value !== "") ||
+ "This field is required"
+ )
+}
diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte
index dbc89131cb..fccfe0f23d 100644
--- a/packages/builder/src/pages/builder/_layout.svelte
+++ b/packages/builder/src/pages/builder/_layout.svelte
@@ -22,7 +22,13 @@
// Redirect to log in at any time if the user isn't authenticated
$: {
- if (loaded && hasAdminUser && !$auth.user && !$isActive("./auth")) {
+ if (
+ loaded &&
+ hasAdminUser &&
+ !$auth.user &&
+ !$isActive("./auth") &&
+ !$isActive("./invite")
+ ) {
$goto("./auth/login")
}
}
diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
index ba4181c6cc..53d6e17130 100644
--- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
@@ -1,7 +1,16 @@
+
+
+
+
+
+
+
+
+ Accept Invitation
+ Please enter a password to setup your user.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte
index ad624972e1..2806f0c77f 100644
--- a/packages/builder/src/pages/builder/portal/_layout.svelte
+++ b/packages/builder/src/pages/builder/portal/_layout.svelte
@@ -1,6 +1,5 @@
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/oauth/logos/Google.svelte b/packages/builder/src/pages/builder/portal/manage/auth/_logos/Google.svelte
similarity index 100%
rename from packages/builder/src/pages/builder/portal/oauth/logos/Google.svelte
rename to packages/builder/src/pages/builder/portal/manage/auth/_logos/Google.svelte
diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte
new file mode 100644
index 0000000000..af05e0e92c
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte
@@ -0,0 +1,114 @@
+
+
+
+
+
+ OAuth
+
+ Every budibase app comes with basic authentication (email/password)
+ included. You can add additional authentication methods from the options
+ below.
+
+
+
+ {#if google}
+
+
+
+
+ Google
+
+
+
+ To allow users to authenticate using their Google accounts, fill out
+ the fields below.
+
+
+
+ {#each ConfigFields.Google as field}
+
+
+
+
+ {/each}
+
+
+
+
+ {/if}
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/email/[template].svelte b/packages/builder/src/pages/builder/portal/manage/email/[template].svelte
similarity index 91%
rename from packages/builder/src/pages/builder/portal/email/[template].svelte
rename to packages/builder/src/pages/builder/portal/manage/email/[template].svelte
index e819aafae4..ff5b2d4737 100644
--- a/packages/builder/src/pages/builder/portal/email/[template].svelte
+++ b/packages/builder/src/pages/builder/portal/manage/email/[template].svelte
@@ -1,32 +1,19 @@
+
+
+
+ OAuth
+
+ Every budibase app comes with basic authentication (email/password)
+ included. You can add additional authentication methods from the options
+ below.
+
+
+
+ {#if google}
+
+
+ {/if}
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte
new file mode 100644
index 0000000000..b8c55d6aca
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte
@@ -0,0 +1,168 @@
+
+
+
+
+
$goto("./")} quiet size="S" icon="BackAndroid"
+ >Back to users
+
+
+
+ User: {$roleFetch?.data?.email}
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro
+ ut nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit
+ veritatis quos quas laborum magnam tenetur perspiciatis ex hic.
+
+
+
+
+
+
+
+
+
+
Delete user
+ Deleting a user completely removes them from your account.
+
+
+
+
+
+
+
+
+ Are you sure you want to delete {$roleFetch?.data?.email}
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
new file mode 100644
index 0000000000..8646da9c06
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
@@ -0,0 +1,59 @@
+
+
+
+ If you have SMTP configured and an email for the new user, you can use the
+ automated email onboarding flow. Otherwise, use our basic onboarding process
+ with autogenerated passwords.
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte
new file mode 100644
index 0000000000..03f607127f
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte
@@ -0,0 +1,40 @@
+
+
+
+ Below you will find the user’s username and password. The password will not
+ be accessible from this point. Please download the credentials.
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/TagsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/TagsTableRenderer.svelte
new file mode 100644
index 0000000000..f21a6b1da8
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/TagsTableRenderer.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {#each tags as tag}
+
+ {tag}
+
+ {/each}
+ {#if leftover}
+ +{leftover} more
+ {/if}
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte
new file mode 100644
index 0000000000..4e47d96552
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte
@@ -0,0 +1,51 @@
+
+
+
+ Update {user.email}'s roles for {app.name}.
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte
new file mode 100644
index 0000000000..38f1c3c3e9
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte
@@ -0,0 +1,105 @@
+
+
+
+
+ Users
+ Users are the common denominator in Budibase. Each user is assigned to a
+ group that contains apps and permissions. In this section, you can add
+ users, or edit and delete an existing user.
+
+
+
+
+
Users
+
+
+
+
+
+
+
+
+
+
+
$goto(`./${detail._id}`)}
+ {schema}
+ data={filteredUsers || $users}
+ allowEditColumns={false}
+ allowEditRows={false}
+ allowSelectRows={false}
+ customRenderers={[{ column: "group", component: TagsRenderer }]}
+ />
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/oauth/_logos/Google.svelte b/packages/builder/src/pages/builder/portal/oauth/_logos/Google.svelte
new file mode 100644
index 0000000000..5686e50abc
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/oauth/_logos/Google.svelte
@@ -0,0 +1,43 @@
+
diff --git a/packages/builder/src/pages/builder/portal/oauth/index.svelte b/packages/builder/src/pages/builder/portal/oauth/index.svelte
index d5962e38e1..f8d9b43dec 100644
--- a/packages/builder/src/pages/builder/portal/oauth/index.svelte
+++ b/packages/builder/src/pages/builder/portal/oauth/index.svelte
@@ -1,7 +1,8 @@
{#if loaded && $screenStore.activeLayout}
-
-
-
-
+
{/if}
+
+
diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js
index b14660b7d7..b9f9dd8c5a 100644
--- a/packages/server/src/api/controllers/application.js
+++ b/packages/server/src/api/controllers/application.js
@@ -244,7 +244,7 @@ exports.delete = async function (ctx) {
}
const createEmptyAppPackage = async (ctx, app) => {
- const db = new CouchDB(app.instance._id)
+ const db = new CouchDB(app.appId)
let screensAndLayouts = []
for (let layout of BASE_LAYOUTS) {
diff --git a/packages/server/src/api/controllers/role.js b/packages/server/src/api/controllers/role.js
index 1ab368e5c5..7b127fb2d0 100644
--- a/packages/server/src/api/controllers/role.js
+++ b/packages/server/src/api/controllers/role.js
@@ -1,15 +1,12 @@
const CouchDB = require("../../db")
const {
- getBuiltinRoles,
Role,
getRole,
isBuiltin,
- getExternalRoleID,
getAllRoles,
} = require("@budibase/auth/roles")
const {
generateRoleID,
- getRoleParams,
getUserMetadataParams,
InternalTables,
} = require("../../db/utils")
diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js
index 6d0cc08548..1d73abea47 100644
--- a/packages/server/src/api/controllers/row.js
+++ b/packages/server/src/api/controllers/row.js
@@ -16,7 +16,6 @@ const {
const { FieldTypes } = require("../../constants")
const { isEqual } = require("lodash")
const { cloneDeep } = require("lodash/fp")
-const { QueryBuilder, search } = require("./search/utils")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
@@ -248,45 +247,6 @@ exports.fetchView = async function (ctx) {
}
}
-exports.search = async function (ctx) {
- const appId = ctx.appId
- const db = new CouchDB(appId)
- const {
- query,
- pagination: { pageSize = 10, bookmark },
- } = ctx.request.body
- const tableId = ctx.params.tableId
-
- const queryBuilder = new QueryBuilder(appId)
- .setLimit(pageSize)
- .addTable(tableId)
- if (bookmark) {
- queryBuilder.setBookmark(bookmark)
- }
-
- let searchString
- if (ctx.query && ctx.query.raw && ctx.query.raw !== "") {
- searchString = queryBuilder.complete(query["RAW"])
- } else {
- // make all strings a starts with operation rather than pure equality
- for (const [key, queryVal] of Object.entries(query)) {
- if (typeof queryVal === "string") {
- queryBuilder.addString(key, queryVal)
- } else {
- queryBuilder.addEqual(key, queryVal)
- }
- }
- searchString = queryBuilder.complete()
- }
-
- const response = await search(searchString)
- const table = await db.get(tableId)
- ctx.body = {
- rows: await outputProcessing(appId, table, response.rows),
- bookmark: response.bookmark,
- }
-}
-
exports.fetchTableRows = async function (ctx) {
const appId = ctx.appId
const db = new CouchDB(appId)
diff --git a/packages/server/src/api/controllers/search/index.js b/packages/server/src/api/controllers/search/index.js
index 234c7eb258..ede0556e18 100644
--- a/packages/server/src/api/controllers/search/index.js
+++ b/packages/server/src/api/controllers/search/index.js
@@ -1,18 +1,26 @@
-const { QueryBuilder, buildSearchUrl, search } = require("./utils")
+const { fullSearch, paginatedSearch } = require("./utils")
+const CouchDB = require("../../../db")
+const { outputProcessing } = require("../../../utilities/rowProcessor")
exports.rowSearch = async ctx => {
const appId = ctx.appId
const { tableId } = ctx.params
- const { bookmark, query, raw } = ctx.request.body
- let url
- if (query) {
- url = new QueryBuilder(appId, query, bookmark).addTable(tableId).complete()
- } else if (raw) {
- url = buildSearchUrl({
- appId,
- query: raw,
- bookmark,
- })
+ const db = new CouchDB(appId)
+ const { paginate, query, ...params } = ctx.request.body
+ params.tableId = tableId
+
+ let response
+ if (paginate) {
+ response = await paginatedSearch(appId, query, params)
+ } else {
+ response = await fullSearch(appId, query, params)
}
- ctx.body = await search(url)
+
+ // Enrich search results with relationships
+ if (response.rows && response.rows.length) {
+ const table = await db.get(tableId)
+ response.rows = await outputProcessing(appId, table, response.rows)
+ }
+
+ ctx.body = response
}
diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js
index d3ffb26be7..7225ab633a 100644
--- a/packages/server/src/api/controllers/search/utils.js
+++ b/packages/server/src/api/controllers/search/utils.js
@@ -1,31 +1,21 @@
const { SearchIndexes } = require("../../../db/utils")
-const { checkSlashesInUrl } = require("../../../utilities")
const env = require("../../../environment")
const fetch = require("node-fetch")
/**
- * Given a set of inputs this will generate the URL which is to be sent to the search proxy in CouchDB.
- * @param {string} appId The ID of the app which we will be searching within.
- * @param {string} query The lucene query string which is to be used for searching.
- * @param {string|null} bookmark If there were more than the limit specified can send the bookmark that was
- * returned with query for next set of search results.
- * @param {number} limit The number of entries to return per query.
- * @param {boolean} excludeDocs By default full rows are returned, if required this can be disabled.
- * @return {string} The URL which a GET can be performed on to receive results.
+ * Escapes any characters in a string which lucene searches require to be
+ * escaped.
+ * @param value The value to escape
+ * @returns {string}
*/
-function buildSearchUrl({ appId, query, bookmark, excludeDocs, limit = 50 }) {
- let url = `${env.COUCH_DB_URL}/${appId}/_design/database/_search`
- url += `/${SearchIndexes.ROWS}?q=${query}`
- url += `&limit=${limit}`
- if (!excludeDocs) {
- url += "&include_docs=true"
- }
- if (bookmark) {
- url += `&bookmark=${bookmark}`
- }
- return checkSlashesInUrl(url)
+const luceneEscape = value => {
+ return `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
}
+/**
+ * Class to build lucene query URLs.
+ * Optionally takes a base lucene query object.
+ */
class QueryBuilder {
constructor(appId, base) {
this.appId = appId
@@ -34,10 +24,20 @@ class QueryBuilder {
fuzzy: {},
range: {},
equal: {},
+ notEqual: {},
+ empty: {},
+ notEmpty: {},
...base,
}
this.limit = 50
- this.bookmark = null
+ this.sortOrder = "ascending"
+ this.sortType = "string"
+ this.includeDocs = true
+ }
+
+ setTable(tableId) {
+ this.query.equal.tableId = tableId
+ return this
}
setLimit(limit) {
@@ -45,11 +45,31 @@ class QueryBuilder {
return this
}
+ setSort(sort) {
+ this.sort = sort
+ return this
+ }
+
+ setSortOrder(sortOrder) {
+ this.sortOrder = sortOrder
+ return this
+ }
+
+ setSortType(sortType) {
+ this.sortType = sortType
+ return this
+ }
+
setBookmark(bookmark) {
this.bookmark = bookmark
return this
}
+ excludeDocs() {
+ this.includeDocs = false
+ return this
+ }
+
addString(key, partial) {
this.query.string[key] = partial
return this
@@ -73,52 +93,113 @@ class QueryBuilder {
return this
}
- addTable(tableId) {
- this.query.equal.tableId = tableId
+ addNotEqual(key, value) {
+ this.query.notEqual[key] = value
return this
}
- complete(rawQuery = null) {
- let output = ""
+ addEmpty(key, value) {
+ this.query.empty[key] = value
+ return this
+ }
+
+ addNotEmpty(key, value) {
+ this.query.notEmpty[key] = value
+ return this
+ }
+
+ buildSearchQuery() {
+ let query = "*:*"
+
function build(structure, queryFn) {
for (let [key, value] of Object.entries(structure)) {
- if (output.length !== 0) {
- output += " AND "
+ const expression = queryFn(luceneEscape(key.replace(/ /, "_")), value)
+ if (expression == null) {
+ continue
}
- output += queryFn(key, value)
+ query += ` AND ${expression}`
}
}
+ // Construct the actual lucene search query string from JSON structure
if (this.query.string) {
- build(this.query.string, (key, value) => `${key}:${value}*`)
+ build(this.query.string, (key, value) => {
+ return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null
+ })
}
if (this.query.range) {
- build(
- this.query.range,
- (key, value) => `${key}:[${value.low} TO ${value.high}]`
- )
+ build(this.query.range, (key, value) => {
+ if (!value) {
+ return null
+ }
+ if (value.low == null || value.low === "") {
+ return null
+ }
+ if (value.high == null || value.high === "") {
+ return null
+ }
+ return `${key}:[${value.low} TO ${value.high}]`
+ })
}
if (this.query.fuzzy) {
- build(this.query.fuzzy, (key, value) => `${key}:${value}~`)
+ build(this.query.fuzzy, (key, value) => {
+ return value ? `${key}:${luceneEscape(value.toLowerCase())}~` : null
+ })
}
if (this.query.equal) {
- build(this.query.equal, (key, value) => `${key}:${value}`)
+ build(this.query.equal, (key, value) => {
+ return value ? `${key}:${luceneEscape(value.toLowerCase())}` : null
+ })
}
- if (rawQuery) {
- output = output.length === 0 ? rawQuery : `&${rawQuery}`
+ if (this.query.notEqual) {
+ build(this.query.notEqual, (key, value) => {
+ return value ? `!${key}:${luceneEscape(value.toLowerCase())}` : null
+ })
}
- return buildSearchUrl({
- appId: this.appId,
- query: output,
- bookmark: this.bookmark,
- limit: this.limit,
- })
+ if (this.query.empty) {
+ build(this.query.empty, key => `!${key}:["" TO *]`)
+ }
+ if (this.query.notEmpty) {
+ build(this.query.notEmpty, key => `${key}:["" TO *]`)
+ }
+
+ return query
+ }
+
+ buildSearchBody() {
+ let body = {
+ q: this.buildSearchQuery(),
+ limit: Math.min(this.limit, 200),
+ include_docs: this.includeDocs,
+ }
+ if (this.bookmark) {
+ body.bookmark = this.bookmark
+ }
+ if (this.sort) {
+ const order = this.sortOrder === "descending" ? "-" : ""
+ const type = `<${this.sortType}>`
+ body.sort = `${order}${this.sort.replace(/ /, "_")}${type}`
+ }
+ return body
+ }
+
+ async run() {
+ const url = `${env.COUCH_DB_URL}/${this.appId}/_design/database/_search/${SearchIndexes.ROWS}`
+ const body = this.buildSearchBody()
+ return await runQuery(url, body)
}
}
-exports.search = async query => {
- const response = await fetch(query, {
- method: "GET",
+/**
+ * Executes a lucene search query.
+ * @param url The query URL
+ * @param body The request body defining search criteria
+ * @returns {Promise<{rows: []}>}
+ */
+const runQuery = async (url, body) => {
+ const response = await fetch(url, {
+ body: JSON.stringify(body),
+ method: "POST",
})
const json = await response.json()
let output = {
@@ -133,5 +214,122 @@ exports.search = async query => {
return output
}
-exports.QueryBuilder = QueryBuilder
-exports.buildSearchUrl = buildSearchUrl
+/**
+ * Gets round the fixed limit of 200 results from a query by fetching as many
+ * pages as required and concatenating the results. This recursively operates
+ * until enough results have been found.
+ * @param appId {string} The app ID to search
+ * @param query {object} The JSON query structure
+ * @param params {object} The search params including:
+ * tableId {string} The table ID to search
+ * sort {string} The sort column
+ * sortOrder {string} The sort order ("ascending" or "descending")
+ * sortType {string} Whether to treat sortable values as strings or
+ * numbers. ("string" or "number")
+ * limit {number} The number of results to fetch
+ * bookmark {string|null} Current bookmark in the recursive search
+ * rows {array|null} Current results in the recursive search
+ * @returns {Promise<*[]|*>}
+ */
+const recursiveSearch = async (appId, query, params) => {
+ const bookmark = params.bookmark
+ const rows = params.rows || []
+ if (rows.length >= params.limit) {
+ return rows
+ }
+ let pageSize = 200
+ if (rows.length > params.limit - 200) {
+ pageSize = params.limit - rows.length
+ }
+ const page = await new QueryBuilder(appId, query)
+ .setTable(params.tableId)
+ .setBookmark(bookmark)
+ .setLimit(pageSize)
+ .setSort(params.sort)
+ .setSortOrder(params.sortOrder)
+ .setSortType(params.sortType)
+ .run()
+ if (!page.rows.length) {
+ return rows
+ }
+ if (page.rows.length < 200) {
+ return [...rows, ...page.rows]
+ }
+ const newParams = {
+ ...params,
+ bookmark: page.bookmark,
+ rows: [...rows, ...page.rows],
+ }
+ return await recursiveSearch(appId, query, newParams)
+}
+
+/**
+ * Performs a paginated search. A bookmark will be returned to allow the next
+ * page to be fetched. There is a max limit off 200 results per page in a
+ * paginated search.
+ * @param appId {string} The app ID to search
+ * @param query {object} The JSON query structure
+ * @param params {object} The search params including:
+ * tableId {string} The table ID to search
+ * sort {string} The sort column
+ * sortOrder {string} The sort order ("ascending" or "descending")
+ * sortType {string} Whether to treat sortable values as strings or
+ * numbers. ("string" or "number")
+ * limit {number} The desired page size
+ * bookmark {string} The bookmark to resume from
+ * @returns {Promise<{hasNextPage: boolean, rows: *[]}>}
+ */
+exports.paginatedSearch = async (appId, query, params) => {
+ let limit = params.limit
+ if (limit == null || isNaN(limit) || limit < 0) {
+ limit = 50
+ }
+ limit = Math.min(limit, 200)
+ const search = new QueryBuilder(appId, query)
+ .setTable(params.tableId)
+ .setSort(params.sort)
+ .setSortOrder(params.sortOrder)
+ .setSortType(params.sortType)
+ const searchResults = await search
+ .setBookmark(params.bookmark)
+ .setLimit(limit)
+ .run()
+
+ // Try fetching 1 row in the next page to see if another page of results
+ // exists or not
+ const nextResults = await search
+ .setBookmark(searchResults.bookmark)
+ .setLimit(1)
+ .run()
+
+ return {
+ ...searchResults,
+ hasNextPage: nextResults.rows && nextResults.rows.length > 0,
+ }
+}
+
+/**
+ * Performs a full search, fetching multiple pages if required to return the
+ * desired amount of results. There is a limit of 1000 results to avoid
+ * heavy performance hits, and to avoid client components breaking from
+ * handling too much data.
+ * @param appId {string} The app ID to search
+ * @param query {object} The JSON query structure
+ * @param params {object} The search params including:
+ * tableId {string} The table ID to search
+ * sort {string} The sort column
+ * sortOrder {string} The sort order ("ascending" or "descending")
+ * sortType {string} Whether to treat sortable values as strings or
+ * numbers. ("string" or "number")
+ * limit {number} The desired number of results
+ * @returns {Promise<{rows: *}>}
+ */
+exports.fullSearch = async (appId, query, params) => {
+ let limit = params.limit
+ if (limit == null || isNaN(limit) || limit < 0) {
+ limit = 1000
+ }
+ params.limit = Math.min(limit, 1000)
+ const rows = await recursiveSearch(appId, query, params)
+ return { rows }
+}
diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js
index 0b09a78bb8..5ea3ddacef 100644
--- a/packages/server/src/api/routes/index.js
+++ b/packages/server/src/api/routes/index.js
@@ -23,6 +23,7 @@ const queryRoutes = require("./query")
const hostingRoutes = require("./hosting")
const backupRoutes = require("./backup")
const devRoutes = require("./dev")
+const searchRoutes = require("./search")
exports.mainRoutes = [
authRoutes,
@@ -51,6 +52,7 @@ exports.mainRoutes = [
// this could be breaking as koa may recognise other routes as this
tableRoutes,
rowRoutes,
+ searchRoutes,
]
exports.staticRoutes = staticRoutes
diff --git a/packages/server/src/api/routes/row.js b/packages/server/src/api/routes/row.js
index e0e3c5ab81..ca1e170754 100644
--- a/packages/server/src/api/routes/row.js
+++ b/packages/server/src/api/routes/row.js
@@ -39,12 +39,6 @@ router
usage,
rowController.save
)
- .post(
- "/api/:tableId/rows/search",
- paramResource("tableId"),
- authorized(PermissionTypes.TABLE, PermissionLevels.READ),
- rowController.search
- )
.patch(
"/api/:tableId/rows/:rowId",
paramSubResource("tableId", "rowId"),
diff --git a/packages/server/src/db/views/staticViews.js b/packages/server/src/db/views/staticViews.js
index 5f5bc7db14..23f320d7eb 100644
--- a/packages/server/src/db/views/staticViews.js
+++ b/packages/server/src/db/views/staticViews.js
@@ -84,6 +84,7 @@ async function searchIndex(appId, indexName, fnString) {
designDoc.indexes = {
[indexName]: {
index: fnString,
+ analyzer: "keyword",
},
}
await db.put(designDoc)
@@ -96,11 +97,15 @@ exports.createAllSearchIndex = async appId => {
function (doc) {
function idx(input, prev) {
for (let key of Object.keys(input)) {
- const idxKey = prev != null ? `${prev}.${key}` : key
- if (key === "_id" || key === "_rev") {
+ let idxKey = prev != null ? `${prev}.${key}` : key
+ idxKey = idxKey.replace(/ /, "_")
+ if (key === "_id" || key === "_rev" || input[key] == null) {
continue
}
- if (typeof input[key] !== "object") {
+ if (typeof input[key] === "string") {
+ // eslint-disable-next-line no-undef
+ index(idxKey, input[key].toLowerCase(), { store: true })
+ } else if (typeof input[key] !== "object") {
// eslint-disable-next-line no-undef
index(idxKey, input[key], { store: true })
} else {
diff --git a/packages/server/src/utilities/rowProcessor.js b/packages/server/src/utilities/rowProcessor.js
index fd79751c3e..2267c9e986 100644
--- a/packages/server/src/utilities/rowProcessor.js
+++ b/packages/server/src/utilities/rowProcessor.js
@@ -123,24 +123,6 @@ function processAutoColumn(user, table, row) {
return { table, row }
}
-/**
- * Given a set of rows and the table they came from this function will sort by auto ID or a custom
- * method if provided (not implemented yet).
- */
-function sortRows(table, rows) {
- // sort based on auto ID (if found)
- let autoIDColumn = Object.entries(table.schema).find(
- schema => schema[1].subtype === AutoFieldSubTypes.AUTO_ID
- )
- // get the column name, this is the first element in the array (Object.entries)
- autoIDColumn = autoIDColumn && autoIDColumn.length ? autoIDColumn[0] : null
- if (autoIDColumn) {
- // sort in ascending order
- rows.sort((a, b) => a[autoIDColumn] - b[autoIDColumn])
- }
- return rows
-}
-
/**
* Looks through the rows provided and finds formulas - which it then processes.
*/
@@ -213,8 +195,6 @@ exports.outputProcessing = async (appId, table, rows) => {
rows = [rows]
wasArray = false
}
- // sort by auto ID
- rows = sortRows(table, rows)
// attach any linked row information
let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows)
diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json
index 163caa3dbc..a3fbc5aa52 100644
--- a/packages/standard-components/manifest.json
+++ b/packages/standard-components/manifest.json
@@ -65,41 +65,6 @@
"type": "schema"
}
},
- "search": {
- "name": "Search",
- "description": "A searchable list of items.",
- "icon": "Search",
- "styleable": true,
- "hasChildren": 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."
- }
- ],
- "context": {
- "type": "schema"
- }
- },
"stackedlist": {
"name": "Stacked List",
"icon": "TaskList",
@@ -1416,6 +1381,7 @@
},
"dataprovider": {
"name": "Data Provider",
+ "info": "Pagination is only available for data stored in internal tables.",
"icon": "Data",
"styleable": false,
"hasChildren": true,
@@ -1445,7 +1411,14 @@
{
"type": "number",
"label": "Limit",
- "key": "limit"
+ "key": "limit",
+ "defaultValue": 50
+ },
+ {
+ "type": "boolean",
+ "label": "Paginate",
+ "key": "paginate",
+ "defaultValue": true
}
],
"context": {
@@ -1464,12 +1437,8 @@
"key": "schema"
},
{
- "label": "Loading",
- "key": "loading"
- },
- {
- "label": "Loaded",
- "key": "loaded"
+ "label": "Page Number",
+ "key": "pageNumber"
}
]
}
diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte
index a8da4925d3..e0b2ad859a 100644
--- a/packages/standard-components/src/DataProvider.svelte
+++ b/packages/standard-components/src/DataProvider.svelte
@@ -1,11 +1,13 @@
-
+
-
+ {#if !loaded}
+
+ {:else}
+
+ {#if paginate && internalTable}
+
+ {/if}
+ {/if}
+
+
diff --git a/packages/standard-components/src/Search.svelte b/packages/standard-components/src/Search.svelte
deleted file mode 100644
index 29ca3b011b..0000000000
--- a/packages/standard-components/src/Search.svelte
+++ /dev/null
@@ -1,195 +0,0 @@
-
-
-
-
-
- {#if loaded}
- {#if rows.length > 0}
- {#if $component.children === 0 && $builderStore.inBuilder}
-
Add some components to display.
- {:else}
- {#each rows as row}
-
-
-
- {/each}
- {/if}
- {:else if noRowsMessage}
-
{noRowsMessage}
- {/if}
- {/if}
-
-
-
-
-
diff --git a/packages/standard-components/src/charts/ApexChart.svelte b/packages/standard-components/src/charts/ApexChart.svelte
index cf9cade436..a7e25514e0 100644
--- a/packages/standard-components/src/charts/ApexChart.svelte
+++ b/packages/standard-components/src/charts/ApexChart.svelte
@@ -10,9 +10,9 @@
{#if options}
-{:else if builderStore.inBuilder}
-
- Use the settings panel to build your chart -->
+{:else if $builderStore.inBuilder}
+
+ Use the settings panel to build your chart.
{/if}
@@ -21,4 +21,10 @@
display: flex !important;
text-transform: capitalize;
}
+ div :global(.apexcharts-yaxis-label, .apexcharts-xaxis-label) {
+ fill: #aaa;
+ }
+ div.placeholder {
+ padding: 10px;
+ }
diff --git a/packages/standard-components/src/forms/Form.svelte b/packages/standard-components/src/forms/Form.svelte
index ccc61fab9e..afa4aeeeb4 100644
--- a/packages/standard-components/src/forms/Form.svelte
+++ b/packages/standard-components/src/forms/Form.svelte
@@ -187,5 +187,6 @@
div {
padding: 20px;
position: relative;
+ background-color: var(--spectrum-alias-background-color-secondary);
}
diff --git a/packages/standard-components/src/index.js b/packages/standard-components/src/index.js
index 2ad685033a..7b4d492fa9 100644
--- a/packages/standard-components/src/index.js
+++ b/packages/standard-components/src/index.js
@@ -27,7 +27,6 @@ export { default as embed } from "./Embed.svelte"
export { default as cardhorizontal } from "./CardHorizontal.svelte"
export { default as cardstat } from "./CardStat.svelte"
export { default as icon } from "./Icon.svelte"
-export { default as search } from "./Search.svelte"
export { default as backgroundimage } from "./BackgroundImage.svelte"
export * from "./charts"
export * from "./forms"
diff --git a/packages/standard-components/src/table/Table.svelte b/packages/standard-components/src/table/Table.svelte
index f68835d75b..35839e4722 100644
--- a/packages/standard-components/src/table/Table.svelte
+++ b/packages/standard-components/src/table/Table.svelte
@@ -94,3 +94,9 @@
+
+
diff --git a/packages/string-templates/src/index.cjs b/packages/string-templates/src/index.cjs
index e662f253c6..b62521942a 100644
--- a/packages/string-templates/src/index.cjs
+++ b/packages/string-templates/src/index.cjs
@@ -4,7 +4,7 @@ const processors = require("./processors")
const { cloneDeep } = require("lodash/fp")
const {
removeNull,
- addConstants,
+ updateContext,
removeHandlebarsStatements,
} = require("./utilities")
const manifest = require("../manifest.json")
@@ -92,8 +92,7 @@ module.exports.processStringSync = (string, context) => {
}
// take a copy of input incase error
const input = string
- let clonedContext = removeNull(cloneDeep(context))
- clonedContext = addConstants(clonedContext)
+ const clonedContext = removeNull(updateContext(cloneDeep(context)))
// remove any null/undefined properties
if (typeof string !== "string") {
throw "Cannot process non-string types."
diff --git a/packages/string-templates/src/utilities.js b/packages/string-templates/src/utilities.js
index 38496b04b4..e94b7f8ee7 100644
--- a/packages/string-templates/src/utilities.js
+++ b/packages/string-templates/src/utilities.js
@@ -23,11 +23,24 @@ module.exports.removeNull = obj => {
return obj
}
-module.exports.addConstants = obj => {
+module.exports.updateContext = obj => {
if (obj.now == null) {
- obj.now = new Date()
+ obj.now = new Date().toISOString()
}
- return obj
+ function recurse(obj) {
+ for (let key of Object.keys(obj)) {
+ if (!obj[key]) {
+ continue
+ }
+ if (obj[key] instanceof Date) {
+ obj[key] = obj[key].toISOString()
+ } else if (typeof obj[key] === "object") {
+ obj[key] = recurse(obj[key])
+ }
+ }
+ return obj
+ }
+ return recurse(obj)
}
module.exports.removeHandlebarsStatements = string => {
diff --git a/packages/string-templates/test/basic.spec.js b/packages/string-templates/test/basic.spec.js
index 5732181b13..f5c7c8be75 100644
--- a/packages/string-templates/test/basic.spec.js
+++ b/packages/string-templates/test/basic.spec.js
@@ -107,6 +107,12 @@ describe("check the utility functions", () => {
const property = makePropSafe("thing")
expect(property).toEqual("[thing]")
})
+
+ it("should be able to handle an input date object", async () => {
+ const date = new Date()
+ const output = await processString("{{ dateObj }}", { dateObj: date })
+ expect(date.toISOString()).toEqual(output)
+ })
})
describe("check manifest", () => {
diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js
index 43801ff882..7845eee0d6 100644
--- a/packages/worker/src/api/controllers/admin/configs.js
+++ b/packages/worker/src/api/controllers/admin/configs.js
@@ -118,16 +118,17 @@ exports.upload = async function (ctx) {
// add to configuration structure
// TODO: right now this only does a global level
const db = new CouchDB(GLOBAL_DB)
- let config = await getScopedFullConfig(db, { type })
- if (!config) {
- config = {
+ let cfgStructure = await getScopedFullConfig(db, { type })
+ if (!cfgStructure) {
+ cfgStructure = {
_id: generateConfigID({ type }),
+ config: {},
}
}
const url = `/${bucket}/${key}`
- config[`${name}Url`] = url
+ cfgStructure.config[`${name}Url`] = url
// write back to db with url updated
- await db.put(config)
+ await db.put(cfgStructure)
ctx.body = {
message: "File has been uploaded and url stored to config.",
diff --git a/packages/worker/src/api/controllers/admin/roles.js b/packages/worker/src/api/controllers/admin/roles.js
index 8ef45d3765..84f5ab2b71 100644
--- a/packages/worker/src/api/controllers/admin/roles.js
+++ b/packages/worker/src/api/controllers/admin/roles.js
@@ -1,5 +1,9 @@
const { getAllRoles } = require("@budibase/auth/roles")
-const { getAllApps, getDeployedAppID, DocumentTypes } = require("@budibase/auth/db")
+const {
+ getAllApps,
+ getDeployedAppID,
+ DocumentTypes,
+} = require("@budibase/auth/db")
const CouchDB = require("../../../db")
exports.fetch = async ctx => {
diff --git a/packages/worker/src/api/controllers/app.js b/packages/worker/src/api/controllers/app.js
index 8b556fbb1e..75366c2365 100644
--- a/packages/worker/src/api/controllers/app.js
+++ b/packages/worker/src/api/controllers/app.js
@@ -29,7 +29,7 @@ exports.getApps = async ctx => {
let url = app.url || encodeURI(`${app.name}`)
url = `/${url.replace(URL_REGEX_SLASH, "")}`
body[url] = {
- appId: app.instance._id,
+ appId: app.appId,
name: app.name,
url,
}
diff --git a/packages/worker/src/api/routes/admin/configs.js b/packages/worker/src/api/routes/admin/configs.js
index 1da11f2266..a028ebdd81 100644
--- a/packages/worker/src/api/routes/admin/configs.js
+++ b/packages/worker/src/api/routes/admin/configs.js
@@ -25,9 +25,9 @@ function smtpValidation() {
function settingValidation() {
// prettier-ignore
return Joi.object({
- platformUrl: Joi.string().valid("", null),
- logoUrl: Joi.string().valid("", null),
- docsUrl: Joi.string().valid("", null),
+ platformUrl: Joi.string().optional(),
+ logoUrl: Joi.string().optional(),
+ docsUrl: Joi.string().optional(),
company: Joi.string().required(),
}).unknown(true)
}
@@ -44,9 +44,9 @@ function googleValidation() {
function buildConfigSaveValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
- _id: Joi.string(),
- _rev: Joi.string(),
- group: Joi.string(),
+ _id: Joi.string().optional(),
+ _rev: Joi.string().optional(),
+ group: Joi.string().optional(),
type: Joi.string().valid(...Object.values(Configs)).required(),
config: Joi.alternatives()
.conditional("type", {
diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js
index 6b860e02bd..5e712eea1c 100644
--- a/packages/worker/src/utilities/templates.js
+++ b/packages/worker/src/utilities/templates.js
@@ -7,9 +7,8 @@ const {
EmailTemplatePurpose,
} = require("../constants")
const { checkSlashesInUrl } = require("./index")
-const env = require("../environment")
-const LOCAL_URL = `http://localhost:${env.PORT}`
+const LOCAL_URL = `http://localhost:10000`
const BASE_COMPANY = "Budibase"
exports.getSettingsTemplateContext = async (purpose, code = null) => {
@@ -42,7 +41,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
case EmailTemplatePurpose.INVITATION:
context[InternalTemplateBindings.INVITE_CODE] = code
context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl(
- `${URL}/invite?code=${code}`
+ `${URL}/builder/invite?code=${code}`
)
break
}
diff --git a/yarn.lock b/yarn.lock
index c8c4430e09..f344bd8338 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4559,7 +4559,7 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
-svelte@^3.37.0:
+svelte@^3.38.2:
version "3.38.2"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==