Merge branch 'develop' into fix/BUDI-7258

This commit is contained in:
Michael Drury 2023-09-19 13:45:04 +01:00 committed by GitHub
commit 9e5b6bf654
18 changed files with 261 additions and 53 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.10.9-alpha.1", "version": "2.10.9-alpha.5",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -0,0 +1,145 @@
import { User } from "@budibase/types"
import { generator, structures } from "../../../tests"
import { DBTestConfiguration } from "../../../tests/extra"
import { getUsers } from "../user"
import { getGlobalDB } from "../../context"
import _ from "lodash"
import * as redis from "../../redis/init"
import { UserDB } from "../../users"
const config = new DBTestConfiguration()
describe("user cache", () => {
describe("getUsers", () => {
const users: User[] = []
beforeAll(async () => {
const userCount = 10
const userIds = generator.arrayOf(() => generator.guid(), {
min: userCount,
max: userCount,
})
await config.doInTenant(async () => {
const db = getGlobalDB()
for (const userId of userIds) {
const user = structures.users.user({ _id: userId })
await db.put(user)
users.push(user)
}
})
})
beforeEach(async () => {
jest.clearAllMocks()
const redisClient = await redis.getUserClient()
await redisClient.clear()
})
it("when no user is in cache, all of them are retrieved from db", async () => {
const usersToRequest = _.sampleSize(users, 5)
const userIdsToRequest = usersToRequest.map(x => x._id!)
jest.spyOn(UserDB, "bulkGet")
const results = await config.doInTenant(() => getUsers(userIdsToRequest))
expect(results.users).toHaveLength(5)
expect(results).toEqual({
users: usersToRequest.map(u => ({
...u,
budibaseAccess: true,
_rev: expect.any(String),
})),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest)
})
it("on a second all, all of them are retrieved from cache", async () => {
const usersToRequest = _.sampleSize(users, 5)
const userIdsToRequest = usersToRequest.map(x => x._id!)
jest.spyOn(UserDB, "bulkGet")
await config.doInTenant(() => getUsers(userIdsToRequest))
const resultsFromCache = await config.doInTenant(() =>
getUsers(userIdsToRequest)
)
expect(resultsFromCache.users).toHaveLength(5)
expect(resultsFromCache).toEqual({
users: expect.arrayContaining(
usersToRequest.map(u => ({
...u,
budibaseAccess: true,
_rev: expect.any(String),
}))
),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
})
it("when some users are cached, only the missing ones are retrieved from db", async () => {
const usersToRequest = _.sampleSize(users, 5)
const userIdsToRequest = usersToRequest.map(x => x._id!)
jest.spyOn(UserDB, "bulkGet")
await config.doInTenant(() =>
getUsers([userIdsToRequest[0], userIdsToRequest[3]])
)
;(UserDB.bulkGet as jest.Mock).mockClear()
const results = await config.doInTenant(() => getUsers(userIdsToRequest))
expect(results.users).toHaveLength(5)
expect(results).toEqual({
users: expect.arrayContaining(
usersToRequest.map(u => ({
...u,
budibaseAccess: true,
_rev: expect.any(String),
}))
),
})
expect(UserDB.bulkGet).toBeCalledTimes(1)
expect(UserDB.bulkGet).toBeCalledWith([
userIdsToRequest[1],
userIdsToRequest[2],
userIdsToRequest[4],
])
})
it("requesting existing and unexisting ids will return found ones", async () => {
const usersToRequest = _.sampleSize(users, 3)
const missingIds = [generator.guid(), generator.guid()]
const userIdsToRequest = _.shuffle([
...missingIds,
...usersToRequest.map(x => x._id!),
])
const results = await config.doInTenant(() => getUsers(userIdsToRequest))
expect(results.users).toHaveLength(3)
expect(results).toEqual({
users: expect.arrayContaining(
usersToRequest.map(u => ({
...u,
budibaseAccess: true,
_rev: expect.any(String),
}))
),
notFoundIds: expect.arrayContaining(missingIds),
})
})
})
})

View File

@ -6,6 +6,7 @@ import env from "../environment"
import * as accounts from "../accounts" import * as accounts from "../accounts"
import { UserDB } from "../users" import { UserDB } from "../users"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { User } from "@budibase/types"
const EXPIRY_SECONDS = 3600 const EXPIRY_SECONDS = 3600
@ -27,6 +28,35 @@ async function populateFromDB(userId: string, tenantId: string) {
return user return user
} }
async function populateUsersFromDB(
userIds: string[]
): Promise<{ users: User[]; notFoundIds?: string[] }> {
const getUsersResponse = await UserDB.bulkGet(userIds)
// Handle missed user ids
const notFoundIds = userIds.filter((uid, i) => !getUsersResponse[i])
const users = getUsersResponse.filter(x => x)
await Promise.all(
users.map(async (user: any) => {
user.budibaseAccess = true
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(user.email)
if (account) {
user.account = account
user.accountPortalAccess = true
}
}
})
)
if (notFoundIds.length) {
return { users, notFoundIds }
}
return { users }
}
/** /**
* Get the requested user by id. * Get the requested user by id.
* Use redis cache to first read the user. * Use redis cache to first read the user.
@ -77,6 +107,36 @@ export async function getUser(
return user return user
} }
/**
* Get the requested users by id.
* Use redis cache to first read the users.
* If not present fallback to loading the users directly and re-caching.
* @param {*} userIds the ids of the user to get
* @param {*} tenantId the tenant of the users to get
* @returns
*/
export async function getUsers(
userIds: string[]
): Promise<{ users: User[]; notFoundIds?: string[] }> {
const client = await redis.getUserClient()
// try cache
let usersFromCache = await client.bulkGet(userIds)
const missingUsersFromCache = userIds.filter(uid => !usersFromCache[uid])
const users = Object.values(usersFromCache)
let notFoundIds
if (missingUsersFromCache.length) {
const usersFromDb = await populateUsersFromDB(missingUsersFromCache)
notFoundIds = usersFromDb.notFoundIds
for (const userToCache of usersFromDb.users) {
await client.store(userToCache._id!, userToCache, EXPIRY_SECONDS)
}
users.push(...usersFromDb.users)
}
return { users, notFoundIds: notFoundIds }
}
export async function invalidateUser(userId: string) { export async function invalidateUser(userId: string) {
const client = await redis.getUserClient() const client = await redis.getUserClient()
await client.delete(userId) await client.delete(userId)

View File

@ -102,6 +102,7 @@ describe("sso", () => {
// modified external id to match user format // modified external id to match user format
ssoUser._id = "us_" + details.userId ssoUser._id = "us_" + details.userId
delete ssoUser.userId
// new sso user won't have a password // new sso user won't have a password
delete ssoUser.password delete ssoUser.password

View File

@ -250,7 +250,7 @@ class RedisWrapper {
const prefixedKeys = keys.map(key => addDbPrefix(db, key)) const prefixedKeys = keys.map(key => addDbPrefix(db, key))
let response = await this.getClient().mget(prefixedKeys) let response = await this.getClient().mget(prefixedKeys)
if (Array.isArray(response)) { if (Array.isArray(response)) {
let final: any = {} let final: Record<string, any> = {}
let count = 0 let count = 0
for (let result of response) { for (let result of response) {
if (result) { if (result) {

View File

@ -1,19 +0,0 @@
import { User } from "@budibase/types"
import { generator } from "./generator"
import { uuid } from "./common"
export const newEmail = () => {
return `${uuid()}@test.com`
}
export const user = (userProps?: any): User => {
return {
email: newEmail(),
password: "test",
roles: { app_test: "admin" },
firstName: generator.first(),
lastName: generator.last(),
pictureUrl: "http://test.com",
...userProps,
}
}

View File

@ -13,8 +13,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { generator } from "./generator" import { generator } from "./generator"
import { email, uuid } from "./common" import { email, uuid } from "./common"
import * as shared from "./shared" import * as users from "./users"
import { user } from "./shared"
import sample from "lodash/sample" import sample from "lodash/sample"
export function OAuth(): OAuth2 { export function OAuth(): OAuth2 {
@ -26,7 +25,7 @@ export function OAuth(): OAuth2 {
export function authDetails(userDoc?: User): SSOAuthDetails { export function authDetails(userDoc?: User): SSOAuthDetails {
if (!userDoc) { if (!userDoc) {
userDoc = user() userDoc = users.user()
} }
const userId = userDoc._id || uuid() const userId = userDoc._id || uuid()
@ -52,7 +51,7 @@ export function providerType(): SSOProviderType {
export function ssoProfile(user?: User): SSOProfile { export function ssoProfile(user?: User): SSOProfile {
if (!user) { if (!user) {
user = shared.user() user = users.user()
} }
return { return {
id: user._id!, id: user._id!,

View File

@ -4,11 +4,33 @@ import {
BuilderUser, BuilderUser,
SSOAuthDetails, SSOAuthDetails,
SSOUser, SSOUser,
User,
} from "@budibase/types" } from "@budibase/types"
import { user } from "./shared"
import { authDetails } from "./sso" import { authDetails } from "./sso"
import { uuid } from "./common"
import { generator } from "./generator"
import { tenant } from "."
import { generateGlobalUserID } from "../../../../src/docIds"
export { user, newEmail } from "./shared" export const newEmail = () => {
return `${uuid()}@test.com`
}
export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
const userId = userProps?._id || generateGlobalUserID()
return {
_id: userId,
userId,
email: newEmail(),
password: "test",
roles: { app_test: "admin" },
firstName: generator.first(),
lastName: generator.last(),
pictureUrl: "http://test.com",
tenantId: tenant.id(),
...userProps,
}
}
export const adminUser = (userProps?: any): AdminUser => { export const adminUser = (userProps?: any): AdminUser => {
return { return {

View File

@ -82,7 +82,7 @@
"@spectrum-css/typography": "3.0.1", "@spectrum-css/typography": "3.0.1",
"@spectrum-css/underlay": "2.0.9", "@spectrum-css/underlay": "2.0.9",
"@spectrum-css/vars": "3.0.1", "@spectrum-css/vars": "3.0.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.8",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"svelte-flatpickr": "3.2.3", "svelte-flatpickr": "3.2.3",
"svelte-portal": "^1.0.0", "svelte-portal": "^1.0.0",

View File

@ -126,8 +126,9 @@
transition: top 130ms ease-out, left 130ms ease-out; transition: top 130ms ease-out, left 130ms ease-out;
} }
.spectrum-Tooltip-label { .spectrum-Tooltip-label {
text-overflow: ellipsis; display: -webkit-box;
white-space: nowrap; -webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;

View File

@ -69,7 +69,7 @@
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"codemirror": "^5.59.0", "codemirror": "^5.59.0",
"dayjs": "^1.11.2", "dayjs": "^1.10.8",
"downloadjs": "1.4.7", "downloadjs": "1.4.7",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"lodash": "4.17.21", "lodash": "4.17.21",

View File

@ -502,7 +502,7 @@
</div> </div>
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"} {#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
<div> <div>
<div> <div class="row">
<Label>Time zones</Label> <Label>Time zones</Label>
<AbsTooltip <AbsTooltip
position="top" position="top"
@ -690,18 +690,19 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.tooltip-alignment { .tooltip-alignment {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: var(--spacing-xs);
} }
.label-length { .label-length {
flex-basis: 40%; flex-basis: 40%;
} }
.input-length { .input-length {
flex-grow: 1; flex-grow: 1;
} }
.row {
gap: 8px;
display: flex;
}
</style> </style>

View File

@ -33,7 +33,7 @@
"@spectrum-css/typography": "^3.0.2", "@spectrum-css/typography": "^3.0.2",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1", "apexcharts": "^3.22.1",
"dayjs": "^1.10.5", "dayjs": "^1.10.8",
"downloadjs": "1.4.7", "downloadjs": "1.4.7",
"html5-qrcode": "^2.2.1", "html5-qrcode": "^2.2.1",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",

View File

@ -8,7 +8,7 @@
"dependencies": { "dependencies": {
"@budibase/bbui": "0.0.0", "@budibase/bbui": "0.0.0",
"@budibase/shared-core": "0.0.0", "@budibase/shared-core": "0.0.0",
"dayjs": "^1.11.7", "dayjs": "^1.10.8",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"socket.io-client": "^4.6.1", "socket.io-client": "^4.6.1",
"svelte": "^3.46.2" "svelte": "^3.46.2"

View File

@ -1,5 +1,5 @@
<script> <script>
import { dayjs } from "dayjs" import dayjs from "dayjs"
import { CoreDatePicker, Icon } from "@budibase/bbui" import { CoreDatePicker, Icon } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"

View File

@ -158,6 +158,12 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
) { ) {
config.ssl.rejectUnauthorized = config.rejectUnauthorized config.ssl.rejectUnauthorized = config.rejectUnauthorized
} }
// The MySQL library we use doesn't directly document the parameters that can be passed in the ssl
// object, it instead points to an older library that it says it is mostly API compatible with, that
// older library actually documents what parameters can be passed in the ssl object.
// https://github.com/sidorares/node-mysql2#api-and-configuration
// https://github.com/mysqljs/mysql#ssl-options
// @ts-ignore // @ts-ignore
delete config.rejectUnauthorized delete config.rejectUnauthorized
this.config = { this.config = {

View File

@ -26,7 +26,7 @@
}, },
"dependencies": { "dependencies": {
"@budibase/handlebars-helpers": "^0.11.9", "@budibase/handlebars-helpers": "^0.11.9",
"dayjs": "^1.10.4", "dayjs": "^1.10.8",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"handlebars-utils": "^1.0.6", "handlebars-utils": "^1.0.6",
"lodash": "^4.17.20", "lodash": "^4.17.20",

View File

@ -6269,14 +6269,6 @@
"@types/tedious" "*" "@types/tedious" "*"
tarn "^3.0.1" tarn "^3.0.1"
"@types/node-fetch@2.6.1":
version "2.6.1"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node-fetch@2.6.4": "@types/node-fetch@2.6.4":
version "2.6.4" version "2.6.4"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660"
@ -6298,11 +6290,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==
"@types/node@14.18.20":
version "14.18.20"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650"
integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA==
"@types/node@16.9.1": "@types/node@16.9.1":
version "16.9.1" version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
@ -9792,11 +9779,16 @@ dateformat@^4.5.1, dateformat@^4.6.3:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
dayjs@^1.10.4, dayjs@^1.10.5, dayjs@^1.11.2, dayjs@^1.11.7: dayjs@^1.10.4, dayjs@^1.10.5:
version "1.11.7" version "1.11.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
dayjs@^1.10.8:
version "1.11.9"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a"
integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==
dd-trace@3.13.2: dd-trace@3.13.2:
version "3.13.2" version "3.13.2"
resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.13.2.tgz#95b1ec480ab9ac406e1da7591a8c6f678d3799fd" resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.13.2.tgz#95b1ec480ab9ac406e1da7591a8c6f678d3799fd"