Merge branch 'master' of github.com:Budibase/budibase into new-data-ui
This commit is contained in:
commit
d8b6d10dce
|
@ -240,7 +240,7 @@ jobs:
|
|||
|
||||
any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit)
|
||||
|
||||
if [ -n "$any_commit" ]; then
|
||||
if [ -n "$any_commit" ] && [ "$base_commit" != "$pro_commit" ]; then
|
||||
echo $any_commit
|
||||
|
||||
echo "An error occurred: <error_message>"
|
||||
|
|
|
@ -212,10 +212,6 @@ spec:
|
|||
- name: APP_FEATURES
|
||||
value: "api"
|
||||
{{- end }}
|
||||
{{- if .Values.globals.sqs.enabled }}
|
||||
- name: SQS_SEARCH_ENABLE
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- range .Values.services.apps.extraEnv }}
|
||||
- name: {{ .name }}
|
||||
value: {{ .value | quote }}
|
||||
|
|
|
@ -198,10 +198,6 @@ spec:
|
|||
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||
{{ end }}
|
||||
{{- if .Values.globals.sqs.enabled }}
|
||||
- name: SQS_SEARCH_ENABLE
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- range .Values.services.worker.extraEnv }}
|
||||
- name: {{ .name }}
|
||||
value: {{ .value | quote }}
|
||||
|
|
|
@ -329,7 +329,7 @@ brace-expansion@^1.1.7:
|
|||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.1, braces@~3.0.2:
|
||||
braces@^3.0.3, braces@~3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
|
@ -1201,12 +1201,12 @@ merge2@^1.3.0, merge2@^1.4.1:
|
|||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
|
||||
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
dependencies:
|
||||
braces "^3.0.1"
|
||||
picomatch "^2.2.3"
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
minimatch@^3.0.4, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
|
@ -1422,7 +1422,7 @@ picocolors@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
|
|
@ -29,7 +29,7 @@ services:
|
|||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||
SQS_SEARCH_ENABLE: 1
|
||||
TENANT_FEATURE_FLAGS: "*:SQS"
|
||||
depends_on:
|
||||
- worker-service
|
||||
- redis-service
|
||||
|
@ -57,7 +57,7 @@ services:
|
|||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
SQS_SEARCH_ENABLE: 1
|
||||
TENANT_FEATURE_FLAGS: "*:SQS"
|
||||
depends_on:
|
||||
- redis-service
|
||||
- minio-service
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "2.31.1",
|
||||
"version": "2.31.3",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 516b27b74cbcb7069a25f5e738dc91c22d7c4538
|
||||
Subproject commit c24374879d2b61516fabc24d7404e7da235be05e
|
|
@ -25,8 +25,8 @@ import { newid } from "../../docIds/newid"
|
|||
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
||||
import { DDInstrumentedDatabase } from "../instrumentation"
|
||||
import { checkSlashesInUrl } from "../../helpers"
|
||||
import env from "../../environment"
|
||||
import { sqlLog } from "../../sql/utils"
|
||||
import { flags } from "../../features"
|
||||
|
||||
const DATABASE_NOT_FOUND = "Database does not exist."
|
||||
|
||||
|
@ -401,7 +401,10 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
async destroy() {
|
||||
if (env.SQS_SEARCH_ENABLE && (await this.exists(SQLITE_DESIGN_DOC_ID))) {
|
||||
if (
|
||||
(await flags.isEnabled("SQS")) &&
|
||||
(await this.exists(SQLITE_DESIGN_DOC_ID))
|
||||
) {
|
||||
// delete the design document, then run the cleanup operation
|
||||
const definition = await this.get<SQLiteDefinition>(SQLITE_DESIGN_DOC_ID)
|
||||
// remove all tables - save the definition then trigger a cleanup
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
require("../../../tests")
|
||||
const { structures } = require("../../../tests")
|
||||
const { getDB } = require("../db")
|
||||
|
||||
describe("db", () => {
|
||||
describe("getDB", () => {
|
||||
it("returns a db", async () => {
|
||||
const dbName = structures.db.id()
|
||||
const db = getDB(dbName)
|
||||
expect(db).toBeDefined()
|
||||
expect(db.name).toBe(dbName)
|
||||
})
|
||||
|
||||
it("uses the custom put function", async () => {
|
||||
const db = getDB(structures.db.id())
|
||||
let doc = { _id: "test" }
|
||||
await db.put(doc)
|
||||
doc = await db.get(doc._id)
|
||||
expect(doc.createdAt).toBe(new Date().toISOString())
|
||||
expect(doc.updatedAt).toBe(new Date().toISOString())
|
||||
await db.destroy()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
import { doInTenant } from "../../context"
|
||||
import { structures } from "../../../tests"
|
||||
import { getDB } from "../db"
|
||||
|
||||
interface Doc {
|
||||
_id: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
describe("db", () => {
|
||||
describe("getDB", () => {
|
||||
it("returns a db", async () => {
|
||||
const dbName = structures.db.id()
|
||||
const db = getDB(dbName)
|
||||
expect(db).toBeDefined()
|
||||
expect(db.name).toBe(dbName)
|
||||
})
|
||||
|
||||
it("uses the custom put function", async () => {
|
||||
await doInTenant("foo", async () => {
|
||||
const db = getDB(structures.db.id())
|
||||
let doc: Doc = { _id: "test" }
|
||||
await db.put(doc)
|
||||
doc = await db.get(doc._id)
|
||||
expect(doc.createdAt).toBe(new Date().toISOString())
|
||||
expect(doc.updatedAt).toBe(new Date().toISOString())
|
||||
await db.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
import env from "../environment"
|
||||
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
|
||||
import { getTenantId, getGlobalDBName, isMultiTenant } from "../context"
|
||||
import { getTenantId, getGlobalDBName } from "../context"
|
||||
import { doWithDB, directCouchAllDbs } from "./db"
|
||||
import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
|
||||
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
|
||||
|
@ -206,34 +206,3 @@ export function pagination<T>(
|
|||
nextPage,
|
||||
}
|
||||
}
|
||||
|
||||
export function isSqsEnabledForTenant(): boolean {
|
||||
const tenantId = getTenantId()
|
||||
if (!env.SQS_SEARCH_ENABLE) {
|
||||
return false
|
||||
}
|
||||
|
||||
// single tenant (self host and dev) always enabled if flag set
|
||||
if (!isMultiTenant()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// This is to guard against the situation in tests where tests pass because
|
||||
// we're not actually using SQS, we're using Lucene and the tests pass due to
|
||||
// parity.
|
||||
if (env.isTest() && env.SQS_SEARCH_ENABLE_TENANTS.length === 0) {
|
||||
throw new Error(
|
||||
"to enable SQS you must specify a list of tenants in the SQS_SEARCH_ENABLE_TENANTS env var"
|
||||
)
|
||||
}
|
||||
|
||||
// Special case to enable all tenants, for testing in QA.
|
||||
if (
|
||||
env.SQS_SEARCH_ENABLE_TENANTS.length === 1 &&
|
||||
env.SQS_SEARCH_ENABLE_TENANTS[0] === "*"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return env.SQS_SEARCH_ENABLE_TENANTS.includes(tenantId)
|
||||
}
|
||||
|
|
|
@ -116,10 +116,6 @@ const environment = {
|
|||
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL,
|
||||
SQS_SEARCH_ENABLE: process.env.SQS_SEARCH_ENABLE,
|
||||
SQS_SEARCH_ENABLE_TENANTS:
|
||||
process.env.SQS_SEARCH_ENABLE_TENANTS?.split(",") || [],
|
||||
SQS_MIGRATION_ENABLE: process.env.SQS_MIGRATION_ENABLE,
|
||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
|
|
|
@ -6,7 +6,7 @@ import tracer from "dd-trace"
|
|||
|
||||
let posthog: PostHog | undefined
|
||||
export function init(opts?: PostHogOptions) {
|
||||
if (env.POSTHOG_TOKEN && env.POSTHOG_API_HOST) {
|
||||
if (env.POSTHOG_TOKEN && env.POSTHOG_API_HOST && !env.SELF_HOSTED) {
|
||||
console.log("initializing posthog client...")
|
||||
posthog = new PostHog(env.POSTHOG_TOKEN, {
|
||||
host: env.POSTHOG_API_HOST,
|
||||
|
@ -267,4 +267,5 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|||
// default values set correctly and their types flow through the system.
|
||||
export const flags = new FlagSet({
|
||||
DEFAULT_VALUES: Flag.boolean(false),
|
||||
SQS: Flag.boolean(env.isDev()),
|
||||
})
|
||||
|
|
|
@ -147,13 +147,13 @@ describe("feature flags", () => {
|
|||
}) => {
|
||||
const env: Partial<typeof environment> = {
|
||||
TENANT_FEATURE_FLAGS: environmentFlags,
|
||||
SELF_HOSTED: false,
|
||||
}
|
||||
|
||||
if (posthogFlags) {
|
||||
mockPosthogFlags(posthogFlags)
|
||||
env.POSTHOG_TOKEN = "test"
|
||||
env.POSTHOG_API_HOST = "https://us.i.posthog.com"
|
||||
env.POSTHOG_PERSONAL_TOKEN = "test"
|
||||
}
|
||||
|
||||
const ctx = { user: { license: { features: licenseFlags || [] } } }
|
||||
|
|
|
@ -15,7 +15,15 @@ export async function saveTenantInfo(tenantInfo: TenantInfo) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function getTenantInfo(tenantId: string): Promise<TenantInfo> {
|
||||
export async function getTenantInfo(
|
||||
tenantId: string
|
||||
): Promise<TenantInfo | undefined> {
|
||||
try {
|
||||
const db = getTenantDB(tenantId)
|
||||
return db.get("tenant_info")
|
||||
const tenantInfo = (await db.get("tenant_info")) as TenantInfo
|
||||
delete tenantInfo.owner.password
|
||||
return tenantInfo
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
export let inline = false
|
||||
export let disableCancel = false
|
||||
export let autoFocus = true
|
||||
export let zIndex = 999
|
||||
export let zIndex = 1001
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let visible = fixed || inline
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
export let onEdit
|
||||
export let allowSelectRows = false
|
||||
export let allowEditRows = false
|
||||
export let data
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if allowSelectRows}
|
||||
{#if allowSelectRows && data.__selectable !== false}
|
||||
<Checkbox value={selected} />
|
||||
{/if}
|
||||
{#if allowEditRows}
|
||||
|
|
|
@ -43,6 +43,8 @@
|
|||
export let showHeaderBorder = true
|
||||
export let placeholderText = "No rows found"
|
||||
export let snippets = []
|
||||
export let defaultSortColumn
|
||||
export let defaultSortOrder = "Ascending"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -162,6 +164,8 @@
|
|||
}
|
||||
|
||||
const sortRows = (rows, sortColumn, sortOrder) => {
|
||||
sortColumn = sortColumn ?? defaultSortColumn
|
||||
sortOrder = sortOrder ?? defaultSortOrder
|
||||
if (!sortColumn || !sortOrder || disableSorting) {
|
||||
return rows
|
||||
}
|
||||
|
@ -259,7 +263,10 @@
|
|||
if (select) {
|
||||
// Add any rows which are not already in selected rows
|
||||
rows.forEach(row => {
|
||||
if (selectedRows.findIndex(x => x._id === row._id) === -1) {
|
||||
if (
|
||||
row.__selectable !== false &&
|
||||
selectedRows.findIndex(x => x._id === row._id) === -1
|
||||
) {
|
||||
selectedRows.push(row)
|
||||
}
|
||||
})
|
||||
|
@ -396,6 +403,9 @@
|
|||
class:noBorderCheckbox={!showHeaderBorder}
|
||||
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||
on:click={e => {
|
||||
if (row.__selectable === false) {
|
||||
return
|
||||
}
|
||||
toggleSelectRow(row)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
|
|
|
@ -1065,7 +1065,12 @@
|
|||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<CodeEditorModal
|
||||
on:hide={() => {
|
||||
// Push any pending changes when the window closes
|
||||
onChange({ [key]: inputData[key] })
|
||||
}}
|
||||
>
|
||||
<div class:js-editor={editingJs}>
|
||||
<div
|
||||
class:js-code={editingJs}
|
||||
|
@ -1075,7 +1080,6 @@
|
|||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ [key]: e.detail })
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={stepCompletions}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<Modal bind:this={modal} on:hide>
|
||||
<ModalContent
|
||||
size="XL"
|
||||
title="Edit Code"
|
||||
|
|
|
@ -57,6 +57,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
const stateBindings = getStateBindings()
|
||||
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||
const roleBindings = getRoleBindings()
|
||||
const embedBindings = getEmbedBindings()
|
||||
return [
|
||||
...contextBindings,
|
||||
...urlBindings,
|
||||
|
@ -65,6 +66,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
...deviceBindings,
|
||||
...selectedRowsBindings,
|
||||
...roleBindings,
|
||||
...embedBindings,
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -813,6 +815,25 @@ export const getActionBindings = (actions, actionId) => {
|
|||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all device bindings for embeds.
|
||||
*/
|
||||
const getEmbedBindings = () => {
|
||||
let bindings = []
|
||||
const safeEmbed = makePropSafe("embed")
|
||||
|
||||
bindings = [
|
||||
{
|
||||
type: "context",
|
||||
runtimeBinding: `${safeEmbed}`,
|
||||
readableBinding: `ParentWindow`,
|
||||
category: "Embed",
|
||||
icon: "DistributeVertically",
|
||||
},
|
||||
]
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the schema for a certain datasource plus.
|
||||
* The options which can be passed in are:
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
let popoverAnchor
|
||||
let searchTerm = ""
|
||||
let popover
|
||||
let user
|
||||
let user, tenantOwner
|
||||
let loaded = false
|
||||
|
||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||
|
@ -104,6 +104,7 @@
|
|||
})
|
||||
})
|
||||
$: globalRole = users.getUserRole(user)
|
||||
$: isTenantOwner = tenantOwner?.email && tenantOwner.email === user?.email
|
||||
|
||||
const getAvailableApps = (appList, privileged, roles) => {
|
||||
let availableApps = appList.slice()
|
||||
|
@ -205,6 +206,7 @@
|
|||
if (!user?._id) {
|
||||
$goto("./")
|
||||
}
|
||||
tenantOwner = await users.tenantOwner($auth.tenantId)
|
||||
}
|
||||
|
||||
async function toggleFlags(detail) {
|
||||
|
@ -268,9 +270,11 @@
|
|||
Force password reset
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{#if !isTenantOwner}
|
||||
<MenuItem on:click={deleteModal.show} icon="Delete">
|
||||
Delete
|
||||
</MenuItem>
|
||||
{/if}
|
||||
</ActionMenu>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -310,9 +314,11 @@
|
|||
<Label size="L">Role</Label>
|
||||
<Select
|
||||
placeholder={null}
|
||||
disabled={!sdk.users.isAdmin($auth.user)}
|
||||
value={globalRole}
|
||||
options={Constants.BudibaseRoleOptions}
|
||||
disabled={!sdk.users.isAdmin($auth.user) || isTenantOwner}
|
||||
value={isTenantOwner ? "owner" : globalRole}
|
||||
options={isTenantOwner
|
||||
? Constants.ExtendedBudibaseRoleOptions
|
||||
: Constants.BudibaseRoleOptions}
|
||||
on:change={updateUserRole}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,15 @@
|
|||
|
||||
export let user
|
||||
|
||||
const password = Math.random().toString(36).slice(2, 20)
|
||||
const generatePassword = length => {
|
||||
const array = new Uint8Array(length)
|
||||
crypto.getRandomValues(array)
|
||||
return Array.from(array, byte => byte.toString(36).padStart(2, "0"))
|
||||
.join("")
|
||||
.slice(0, length)
|
||||
}
|
||||
|
||||
const password = generatePassword(12)
|
||||
|
||||
async function resetPassword() {
|
||||
try {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
export let row
|
||||
|
||||
$: role = Constants.BudibaseRoleOptions.find(
|
||||
$: role = Constants.ExtendedBudibaseRoleOptions.find(
|
||||
x => x.value === users.getUserRole(row)
|
||||
)
|
||||
$: value = role?.label || "Not available"
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
|
||||
let groupsLoaded = !$licensing.groupsEnabled || $groups?.length
|
||||
let enrichedUsers = []
|
||||
let tenantOwner
|
||||
let createUserModal,
|
||||
inviteConfirmationModal,
|
||||
onboardingTypeModal,
|
||||
|
@ -70,6 +71,7 @@
|
|||
]
|
||||
let userData = []
|
||||
let invitesLoaded = false
|
||||
let tenantOwnerLoaded = false
|
||||
let pendingInvites = []
|
||||
let parsedInvites = []
|
||||
|
||||
|
@ -98,8 +100,14 @@
|
|||
$: pendingSchema = getPendingSchema(schema)
|
||||
$: userData = []
|
||||
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
||||
$: {
|
||||
enrichedUsers = $fetch.rows?.map(user => {
|
||||
$: setEnrichedUsers($fetch.rows, tenantOwnerLoaded)
|
||||
|
||||
const setEnrichedUsers = async rows => {
|
||||
if (!tenantOwnerLoaded) {
|
||||
enrichedUsers = []
|
||||
return
|
||||
}
|
||||
enrichedUsers = rows?.map(user => {
|
||||
let userGroups = []
|
||||
$groups.forEach(group => {
|
||||
if (group.users) {
|
||||
|
@ -110,15 +118,21 @@
|
|||
})
|
||||
}
|
||||
})
|
||||
user.tenantOwnerEmail = tenantOwner?.email
|
||||
const role = Constants.ExtendedBudibaseRoleOptions.find(
|
||||
x => x.value === users.getUserRole(user)
|
||||
)
|
||||
return {
|
||||
...user,
|
||||
name: user.firstName ? user.firstName + " " + user.lastName : "",
|
||||
userGroups,
|
||||
__selectable:
|
||||
role.value === Constants.BudibaseRoles.Owner ? false : undefined,
|
||||
apps: [...new Set(Object.keys(user.roles))],
|
||||
access: role.sortOrder,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getPendingSchema = tblSchema => {
|
||||
if (!tblSchema) {
|
||||
return {}
|
||||
|
@ -302,6 +316,8 @@
|
|||
groupsLoaded = true
|
||||
pendingInvites = await users.getInvites()
|
||||
invitesLoaded = true
|
||||
tenantOwner = await users.tenantOwner($auth.tenantId)
|
||||
tenantOwnerLoaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching user group data")
|
||||
}
|
||||
|
@ -376,6 +392,7 @@
|
|||
allowSelectRows={!readonly}
|
||||
{customRenderers}
|
||||
loading={!$fetch.loaded || !groupsLoaded}
|
||||
defaultSortColumn={"access"}
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
|
|
|
@ -198,7 +198,7 @@ export const createLicensingStore = () => {
|
|||
}, {})
|
||||
}
|
||||
const monthlyMetrics = getMetrics(
|
||||
["dayPasses", "queries", "automations"],
|
||||
["queries", "automations"],
|
||||
license.quotas.usage.monthly,
|
||||
usage.monthly.current
|
||||
)
|
||||
|
|
|
@ -128,8 +128,15 @@ export function createUsersStore() {
|
|||
return await API.removeAppBuilder({ userId, appId })
|
||||
}
|
||||
|
||||
async function getTenantOwner(tenantId) {
|
||||
const tenantInfo = await API.getTenantInfo({ tenantId })
|
||||
return tenantInfo?.owner
|
||||
}
|
||||
|
||||
const getUserRole = user => {
|
||||
if (sdk.users.isAdmin(user)) {
|
||||
if (user && user.email === user.tenantOwnerEmail) {
|
||||
return Constants.BudibaseRoles.Owner
|
||||
} else if (sdk.users.isAdmin(user)) {
|
||||
return Constants.BudibaseRoles.Admin
|
||||
} else if (sdk.users.isBuilder(user)) {
|
||||
return Constants.BudibaseRoles.Developer
|
||||
|
@ -169,6 +176,7 @@ export function createUsersStore() {
|
|||
save: refreshUsage(save),
|
||||
bulkDelete: refreshUsage(bulkDelete),
|
||||
delete: refreshUsage(del),
|
||||
tenantOwner: getTenantOwner,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
import FreeFooter from "components/FreeFooter.svelte"
|
||||
import MaintenanceScreen from "components/MaintenanceScreen.svelte"
|
||||
import SnippetsProvider from "./context/SnippetsProvider.svelte"
|
||||
import EmbedProvider from "./context/EmbedProvider.svelte"
|
||||
|
||||
// Provide contexts
|
||||
setContext("sdk", SDK)
|
||||
|
@ -160,6 +161,7 @@
|
|||
{#if $environmentStore.maintenance.length > 0}
|
||||
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
|
||||
{:else}
|
||||
<EmbedProvider>
|
||||
<DeviceBindingsProvider>
|
||||
<UserBindingsProvider>
|
||||
<StateBindingsProvider>
|
||||
|
@ -212,7 +214,8 @@
|
|||
Something went wrong rendering your app
|
||||
</Heading>
|
||||
<Body size="S">
|
||||
Get in touch with support if this issue persists
|
||||
Get in touch with support if this issue
|
||||
persists
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
|
@ -270,6 +273,7 @@
|
|||
</StateBindingsProvider>
|
||||
</UserBindingsProvider>
|
||||
</DeviceBindingsProvider>
|
||||
</EmbedProvider>
|
||||
{/if}
|
||||
</div>
|
||||
<KeyboardManager />
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import Provider from "./Provider.svelte"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let data = {}
|
||||
|
||||
function extractDomainFromUrl(url) {
|
||||
const { hostname } = new URL(url)
|
||||
const parts = hostname.split(".")
|
||||
const tld = parts.slice(-2).join(".")
|
||||
return tld
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
if (event.data?.type !== "bb-parent-window-event") {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the event origin to ensure it's coming from a trusted source
|
||||
// Allow different subdomains but must match TLD
|
||||
const appOrigin = extractDomainFromUrl(window.location.origin)
|
||||
const eventOrigin = extractDomainFromUrl(event.origin)
|
||||
|
||||
if (appOrigin === eventOrigin) {
|
||||
data = event.data
|
||||
} else {
|
||||
console.error(
|
||||
`Embedded budibase app domain ${appOrigin} does not match origin of event ${eventOrigin}.
|
||||
Top level domains must match`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("message", handleMessage)
|
||||
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Provider key="embed" {data}>
|
||||
<slot />
|
||||
</Provider>
|
|
@ -295,4 +295,10 @@ export const buildUserEndpoints = API => ({
|
|||
url: `/api/global/users/${userId}/app/${appId}/builder`,
|
||||
})
|
||||
},
|
||||
|
||||
getTenantInfo: async ({ tenantId }) => {
|
||||
return await API.get({
|
||||
url: `/api/global/tenant/${tenantId}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -41,6 +41,7 @@ export const BudibaseRoles = {
|
|||
Developer: "developer",
|
||||
Creator: "creator",
|
||||
Admin: "admin",
|
||||
Owner: "owner",
|
||||
}
|
||||
|
||||
export const BudibaseRoleOptionsOld = [
|
||||
|
@ -54,18 +55,28 @@ export const BudibaseRoleOptions = [
|
|||
label: "Account admin",
|
||||
value: BudibaseRoles.Admin,
|
||||
subtitle: "Has full access to all apps and settings in your account",
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
label: "Creator",
|
||||
value: BudibaseRoles.Creator,
|
||||
subtitle: "Can create and edit apps they have access to",
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
label: "App user",
|
||||
value: BudibaseRoles.AppUser,
|
||||
subtitle: "Can only use published apps they have access to",
|
||||
sortOrder: 3,
|
||||
},
|
||||
]
|
||||
export const ExtendedBudibaseRoleOptions = [
|
||||
{
|
||||
label: "Account holder",
|
||||
value: BudibaseRoles.Owner,
|
||||
sortOrder: 0,
|
||||
},
|
||||
].concat(BudibaseRoleOptions)
|
||||
|
||||
export const PlanType = {
|
||||
FREE: "free",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 14c89a5b20ee4de07723063458a2437b0dd4f2c9
|
||||
Subproject commit a4d1d15d9ce6ac3deedb2e42625c90ba32756758
|
|
@ -47,7 +47,6 @@ async function init() {
|
|||
HTTP_LOGGING: "0",
|
||||
VERSION: "0.0.0+local",
|
||||
PASSWORD_MIN_LENGTH: "1",
|
||||
SQS_SEARCH_ENABLE: "1",
|
||||
}
|
||||
|
||||
config = { ...config, ...existingConfig }
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
import sdk from "../../sdk"
|
||||
import { builderSocket } from "../../websockets"
|
||||
import { isEqual } from "lodash"
|
||||
import { processTable } from "../../sdk/app/tables/getters"
|
||||
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
ctx.body = await sdk.datasources.fetch()
|
||||
|
@ -188,6 +189,7 @@ export async function update(
|
|||
for (let table of Object.values(datasource.entities)) {
|
||||
const oldTable = baseDatasource.entities?.[table.name]
|
||||
if (!oldTable || !isEqual(oldTable, table)) {
|
||||
table = await processTable(table)
|
||||
builderSocket?.emitTableUpdate(ctx, table, { includeOriginator: true })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export async function handleRequest<T extends Operation>(
|
|||
}
|
||||
|
||||
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
|
||||
const { _id, ...rowData } = ctx.request.body
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
|
@ -93,7 +93,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
|||
}
|
||||
|
||||
export async function destroy(ctx: UserCtx) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const _id = ctx.request.body._id
|
||||
const { row } = await handleRequest(Operation.DELETE, tableId, {
|
||||
id: breakRowIdField(_id),
|
||||
|
@ -104,7 +104,7 @@ export async function destroy(ctx: UserCtx) {
|
|||
|
||||
export async function bulkDestroy(ctx: UserCtx) {
|
||||
const { rows } = ctx.request.body
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
let promises: Promise<{ row: Row; table: Table }>[] = []
|
||||
for (let row of rows) {
|
||||
promises.push(
|
||||
|
@ -123,7 +123,7 @@ export async function bulkDestroy(ctx: UserCtx) {
|
|||
|
||||
export async function fetchEnrichedRow(ctx: UserCtx) {
|
||||
const id = ctx.params.rowId
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||
const datasource: Datasource = await sdk.datasources.get(datasourceId)
|
||||
if (!datasource || !datasource.entities) {
|
||||
|
|
|
@ -47,7 +47,7 @@ export async function patch(
|
|||
ctx: UserCtx<PatchRowRequest, PatchRowResponse>
|
||||
): Promise<any> {
|
||||
const appId = ctx.appId
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const body = ctx.request.body
|
||||
|
||||
// if it doesn't have an _id then its save
|
||||
|
@ -72,7 +72,7 @@ export async function patch(
|
|||
|
||||
export const save = async (ctx: UserCtx<Row, Row>) => {
|
||||
const appId = ctx.appId
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const body = ctx.request.body
|
||||
|
||||
// user metadata doesn't exist yet - don't allow creation
|
||||
|
@ -97,13 +97,12 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
|
|||
gridSocket?.emitRowUpdate(ctx, row || squashed)
|
||||
}
|
||||
|
||||
export async function fetchView(ctx: any) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
export async function fetchLegacyView(ctx: any) {
|
||||
const viewName = decodeURIComponent(ctx.params.viewName)
|
||||
|
||||
const { calculation, group, field } = ctx.query
|
||||
|
||||
ctx.body = await sdk.rows.fetchView(tableId, viewName, {
|
||||
ctx.body = await sdk.rows.fetchLegacyView(viewName, {
|
||||
calculation,
|
||||
group: calculation ? group : null,
|
||||
field,
|
||||
|
@ -111,12 +110,12 @@ export async function fetchView(ctx: any) {
|
|||
}
|
||||
|
||||
export async function fetch(ctx: any) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
ctx.body = await sdk.rows.fetch(tableId)
|
||||
}
|
||||
|
||||
export async function find(ctx: UserCtx<void, GetRowResponse>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const rowId = ctx.params.rowId
|
||||
|
||||
ctx.body = await sdk.rows.find(tableId, rowId)
|
||||
|
@ -132,7 +131,7 @@ function isDeleteRow(input: any): input is DeleteRow {
|
|||
|
||||
async function processDeleteRowsRequest(ctx: UserCtx<DeleteRowRequest>) {
|
||||
let request = ctx.request.body as DeleteRows
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
|
||||
const processedRows = request.rows.map(row => {
|
||||
let processedRow: Row = typeof row == "string" ? { _id: row } : row
|
||||
|
@ -148,7 +147,7 @@ async function processDeleteRowsRequest(ctx: UserCtx<DeleteRowRequest>) {
|
|||
}
|
||||
|
||||
async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const appId = ctx.appId
|
||||
|
||||
let deleteRequest = ctx.request.body as DeleteRows
|
||||
|
@ -170,7 +169,7 @@ async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
|
|||
|
||||
async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
|
||||
const appId = ctx.appId
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
|
||||
const resp = await pickApi(tableId).destroy(ctx)
|
||||
if (!tableId.includes("datasource_plus")) {
|
||||
|
@ -204,7 +203,7 @@ export async function destroy(ctx: UserCtx<DeleteRowRequest>) {
|
|||
}
|
||||
|
||||
export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
|
||||
await context.ensureSnippetContext(true)
|
||||
|
||||
|
@ -226,7 +225,7 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
|
|||
}
|
||||
|
||||
export async function validate(ctx: Ctx<Row, ValidateResponse>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
// external tables are hard to validate currently
|
||||
if (isExternalTableID(tableId)) {
|
||||
ctx.body = { valid: true, errors: {} }
|
||||
|
@ -239,14 +238,14 @@ export async function validate(ctx: Ctx<Row, ValidateResponse>) {
|
|||
}
|
||||
|
||||
export async function fetchEnrichedRow(ctx: UserCtx<void, Row>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
ctx.body = await pickApi(tableId).fetchEnrichedRow(ctx)
|
||||
}
|
||||
|
||||
export const exportRows = async (
|
||||
ctx: Ctx<ExportRowsRequest, ExportRowsResponse>
|
||||
) => {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
|
||||
const format = ctx.query.format
|
||||
|
||||
|
@ -279,7 +278,7 @@ export const exportRows = async (
|
|||
export async function downloadAttachment(ctx: UserCtx) {
|
||||
const { columnName } = ctx.params
|
||||
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const rowId = ctx.params.rowId
|
||||
const row = await sdk.rows.find(tableId, rowId)
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
PatchRowRequest,
|
||||
PatchRowResponse,
|
||||
Row,
|
||||
Table,
|
||||
UserCtx,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
|
@ -24,7 +23,7 @@ import { getLinkedTableIDs } from "../../../db/linkedRows/linkUtils"
|
|||
import { flatten } from "lodash"
|
||||
|
||||
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const inputs = ctx.request.body
|
||||
const isUserTable = tableId === InternalTables.USER_METADATA
|
||||
let oldRow
|
||||
|
@ -98,7 +97,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
|||
|
||||
export async function destroy(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const { _id } = ctx.request.body
|
||||
let row = await db.get<Row>(_id)
|
||||
let _rev = ctx.request.body._rev || row._rev
|
||||
|
@ -137,7 +136,7 @@ export async function destroy(ctx: UserCtx) {
|
|||
}
|
||||
|
||||
export async function bulkDestroy(ctx: UserCtx) {
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
let { rows } = ctx.request.body
|
||||
|
||||
|
@ -179,7 +178,7 @@ export async function bulkDestroy(ctx: UserCtx) {
|
|||
export async function fetchEnrichedRow(ctx: UserCtx) {
|
||||
const fieldName = ctx.request.query.field as string | undefined
|
||||
const db = context.getAppDB()
|
||||
const tableId = utils.getTableId(ctx)
|
||||
const { tableId } = utils.getSourceId(ctx)
|
||||
const rowId = ctx.params.rowId as string
|
||||
// need table to work out where links go in row, as well as the link docs
|
||||
const [table, links] = await Promise.all([
|
||||
|
@ -197,7 +196,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
|
|||
)
|
||||
|
||||
// get the linked tables
|
||||
const linkTableIds = getLinkedTableIDs(table as Table)
|
||||
const linkTableIds = getLinkedTableIDs(table.schema)
|
||||
const linkTables = await sdk.tables.getTables(linkTableIds)
|
||||
|
||||
// perform output processing
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { InternalTables } from "../../../../db/utils"
|
||||
import * as utils from "../../../../db/utils"
|
||||
|
||||
import { context } from "@budibase/backend-core"
|
||||
import {
|
||||
|
@ -67,7 +67,7 @@ export async function findRow(tableId: string, rowId: string) {
|
|||
const db = context.getAppDB()
|
||||
let row: Row
|
||||
// TODO remove special user case in future
|
||||
if (tableId === InternalTables.USER_METADATA) {
|
||||
if (tableId === utils.InternalTables.USER_METADATA) {
|
||||
row = await getFullUser(rowId)
|
||||
} else {
|
||||
row = await db.get(rowId)
|
||||
|
@ -78,22 +78,25 @@ export async function findRow(tableId: string, rowId: string) {
|
|||
return row
|
||||
}
|
||||
|
||||
export function getTableId(ctx: Ctx): string {
|
||||
export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
|
||||
// top priority, use the URL first
|
||||
if (ctx.params?.sourceId) {
|
||||
return ctx.params.sourceId
|
||||
const { sourceId } = ctx.params
|
||||
if (utils.isViewID(sourceId)) {
|
||||
return {
|
||||
tableId: utils.extractViewInfoFromID(sourceId).tableId,
|
||||
viewId: sourceId,
|
||||
}
|
||||
}
|
||||
return { tableId: ctx.params.sourceId }
|
||||
}
|
||||
// now check for old way of specifying table ID
|
||||
if (ctx.params?.tableId) {
|
||||
return ctx.params.tableId
|
||||
return { tableId: ctx.params.tableId }
|
||||
}
|
||||
// check body for a table ID
|
||||
if (ctx.request.body?.tableId) {
|
||||
return ctx.request.body.tableId
|
||||
}
|
||||
// now check if a specific view name
|
||||
if (ctx.params?.viewName) {
|
||||
return ctx.params.viewName
|
||||
return { tableId: ctx.request.body.tableId }
|
||||
}
|
||||
throw new Error("Unable to find table ID in request")
|
||||
}
|
||||
|
@ -198,7 +201,7 @@ export async function sqlOutputProcessing(
|
|||
}
|
||||
|
||||
export function isUserMetadataTable(tableId: string) {
|
||||
return tableId === InternalTables.USER_METADATA
|
||||
return tableId === utils.InternalTables.USER_METADATA
|
||||
}
|
||||
|
||||
export async function enrichArrayContext(
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { db, context } from "@budibase/backend-core"
|
||||
import { db, context, features } from "@budibase/backend-core"
|
||||
import { enrichSearchContext } from "./utils"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
|
||||
|
@ -40,7 +40,10 @@ export async function searchView(
|
|||
// Delete extraneous search params that cannot be overridden
|
||||
delete body.query.onEmptyFilter
|
||||
|
||||
if (!isExternalTableID(view.tableId) && !db.isSqsEnabledForTenant()) {
|
||||
if (
|
||||
!isExternalTableID(view.tableId) &&
|
||||
!(await features.flags.isEnabled("SQS"))
|
||||
) {
|
||||
// Extract existing fields
|
||||
const existingFields =
|
||||
view.query
|
||||
|
@ -56,13 +59,14 @@ export async function searchView(
|
|||
}
|
||||
})
|
||||
})
|
||||
} else
|
||||
} else {
|
||||
query = {
|
||||
$and: {
|
||||
conditions: [query, body.query],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await context.ensureSnippetContext(true)
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
PROTECTED_EXTERNAL_COLUMNS,
|
||||
PROTECTED_INTERNAL_COLUMNS,
|
||||
} from "@budibase/shared-core"
|
||||
import { processTable } from "../../../sdk/app/tables/getters"
|
||||
|
||||
function pickApi({ tableId, table }: { tableId?: string; table?: Table }) {
|
||||
if (table && isExternalTable(table)) {
|
||||
|
@ -118,6 +119,8 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
|||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitTable(`table:save`, appId, { ...savedTable })
|
||||
ctx.body = savedTable
|
||||
|
||||
savedTable = await processTable(savedTable)
|
||||
builderSocket?.emitTableUpdate(ctx, cloneDeep(savedTable))
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { getViews, saveView } from "../view/utils"
|
|||
import viewTemplate from "../view/viewBuilder"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { events, context, db as dbCore } from "@budibase/backend-core"
|
||||
import { events, context, features } from "@budibase/backend-core"
|
||||
import {
|
||||
AutoFieldSubType,
|
||||
ContextUser,
|
||||
|
@ -332,7 +332,7 @@ class TableSaveFunctions {
|
|||
importRows: this.importRows,
|
||||
user: this.user,
|
||||
})
|
||||
if (dbCore.isSqsEnabledForTenant()) {
|
||||
if (await features.flags.isEnabled("SQS")) {
|
||||
await sdk.tables.sqs.addTable(table)
|
||||
}
|
||||
return table
|
||||
|
@ -526,7 +526,7 @@ export async function internalTableCleanup(table: Table, rows?: Row[]) {
|
|||
if (rows) {
|
||||
await AttachmentCleanup.tableDelete(table, rows)
|
||||
}
|
||||
if (dbCore.isSqsEnabledForTenant()) {
|
||||
if (await features.flags.isEnabled("SQS")) {
|
||||
await sdk.tables.sqs.removeTable(table)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import viewTemplate from "./viewBuilder"
|
|||
import { apiFileReturn } from "../../../utilities/fileSystem"
|
||||
import { csv, json, jsonWithSchema, Format, isFormat } from "./exporters"
|
||||
import { deleteView, getView, getViews, saveView } from "./utils"
|
||||
import { fetchView } from "../row"
|
||||
import { fetchLegacyView } from "../row"
|
||||
import { context, events } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
import {
|
||||
|
@ -170,7 +170,7 @@ export async function exportView(ctx: Ctx) {
|
|||
ctx.params.viewName = viewName
|
||||
}
|
||||
|
||||
await fetchView(ctx)
|
||||
await fetchLegacyView(ctx)
|
||||
let rows = ctx.body as Row[]
|
||||
|
||||
let schema: TableSchema = view && view.meta && view.meta.schema
|
||||
|
|
|
@ -352,13 +352,13 @@ describe("/applications", () => {
|
|||
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should be able to delete an app after SQS_SEARCH_ENABLE has been set but app hasn't been migrated", async () => {
|
||||
it("should be able to delete an app after SQS has been set but app hasn't been migrated", async () => {
|
||||
const prodAppId = app.appId.replace("_dev", "")
|
||||
nock("http://localhost:10000")
|
||||
.delete(`/api/global/roles/${prodAppId}`)
|
||||
.reply(200, {})
|
||||
|
||||
await withCoreEnv({ SQS_SEARCH_ENABLE: "true" }, async () => {
|
||||
await withCoreEnv({ TENANT_FEATURE_FLAGS: "*:SQS" }, async () => {
|
||||
await config.api.application.delete(app.appId)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -159,7 +159,7 @@ describe("run misc tests", () => {
|
|||
const rowThree = rows.find(row => row.e === 3)
|
||||
expect(rowThree.a).toEqual("9")
|
||||
expect(rowThree.f).toEqual(["Two", "Four"])
|
||||
expect(rowThree.g).toEqual(null)
|
||||
expect(rowThree.g).toEqual(undefined)
|
||||
|
||||
const rowFour = rows.find(row => row.e === 4)
|
||||
expect(rowFour.a).toEqual("13")
|
||||
|
|
|
@ -9,7 +9,13 @@ import {
|
|||
import tk from "timekeeper"
|
||||
import emitter from "../../../../src/events"
|
||||
import { outputProcessing } from "../../../utilities/rowProcessor"
|
||||
import { context, InternalTable, tenancy } from "@budibase/backend-core"
|
||||
import {
|
||||
context,
|
||||
InternalTable,
|
||||
tenancy,
|
||||
withEnv as withCoreEnv,
|
||||
setEnv as setCoreEnv,
|
||||
} from "@budibase/backend-core"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import {
|
||||
AttachmentFieldMetadata,
|
||||
|
@ -69,6 +75,7 @@ async function waitForEvent(
|
|||
|
||||
describe.each([
|
||||
["internal", undefined],
|
||||
["sqs", undefined],
|
||||
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
|
||||
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
|
||||
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
|
||||
|
@ -76,6 +83,8 @@ describe.each([
|
|||
[DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)],
|
||||
])("/rows (%s)", (providerType, dsProvider) => {
|
||||
const isInternal = dsProvider === undefined
|
||||
const isLucene = providerType === "lucene"
|
||||
const isSqs = providerType === "sqs"
|
||||
const isMSSQL = providerType === DatabaseName.SQL_SERVER
|
||||
const isOracle = providerType === DatabaseName.ORACLE
|
||||
const config = setup.getConfig()
|
||||
|
@ -83,9 +92,16 @@ describe.each([
|
|||
let table: Table
|
||||
let datasource: Datasource | undefined
|
||||
let client: Knex | undefined
|
||||
let envCleanup: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
await withCoreEnv({ TENANT_FEATURE_FLAGS: "*:SQS" }, () => config.init())
|
||||
if (isLucene) {
|
||||
envCleanup = setCoreEnv({ TENANT_FEATURE_FLAGS: "*:!SQS" })
|
||||
} else if (isSqs) {
|
||||
envCleanup = setCoreEnv({ TENANT_FEATURE_FLAGS: "*:SQS" })
|
||||
}
|
||||
|
||||
if (dsProvider) {
|
||||
const rawDatasource = await dsProvider
|
||||
datasource = await config.createDatasource({
|
||||
|
@ -97,6 +113,9 @@ describe.each([
|
|||
|
||||
afterAll(async () => {
|
||||
setup.afterAll()
|
||||
if (envCleanup) {
|
||||
envCleanup()
|
||||
}
|
||||
})
|
||||
|
||||
function saveTableRequest(
|
||||
|
@ -346,7 +365,7 @@ describe.each([
|
|||
expect(ids).toEqual(expect.arrayContaining(sequence))
|
||||
})
|
||||
|
||||
isInternal &&
|
||||
isLucene &&
|
||||
it("row values are coerced", async () => {
|
||||
const str: FieldSchema = {
|
||||
type: FieldType.STRING,
|
||||
|
|
|
@ -67,11 +67,14 @@ describe.each([
|
|||
let rows: Row[]
|
||||
|
||||
beforeAll(async () => {
|
||||
await withCoreEnv({ SQS_SEARCH_ENABLE: "true" }, () => config.init())
|
||||
if (isSqs) {
|
||||
await withCoreEnv({ TENANT_FEATURE_FLAGS: "*:SQS" }, () => config.init())
|
||||
if (isLucene) {
|
||||
envCleanup = setCoreEnv({
|
||||
SQS_SEARCH_ENABLE: "true",
|
||||
SQS_SEARCH_ENABLE_TENANTS: [config.getTenantId()],
|
||||
TENANT_FEATURE_FLAGS: "*:!SQS",
|
||||
})
|
||||
} else if (isSqs) {
|
||||
envCleanup = setCoreEnv({
|
||||
TENANT_FEATURE_FLAGS: "*:SQS",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ const { basicTable } = setup.structures
|
|||
const ISO_REGEX_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
||||
|
||||
describe.each([
|
||||
["internal", undefined],
|
||||
["sqs", undefined],
|
||||
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
|
||||
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
|
||||
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
|
||||
|
@ -290,7 +290,7 @@ describe.each([
|
|||
expected._rev = expect.stringMatching(/^2-.+/)
|
||||
}
|
||||
|
||||
expect(updatedTable).toEqual(expected)
|
||||
expect(updatedTable).toEqual(expect.objectContaining(expected))
|
||||
|
||||
const persistedTable = await config.api.table.get(updatedTable._id!)
|
||||
expected = {
|
||||
|
@ -304,7 +304,7 @@ describe.each([
|
|||
if (isInternal) {
|
||||
expected._rev = expect.stringMatching(/^2-.+/)
|
||||
}
|
||||
expect(persistedTable).toEqual(expected)
|
||||
expect(persistedTable).toEqual(expect.objectContaining(expected))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -687,7 +687,7 @@ describe.each([
|
|||
basicTable(datasource, { name: generator.guid() })
|
||||
)
|
||||
const res = await config.api.table.get(table._id!)
|
||||
expect(res).toEqual(table)
|
||||
expect(res).toEqual(expect.objectContaining(table))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -727,10 +727,12 @@ describe.each([
|
|||
body: { message: `Table ${testTable._id} deleted.` },
|
||||
})
|
||||
expect(events.table.deleted).toHaveBeenCalledTimes(1)
|
||||
expect(events.table.deleted).toHaveBeenCalledWith({
|
||||
expect(events.table.deleted).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...testTable,
|
||||
tableId: testTable._id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("deletes linked references to the table after deletion", async () => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as setup from "./utilities"
|
|||
import path from "path"
|
||||
import nock from "nock"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
import { withEnv as withCoreEnv } from "@budibase/backend-core"
|
||||
import { withEnv as withCoreEnv, env as coreEnv } from "@budibase/backend-core"
|
||||
|
||||
interface App {
|
||||
background: string
|
||||
|
@ -85,9 +85,8 @@ describe("/templates", () => {
|
|||
it.each(["sqs", "lucene"])(
|
||||
`should be able to create an app from a template (%s)`,
|
||||
async source => {
|
||||
const env = {
|
||||
SQS_SEARCH_ENABLE: source === "sqs" ? "true" : "false",
|
||||
SQS_SEARCH_ENABLE_TENANTS: [config.getTenantId()],
|
||||
const env: Partial<typeof coreEnv> = {
|
||||
TENANT_FEATURE_FLAGS: source === "sqs" ? "*:SQS" : "",
|
||||
}
|
||||
|
||||
await withCoreEnv(env, async () => {
|
||||
|
|
|
@ -444,7 +444,10 @@ describe("/views", () => {
|
|||
|
||||
assertJsonExport(res)
|
||||
expect(events.table.exported).toHaveBeenCalledTimes(1)
|
||||
expect(events.table.exported).toHaveBeenCalledWith(table, "json")
|
||||
expect(events.table.exported).toHaveBeenCalledWith(
|
||||
expect.objectContaining(table),
|
||||
"json"
|
||||
)
|
||||
})
|
||||
|
||||
it("should be able to export a table as CSV", async () => {
|
||||
|
@ -454,7 +457,10 @@ describe("/views", () => {
|
|||
|
||||
assertCSVExport(res)
|
||||
expect(events.table.exported).toHaveBeenCalledTimes(1)
|
||||
expect(events.table.exported).toHaveBeenCalledWith(table, "csv")
|
||||
expect(events.table.exported).toHaveBeenCalledWith(
|
||||
expect.objectContaining(table),
|
||||
"csv"
|
||||
)
|
||||
})
|
||||
|
||||
it("should be able to export a view as JSON", async () => {
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
Table,
|
||||
TableSourceType,
|
||||
UpdateViewRequest,
|
||||
ViewUIFieldMetadata,
|
||||
ViewV2,
|
||||
SearchResponse,
|
||||
BasicOperator,
|
||||
|
@ -95,13 +94,16 @@ describe.each([
|
|||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await withCoreEnv({ SQS_SEARCH_ENABLE: isSqs ? "true" : "false" }, () =>
|
||||
await withCoreEnv({ TENANT_FEATURE_FLAGS: isSqs ? "*:SQS" : "" }, () =>
|
||||
config.init()
|
||||
)
|
||||
if (isSqs) {
|
||||
if (isLucene) {
|
||||
envCleanup = setCoreEnv({
|
||||
SQS_SEARCH_ENABLE: "true",
|
||||
SQS_SEARCH_ENABLE_TENANTS: [config.getTenantId()],
|
||||
TENANT_FEATURE_FLAGS: "*:!SQS",
|
||||
})
|
||||
} else if (isSqs) {
|
||||
envCleanup = setCoreEnv({
|
||||
TENANT_FEATURE_FLAGS: "*:SQS",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -125,18 +127,7 @@ describe.each([
|
|||
mocks.licenses.useCloudFree()
|
||||
})
|
||||
|
||||
const getRowUsage = async () => {
|
||||
const { total } = await config.doInContext(undefined, () =>
|
||||
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
|
||||
)
|
||||
return total
|
||||
}
|
||||
|
||||
const assertRowUsage = async (expected: number) => {
|
||||
const usage = await getRowUsage()
|
||||
expect(usage).toBe(expected)
|
||||
}
|
||||
|
||||
describe("view crud", () => {
|
||||
describe("create", () => {
|
||||
it("persist the view when the view is successfully created", async () => {
|
||||
const newView: CreateViewRequest = {
|
||||
|
@ -584,7 +575,9 @@ describe.each([
|
|||
expect((await config.api.table.get(tableId)).views).toEqual({
|
||||
[view.name]: {
|
||||
...view,
|
||||
query: [{ operator: "equal", field: "newField", value: "thatValue" }],
|
||||
query: [
|
||||
{ operator: "equal", field: "newField", value: "thatValue" },
|
||||
],
|
||||
schema: expect.anything(),
|
||||
},
|
||||
})
|
||||
|
@ -886,7 +879,8 @@ describe.each([
|
|||
|
||||
// Update the view to an invalid state
|
||||
const tableToUpdate = await config.api.table.get(table._id!)
|
||||
;(tableToUpdate.views![view.name] as ViewV2).schema!.id.visible = false
|
||||
;(tableToUpdate.views![view.name] as ViewV2).schema!.id.visible =
|
||||
false
|
||||
await db.getDB(config.appId!).put(tableToUpdate)
|
||||
|
||||
view = await config.api.viewV2.get(view.id)
|
||||
|
@ -937,26 +931,114 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
describe("fetch view (through table)", () => {
|
||||
it("should be able to fetch a view V2", async () => {
|
||||
const res = await config.api.viewV2.create({
|
||||
name: generator.name(),
|
||||
tableId: table._id!,
|
||||
describe.each([
|
||||
["from view api", (view: ViewV2) => config.api.viewV2.get(view.id)],
|
||||
[
|
||||
"from table",
|
||||
async (view: ViewV2) => {
|
||||
const table = await config.api.table.get(view.tableId)
|
||||
return table.views![view.name] as ViewV2
|
||||
},
|
||||
],
|
||||
])("read (%s)", (_, getDelegate) => {
|
||||
let table: Table
|
||||
let tableId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
one: {
|
||||
type: FieldType.STRING,
|
||||
name: "one",
|
||||
},
|
||||
two: {
|
||||
type: FieldType.STRING,
|
||||
name: "two",
|
||||
},
|
||||
three: {
|
||||
type: FieldType.STRING,
|
||||
name: "three",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
tableId = table._id!
|
||||
})
|
||||
|
||||
it("retrieves the view data with the enriched schema", async () => {
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
Price: { visible: false },
|
||||
Category: { visible: true },
|
||||
one: { visible: true },
|
||||
two: { visible: true },
|
||||
},
|
||||
})
|
||||
|
||||
const view = await config.api.viewV2.get(res.id)
|
||||
const updatedTable = await config.api.table.get(table._id!)
|
||||
const viewSchema = updatedTable.views![view!.name!].schema as Record<
|
||||
string,
|
||||
ViewUIFieldMetadata
|
||||
>
|
||||
expect(viewSchema.Price?.visible).toEqual(false)
|
||||
expect(viewSchema.Category?.visible).toEqual(true)
|
||||
expect(await getDelegate(view)).toEqual({
|
||||
...view,
|
||||
schema: {
|
||||
id: { ...table.schema["id"], visible: true },
|
||||
one: { ...table.schema["one"], visible: true },
|
||||
two: { ...table.schema["two"], visible: true },
|
||||
three: { ...table.schema["three"], visible: false },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("does not include columns removed from the table", async () => {
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
one: { visible: true },
|
||||
two: { visible: true },
|
||||
},
|
||||
})
|
||||
const table = await config.api.table.get(tableId)
|
||||
const { one: _, ...newSchema } = table.schema
|
||||
await config.api.table.save({ ...table, schema: newSchema })
|
||||
|
||||
expect(await getDelegate(view)).toEqual({
|
||||
...view,
|
||||
schema: {
|
||||
id: { ...table.schema["id"], visible: true },
|
||||
two: { ...table.schema["two"], visible: true },
|
||||
three: { ...table.schema["three"], visible: false },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("does not include columns hidden from the table", async () => {
|
||||
const view = await config.api.viewV2.create({
|
||||
tableId,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
one: { visible: true },
|
||||
two: { visible: true },
|
||||
},
|
||||
})
|
||||
const table = await config.api.table.get(tableId)
|
||||
await config.api.table.save({
|
||||
...table,
|
||||
schema: {
|
||||
...table.schema,
|
||||
two: { ...table.schema["two"], visible: false },
|
||||
},
|
||||
})
|
||||
|
||||
expect(await getDelegate(view)).toEqual({
|
||||
...view,
|
||||
schema: {
|
||||
id: { ...table.schema["id"], visible: true },
|
||||
one: { ...table.schema["one"], visible: true },
|
||||
three: { ...table.schema["three"], visible: false },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to fetch readonly config after downgrades", async () => {
|
||||
|
@ -966,58 +1048,135 @@ describe.each([
|
|||
tableId: table._id!,
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
Price: { visible: true, readonly: true },
|
||||
one: { visible: true, readonly: true },
|
||||
},
|
||||
})
|
||||
|
||||
mocks.licenses.useCloudFree()
|
||||
const view = await config.api.viewV2.get(res.id)
|
||||
expect(view.schema?.Price).toEqual(
|
||||
const view = await getDelegate(res)
|
||||
expect(view.schema?.one).toEqual(
|
||||
expect.objectContaining({ visible: true, readonly: true })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("read", () => {
|
||||
let view: ViewV2
|
||||
|
||||
beforeAll(async () => {
|
||||
describe("updating table schema", () => {
|
||||
describe("existing columns changed to required", () => {
|
||||
beforeEach(async () => {
|
||||
table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
Country: {
|
||||
type: FieldType.STRING,
|
||||
name: "Country",
|
||||
id: {
|
||||
name: "id",
|
||||
type: FieldType.NUMBER,
|
||||
autocolumn: true,
|
||||
},
|
||||
Story: {
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
name: "Story",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
view = await config.api.viewV2.create({
|
||||
it("allows updating when no views constrains the field", async () => {
|
||||
await config.api.viewV2.create({
|
||||
name: "view a",
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
Country: {
|
||||
name: { visible: true },
|
||||
},
|
||||
})
|
||||
|
||||
table = await config.api.table.get(table._id!)
|
||||
await config.api.table.save(
|
||||
{
|
||||
...table,
|
||||
schema: {
|
||||
...table.schema,
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
constraints: { presence: { allowEmpty: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
})
|
||||
|
||||
it("rejects if field is readonly in any view", async () => {
|
||||
mocks.licenses.useViewReadonlyColumns()
|
||||
|
||||
await config.api.viewV2.create({
|
||||
name: "view a",
|
||||
tableId: table._id!,
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
name: {
|
||||
visible: true,
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
table = await config.api.table.get(table._id!)
|
||||
await config.api.table.save(
|
||||
{
|
||||
...table,
|
||||
schema: {
|
||||
...table.schema,
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
constraints: { presence: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
body: {
|
||||
status: 400,
|
||||
message:
|
||||
'To make field "name" required, this field must be present and writable in views: view a.',
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("views have extra data trimmed", async () => {
|
||||
let row = await config.api.row.save(view.id, {
|
||||
Country: "Aussy",
|
||||
Story: "aaaaa",
|
||||
it("rejects if field is hidden in any view", async () => {
|
||||
await config.api.viewV2.create({
|
||||
name: "view a",
|
||||
tableId: table._id!,
|
||||
schema: { id: { visible: true } },
|
||||
})
|
||||
|
||||
row = await config.api.row.get(table._id!, row._id!)
|
||||
expect(row.Story).toBeUndefined()
|
||||
expect(row.Country).toEqual("Aussy")
|
||||
table = await config.api.table.get(table._id!)
|
||||
await config.api.table.save(
|
||||
{
|
||||
...table,
|
||||
schema: {
|
||||
...table.schema,
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
constraints: { presence: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
body: {
|
||||
status: 400,
|
||||
message:
|
||||
'To make field "name" required, this field must be present and writable in views: view a.',
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1084,9 +1243,36 @@ describe.each([
|
|||
expect(row.one).toBeUndefined()
|
||||
expect(row.two).toEqual("bar")
|
||||
})
|
||||
|
||||
it("should not return non-view view fields for a row", async () => {
|
||||
const newRow = await config.api.row.save(view.id, {
|
||||
one: "foo",
|
||||
two: "bar",
|
||||
})
|
||||
|
||||
expect(newRow.one).toBeUndefined()
|
||||
expect(newRow.two).toEqual("bar")
|
||||
})
|
||||
})
|
||||
|
||||
describe("patch", () => {
|
||||
it("should not return non-view view fields for a row", async () => {
|
||||
const newRow = await config.api.row.save(table._id!, {
|
||||
one: "foo",
|
||||
two: "bar",
|
||||
})
|
||||
const row = await config.api.row.patch(view.id, {
|
||||
tableId: table._id!,
|
||||
_id: newRow._id!,
|
||||
_rev: newRow._rev!,
|
||||
one: "newFoo",
|
||||
two: "newBar",
|
||||
})
|
||||
|
||||
expect(row.one).toBeUndefined()
|
||||
expect(row.two).toEqual("newBar")
|
||||
})
|
||||
|
||||
it("should update only the view fields for a row", async () => {
|
||||
const newRow = await config.api.row.save(table._id!, {
|
||||
one: "foo",
|
||||
|
@ -1135,6 +1321,21 @@ describe.each([
|
|||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
const getRowUsage = async () => {
|
||||
const { total } = await config.doInContext(undefined, () =>
|
||||
quotas.getCurrentUsageValues(
|
||||
QuotaUsageType.STATIC,
|
||||
StaticQuotaName.ROWS
|
||||
)
|
||||
)
|
||||
return total
|
||||
}
|
||||
|
||||
const assertRowUsage = async (expected: number) => {
|
||||
const usage = await getRowUsage()
|
||||
expect(usage).toBe(expected)
|
||||
}
|
||||
|
||||
it("should be able to delete a row", async () => {
|
||||
const createdRow = await config.api.row.save(table._id!, {})
|
||||
const rowUsage = await getRowUsage()
|
||||
|
@ -1167,6 +1368,50 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
describe("read", () => {
|
||||
let view: ViewV2
|
||||
let table: Table
|
||||
|
||||
beforeAll(async () => {
|
||||
table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
Country: {
|
||||
type: FieldType.STRING,
|
||||
name: "Country",
|
||||
},
|
||||
Story: {
|
||||
type: FieldType.STRING,
|
||||
name: "Story",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
view = await config.api.viewV2.create({
|
||||
tableId: table._id!,
|
||||
name: generator.guid(),
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
Country: {
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("views have extra data trimmed", async () => {
|
||||
let row = await config.api.row.save(view.id, {
|
||||
Country: "Aussy",
|
||||
Story: "aaaaa",
|
||||
})
|
||||
|
||||
row = await config.api.row.get(table._id!, row._id!)
|
||||
expect(row.Story).toBeUndefined()
|
||||
expect(row.Country).toEqual("Aussy")
|
||||
})
|
||||
})
|
||||
|
||||
describe("search", () => {
|
||||
it("returns empty rows from view when no schema is passed", async () => {
|
||||
const rows = await Promise.all(
|
||||
|
@ -1795,123 +2040,4 @@ describe.each([
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updating table schema", () => {
|
||||
describe("existing columns changed to required", () => {
|
||||
beforeEach(async () => {
|
||||
table = await config.api.table.save(
|
||||
saveTableRequest({
|
||||
schema: {
|
||||
id: {
|
||||
name: "id",
|
||||
type: FieldType.NUMBER,
|
||||
autocolumn: true,
|
||||
},
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("allows updating when no views constrains the field", async () => {
|
||||
await config.api.viewV2.create({
|
||||
name: "view a",
|
||||
tableId: table._id!,
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
name: { visible: true },
|
||||
},
|
||||
})
|
||||
|
||||
table = await config.api.table.get(table._id!)
|
||||
await config.api.table.save(
|
||||
{
|
||||
...table,
|
||||
schema: {
|
||||
...table.schema,
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
constraints: { presence: { allowEmpty: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
})
|
||||
|
||||
it("rejects if field is readonly in any view", async () => {
|
||||
mocks.licenses.useViewReadonlyColumns()
|
||||
|
||||
await config.api.viewV2.create({
|
||||
name: "view a",
|
||||
tableId: table._id!,
|
||||
schema: {
|
||||
id: { visible: true },
|
||||
name: {
|
||||
visible: true,
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
table = await config.api.table.get(table._id!)
|
||||
await config.api.table.save(
|
||||
{
|
||||
...table,
|
||||
schema: {
|
||||
...table.schema,
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
constraints: { presence: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
body: {
|
||||
status: 400,
|
||||
message:
|
||||
'To make field "name" required, this field must be present and writable in views: view a.',
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("rejects if field is hidden in any view", async () => {
|
||||
await config.api.viewV2.create({
|
||||
name: "view a",
|
||||
tableId: table._id!,
|
||||
schema: { id: { visible: true } },
|
||||
})
|
||||
|
||||
table = await config.api.table.get(table._id!)
|
||||
await config.api.table.save(
|
||||
{
|
||||
...table,
|
||||
schema: {
|
||||
...table.schema,
|
||||
name: {
|
||||
name: "name",
|
||||
type: FieldType.STRING,
|
||||
constraints: { presence: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
body: {
|
||||
status: 400,
|
||||
message:
|
||||
'To make field "name" required, this field must be present and writable in views: view a.',
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -46,7 +46,7 @@ router
|
|||
permissions.PermissionType.TABLE,
|
||||
permissions.PermissionLevel.READ
|
||||
),
|
||||
rowController.fetchView
|
||||
rowController.fetchLegacyView
|
||||
)
|
||||
.get("/api/views", authorized(permissions.BUILDER), viewController.v1.fetch)
|
||||
.delete(
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// This file should never be manually modified, use `yarn add-app-migration` in order to add a new one
|
||||
|
||||
import { env } from "@budibase/backend-core"
|
||||
import { AppMigration } from "."
|
||||
|
||||
import m20240604153647_initial_sqs from "./migrations/20240604153647_initial_sqs"
|
||||
|
@ -10,6 +9,5 @@ export const MIGRATIONS: AppMigration[] = [
|
|||
{
|
||||
id: "20240604153647_initial_sqs",
|
||||
func: m20240604153647_initial_sqs,
|
||||
disabled: !(env.SQS_MIGRATION_ENABLE || env.SQS_SEARCH_ENABLE),
|
||||
},
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { context, env } from "@budibase/backend-core"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { allLinkDocs } from "../../db/utils"
|
||||
import LinkDocumentImpl from "../../db/linkedRows/LinkDocument"
|
||||
import sdk from "../../sdk"
|
||||
|
@ -36,16 +36,6 @@ const migration = async () => {
|
|||
|
||||
// at the end make sure design doc is ready
|
||||
await sdk.tables.sqs.syncDefinition()
|
||||
// only do initial search if environment is using SQS already
|
||||
// initial search makes sure that all the indexes have been created
|
||||
// and are ready to use, avoiding any initial waits for large tables
|
||||
if (env.SQS_MIGRATION_ENABLE || env.SQS_SEARCH_ENABLE) {
|
||||
const tables = await sdk.tables.getAllInternalTables()
|
||||
// do these one by one - running in parallel could cause problems
|
||||
for (let table of tables) {
|
||||
await db.sql(`select * from ${table._id} limit 1`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from "../../../db/utils"
|
||||
import { processMigrations } from "../../migrationsProcessor"
|
||||
import migration from "../20240604153647_initial_sqs"
|
||||
import { AppMigration } from "src/appMigrations"
|
||||
import { AppMigration, updateAppMigrationMetadata } from "../../"
|
||||
import sdk from "../../../sdk"
|
||||
|
||||
const MIGRATIONS: AppMigration[] = [
|
||||
|
@ -70,24 +70,17 @@ function oldLinkDocument(): Omit<LinkDocument, "tableId"> {
|
|||
}
|
||||
}
|
||||
|
||||
type SQSEnvVar = "SQS_MIGRATION_ENABLE" | "SQS_SEARCH_ENABLE"
|
||||
|
||||
async function sqsDisabled(envVar: SQSEnvVar, cb: () => Promise<void>) {
|
||||
await withCoreEnv({ [envVar]: "", SQS_SEARCH_ENABLE_TENANTS: [] }, cb)
|
||||
async function sqsDisabled(cb: () => Promise<void>) {
|
||||
await withCoreEnv({ TENANT_FEATURE_FLAGS: "*:!SQS" }, cb)
|
||||
}
|
||||
|
||||
async function sqsEnabled(envVar: SQSEnvVar, cb: () => Promise<void>) {
|
||||
await withCoreEnv(
|
||||
{ [envVar]: "1", SQS_SEARCH_ENABLE_TENANTS: [config.getTenantId()] },
|
||||
cb
|
||||
)
|
||||
async function sqsEnabled(cb: () => Promise<void>) {
|
||||
await withCoreEnv({ TENANT_FEATURE_FLAGS: "*:SQS" }, cb)
|
||||
}
|
||||
|
||||
describe.each(["SQS_MIGRATION_ENABLE", "SQS_SEARCH_ENABLE"] as SQSEnvVar[])(
|
||||
"SQS migration with (%s)",
|
||||
envVar => {
|
||||
describe("SQS migration", () => {
|
||||
beforeAll(async () => {
|
||||
await sqsDisabled(envVar, async () => {
|
||||
await sqsDisabled(async () => {
|
||||
await config.init()
|
||||
const table = await config.api.table.save(basicTable())
|
||||
tableId = table._id!
|
||||
|
@ -97,10 +90,19 @@ describe.each(["SQS_MIGRATION_ENABLE", "SQS_SEARCH_ENABLE"] as SQSEnvVar[])(
|
|||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await config.doInTenant(async () => {
|
||||
await updateAppMigrationMetadata({
|
||||
appId: config.getAppId(),
|
||||
version: "",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("test migration runs as expected against an older DB", async () => {
|
||||
const db = dbCore.getDB(config.appId!)
|
||||
// confirm nothing exists initially
|
||||
await sqsDisabled(envVar, async () => {
|
||||
await sqsDisabled(async () => {
|
||||
let error: any | undefined
|
||||
try {
|
||||
await db.get(SQLITE_DESIGN_DOC_ID)
|
||||
|
@ -110,7 +112,8 @@ describe.each(["SQS_MIGRATION_ENABLE", "SQS_SEARCH_ENABLE"] as SQSEnvVar[])(
|
|||
expect(error).toBeDefined()
|
||||
expect(error.status).toBe(404)
|
||||
})
|
||||
await sqsEnabled(envVar, async () => {
|
||||
|
||||
await sqsEnabled(async () => {
|
||||
await processMigrations(config.appId!, MIGRATIONS)
|
||||
const designDoc = await db.get<SQLiteDefinition>(SQLITE_DESIGN_DOC_ID)
|
||||
expect(designDoc.sql.tables).toBeDefined()
|
||||
|
@ -137,5 +140,4 @@ describe.each(["SQS_MIGRATION_ENABLE", "SQS_SEARCH_ENABLE"] as SQSEnvVar[])(
|
|||
expect(linkDoc.doc2.rowId).toEqual(rowId1)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
LinkDocumentValue,
|
||||
Row,
|
||||
Table,
|
||||
TableSchema,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../sdk"
|
||||
|
||||
|
@ -46,8 +47,8 @@ export const EventType = {
|
|||
TABLE_DELETE: "table:delete",
|
||||
}
|
||||
|
||||
function clearRelationshipFields(table: Table, rows: Row[]) {
|
||||
for (let [key, field] of Object.entries(table.schema)) {
|
||||
function clearRelationshipFields(schema: TableSchema, rows: Row[]) {
|
||||
for (let [key, field] of Object.entries(schema)) {
|
||||
if (field.type === FieldType.LINK) {
|
||||
rows = rows.map(row => {
|
||||
delete row[key]
|
||||
|
@ -158,11 +159,11 @@ export async function updateLinks(args: {
|
|||
* @return returns the rows with all of the enriched relationships on it.
|
||||
*/
|
||||
export async function attachFullLinkedDocs(
|
||||
table: Table,
|
||||
schema: TableSchema,
|
||||
rows: Row[],
|
||||
opts?: { fromRow?: Row }
|
||||
) {
|
||||
const linkedTableIds = getLinkedTableIDs(table)
|
||||
const linkedTableIds = getLinkedTableIDs(schema)
|
||||
if (linkedTableIds.length === 0) {
|
||||
return rows
|
||||
}
|
||||
|
@ -182,7 +183,7 @@ export async function attachFullLinkedDocs(
|
|||
}
|
||||
const linkedTables = response[1] as Table[]
|
||||
// clear any existing links that could be dupe'd
|
||||
rows = clearRelationshipFields(table, rows)
|
||||
rows = clearRelationshipFields(schema, rows)
|
||||
// now get the docs and combine into the rows
|
||||
let linked: Row[] = []
|
||||
if (linksWithoutFromRow.length > 0) {
|
||||
|
@ -201,7 +202,7 @@ export async function attachFullLinkedDocs(
|
|||
}
|
||||
if (linkedRow) {
|
||||
const linkedTableId =
|
||||
linkedRow.tableId || getRelatedTableForField(table, link.fieldName)
|
||||
linkedRow.tableId || getRelatedTableForField(schema, link.fieldName)
|
||||
const linkedTable = linkedTables.find(
|
||||
table => table._id === linkedTableId
|
||||
)
|
||||
|
@ -263,7 +264,8 @@ export async function squashLinksToPrimaryDisplay(
|
|||
}
|
||||
const newLinks = []
|
||||
for (let link of row[column]) {
|
||||
const linkTblId = link.tableId || getRelatedTableForField(table, column)
|
||||
const linkTblId =
|
||||
link.tableId || getRelatedTableForField(table.schema, column)
|
||||
const linkedTable = await getLinkedTable(linkTblId!, linkedTables)
|
||||
const obj: any = { _id: link._id }
|
||||
obj.primaryDisplay = getPrimaryDisplayValue(link, linkedTable)
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
LinkDocument,
|
||||
LinkDocumentValue,
|
||||
Table,
|
||||
TableSchema,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../sdk"
|
||||
|
||||
|
@ -121,8 +122,8 @@ export function getUniqueByProp(array: any[], prop: string) {
|
|||
return filteredArray
|
||||
}
|
||||
|
||||
export function getLinkedTableIDs(table: Table): string[] {
|
||||
return Object.values(table.schema)
|
||||
export function getLinkedTableIDs(schema: TableSchema): string[] {
|
||||
return Object.values(schema)
|
||||
.filter(isRelationshipColumn)
|
||||
.map(column => column.tableId)
|
||||
}
|
||||
|
@ -139,13 +140,16 @@ export async function getLinkedTable(id: string, tables: Table[]) {
|
|||
return linkedTable
|
||||
}
|
||||
|
||||
export function getRelatedTableForField(table: Table, fieldName: string) {
|
||||
export function getRelatedTableForField(
|
||||
schema: TableSchema,
|
||||
fieldName: string
|
||||
) {
|
||||
// look to see if its on the table, straight in the schema
|
||||
const field = table.schema[fieldName]
|
||||
const field = schema[fieldName]
|
||||
if (field?.type === FieldType.LINK) {
|
||||
return field.tableId
|
||||
}
|
||||
for (let column of Object.values(table.schema)) {
|
||||
for (let column of Object.values(schema)) {
|
||||
if (column.type === FieldType.LINK && column.fieldName === fieldName) {
|
||||
return column.tableId
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ describe("test link functionality", () => {
|
|||
})
|
||||
|
||||
describe("getRelatedTableForField", () => {
|
||||
let link = basicTable()
|
||||
const link = basicTable()
|
||||
link.schema.link = {
|
||||
name: "link",
|
||||
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||
|
@ -44,11 +44,13 @@ describe("test link functionality", () => {
|
|||
}
|
||||
|
||||
it("should get the field from the table directly", () => {
|
||||
expect(linkUtils.getRelatedTableForField(link, "link")).toBe("tableID")
|
||||
expect(linkUtils.getRelatedTableForField(link.schema, "link")).toBe(
|
||||
"tableID"
|
||||
)
|
||||
})
|
||||
|
||||
it("should get the field from the link", () => {
|
||||
expect(linkUtils.getRelatedTableForField(link, "otherLink")).toBe(
|
||||
expect(linkUtils.getRelatedTableForField(link.schema, "otherLink")).toBe(
|
||||
"tableID"
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
import { Ctx, Row } from "@budibase/types"
|
||||
import * as utils from "../db/utils"
|
||||
import { Ctx, Row, ViewV2 } from "@budibase/types"
|
||||
|
||||
import sdk from "../sdk"
|
||||
import { Next } from "koa"
|
||||
import { getTableId } from "../api/controllers/row/utils"
|
||||
import { getSourceId } from "../api/controllers/row/utils"
|
||||
|
||||
export default async (ctx: Ctx<Row>, next: Next) => {
|
||||
export default async (ctx: Ctx<Row, Row>, next: Next) => {
|
||||
const { body } = ctx.request
|
||||
let { _viewId: viewId } = body
|
||||
|
||||
const possibleViewId = getTableId(ctx)
|
||||
if (utils.isViewID(possibleViewId)) {
|
||||
viewId = possibleViewId
|
||||
}
|
||||
const viewId = getSourceId(ctx).viewId ?? body._viewId
|
||||
|
||||
// nothing to do, it is not a view (just a table ID)
|
||||
if (!viewId) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||
|
||||
// don't need to trim delete requests
|
||||
if (ctx?.method?.toLowerCase() !== "delete") {
|
||||
await trimViewFields(ctx.request.body, viewId)
|
||||
}
|
||||
|
||||
ctx.params.sourceId = tableId
|
||||
ctx.params.viewId = viewId
|
||||
|
||||
const trimFields = ctx?.method?.toLowerCase() !== "delete"
|
||||
if (!trimFields) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// have to mutate the koa context, can't return
|
||||
export async function trimViewFields(body: Row, viewId: string): Promise<void> {
|
||||
const view = await sdk.views.get(viewId)
|
||||
const allowedKeys = sdk.views.allowedFields(view)
|
||||
ctx.request.body = await trimNonViewFields(ctx.request.body, view, "WRITE")
|
||||
|
||||
await next()
|
||||
|
||||
ctx.body = await trimNonViewFields(ctx.body, view, "READ")
|
||||
}
|
||||
|
||||
// have to mutate the koa context, can't return
|
||||
export async function trimNonViewFields(
|
||||
row: Row,
|
||||
view: ViewV2,
|
||||
permission: "WRITE" | "READ"
|
||||
): Promise<Row> {
|
||||
row = { ...row }
|
||||
const allowedKeys = sdk.views.allowedFields(view, permission)
|
||||
// have to mutate the context, can't update reference
|
||||
const toBeRemoved = Object.keys(body).filter(
|
||||
key => !allowedKeys.includes(key)
|
||||
)
|
||||
const toBeRemoved = Object.keys(row).filter(key => !allowedKeys.includes(key))
|
||||
for (let removeKey of toBeRemoved) {
|
||||
delete body[removeKey]
|
||||
delete row[removeKey]
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
|||
import { dataFilters } from "@budibase/shared-core"
|
||||
import sdk from "../../index"
|
||||
import { searchInputMapping } from "./search/utils"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import { features } from "@budibase/backend-core"
|
||||
import tracer from "dd-trace"
|
||||
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
||||
|
||||
|
@ -90,7 +90,7 @@ export async function search(
|
|||
if (isExternalTable) {
|
||||
span?.addTags({ searchType: "external" })
|
||||
result = await external.search(options, table)
|
||||
} else if (dbCore.isSqsEnabledForTenant()) {
|
||||
} else if (await features.flags.isEnabled("SQS")) {
|
||||
span?.addTags({ searchType: "sqs" })
|
||||
result = await internal.sqs.search(options, table)
|
||||
} else {
|
||||
|
@ -121,10 +121,9 @@ export async function fetchRaw(tableId: string): Promise<Row[]> {
|
|||
return pickApi(tableId).fetchRaw(tableId)
|
||||
}
|
||||
|
||||
export async function fetchView(
|
||||
tableId: string,
|
||||
export async function fetchLegacyView(
|
||||
viewName: string,
|
||||
params: ViewParams
|
||||
): Promise<Row[]> {
|
||||
return pickApi(tableId).fetchView(viewName, params)
|
||||
return internal.fetchLegacyView(viewName, params)
|
||||
}
|
||||
|
|
|
@ -272,11 +272,3 @@ export async function fetchRaw(tableId: string): Promise<Row[]> {
|
|||
})
|
||||
return response.rows
|
||||
}
|
||||
|
||||
export async function fetchView(viewName: string) {
|
||||
// there are no views in external datasources, shouldn't ever be called
|
||||
// for now just fetch
|
||||
const split = viewName.split("all_")
|
||||
const tableId = split[1] ? split[1] : split[0]
|
||||
return fetch(tableId)
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ export async function fetchRaw(tableId: string): Promise<Row[]> {
|
|||
return rows as Row[]
|
||||
}
|
||||
|
||||
export async function fetchView(
|
||||
export async function fetchLegacyView(
|
||||
viewName: string,
|
||||
options: { calculation: string; group: string; field: string }
|
||||
): Promise<Row[]> {
|
||||
|
|
|
@ -41,14 +41,17 @@ describe.each([
|
|||
let table: Table
|
||||
|
||||
beforeAll(async () => {
|
||||
await withCoreEnv({ SQS_SEARCH_ENABLE: isSqs ? "true" : "false" }, () =>
|
||||
await withCoreEnv({ TENANT_FEATURE_FLAGS: isSqs ? "*:SQS" : "" }, () =>
|
||||
config.init()
|
||||
)
|
||||
|
||||
if (isSqs) {
|
||||
if (isLucene) {
|
||||
envCleanup = setCoreEnv({
|
||||
SQS_SEARCH_ENABLE: "true",
|
||||
SQS_SEARCH_ENABLE_TENANTS: [config.getTenantId()],
|
||||
TENANT_FEATURE_FLAGS: "*:!SQS",
|
||||
})
|
||||
} else if (isSqs) {
|
||||
envCleanup = setCoreEnv({
|
||||
TENANT_FEATURE_FLAGS: "*:SQS",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { context, db as dbCore, env } from "@budibase/backend-core"
|
||||
import { context, features } from "@budibase/backend-core"
|
||||
import { getTableParams } from "../../../db/utils"
|
||||
import {
|
||||
breakExternalTableId,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
import datasources from "../datasources"
|
||||
import sdk from "../../../sdk"
|
||||
|
||||
export function processTable(table: Table): Table {
|
||||
export async function processTable(table: Table): Promise<Table> {
|
||||
if (!table) {
|
||||
return table
|
||||
}
|
||||
|
@ -33,20 +33,21 @@ export function processTable(table: Table): Table {
|
|||
sourceId: table.sourceId || INTERNAL_TABLE_SOURCE_ID,
|
||||
sourceType: TableSourceType.INTERNAL,
|
||||
}
|
||||
if (dbCore.isSqsEnabledForTenant()) {
|
||||
processed.sql = !!env.SQS_SEARCH_ENABLE
|
||||
const sqsEnabled = await features.flags.isEnabled("SQS")
|
||||
if (sqsEnabled) {
|
||||
processed.sql = true
|
||||
}
|
||||
return processed
|
||||
}
|
||||
}
|
||||
|
||||
export function processTables(tables: Table[]): Table[] {
|
||||
return tables.map(table => processTable(table))
|
||||
export async function processTables(tables: Table[]): Promise<Table[]> {
|
||||
return await Promise.all(tables.map(table => processTable(table)))
|
||||
}
|
||||
|
||||
function processEntities(tables: Record<string, Table>) {
|
||||
async function processEntities(tables: Record<string, Table>) {
|
||||
for (let key of Object.keys(tables)) {
|
||||
tables[key] = processTable(tables[key])
|
||||
tables[key] = await processTable(tables[key])
|
||||
}
|
||||
return tables
|
||||
}
|
||||
|
@ -60,7 +61,7 @@ export async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
|||
include_docs: true,
|
||||
})
|
||||
)
|
||||
return processTables(internalTables.rows.map(row => row.doc!))
|
||||
return await processTables(internalTables.rows.map(row => row.doc!))
|
||||
}
|
||||
|
||||
async function getAllExternalTables(): Promise<Table[]> {
|
||||
|
@ -72,7 +73,7 @@ async function getAllExternalTables(): Promise<Table[]> {
|
|||
final = final.concat(Object.values(entities))
|
||||
}
|
||||
}
|
||||
return processTables(final)
|
||||
return await processTables(final)
|
||||
}
|
||||
|
||||
export async function getExternalTable(
|
||||
|
@ -97,7 +98,7 @@ export async function getTable(tableId: string): Promise<Table> {
|
|||
} else {
|
||||
output = await db.get<Table>(tableId)
|
||||
}
|
||||
return processTable(output)
|
||||
return await processTable(output)
|
||||
}
|
||||
|
||||
export async function getAllTables() {
|
||||
|
@ -105,7 +106,7 @@ export async function getAllTables() {
|
|||
getAllInternalTables(),
|
||||
getAllExternalTables(),
|
||||
])
|
||||
return processTables([...internal, ...external])
|
||||
return await processTables([...internal, ...external])
|
||||
}
|
||||
|
||||
export async function getExternalTablesInDatasource(
|
||||
|
@ -115,7 +116,7 @@ export async function getExternalTablesInDatasource(
|
|||
if (!datasource || !datasource.entities) {
|
||||
throw new Error("Datasource is not configured fully.")
|
||||
}
|
||||
return processEntities(datasource.entities)
|
||||
return await processEntities(datasource.entities)
|
||||
}
|
||||
|
||||
export async function getTables(tableIds: string[]): Promise<Table[]> {
|
||||
|
@ -139,7 +140,7 @@ export async function getTables(tableIds: string[]): Promise<Table[]> {
|
|||
})
|
||||
tables = tables.concat(internalTables)
|
||||
}
|
||||
return processTables(tables)
|
||||
return await processTables(tables)
|
||||
}
|
||||
|
||||
export function enrichViewSchemas(table: Table): TableResponse {
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
PROTECTED_EXTERNAL_COLUMNS,
|
||||
PROTECTED_INTERNAL_COLUMNS,
|
||||
} from "@budibase/shared-core"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
import * as utils from "../../../db/utils"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
|
@ -139,14 +138,20 @@ export async function remove(viewId: string): Promise<ViewV2> {
|
|||
return pickApi(tableId).remove(viewId)
|
||||
}
|
||||
|
||||
export function allowedFields(view: View | ViewV2) {
|
||||
export function allowedFields(
|
||||
view: View | ViewV2,
|
||||
permission: "WRITE" | "READ"
|
||||
) {
|
||||
return [
|
||||
...Object.keys(view?.schema || {}).filter(key => {
|
||||
if (!isV2(view)) {
|
||||
return true
|
||||
}
|
||||
const fieldSchema = view.schema![key]
|
||||
if (permission === "WRITE") {
|
||||
return fieldSchema.visible && !fieldSchema.readonly
|
||||
}
|
||||
return fieldSchema.visible
|
||||
}),
|
||||
...PROTECTED_EXTERNAL_COLUMNS,
|
||||
...PROTECTED_INTERNAL_COLUMNS,
|
||||
|
@ -157,17 +162,19 @@ export function enrichSchema(
|
|||
view: ViewV2,
|
||||
tableSchema: TableSchema
|
||||
): ViewV2Enriched {
|
||||
let schema = cloneDeep(tableSchema)
|
||||
let schema: TableSchema = {}
|
||||
const anyViewOrder = Object.values(view.schema || {}).some(
|
||||
ui => ui.order != null
|
||||
)
|
||||
for (const key of Object.keys(schema)) {
|
||||
for (const key of Object.keys(tableSchema).filter(
|
||||
key => tableSchema[key].visible !== false
|
||||
)) {
|
||||
// if nothing specified in view, then it is not visible
|
||||
const ui = view.schema?.[key] || { visible: false }
|
||||
schema[key] = {
|
||||
...schema[key],
|
||||
...tableSchema[key],
|
||||
...ui,
|
||||
order: anyViewOrder ? ui?.order ?? undefined : schema[key].order,
|
||||
order: anyViewOrder ? ui?.order ?? undefined : tableSchema[key].order,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -101,14 +101,6 @@ describe("table sdk", () => {
|
|||
type: "number",
|
||||
},
|
||||
},
|
||||
hiddenField: {
|
||||
type: "string",
|
||||
name: "hiddenField",
|
||||
visible: false,
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -143,10 +135,6 @@ describe("table sdk", () => {
|
|||
...basicTable.schema.id,
|
||||
visible: true,
|
||||
},
|
||||
hiddenField: {
|
||||
...basicTable.schema.hiddenField,
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -181,10 +169,6 @@ describe("table sdk", () => {
|
|||
...basicTable.schema.id,
|
||||
visible: false,
|
||||
},
|
||||
hiddenField: {
|
||||
...basicTable.schema.hiddenField,
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -209,7 +193,6 @@ describe("table sdk", () => {
|
|||
expect.objectContaining({
|
||||
...view,
|
||||
schema: {
|
||||
...basicTable.schema,
|
||||
name: {
|
||||
type: "string",
|
||||
name: "name",
|
||||
|
@ -264,7 +247,6 @@ describe("table sdk", () => {
|
|||
expect.objectContaining({
|
||||
...view,
|
||||
schema: {
|
||||
...basicTable.schema,
|
||||
name: {
|
||||
type: "string",
|
||||
name: "name",
|
||||
|
|
|
@ -3,6 +3,8 @@ import { tmpdir } from "os"
|
|||
process.env.SELF_HOSTED = "1"
|
||||
process.env.NODE_ENV = "jest"
|
||||
process.env.MULTI_TENANCY = "1"
|
||||
process.env.APP_PORT = "0"
|
||||
process.env.WORKER_PORT = "0"
|
||||
// @ts-ignore
|
||||
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
|
||||
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
|
||||
|
|
|
@ -3,6 +3,7 @@ import { fixAutoColumnSubType, processFormulas } from "./utils"
|
|||
import {
|
||||
cache,
|
||||
context,
|
||||
features,
|
||||
HTTPError,
|
||||
objectStore,
|
||||
utils,
|
||||
|
@ -262,7 +263,7 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
}
|
||||
// attach any linked row information
|
||||
let enriched = !opts.preserveLinks
|
||||
? await linkRows.attachFullLinkedDocs(table, safeRows, {
|
||||
? await linkRows.attachFullLinkedDocs(table.schema, safeRows, {
|
||||
fromRow: opts?.fromRow,
|
||||
})
|
||||
: safeRows
|
||||
|
@ -349,11 +350,19 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
}
|
||||
// remove null properties to match internal API
|
||||
const isExternal = isExternalTableID(table._id!)
|
||||
if (isExternal) {
|
||||
if (isExternal || (await features.flags.isEnabled("SQS"))) {
|
||||
for (const row of enriched) {
|
||||
for (const key of Object.keys(row)) {
|
||||
if (row[key] === null) {
|
||||
delete row[key]
|
||||
} else if (row[key] && table.schema[key]?.type === FieldType.LINK) {
|
||||
for (const link of row[key] || []) {
|
||||
for (const linkKey of Object.keys(link)) {
|
||||
if (link[linkKey] === null) {
|
||||
delete link[linkKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { outputProcessing } from ".."
|
||||
import { generator, structures } from "@budibase/backend-core/tests"
|
||||
import { setEnv as setCoreEnv } from "@budibase/backend-core"
|
||||
import * as bbReferenceProcessor from "../bbReferenceProcessor"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
||||
processInputBBReference: jest.fn(),
|
||||
|
@ -18,8 +20,24 @@ jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
|||
}))
|
||||
|
||||
describe("rowProcessor - outputProcessing", () => {
|
||||
const config = new TestConfiguration()
|
||||
let cleanupEnv: () => void = () => {}
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
cleanupEnv = setCoreEnv({ TENANT_FEATURE_FLAGS: "*SQS" })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanupEnv()
|
||||
})
|
||||
|
||||
const processOutputBBReferenceMock =
|
||||
|
@ -28,6 +46,7 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
bbReferenceProcessor.processOutputBBReferences as jest.Mock
|
||||
|
||||
it("fetches single user references given a populated field", async () => {
|
||||
await config.doInContext(config.getAppId(), async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -67,16 +86,17 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
|
||||
expect(result).toEqual({ name: "Jack", user })
|
||||
|
||||
expect(bbReferenceProcessor.processOutputBBReference).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(bbReferenceProcessor.processOutputBBReference).toHaveBeenCalledWith(
|
||||
"123",
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
expect(
|
||||
bbReferenceProcessor.processOutputBBReference
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
bbReferenceProcessor.processOutputBBReference
|
||||
).toHaveBeenCalledWith("123", BBReferenceFieldSubType.USER)
|
||||
})
|
||||
})
|
||||
|
||||
it("fetches users references given a populated field", async () => {
|
||||
await config.doInContext(config.getAppId(), async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -119,13 +139,14 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
expect(
|
||||
bbReferenceProcessor.processOutputBBReferences
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(bbReferenceProcessor.processOutputBBReferences).toHaveBeenCalledWith(
|
||||
"123",
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
expect(
|
||||
bbReferenceProcessor.processOutputBBReferences
|
||||
).toHaveBeenCalledWith("123", BBReferenceFieldSubType.USER)
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle attachment list correctly", async () => {
|
||||
await config.doInContext(config.getAppId(), async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -167,8 +188,10 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
const output3 = await outputProcessing(table, row, { squash: false })
|
||||
expect(output3.attach[0].url).toBe("aaaa")
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle single attachment correctly", async () => {
|
||||
await config.doInContext(config.getAppId(), async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -208,8 +231,10 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
const output3 = await outputProcessing(table, row, { squash: false })
|
||||
expect(output3.attach.url).toBe("aaaa")
|
||||
})
|
||||
})
|
||||
|
||||
it("process output even when the field is not empty", async () => {
|
||||
await config.doInContext(config.getAppId(), async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -249,8 +274,10 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
bbReferenceProcessor.processOutputBBReferences
|
||||
).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it("does not fetch bb references when not in the schema", async () => {
|
||||
await config.doInContext(config.getAppId(), async () => {
|
||||
const table: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -291,3 +318,4 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,7 +16,6 @@ import { gridSocket } from "./index"
|
|||
import { clearLock, updateLock } from "../utilities/redis"
|
||||
import { Socket } from "socket.io"
|
||||
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||
import { processTable } from "../sdk/app/tables/getters"
|
||||
|
||||
export default class BuilderSocket extends BaseSocket {
|
||||
constructor(app: Koa, server: http.Server) {
|
||||
|
@ -102,10 +101,9 @@ export default class BuilderSocket extends BaseSocket {
|
|||
}
|
||||
|
||||
emitTableUpdate(ctx: any, table: Table, options?: EmitOptions) {
|
||||
// This was added to make sure that sourceId is always present when
|
||||
// sending this message to clients. Without this, tables without a
|
||||
// sourceId (e.g. ta_users) won't get correctly updated client-side.
|
||||
table = processTable(table)
|
||||
if (table.sourceId == null || table.sourceId === "") {
|
||||
throw new Error("Table sourceId is not set")
|
||||
}
|
||||
|
||||
this.emitToRoom(
|
||||
ctx,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { BaseSocket } from "./websocket"
|
|||
import { auth, permissions } from "@budibase/backend-core"
|
||||
import http from "http"
|
||||
import Koa from "koa"
|
||||
import { getTableId } from "../api/controllers/row/utils"
|
||||
import { getSourceId } from "../api/controllers/row/utils"
|
||||
import { Row, Table, View, ViewV2 } from "@budibase/types"
|
||||
import { Socket } from "socket.io"
|
||||
import { GridSocketEvent } from "@budibase/shared-core"
|
||||
|
@ -80,7 +80,7 @@ export default class GridSocket extends BaseSocket {
|
|||
}
|
||||
|
||||
emitRowUpdate(ctx: any, row: Row) {
|
||||
const resourceId = ctx.params?.viewId || getTableId(ctx)
|
||||
const resourceId = ctx.params?.viewId || getSourceId(ctx)
|
||||
const room = `${ctx.appId}-${resourceId}`
|
||||
this.emitToRoom(ctx, room, GridSocketEvent.RowChange, {
|
||||
id: row._id,
|
||||
|
@ -89,7 +89,7 @@ export default class GridSocket extends BaseSocket {
|
|||
}
|
||||
|
||||
emitRowDeletion(ctx: any, row: Row) {
|
||||
const resourceId = ctx.params?.viewId || getTableId(ctx)
|
||||
const resourceId = ctx.params?.viewId || getSourceId(ctx)
|
||||
const room = `${ctx.appId}-${resourceId}`
|
||||
this.emitToRoom(ctx, room, GridSocketEvent.RowChange, {
|
||||
id: row._id,
|
||||
|
|
|
@ -30,7 +30,6 @@ async function init() {
|
|||
HTTP_LOGGING: "0",
|
||||
VERSION: "0.0.0+local",
|
||||
PASSWORD_MIN_LENGTH: "1",
|
||||
SQS_SEARCH_ENABLE: "1",
|
||||
}
|
||||
|
||||
config = { ...config, ...existingConfig }
|
||||
|
|
|
@ -54,6 +54,17 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
|
|||
const currentUserId = ctx.user?._id
|
||||
const requestUser = ctx.request.body
|
||||
|
||||
// Do not allow the account holder role to be changed
|
||||
const tenantInfo = await tenancy.getTenantInfo(requestUser.tenantId)
|
||||
if (tenantInfo?.owner.email === requestUser.email) {
|
||||
if (
|
||||
requestUser.admin?.global !== true ||
|
||||
requestUser.builder?.global !== true
|
||||
) {
|
||||
throw Error("Cannot set role of account holder")
|
||||
}
|
||||
}
|
||||
|
||||
const user = await userSdk.db.save(requestUser, { currentUserId })
|
||||
|
||||
ctx.body = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Ctx, MaintenanceType } from "@budibase/types"
|
||||
import env from "../../../environment"
|
||||
import { env as coreEnv, db as dbCore } from "@budibase/backend-core"
|
||||
import { env as coreEnv, db as dbCore, features } from "@budibase/backend-core"
|
||||
import nodeFetch from "node-fetch"
|
||||
|
||||
let sqsAvailable: boolean
|
||||
|
@ -29,7 +29,7 @@ async function isSqsAvailable() {
|
|||
}
|
||||
|
||||
async function isSqsMissing() {
|
||||
return coreEnv.SQS_SEARCH_ENABLE && !(await isSqsAvailable())
|
||||
return (await features.flags.isEnabled("SQS")) && !(await isSqsAvailable())
|
||||
}
|
||||
|
||||
export const fetch = async (ctx: Ctx) => {
|
||||
|
|
|
@ -4,12 +4,8 @@ const compress = require("koa-compress")
|
|||
|
||||
import zlib from "zlib"
|
||||
import { routes } from "./routes"
|
||||
import { middleware as pro, sdk } from "@budibase/pro"
|
||||
import { auth, middleware, env } from "@budibase/backend-core"
|
||||
|
||||
if (env.SQS_SEARCH_ENABLE) {
|
||||
sdk.auditLogs.useSQLSearch()
|
||||
}
|
||||
import { middleware as pro } from "@budibase/pro"
|
||||
import { auth, middleware } from "@budibase/backend-core"
|
||||
|
||||
const PUBLIC_ENDPOINTS = [
|
||||
// deprecated single tenant sso callback
|
||||
|
|
|
@ -1,36 +1,11 @@
|
|||
import Router from "@koa/router"
|
||||
import Joi from "joi"
|
||||
import { auth } from "@budibase/backend-core"
|
||||
import * as controller from "../../controllers/global/tenant"
|
||||
import cloudRestricted from "../../../middleware/cloudRestricted"
|
||||
|
||||
const router: Router = new Router()
|
||||
const OPTIONAL_STRING = Joi.string().optional().allow(null).allow("")
|
||||
|
||||
function buildTenantInfoValidation() {
|
||||
return auth.joiValidator.body(
|
||||
Joi.object({
|
||||
owner: Joi.object({
|
||||
email: Joi.string().required(),
|
||||
password: OPTIONAL_STRING,
|
||||
ssoId: OPTIONAL_STRING,
|
||||
givenName: OPTIONAL_STRING,
|
||||
familyName: OPTIONAL_STRING,
|
||||
budibaseUserId: OPTIONAL_STRING,
|
||||
}).required(),
|
||||
hosting: Joi.string().required(),
|
||||
tenantId: Joi.string().required(),
|
||||
}).required()
|
||||
)
|
||||
}
|
||||
|
||||
router
|
||||
.post(
|
||||
"/api/global/tenant",
|
||||
cloudRestricted,
|
||||
buildTenantInfoValidation(),
|
||||
controller.save
|
||||
)
|
||||
.post("/api/global/tenant", cloudRestricted, controller.save)
|
||||
.get("/api/global/tenant/:id", controller.get)
|
||||
|
||||
export default router
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { mocks, structures } from "@budibase/backend-core/tests"
|
||||
import { context, events } from "@budibase/backend-core"
|
||||
import { context, events, setEnv as setCoreEnv } from "@budibase/backend-core"
|
||||
import { Event, IdentityType } from "@budibase/types"
|
||||
import { auditLogs } from "@budibase/pro"
|
||||
import { TestConfiguration } from "../../../../tests"
|
||||
|
||||
mocks.licenses.useAuditLogs()
|
||||
|
@ -15,15 +14,19 @@ const APP_ID = "app_1"
|
|||
|
||||
describe.each(["lucene", "sql"])("/api/global/auditlogs (%s)", method => {
|
||||
const config = new TestConfiguration()
|
||||
let envCleanup: (() => void) | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
if (method === "sql") {
|
||||
auditLogs.useSQLSearch()
|
||||
if (method === "lucene") {
|
||||
envCleanup = setCoreEnv({ TENANT_FEATURE_FLAGS: "*:!SQS" })
|
||||
} else if (method === "sql") {
|
||||
envCleanup = setCoreEnv({ TENANT_FEATURE_FLAGS: "*:SQS" })
|
||||
}
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
envCleanup?.()
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
|
|
|
@ -412,6 +412,28 @@ describe("/api/global/users", () => {
|
|||
expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should not be able to update an account holder user to a basic user", async () => {
|
||||
const accountHolderUser = await config.createUser(
|
||||
structures.users.adminUser()
|
||||
)
|
||||
jest.clearAllMocks()
|
||||
tenancy.getTenantInfo = jest.fn().mockImplementation(() => ({
|
||||
owner: {
|
||||
email: accountHolderUser.email,
|
||||
},
|
||||
}))
|
||||
|
||||
accountHolderUser.admin!.global = false
|
||||
accountHolderUser.builder!.global = false
|
||||
|
||||
await config.api.users.saveUser(accountHolderUser, 400)
|
||||
|
||||
expect(events.user.created).not.toHaveBeenCalled()
|
||||
expect(events.user.updated).not.toHaveBeenCalled()
|
||||
expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled()
|
||||
expect(events.user.permissionBuilderRemoved).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should be able to update an builder user to a basic user", async () => {
|
||||
const user = await config.createUser(structures.users.builderUser())
|
||||
jest.clearAllMocks()
|
||||
|
|
|
@ -9,6 +9,7 @@ export class EnvironmentAPI extends TestAPI {
|
|||
getEnvironment = () => {
|
||||
return this.request
|
||||
.get(`/api/system/environment`)
|
||||
.set(this.config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
}
|
||||
|
|
238
yarn.lock
238
yarn.lock
|
@ -2003,9 +2003,9 @@
|
|||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.13.10":
|
||||
version "7.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb"
|
||||
integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==
|
||||
version "7.25.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2"
|
||||
integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
|
@ -2200,9 +2200,9 @@
|
|||
"@bull-board/api" "5.10.2"
|
||||
|
||||
"@camunda8/sdk@^8.5.3":
|
||||
version "8.6.10"
|
||||
resolved "https://registry.yarnpkg.com/@camunda8/sdk/-/sdk-8.6.10.tgz#61fdadc6bc89a234648ba4bc622b0db10f283de9"
|
||||
integrity sha512-FzSoLYd0yFFElC2G3NX93GnP7r53uQDR+6njV1EEAGPhz4QQfZeEW07vMNZ9BFeNn5jhtv9IWmHdHxYwJxxmcw==
|
||||
version "8.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@camunda8/sdk/-/sdk-8.6.12.tgz#8a210359cd9873b9e1750dcde45e62045409cc17"
|
||||
integrity sha512-dQNw9rMCrL0hJezjAeCAJWMyhuV/ouizP21UzgG9Edqnj/od9ko9XQjEd/AuSj9VMEEQ+bt40mBMnZszbISONg==
|
||||
dependencies:
|
||||
"@grpc/grpc-js" "1.10.9"
|
||||
"@grpc/proto-loader" "0.7.13"
|
||||
|
@ -2650,10 +2650,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
|
||||
integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
|
||||
|
||||
"@eslint/config-array@^0.17.1":
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.17.1.tgz#d9b8b8b6b946f47388f32bedfd3adf29ca8f8910"
|
||||
integrity sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==
|
||||
"@eslint/config-array@^0.18.0":
|
||||
version "0.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d"
|
||||
integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==
|
||||
dependencies:
|
||||
"@eslint/object-schema" "^2.1.4"
|
||||
debug "^4.3.1"
|
||||
|
@ -2694,10 +2694,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
|
||||
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
|
||||
|
||||
"@eslint/js@9.9.0", "@eslint/js@^9.7.0":
|
||||
version "9.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.0.tgz#d8437adda50b3ed4401964517b64b4f59b0e2638"
|
||||
integrity sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==
|
||||
"@eslint/js@9.9.1", "@eslint/js@^9.7.0":
|
||||
version "9.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06"
|
||||
integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==
|
||||
|
||||
"@eslint/object-schema@^2.1.4":
|
||||
version "2.1.4"
|
||||
|
@ -4208,160 +4208,160 @@
|
|||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27"
|
||||
integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz#d941173f82f9b041c61b0dc1a2a91dcd06e4b31e"
|
||||
integrity sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==
|
||||
"@rollup/rollup-android-arm-eabi@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz#0412834dc423d1ff7be4cb1fc13a86a0cd262c11"
|
||||
integrity sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203"
|
||||
integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz#7e7157c8543215245ceffc445134d9e843ba51c0"
|
||||
integrity sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==
|
||||
"@rollup/rollup-android-arm64@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz#baf1a014b13654f3b9e835388df9caf8c35389cb"
|
||||
integrity sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096"
|
||||
integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz#f0a18a4fc8dc6eb1e94a51fa2adb22876f477947"
|
||||
integrity sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==
|
||||
"@rollup/rollup-darwin-arm64@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz#0a2c364e775acdf1172fe3327662eec7c46e55b1"
|
||||
integrity sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c"
|
||||
integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz#34b7867613e5cc42d2b85ddc0424228cc33b43f0"
|
||||
integrity sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==
|
||||
"@rollup/rollup-darwin-x64@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz#a972db75890dfab8df0da228c28993220a468c42"
|
||||
integrity sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8"
|
||||
integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz#422b19ff9ae02b05d3395183d1d43b38c7c8be0b"
|
||||
integrity sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz#1609d0630ef61109dd19a278353e5176d92e30a1"
|
||||
integrity sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549"
|
||||
integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz#568aa29195ef6fc57ec6ed3f518923764406a8ee"
|
||||
integrity sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz#3c1dca5f160aa2e79e4b20ff6395eab21804f266"
|
||||
integrity sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577"
|
||||
integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz#22309c8bcba9a73114f69165c72bc94b2fbec085"
|
||||
integrity sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==
|
||||
"@rollup/rollup-linux-arm64-gnu@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz#c2fe376e8b04eafb52a286668a8df7c761470ac7"
|
||||
integrity sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c"
|
||||
integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz#c93c388af6d33f082894b8a60839d7265b2b9bc5"
|
||||
integrity sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==
|
||||
"@rollup/rollup-linux-arm64-musl@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz#e62a4235f01e0f66dbba587c087ca6db8008ec80"
|
||||
integrity sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf"
|
||||
integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz#493c5e19e395cf3c6bd860c7139c8a903dea72b4"
|
||||
integrity sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz#24b3457e75ee9ae5b1c198bd39eea53222a74e54"
|
||||
integrity sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9"
|
||||
integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz#a2eab4346fbe5909165ce99adb935ba30c9fb444"
|
||||
integrity sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz#38edfba9620fe2ca8116c97e02bd9f2d606bde09"
|
||||
integrity sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec"
|
||||
integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz#0bc49a79db4345d78d757bb1b05e73a1b42fa5c3"
|
||||
integrity sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==
|
||||
"@rollup/rollup-linux-s390x-gnu@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz#a3bfb8bc5f1e802f8c76cff4a4be2e9f9ac36a18"
|
||||
integrity sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942"
|
||||
integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz#4fd36a6a41f3406d8693321b13d4f9b7658dd4b9"
|
||||
integrity sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==
|
||||
"@rollup/rollup-linux-x64-gnu@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz#0dadf34be9199fcdda44b5985a086326344f30ad"
|
||||
integrity sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d"
|
||||
integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz#10ebb13bd4469cbad1a5d9b073bd27ec8a886200"
|
||||
integrity sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==
|
||||
"@rollup/rollup-linux-x64-musl@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz#7b7deddce240400eb87f2406a445061b4fed99a8"
|
||||
integrity sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf"
|
||||
integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz#2fef1a90f1402258ef915ae5a94cc91a5a1d5bfc"
|
||||
integrity sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==
|
||||
"@rollup/rollup-win32-arm64-msvc@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz#a0ca0c5149c2cfb26fab32e6ba3f16996fbdb504"
|
||||
integrity sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54"
|
||||
integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz#a18ad47a95c5f264defb60acdd8c27569f816fc1"
|
||||
integrity sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==
|
||||
"@rollup/rollup-win32-ia32-msvc@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz#aae2886beec3024203dbb5569db3a137bc385f8e"
|
||||
integrity sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.18.0":
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4"
|
||||
integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.21.0":
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz#20c09cf44dcb082140cc7f439dd679fe4bba3375"
|
||||
integrity sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==
|
||||
"@rollup/rollup-win32-x64-msvc@4.21.2":
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz#e4291e3c1bc637083f87936c333cdbcad22af63b"
|
||||
integrity sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==
|
||||
|
||||
"@roxi/routify@2.18.0":
|
||||
version "2.18.0"
|
||||
|
@ -5564,9 +5564,9 @@
|
|||
"@types/node" "*"
|
||||
|
||||
"@types/eslint@*":
|
||||
version "9.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.0.tgz#51d4fe4d0316da9e9f2c80884f2c20ed5fb022ff"
|
||||
integrity sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==
|
||||
version "9.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584"
|
||||
integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
"@types/json-schema" "*"
|
||||
|
@ -5879,9 +5879,9 @@
|
|||
integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==
|
||||
|
||||
"@types/node@>=8.1.0":
|
||||
version "22.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.4.2.tgz#55fefb1c3dba2ecd7eb76738c6b80da75760523f"
|
||||
integrity sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==
|
||||
version "22.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.1.tgz#de01dce265f6b99ed32b295962045d10b5b99560"
|
||||
integrity sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==
|
||||
dependencies:
|
||||
undici-types "~6.19.2"
|
||||
|
||||
|
@ -6219,9 +6219,9 @@
|
|||
"@types/node" "*"
|
||||
|
||||
"@types/server-destroy@^1.0.1":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/server-destroy/-/server-destroy-1.0.3.tgz#2460932ea3a02a70ec99669c8f40ff089a5b8a2b"
|
||||
integrity sha512-Qq0fn70C7TLDG1W9FCblKufNWW1OckQ41dVKV2Dku5KdZF7bexezG4e2WBaBKhdwL3HZ+cYCEIKwg2BRgzrWmA==
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/server-destroy/-/server-destroy-1.0.4.tgz#bd94af933e73e04795042edf38af267ddebd4e98"
|
||||
integrity sha512-+x8oAQ4Xp1wtDi2Hlmi7gUNXZNVhB5EoSQpi0qEmINdDN5Ab724WLGAalEdT1SudVY/NzMhbfZO7vU+klT0R+A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
|
@ -7878,7 +7878,7 @@ brace-expansion@^2.0.1:
|
|||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
braces@^3.0.3, braces@~3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
|
@ -10972,15 +10972,15 @@ eslint@^8.52.0, eslint@^8.56.0:
|
|||
text-table "^0.2.0"
|
||||
|
||||
eslint@^9.7.0:
|
||||
version "9.9.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.9.0.tgz#8d214e69ae4debeca7ae97daebbefe462072d975"
|
||||
integrity sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==
|
||||
version "9.9.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.9.1.tgz#147ac9305d56696fb84cf5bdecafd6517ddc77ec"
|
||||
integrity sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.11.0"
|
||||
"@eslint/config-array" "^0.17.1"
|
||||
"@eslint/config-array" "^0.18.0"
|
||||
"@eslint/eslintrc" "^3.1.0"
|
||||
"@eslint/js" "9.9.0"
|
||||
"@eslint/js" "9.9.1"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@humanwhocodes/retry" "^0.3.0"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
|
@ -15914,11 +15914,11 @@ methods@^1.1.2:
|
|||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||
|
||||
micromatch@^4.0.4, micromatch@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
dependencies:
|
||||
braces "^3.0.2"
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
miller-rabin@^4.0.0:
|
||||
|
@ -18394,9 +18394,9 @@ posthog-js@^1.118.0:
|
|||
preact "^10.19.3"
|
||||
|
||||
posthog-js@^1.13.4:
|
||||
version "1.157.2"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.157.2.tgz#dc2515818ead408aefb900e90c535fb57beb1f59"
|
||||
integrity sha512-ATYKGs+Q51u26nHHhrhWNh1whqFm7j/rwQQYw+y6/YzNmRlo+YsqrGZji9nqXb9/4fo0ModDr+ZmuOI3hKkUXA==
|
||||
version "1.160.0"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.160.0.tgz#ad686f3c161c7dc2ba716281b5cef94c64ce41b1"
|
||||
integrity sha512-K/RRgmPYIpP69nnveCJfkclb8VU+R+jsgqlrKaLGsM5CtQM9g01WOzAiT3u36WLswi58JiFMXgJtECKQuoqTgQ==
|
||||
dependencies:
|
||||
fflate "^0.4.8"
|
||||
preact "^10.19.3"
|
||||
|
@ -19817,28 +19817,28 @@ rollup@^3.27.1:
|
|||
fsevents "~2.3.2"
|
||||
|
||||
rollup@^4.20.0:
|
||||
version "4.21.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.0.tgz#28db5f5c556a5180361d35009979ccc749560b9d"
|
||||
integrity sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.2.tgz#f41f277a448d6264e923dd1ea179f0a926aaf9b7"
|
||||
integrity sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.5"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.21.0"
|
||||
"@rollup/rollup-android-arm64" "4.21.0"
|
||||
"@rollup/rollup-darwin-arm64" "4.21.0"
|
||||
"@rollup/rollup-darwin-x64" "4.21.0"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.21.0"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.21.0"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.21.0"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.21.0"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.21.0"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.21.0"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.21.0"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.21.0"
|
||||
"@rollup/rollup-linux-x64-musl" "4.21.0"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.21.0"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.21.0"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.21.0"
|
||||
"@rollup/rollup-android-arm-eabi" "4.21.2"
|
||||
"@rollup/rollup-android-arm64" "4.21.2"
|
||||
"@rollup/rollup-darwin-arm64" "4.21.2"
|
||||
"@rollup/rollup-darwin-x64" "4.21.2"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.21.2"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.21.2"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.21.2"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.21.2"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.21.2"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.21.2"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.21.2"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.21.2"
|
||||
"@rollup/rollup-linux-x64-musl" "4.21.2"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.21.2"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.21.2"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.21.2"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rollup@^4.9.4, rollup@^4.9.6:
|
||||
|
|
Loading…
Reference in New Issue