Merge branch 'master' into budi-8455-on-screen-load-open-side-panel-issue-side-panel-will-open

This commit is contained in:
Conor Webb 2024-07-16 13:07:49 +01:00 committed by GitHub
commit c2fda977bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 254 additions and 135 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.29.17", "version": "2.29.20",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -1,6 +1,6 @@
import env from "../environment" import env from "../environment"
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants" import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
import { getTenantId, getGlobalDBName } from "../context" import { getTenantId, getGlobalDBName, isMultiTenant } from "../context"
import { doWithDB, directCouchAllDbs } from "./db" import { doWithDB, directCouchAllDbs } from "./db"
import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata" import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
@ -213,6 +213,11 @@ export function isSqsEnabledForTenant(): boolean {
return false 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 // 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 // we're not actually using SQS, we're using Lucene and the tests pass due to
// parity. // parity.

View File

@ -2,6 +2,7 @@ import { GenericContainer, StartedTestContainer } from "testcontainers"
import { generator, structures } from "../../../tests" import { generator, structures } from "../../../tests"
import RedisWrapper from "../redis" import RedisWrapper from "../redis"
import { env } from "../.." import { env } from "../.."
import { randomUUID } from "crypto"
jest.setTimeout(30000) jest.setTimeout(30000)
@ -52,10 +53,10 @@ describe("redis", () => {
describe("bulkStore", () => { describe("bulkStore", () => {
function createRandomObject( function createRandomObject(
keyLength: number, keyLength: number,
valueGenerator: () => any = () => generator.word() valueGenerator: () => any = () => randomUUID()
) { ) {
return generator return generator
.unique(() => generator.word(), keyLength) .unique(() => randomUUID(), keyLength)
.reduce((acc, key) => { .reduce((acc, key) => {
acc[key] = valueGenerator() acc[key] = valueGenerator()
return acc return acc

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui" import { Select, Label, Checkbox, Body } from "@budibase/bbui"
import { tables, viewsV2 } from "stores/builder" import { tables, viewsV2 } from "stores/builder"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
@ -46,19 +46,35 @@
{#if parameters.confirm} {#if parameters.confirm}
<Label small>Title</Label> <Label small>Title</Label>
<Input placeholder="Delete Row" bind:value={parameters.customTitleText} /> <DrawerBindableInput
placeholder="Prompt User"
value={parameters.customTitleText}
on:change={e => (parameters.customTitleText = e.detail)}
{bindings}
/>
<Label small>Text</Label> <Label small>Text</Label>
<Input <DrawerBindableInput
placeholder="Are you sure you want to delete?" placeholder="Are you sure you want to continue?"
bind:value={parameters.confirmText} value={parameters.confirmText}
on:change={e => (parameters.confirmText = e.detail)}
{bindings}
/> />
<Label small>Confirm Text</Label> <Label small>Confirm Text</Label>
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} /> <DrawerBindableInput
placeholder="Confirm"
value={parameters.confirmButtonText}
on:change={e => (parameters.confirmButtonText = e.detail)}
{bindings}
/>
<Label small>Cancel Text</Label> <Label small>Cancel Text</Label>
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} /> <DrawerBindableInput
placeholder="Cancel"
value={parameters.cancelButtonText}
on:change={e => (parameters.cancelButtonText = e.detail)}
{bindings}
/>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -1,11 +1,12 @@
<script> <script>
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox } from "@budibase/bbui"
import { import {
selectedScreen, selectedScreen,
componentStore, componentStore,
tables, tables,
viewsV2, viewsV2,
} from "stores/builder" } from "stores/builder"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { getSchemaForDatasourcePlus } from "dataBinding" import { getSchemaForDatasourcePlus } from "dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils" import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
@ -73,22 +74,35 @@
{#if parameters.confirm} {#if parameters.confirm}
<Label small>Title</Label> <Label small>Title</Label>
<Input <DrawerBindableInput
placeholder="Duplicate Row" placeholder="Prompt User"
bind:value={parameters.customTitleText} value={parameters.customTitleText}
on:change={e => (parameters.customTitleText = e.detail)}
{bindings}
/> />
<Label small>Text</Label> <Label small>Text</Label>
<Input <DrawerBindableInput
placeholder="Are you sure you want to duplicate this row?" placeholder="Are you sure you want to continue?"
bind:value={parameters.confirmText} value={parameters.confirmText}
on:change={e => (parameters.confirmText = e.detail)}
{bindings}
/> />
<Label small>Confirm Text</Label> <Label small>Confirm Text</Label>
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} /> <DrawerBindableInput
placeholder="Confirm"
value={parameters.confirmButtonText}
on:change={e => (parameters.confirmButtonText = e.detail)}
{bindings}
/>
<Label small>Cancel Text</Label> <Label small>Cancel Text</Label>
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} /> <DrawerBindableInput
placeholder="Cancel"
value={parameters.cancelButtonText}
on:change={e => (parameters.cancelButtonText = e.detail)}
{bindings}
/>
{/if} {/if}
</div> </div>

View File

@ -1,5 +1,6 @@
<script> <script>
import { Select, Layout, Input, Checkbox } from "@budibase/bbui" import { Select, Layout, Checkbox } from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { datasources, integrations, queries } from "stores/builder" import { datasources, integrations, queries } from "stores/builder"
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte" import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
@ -58,37 +59,46 @@
text="Do not display default notification" text="Do not display default notification"
bind:value={parameters.notificationOverride} bind:value={parameters.notificationOverride}
/> />
<br />
{#if parameters.queryId} {#if parameters.queryId}
<Checkbox text="Require confirmation" bind:value={parameters.confirm} /> <Checkbox text="Require confirmation" bind:value={parameters.confirm} />
{#if parameters.confirm} {#if parameters.confirm}
<Input <div class="params">
<DrawerBindableInput
label="Title" label="Title"
placeholder="Execute Query" placeholder="Prompt User"
bind:value={parameters.customTitleText} value={parameters.customTitleText}
on:change={e => (parameters.customTitleText = e.detail)}
{bindings}
/>
<DrawerBindableInput
label="Message"
placeholder="Are you sure you want to continue?"
value={parameters.confirmText}
on:change={e => (parameters.confirmText = e.detail)}
{bindings}
/> />
<Input <DrawerBindableInput
label="Text"
placeholder="Are you sure you want to execute this query?"
bind:value={parameters.confirmText}
/>
<Input
label="Confirm Text" label="Confirm Text"
placeholder="Confirm" placeholder="Confirm"
bind:value={parameters.confirmButtonText} value={parameters.confirmButtonText}
on:change={e => (parameters.confirmButtonText = e.detail)}
{bindings}
/> />
<Input <DrawerBindableInput
label="Cancel Text" label="Cancel Text"
placeholder="Cancel" placeholder="Cancel"
bind:value={parameters.cancelButtonText} value={parameters.cancelButtonText}
on:change={e => (parameters.cancelButtonText = e.detail)}
{bindings}
/> />
</div>
{/if} {/if}
{#if query?.parameters?.length > 0} {#if query?.parameters?.length > 0}
<br />
<div class="params"> <div class="params">
<BindingBuilder <BindingBuilder
customParams={parameters.queryParams} customParams={parameters.queryParams}

View File

@ -1,11 +1,12 @@
<script> <script>
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox } from "@budibase/bbui"
import { import {
selectedScreen, selectedScreen,
componentStore, componentStore,
tables, tables,
viewsV2, viewsV2,
} from "stores/builder" } from "stores/builder"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { getSchemaForDatasourcePlus } from "dataBinding" import { getSchemaForDatasourcePlus } from "dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils" import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
@ -73,19 +74,35 @@
{#if parameters.confirm} {#if parameters.confirm}
<Label small>Title</Label> <Label small>Title</Label>
<Input placeholder="Save Row" bind:value={parameters.customTitleText} /> <DrawerBindableInput
placeholder="Prompt User"
value={parameters.customTitleText}
on:change={e => (parameters.customTitleText = e.detail)}
{bindings}
/>
<Label small>Text</Label> <Label small>Text</Label>
<Input <DrawerBindableInput
placeholder="Are you sure you want to save this row?" placeholder="Are you sure you want to continue?"
bind:value={parameters.confirmText} value={parameters.confirmText}
on:change={e => (parameters.confirmText = e.detail)}
{bindings}
/> />
<Label small>Confirm Text</Label> <Label small>Confirm Text</Label>
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} /> <DrawerBindableInput
placeholder="Confirm"
value={parameters.confirmButtonText}
on:change={e => (parameters.confirmButtonText = e.detail)}
{bindings}
/>
<Label small>Cancel Text</Label> <Label small>Cancel Text</Label>
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} /> <DrawerBindableInput
placeholder="Cancel"
value={parameters.cancelButtonText}
on:change={e => (parameters.cancelButtonText = e.detail)}
{bindings}
/>
{/if} {/if}
</div> </div>

View File

@ -124,7 +124,7 @@
<PropertyControl <PropertyControl
label="Text align" label="Text align"
control={BarButtonList} control={BarButtonList}
onChange={align => nav.syncAppNavigation({ textAlign: align })} onChange={align => update("textAlign", align)}
value={$nav.textAlign} value={$nav.textAlign}
props={{ props={{
options: alignmentOptions, options: alignmentOptions,

View File

@ -1,5 +1,4 @@
import newid from "../../../db/newid" import { context, utils } from "@budibase/backend-core"
import { context } from "@budibase/backend-core"
/** /**
* This is used to pass around information about the deployment that is occurring * This is used to pass around information about the deployment that is occurring
@ -12,7 +11,7 @@ export default class Deployment {
appUrl?: string appUrl?: string
constructor(id = null) { constructor(id = null) {
this._id = id || newid() this._id = id || utils.newid()
} }
setVerification(verification: any) { setVerification(verification: any) {

View File

@ -203,7 +203,7 @@ describe("/permission", () => {
// replicate changes before checking permissions // replicate changes before checking permissions
await config.publish() await config.publish()
await config.api.viewV2.publicSearch(view.id, undefined, { status: 403 }) await config.api.viewV2.publicSearch(view.id, undefined, { status: 401 })
}) })
it("should ignore the view permissions if the flag is not on", async () => { it("should ignore the view permissions if the flag is not on", async () => {
@ -221,7 +221,7 @@ describe("/permission", () => {
await config.publish() await config.publish()
await config.api.viewV2.publicSearch(view.id, undefined, { await config.api.viewV2.publicSearch(view.id, undefined, {
status: 403, status: 401,
}) })
}) })
@ -250,8 +250,8 @@ describe("/permission", () => {
.send(basicRow(table._id)) .send(basicRow(table._id))
.set(config.publicHeaders()) .set(config.publicHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(403) .expect(401)
expect(res.status).toEqual(403) expect(res.status).toEqual(401)
}) })
}) })

View File

@ -819,7 +819,10 @@ describe.each([
const table = await config.api.table.save(tableRequest) const table = await config.api.table.save(tableRequest)
const stringValue = generator.word() const stringValue = generator.word()
const naturalValue = generator.integer({ min: 0, max: 1000 })
// MySQL and MariaDB auto-increment fields have a minimum value of 1. If
// you try to save a row with a value of 0 it will use 1 instead.
const naturalValue = generator.integer({ min: 1, max: 1000 })
const existing = await config.api.row.save(table._id!, { const existing = await config.api.row.save(table._id!, {
string: stringValue, string: stringValue,
@ -1457,17 +1460,12 @@ describe.each([
delete tableRequest.schema.id delete tableRequest.schema.id
const table = await config.api.table.save(tableRequest) const table = await config.api.table.save(tableRequest)
const toCreate = generator
.unique(() => generator.integer({ min: 0, max: 10000 }), 10)
.map(number => ({ number, string: generator.word({ length: 30 }) }))
const rows = await Promise.all( const rows = await Promise.all(
generator toCreate.map(d => config.api.row.save(table._id!, d))
.unique(
() => ({
string: generator.word({ length: 30 }),
number: generator.integer({ min: 0, max: 10000 }),
}),
10
)
.map(d => config.api.row.save(table._id!, d))
) )
const res = await config.api.row.exportRows(table._id!, { const res = await config.api.row.exportRows(table._id!, {

View File

@ -4,7 +4,12 @@ import {
getDatasource, getDatasource,
knexClient, knexClient,
} from "../../../integrations/tests/utils" } from "../../../integrations/tests/utils"
import { db as dbCore, utils } from "@budibase/backend-core" import {
db as dbCore,
MAX_VALID_DATE,
MIN_VALID_DATE,
utils,
} from "@budibase/backend-core"
import * as setup from "./utilities" import * as setup from "./utilities"
import { import {
@ -1098,19 +1103,35 @@ describe.each([
}).toFindNothing() }).toFindNothing()
}) })
// We never implemented half-open ranges in Lucene. it("greater than equal to", async () => {
!isLucene &&
it("can search using just a low value", async () => {
await expectQuery({ await expectQuery({
range: { age: { low: 5 } }, range: {
age: { low: 10, high: Number.MAX_SAFE_INTEGER },
},
}).toContainExactly([{ age: 10 }]) }).toContainExactly([{ age: 10 }])
}) })
// We never implemented half-open ranges in Lucene. it("greater than", async () => {
!isLucene &&
it("can search using just a high value", async () => {
await expectQuery({ await expectQuery({
range: { age: { high: 5 } }, range: {
age: { low: 5, high: Number.MAX_SAFE_INTEGER },
},
}).toContainExactly([{ age: 10 }])
})
it("less than equal to", async () => {
await expectQuery({
range: {
age: { high: 1, low: Number.MIN_SAFE_INTEGER },
},
}).toContainExactly([{ age: 1 }])
})
it("less than", async () => {
await expectQuery({
range: {
age: { high: 5, low: Number.MIN_SAFE_INTEGER },
},
}).toContainExactly([{ age: 1 }]) }).toContainExactly([{ age: 1 }])
}) })
}) })
@ -1232,19 +1253,27 @@ describe.each([
}).toFindNothing() }).toFindNothing()
}) })
// We never implemented half-open ranges in Lucene. it("greater than equal to", async () => {
!isLucene &&
it("can search using just a low value", async () => {
await expectQuery({ await expectQuery({
range: { dob: { low: JAN_5TH } }, range: { dob: { low: JAN_10TH, high: MAX_VALID_DATE.toISOString() } },
}).toContainExactly([{ dob: JAN_10TH }]) }).toContainExactly([{ dob: JAN_10TH }])
}) })
// We never implemented half-open ranges in Lucene. it("greater than", async () => {
!isLucene &&
it("can search using just a high value", async () => {
await expectQuery({ await expectQuery({
range: { dob: { high: JAN_5TH } }, range: { dob: { low: JAN_5TH, high: MAX_VALID_DATE.toISOString() } },
}).toContainExactly([{ dob: JAN_10TH }])
})
it("less than equal to", async () => {
await expectQuery({
range: { dob: { high: JAN_1ST, low: MIN_VALID_DATE.toISOString() } },
}).toContainExactly([{ dob: JAN_1ST }])
})
it("less than", async () => {
await expectQuery({
range: { dob: { high: JAN_5TH, low: MIN_VALID_DATE.toISOString() } },
}).toContainExactly([{ dob: JAN_1ST }]) }).toContainExactly([{ dob: JAN_1ST }])
}) })
}) })

View File

@ -151,7 +151,7 @@ export const checkPermissionsEndpoint = async ({
await exports await exports
.createRequest(config.request, method, url, body) .createRequest(config.request, method, url, body)
.set(failHeader) .set(failHeader)
.expect(403) .expect(401)
} }
export const getDB = () => { export const getDB = () => {

View File

@ -1490,7 +1490,7 @@ describe.each([
it("does not allow public users to fetch by default", async () => { it("does not allow public users to fetch by default", async () => {
await config.publish() await config.publish()
await config.api.viewV2.publicSearch(view.id, undefined, { await config.api.viewV2.publicSearch(view.id, undefined, {
status: 403, status: 401,
}) })
}) })
@ -1534,7 +1534,7 @@ describe.each([
await config.publish() await config.publish()
await config.api.viewV2.publicSearch(view.id, undefined, { await config.api.viewV2.publicSearch(view.id, undefined, {
status: 403, status: 401,
}) })
}) })
}) })

View File

@ -1,10 +1,9 @@
import { Thread, ThreadType } from "../threads" import { Thread, ThreadType } from "../threads"
import { definitions } from "./triggerInfo" import { definitions } from "./triggerInfo"
import { automationQueue } from "./bullboard" import { automationQueue } from "./bullboard"
import newid from "../db/newid"
import { updateEntityMetadata } from "../utilities" import { updateEntityMetadata } from "../utilities"
import { MetadataTypes } from "../constants" import { MetadataTypes } from "../constants"
import { db as dbCore, context } from "@budibase/backend-core" import { db as dbCore, context, utils } from "@budibase/backend-core"
import { getAutomationMetadataParams } from "../db/utils" import { getAutomationMetadataParams } from "../db/utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
@ -207,7 +206,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
) )
} }
// make a job id rather than letting Bull decide, makes it easier to handle on way out // make a job id rather than letting Bull decide, makes it easier to handle on way out
const jobId = `${appId}_cron_${newid()}` const jobId = `${appId}_cron_${utils.newid()}`
const job: any = await automationQueue.add( const job: any = await automationQueue.add(
{ {
automation, automation,

View File

@ -1,9 +1,8 @@
import newid from "./newid"
import { Row, Document, DBView } from "@budibase/types" import { Row, Document, DBView } from "@budibase/types"
// bypass the main application db config // bypass the main application db config
// use in memory pouchdb directly // use in memory pouchdb directly
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore, utils } from "@budibase/backend-core"
const Pouch = dbCore.getPouch({ inMemory: true }) const Pouch = dbCore.getPouch({ inMemory: true })
@ -16,7 +15,7 @@ export async function runView(
// use a different ID each time for the DB, make sure they // use a different ID each time for the DB, make sure they
// are always unique for each query, don't want overlap // are always unique for each query, don't want overlap
// which could cause 409s // which could cause 409s
const db = new Pouch(newid()) const db = new Pouch(utils.newid())
try { try {
// write all the docs to the in memory Pouch (remove revs) // write all the docs to the in memory Pouch (remove revs)
await db.bulkDocs( await db.bulkDocs(

View File

@ -1,5 +0,0 @@
import { v4 } from "uuid"
export default function (): string {
return v4().replace(/-/g, "")
}

View File

@ -1,5 +1,4 @@
import newid from "./newid" import { context, db as dbCore, utils } from "@budibase/backend-core"
import { context, db as dbCore } from "@budibase/backend-core"
import { import {
DatabaseQueryOpts, DatabaseQueryOpts,
Datasource, Datasource,
@ -15,6 +14,8 @@ import {
export { DocumentType, VirtualDocumentType } from "@budibase/types" export { DocumentType, VirtualDocumentType } from "@budibase/types"
const newid = utils.newid
type Optional = string | null type Optional = string | null
export const enum AppStatus { export const enum AppStatus {

View File

@ -96,7 +96,7 @@ const authorized =
} }
if (!ctx.user) { if (!ctx.user) {
return ctx.throw(403, "No user info found") return ctx.throw(401, "No user info found")
} }
// get the resource roles // get the resource roles
@ -148,7 +148,7 @@ const authorized =
// check authenticated // check authenticated
if (!ctx.isAuthenticated) { if (!ctx.isAuthenticated) {
return ctx.throw(403, "Session not authenticated") return ctx.throw(401, "Session not authenticated")
} }
// check general builder stuff, this middleware is a good way // check general builder stuff, this middleware is a good way

View File

@ -105,7 +105,7 @@ describe("Authorization middleware", () => {
it("throws when no user data is present in context", async () => { it("throws when no user data is present in context", async () => {
await config.executeMiddleware() await config.executeMiddleware()
expect(config.throw).toHaveBeenCalledWith(403, "No user info found") expect(config.throw).toHaveBeenCalledWith(401, "No user info found")
}) })
it("passes on to next() middleware if user is an admin", async () => { it("passes on to next() middleware if user is an admin", async () => {
@ -157,7 +157,7 @@ describe("Authorization middleware", () => {
await config.executeMiddleware() await config.executeMiddleware()
expect(config.throw).toHaveBeenCalledWith( expect(config.throw).toHaveBeenCalledWith(
403, 401,
"Session not authenticated" "Session not authenticated"
) )
}) })

View File

@ -45,13 +45,10 @@ import {
getTableIDList, getTableIDList,
} from "./filters" } from "./filters"
import { dataFilters } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import { DEFAULT_TABLE_IDS } from "../../../../constants"
const builder = new sql.Sql(SqlClient.SQL_LITE) const builder = new sql.Sql(SqlClient.SQL_LITE)
const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`) const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
const USER_COLUMN_PREFIX_REGEX = new RegExp( const MISSING_TABLE_REGX = new RegExp(`no such table: .+`)
`no such column: .+${USER_COLUMN_PREFIX}`
)
function buildInternalFieldList( function buildInternalFieldList(
table: Table, table: Table,
@ -240,10 +237,10 @@ async function runSqlQuery(
function resyncDefinitionsRequired(status: number, message: string) { function resyncDefinitionsRequired(status: number, message: string) {
// pre data_ prefix on column names, need to resync // pre data_ prefix on column names, need to resync
return ( return (
(status === 400 && message?.match(USER_COLUMN_PREFIX_REGEX)) || // there are tables missing - try a resync
// default tables aren't included in definition (status === 400 && message.match(MISSING_TABLE_REGX)) ||
(status === 400 && // there are columns missing - try a resync
DEFAULT_TABLE_IDS.find(tableId => message?.includes(tableId))) || (status === 400 && message.match(MISSING_COLUMN_REGEX)) ||
// no design document found, needs a full sync // no design document found, needs a full sync
(status === 404 && message?.includes(SQLITE_DESIGN_DOC_ID)) (status === 404 && message?.includes(SQLITE_DESIGN_DOC_ID))
) )
@ -251,7 +248,8 @@ function resyncDefinitionsRequired(status: number, message: string) {
export async function search( export async function search(
options: RowSearchParams, options: RowSearchParams,
table: Table table: Table,
opts?: { retrying?: boolean }
): Promise<SearchResponse<Row>> { ): Promise<SearchResponse<Row>> {
let { paginate, query, ...params } = options let { paginate, query, ...params } = options
@ -376,9 +374,9 @@ export async function search(
return response return response
} catch (err: any) { } catch (err: any) {
const msg = typeof err === "string" ? err : err.message const msg = typeof err === "string" ? err : err.message
if (resyncDefinitionsRequired(err.status, msg)) { if (!opts?.retrying && resyncDefinitionsRequired(err.status, msg)) {
await sdk.tables.sqs.syncDefinition() await sdk.tables.sqs.syncDefinition()
return search(options, table) return search(options, table, { retrying: true })
} }
// previously the internal table didn't error when a column didn't exist in search // previously the internal table didn't error when a column didn't exist in search
if (err.status === 400 && msg?.match(MISSING_COLUMN_REGEX)) { if (err.status === 400 && msg?.match(MISSING_COLUMN_REGEX)) {

View File

@ -127,9 +127,14 @@ function mapTable(table: Table): SQLiteTables {
// nothing exists, need to iterate though existing tables // nothing exists, need to iterate though existing tables
async function buildBaseDefinition(): Promise<PreSaveSQLiteDefinition> { async function buildBaseDefinition(): Promise<PreSaveSQLiteDefinition> {
const tables = await tablesSdk.getAllInternalTables() const tables = await tablesSdk.getAllInternalTables()
const defaultTables = DEFAULT_TABLES for (const defaultTable of DEFAULT_TABLES) {
// the default table doesn't exist in Couch, use the in-memory representation
if (!tables.find(table => table._id === defaultTable._id)) {
tables.push(defaultTable)
}
}
const definition = sql.designDoc.base("tableId") const definition = sql.designDoc.base("tableId")
for (let table of tables.concat(defaultTables)) { for (let table of tables) {
definition.sql.tables = { definition.sql.tables = {
...definition.sql.tables, ...definition.sql.tables,
...mapTable(table), ...mapTable(table),

View File

@ -26,6 +26,7 @@ import {
roles, roles,
sessions, sessions,
tenancy, tenancy,
utils,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { import {
app as appController, app as appController,
@ -40,7 +41,6 @@ import {
} from "./controllers" } from "./controllers"
import { cleanup } from "../../utilities/fileSystem" import { cleanup } from "../../utilities/fileSystem"
import newid from "../../db/newid"
import { generateUserMetadataID } from "../../db/utils" import { generateUserMetadataID } from "../../db/utils"
import { startup } from "../../startup" import { startup } from "../../startup"
import supertest from "supertest" import supertest from "supertest"
@ -74,6 +74,8 @@ import { cloneDeep } from "lodash"
import jwt, { Secret } from "jsonwebtoken" import jwt, { Secret } from "jsonwebtoken"
import { Server } from "http" import { Server } from "http"
const newid = utils.newid
mocks.licenses.init(pro) mocks.licenses.init(pro)
// use unlimited license by default // use unlimited license by default

View File

@ -106,7 +106,7 @@ export const NoEmptyFilterStrings = [
OperatorOptions.NotEquals.value, OperatorOptions.NotEquals.value,
OperatorOptions.Contains.value, OperatorOptions.Contains.value,
OperatorOptions.NotContains.value, OperatorOptions.NotContains.value,
OperatorOptions.NotContains.value, OperatorOptions.ContainsAny.value,
OperatorOptions.In.value, OperatorOptions.In.value,
] as (keyof SearchQueryFields)[] ] as (keyof SearchQueryFields)[]

View File

@ -0,0 +1,31 @@
const nodemailer = require("nodemailer")
const options = {
port: 587,
host: "smtp.ethereal.email",
secure: false,
auth: {
user: "seamus99@ethereal.email",
pass: "5ghVteZAqj6jkKJF9R",
},
}
const transporter = nodemailer.createTransport(options)
transporter.verify(function (error) {
if (error) {
console.log(error)
} else {
console.log("Ethereal server is ready to take our messages")
}
})
const message = {
from: "from@example.com",
to: "to@example.com",
subject: "Did this email arrive?",
html: "Hello World!",
}
transporter.sendMail(message).then(response => {
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
})