diff --git a/.github/workflows/close-featurebranch.yml b/.github/workflows/close-featurebranch.yml new file mode 100644 index 0000000000..0ec3b43598 --- /dev/null +++ b/.github/workflows/close-featurebranch.yml @@ -0,0 +1,21 @@ +name: close-featurebranch + +on: + pull_request: + types: [closed] + branches: + - develop + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: passeidireto/trigger-external-workflow-action@main + env: + PAYLOAD_BRANCH: ${{ github.head_ref }} + PAYLOAD_PR_NUMBER: ${{ github.ref }} + with: + repository: budibase/budibase-deploys + event: featurebranch-qa-close + github_pat: ${{ secrets.GH_ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-featurebranch.yml b/.github/workflows/deploy-featurebranch.yml index 9057d32c4c..f06707ab2b 100644 --- a/.github/workflows/deploy-featurebranch.yml +++ b/.github/workflows/deploy-featurebranch.yml @@ -12,7 +12,8 @@ jobs: - uses: actions/checkout@v3 - uses: passeidireto/trigger-external-workflow-action@main env: - BRANCH: ${{ github.head_ref }} + PAYLOAD_BRANCH: ${{ github.head_ref }} + PAYLOAD_PR_NUMBER: ${{ github.ref }} with: repository: budibase/budibase-deploys event: featurebranch-qa-deploy diff --git a/hosting/proxy/nginx.prod.conf b/hosting/proxy/nginx.prod.conf index 634bce18ac..365765ccbb 100644 --- a/hosting/proxy/nginx.prod.conf +++ b/hosting/proxy/nginx.prod.conf @@ -55,7 +55,7 @@ http { set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_object "object-src 'none'"; set $csp_base_uri "base-uri 'self'"; - set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibase.qa https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; + set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibaseqa.app https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_frame "frame-src 'self' https:"; set $csp_img "img-src http: https: data: blob:"; diff --git a/lerna.json b/lerna.json index c0e77576f1..0283ac132c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.10.12-alpha.7", + "version": "2.10.12-alpha.17", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 6df4105e25..d758888eab 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@esbuild-plugins/tsconfig-paths": "^0.1.2", "@nx/js": "16.4.3", "@rollup/plugin-json": "^4.0.2", - "@typescript-eslint/parser": "5.45.0", + "@typescript-eslint/parser": "6.7.2", "esbuild": "^0.18.17", "esbuild-node-externals": "^1.8.0", "eslint": "^8.44.0", @@ -22,7 +22,7 @@ "rimraf": "^3.0.2", "rollup-plugin-replace": "^2.2.0", "svelte": "^3.38.2", - "typescript": "4.7.3", + "typescript": "5.2.2", "@babel/core": "^7.22.5", "@babel/eslint-parser": "^7.22.5", "@babel/preset-env": "^7.22.5", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 739469b49a..1c94163d93 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -87,7 +87,7 @@ "timekeeper": "2.2.0", "ts-node": "10.8.1", "tsconfig-paths": "4.0.0", - "typescript": "4.7.3" + "typescript": "5.2.2" }, "nx": { "targets": { diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index 309f0fd159..758fd6bf9a 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -86,8 +86,8 @@ export const useAuditLogs = () => { return useFeature(Feature.AUDIT_LOGS) } -export const usePublicApiUserRoles = () => { - return useFeature(Feature.USER_ROLE_PUBLIC_API) +export const useExpandedPublicApi = () => { + return useFeature(Feature.EXPANDED_PUBLIC_API) } export const useScimIntegration = () => { diff --git a/packages/backend-core/tests/core/utilities/structures/users.ts b/packages/backend-core/tests/core/utilities/structures/users.ts index 420a9fde0e..66d23696e0 100644 --- a/packages/backend-core/tests/core/utilities/structures/users.ts +++ b/packages/backend-core/tests/core/utilities/structures/users.ts @@ -10,14 +10,13 @@ import { authDetails } from "./sso" import { uuid } from "./common" import { generator } from "./generator" import { tenant } from "." -import { generateGlobalUserID } from "../../../../src/docIds" export const newEmail = () => { return `${uuid()}@test.com` } export const user = (userProps?: Partial>): User => { - const userId = userProps?._id || generateGlobalUserID() + const userId = userProps?._id return { _id: userId, userId, @@ -53,7 +52,7 @@ export const adminOnlyUser = (userProps?: any): AdminOnlyUser => { } } -export const builderUser = (userProps?: any): BuilderUser => { +export const builderUser = (userProps?: Partial): BuilderUser => { return { ...user(userProps), builder: { diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index 8816da33c4..8d72dd0652 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -14,12 +14,12 @@ export let autocomplete = false export let sort = false export let autoWidth = false - export let fetchTerm = null - export let useFetch = false + export let searchTerm = null export let customPopoverHeight export let customPopoverOffsetBelow export let customPopoverMaxHeight export let open = false + export let loading const dispatch = createEventDispatcher() @@ -82,6 +82,7 @@ diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 9b90c1a865..aa06d5f748 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -2,7 +2,7 @@ import "@spectrum-css/picker/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css" - import { createEventDispatcher } from "svelte" + import { createEventDispatcher, onDestroy } from "svelte" import clickOutside from "../../Actions/click_outside" import Search from "./Search.svelte" import Icon from "../../Icon/Icon.svelte" @@ -10,6 +10,7 @@ import Popover from "../../Popover/Popover.svelte" import Tags from "../../Tags/Tags.svelte" import Tag from "../../Tags/Tag.svelte" + import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte" export let id = null export let disabled = false @@ -35,19 +36,20 @@ export let autoWidth = false export let autocomplete = false export let sort = false - export let fetchTerm = null - export let useFetch = false + export let searchTerm = null export let customPopoverHeight export let customPopoverOffsetBelow export let customPopoverMaxHeight export let align = "left" export let footer = null export let customAnchor = null + export let loading + const dispatch = createEventDispatcher() - let searchTerm = null let button let popover + let component $: sortedOptions = getSortedOptions(options, getOptionLabel, sort) $: filteredOptions = getFilteredOptions( @@ -82,7 +84,7 @@ } const getFilteredOptions = (options, term, getLabel) => { - if (autocomplete && term && !fetchTerm) { + if (autocomplete && term) { const lowerCaseTerm = term.toLowerCase() return options.filter(option => { return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm) @@ -90,6 +92,20 @@ } return options } + + const onScroll = e => { + const scrollPxThreshold = 100 + const scrollPositionFromBottom = + e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop + if (scrollPositionFromBottom < scrollPxThreshold) { + dispatch("loadMore") + } + } + + $: component?.addEventListener("scroll", onScroll) + onDestroy(() => { + component?.removeEventListener("scroll", null) + }) + + + + + diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte index 1c264a5aaf..0cc61c69e6 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte @@ -180,7 +180,7 @@
selectTable(TableNames.USERS)} diff --git a/packages/builder/src/components/start/ImportAppModal.svelte b/packages/builder/src/components/start/ImportAppModal.svelte new file mode 100644 index 0000000000..7d30ded896 --- /dev/null +++ b/packages/builder/src/components/start/ImportAppModal.svelte @@ -0,0 +1,69 @@ + + + + Updating an app using an app export will replace all tables, datasources, + queries, screens and automations. It is recommended to perform a backup + before running this operation. + + { + file = e.detail?.[0] + }} + /> + + {#if encrypted} + + {/if} + + diff --git a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte index 519a9c0f45..801ddd1130 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte @@ -32,9 +32,9 @@ active={$isActive("./embed")} /> app.devId == $store.appId) $: app = filteredApps.length ? filteredApps[0] : {} $: appDeployed = app?.status === AppStatus.DEPLOYED - let exportModal + let exportModal, importModal let exportPublishedVersion = false const exportApp = opts => { exportPublishedVersion = !!opts?.published exportModal.show() } + + const importApp = () => { + importModal.show() + } + + + + Export your app Export your latest edited or published app - -
+
exportApp({ published: false })}> Export latest edited app @@ -47,10 +55,20 @@ Export latest published app
+ + + Import your app + Import an export to update this app + +
+ importApp()}> + Import app + +
diff --git a/packages/builder/src/pages/builder/portal/apps/_components/PortalSideBar.svelte b/packages/builder/src/pages/builder/portal/apps/_components/PortalSideBar.svelte new file mode 100644 index 0000000000..1e21bd7a9a --- /dev/null +++ b/packages/builder/src/pages/builder/portal/apps/_components/PortalSideBar.svelte @@ -0,0 +1,140 @@ + + + + + diff --git a/packages/builder/src/pages/builder/portal/apps/_layout.svelte b/packages/builder/src/pages/builder/portal/apps/_layout.svelte index bf0bca0df4..c4a0bfd913 100644 --- a/packages/builder/src/pages/builder/portal/apps/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/apps/_layout.svelte @@ -11,6 +11,7 @@ import { onMount } from "svelte" import { redirect } from "@roxi/routify" import { sdk } from "@budibase/shared-core" + import PortalSideBar from "./_components/PortalSideBar.svelte" // Don't block loading if we've already hydrated state let loaded = $apps.length != null @@ -44,5 +45,18 @@ {#if loaded} - +
+ + +
{/if} + + diff --git a/packages/builder/src/stores/portal/index.js b/packages/builder/src/stores/portal/index.js index a7c430e621..e70df5c3ee 100644 --- a/packages/builder/src/stores/portal/index.js +++ b/packages/builder/src/stores/portal/index.js @@ -1,3 +1,5 @@ +import { writable } from "svelte/store" + export { organisation } from "./organisation" export { users } from "./users" export { admin } from "./admin" @@ -14,3 +16,5 @@ export { environment } from "./environment" export { menu } from "./menu" export { auditLogs } from "./auditLogs" export { features } from "./features" + +export const sideBarCollapsed = writable(false) diff --git a/packages/cli/package.json b/packages/cli/package.json index 6d4d78b7d7..04eeb523cd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,6 +62,6 @@ "eslint": "^7.20.0", "renamer": "^4.0.0", "ts-node": "^10.9.1", - "typescript": "4.7.3" + "typescript": "5.2.2" } } diff --git a/packages/cli/src/plugins/index.ts b/packages/cli/src/plugins/index.ts index 45e024be7f..c29de2a297 100644 --- a/packages/cli/src/plugins/index.ts +++ b/packages/cli/src/plugins/index.ts @@ -49,8 +49,8 @@ async function askAboutTopLevel(name: string) { } } -async function init(opts: PluginOpts) { - const type = opts["init"] || (opts as PluginType) +async function init(opts: PluginOpts | PluginType) { + const type = (opts as PluginOpts).init || (opts as PluginType) if (!type || !PLUGIN_TYPE_ARR.includes(type)) { console.log( error( diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte index ecbbfcde6a..c9fe4a8549 100644 --- a/packages/client/src/components/app/forms/RelationshipField.svelte +++ b/packages/client/src/components/app/forms/RelationshipField.svelte @@ -1,11 +1,6 @@ {#if fieldState} -
- option._id} - {placeholder} - customPopoverOffsetBelow={autocomplete ? 32 : null} - customPopoverMaxHeight={autocomplete ? 240 : null} - sort={true} - /> - {#if autocomplete} - - {/if} -
+ option._id} + {placeholder} + bind:searchTerm + loading={$fetch.loading} + bind:open + customPopoverMaxHeight={400} + /> {/if}
- - diff --git a/packages/frontend-core/src/api/app.js b/packages/frontend-core/src/api/app.js index 982066f05a..49137cbecd 100644 --- a/packages/frontend-core/src/api/app.js +++ b/packages/frontend-core/src/api/app.js @@ -1,3 +1,5 @@ +import { sdk } from "@budibase/shared-core" + export const buildAppEndpoints = API => ({ /** * Fetches screen definition for an app. @@ -81,6 +83,22 @@ export const buildAppEndpoints = API => ({ }) }, + /** + * Update an application using an export - the body + * should be of type FormData, with a "file" and a "password" if encrypted. + * @param appId The ID of the app to update - this will always be + * converted to development ID. + * @param body a FormData body with a file and password. + */ + updateAppFromExport: async (appId, body) => { + const devId = sdk.applications.getDevAppID(appId) + return await API.post({ + url: `/api/applications/${devId}/import`, + body, + json: false, + }) + }, + /** * Imports an export of all apps. * @param apps the FormData containing the apps to import diff --git a/packages/pro b/packages/pro index 4638ae916e..3038568214 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 4638ae916e55ce89166095578cbd01745d0ee9ee +Subproject commit 30385682141e5ba9d98de7d71d5be1672109cd15 diff --git a/packages/server/package.json b/packages/server/package.json index 8851237d8e..c4d4f883b7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -153,7 +153,6 @@ "@types/server-destroy": "1.0.1", "@types/supertest": "2.0.12", "@types/tar": "6.1.5", - "@typescript-eslint/parser": "5.45.0", "apidoc": "0.50.4", "babel-jest": "29.6.2", "copyfiles": "2.4.1", @@ -174,7 +173,7 @@ "timekeeper": "2.2.0", "ts-node": "10.8.1", "tsconfig-paths": "4.0.0", - "typescript": "4.7.3", + "typescript": "5.2.2", "update-dotenv": "1.1.1" }, "optionalDependencies": { diff --git a/packages/server/scripts/integrations/postgres/init.sql b/packages/server/scripts/integrations/postgres/init.sql index f89ad2812d..b7ce1b7d5b 100644 --- a/packages/server/scripts/integrations/postgres/init.sql +++ b/packages/server/scripts/integrations/postgres/init.sql @@ -9,6 +9,7 @@ CREATE TABLE Persons ( Address varchar(255), City varchar(255) DEFAULT 'Belfast', Age INTEGER DEFAULT 20 NOT NULL, + Year INTEGER, Type person_job ); CREATE TABLE Tasks ( @@ -49,9 +50,10 @@ CREATE TABLE CompositeTable ( Name varchar(255), PRIMARY KEY (KeyPartOne, KeyPartTwo) ); -INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast', 'qa'); -INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer'); -INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age) VALUES ('Foo', 'Bar', 'Foo Street', 'Bartown', 'support', 0); +INSERT INTO Persons (FirstName, LastName, Address, City, Type, Year) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast', 'qa', 1999); +INSERT INTO Persons (FirstName, LastName, Address, City, Type, Year) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer', 1996); +INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Foo', 'Bar', 'Foo Street', 'Bartown', 'support', 0, 1993); +INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Jonny', 'Muffin', 'Muffin Street', 'Cork', 'support'); INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE); INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (2, 1, 'processing', FALSE); INSERT INTO Products (ProductName) VALUES ('Computers'); diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index 1e5718c5b5..18f9dd4245 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -613,6 +613,23 @@ "data" ] }, + "appExport": { + "type": "object", + "properties": { + "encryptPassword": { + "description": "An optional password used to encrypt the export.", + "type": "string" + }, + "excludeRows": { + "description": "Set whether the internal table rows should be excluded from the export.", + "type": "boolean" + } + }, + "required": [ + "encryptPassword", + "excludeRows" + ] + }, "row": { "description": "The row to be created/updated, based on the table schema.", "type": "object", @@ -2163,6 +2180,87 @@ } } }, + "/applications/{appId}/import": { + "post": { + "operationId": "appImport", + "summary": "Import an app to an existing app 🔒", + "description": "This endpoint is only available on a business or enterprise license.", + "tags": [ + "applications" + ], + "parameters": [ + { + "$ref": "#/components/parameters/appIdUrl" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "encryptedPassword": { + "description": "Password for the export if it is encrypted.", + "type": "string" + }, + "appExport": { + "description": "The app export to import.", + "type": "string", + "format": "binary" + } + }, + "required": [ + "appExport" + ] + } + } + } + }, + "responses": { + "204": { + "description": "Application has been updated." + } + } + } + }, + "/applications/{appId}/export": { + "post": { + "operationId": "appExport", + "summary": "Export an app 🔒", + "description": "This endpoint is only available on a business or enterprise license.", + "tags": [ + "applications" + ], + "parameters": [ + { + "$ref": "#/components/parameters/appIdUrl" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/appExport" + } + } + } + }, + "responses": { + "200": { + "description": "A gzip tarball containing the app export, encrypted if password provided.", + "content": { + "application/gzip": { + "schema": { + "type": "string", + "format": "binary", + "example": "Tarball containing database and object store contents..." + } + } + } + } + } + } + }, "/applications/search": { "post": { "operationId": "appSearch", diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index 07320917b8..4916141569 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -587,6 +587,19 @@ components: - appUrl required: - data + appExport: + type: object + properties: + encryptPassword: + description: An optional password used to encrypt the export. + type: string + excludeRows: + description: Set whether the internal table rows should be excluded from the + export. + type: boolean + required: + - encryptPassword + - excludeRows row: description: The row to be created/updated, based on the table schema. type: object @@ -1763,6 +1776,57 @@ paths: examples: deployment: $ref: "#/components/examples/deploymentOutput" + "/applications/{appId}/import": + post: + operationId: appImport + summary: Import an app to an existing app 🔒 + description: This endpoint is only available on a business or enterprise license. + tags: + - applications + parameters: + - $ref: "#/components/parameters/appIdUrl" + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + encryptedPassword: + description: Password for the export if it is encrypted. + type: string + appExport: + description: The app export to import. + type: string + format: binary + required: + - appExport + responses: + "204": + description: Application has been updated. + "/applications/{appId}/export": + post: + operationId: appExport + summary: Export an app 🔒 + description: This endpoint is only available on a business or enterprise license. + tags: + - applications + parameters: + - $ref: "#/components/parameters/appIdUrl" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/appExport" + responses: + "200": + description: A gzip tarball containing the app export, encrypted if password + provided. + content: + application/gzip: + schema: + type: string + format: binary + example: Tarball containing database and object store contents... /applications/search: post: operationId: appSearch diff --git a/packages/server/specs/resources/application.ts b/packages/server/specs/resources/application.ts index cd7a68c049..081dd9e72a 100644 --- a/packages/server/specs/resources/application.ts +++ b/packages/server/specs/resources/application.ts @@ -134,4 +134,15 @@ export default new Resource() deploymentOutput: object({ data: deploymentOutputSchema, }), + appExport: object({ + encryptPassword: { + description: "An optional password used to encrypt the export.", + type: "string", + }, + excludeRows: { + description: + "Set whether the internal table rows should be excluded from the export.", + type: "boolean", + }, + }), }) diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index 012aa7c66d..99241a4831 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -39,9 +39,8 @@ import { } from "../../db/defaultData/datasource_bb_default" import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { stringToReadStream } from "../../utilities" -import { doesUserHaveLock, getLocksById } from "../../utilities/redis" +import { doesUserHaveLock } from "../../utilities/redis" import { cleanupAutomations } from "../../automations/utils" -import { checkAppMetadata } from "../../automations/logging" import { getUniqueRows } from "../../utilities/usageQuota/rows" import { groups, licensing, quotas } from "@budibase/pro" import { @@ -51,7 +50,6 @@ import { PlanType, Screen, UserCtx, - ContextUser, } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import sdk from "../../sdk" @@ -575,6 +573,28 @@ export async function sync(ctx: UserCtx) { } } +export async function importToApp(ctx: UserCtx) { + const { appId } = ctx.params + const appExport = ctx.request.files?.appExport + const password = ctx.request.body.encryptionPassword as string + if (!appExport) { + ctx.throw(400, "Must supply app export to import") + } + if (Array.isArray(appExport)) { + ctx.throw(400, "Must only supply one app export") + } + const fileAttributes = { type: appExport.type!, path: appExport.path! } + try { + await sdk.applications.updateWithExport(appId, fileAttributes, password) + } catch (err: any) { + ctx.throw( + 500, + `Unable to perform update, please retry - ${err?.message || err}` + ) + } + ctx.body = { message: "app updated" } +} + export async function updateAppPackage(appPackage: any, appId: any) { return context.doInAppContext(appId, async () => { const db = context.getAppDB() diff --git a/packages/server/src/api/controllers/public/applications.ts b/packages/server/src/api/controllers/public/applications.ts index fd72db95d3..316da72377 100644 --- a/packages/server/src/api/controllers/public/applications.ts +++ b/packages/server/src/api/controllers/public/applications.ts @@ -2,9 +2,11 @@ import { db as dbCore, context } from "@budibase/backend-core" import { search as stringSearch, addRev } from "./utils" import * as controller from "../application" import * as deployController from "../deploy" +import * as backupController from "../backup" import { Application } from "../../../definitions/common" import { UserCtx } from "@budibase/types" import { Next } from "koa" +import { sdk as proSdk } from "@budibase/pro" function fixAppID(app: Application, params: any) { if (!params) { @@ -80,6 +82,8 @@ export async function destroy(ctx: UserCtx, next: Next) { export async function unpublish(ctx: UserCtx, next: Next) { await context.doInAppContext(ctx.params.appId, async () => { await controller.unpublish(ctx) + ctx.body = undefined + ctx.status = 204 await next() }) } @@ -91,12 +95,22 @@ export async function publish(ctx: UserCtx, next: Next) { }) } +// get licensed endpoints from pro +export const importToApp = proSdk.publicApi.applications.buildImportFn( + controller.importToApp +) +export const exportApp = proSdk.publicApi.applications.buildExportFn( + backupController.exportAppDump +) + export default { create, update, read, destroy, search, - publish, unpublish, + publish, + importToApp, + exportApp, } diff --git a/packages/server/src/api/routes/application.ts b/packages/server/src/api/routes/application.ts index 18760d485a..a21d6a2153 100644 --- a/packages/server/src/api/routes/application.ts +++ b/packages/server/src/api/routes/application.ts @@ -4,6 +4,7 @@ import * as deploymentController from "../controllers/deploy" import authorized from "../../middleware/authorized" import { permissions } from "@budibase/backend-core" import { applicationValidator } from "./utils/validators" +import { importToApp } from "../controllers/application" const router: Router = new Router() @@ -58,5 +59,10 @@ router authorized(permissions.GLOBAL_BUILDER), controller.destroy ) + .post( + "/api/applications/:appId/import", + authorized(permissions.BUILDER), + controller.importToApp + ) export default router diff --git a/packages/server/src/api/routes/public/applications.ts b/packages/server/src/api/routes/public/applications.ts index 088d974e6c..5410eb7dcf 100644 --- a/packages/server/src/api/routes/public/applications.ts +++ b/packages/server/src/api/routes/public/applications.ts @@ -137,6 +137,70 @@ write.push( new Endpoint("post", "/applications/:appId/publish", controller.publish) ) +/** + * @openapi + * /applications/{appId}/import: + * post: + * operationId: appImport + * summary: Import an app to an existing app 🔒 + * description: This endpoint is only available on a business or enterprise license. + * tags: + * - applications + * parameters: + * - $ref: '#/components/parameters/appIdUrl' + * requestBody: + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * encryptedPassword: + * description: Password for the export if it is encrypted. + * type: string + * appExport: + * description: The app export to import. + * type: string + * format: binary + * required: + * - appExport + * responses: + * 204: + * description: Application has been updated. + */ +write.push( + new Endpoint("post", "/applications/:appId/import", controller.importToApp) +) + +/** + * @openapi + * /applications/{appId}/export: + * post: + * operationId: appExport + * summary: Export an app 🔒 + * description: This endpoint is only available on a business or enterprise license. + * tags: + * - applications + * parameters: + * - $ref: '#/components/parameters/appIdUrl' + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/appExport' + * responses: + * 200: + * description: A gzip tarball containing the app export, encrypted if password provided. + * content: + * application/gzip: + * schema: + * type: string + * format: binary + * example: Tarball containing database and object store contents... + */ +read.push( + new Endpoint("post", "/applications/:appId/export", controller.exportApp) +) + /** * @openapi * /applications/{appId}: diff --git a/packages/server/src/api/routes/public/middleware/mapper.ts b/packages/server/src/api/routes/public/middleware/mapper.ts index 138a5ac23f..03feb6cc5c 100644 --- a/packages/server/src/api/routes/public/middleware/mapper.ts +++ b/packages/server/src/api/routes/public/middleware/mapper.ts @@ -1,3 +1,4 @@ +import { Ctx } from "@budibase/types" import mapping from "../../../controllers/public/mapping" enum Resources { @@ -9,11 +10,19 @@ enum Resources { SEARCH = "search", } -function isArrayResponse(ctx: any) { +function isAttachment(ctx: Ctx) { + return ctx.body?.path && ctx.body?.flags && ctx.body?.mode +} + +function isArrayResponse(ctx: Ctx) { return ctx.url.endsWith(Resources.SEARCH) || Array.isArray(ctx.body) } -function processApplications(ctx: any) { +function noResponse(ctx: Ctx) { + return !Array.isArray(ctx.body) && Object.keys(ctx.body).length === 0 +} + +function processApplications(ctx: Ctx) { if (isArrayResponse(ctx)) { return mapping.mapApplications(ctx) } else { @@ -21,7 +30,7 @@ function processApplications(ctx: any) { } } -function processTables(ctx: any) { +function processTables(ctx: Ctx) { if (isArrayResponse(ctx)) { return mapping.mapTables(ctx) } else { @@ -29,7 +38,7 @@ function processTables(ctx: any) { } } -function processRows(ctx: any) { +function processRows(ctx: Ctx) { if (isArrayResponse(ctx)) { return mapping.mapRowSearch(ctx) } else { @@ -37,7 +46,7 @@ function processRows(ctx: any) { } } -function processUsers(ctx: any) { +function processUsers(ctx: Ctx) { if (isArrayResponse(ctx)) { return mapping.mapUsers(ctx) } else { @@ -45,7 +54,7 @@ function processUsers(ctx: any) { } } -function processQueries(ctx: any) { +function processQueries(ctx: Ctx) { if (isArrayResponse(ctx)) { return mapping.mapQueries(ctx) } else { @@ -53,8 +62,8 @@ function processQueries(ctx: any) { } } -export default async (ctx: any, next: any) => { - if (!ctx.body) { +export default async (ctx: Ctx, next: any) => { + if (!ctx.body || noResponse(ctx) || isAttachment(ctx)) { return await next() } let urlParts = ctx.url.split("/") diff --git a/packages/server/src/api/routes/public/tests/applications.spec.ts b/packages/server/src/api/routes/public/tests/applications.spec.ts new file mode 100644 index 0000000000..0a2ffe9e95 --- /dev/null +++ b/packages/server/src/api/routes/public/tests/applications.spec.ts @@ -0,0 +1,91 @@ +import * as setup from "../../tests/utilities" +import { + generateMakeRequest, + generateMakeRequestWithFormData, + MakeRequestResponse, + MakeRequestWithFormDataResponse, +} from "./utils" +import { User } from "@budibase/types" +import { join } from "path" +import { mocks } from "@budibase/backend-core/tests" + +const PASSWORD = "testtest" +const NO_LICENSE_MSG = "Endpoint unavailable, license required." + +let config = setup.getConfig() +let apiKey: string, + globalUser: User, + makeRequest: MakeRequestResponse, + makeRequestFormData: MakeRequestWithFormDataResponse + +beforeAll(async () => { + await config.init() + globalUser = await config.globalUser() + apiKey = await config.generateApiKey(globalUser._id) + makeRequest = generateMakeRequest(apiKey) + makeRequestFormData = generateMakeRequestWithFormData(apiKey) +}) + +afterAll(setup.afterAll) + +describe("check export/import", () => { + async function runExport() { + return await makeRequest("post", `/applications/${config.appId}/export`, { + encryptionPassword: PASSWORD, + excludeRows: true, + }) + } + + async function runImport() { + const pathToExport = join( + __dirname, + "..", + "..", + "tests", + "assets", + "export.tar.gz" + ) + return await makeRequestFormData( + "post", + `/applications/${config.appId}/import`, + { + encryptionPassword: PASSWORD, + appExport: { path: pathToExport }, + } + ) + } + + it("check licensing for export", async () => { + const res = await runExport() + expect(res.status).toBe(403) + expect(res.body.message).toBe(NO_LICENSE_MSG) + }) + + it("check licensing for import", async () => { + const res = await runImport() + expect(res.status).toBe(403) + expect(res.body.message).toBe(NO_LICENSE_MSG) + }) + + it("should be able to export app", async () => { + mocks.licenses.useExpandedPublicApi() + const res = await runExport() + expect(res.headers["content-disposition"]).toMatch( + /attachment; filename=".*-export-.*\.tar.gz"/g + ) + expect(res.body instanceof Buffer).toBe(true) + expect(res.status).toBe(200) + }) + + it("should be able to import app", async () => { + mocks.licenses.useExpandedPublicApi() + const res = await runImport() + expect(Object.keys(res.body).length).toBe(0) + // check screens imported correctly + const screens = await config.api.screen.list() + expect(screens.length).toBe(2) + expect(screens[0].routing.route).toBe("/derp") + expect(screens[1].routing.route).toBe("/blank") + expect(res.status).toBe(204) + }) +}) diff --git a/packages/server/src/api/routes/public/tests/users.spec.ts b/packages/server/src/api/routes/public/tests/users.spec.ts index c81acca1df..9d38dc4791 100644 --- a/packages/server/src/api/routes/public/tests/users.spec.ts +++ b/packages/server/src/api/routes/public/tests/users.spec.ts @@ -92,7 +92,7 @@ describe("no user role update in free", () => { describe("no user role update in business", () => { beforeAll(() => { updateMock() - mocks.licenses.usePublicApiUserRoles() + mocks.licenses.useExpandedPublicApi() }) it("should allow 'roles' to be updated", async () => { @@ -105,7 +105,7 @@ describe("no user role update in business", () => { }) it("should allow 'admin' to be updated", async () => { - mocks.licenses.usePublicApiUserRoles() + mocks.licenses.useExpandedPublicApi() const res = await makeRequest("post", "/users", { ...base(), admin: { global: true }, @@ -115,7 +115,7 @@ describe("no user role update in business", () => { }) it("should allow 'builder' to be updated", async () => { - mocks.licenses.usePublicApiUserRoles() + mocks.licenses.useExpandedPublicApi() const res = await makeRequest("post", "/users", { ...base(), builder: { global: true }, diff --git a/packages/server/src/api/routes/public/tests/utils.ts b/packages/server/src/api/routes/public/tests/utils.ts index 755e2d659f..1b57682af9 100644 --- a/packages/server/src/api/routes/public/tests/utils.ts +++ b/packages/server/src/api/routes/public/tests/utils.ts @@ -11,6 +11,32 @@ export type MakeRequestResponse = ( intAppId?: string ) => Promise +export type MakeRequestWithFormDataResponse = ( + method: HttpMethod, + endpoint: string, + fields: Record, + intAppId?: string +) => Promise + +function base( + apiKey: string, + endpoint: string, + intAppId: string | null, + isInternal: boolean +) { + const extraHeaders: any = { + "x-budibase-api-key": apiKey, + } + if (intAppId) { + extraHeaders["x-budibase-app-id"] = intAppId + } + + const url = isInternal + ? endpoint + : checkSlashesInUrl(`/api/public/v1/${endpoint}`) + return { headers: extraHeaders, url } +} + export function generateMakeRequest( apiKey: string, isInternal = false @@ -23,18 +49,8 @@ export function generateMakeRequest( body?: any, intAppId: string | null = config.getAppId() ) => { - const extraHeaders: any = { - "x-budibase-api-key": apiKey, - } - if (intAppId) { - extraHeaders["x-budibase-app-id"] = intAppId - } - - const url = isInternal - ? endpoint - : checkSlashesInUrl(`/api/public/v1/${endpoint}`) - - const req = request[method](url).set(config.defaultHeaders(extraHeaders)) + const { headers, url } = base(apiKey, endpoint, intAppId, isInternal) + const req = request[method](url).set(config.defaultHeaders(headers)) if (body) { req.send(body) } @@ -43,3 +59,30 @@ export function generateMakeRequest( return res } } + +export function generateMakeRequestWithFormData( + apiKey: string, + isInternal = false +): MakeRequestWithFormDataResponse { + const request = setup.getRequest()! + const config = setup.getConfig()! + return async ( + method: HttpMethod, + endpoint: string, + fields: Record, + intAppId: string | null = config.getAppId() + ) => { + const { headers, url } = base(apiKey, endpoint, intAppId, isInternal) + const req = request[method](url).set(config.defaultHeaders(headers)) + for (let [field, value] of Object.entries(fields)) { + if (typeof value === "string") { + req.field(field, value) + } else { + req.attach(field, value.path) + } + } + const res = await req + expect(res.body).toBeDefined() + return res + } +} diff --git a/packages/server/src/api/routes/tests/appImport.spec.ts b/packages/server/src/api/routes/tests/appImport.spec.ts new file mode 100644 index 0000000000..ef3c739e72 --- /dev/null +++ b/packages/server/src/api/routes/tests/appImport.spec.ts @@ -0,0 +1,32 @@ +import * as setup from "./utilities" +import path from "path" + +jest.setTimeout(15000) +const PASSWORD = "testtest" + +describe("/applications/:appId/import", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + afterAll(setup.afterAll) + + beforeAll(async () => { + await config.init() + }) + + it("should be able to perform import", async () => { + const appId = config.getAppId() + const res = await request + .post(`/api/applications/${appId}/import`) + .field("encryptionPassword", PASSWORD) + .attach("appExport", path.join(__dirname, "assets", "export.tar.gz")) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toBe("app updated") + const screens = await config.api.screen.list() + expect(screens.length).toBe(2) + expect(screens[0].routing.route).toBe("/derp") + expect(screens[1].routing.route).toBe("/blank") + }) +}) diff --git a/packages/server/src/api/routes/tests/assets/export.tar.gz b/packages/server/src/api/routes/tests/assets/export.tar.gz new file mode 100644 index 0000000000..af16873a78 Binary files /dev/null and b/packages/server/src/api/routes/tests/assets/export.tar.gz differ diff --git a/packages/server/src/definitions/openapi.ts b/packages/server/src/definitions/openapi.ts index fe5c17b218..52434494e5 100644 --- a/packages/server/src/definitions/openapi.ts +++ b/packages/server/src/definitions/openapi.ts @@ -18,6 +18,14 @@ export interface paths { "/applications/{appId}/publish": { post: operations["appPublish"]; }; + "/applications/{appId}/import": { + /** This endpoint is only available on a business or enterprise license. */ + post: operations["appImport"]; + }; + "/applications/{appId}/export": { + /** This endpoint is only available on a business or enterprise license. */ + post: operations["appExport"]; + }; "/applications/search": { /** Based on application properties (currently only name) search for applications. */ post: operations["appSearch"]; @@ -158,6 +166,12 @@ export interface components { appUrl: string; }; }; + appExport: { + /** @description An optional password used to encrypt the export. */ + encryptPassword: string; + /** @description Set whether the internal table rows should be excluded from the export. */ + excludeRows: boolean; + }; /** @description The row to be created/updated, based on the table schema. */ row: { [key: string]: unknown }; searchOutput: { @@ -889,6 +903,54 @@ export interface operations { }; }; }; + /** This endpoint is only available on a business or enterprise license. */ + appImport: { + parameters: { + path: { + /** The ID of the app which this request is targeting. */ + appId: components["parameters"]["appIdUrl"]; + }; + }; + responses: { + /** Application has been updated. */ + 204: never; + }; + requestBody: { + content: { + "multipart/form-data": { + /** @description Password for the export if it is encrypted. */ + encryptedPassword?: string; + /** + * Format: binary + * @description The app export to import. + */ + appExport: string; + }; + }; + }; + }; + /** This endpoint is only available on a business or enterprise license. */ + appExport: { + parameters: { + path: { + /** The ID of the app which this request is targeting. */ + appId: components["parameters"]["appIdUrl"]; + }; + }; + responses: { + /** A gzip tarball containing the app export, encrypted if password provided. */ + 200: { + content: { + "application/gzip": string; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["appExport"]; + }; + }; + }; /** Based on application properties (currently only name) search for applications. */ appSearch: { responses: { diff --git a/packages/server/src/sdk/app/applications/import.ts b/packages/server/src/sdk/app/applications/import.ts new file mode 100644 index 0000000000..a7788924d8 --- /dev/null +++ b/packages/server/src/sdk/app/applications/import.ts @@ -0,0 +1,102 @@ +import { db as dbCore } from "@budibase/backend-core" +import { + DocumentTypesToImport, + Document, + Database, + RowValue, +} from "@budibase/types" +import backups from "../backups" + +export type FileAttributes = { + type: string + path: string +} + +function mergeUpdateAndDeleteDocuments( + updateDocs: Document[], + deleteDocs: Document[] +) { + // compress the documents to create and to delete (if same ID, then just update the rev) + const finalToDelete = [] + for (let deleteDoc of deleteDocs) { + const found = updateDocs.find(doc => doc._id === deleteDoc._id) + if (found) { + found._rev = deleteDoc._rev + } else { + finalToDelete.push(deleteDoc) + } + } + return [...updateDocs, ...finalToDelete] +} + +async function removeImportableDocuments(db: Database) { + // get the references to the documents, not the whole document + const docPromises = [] + for (let docType of DocumentTypesToImport) { + docPromises.push(db.allDocs(dbCore.getDocParams(docType))) + } + let documentRefs: { _id: string; _rev: string }[] = [] + for (let response of await Promise.all(docPromises)) { + documentRefs = documentRefs.concat( + response.rows.map(row => ({ + _id: row.id, + _rev: (row.value as RowValue).rev, + })) + ) + } + // add deletion key + return documentRefs.map(ref => ({ _deleted: true, ...ref })) +} + +async function getImportableDocuments(db: Database) { + // get the whole document + const docPromises = [] + for (let docType of DocumentTypesToImport) { + docPromises.push( + db.allDocs(dbCore.getDocParams(docType, null, { include_docs: true })) + ) + } + // map the responses to the document itself + let documents: Document[] = [] + for (let response of await Promise.all(docPromises)) { + documents = documents.concat(response.rows.map(row => row.doc)) + } + // remove the _rev, stops it being written + documents.forEach(doc => { + delete doc._rev + }) + return documents +} + +export async function updateWithExport( + appId: string, + file: FileAttributes, + password?: string +) { + const devId = dbCore.getDevAppID(appId) + const tempAppName = `temp_${devId}` + const tempDb = dbCore.getDB(tempAppName) + const appDb = dbCore.getDB(devId) + try { + const template = { + file: { + type: file.type!, + path: file.path!, + password, + }, + } + // get a temporary version of the import + // don't need obj store, the existing app already has everything we need + await backups.importApp(devId, tempDb, template, { + importObjStoreContents: false, + }) + // get the documents to copy + const toUpdate = await getImportableDocuments(tempDb) + // clear out the old documents + const toDelete = await removeImportableDocuments(appDb) + // now bulk update documents - add new ones, delete old ones and update common ones + await appDb.bulkDocs(mergeUpdateAndDeleteDocuments(toUpdate, toDelete)) + } finally { + await tempDb.destroy() + } +} diff --git a/packages/server/src/sdk/app/applications/index.ts b/packages/server/src/sdk/app/applications/index.ts index 963d065ce2..04ed3b2919 100644 --- a/packages/server/src/sdk/app/applications/index.ts +++ b/packages/server/src/sdk/app/applications/index.ts @@ -1,9 +1,11 @@ import * as sync from "./sync" import * as utils from "./utils" import * as applications from "./applications" +import * as imports from "./import" export default { ...sync, ...utils, ...applications, + ...imports, } diff --git a/packages/server/src/sdk/app/backups/exports.ts b/packages/server/src/sdk/app/backups/exports.ts index 307cdf4015..fe875f0c3d 100644 --- a/packages/server/src/sdk/app/backups/exports.ts +++ b/packages/server/src/sdk/app/backups/exports.ts @@ -8,11 +8,7 @@ import { TABLE_ROW_PREFIX, USER_METDATA_PREFIX, } from "../../../db/utils" -import { - DB_EXPORT_FILE, - GLOBAL_DB_EXPORT_FILE, - STATIC_APP_FILES, -} from "./constants" +import { DB_EXPORT_FILE, STATIC_APP_FILES } from "./constants" import fs from "fs" import { join } from "path" import env from "../../../environment" diff --git a/packages/server/src/sdk/app/backups/imports.ts b/packages/server/src/sdk/app/backups/imports.ts index 619f888329..c8e54e9e1d 100644 --- a/packages/server/src/sdk/app/backups/imports.ts +++ b/packages/server/src/sdk/app/backups/imports.ts @@ -151,7 +151,8 @@ export function getListOfAppsInMulti(tmpPath: string) { export async function importApp( appId: string, db: Database, - template: TemplateType + template: TemplateType, + opts: { importObjStoreContents: boolean } = { importObjStoreContents: true } ) { let prodAppId = dbCore.getProdAppID(appId) let dbStream: any @@ -165,7 +166,7 @@ export async function importApp( } const contents = fs.readdirSync(tmpPath) // have to handle object import - if (contents.length) { + if (contents.length && opts.importObjStoreContents) { let promises = [] let excludedFiles = [GLOBAL_DB_EXPORT_FILE, DB_EXPORT_FILE] for (let filename of contents) { diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index da7af8acd7..799e6f34e9 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -53,7 +53,6 @@ import { View, FieldType, RelationshipType, - ViewV2, CreateViewRequest, } from "@budibase/types" diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index 31c74a0e78..889133b847 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -5,6 +5,7 @@ import { TableAPI } from "./table" import { ViewV2API } from "./viewV2" import { DatasourceAPI } from "./datasource" import { LegacyViewAPI } from "./legacyView" +import { ScreenAPI } from "./screen" export default class API { table: TableAPI @@ -13,6 +14,7 @@ export default class API { row: RowAPI permission: PermissionAPI datasource: DatasourceAPI + screen: ScreenAPI constructor(config: TestConfiguration) { this.table = new TableAPI(config) @@ -21,5 +23,6 @@ export default class API { this.row = new RowAPI(config) this.permission = new PermissionAPI(config) this.datasource = new DatasourceAPI(config) + this.screen = new ScreenAPI(config) } } diff --git a/packages/server/src/tests/utilities/api/screen.ts b/packages/server/src/tests/utilities/api/screen.ts new file mode 100644 index 0000000000..9245ffe4ba --- /dev/null +++ b/packages/server/src/tests/utilities/api/screen.ts @@ -0,0 +1,18 @@ +import TestConfiguration from "../TestConfiguration" +import { Screen } from "@budibase/types" +import { TestAPI } from "./base" + +export class ScreenAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + list = async (): Promise => { + const res = await this.request + .get(`/api/screens`) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + return res.body as Screen[] + } +} diff --git a/packages/shared-core/package.json b/packages/shared-core/package.json index 160a42f086..9d03df21e0 100644 --- a/packages/shared-core/package.json +++ b/packages/shared-core/package.json @@ -19,7 +19,7 @@ "devDependencies": { "concurrently": "^7.6.0", "rimraf": "3.0.2", - "typescript": "4.7.3" + "typescript": "5.2.2" }, "nx": { "targets": { @@ -43,7 +43,6 @@ } ] } - } } } diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index aee60f4732..317b0fc3af 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -45,6 +45,6 @@ "rollup-plugin-node-globals": "^1.4.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-terser": "^7.0.2", - "typescript": "4.7.3" + "typescript": "5.2.2" } } diff --git a/packages/types/package.json b/packages/types/package.json index df49ec3cea..ad190bb1a6 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -24,7 +24,7 @@ "concurrently": "^7.6.0", "koa-body": "4.2.0", "rimraf": "3.0.2", - "typescript": "4.7.3" + "typescript": "5.2.2" }, "dependencies": { "scim-patch": "^0.7.0" diff --git a/packages/types/src/documents/document.ts b/packages/types/src/documents/document.ts index 03e01907b8..763da62d61 100644 --- a/packages/types/src/documents/document.ts +++ b/packages/types/src/documents/document.ts @@ -39,6 +39,25 @@ export enum DocumentType { AUDIT_LOG = "al", } +// these are the core documents that make up the data, design +// and automation sections of an app. This excludes any internal +// rows as we shouldn't import data. +export const DocumentTypesToImport: DocumentType[] = [ + DocumentType.ROLE, + DocumentType.DATASOURCE, + DocumentType.DATASOURCE_PLUS, + DocumentType.TABLE, + DocumentType.AUTOMATION, + DocumentType.WEBHOOK, + DocumentType.SCREEN, + DocumentType.QUERY, + DocumentType.METADATA, + DocumentType.MEM_VIEW, + // Deprecated but still copied + DocumentType.INSTANCE, + DocumentType.LAYOUT, +] + // these documents don't really exist, they are part of other // documents or enriched into existence as part of get requests export enum VirtualDocumentType { diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index bd3a6583bf..732a4a6c77 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -11,7 +11,7 @@ export enum Feature { SYNC_AUTOMATIONS = "syncAutomations", APP_BUILDERS = "appBuilders", OFFLINE = "offline", - USER_ROLE_PUBLIC_API = "userRolePublicApi", + EXPANDED_PUBLIC_API = "expandedPublicApi", VIEW_PERMISSIONS = "viewPermissions", } diff --git a/packages/worker/package.json b/packages/worker/package.json index a1344d89e3..4d1f1581b8 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -88,7 +88,6 @@ "@types/server-destroy": "1.0.1", "@types/supertest": "2.0.12", "@types/uuid": "8.3.4", - "@typescript-eslint/parser": "5.45.0", "copyfiles": "2.4.1", "eslint": "6.8.0", "jest": "29.6.2", @@ -100,7 +99,7 @@ "timekeeper": "2.2.0", "ts-node": "10.8.1", "tsconfig-paths": "4.0.0", - "typescript": "4.7.3", + "typescript": "5.2.2", "update-dotenv": "1.1.1" }, "nx": { diff --git a/qa-core/package.json b/qa-core/package.json index 4954991f67..3c789d89e6 100644 --- a/qa-core/package.json +++ b/qa-core/package.json @@ -38,7 +38,7 @@ "ts-jest": "29.1.1", "ts-node": "10.8.1", "tsconfig-paths": "4.0.0", - "typescript": "4.7.3" + "typescript": "5.2.2" }, "dependencies": { "@budibase/backend-core": "^2.3.17", diff --git a/yarn.lock b/yarn.lock index d777dca0bd..34441ee45f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6696,51 +6696,52 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/parser@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e" - integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ== +"@typescript-eslint/parser@6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.2.tgz#e0ae93771441b9518e67d0660c79e3a105497af4" + integrity sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw== dependencies: - "@typescript-eslint/scope-manager" "5.45.0" - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/typescript-estree" "5.45.0" + "@typescript-eslint/scope-manager" "6.7.2" + "@typescript-eslint/types" "6.7.2" + "@typescript-eslint/typescript-estree" "6.7.2" + "@typescript-eslint/visitor-keys" "6.7.2" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96" - integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw== +"@typescript-eslint/scope-manager@6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.2.tgz#cf59a2095d2f894770c94be489648ad1c78dc689" + integrity sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw== dependencies: - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/types" "6.7.2" + "@typescript-eslint/visitor-keys" "6.7.2" "@typescript-eslint/types@4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== -"@typescript-eslint/types@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" - integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== - "@typescript-eslint/types@5.53.0": version "5.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== -"@typescript-eslint/typescript-estree@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" - integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ== +"@typescript-eslint/types@6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.2.tgz#75a615a6dbeca09cafd102fe7f465da1d8a3c066" + integrity sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg== + +"@typescript-eslint/typescript-estree@6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.2.tgz#ce5883c23b581a5caf878af641e49dd0349238c7" + integrity sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ== dependencies: - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/types" "6.7.2" + "@typescript-eslint/visitor-keys" "6.7.2" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" "@typescript-eslint/typescript-estree@^4.33.0": version "4.33.0" @@ -6776,14 +6777,6 @@ "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" - integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg== - dependencies: - "@typescript-eslint/types" "5.45.0" - eslint-visitor-keys "^3.3.0" - "@typescript-eslint/visitor-keys@5.53.0": version "5.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" @@ -6792,6 +6785,14 @@ "@typescript-eslint/types" "5.53.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.2.tgz#4cb2bd786f1f459731b0ad1584c9f73e1c7a4d5c" + integrity sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ== + dependencies: + "@typescript-eslint/types" "6.7.2" + eslint-visitor-keys "^3.4.1" + "@vitest/expect@0.29.8": version "0.29.8" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.29.8.tgz#6ecdd031b4ea8414717d10b65ccd800908384612" @@ -21927,6 +21928,13 @@ semver@^7.3.2, semver@^7.3.7, semver@^7.3.8: dependencies: lru-cache "^6.0.0" +semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + semver@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52" @@ -23882,6 +23890,11 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + ts-graphviz@^1.5.0: version "1.5.4" resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.5.4.tgz#61a3059afeac4f6d4be3c6729a4d88546ca9e095" @@ -24120,10 +24133,10 @@ typeof@^1.0.0: resolved "https://registry.yarnpkg.com/typeof/-/typeof-1.0.0.tgz#9c84403f2323ae5399167275497638ea1d2f2440" integrity sha512-Pze0mIxYXhaJdpw1ayMzOA7rtGr1OmsTY/Z+FWtRKIqXFz6aoDLjqdbWE/tcIBSC8nhnVXiRrEXujodR/xiFAA== -typescript@4.7.3: - version "4.7.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" - integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== +typescript@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== "typescript@>=3 < 6": version "5.0.4"