Merge branch 'master' into feature/form-screen-template
This commit is contained in:
commit
79ed0e0d89
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.20.10",
|
"version": "2.20.11",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ab324e35d855012bd0f49caa53c6dd765223c6fa
|
Subproject commit de6d44c372a7f48ca0ce8c6c0c19311d4bc21646
|
|
@ -6,7 +6,7 @@ import { Plugin } from "@budibase/types"
|
||||||
|
|
||||||
// URLS
|
// URLS
|
||||||
|
|
||||||
export function enrichPluginURLs(plugins: Plugin[]) {
|
export function enrichPluginURLs(plugins?: Plugin[]): Plugin[] {
|
||||||
if (!plugins || !plugins.length) {
|
if (!plugins || !plugins.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ export enum Databases {
|
||||||
WRITE_THROUGH = "writeThrough",
|
WRITE_THROUGH = "writeThrough",
|
||||||
LOCKS = "locks",
|
LOCKS = "locks",
|
||||||
SOCKET_IO = "socket_io",
|
SOCKET_IO = "socket_io",
|
||||||
|
BPM_EVENTS = "bpmEvents",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import FontAwesomeIcon from "./FontAwesomeIcon.svelte"
|
import FontAwesomeIcon from "./FontAwesomeIcon.svelte"
|
||||||
import { Popover, Heading, Body } from "@budibase/bbui"
|
import { Popover, Heading, Body } from "@budibase/bbui"
|
||||||
import { licensing } from "stores/portal"
|
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
import { isPremiumOrAbove } from "helpers/planTitle"
|
||||||
|
|
||||||
$: isBusinessAndAbove =
|
$: premiumOrAboveLicense = isPremiumOrAbove($licensing?.license.plan.type)
|
||||||
$licensing.isBusinessPlan || $licensing.isEnterprisePlan
|
|
||||||
|
|
||||||
let show
|
let show
|
||||||
let hide
|
let hide
|
||||||
|
@ -56,22 +56,25 @@
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
|
||||||
<a
|
<a
|
||||||
href={isBusinessAndAbove
|
href={premiumOrAboveLicense
|
||||||
? "mailto:support@budibase.com"
|
? "mailto:support@budibase.com"
|
||||||
: "/builder/portal/account/usage"}
|
: "/builder/portal/account/usage"}
|
||||||
>
|
>
|
||||||
<div class="premiumLinkContent" class:disabled={!isBusinessAndAbove}>
|
<div
|
||||||
|
class="premiumLinkContent"
|
||||||
|
class:disabled={!premiumOrAboveLicense}
|
||||||
|
>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
<FontAwesomeIcon name="fa-solid fa-envelope" />
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">Email support</Body>
|
<Body size="S">Email support</Body>
|
||||||
</div>
|
</div>
|
||||||
{#if !isBusinessAndAbove}
|
{#if !premiumOrAboveLicense}
|
||||||
<div class="premiumBadge">
|
<div class="premiumBadge">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<FontAwesomeIcon name="fa-solid fa-lock" />
|
<FontAwesomeIcon name="fa-solid fa-lock" />
|
||||||
</div>
|
</div>
|
||||||
<Body size="XS">Business</Body>
|
<Body size="XS">Premium</Body>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Label, Select, Body, Multiselect } from "@budibase/bbui"
|
import { Label, Select, Body } from "@budibase/bbui"
|
||||||
import { findAllMatchingComponents, findComponent } from "helpers/components"
|
|
||||||
import { selectedScreen } from "stores/builder"
|
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
import ColumnEditor from "../../ColumnEditor/ColumnEditor.svelte"
|
||||||
|
import { findAllMatchingComponents } from "helpers/components"
|
||||||
|
import { selectedScreen } from "stores/builder"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
|
||||||
|
@ -18,37 +18,65 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const DELIMITERS = [
|
||||||
|
{
|
||||||
|
label: ",",
|
||||||
|
value: ",",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: ";",
|
||||||
|
value: ";",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: ":",
|
||||||
|
value: ":",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "|",
|
||||||
|
value: "|",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "~",
|
||||||
|
value: "~",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "[tab]",
|
||||||
|
value: "\t",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "[space]",
|
||||||
|
value: " ",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
$: tables = findAllMatchingComponents($selectedScreen?.props, component =>
|
$: tables = findAllMatchingComponents($selectedScreen?.props, component =>
|
||||||
component._component.endsWith("table")
|
component._component.endsWith("table")
|
||||||
).map(table => ({
|
)
|
||||||
label: table._instanceName,
|
|
||||||
value: table._id,
|
|
||||||
}))
|
|
||||||
$: tableBlocks = findAllMatchingComponents(
|
$: tableBlocks = findAllMatchingComponents(
|
||||||
$selectedScreen?.props,
|
$selectedScreen?.props,
|
||||||
component => component._component.endsWith("tableblock")
|
component => component._component.endsWith("tableblock")
|
||||||
).map(block => ({
|
)
|
||||||
label: block._instanceName,
|
$: components = tables.concat(tableBlocks)
|
||||||
value: `${block._id}-table`,
|
$: componentOptions = components.map(table => ({
|
||||||
|
label: table._instanceName,
|
||||||
|
value: table._component.includes("tableblock")
|
||||||
|
? `${table._id}-table`
|
||||||
|
: table._id,
|
||||||
}))
|
}))
|
||||||
$: componentOptions = tables.concat(tableBlocks)
|
$: selectedTableId = parameters.tableComponentId?.includes("-")
|
||||||
$: columnOptions = getColumnOptions(parameters.tableComponentId)
|
? parameters.tableComponentId.split("-")[0]
|
||||||
|
: parameters.tableComponentId
|
||||||
const getColumnOptions = tableId => {
|
$: selectedTable = components.find(
|
||||||
// Strip block suffix if block component
|
component => component._id === selectedTableId
|
||||||
if (tableId?.includes("-")) {
|
)
|
||||||
tableId = tableId.split("-")[0]
|
|
||||||
}
|
|
||||||
const selectedTable = findComponent($selectedScreen?.props, tableId)
|
|
||||||
const datasource = getDatasourceForProvider($selectedScreen, selectedTable)
|
|
||||||
const { schema } = getSchemaForDatasource($selectedScreen, datasource)
|
|
||||||
return Object.keys(schema || {})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!parameters.type) {
|
if (!parameters.type) {
|
||||||
parameters.type = "csv"
|
parameters.type = "csv"
|
||||||
}
|
}
|
||||||
|
if (!parameters.delimiter) {
|
||||||
|
parameters.delimiter = ","
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -67,13 +95,29 @@
|
||||||
options={componentOptions}
|
options={componentOptions}
|
||||||
on:change={() => (parameters.columns = [])}
|
on:change={() => (parameters.columns = [])}
|
||||||
/>
|
/>
|
||||||
|
<span />
|
||||||
<Label small>Export as</Label>
|
<Label small>Export as</Label>
|
||||||
<Select bind:value={parameters.type} options={FORMATS} />
|
<Select bind:value={parameters.type} options={FORMATS} />
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.delimiter}
|
||||||
|
placeholder={null}
|
||||||
|
options={DELIMITERS}
|
||||||
|
disabled={parameters.type !== "csv"}
|
||||||
|
/>
|
||||||
<Label small>Export columns</Label>
|
<Label small>Export columns</Label>
|
||||||
<Multiselect
|
<ColumnEditor
|
||||||
placeholder="All columns"
|
|
||||||
bind:value={parameters.columns}
|
bind:value={parameters.columns}
|
||||||
options={columnOptions}
|
allowCellEditing={false}
|
||||||
|
componentInstance={selectedTable}
|
||||||
|
on:change={e => {
|
||||||
|
const columns = e.detail
|
||||||
|
parameters.customHeaders = columns.reduce((headerMap, column) => {
|
||||||
|
return {
|
||||||
|
[column.name]: column.displayName,
|
||||||
|
...headerMap,
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -97,8 +141,8 @@
|
||||||
.params {
|
.params {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-xs);
|
column-gap: var(--spacing-xs);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-m);
|
||||||
grid-template-columns: 90px 1fr;
|
grid-template-columns: 90px 1fr 90px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -29,6 +29,12 @@
|
||||||
allowLinks: true,
|
allowLinks: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: {
|
||||||
|
value = (value || []).filter(
|
||||||
|
column => (schema || {})[column.name || column] !== undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const getText = value => {
|
const getText = value => {
|
||||||
if (!value?.length) {
|
if (!value?.length) {
|
||||||
return "All columns"
|
return "All columns"
|
||||||
|
|
|
@ -17,6 +17,10 @@ export function breakQueryString(qs) {
|
||||||
return paramObj
|
return paramObj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEncoded(str) {
|
||||||
|
return typeof str == "string" && decodeURIComponent(str) !== str
|
||||||
|
}
|
||||||
|
|
||||||
export function buildQueryString(obj) {
|
export function buildQueryString(obj) {
|
||||||
let str = ""
|
let str = ""
|
||||||
if (obj) {
|
if (obj) {
|
||||||
|
@ -35,7 +39,7 @@ export function buildQueryString(obj) {
|
||||||
value = value.replace(binding, marker)
|
value = value.replace(binding, marker)
|
||||||
bindingMarkers[marker] = binding
|
bindingMarkers[marker] = binding
|
||||||
})
|
})
|
||||||
let encoded = encodeURIComponent(value || "")
|
let encoded = isEncoded(value) ? value : encodeURIComponent(value || "")
|
||||||
Object.entries(bindingMarkers).forEach(([marker, binding]) => {
|
Object.entries(bindingMarkers).forEach(([marker, binding]) => {
|
||||||
encoded = encoded.replace(marker, binding)
|
encoded = encoded.replace(marker, binding)
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,3 +25,7 @@ export function getFormattedPlanName(userPlanType) {
|
||||||
}
|
}
|
||||||
return `${planName} Plan`
|
return `${planName} Plan`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPremiumOrAbove(userPlanType) {
|
||||||
|
return ![PlanType.PRO, PlanType.TEAM, PlanType.FREE].includes(userPlanType)
|
||||||
|
}
|
||||||
|
|
|
@ -39,4 +39,11 @@ describe("check query string utils", () => {
|
||||||
expect(broken.key1).toBe(obj2.key1)
|
expect(broken.key1).toBe(obj2.key1)
|
||||||
expect(broken.key2).toBe(obj2.key2)
|
expect(broken.key2).toBe(obj2.key2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should not encode a URL more than once when building the query string", () => {
|
||||||
|
const queryString = buildQueryString({
|
||||||
|
values: "a%2Cb%2Cc",
|
||||||
|
})
|
||||||
|
expect(queryString).toBe("values=a%2Cb%2Cc")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -341,7 +341,11 @@ const exportDataHandler = async action => {
|
||||||
tableId: selection.tableId,
|
tableId: selection.tableId,
|
||||||
rows: selection.selectedRows,
|
rows: selection.selectedRows,
|
||||||
format: action.parameters.type,
|
format: action.parameters.type,
|
||||||
columns: action.parameters.columns,
|
columns: action.parameters.columns?.map(
|
||||||
|
column => column.name || column
|
||||||
|
),
|
||||||
|
delimiter: action.parameters.delimiter,
|
||||||
|
customHeaders: action.parameters.customHeaders,
|
||||||
})
|
})
|
||||||
download(
|
download(
|
||||||
new Blob([data], { type: "text/plain" }),
|
new Blob([data], { type: "text/plain" }),
|
||||||
|
|
|
@ -89,13 +89,24 @@ export const buildRowEndpoints = API => ({
|
||||||
* @param rows the array of rows to export
|
* @param rows the array of rows to export
|
||||||
* @param format the format to export (csv or json)
|
* @param format the format to export (csv or json)
|
||||||
* @param columns which columns to export (all if undefined)
|
* @param columns which columns to export (all if undefined)
|
||||||
|
* @param delimiter how values should be separated in a CSV (default is comma)
|
||||||
*/
|
*/
|
||||||
exportRows: async ({ tableId, rows, format, columns, search }) => {
|
exportRows: async ({
|
||||||
|
tableId,
|
||||||
|
rows,
|
||||||
|
format,
|
||||||
|
columns,
|
||||||
|
search,
|
||||||
|
delimiter,
|
||||||
|
customHeaders,
|
||||||
|
}) => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/${tableId}/rows/exportRows?format=${format}`,
|
url: `/api/${tableId}/rows/exportRows?format=${format}`,
|
||||||
body: {
|
body: {
|
||||||
rows,
|
rows,
|
||||||
columns,
|
columns,
|
||||||
|
delimiter,
|
||||||
|
customHeaders,
|
||||||
...search,
|
...search,
|
||||||
},
|
},
|
||||||
parseResponse: async response => {
|
parseResponse: async response => {
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
"build:sdk": "yarn run generate && rollup -c"
|
"build:sdk": "yarn run generate && rollup -c"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^18.0.0",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"rollup": "^2.44.0",
|
"rollup": "^4.9.6",
|
||||||
"rollup-plugin-terser": "^7.0.2"
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"rollup-plugin-polyfill-node": "^0.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ set -e
|
||||||
|
|
||||||
if [[ -n $CI ]]
|
if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot"
|
export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS"
|
||||||
echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@"
|
echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@"
|
||||||
jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@
|
jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
export NODE_OPTIONS="--no-node-snapshot"
|
export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS"
|
||||||
echo "jest --coverage --maxWorkers=2 --forceExit $@"
|
echo "jest --coverage --maxWorkers=2 --forceExit $@"
|
||||||
jest --coverage --maxWorkers=2 --forceExit $@
|
jest --coverage --maxWorkers=2 --forceExit $@
|
||||||
fi
|
fi
|
|
@ -47,6 +47,9 @@ import {
|
||||||
PlanType,
|
PlanType,
|
||||||
Screen,
|
Screen,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
|
CreateAppRequest,
|
||||||
|
FetchAppDefinitionResponse,
|
||||||
|
FetchAppPackageResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
@ -58,23 +61,23 @@ import * as appMigrations from "../../appMigrations"
|
||||||
async function getLayouts() {
|
async function getLayouts() {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
return (
|
return (
|
||||||
await db.allDocs(
|
await db.allDocs<Layout>(
|
||||||
getLayoutParams(null, {
|
getLayoutParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).rows.map((row: any) => row.doc)
|
).rows.map(row => row.doc!)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getScreens() {
|
async function getScreens() {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
return (
|
return (
|
||||||
await db.allDocs(
|
await db.allDocs<Screen>(
|
||||||
getScreenParams(null, {
|
getScreenParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).rows.map((row: any) => row.doc)
|
).rows.map(row => row.doc!)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserRoleId(ctx: UserCtx) {
|
function getUserRoleId(ctx: UserCtx) {
|
||||||
|
@ -116,8 +119,8 @@ function checkAppName(
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppTemplate {
|
interface AppTemplate {
|
||||||
templateString: string
|
templateString?: string
|
||||||
useTemplate: string
|
useTemplate?: string
|
||||||
file?: {
|
file?: {
|
||||||
type: string
|
type: string
|
||||||
path: string
|
path: string
|
||||||
|
@ -174,14 +177,16 @@ export const addSampleData = async (ctx: UserCtx) => {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx<void, App[]>) {
|
||||||
ctx.body = await sdk.applications.fetch(
|
ctx.body = await sdk.applications.fetch(
|
||||||
ctx.query.status as AppStatus,
|
ctx.query.status as AppStatus,
|
||||||
ctx.user
|
ctx.user
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAppDefinition(ctx: UserCtx) {
|
export async function fetchAppDefinition(
|
||||||
|
ctx: UserCtx<void, FetchAppDefinitionResponse>
|
||||||
|
) {
|
||||||
const layouts = await getLayouts()
|
const layouts = await getLayouts()
|
||||||
const userRoleId = getUserRoleId(ctx)
|
const userRoleId = getUserRoleId(ctx)
|
||||||
const accessController = new roles.AccessController()
|
const accessController = new roles.AccessController()
|
||||||
|
@ -196,10 +201,12 @@ export async function fetchAppDefinition(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAppPackage(ctx: UserCtx) {
|
export async function fetchAppPackage(
|
||||||
|
ctx: UserCtx<void, FetchAppPackageResponse>
|
||||||
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
let application = await db.get<any>(DocumentType.APP_METADATA)
|
let application = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
const layouts = await getLayouts()
|
const layouts = await getLayouts()
|
||||||
let screens = await getScreens()
|
let screens = await getScreens()
|
||||||
const license = await licensing.cache.getCachedLicense()
|
const license = await licensing.cache.getCachedLicense()
|
||||||
|
@ -231,17 +238,21 @@ export async function fetchAppPackage(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performAppCreate(ctx: UserCtx) {
|
async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
||||||
const name = ctx.request.body.name,
|
const {
|
||||||
possibleUrl = ctx.request.body.url,
|
name,
|
||||||
encryptionPassword = ctx.request.body.encryptionPassword
|
url,
|
||||||
|
encryptionPassword,
|
||||||
|
useTemplate,
|
||||||
|
templateKey,
|
||||||
|
templateString,
|
||||||
|
} = ctx.request.body
|
||||||
|
|
||||||
checkAppName(ctx, apps, name)
|
checkAppName(ctx, apps, name)
|
||||||
const url = sdk.applications.getAppUrl({ name, url: possibleUrl })
|
const appUrl = sdk.applications.getAppUrl({ name, url })
|
||||||
checkAppUrl(ctx, apps, url)
|
checkAppUrl(ctx, apps, appUrl)
|
||||||
|
|
||||||
const { useTemplate, templateKey, templateString } = ctx.request.body
|
|
||||||
const instanceConfig: AppTemplate = {
|
const instanceConfig: AppTemplate = {
|
||||||
useTemplate,
|
useTemplate,
|
||||||
key: templateKey,
|
key: templateKey,
|
||||||
|
@ -268,7 +279,7 @@ async function performAppCreate(ctx: UserCtx) {
|
||||||
version: envCore.VERSION,
|
version: envCore.VERSION,
|
||||||
componentLibraries: ["@budibase/standard-components"],
|
componentLibraries: ["@budibase/standard-components"],
|
||||||
name: name,
|
name: name,
|
||||||
url: url,
|
url: appUrl,
|
||||||
template: templateKey,
|
template: templateKey,
|
||||||
instance,
|
instance,
|
||||||
tenantId: tenancy.getTenantId(),
|
tenantId: tenancy.getTenantId(),
|
||||||
|
@ -420,7 +431,9 @@ export async function create(ctx: UserCtx) {
|
||||||
|
|
||||||
// This endpoint currently operates as a PATCH rather than a PUT
|
// This endpoint currently operates as a PATCH rather than a PUT
|
||||||
// Thus name and url fields are handled only if present
|
// Thus name and url fields are handled only if present
|
||||||
export async function update(ctx: UserCtx) {
|
export async function update(
|
||||||
|
ctx: UserCtx<{ name?: string; url?: string }, App>
|
||||||
|
) {
|
||||||
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
||||||
// validation
|
// validation
|
||||||
const name = ctx.request.body.name,
|
const name = ctx.request.body.name,
|
||||||
|
@ -493,7 +506,7 @@ export async function revertClient(ctx: UserCtx) {
|
||||||
const revertedToVersion = application.revertableVersion
|
const revertedToVersion = application.revertableVersion
|
||||||
const appPackageUpdates = {
|
const appPackageUpdates = {
|
||||||
version: revertedToVersion,
|
version: revertedToVersion,
|
||||||
revertableVersion: null,
|
revertableVersion: undefined,
|
||||||
}
|
}
|
||||||
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
|
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
|
||||||
await events.app.versionReverted(app, currentVersion, revertedToVersion)
|
await events.app.versionReverted(app, currentVersion, revertedToVersion)
|
||||||
|
@ -613,12 +626,15 @@ export async function importToApp(ctx: UserCtx) {
|
||||||
ctx.body = { message: "app updated" }
|
ctx.body = { message: "app updated" }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateAppPackage(appPackage: any, appId: any) {
|
export async function updateAppPackage(
|
||||||
|
appPackage: Partial<App>,
|
||||||
|
appId: string
|
||||||
|
) {
|
||||||
return context.doInAppContext(appId, async () => {
|
return context.doInAppContext(appId, async () => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const application = await db.get<App>(DocumentType.APP_METADATA)
|
const application = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
|
|
||||||
const newAppPackage = { ...application, ...appPackage }
|
const newAppPackage: App = { ...application, ...appPackage }
|
||||||
if (appPackage._rev !== application._rev) {
|
if (appPackage._rev !== application._rev) {
|
||||||
newAppPackage._rev = application._rev
|
newAppPackage._rev = application._rev
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,7 +223,8 @@ export const exportRows = async (
|
||||||
|
|
||||||
const format = ctx.query.format
|
const format = ctx.query.format
|
||||||
|
|
||||||
const { rows, columns, query, sort, sortOrder } = ctx.request.body
|
const { rows, columns, query, sort, sortOrder, delimiter, customHeaders } =
|
||||||
|
ctx.request.body
|
||||||
if (typeof format !== "string" || !exporters.isFormat(format)) {
|
if (typeof format !== "string" || !exporters.isFormat(format)) {
|
||||||
ctx.throw(
|
ctx.throw(
|
||||||
400,
|
400,
|
||||||
|
@ -241,6 +242,8 @@ export const exportRows = async (
|
||||||
query,
|
query,
|
||||||
sort,
|
sort,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
delimiter,
|
||||||
|
customHeaders,
|
||||||
})
|
})
|
||||||
ctx.attachment(fileName)
|
ctx.attachment(fileName)
|
||||||
ctx.body = apiFileReturn(content)
|
ctx.body = apiFileReturn(content)
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { Ctx } from "@budibase/types"
|
import { Ctx } from "@budibase/types"
|
||||||
import { IsolatedVM } from "../../jsRunner/vm"
|
import { IsolatedVM } from "../../jsRunner/vm"
|
||||||
|
import { iifeWrapper } from "../../jsRunner/utilities"
|
||||||
|
|
||||||
export async function execute(ctx: Ctx) {
|
export async function execute(ctx: Ctx) {
|
||||||
const { script, context } = ctx.request.body
|
const { script, context } = ctx.request.body
|
||||||
const vm = new IsolatedVM()
|
const vm = new IsolatedVM()
|
||||||
const result = vm.withContext(context, () =>
|
ctx.body = vm.withContext(context, () => vm.execute(iifeWrapper(script)))
|
||||||
vm.execute(`(function(){\n${script}\n})();`)
|
|
||||||
)
|
|
||||||
ctx.body = result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: Ctx) {
|
export async function save(ctx: Ctx) {
|
||||||
|
|
|
@ -1,7 +1,19 @@
|
||||||
import { Row, TableSchema } from "@budibase/types"
|
import { Row, TableSchema } from "@budibase/types"
|
||||||
|
|
||||||
export function csv(headers: string[], rows: Row[]) {
|
function getHeaders(
|
||||||
let csv = headers.map(key => `"${key}"`).join(",")
|
headers: string[],
|
||||||
|
customHeaders: { [key: string]: string }
|
||||||
|
) {
|
||||||
|
return headers.map(header => `"${customHeaders[header] || header}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function csv(
|
||||||
|
headers: string[],
|
||||||
|
rows: Row[],
|
||||||
|
delimiter: string = ",",
|
||||||
|
customHeaders: { [key: string]: string } = {}
|
||||||
|
) {
|
||||||
|
let csv = getHeaders(headers, customHeaders).join(delimiter)
|
||||||
|
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
csv = `${csv}\n${headers
|
csv = `${csv}\n${headers
|
||||||
|
@ -15,7 +27,7 @@ export function csv(headers: string[], rows: Row[]) {
|
||||||
: ""
|
: ""
|
||||||
return val.trim()
|
return val.trim()
|
||||||
})
|
})
|
||||||
.join(",")}`
|
.join(delimiter)}`
|
||||||
}
|
}
|
||||||
return csv
|
return csv
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import * as deploymentController from "../controllers/deploy"
|
||||||
import authorized from "../../middleware/authorized"
|
import authorized from "../../middleware/authorized"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { permissions } from "@budibase/backend-core"
|
||||||
import { applicationValidator } from "./utils/validators"
|
import { applicationValidator } from "./utils/validators"
|
||||||
import { importToApp } from "../controllers/application"
|
|
||||||
|
|
||||||
const router: Router = new Router()
|
const router: Router = new Router()
|
||||||
|
|
||||||
|
|
|
@ -11,65 +11,54 @@ jest.mock("../../../utilities/redis", () => ({
|
||||||
checkDebounce: jest.fn(),
|
checkDebounce: jest.fn(),
|
||||||
shutdown: jest.fn(),
|
shutdown: jest.fn(),
|
||||||
}))
|
}))
|
||||||
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions"
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { AppStatus } from "../../../db/utils"
|
import { AppStatus } from "../../../db/utils"
|
||||||
import { events, utils, context } from "@budibase/backend-core"
|
import { events, utils, context } from "@budibase/backend-core"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
|
import type { App } from "@budibase/types"
|
||||||
jest.setTimeout(15000)
|
import tk from "timekeeper"
|
||||||
|
|
||||||
describe("/applications", () => {
|
describe("/applications", () => {
|
||||||
let request = setup.getRequest()
|
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
let app: App
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
beforeAll(async () => await config.init())
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
app = await config.api.application.create({ name: utils.newid() })
|
||||||
|
const deployment = await config.api.application.publish(app.appId)
|
||||||
|
expect(deployment.status).toBe("SUCCESS")
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("creates empty app", async () => {
|
it("creates empty app", async () => {
|
||||||
const res = await request
|
const app = await config.api.application.create({ name: utils.newid() })
|
||||||
.post("/api/applications")
|
expect(app._id).toBeDefined()
|
||||||
.field("name", utils.newid())
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body._id).toBeDefined()
|
|
||||||
expect(events.app.created).toBeCalledTimes(1)
|
expect(events.app.created).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates app from template", async () => {
|
it("creates app from template", async () => {
|
||||||
const res = await request
|
const app = await config.api.application.create({
|
||||||
.post("/api/applications")
|
name: utils.newid(),
|
||||||
.field("name", utils.newid())
|
useTemplate: "true",
|
||||||
.field("useTemplate", "true")
|
templateKey: "test",
|
||||||
.field("templateKey", "test")
|
templateString: "{}",
|
||||||
.field("templateString", "{}") // override the file download
|
})
|
||||||
.set(config.defaultHeaders())
|
expect(app._id).toBeDefined()
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body._id).toBeDefined()
|
|
||||||
expect(events.app.created).toBeCalledTimes(1)
|
expect(events.app.created).toBeCalledTimes(1)
|
||||||
expect(events.app.templateImported).toBeCalledTimes(1)
|
expect(events.app.templateImported).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates app from file", async () => {
|
it("creates app from file", async () => {
|
||||||
const res = await request
|
const app = await config.api.application.create({
|
||||||
.post("/api/applications")
|
name: utils.newid(),
|
||||||
.field("name", utils.newid())
|
useTemplate: "true",
|
||||||
.field("useTemplate", "true")
|
templateFile: "src/api/routes/tests/data/export.txt",
|
||||||
.set(config.defaultHeaders())
|
})
|
||||||
.attach("templateFile", "src/api/routes/tests/data/export.txt")
|
expect(app._id).toBeDefined()
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body._id).toBeDefined()
|
|
||||||
expect(events.app.created).toBeCalledTimes(1)
|
expect(events.app.created).toBeCalledTimes(1)
|
||||||
expect(events.app.fileImported).toBeCalledTimes(1)
|
expect(events.app.fileImported).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
@ -84,24 +73,21 @@ describe("/applications", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("migrates navigation settings from old apps", async () => {
|
it("migrates navigation settings from old apps", async () => {
|
||||||
const res = await request
|
const app = await config.api.application.create({
|
||||||
.post("/api/applications")
|
name: utils.newid(),
|
||||||
.field("name", "Old App")
|
useTemplate: "true",
|
||||||
.field("useTemplate", "true")
|
templateFile: "src/api/routes/tests/data/old-app.txt",
|
||||||
.set(config.defaultHeaders())
|
})
|
||||||
.attach("templateFile", "src/api/routes/tests/data/old-app.txt")
|
expect(app._id).toBeDefined()
|
||||||
.expect("Content-Type", /json/)
|
expect(app.navigation).toBeDefined()
|
||||||
.expect(200)
|
expect(app.navigation!.hideLogo).toBe(true)
|
||||||
expect(res.body._id).toBeDefined()
|
expect(app.navigation!.title).toBe("Custom Title")
|
||||||
expect(res.body.navigation).toBeDefined()
|
expect(app.navigation!.hideLogo).toBe(true)
|
||||||
expect(res.body.navigation.hideLogo).toBe(true)
|
expect(app.navigation!.navigation).toBe("Left")
|
||||||
expect(res.body.navigation.title).toBe("Custom Title")
|
expect(app.navigation!.navBackground).toBe(
|
||||||
expect(res.body.navigation.hideLogo).toBe(true)
|
|
||||||
expect(res.body.navigation.navigation).toBe("Left")
|
|
||||||
expect(res.body.navigation.navBackground).toBe(
|
|
||||||
"var(--spectrum-global-color-blue-600)"
|
"var(--spectrum-global-color-blue-600)"
|
||||||
)
|
)
|
||||||
expect(res.body.navigation.navTextColor).toBe(
|
expect(app.navigation!.navTextColor).toBe(
|
||||||
"var(--spectrum-global-color-gray-50)"
|
"var(--spectrum-global-color-gray-50)"
|
||||||
)
|
)
|
||||||
expect(events.app.created).toBeCalledTimes(1)
|
expect(events.app.created).toBeCalledTimes(1)
|
||||||
|
@ -110,164 +96,106 @@ describe("/applications", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
beforeEach(async () => {
|
|
||||||
// Clean all apps but the onde from config
|
|
||||||
await clearAllApps(config.getTenantId(), [config.getAppId()!])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("lists all applications", async () => {
|
it("lists all applications", async () => {
|
||||||
await config.createApp("app1")
|
const apps = await config.api.application.fetch({ status: AppStatus.DEV })
|
||||||
await config.createApp("app2")
|
expect(apps.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
const res = await request
|
|
||||||
.get(`/api/applications?status=${AppStatus.DEV}`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
// two created apps + the inited app
|
|
||||||
expect(res.body.length).toBe(3)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetchAppDefinition", () => {
|
describe("fetchAppDefinition", () => {
|
||||||
it("should be able to get an apps definition", async () => {
|
it("should be able to get an apps definition", async () => {
|
||||||
const res = await request
|
const res = await config.api.application.getDefinition(app.appId)
|
||||||
.get(`/api/applications/${config.getAppId()}/definition`)
|
expect(res.libraries.length).toEqual(1)
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.libraries.length).toEqual(1)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetchAppPackage", () => {
|
describe("fetchAppPackage", () => {
|
||||||
it("should be able to fetch the app package", async () => {
|
it("should be able to fetch the app package", async () => {
|
||||||
const res = await request
|
const res = await config.api.application.getAppPackage(app.appId)
|
||||||
.get(`/api/applications/${config.getAppId()}/appPackage`)
|
expect(res.application).toBeDefined()
|
||||||
.set(config.defaultHeaders())
|
expect(res.application.appId).toEqual(config.getAppId())
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.application).toBeDefined()
|
|
||||||
expect(res.body.application.appId).toEqual(config.getAppId())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("should be able to update the app package", async () => {
|
it("should be able to update the app package", async () => {
|
||||||
const res = await request
|
const updatedApp = await config.api.application.update(app.appId, {
|
||||||
.put(`/api/applications/${config.getAppId()}`)
|
name: "TEST_APP",
|
||||||
.send({
|
})
|
||||||
name: "TEST_APP",
|
expect(updatedApp._rev).toBeDefined()
|
||||||
})
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body._rev).toBeDefined()
|
|
||||||
expect(events.app.updated).toBeCalledTimes(1)
|
expect(events.app.updated).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("publish", () => {
|
describe("publish", () => {
|
||||||
it("should publish app with dev app ID", async () => {
|
it("should publish app with dev app ID", async () => {
|
||||||
const appId = config.getAppId()
|
await config.api.application.publish(app.appId)
|
||||||
await request
|
|
||||||
.post(`/api/applications/${appId}/publish`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(events.app.published).toBeCalledTimes(1)
|
expect(events.app.published).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should publish app with prod app ID", async () => {
|
it("should publish app with prod app ID", async () => {
|
||||||
const appId = config.getProdAppId()
|
await config.api.application.publish(app.appId.replace("_dev", ""))
|
||||||
await request
|
|
||||||
.post(`/api/applications/${appId}/publish`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(events.app.published).toBeCalledTimes(1)
|
expect(events.app.published).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("manage client library version", () => {
|
describe("manage client library version", () => {
|
||||||
it("should be able to update the app client library version", async () => {
|
it("should be able to update the app client library version", async () => {
|
||||||
await request
|
await config.api.application.updateClient(app.appId)
|
||||||
.post(`/api/applications/${config.getAppId()}/client/update`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(events.app.versionUpdated).toBeCalledTimes(1)
|
expect(events.app.versionUpdated).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to revert the app client library version", async () => {
|
it("should be able to revert the app client library version", async () => {
|
||||||
// We need to first update the version so that we can then revert
|
await config.api.application.updateClient(app.appId)
|
||||||
await request
|
await config.api.application.revertClient(app.appId)
|
||||||
.post(`/api/applications/${config.getAppId()}/client/update`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
await request
|
|
||||||
.post(`/api/applications/${config.getAppId()}/client/revert`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(events.app.versionReverted).toBeCalledTimes(1)
|
expect(events.app.versionReverted).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("edited at", () => {
|
describe("edited at", () => {
|
||||||
it("middleware should set edited at", async () => {
|
it("middleware should set updatedAt", async () => {
|
||||||
const headers = config.defaultHeaders()
|
const app = await tk.withFreeze(
|
||||||
headers["referer"] = `/${config.getAppId()}/test`
|
"2021-01-01",
|
||||||
const res = await request
|
async () => await config.api.application.create({ name: utils.newid() })
|
||||||
.put(`/api/applications/${config.getAppId()}`)
|
)
|
||||||
.send({
|
expect(app.updatedAt).toEqual("2021-01-01T00:00:00.000Z")
|
||||||
name: "UPDATED_NAME",
|
|
||||||
})
|
const updatedApp = await tk.withFreeze(
|
||||||
.set(headers)
|
"2021-02-01",
|
||||||
.expect("Content-Type", /json/)
|
async () =>
|
||||||
.expect(200)
|
await config.api.application.update(app.appId, {
|
||||||
expect(res.body._rev).toBeDefined()
|
name: "UPDATED_NAME",
|
||||||
// retrieve the app to check it
|
})
|
||||||
const getRes = await request
|
)
|
||||||
.get(`/api/applications/${config.getAppId()}/appPackage`)
|
expect(updatedApp._rev).toBeDefined()
|
||||||
.set(headers)
|
expect(updatedApp.updatedAt).toEqual("2021-02-01T00:00:00.000Z")
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
const fetchedApp = await config.api.application.get(app.appId)
|
||||||
expect(getRes.body.application.updatedAt).toBeDefined()
|
expect(fetchedApp.updatedAt).toEqual("2021-02-01T00:00:00.000Z")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("sync", () => {
|
describe("sync", () => {
|
||||||
it("app should sync correctly", async () => {
|
it("app should sync correctly", async () => {
|
||||||
const res = await request
|
const { message } = await config.api.application.sync(app.appId)
|
||||||
.post(`/api/applications/${config.getAppId()}/sync`)
|
expect(message).toEqual("App sync completed successfully.")
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.message).toEqual("App sync completed successfully.")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("app should not sync if production", async () => {
|
it("app should not sync if production", async () => {
|
||||||
const res = await request
|
const { message } = await config.api.application.sync(
|
||||||
.post(`/api/applications/app_123456/sync`)
|
app.appId.replace("_dev", ""),
|
||||||
.set(config.defaultHeaders())
|
{ statusCode: 400 }
|
||||||
.expect("Content-Type", /json/)
|
)
|
||||||
.expect(400)
|
|
||||||
expect(res.body.message).toEqual(
|
expect(message).toEqual(
|
||||||
"This action cannot be performed for production apps"
|
"This action cannot be performed for production apps"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("app should not sync if sync is disabled", async () => {
|
it("app should not sync if sync is disabled", async () => {
|
||||||
env._set("DISABLE_AUTO_PROD_APP_SYNC", true)
|
env._set("DISABLE_AUTO_PROD_APP_SYNC", true)
|
||||||
const res = await request
|
const { message } = await config.api.application.sync(app.appId)
|
||||||
.post(`/api/applications/${config.getAppId()}/sync`)
|
expect(message).toEqual(
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.message).toEqual(
|
|
||||||
"App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable."
|
"App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable."
|
||||||
)
|
)
|
||||||
env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
|
env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
|
||||||
|
@ -275,51 +203,26 @@ describe("/applications", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("unpublish", () => {
|
describe("unpublish", () => {
|
||||||
beforeEach(async () => {
|
|
||||||
// We want to republish as the unpublish will delete the prod app
|
|
||||||
await config.publish()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should unpublish app with dev app ID", async () => {
|
it("should unpublish app with dev app ID", async () => {
|
||||||
const appId = config.getAppId()
|
await config.api.application.unpublish(app.appId)
|
||||||
await request
|
|
||||||
.post(`/api/applications/${appId}/unpublish`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect(204)
|
|
||||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should unpublish app with prod app ID", async () => {
|
it("should unpublish app with prod app ID", async () => {
|
||||||
const appId = config.getProdAppId()
|
await config.api.application.unpublish(app.appId.replace("_dev", ""))
|
||||||
await request
|
|
||||||
.post(`/api/applications/${appId}/unpublish`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect(204)
|
|
||||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("delete", () => {
|
describe("delete", () => {
|
||||||
it("should delete published app and dev apps with dev app ID", async () => {
|
it("should delete published app and dev apps with dev app ID", async () => {
|
||||||
await config.createApp("to-delete")
|
await config.api.application.delete(app.appId)
|
||||||
const appId = config.getAppId()
|
|
||||||
await request
|
|
||||||
.delete(`/api/applications/${appId}`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(events.app.deleted).toBeCalledTimes(1)
|
expect(events.app.deleted).toBeCalledTimes(1)
|
||||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete published app and dev app with prod app ID", async () => {
|
it("should delete published app and dev app with prod app ID", async () => {
|
||||||
await config.createApp("to-delete")
|
await config.api.application.delete(app.appId.replace("_dev", ""))
|
||||||
const appId = config.getProdAppId()
|
|
||||||
await request
|
|
||||||
.delete(`/api/applications/${appId}`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(events.app.deleted).toBeCalledTimes(1)
|
expect(events.app.deleted).toBeCalledTimes(1)
|
||||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
@ -327,28 +230,18 @@ describe("/applications", () => {
|
||||||
|
|
||||||
describe("POST /api/applications/:appId/sync", () => {
|
describe("POST /api/applications/:appId/sync", () => {
|
||||||
it("should not sync automation logs", async () => {
|
it("should not sync automation logs", async () => {
|
||||||
// setup the apps
|
|
||||||
await config.createApp("testing-auto-logs")
|
|
||||||
const automation = await config.createAutomation()
|
const automation = await config.createAutomation()
|
||||||
await config.publish()
|
await context.doInAppContext(app.appId, () =>
|
||||||
await context.doInAppContext(config.getProdAppId(), () => {
|
config.createAutomationLog(automation)
|
||||||
return config.createAutomationLog(automation)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// do the sync
|
await config.api.application.sync(app.appId)
|
||||||
const appId = config.getAppId()
|
|
||||||
await request
|
|
||||||
.post(`/api/applications/${appId}/sync`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
// does exist in prod
|
// does exist in prod
|
||||||
const prodLogs = await config.getAutomationLogs()
|
const prodLogs = await config.getAutomationLogs()
|
||||||
expect(prodLogs.data.length).toBe(1)
|
expect(prodLogs.data.length).toBe(1)
|
||||||
|
|
||||||
// delete prod app so we revert to dev log search
|
await config.api.application.unpublish(app.appId)
|
||||||
await config.unpublish()
|
|
||||||
|
|
||||||
// doesn't exist in dev
|
// doesn't exist in dev
|
||||||
const devLogs = await config.getAutomationLogs()
|
const devLogs = await config.getAutomationLogs()
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { context, logging } from "@budibase/backend-core"
|
import { context, logging } from "@budibase/backend-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
|
|
||||||
import { IsolatedVM } from "./vm"
|
import { IsolatedVM } from "./vm"
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { IsolatedVM } from "../vm"
|
||||||
|
import { iifeWrapper } from "../utilities"
|
||||||
|
|
||||||
|
function runJSWithIsolatedVM(script: string, context: Record<string, any>) {
|
||||||
|
const runner = new IsolatedVM()
|
||||||
|
return runner.withContext(context, () => {
|
||||||
|
return runner.execute(iifeWrapper(script))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Test isolated vm directly", () => {
|
||||||
|
it("should handle a very large file", () => {
|
||||||
|
const marked = fs.readFileSync(
|
||||||
|
path.join(__dirname, "largeJSExample.txt"),
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
const result = runJSWithIsolatedVM(marked, {
|
||||||
|
trigger: { row: { Message: "dddd" } },
|
||||||
|
})
|
||||||
|
expect(result).toBe("<p>dddd</p>\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handle a mapping case", async () => {
|
||||||
|
const context = {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
searchProducts: {
|
||||||
|
results: [{ imageLinks: ["_S/"] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = await runJSWithIsolatedVM(
|
||||||
|
`
|
||||||
|
const dataUnnested = data.data.searchProducts.results
|
||||||
|
const emptyLink = "https://budibase.com"
|
||||||
|
let pImage = emptyLink
|
||||||
|
let sImage = emptyLink
|
||||||
|
let uImage = emptyLink
|
||||||
|
let lImage = emptyLink
|
||||||
|
let b1Image = emptyLink
|
||||||
|
let b2Image = emptyLink
|
||||||
|
|
||||||
|
const dataTransformed = dataUnnested.map(x=> {
|
||||||
|
let imageLinks = x.imageLinks
|
||||||
|
for (let i = 0; i < imageLinks.length; i++){
|
||||||
|
if(imageLinks[i].includes("_P/") || imageLinks[i].includes("_p/")){
|
||||||
|
pImage = imageLinks[i]
|
||||||
|
} else if (imageLinks[i].includes("_S/") || imageLinks[i].includes("_s/")){
|
||||||
|
sImage = imageLinks[i]
|
||||||
|
} else if (imageLinks[i].includes("_U/") || imageLinks[i].includes("_u/")){
|
||||||
|
uImage = imageLinks[i]
|
||||||
|
} else if (imageLinks[i].includes("_L/") || imageLinks[i].includes("_l/")){
|
||||||
|
lImage = imageLinks[i]
|
||||||
|
} else if (imageLinks[i].includes("_B/") || imageLinks[i].includes("_b/")){
|
||||||
|
b1Image = imageLinks[i]
|
||||||
|
} else if (imageLinks[i].includes("_B2/") || imageLinks[i].includes("_b2/")){
|
||||||
|
b2Image = imageLinks[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrangedLinks = [pImage, sImage, uImage, lImage, b1Image, b2Image]
|
||||||
|
x.imageLinks = arrangedLinks
|
||||||
|
|
||||||
|
return x
|
||||||
|
})
|
||||||
|
|
||||||
|
return dataTransformed
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
expect(result.length).toBe(1)
|
||||||
|
expect(result[0].imageLinks).toEqual([
|
||||||
|
"https://budibase.com",
|
||||||
|
"_S/",
|
||||||
|
"https://budibase.com",
|
||||||
|
"https://budibase.com",
|
||||||
|
"https://budibase.com",
|
||||||
|
"https://budibase.com",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle automation script example", () => {
|
||||||
|
const context = {
|
||||||
|
steps: [{}, { response: "hello" }, { items: [{ rows: [{ a: 1 }] }] }],
|
||||||
|
}
|
||||||
|
const result = runJSWithIsolatedVM(
|
||||||
|
`const queryResults = steps[2].items;
|
||||||
|
|
||||||
|
const intervals = steps[1].response;
|
||||||
|
const whereNoItemsReturned = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
for (let queryResult of queryResults) {
|
||||||
|
if (queryResult.rows.length === 0) {
|
||||||
|
whereNoItemsReturned.push(intervals[index]);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return whereNoItemsReturned;
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
|
@ -7,7 +7,9 @@ import tk from "timekeeper"
|
||||||
import { init } from ".."
|
import { init } from ".."
|
||||||
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||||
|
|
||||||
tk.freeze("2021-01-21T12:00:00")
|
const DATE = "2021-01-21T12:00:00"
|
||||||
|
|
||||||
|
tk.freeze(DATE)
|
||||||
|
|
||||||
describe("jsRunner (using isolated-vm)", () => {
|
describe("jsRunner (using isolated-vm)", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
|
@ -70,4 +72,278 @@ describe("jsRunner (using isolated-vm)", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// the test cases here were extracted from templates/real world examples of JS in Budibase
|
||||||
|
describe("real test cases from Budicloud", () => {
|
||||||
|
const context = {
|
||||||
|
"Unit Value": 2,
|
||||||
|
Quantity: 1,
|
||||||
|
}
|
||||||
|
it("handle test case 1", async () => {
|
||||||
|
const result = await processJS(
|
||||||
|
`
|
||||||
|
var Gross = $("[Unit Value]") * $("[Quantity]")
|
||||||
|
return Gross.toFixed(2)`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
expect(result).toBe("2.00")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handle test case 2", async () => {
|
||||||
|
const context = {
|
||||||
|
"Purchase Date": DATE,
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`
|
||||||
|
var purchase = new Date($("[Purchase Date]"));
|
||||||
|
let purchaseyear = purchase.getFullYear();
|
||||||
|
let purchasemonth = purchase.getMonth();
|
||||||
|
|
||||||
|
var today = new Date ();
|
||||||
|
let todayyear = today.getFullYear();
|
||||||
|
let todaymonth = today.getMonth();
|
||||||
|
|
||||||
|
var age = todayyear - purchaseyear
|
||||||
|
|
||||||
|
if (((todaymonth - purchasemonth) < 6) == true){
|
||||||
|
return age
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
expect(result).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 3", async () => {
|
||||||
|
const context = {
|
||||||
|
Escalate: true,
|
||||||
|
"Budget ($)": 1100,
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`
|
||||||
|
if ($("[Escalate]") == true) {
|
||||||
|
if ($("Budget ($)") <= 1000)
|
||||||
|
{return 2;}
|
||||||
|
if ($("Budget ($)") > 1000)
|
||||||
|
{return 3;}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($("Budget ($)") <= 1000)
|
||||||
|
{return 1;}
|
||||||
|
if ($("Budget ($)") > 1000)
|
||||||
|
if ($("Budget ($)") < 10000)
|
||||||
|
{return 2;}
|
||||||
|
else
|
||||||
|
{return 3}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
expect(result).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 4", async () => {
|
||||||
|
const context = {
|
||||||
|
"Time Sheets": ["a", "b"],
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`
|
||||||
|
let hours = 0
|
||||||
|
if (($("[Time Sheets]") != null) == true){
|
||||||
|
for (i = 0; i < $("[Time Sheets]").length; i++){
|
||||||
|
let hoursLogged = "Time Sheets." + i + ".Hours"
|
||||||
|
hours += $(hoursLogged)
|
||||||
|
}
|
||||||
|
return hours
|
||||||
|
}
|
||||||
|
if (($("[Time Sheets]") != null) == false){
|
||||||
|
return hours
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
expect(result).toBe("0ab")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 5", async () => {
|
||||||
|
const context = {
|
||||||
|
change: JSON.stringify({ a: 1, primaryDisplay: "a" }),
|
||||||
|
previous: JSON.stringify({ a: 2, primaryDisplay: "b" }),
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`
|
||||||
|
let change = $("[change]") ? JSON.parse($("[change]")) : {}
|
||||||
|
let previous = $("[previous]") ? JSON.parse($("[previous]")) : {}
|
||||||
|
|
||||||
|
function simplifyLink(originalKey, value, parent) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.filter(item => Object.keys(item || {}).includes("primaryDisplay")).length > 0) {
|
||||||
|
parent[originalKey] = value.map(link => link.primaryDisplay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let entry of Object.entries(change)) {
|
||||||
|
simplifyLink(entry[0], entry[1], change)
|
||||||
|
}
|
||||||
|
for (let entry of Object.entries(previous)) {
|
||||||
|
simplifyLink(entry[0], entry[1], previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff = Object.fromEntries(Object.entries(change).filter(([k, v]) => previous[k]?.toString() !== v?.toString()))
|
||||||
|
|
||||||
|
delete diff.audit_change
|
||||||
|
delete diff.audit_previous
|
||||||
|
delete diff._id
|
||||||
|
delete diff._rev
|
||||||
|
delete diff.tableId
|
||||||
|
delete diff.audit
|
||||||
|
|
||||||
|
for (let entry of Object.entries(diff)) {
|
||||||
|
simplifyLink(entry[0], entry[1], diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(change)?.replaceAll(",\\"", ",\\n\\t\\"").replaceAll("{\\"", "{\\n\\t\\"").replaceAll("}", "\\n}")
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBe(`{\n\t"a":1,\n\t"primaryDisplay":"a"\n}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 6", async () => {
|
||||||
|
const context = {
|
||||||
|
"Join Date": DATE,
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`
|
||||||
|
var rate = 5;
|
||||||
|
var today = new Date();
|
||||||
|
|
||||||
|
// comment
|
||||||
|
function monthDiff(dateFrom, dateTo) {
|
||||||
|
return dateTo.getMonth() - dateFrom.getMonth() +
|
||||||
|
(12 * (dateTo.getFullYear() - dateFrom.getFullYear()))
|
||||||
|
}
|
||||||
|
var serviceMonths = monthDiff( new Date($("[Join Date]")), today);
|
||||||
|
var serviceYears = serviceMonths / 12;
|
||||||
|
|
||||||
|
if (serviceYears >= 1 && serviceYears < 5){
|
||||||
|
rate = 10;
|
||||||
|
}
|
||||||
|
if (serviceYears >= 5 && serviceYears < 10){
|
||||||
|
rate = 15;
|
||||||
|
}
|
||||||
|
if (serviceYears >= 10){
|
||||||
|
rate = 15;
|
||||||
|
rate += 0.5 * (Number(serviceYears.toFixed(0)) - 10);
|
||||||
|
}
|
||||||
|
return rate;
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 7", async () => {
|
||||||
|
const context = {
|
||||||
|
"P I": "Pass",
|
||||||
|
"PA I": "Pass",
|
||||||
|
"F I": "Fail",
|
||||||
|
"V I": "Pass",
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`if (($("[P I]") == "Pass") == true)
|
||||||
|
if (($("[ P I]") == "Pass") == true)
|
||||||
|
if (($("[F I]") == "Pass") == true)
|
||||||
|
if (($("[V I]") == "Pass") == true)
|
||||||
|
{return "Pass"}
|
||||||
|
|
||||||
|
if (($("[PA I]") == "Fail") == true)
|
||||||
|
{return "Fail"}
|
||||||
|
if (($("[ P I]") == "Fail") == true)
|
||||||
|
{return "Fail"}
|
||||||
|
if (($("[F I]") == "Fail") == true)
|
||||||
|
{return "Fail"}
|
||||||
|
if (($("[V I]") == "Fail") == true)
|
||||||
|
{return "Fail"}
|
||||||
|
|
||||||
|
else
|
||||||
|
{return ""}`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBe("Fail")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 8", async () => {
|
||||||
|
const context = {
|
||||||
|
"T L": [{ Hours: 10 }],
|
||||||
|
"B H": 50,
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`var totalHours = 0;
|
||||||
|
if (($("[T L]") != null) == true){
|
||||||
|
for (let i = 0; i < ($("[T L]").length); i++){
|
||||||
|
var individualHours = "T L." + i + ".Hours";
|
||||||
|
var hoursNum = Number($(individualHours));
|
||||||
|
totalHours += hoursNum;
|
||||||
|
}
|
||||||
|
return totalHours.toFixed(2);
|
||||||
|
}
|
||||||
|
if (($("[T L]") != null) == false) {
|
||||||
|
return totalHours.toFixed(2);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBe("10.00")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 9", async () => {
|
||||||
|
const context = {
|
||||||
|
"T L": [{ Hours: 10 }],
|
||||||
|
"B H": 50,
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`var totalHours = 0;
|
||||||
|
if (($("[T L]") != null) == true){
|
||||||
|
for (let i = 0; i < ($("[T L]").length); i++){
|
||||||
|
var individualHours = "T L." + i + ".Hours";
|
||||||
|
var hoursNum = Number($(individualHours));
|
||||||
|
totalHours += hoursNum;
|
||||||
|
}
|
||||||
|
return ($("[B H]") - totalHours).toFixed(2);
|
||||||
|
}
|
||||||
|
if (($("[T L]") != null) == false) {
|
||||||
|
return ($("[B H]") - totalHours).toFixed(2);
|
||||||
|
}`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBe("40.00")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle test case 10", async () => {
|
||||||
|
const context = {
|
||||||
|
"F F": [{ "F S": 10 }],
|
||||||
|
}
|
||||||
|
const result = await processJS(
|
||||||
|
`var rating = 0;
|
||||||
|
|
||||||
|
if ($("[F F]") != null){
|
||||||
|
for (i = 0; i < $("[F F]").length; i++){
|
||||||
|
var individualRating = $("F F." + i + ".F S");
|
||||||
|
rating += individualRating;
|
||||||
|
}
|
||||||
|
rating = (rating / $("[F F]").length);
|
||||||
|
}
|
||||||
|
return rating;
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
expect(result).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
||||||
|
export function iifeWrapper(script: string) {
|
||||||
|
return `(function(){\n${script}\n})();`
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import querystring from "querystring"
|
||||||
|
|
||||||
import { BundleType, loadBundle } from "../bundles"
|
import { BundleType, loadBundle } from "../bundles"
|
||||||
import { VM } from "@budibase/types"
|
import { VM } from "@budibase/types"
|
||||||
|
import { iifeWrapper } from "../utilities"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
|
|
||||||
class ExecutionTimeoutError extends Error {
|
class ExecutionTimeoutError extends Error {
|
||||||
|
@ -118,11 +119,11 @@ export class IsolatedVM implements VM {
|
||||||
// 3. Process script
|
// 3. Process script
|
||||||
// 4. Stringify the result in order to convert the result from BSON to json
|
// 4. Stringify the result in order to convert the result from BSON to json
|
||||||
this.codeWrapper = code =>
|
this.codeWrapper = code =>
|
||||||
`(function(){
|
iifeWrapper(`
|
||||||
const data = bson.deserialize(bsonData, { validation: { utf8: false } }).data;
|
const data = bson.deserialize(bsonData, { validation: { utf8: false } }).data;
|
||||||
const result = ${code}
|
const result = ${code}
|
||||||
return bson.toJson(result);
|
return bson.toJson(result);
|
||||||
})();`
|
`)
|
||||||
|
|
||||||
const bsonSource = loadBundle(BundleType.BSON)
|
const bsonSource = loadBundle(BundleType.BSON)
|
||||||
|
|
||||||
|
|
|
@ -36,11 +36,13 @@ export async function search(options: SearchParams): Promise<{
|
||||||
export interface ExportRowsParams {
|
export interface ExportRowsParams {
|
||||||
tableId: string
|
tableId: string
|
||||||
format: Format
|
format: Format
|
||||||
|
delimiter?: string
|
||||||
rowIds?: string[]
|
rowIds?: string[]
|
||||||
columns?: string[]
|
columns?: string[]
|
||||||
query?: SearchFilters
|
query?: SearchFilters
|
||||||
sort?: string
|
sort?: string
|
||||||
sortOrder?: SortOrder
|
sortOrder?: SortOrder
|
||||||
|
customHeaders?: { [key: string]: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportRowsResult {
|
export interface ExportRowsResult {
|
||||||
|
|
|
@ -101,7 +101,17 @@ export async function search(options: SearchParams) {
|
||||||
export async function exportRows(
|
export async function exportRows(
|
||||||
options: ExportRowsParams
|
options: ExportRowsParams
|
||||||
): Promise<ExportRowsResult> {
|
): Promise<ExportRowsResult> {
|
||||||
const { tableId, format, columns, rowIds, query, sort, sortOrder } = options
|
const {
|
||||||
|
tableId,
|
||||||
|
format,
|
||||||
|
columns,
|
||||||
|
rowIds,
|
||||||
|
query,
|
||||||
|
sort,
|
||||||
|
sortOrder,
|
||||||
|
delimiter,
|
||||||
|
customHeaders,
|
||||||
|
} = options
|
||||||
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
|
|
||||||
let requestQuery: SearchFilters = {}
|
let requestQuery: SearchFilters = {}
|
||||||
|
@ -153,12 +163,17 @@ export async function exportRows(
|
||||||
rows = result.rows
|
rows = result.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
let exportRows = cleanExportRows(rows, schema, format, columns)
|
let exportRows = cleanExportRows(rows, schema, format, columns, customHeaders)
|
||||||
|
|
||||||
let content: string
|
let content: string
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case exporters.Format.CSV:
|
case exporters.Format.CSV:
|
||||||
content = exporters.csv(headers ?? Object.keys(schema), exportRows)
|
content = exporters.csv(
|
||||||
|
headers ?? Object.keys(schema),
|
||||||
|
exportRows,
|
||||||
|
delimiter,
|
||||||
|
customHeaders
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case exporters.Format.JSON:
|
case exporters.Format.JSON:
|
||||||
content = exporters.json(exportRows)
|
content = exporters.json(exportRows)
|
||||||
|
|
|
@ -84,7 +84,17 @@ export async function search(options: SearchParams) {
|
||||||
export async function exportRows(
|
export async function exportRows(
|
||||||
options: ExportRowsParams
|
options: ExportRowsParams
|
||||||
): Promise<ExportRowsResult> {
|
): Promise<ExportRowsResult> {
|
||||||
const { tableId, format, rowIds, columns, query, sort, sortOrder } = options
|
const {
|
||||||
|
tableId,
|
||||||
|
format,
|
||||||
|
rowIds,
|
||||||
|
columns,
|
||||||
|
query,
|
||||||
|
sort,
|
||||||
|
sortOrder,
|
||||||
|
delimiter,
|
||||||
|
customHeaders,
|
||||||
|
} = options
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const table = await sdk.tables.getTable(tableId)
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
|
||||||
|
@ -124,11 +134,16 @@ export async function exportRows(
|
||||||
rows = result
|
rows = result
|
||||||
}
|
}
|
||||||
|
|
||||||
let exportRows = cleanExportRows(rows, schema, format, columns)
|
let exportRows = cleanExportRows(rows, schema, format, columns, customHeaders)
|
||||||
if (format === Format.CSV) {
|
if (format === Format.CSV) {
|
||||||
return {
|
return {
|
||||||
fileName: "export.csv",
|
fileName: "export.csv",
|
||||||
content: csv(headers ?? Object.keys(rows[0]), exportRows),
|
content: csv(
|
||||||
|
headers ?? Object.keys(rows[0]),
|
||||||
|
exportRows,
|
||||||
|
delimiter,
|
||||||
|
customHeaders
|
||||||
|
),
|
||||||
}
|
}
|
||||||
} else if (format === Format.JSON) {
|
} else if (format === Format.JSON) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -16,7 +16,8 @@ export function cleanExportRows(
|
||||||
rows: any[],
|
rows: any[],
|
||||||
schema: TableSchema,
|
schema: TableSchema,
|
||||||
format: string,
|
format: string,
|
||||||
columns?: string[]
|
columns?: string[],
|
||||||
|
customHeaders: { [key: string]: string } = {}
|
||||||
) {
|
) {
|
||||||
let cleanRows = [...rows]
|
let cleanRows = [...rows]
|
||||||
|
|
||||||
|
@ -44,11 +45,27 @@ export function cleanExportRows(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (format === Format.JSON) {
|
||||||
|
// Replace row keys with custom headers
|
||||||
|
for (let row of cleanRows) {
|
||||||
|
renameKeys(customHeaders, row)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanRows
|
return cleanRows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renameKeys(keysMap: { [key: string]: any }, row: any) {
|
||||||
|
for (const key in keysMap) {
|
||||||
|
Object.defineProperty(
|
||||||
|
row,
|
||||||
|
keysMap[key],
|
||||||
|
Object.getOwnPropertyDescriptor(row, key) || {}
|
||||||
|
)
|
||||||
|
delete row[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isForeignKey(key: string, table: Table) {
|
function isForeignKey(key: string, table: Table) {
|
||||||
const relationships = Object.values(table.schema).filter(isRelationshipColumn)
|
const relationships = Object.values(table.schema).filter(isRelationshipColumn)
|
||||||
return relationships.some(
|
return relationships.some(
|
||||||
|
|
|
@ -1,17 +1,96 @@
|
||||||
import { Response } from "supertest"
|
import { Response } from "supertest"
|
||||||
import { App } from "@budibase/types"
|
import {
|
||||||
|
App,
|
||||||
|
type CreateAppRequest,
|
||||||
|
type FetchAppDefinitionResponse,
|
||||||
|
type FetchAppPackageResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
import TestConfiguration from "../TestConfiguration"
|
import TestConfiguration from "../TestConfiguration"
|
||||||
import { TestAPI } from "./base"
|
import { TestAPI } from "./base"
|
||||||
|
import { AppStatus } from "../../../db/utils"
|
||||||
|
import { constants } from "@budibase/backend-core"
|
||||||
|
|
||||||
export class ApplicationAPI extends TestAPI {
|
export class ApplicationAPI extends TestAPI {
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
super(config)
|
super(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create = async (app: CreateAppRequest): Promise<App> => {
|
||||||
|
const request = this.request
|
||||||
|
.post("/api/applications")
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
|
||||||
|
for (const key of Object.keys(app)) {
|
||||||
|
request.field(key, (app as any)[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.templateFile) {
|
||||||
|
request.attach("templateFile", app.templateFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await request
|
||||||
|
|
||||||
|
if (result.statusCode !== 200) {
|
||||||
|
throw new Error(JSON.stringify(result.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.body as App
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (appId: string): Promise<void> => {
|
||||||
|
await this.request
|
||||||
|
.delete(`/api/applications/${appId}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
publish = async (
|
||||||
|
appId: string
|
||||||
|
): Promise<{ _id: string; status: string; appUrl: string }> => {
|
||||||
|
// While the publish endpoint does take an :appId parameter, it doesn't
|
||||||
|
// use it. It uses the appId from the context.
|
||||||
|
let headers = {
|
||||||
|
...this.config.defaultHeaders(),
|
||||||
|
[constants.Header.APP_ID]: appId,
|
||||||
|
}
|
||||||
|
const result = await this.request
|
||||||
|
.post(`/api/applications/${appId}/publish`)
|
||||||
|
.set(headers)
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return result.body as { _id: string; status: string; appUrl: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
unpublish = async (appId: string): Promise<void> => {
|
||||||
|
await this.request
|
||||||
|
.post(`/api/applications/${appId}/unpublish`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect(204)
|
||||||
|
}
|
||||||
|
|
||||||
|
sync = async (
|
||||||
|
appId: string,
|
||||||
|
{ statusCode }: { statusCode: number } = { statusCode: 200 }
|
||||||
|
): Promise<{ message: string }> => {
|
||||||
|
const result = await this.request
|
||||||
|
.post(`/api/applications/${appId}/sync`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(statusCode)
|
||||||
|
return result.body
|
||||||
|
}
|
||||||
|
|
||||||
getRaw = async (appId: string): Promise<Response> => {
|
getRaw = async (appId: string): Promise<Response> => {
|
||||||
|
// While the appPackage endpoint does take an :appId parameter, it doesn't
|
||||||
|
// use it. It uses the appId from the context.
|
||||||
|
let headers = {
|
||||||
|
...this.config.defaultHeaders(),
|
||||||
|
[constants.Header.APP_ID]: appId,
|
||||||
|
}
|
||||||
const result = await this.request
|
const result = await this.request
|
||||||
.get(`/api/applications/${appId}/appPackage`)
|
.get(`/api/applications/${appId}/appPackage`)
|
||||||
.set(this.config.defaultHeaders())
|
.set(headers)
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
return result
|
return result
|
||||||
|
@ -21,4 +100,94 @@ export class ApplicationAPI extends TestAPI {
|
||||||
const result = await this.getRaw(appId)
|
const result = await this.getRaw(appId)
|
||||||
return result.body.application as App
|
return result.body.application as App
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDefinition = async (
|
||||||
|
appId: string
|
||||||
|
): Promise<FetchAppDefinitionResponse> => {
|
||||||
|
const result = await this.request
|
||||||
|
.get(`/api/applications/${appId}/definition`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return result.body as FetchAppDefinitionResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppPackage = async (appId: string): Promise<FetchAppPackageResponse> => {
|
||||||
|
const result = await this.request
|
||||||
|
.get(`/api/applications/${appId}/appPackage`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return result.body
|
||||||
|
}
|
||||||
|
|
||||||
|
update = async (
|
||||||
|
appId: string,
|
||||||
|
app: { name?: string; url?: string }
|
||||||
|
): Promise<App> => {
|
||||||
|
const request = this.request
|
||||||
|
.put(`/api/applications/${appId}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
|
||||||
|
for (const key of Object.keys(app)) {
|
||||||
|
request.field(key, (app as any)[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await request
|
||||||
|
|
||||||
|
if (result.statusCode !== 200) {
|
||||||
|
throw new Error(JSON.stringify(result.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.body as App
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClient = async (appId: string): Promise<void> => {
|
||||||
|
// While the updateClient endpoint does take an :appId parameter, it doesn't
|
||||||
|
// use it. It uses the appId from the context.
|
||||||
|
let headers = {
|
||||||
|
...this.config.defaultHeaders(),
|
||||||
|
[constants.Header.APP_ID]: appId,
|
||||||
|
}
|
||||||
|
const response = await this.request
|
||||||
|
.post(`/api/applications/${appId}/client/update`)
|
||||||
|
.set(headers)
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
throw new Error(JSON.stringify(response.body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
revertClient = async (appId: string): Promise<void> => {
|
||||||
|
// While the revertClient endpoint does take an :appId parameter, it doesn't
|
||||||
|
// use it. It uses the appId from the context.
|
||||||
|
let headers = {
|
||||||
|
...this.config.defaultHeaders(),
|
||||||
|
[constants.Header.APP_ID]: appId,
|
||||||
|
}
|
||||||
|
const response = await this.request
|
||||||
|
.post(`/api/applications/${appId}/client/revert`)
|
||||||
|
.set(headers)
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
throw new Error(JSON.stringify(response.body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch = async ({ status }: { status?: AppStatus } = {}): Promise<App[]> => {
|
||||||
|
let query = []
|
||||||
|
if (status) {
|
||||||
|
query.push(`status=${status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.request
|
||||||
|
.get(`/api/applications${query.length ? `?${query.join("&")}` : ""}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return result.body as App[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
QueryResponse,
|
QueryResponse,
|
||||||
} from "./definitions"
|
} from "./definitions"
|
||||||
import { IsolatedVM } from "../jsRunner/vm"
|
import { IsolatedVM } from "../jsRunner/vm"
|
||||||
|
import { iifeWrapper } from "../jsRunner/utilities"
|
||||||
import { getIntegration } from "../integrations"
|
import { getIntegration } from "../integrations"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import { context, cache, auth } from "@budibase/backend-core"
|
import { context, cache, auth } from "@budibase/backend-core"
|
||||||
|
@ -127,7 +128,7 @@ class QueryRunner {
|
||||||
|
|
||||||
// transform as required
|
// transform as required
|
||||||
if (transformer) {
|
if (transformer) {
|
||||||
transformer = `(function(){\n${transformer}\n})();`
|
transformer = iifeWrapper(transformer)
|
||||||
let vm = new IsolatedVM()
|
let vm = new IsolatedVM()
|
||||||
if (datasource.source === SourceName.MONGODB) {
|
if (datasource.source === SourceName.MONGODB) {
|
||||||
vm = vm.withParsingBson(rows)
|
vm = vm.withParsingBson(rows)
|
||||||
|
|
|
@ -37,6 +37,8 @@ export interface ExportRowsRequest {
|
||||||
query?: SearchFilters
|
query?: SearchFilters
|
||||||
sort?: string
|
sort?: string
|
||||||
sortOrder?: SortOrder
|
sortOrder?: SortOrder
|
||||||
|
delimiter?: string
|
||||||
|
customHeaders?: { [key: string]: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExportRowsResponse = ReadStream
|
export type ExportRowsResponse = ReadStream
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { PlanType } from "../../sdk"
|
||||||
|
import type { Layout, App, Screen } from "../../documents"
|
||||||
|
|
||||||
|
export interface CreateAppRequest {
|
||||||
|
name: string
|
||||||
|
url?: string
|
||||||
|
useTemplate?: string
|
||||||
|
templateName?: string
|
||||||
|
templateKey?: string
|
||||||
|
templateFile?: string
|
||||||
|
includeSampleData?: boolean
|
||||||
|
encryptionPassword?: string
|
||||||
|
templateString?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchAppDefinitionResponse {
|
||||||
|
layouts: Layout[]
|
||||||
|
screens: Screen[]
|
||||||
|
libraries: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchAppPackageResponse {
|
||||||
|
application: App
|
||||||
|
licenseType: PlanType
|
||||||
|
screens: Screen[]
|
||||||
|
layouts: Layout[]
|
||||||
|
clientLibPath: string
|
||||||
|
hasLock: boolean
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from "./application"
|
||||||
export * from "./analytics"
|
export * from "./analytics"
|
||||||
export * from "./auth"
|
export * from "./auth"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { User, Document } from "../"
|
import { User, Document, Plugin } from "../"
|
||||||
import { SocketSession } from "../../sdk"
|
import { SocketSession } from "../../sdk"
|
||||||
|
|
||||||
export type AppMetadataErrors = { [key: string]: string[] }
|
export type AppMetadataErrors = { [key: string]: string[] }
|
||||||
|
@ -24,6 +24,8 @@ export interface App extends Document {
|
||||||
icon?: AppIcon
|
icon?: AppIcon
|
||||||
features?: AppFeatures
|
features?: AppFeatures
|
||||||
automations?: AutomationSettings
|
automations?: AutomationSettings
|
||||||
|
usedPlugins?: Plugin[]
|
||||||
|
upgradableVersion?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppInstance {
|
export interface AppInstance {
|
||||||
|
|
|
@ -4,10 +4,10 @@ set -e
|
||||||
if [[ -n $CI ]]
|
if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
# Running in ci, where resources are limited
|
# Running in ci, where resources are limited
|
||||||
echo "jest --coverage --maxWorkers=2 --forceExit --bail"
|
echo "jest --coverage --maxWorkers=2 --forceExit --bail $@"
|
||||||
jest --coverage --maxWorkers=2 --forceExit --bail
|
jest --coverage --maxWorkers=2 --forceExit --bail $@
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
echo "jest --coverage --maxWorkers=2 --forceExit"
|
echo "jest --coverage --maxWorkers=2 --forceExit $@"
|
||||||
jest --coverage --maxWorkers=2 --forceExit
|
jest --coverage --maxWorkers=2 --forceExit $@
|
||||||
fi
|
fi
|
|
@ -1,11 +1,10 @@
|
||||||
import { App } from "@budibase/types"
|
import { App, CreateAppRequest } from "@budibase/types"
|
||||||
import { Response } from "node-fetch"
|
import { Response } from "node-fetch"
|
||||||
import {
|
import {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
AppPackageResponse,
|
AppPackageResponse,
|
||||||
DeployConfig,
|
DeployConfig,
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
CreateAppRequest,
|
|
||||||
} from "../../../types"
|
} from "../../../types"
|
||||||
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
|
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
|
||||||
import BaseAPI from "./BaseAPI"
|
import BaseAPI from "./BaseAPI"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { generator } from "../../shared"
|
import { generator } from "../../shared"
|
||||||
import { CreateAppRequest } from "../../types"
|
import { CreateAppRequest } from "@budibase/types"
|
||||||
|
|
||||||
function uniqueWord() {
|
function uniqueWord() {
|
||||||
return generator.word() + generator.hash()
|
return generator.word() + generator.hash()
|
||||||
|
|
|
@ -13,17 +13,6 @@ describe("Internal API - Table Operations", () => {
|
||||||
await config.afterAll()
|
await config.afterAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function createAppFromTemplate() {
|
|
||||||
return config.api.apps.create({
|
|
||||||
name: generator.word(),
|
|
||||||
url: `/${generator.word()}`,
|
|
||||||
useTemplate: "true",
|
|
||||||
templateName: "Near Miss Register",
|
|
||||||
templateKey: "app/near-miss-register",
|
|
||||||
templateFile: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
it("Create and delete table, columns and rows", async () => {
|
it("Create and delete table, columns and rows", async () => {
|
||||||
// create the app
|
// create the app
|
||||||
await config.createApp(fixtures.apps.appFromTemplate())
|
await config.createApp(fixtures.apps.appFromTemplate())
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { BudibaseInternalAPI } from "../internal-api"
|
import { BudibaseInternalAPI } from "../internal-api"
|
||||||
import { AccountInternalAPI } from "../account-api"
|
import { AccountInternalAPI } from "../account-api"
|
||||||
import { APIRequestOpts, CreateAppRequest, State } from "../types"
|
import { APIRequestOpts, State } from "../types"
|
||||||
import * as fixtures from "../internal-api/fixtures"
|
import * as fixtures from "../internal-api/fixtures"
|
||||||
import { CreateAccountRequest } from "@budibase/types"
|
import { CreateAccountRequest, CreateAppRequest } from "@budibase/types"
|
||||||
|
|
||||||
export default class BudibaseTestConfiguration {
|
export default class BudibaseTestConfiguration {
|
||||||
// apis
|
// apis
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
// TODO: Integrate with budibase
|
|
||||||
export interface CreateAppRequest {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
useTemplate?: string
|
|
||||||
templateName?: string
|
|
||||||
templateKey?: string
|
|
||||||
templateFile?: string
|
|
||||||
includeSampleData?: boolean
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
export * from "./api"
|
export * from "./api"
|
||||||
export * from "./apiKeyResponse"
|
export * from "./apiKeyResponse"
|
||||||
export * from "./app"
|
|
||||||
export * from "./appPackage"
|
export * from "./appPackage"
|
||||||
export * from "./deploy"
|
export * from "./deploy"
|
||||||
export * from "./newAccount"
|
export * from "./newAccount"
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -3902,19 +3902,6 @@
|
||||||
magic-string "^0.25.7"
|
magic-string "^0.25.7"
|
||||||
resolve "^1.17.0"
|
resolve "^1.17.0"
|
||||||
|
|
||||||
"@rollup/plugin-commonjs@^18.0.0":
|
|
||||||
version "18.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-18.1.0.tgz#5a760d757af168a50727c0ae080251fbfcc5eb02"
|
|
||||||
integrity sha512-h3e6T9rUxVMAQswpDIobfUHn/doMzM9sgkMrsMWCFLmB84PSoC8mV8tOloAJjSRwdqhXBqstlX2BwBpHJvbhxg==
|
|
||||||
dependencies:
|
|
||||||
"@rollup/pluginutils" "^3.1.0"
|
|
||||||
commondir "^1.0.1"
|
|
||||||
estree-walker "^2.0.1"
|
|
||||||
glob "^7.1.6"
|
|
||||||
is-reference "^1.2.1"
|
|
||||||
magic-string "^0.25.7"
|
|
||||||
resolve "^1.17.0"
|
|
||||||
|
|
||||||
"@rollup/plugin-commonjs@^25.0.7":
|
"@rollup/plugin-commonjs@^25.0.7":
|
||||||
version "25.0.7"
|
version "25.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf"
|
||||||
|
@ -19257,7 +19244,7 @@ rollup@2.45.2:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.1"
|
fsevents "~2.3.1"
|
||||||
|
|
||||||
rollup@^2.36.2, rollup@^2.44.0, rollup@^2.45.2:
|
rollup@^2.36.2, rollup@^2.45.2:
|
||||||
version "2.79.1"
|
version "2.79.1"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
||||||
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
|
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
|
||||||
|
|
Loading…
Reference in New Issue