Merge branch 'feat/copy-automation-naming-ux-updates' into feat/automation-naming-ux-updates
This commit is contained in:
commit
87f1aa5022
26 changed files with 307 additions and 89 deletions
9
.github/workflows/release-singleimage.yml
vendored
9
.github/workflows/release-singleimage.yml
vendored
|
@ -66,14 +66,21 @@ jobs:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
build-args: BUDIBASE_VERSION=$BUDIBASE_VERSION
|
||||||
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
|
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
|
||||||
file: ./hosting/single/Dockerfile.v2
|
file: ./hosting/single/Dockerfile.v2
|
||||||
|
env:
|
||||||
|
BUDIBASE_VERSION: ${{ env.RELEASE_VERSION }}
|
||||||
- name: Tag and release Budibase Azure App Service docker image
|
- name: Tag and release Budibase Azure App Service docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
build-args: TARGETBUILD=aas
|
build-args: |
|
||||||
|
TARGETBUILD=aas
|
||||||
|
BUDIBASE_VERSION=$BUDIBASE_VERSION
|
||||||
tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }}
|
tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }}
|
||||||
file: ./hosting/single/Dockerfile.v2
|
file: ./hosting/single/Dockerfile.v2
|
||||||
|
env:
|
||||||
|
BUDIBASE_VERSION: ${{ env.RELEASE_VERSION }}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
if [[ $TARGETARCH == arm* ]] ;
|
|
||||||
then
|
|
||||||
echo "INSTALLING ARM64 MINIO"
|
|
||||||
wget https://dl.min.io/server/minio/release/linux-arm64/minio
|
|
||||||
else
|
|
||||||
echo "INSTALLING AMD64 MINIO"
|
|
||||||
wget https://dl.min.io/server/minio/release/linux-amd64/minio
|
|
||||||
fi
|
|
||||||
chmod +x minio
|
|
|
@ -42,6 +42,7 @@ COPY packages/string-templates packages/string-templates
|
||||||
FROM budibase/couchdb as runner
|
FROM budibase/couchdb as runner
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ENV TARGETARCH $TARGETARCH
|
ENV TARGETARCH $TARGETARCH
|
||||||
|
ENV NODE_MAJOR 18
|
||||||
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
||||||
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
||||||
ARG TARGETBUILD=single
|
ARG TARGETBUILD=single
|
||||||
|
@ -49,10 +50,10 @@ ENV TARGETBUILD $TARGETBUILD
|
||||||
|
|
||||||
# install base dependencies
|
# install base dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
|
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server libaio1
|
||||||
|
|
||||||
# Install postgres client for pg_dump utils
|
# Install postgres client for pg_dump utils
|
||||||
RUN apt install software-properties-common apt-transport-https gpg -y \
|
RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \
|
||||||
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
|
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
|
||||||
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
|
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
|
||||||
&& apt update -y \
|
&& apt update -y \
|
||||||
|
@ -61,10 +62,8 @@ RUN apt install software-properties-common apt-transport-https gpg -y \
|
||||||
|
|
||||||
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||||
WORKDIR /nodejs
|
WORKDIR /nodejs
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_18.x -o /tmp/nodesource_setup.sh && \
|
COPY scripts/install-node.sh ./install.sh
|
||||||
bash /tmp/nodesource_setup.sh && \
|
RUN chmod +x install.sh && ./install.sh
|
||||||
apt-get install -y --no-install-recommends libaio1 nodejs && \
|
|
||||||
npm install --global yarn pm2
|
|
||||||
|
|
||||||
# setup nginx
|
# setup nginx
|
||||||
COPY hosting/single/nginx/nginx.conf /etc/nginx
|
COPY hosting/single/nginx/nginx.conf /etc/nginx
|
||||||
|
|
|
@ -77,7 +77,7 @@ mkdir -p ${DATA_DIR}/minio
|
||||||
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
||||||
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
|
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
|
||||||
/bbcouch-runner.sh &
|
/bbcouch-runner.sh &
|
||||||
/minio/minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
|
minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
|
||||||
/etc/init.d/nginx restart
|
/etc/init.d/nginx restart
|
||||||
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
||||||
# Add monthly cron job to renew certbot certificate
|
# Add monthly cron job to renew certbot certificate
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.12.1",
|
"version": "2.12.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -30,6 +30,7 @@ export * as timers from "./timers"
|
||||||
export { default as env } from "./environment"
|
export { default as env } from "./environment"
|
||||||
export * as blacklist from "./blacklist"
|
export * as blacklist from "./blacklist"
|
||||||
export * as docUpdates from "./docUpdates"
|
export * as docUpdates from "./docUpdates"
|
||||||
|
export * from "./utils/Duration"
|
||||||
export { SearchParams } from "./db"
|
export { SearchParams } from "./db"
|
||||||
// Add context to tenancy for backwards compatibility
|
// Add context to tenancy for backwards compatibility
|
||||||
// only do this for external usages to prevent internal
|
// only do this for external usages to prevent internal
|
||||||
|
|
|
@ -36,7 +36,7 @@ class InMemoryQueue {
|
||||||
* @param opts This is not used by the in memory queue as there is no real use
|
* @param opts This is not used by the in memory queue as there is no real use
|
||||||
* case when in memory, but is the same API as Bull
|
* case when in memory, but is the same API as Bull
|
||||||
*/
|
*/
|
||||||
constructor(name: string, opts = null) {
|
constructor(name: string, opts?: any) {
|
||||||
this._name = name
|
this._name = name
|
||||||
this._opts = opts
|
this._opts = opts
|
||||||
this._messages = []
|
this._messages = []
|
||||||
|
|
|
@ -2,11 +2,18 @@ import env from "../environment"
|
||||||
import { getRedisOptions } from "../redis/utils"
|
import { getRedisOptions } from "../redis/utils"
|
||||||
import { JobQueue } from "./constants"
|
import { JobQueue } from "./constants"
|
||||||
import InMemoryQueue from "./inMemoryQueue"
|
import InMemoryQueue from "./inMemoryQueue"
|
||||||
import BullQueue from "bull"
|
import BullQueue, { QueueOptions } from "bull"
|
||||||
import { addListeners, StalledFn } from "./listeners"
|
import { addListeners, StalledFn } from "./listeners"
|
||||||
|
import { Duration } from "../utils"
|
||||||
import * as timers from "../timers"
|
import * as timers from "../timers"
|
||||||
|
import * as Redis from "ioredis"
|
||||||
|
|
||||||
const CLEANUP_PERIOD_MS = 60 * 1000
|
// the queue lock is held for 5 minutes
|
||||||
|
const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
|
||||||
|
// queue lock is refreshed every 30 seconds
|
||||||
|
const QUEUE_LOCK_RENEW_INTERNAL_MS = Duration.fromSeconds(30).toMs()
|
||||||
|
// cleanup the queue every 60 seconds
|
||||||
|
const CLEANUP_PERIOD_MS = Duration.fromSeconds(60).toMs()
|
||||||
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
|
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
|
||||||
let cleanupInterval: NodeJS.Timeout
|
let cleanupInterval: NodeJS.Timeout
|
||||||
|
|
||||||
|
@ -21,7 +28,14 @@ export function createQueue<T>(
|
||||||
opts: { removeStalledCb?: StalledFn } = {}
|
opts: { removeStalledCb?: StalledFn } = {}
|
||||||
): BullQueue.Queue<T> {
|
): BullQueue.Queue<T> {
|
||||||
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
|
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
|
||||||
const queueConfig: any = redisProtocolUrl || { redis: redisOpts }
|
const queueConfig: QueueOptions = {
|
||||||
|
redis: redisProtocolUrl! || (redisOpts as Redis.RedisOptions),
|
||||||
|
settings: {
|
||||||
|
maxStalledCount: 0,
|
||||||
|
lockDuration: QUEUE_LOCK_MS,
|
||||||
|
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
|
||||||
|
},
|
||||||
|
}
|
||||||
let queue: any
|
let queue: any
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
queue = new BullQueue(jobQueue, queueConfig)
|
queue = new BullQueue(jobQueue, queueConfig)
|
||||||
|
|
49
packages/backend-core/src/utils/Duration.ts
Normal file
49
packages/backend-core/src/utils/Duration.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
export enum DurationType {
|
||||||
|
MILLISECONDS = "milliseconds",
|
||||||
|
SECONDS = "seconds",
|
||||||
|
MINUTES = "minutes",
|
||||||
|
HOURS = "hours",
|
||||||
|
DAYS = "days",
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversion: Record<DurationType, number> = {
|
||||||
|
milliseconds: 1,
|
||||||
|
seconds: 1000,
|
||||||
|
minutes: 60 * 1000,
|
||||||
|
hours: 60 * 60 * 1000,
|
||||||
|
days: 24 * 60 * 60 * 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Duration {
|
||||||
|
static convert(from: DurationType, to: DurationType, duration: number) {
|
||||||
|
const milliseconds = duration * conversion[from]
|
||||||
|
return milliseconds / conversion[to]
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(from: DurationType, duration: number) {
|
||||||
|
return {
|
||||||
|
to: (to: DurationType) => {
|
||||||
|
return Duration.convert(from, to, duration)
|
||||||
|
},
|
||||||
|
toMs: () => {
|
||||||
|
return Duration.convert(from, DurationType.MILLISECONDS, duration)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromSeconds(duration: number) {
|
||||||
|
return Duration.from(DurationType.SECONDS, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMinutes(duration: number) {
|
||||||
|
return Duration.from(DurationType.MINUTES, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHours(duration: number) {
|
||||||
|
return Duration.from(DurationType.HOURS, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromDays(duration: number) {
|
||||||
|
return Duration.from(DurationType.DAYS, duration)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./hashing"
|
export * from "./hashing"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
export * from "./stringUtils"
|
export * from "./stringUtils"
|
||||||
|
export * from "./Duration"
|
||||||
|
|
19
packages/backend-core/src/utils/tests/Duration.spec.ts
Normal file
19
packages/backend-core/src/utils/tests/Duration.spec.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Duration, DurationType } from "../Duration"
|
||||||
|
|
||||||
|
describe("duration", () => {
|
||||||
|
it("should convert minutes to milliseconds", () => {
|
||||||
|
expect(Duration.fromMinutes(5).toMs()).toBe(300000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should convert seconds to milliseconds", () => {
|
||||||
|
expect(Duration.fromSeconds(30).toMs()).toBe(30000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should convert days to milliseconds", () => {
|
||||||
|
expect(Duration.fromDays(1).toMs()).toBe(86400000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should convert minutes to days", () => {
|
||||||
|
expect(Duration.fromMinutes(1440).to(DurationType.DAYS)).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
|
@ -5,6 +5,7 @@
|
||||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import { TableNames } from "constants"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -97,7 +98,6 @@
|
||||||
// Ensure any nullish tableId values get set to empty string so
|
// Ensure any nullish tableId values get set to empty string so
|
||||||
// that the select works
|
// that the select works
|
||||||
$: if (value?.tableId == null) value = { tableId: "" }
|
$: if (value?.tableId == null) value = { tableId: "" }
|
||||||
$: console.log($tables.list)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="schema-fields">
|
<div class="schema-fields">
|
||||||
|
@ -106,7 +106,7 @@
|
||||||
<Select
|
<Select
|
||||||
on:change={onChangeTable}
|
on:change={onChangeTable}
|
||||||
value={value.tableId}
|
value={value.tableId}
|
||||||
options={$tables.list.filter(table => table._id !== "ta_users")}
|
options={$tables.list.filter(table => table._id !== TableNames.USERS)}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<Select
|
<Select
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
bind:value
|
bind:value
|
||||||
options={filteredTables}
|
options={filteredTables.filter(table => table._id !== TableNames.USERS)}
|
||||||
getOptionLabel={table => table.name}
|
getOptionLabel={table => table.name}
|
||||||
getOptionValue={table => table._id}
|
getOptionValue={table => table._id}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5467,17 +5467,17 @@
|
||||||
},
|
},
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "select",
|
"type": "table",
|
||||||
|
"label": "Data",
|
||||||
|
"key": "dataSource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "radio",
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"key": "actionType",
|
"key": "actionType",
|
||||||
"options": ["Create", "Update", "View"],
|
"options": ["Create", "Update", "View"],
|
||||||
"defaultValue": "Create"
|
"defaultValue": "Create"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "table",
|
|
||||||
"label": "Data",
|
|
||||||
"key": "dataSource"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Title",
|
"label": "Title",
|
||||||
|
@ -5508,13 +5508,37 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "Empty text",
|
"label": "No rows found",
|
||||||
"key": "noRowsMessage",
|
"key": "noRowsMessage",
|
||||||
"defaultValue": "We couldn't find a row to display",
|
"defaultValue": "We couldn't find a row to display",
|
||||||
"nested": true
|
"nested": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Fields",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "fieldConfiguration",
|
||||||
|
"key": "fields",
|
||||||
|
"nested": true,
|
||||||
|
"resetOn": "dataSource",
|
||||||
|
"selectAllFields": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false,
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "actionType",
|
||||||
|
"value": "View",
|
||||||
|
"invert": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Buttons",
|
"name": "Buttons",
|
||||||
|
@ -5566,30 +5590,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"section": true,
|
|
||||||
"name": "Fields",
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"type": "fieldConfiguration",
|
|
||||||
"key": "fields",
|
|
||||||
"nested": true,
|
|
||||||
"resetOn": "dataSource",
|
|
||||||
"selectAllFields": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "boolean",
|
|
||||||
"label": "Disabled",
|
|
||||||
"key": "disabled",
|
|
||||||
"defaultValue": false,
|
|
||||||
"dependsOn": {
|
|
||||||
"setting": "actionType",
|
|
||||||
"value": "View",
|
|
||||||
"invert": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"tag": "style",
|
"tag": "style",
|
||||||
"type": "select",
|
"type": "select",
|
||||||
|
@ -5924,4 +5924,4 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,15 +220,11 @@
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{/if}
|
{/if}
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{#if description}
|
|
||||||
<BlockComponent
|
|
||||||
type="text"
|
|
||||||
props={{ text: description }}
|
|
||||||
order={1}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</BlockComponent>
|
</BlockComponent>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if description}
|
||||||
|
<BlockComponent type="text" props={{ text: description }} order={1} />
|
||||||
|
{/if}
|
||||||
{#key fields}
|
{#key fields}
|
||||||
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
|
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
|
||||||
{#each fields as field, idx}
|
{#each fields as field, idx}
|
||||||
|
|
|
@ -32,11 +32,8 @@ import {
|
||||||
tenancy,
|
tenancy,
|
||||||
users,
|
users,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { USERS_TABLE_SCHEMA } from "../../constants"
|
import { USERS_TABLE_SCHEMA, DEFAULT_BB_DATASOURCE_ID } from "../../constants"
|
||||||
import {
|
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
|
||||||
buildDefaultDocs,
|
|
||||||
DEFAULT_BB_DATASOURCE_ID,
|
|
||||||
} from "../../db/defaultData/datasource_bb_default"
|
|
||||||
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
|
import { removeAppFromUserRoles } from "../../utilities/workerRequests"
|
||||||
import { stringToReadStream } from "../../utilities"
|
import { stringToReadStream } from "../../utilities"
|
||||||
import { doesUserHaveLock } from "../../utilities/redis"
|
import { doesUserHaveLock } from "../../utilities/redis"
|
||||||
|
|
|
@ -172,3 +172,8 @@ export enum AutomationErrors {
|
||||||
export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
|
export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
|
||||||
export const MAX_AUTOMATION_RECURRING_ERRORS = 5
|
export const MAX_AUTOMATION_RECURRING_ERRORS = 5
|
||||||
export const GOOGLE_SHEETS_PRIMARY_KEY = "rowNumber"
|
export const GOOGLE_SHEETS_PRIMARY_KEY = "rowNumber"
|
||||||
|
export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs"
|
||||||
|
export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory"
|
||||||
|
export const DEFAULT_EXPENSES_TABLE_ID = "ta_bb_expenses"
|
||||||
|
export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee"
|
||||||
|
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { AutoFieldSubTypes, FieldTypes } from "../../constants"
|
import {
|
||||||
|
AutoFieldSubTypes,
|
||||||
|
FieldTypes,
|
||||||
|
DEFAULT_BB_DATASOURCE_ID,
|
||||||
|
DEFAULT_INVENTORY_TABLE_ID,
|
||||||
|
DEFAULT_EMPLOYEE_TABLE_ID,
|
||||||
|
DEFAULT_EXPENSES_TABLE_ID,
|
||||||
|
DEFAULT_JOBS_TABLE_ID,
|
||||||
|
} from "../../constants"
|
||||||
import { importToRows } from "../../api/controllers/table/utils"
|
import { importToRows } from "../../api/controllers/table/utils"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import LinkDocument from "../linkedRows/LinkDocument"
|
import LinkDocument from "../linkedRows/LinkDocument"
|
||||||
|
@ -16,12 +24,6 @@ import {
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const DEFAULT_JOBS_TABLE_ID = "ta_bb_jobs"
|
|
||||||
export const DEFAULT_INVENTORY_TABLE_ID = "ta_bb_inventory"
|
|
||||||
export const DEFAULT_EXPENSES_TABLE_ID = "ta_bb_expenses"
|
|
||||||
export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee"
|
|
||||||
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
|
|
||||||
|
|
||||||
const defaultDatasource = {
|
const defaultDatasource = {
|
||||||
_id: DEFAULT_BB_DATASOURCE_ID,
|
_id: DEFAULT_BB_DATASOURCE_ID,
|
||||||
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
|
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
|
||||||
|
|
|
@ -7,10 +7,12 @@ import {
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { DocumentType, SEPARATOR } from "../db/utils"
|
import { DocumentType, SEPARATOR } from "../db/utils"
|
||||||
import { InvalidColumns, NoEmptyFilterStrings } from "../constants"
|
import {
|
||||||
|
InvalidColumns,
|
||||||
|
NoEmptyFilterStrings,
|
||||||
|
DEFAULT_BB_DATASOURCE_ID,
|
||||||
|
} from "../constants"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
import * as external from "../api/controllers/table/external"
|
|
||||||
import * as internal from "../api/controllers/table/internal"
|
|
||||||
|
|
||||||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||||
const ROW_ID_REGEX = /^\[.*]$/g
|
const ROW_ID_REGEX = /^\[.*]$/g
|
||||||
|
@ -96,7 +98,8 @@ export function isInternalTableID(tableId: string) {
|
||||||
export function isExternalTable(table: Table) {
|
export function isExternalTable(table: Table) {
|
||||||
if (
|
if (
|
||||||
table?.sourceId &&
|
table?.sourceId &&
|
||||||
table.sourceId.includes(DocumentType.DATASOURCE + SEPARATOR)
|
table.sourceId.includes(DocumentType.DATASOURCE + SEPARATOR) &&
|
||||||
|
table?.sourceId !== DEFAULT_BB_DATASOURCE_ID
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
} else if (table?.sourceType === TableSourceType.EXTERNAL) {
|
} else if (table?.sourceType === TableSourceType.EXTERNAL) {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"test:notify": "node scripts/testResultsWebhook",
|
"test:notify": "node scripts/testResultsWebhook",
|
||||||
"test:cloud:prod": "yarn run test --testPathIgnorePatterns=\\.integration\\.",
|
"test:cloud:prod": "yarn run test --testPathIgnorePatterns=\\.integration\\.",
|
||||||
"test:cloud:qa": "yarn run test",
|
"test:cloud:qa": "yarn run test",
|
||||||
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\. \\.license\\.",
|
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\. \\.licensing\\.",
|
||||||
"serve:test:self:ci": "start-server-and-test dev:built http://localhost:4001/health test:self:ci",
|
"serve:test:self:ci": "start-server-and-test dev:built http://localhost:4001/health test:self:ci",
|
||||||
"serve": "start-server-and-test dev:built http://localhost:4001/health",
|
"serve": "start-server-and-test dev:built http://localhost:4001/health",
|
||||||
"dev:built": "cd ../ && yarn dev:built"
|
"dev:built": "cd ../ && yarn dev:built"
|
||||||
|
|
|
@ -99,9 +99,11 @@ export default class LicenseAPI extends BaseAPI {
|
||||||
}, opts)
|
}, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePlan(opts: APIRequestOpts = { status: 200 }) {
|
async updatePlan(priceId: string, opts: APIRequestOpts = { status: 200 }) {
|
||||||
return this.doRequest(() => {
|
return this.doRequest(() => {
|
||||||
return this.client.put(`/api/license/plan`)
|
return this.client.put(`/api/license/plan`, {
|
||||||
|
body: { priceId },
|
||||||
|
})
|
||||||
}, opts)
|
}, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,19 @@ export default class StripeAPI extends BaseAPI {
|
||||||
}, opts)
|
}, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
async linkStripeCustomer(opts: APIRequestOpts = { status: 200 }) {
|
async linkStripeCustomer(
|
||||||
|
accountId: string,
|
||||||
|
stripeCustomerId: string,
|
||||||
|
opts: APIRequestOpts = { status: 200 }
|
||||||
|
) {
|
||||||
return this.doRequest(() => {
|
return this.doRequest(() => {
|
||||||
return this.client.post(`/api/stripe/link`)
|
return this.client.post(`/api/stripe/link`, {
|
||||||
|
body: {
|
||||||
|
accountId,
|
||||||
|
stripeCustomerId,
|
||||||
|
},
|
||||||
|
internal: true,
|
||||||
|
})
|
||||||
}, opts)
|
}, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
114
qa-core/src/account-api/tests/licensing/license.manage.spec.ts
Normal file
114
qa-core/src/account-api/tests/licensing/license.manage.spec.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import TestConfiguration from "../../config/TestConfiguration"
|
||||||
|
import * as fixtures from "../../fixtures"
|
||||||
|
import { Hosting, PlanType } from "@budibase/types"
|
||||||
|
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY)
|
||||||
|
|
||||||
|
describe("license management", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("retrieves plans, creates checkout session, and updates license", async () => {
|
||||||
|
// Create cloud account
|
||||||
|
const createAccountRequest = fixtures.accounts.generateAccount({
|
||||||
|
hosting: Hosting.CLOUD,
|
||||||
|
})
|
||||||
|
const [createAccountRes, account] =
|
||||||
|
await config.accountsApi.accounts.create(createAccountRequest, {
|
||||||
|
autoVerify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Self response has free license
|
||||||
|
await config.doInNewState(async () => {
|
||||||
|
await config.loginAsAccount(createAccountRequest)
|
||||||
|
const [selfRes, selfBody] = await config.api.accounts.self()
|
||||||
|
expect(selfBody.license.plan.type).toBe(PlanType.FREE)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Retrieve plans
|
||||||
|
const [plansRes, planBody] = await config.api.licenses.getPlans()
|
||||||
|
|
||||||
|
// Select priceId from premium plan
|
||||||
|
let premiumPriceId = null
|
||||||
|
let businessPriceId = ""
|
||||||
|
for (const plan of planBody) {
|
||||||
|
if (plan.type === PlanType.PREMIUM) {
|
||||||
|
premiumPriceId = plan.prices[0].priceId
|
||||||
|
}
|
||||||
|
if (plan.type === PlanType.BUSINESS) {
|
||||||
|
businessPriceId = plan.prices[0].priceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create checkout session for price
|
||||||
|
const checkoutSessionRes = await config.api.stripe.createCheckoutSession(
|
||||||
|
premiumPriceId
|
||||||
|
)
|
||||||
|
const checkoutSessionUrl = checkoutSessionRes[1].url
|
||||||
|
expect(checkoutSessionUrl).toContain("checkout.stripe.com")
|
||||||
|
|
||||||
|
// Create stripe customer
|
||||||
|
const customer = await stripe.customers.create({
|
||||||
|
email: createAccountRequest.email,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create payment method
|
||||||
|
const paymentMethod = await stripe.paymentMethods.create({
|
||||||
|
type: "card",
|
||||||
|
card: {
|
||||||
|
token: "tok_visa", // Test Visa Card
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Attach payment method to customer
|
||||||
|
await stripe.paymentMethods.attach(paymentMethod.id, {
|
||||||
|
customer: customer.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update customer
|
||||||
|
await stripe.customers.update(customer.id, {
|
||||||
|
invoice_settings: {
|
||||||
|
default_payment_method: paymentMethod.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create subscription for premium plan
|
||||||
|
const subscription = await stripe.subscriptions.create({
|
||||||
|
customer: customer.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
price: premiumPriceId,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default_payment_method: paymentMethod.id,
|
||||||
|
collection_method: "charge_automatically",
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.doInNewState(async () => {
|
||||||
|
// License updated from Free to Premium
|
||||||
|
await config.loginAsAccount(createAccountRequest)
|
||||||
|
await config.api.stripe.linkStripeCustomer(account.accountId, customer.id)
|
||||||
|
const [_, selfBodyPremium] = await config.api.accounts.self()
|
||||||
|
expect(selfBodyPremium.license.plan.type).toBe(PlanType.PREMIUM)
|
||||||
|
|
||||||
|
// Create portal session - Check URL
|
||||||
|
const [portalRes, portalSessionBody] =
|
||||||
|
await config.api.stripe.createPortalSession(customer.id)
|
||||||
|
expect(portalSessionBody.url).toContain("billing.stripe.com")
|
||||||
|
|
||||||
|
// Update subscription from premium to business license
|
||||||
|
await config.api.licenses.updatePlan(businessPriceId)
|
||||||
|
|
||||||
|
// License updated to Business
|
||||||
|
const [selfRes, selfBodyBusiness] = await config.api.accounts.self()
|
||||||
|
expect(selfBodyBusiness.license.plan.type).toBe(PlanType.BUSINESS)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -28,6 +28,7 @@ const env = {
|
||||||
MARIADB_DB: process.env.MARIADB_DB,
|
MARIADB_DB: process.env.MARIADB_DB,
|
||||||
MARIADB_USER: process.env.MARIADB_USER,
|
MARIADB_USER: process.env.MARIADB_USER,
|
||||||
MARIADB_PASSWORD: process.env.MARIADB_PASSWORD,
|
MARIADB_PASSWORD: process.env.MARIADB_PASSWORD,
|
||||||
|
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
export = env
|
export = env
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
if [[ $TARGETARCH == arm* ]] ;
|
if [[ $TARGETARCH == arm* ]] ;
|
||||||
then
|
then
|
||||||
echo "INSTALLING ARM64 MINIO"
|
echo "INSTALLING ARM64 MINIO"
|
||||||
wget https://dl.min.io/server/minio/release/linux-arm64/minio
|
wget wget https://dl.min.io/server/minio/release/linux-arm64/archive/minio.deb -O minio.deb
|
||||||
else
|
else
|
||||||
echo "INSTALLING AMD64 MINIO"
|
echo "INSTALLING AMD64 MINIO"
|
||||||
wget https://dl.min.io/server/minio/release/linux-amd64/minio
|
wget wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio.deb -O minio.deb
|
||||||
fi
|
fi
|
||||||
chmod +x minio
|
dpkg -i minio.deb
|
||||||
|
|
8
scripts/install-node.sh
Normal file
8
scripts/install-node.sh
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
apt-get install -y gnupg
|
||||||
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor | tee /usr/share/keyrings/nodesource.gpg > /dev/null
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||||
|
apt-get update
|
||||||
|
echo "INSTALLING NODE $NODE_MAJOR"
|
||||||
|
apt-get install -y --no-install-recommends nodejs
|
||||||
|
npm install --global yarn pm2
|
Loading…
Reference in a new issue