forked from tsai/budibase
91 changed files with 962 additions and 583 deletions
@ -1,9 +1,9 @@ |
|||||
{ |
{ |
||||
"baseUrl": "http://localhost:10000/builder/", |
"baseUrl": "http://localhost:10001/builder/", |
||||
"video": true, |
"video": true, |
||||
"projectId": "bmbemn", |
"projectId": "bmbemn", |
||||
"env": { |
"env": { |
||||
"PORT": "10000", |
"PORT": "10001", |
||||
"JWT_SECRET": "test" |
"JWT_SECRET": "test" |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,11 +1,10 @@ |
|||||
context("Create a User", () => { |
context("Create a User", () => { |
||||
before(() => { |
before(() => { |
||||
cy.login() |
cy.login() |
||||
cy.createTestApp() |
|
||||
}) |
}) |
||||
|
|
||||
it("should create a user", () => { |
it("should create a user", () => { |
||||
cy.createUser("bbuser@test.com", "test", "ADMIN") |
cy.createUser("bbuser@test.com") |
||||
cy.contains("bbuser").should("be.visible") |
cy.contains("bbuser").should("be.visible") |
||||
}) |
}) |
||||
}) |
}) |
||||
|
|||||
@ -0,0 +1,19 @@ |
|||||
|
import { get } from "svelte/store" |
||||
|
import { getContext } from "svelte" |
||||
|
|
||||
|
/** |
||||
|
* Gets a component action. |
||||
|
* @param id The component ID that provides the action |
||||
|
* @param type The action type to get |
||||
|
* @returns {null|*} The action function |
||||
|
*/ |
||||
|
export const getAction = (id, type) => { |
||||
|
if (!id || !type) { |
||||
|
return null |
||||
|
} |
||||
|
const context = getContext("context") |
||||
|
if (!context) { |
||||
|
return null |
||||
|
} |
||||
|
return get(context)?.[`${id}_${type}`] |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
const CouchDB = require("../db") |
||||
|
const { |
||||
|
getMultiIDParams, |
||||
|
getGlobalIDFromUserMetadataID, |
||||
|
StaticDatabases, |
||||
|
} = require("../db/utils") |
||||
|
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") |
||||
|
const { getDeployedAppID } = require("@budibase/auth/db") |
||||
|
const { getGlobalUserParams } = require("@budibase/auth/db") |
||||
|
|
||||
|
exports.updateAppRole = (appId, user) => { |
||||
|
if (!user.roles) { |
||||
|
return user |
||||
|
} |
||||
|
if (user.builder && user.builder.global) { |
||||
|
user.roleId = BUILTIN_ROLE_IDS.ADMIN |
||||
|
} else { |
||||
|
// always use the deployed app
|
||||
|
user.roleId = user.roles[getDeployedAppID(appId)] |
||||
|
if (!user.roleId) { |
||||
|
user.roleId = BUILTIN_ROLE_IDS.PUBLIC |
||||
|
} |
||||
|
} |
||||
|
delete user.roles |
||||
|
return user |
||||
|
} |
||||
|
|
||||
|
exports.getGlobalUser = async (appId, userId) => { |
||||
|
const db = CouchDB(StaticDatabases.GLOBAL.name) |
||||
|
let user = await db.get(getGlobalIDFromUserMetadataID(userId)) |
||||
|
if (user) { |
||||
|
delete user.password |
||||
|
} |
||||
|
return exports.updateAppRole(appId, user) |
||||
|
} |
||||
|
|
||||
|
exports.getGlobalUsers = async (appId = null, users = null) => { |
||||
|
const db = CouchDB(StaticDatabases.GLOBAL.name) |
||||
|
let globalUsers |
||||
|
if (users) { |
||||
|
const globalIds = users.map(user => getGlobalIDFromUserMetadataID(user._id)) |
||||
|
globalUsers = (await db.allDocs(getMultiIDParams(globalIds))).rows.map( |
||||
|
row => row.doc |
||||
|
) |
||||
|
} else { |
||||
|
globalUsers = ( |
||||
|
await db.allDocs( |
||||
|
getGlobalUserParams(null, { |
||||
|
include_docs: true, |
||||
|
}) |
||||
|
) |
||||
|
).rows.map(row => row.doc) |
||||
|
} |
||||
|
globalUsers = globalUsers |
||||
|
.filter(user => user != null) |
||||
|
.map(user => { |
||||
|
delete user.password |
||||
|
return user |
||||
|
}) |
||||
|
if (!appId) { |
||||
|
return globalUsers |
||||
|
} |
||||
|
return globalUsers.map(user => exports.updateAppRole(appId, user)) |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
<script> |
||||
|
import { Select } from "@budibase/bbui" |
||||
|
import { getContext } from "svelte" |
||||
|
import dayjs from "dayjs" |
||||
|
import utc from "dayjs/plugin/utc" |
||||
|
import { onMount } from "svelte" |
||||
|
|
||||
|
dayjs.extend(utc) |
||||
|
|
||||
|
export let dataProvider |
||||
|
export let field |
||||
|
export let defaultValue |
||||
|
|
||||
|
const dataContext = getContext("context") |
||||
|
const component = getContext("component") |
||||
|
const { styleable, builderStore, ActionTypes, getAction } = getContext("sdk") |
||||
|
|
||||
|
const setQuery = getAction(dataProvider?.id, ActionTypes.SetDataProviderQuery) |
||||
|
const options = [ |
||||
|
"Last 1 day", |
||||
|
"Last 7 days", |
||||
|
"Last 30 days", |
||||
|
"Last 3 months", |
||||
|
"Last 6 months", |
||||
|
"Last 1 year", |
||||
|
] |
||||
|
let value = options.includes(defaultValue) ? defaultValue : "Last 30 days" |
||||
|
|
||||
|
const updateDateRange = option => { |
||||
|
const query = dataProvider?.state?.query |
||||
|
if (!query || !setQuery) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
value = option |
||||
|
let low = dayjs.utc().subtract(1, "year") |
||||
|
let high = dayjs.utc().add(1, "day") |
||||
|
|
||||
|
if (option === "Last 1 day") { |
||||
|
low = dayjs.utc().subtract(1, "day") |
||||
|
} else if (option === "Last 7 days") { |
||||
|
low = dayjs.utc().subtract(7, "days") |
||||
|
} else if (option === "Last 30 days") { |
||||
|
low = dayjs.utc().subtract(30, "days") |
||||
|
} else if (option === "Last 3 months") { |
||||
|
low = dayjs.utc().subtract(3, "months") |
||||
|
} else if (option === "Last 6 months") { |
||||
|
low = dayjs.utc().subtract(6, "months") |
||||
|
} |
||||
|
|
||||
|
// Update data provider query with the new filter |
||||
|
setQuery({ |
||||
|
...query, |
||||
|
range: { |
||||
|
...query.range, |
||||
|
[field]: { |
||||
|
high: high.format(), |
||||
|
low: low.format(), |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Update the range on mount to the initial value |
||||
|
onMount(() => { |
||||
|
updateDateRange(value) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<div use:styleable={$component.styles}> |
||||
|
<Select |
||||
|
placeholder={null} |
||||
|
{options} |
||||
|
{value} |
||||
|
on:change={e => updateDateRange(e.detail)} |
||||
|
/> |
||||
|
</div> |
||||
@ -0,0 +1,146 @@ |
|||||
|
/** |
||||
|
* Builds a lucene JSON query from the filter structure generated in the builder |
||||
|
* @param filter the builder filter structure |
||||
|
*/ |
||||
|
export const buildLuceneQuery = filter => { |
||||
|
let query = { |
||||
|
string: {}, |
||||
|
fuzzy: {}, |
||||
|
range: {}, |
||||
|
equal: {}, |
||||
|
notEqual: {}, |
||||
|
empty: {}, |
||||
|
notEmpty: {}, |
||||
|
} |
||||
|
if (Array.isArray(filter)) { |
||||
|
filter.forEach(({ operator, field, type, value }) => { |
||||
|
if (operator.startsWith("range")) { |
||||
|
if (!query.range[field]) { |
||||
|
query.range[field] = { |
||||
|
low: |
||||
|
type === "number" |
||||
|
? Number.MIN_SAFE_INTEGER |
||||
|
: "0000-00-00T00:00:00.000Z", |
||||
|
high: |
||||
|
type === "number" |
||||
|
? Number.MAX_SAFE_INTEGER |
||||
|
: "9999-00-00T00:00:00.000Z", |
||||
|
} |
||||
|
} |
||||
|
if (operator === "rangeLow" && value != null && value !== "") { |
||||
|
query.range[field].low = value |
||||
|
} else if (operator === "rangeHigh" && value != null && value !== "") { |
||||
|
query.range[field].high = value |
||||
|
} |
||||
|
} else if (query[operator]) { |
||||
|
query[operator][field] = value |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
return query |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Performs a client-side lucene search on an array of data |
||||
|
* @param docs the data |
||||
|
* @param query the JSON lucene query |
||||
|
*/ |
||||
|
export const luceneQuery = (docs, query) => { |
||||
|
if (!query) { |
||||
|
return docs |
||||
|
} |
||||
|
|
||||
|
// Iterates over a set of filters and evaluates a fail function against a doc
|
||||
|
const match = (type, failFn) => doc => { |
||||
|
const filters = Object.entries(query[type] || {}) |
||||
|
for (let i = 0; i < filters.length; i++) { |
||||
|
if (failFn(filters[i][0], filters[i][1], doc)) { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Process a string match (fails if the value does not start with the string)
|
||||
|
const stringMatch = match("string", (key, value, doc) => { |
||||
|
return !doc[key] || !doc[key].startsWith(value) |
||||
|
}) |
||||
|
|
||||
|
// Process a range match
|
||||
|
const rangeMatch = match("range", (key, value, doc) => { |
||||
|
return !doc[key] || doc[key] < value.low || doc[key] > value.high |
||||
|
}) |
||||
|
|
||||
|
// Process an equal match (fails if the value is different)
|
||||
|
const equalMatch = match("equal", (key, value, doc) => { |
||||
|
return doc[key] !== value |
||||
|
}) |
||||
|
|
||||
|
// Process a not-equal match (fails if the value is the same)
|
||||
|
const notEqualMatch = match("notEqual", (key, value, doc) => { |
||||
|
return doc[key] === value |
||||
|
}) |
||||
|
|
||||
|
// Process an empty match (fails if the value is not empty)
|
||||
|
const emptyMatch = match("empty", (key, value, doc) => { |
||||
|
return doc[key] != null && doc[key] !== "" |
||||
|
}) |
||||
|
|
||||
|
// Process a not-empty match (fails is the value is empty)
|
||||
|
const notEmptyMatch = match("notEmpty", (key, value, doc) => { |
||||
|
return doc[key] == null || doc[key] === "" |
||||
|
}) |
||||
|
|
||||
|
// Match a document against all criteria
|
||||
|
const docMatch = doc => { |
||||
|
return ( |
||||
|
stringMatch(doc) && |
||||
|
rangeMatch(doc) && |
||||
|
equalMatch(doc) && |
||||
|
notEqualMatch(doc) && |
||||
|
emptyMatch(doc) && |
||||
|
notEmptyMatch(doc) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Process all docs
|
||||
|
return docs.filter(docMatch) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Performs a client-side sort from the equivalent server-side lucene sort |
||||
|
* parameters. |
||||
|
* @param docs the data |
||||
|
* @param sort the sort column |
||||
|
* @param sortOrder the sort order ("ascending" or "descending") |
||||
|
* @param sortType the type of sort ("string" or "number") |
||||
|
*/ |
||||
|
export const luceneSort = (docs, sort, sortOrder, sortType = "string") => { |
||||
|
if (!sort || !sortOrder || !sortType) { |
||||
|
return docs |
||||
|
} |
||||
|
const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x) |
||||
|
return docs.slice().sort((a, b) => { |
||||
|
const colA = parse(a[sort]) |
||||
|
const colB = parse(b[sort]) |
||||
|
if (sortOrder === "Descending") { |
||||
|
return colA > colB ? -1 : 1 |
||||
|
} else { |
||||
|
return colA > colB ? 1 : -1 |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Limits the specified docs to the specified number of rows from the equivalent |
||||
|
* server-side lucene limit parameters. |
||||
|
* @param docs the data |
||||
|
* @param limit the number of docs to limit to |
||||
|
*/ |
||||
|
export const luceneLimit = (docs, limit) => { |
||||
|
const numLimit = parseFloat(limit) |
||||
|
if (isNaN(numLimit)) { |
||||
|
return docs |
||||
|
} |
||||
|
return docs.slice(0, numLimit) |
||||
|
} |
||||
Loading…
Reference in new issue