diff --git a/hosting/scripts/linux/install-docker.sh b/hosting/scripts/linux/install-docker.sh index a71809c31f..fe92736d91 100755 --- a/hosting/scripts/linux/install-docker.sh +++ b/hosting/scripts/linux/install-docker.sh @@ -1,6 +1,6 @@ #!/bin/bash echo "**** WARNING - not for production environments ****" -# warning this is a convience script, for production installations install docker +# warning this is a convenience script, for production installations install docker # properly for your environment! curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh diff --git a/lerna.json b/lerna.json index 78f7618651..0982cc8bb4 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.71", + "version": "0.9.74", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index fd1758232a..4e6c859a8a 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.71", + "version": "0.9.74", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/objectStore/index.js b/packages/auth/src/objectStore/index.js index 80875fdfee..81bdd06b62 100644 --- a/packages/auth/src/objectStore/index.js +++ b/packages/auth/src/objectStore/index.js @@ -22,11 +22,13 @@ const CONTENT_TYPE_MAP = { html: "text/html", css: "text/css", js: "application/javascript", + json: "application/json", } const STRING_CONTENT_TYPES = [ CONTENT_TYPE_MAP.html, CONTENT_TYPE_MAP.css, CONTENT_TYPE_MAP.js, + CONTENT_TYPE_MAP.json, ] // does normal sanitization and then swaps dev apps to apps diff --git a/packages/bbui/package.json b/packages/bbui/package.json index a4b6349128..5118c1b7cf 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "0.9.71", + "version": "0.9.74", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 8847163cb7..398672af10 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -31,7 +31,7 @@ return "" } - // Render the label if the selected option is found, otherwide raw value + // Render the label if the selected option is found, otherwise raw value const index = options.findIndex( (option, idx) => getOptionValue(option, idx) === value ) diff --git a/packages/builder/CONTRIBUTING.md b/packages/builder/CONTRIBUTING.md index 24bd682585..dbefb5a16b 100644 --- a/packages/builder/CONTRIBUTING.md +++ b/packages/builder/CONTRIBUTING.md @@ -9,7 +9,7 @@ Please read this if you are unfamiliar with it. * Please maintain the existing code style. -* Please try to keep your commits small and focussed. +* Please try to keep your commits small and focused. * If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js index 2c09c4b3c5..4ad8e5287d 100644 --- a/packages/builder/cypress/setup.js +++ b/packages/builder/cypress/setup.js @@ -34,7 +34,7 @@ async function run() { process.exit(-1) } - // dont make this a variable or top level require + // don't make this a variable or top level require // it will cause environment module to be loaded prematurely const server = require("../../server/dist/app") process.env.PORT = WORKER_PORT diff --git a/packages/builder/package.json b/packages/builder/package.json index cf856552ce..261db322d7 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.71", + "version": "0.9.74", "license": "AGPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.71", - "@budibase/client": "^0.9.71", + "@budibase/bbui": "^0.9.74", + "@budibase/client": "^0.9.74", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.71", + "@budibase/string-templates": "^0.9.74", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index ac837978a9..b9cd134067 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -32,6 +32,10 @@ const INITIAL_FRONTEND_STATE = { layouts: [], screens: [], components: [], + clientFeatures: { + spectrumThemes: false, + intelligentLoading: false, + }, currentFrontEndType: "none", selectedScreenId: "", selectedLayoutId: "", @@ -56,6 +60,10 @@ export const getFrontendStore = () => { ...state, libraries: application.componentLibraries, components, + clientFeatures: { + ...state.clientFeatures, + ...components.features, + }, name: application.name, description: application.description, appId: application.appId, @@ -67,6 +75,8 @@ export const getFrontendStore = () => { appInstance: application.instance, clientLibPath, previousTopNavPath: {}, + version: application.version, + revertableVersion: application.revertableVersion, })) await hostingStore.actions.fetch() diff --git a/packages/builder/src/components/deploy/VersionModal.svelte b/packages/builder/src/components/deploy/VersionModal.svelte new file mode 100644 index 0000000000..0fb061face --- /dev/null +++ b/packages/builder/src/components/deploy/VersionModal.svelte @@ -0,0 +1,120 @@ + + +
+ +
+ + +
+ {#if revertAvailable} + + {/if} +
+ {#if updateAvailable} + + This app is currently using version {$store.version}, but version + {clientPackage.version} is available. Updates can contain new features, + performance improvements and bug fixes. + + {:else} + + This app is currently using version {$store.version} which is the + latest version available. + + {/if} + {#if revertAvailable} + + You can revert this app to version + {$store.revertableVersion} + if you're experiencing issues with the current version. + + {/if} +
+
+ + diff --git a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte index 3e4789a31d..607061013d 100644 --- a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte @@ -74,14 +74,14 @@ // Initialise the app when mounted iframe.contentWindow.addEventListener( "ready", - () => refreshContent(strippedJson), - { once: true } - ) - - // Display the client app once the iframe is initialised - iframe.contentWindow.addEventListener( - "iframe-loaded", - () => (loading = false), + () => { + // Display preview immediately if the intelligent loading feature + // is not supported + if (!$store.clientFeatures.intelligentLoading) { + loading = false + } + refreshContent(strippedJson) + }, { once: true } ) @@ -106,11 +106,9 @@ idToDelete = data.id confirmDeleteDialog.show() } else if (type === "preview-loaded") { - // We can use this in future to delay displaying the preview - // until the client app has actually initialised. - // This makes a smoother loading experience, but is not backwards - // compatible with old client library versions. - // So do nothing with this for now. + // Wait for this event to show the client library if intelligent + // loading is supported + loading = false } else { console.warning(`Client sent unknown event type: ${type}`) } diff --git a/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte b/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte index df30a17640..24050e1088 100644 --- a/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte +++ b/packages/builder/src/components/design/NavigationPanel/ComponentDropdownMenu.svelte @@ -65,7 +65,7 @@ } -{#if definition.editable !== false} +{#if definition?.editable !== false}
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte index 1d2d772a50..712abd8a7c 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte @@ -48,7 +48,7 @@ Add your first filter column. {:else} Results are filtered to only those which match all of the following - constaints. + constraints. {/if} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/IconSelect/icons.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/IconSelect/icons.js index 8baf58c589..ca57f98180 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/IconSelect/icons.js +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/IconSelect/icons.js @@ -1153,10 +1153,10 @@ export default { space: ["空格"], "page-separator": ["insert", "分页符", "插入"], "code-view": ["代码视图"], - "double-quotes-l": ["left", "quotaion marks", "双引号"], - "double-quotes-r": ["right", "quotaion marks", "双引号"], - "single-quotes-l": ["left", "quotaion marks", "单引号"], - "single-quotes-r": ["right", "quotaion marks", "单引号"], + "double-quotes-l": ["left", "quotation marks", "双引号"], + "double-quotes-r": ["right", "quotation marks", "双引号"], + "single-quotes-l": ["left", "quotation marks", "单引号"], + "single-quotes-r": ["right", "quotation marks", "单引号"], "table-2": ["表格"], subscript: ["角标", "下标", "脚注"], "subscript-2": ["角标", "下标", "脚注"], @@ -1463,7 +1463,7 @@ export default { alipay: ["zhifubao", "支付宝"], amazon: ["亚马逊"], android: ["applications", "安卓", "应用"], - angularjs: ["angular", "programing framework"], + angularjs: ["angular", "programming framework"], "app-store": ["applications", "苹果应用商店"], apple: ["苹果"], baidu: ["du", "claw", "百度", "爪"], @@ -1519,7 +1519,7 @@ export default { playstation: ["ps"], "product-hunt": ["product hunt"], qq: ["penguin", "tencent", "腾讯", "企鹅"], - reactjs: ["react", "programing framework", "facebook"], + reactjs: ["react", "programming framework", "facebook"], reddit: ["reddit"], remixicon: ["remix icon", "图标"], safari: ["safari浏览器"], @@ -1543,7 +1543,7 @@ export default { unsplash: ["photos"], vimeo: ["视频"], visa: ["bank card", "银行卡"], - vuejs: ["vue", "programing framework"], + vuejs: ["vue", "programming framework"], wechat: ["微信"], "wechat-2": ["微信"], "wechat-pay": ["微信支付"], diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index eab765b502..f3ae6e7c8e 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -4,6 +4,7 @@ import { Icon, ActionGroup, Tabs, Tab } from "@budibase/bbui" import DeployModal from "components/deploy/DeployModal.svelte" import RevertModal from "components/deploy/RevertModal.svelte" + import VersionModal from "components/deploy/VersionModal.svelte" import { get } from "builderStore/api" import { isActive, goto, layout } from "@roxi/routify" import Logo from "assets/bb-emblem.svg" @@ -80,6 +81,7 @@
+ - + {#if $store.clientFeatures.spectrumThemes} + + {/if}
- + {#key $store.version} + + {/key}
{/if} diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte index 8ecb274585..9c7b9aa0ea 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte @@ -20,7 +20,7 @@ if (res.status) { notifications.error(res.message) } else { - notifications.success("Succesfully created user") + notifications.success("Successfully created user") } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 2573838703..b616255130 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "0.9.71", + "version": "0.9.74", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index c2d680d31b..c9f3935569 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "0.9.71", + "version": "0.9.74", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -18,9 +18,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^0.9.71", - "@budibase/standard-components": "^0.9.71", - "@budibase/string-templates": "^0.9.71", + "@budibase/bbui": "^0.9.74", + "@budibase/standard-components": "^0.9.74", + "@budibase/string-templates": "^0.9.74", "regexparam": "^1.3.0", "shortid": "^2.2.15", "svelte-spa-router": "^3.0.5" diff --git a/packages/server/.gitignore b/packages/server/.gitignore index c737d00466..e8589f631d 100644 --- a/packages/server/.gitignore +++ b/packages/server/.gitignore @@ -2,6 +2,7 @@ node_modules/ myapps/ .env builder/* +client/* public/ db/dev.db/ dist diff --git a/packages/server/package.json b/packages/server/package.json index 26ba52e656..88961336cf 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "0.9.71", + "version": "0.9.74", "description": "Budibase Web Server", "main": "src/index.js", "repository": { @@ -13,7 +13,8 @@ "postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/", "test": "jest --coverage --maxWorkers=2", "test:watch": "jest --watch", - "build:docker": "docker build . -t app-service", + "predocker": "copyfiles -f ../client/dist/budibase-client.js ../standard-components/manifest.json client", + "build:docker": "yarn run predocker && docker build . -t app-service", "run:docker": "node dist/index.js", "dev:stack:up": "node scripts/dev/manage.js up", "dev:stack:down": "node scripts/dev/manage.js down", @@ -59,9 +60,9 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.71", - "@budibase/client": "^0.9.71", - "@budibase/string-templates": "^0.9.71", + "@budibase/auth": "^0.9.74", + "@budibase/client": "^0.9.74", + "@budibase/string-templates": "^0.9.74", "@elastic/elasticsearch": "7.10.0", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", @@ -114,7 +115,7 @@ "devDependencies": { "@babel/core": "^7.14.3", "@babel/preset-env": "^7.14.4", - "@budibase/standard-components": "^0.9.71", + "@budibase/standard-components": "^0.9.74", "@jest/test-sequencer": "^24.8.0", "@types/bull": "^3.15.1", "@types/jest": "^26.0.23", diff --git a/packages/server/scripts/integrations/mysql/init.sql b/packages/server/scripts/integrations/mysql/init.sql index 8cd120a8a3..4dd75c36d3 100644 --- a/packages/server/scripts/integrations/mysql/init.sql +++ b/packages/server/scripts/integrations/mysql/init.sql @@ -2,6 +2,8 @@ CREATE DATABASE IF NOT EXISTS main; USE main; CREATE TABLE Persons ( PersonID int NOT NULL AUTO_INCREMENT, + CreatedAt datetime, + Age float, LastName varchar(255), FirstName varchar(255), Address varchar(255), @@ -17,6 +19,6 @@ CREATE TABLE Tasks ( FOREIGN KEY(PersonID) REFERENCES Persons(PersonID) ); -INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast'); +INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Mike', 'Hughes', 28.2, '123 Fake Street', 'Belfast', '2021-01-19 03:14:07'); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling'); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing'); diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 9881a07f43..a2e254461a 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -33,6 +33,11 @@ const { } = require("../../utilities/workerRequests") const { clientLibraryPath } = require("../../utilities") const { getAllLocks } = require("../../utilities/redis") +const { + updateClientLibrary, + backupClientLibrary, + revertClientLibrary, +} = require("../../utilities/fileSystem/clientLibrary") const URL_REGEX_SLASH = /\/|\\/g @@ -231,27 +236,54 @@ exports.create = async function (ctx) { } exports.update = async function (ctx) { - const url = await getAppUrlIfNotInUse(ctx) + const data = await updateAppPackage(ctx, ctx.request.body, ctx.params.appId) + ctx.status = 200 + ctx.body = data +} + +exports.updateClient = async function (ctx) { + // Get current app version const db = new CouchDB(ctx.params.appId) const application = await db.get(DocumentTypes.APP_METADATA) + const currentVersion = application.version - const data = ctx.request.body - const newData = { ...application, ...data, url } - if (ctx.request.body._rev !== application._rev) { - newData._rev = application._rev + // Update client library and manifest + if (!env.isTest()) { + await backupClientLibrary(ctx.params.appId) + await updateClientLibrary(ctx.params.appId) } - // the locked by property is attached by server but generated from - // Redis, shouldn't ever store it - if (newData.lockedBy) { - delete newData.lockedBy + // Update versions in app package + const appPackageUpdates = { + version: packageJson.version, + revertableVersion: currentVersion, } - - const response = await db.put(newData) - data._rev = response.rev - + const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId) ctx.status = 200 - ctx.body = response + ctx.body = data +} + +exports.revertClient = async function (ctx) { + // Check app can be reverted + const db = new CouchDB(ctx.params.appId) + const application = await db.get(DocumentTypes.APP_METADATA) + if (!application.revertableVersion) { + ctx.throw(400, "There is no version to revert to") + } + + // Update client library and manifest + if (!env.isTest()) { + await revertClientLibrary(ctx.params.appId) + } + + // Update versions in app package + const appPackageUpdates = { + version: application.revertableVersion, + revertableVersion: null, + } + const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId) + ctx.status = 200 + ctx.body = data } exports.delete = async function (ctx) { @@ -269,6 +301,23 @@ exports.delete = async function (ctx) { ctx.body = result } +const updateAppPackage = async (ctx, appPackage, appId) => { + const url = await getAppUrlIfNotInUse(ctx) + const db = new CouchDB(appId) + const application = await db.get(DocumentTypes.APP_METADATA) + + const newAppPackage = { ...application, ...appPackage, url } + if (appPackage._rev !== application._rev) { + newAppPackage._rev = application._rev + } + + // the locked by property is attached by server but generated from + // Redis, shouldn't ever store it + delete newAppPackage.lockedBy + + return await db.put(newAppPackage) +} + const createEmptyAppPackage = async (ctx, app) => { const db = new CouchDB(app.appId) diff --git a/packages/server/src/api/controllers/component.js b/packages/server/src/api/controllers/component.js index c678d8e587..06cb2cd211 100644 --- a/packages/server/src/api/controllers/component.js +++ b/packages/server/src/api/controllers/component.js @@ -20,10 +20,14 @@ exports.fetchAppComponentDefinitions = async function (ctx) { const definitions = {} for (let { manifest, library } of componentManifests) { for (let key of Object.keys(manifest)) { - const fullComponentName = `${library}/${key}`.toLowerCase() - definitions[fullComponentName] = { - component: fullComponentName, - ...manifest[key], + if (key === "features") { + definitions[key] = manifest[key] + } else { + const fullComponentName = `${library}/${key}`.toLowerCase() + definitions[fullComponentName] = { + component: fullComponentName, + ...manifest[key], + } } } } diff --git a/packages/server/src/api/controllers/view/viewBuilder.js b/packages/server/src/api/controllers/view/viewBuilder.js index ed1199fa5b..068c6ab7b1 100644 --- a/packages/server/src/api/controllers/view/viewBuilder.js +++ b/packages/server/src/api/controllers/view/viewBuilder.js @@ -108,7 +108,7 @@ function parseEmitExpression(field, groupBy) { * calculation: an optional calculation to be performed over the view data. */ function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) { - // first filter can't have a conjuction + // first filter can't have a conjunction if (filters && filters.length > 0 && filters[0].conjunction) { delete filters[0].conjunction } diff --git a/packages/server/src/api/routes/application.js b/packages/server/src/api/routes/application.js index a7209df3e9..c2eb19e101 100644 --- a/packages/server/src/api/routes/application.js +++ b/packages/server/src/api/routes/application.js @@ -11,6 +11,16 @@ router .get("/api/applications/:appId/appPackage", controller.fetchAppPackage) .put("/api/applications/:appId", authorized(BUILDER), controller.update) .post("/api/applications", authorized(BUILDER), controller.create) + .post( + "/api/applications/:appId/client/update", + authorized(BUILDER), + controller.updateClient + ) + .post( + "/api/applications/:appId/client/revert", + authorized(BUILDER), + controller.revertClient + ) .delete("/api/applications/:appId", authorized(BUILDER), controller.delete) module.exports = router diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js index 2333787e6e..05e0bc231b 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.js @@ -94,7 +94,7 @@ describe("/applications", () => { }) describe("update", () => { - it("should be able to fetch the app package", async () => { + it("should be able to update the app package", async () => { const res = await request .put(`/api/applications/${config.getAppId()}`) .send({ @@ -107,6 +107,30 @@ describe("/applications", () => { }) }) + describe("manage client library version", () => { + it("should be able to update the app client library version", async () => { + console.log(config.getAppId()) + await request + .post(`/api/applications/${config.getAppId()}/client/update`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + }) + it("should be able to revert the app client library version", async () => { + // We need to first update the version so that we can then revert + await request + .post(`/api/applications/${config.getAppId()}/client/update`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + await request + .post(`/api/applications/${config.getAppId()}/client/revert`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + }) + }) + describe("edited at", () => { it("middleware should set edited at", async () => { const headers = config.defaultHeaders() diff --git a/packages/server/src/definitions/datasource.ts b/packages/server/src/definitions/datasource.ts index 22f1998601..90270dd113 100644 --- a/packages/server/src/definitions/datasource.ts +++ b/packages/server/src/definitions/datasource.ts @@ -129,9 +129,11 @@ export interface QueryJson { export interface SqlQuery { sql: string - bindings?: { - [key: string]: any - } + bindings?: + | string[] + | { + [key: string]: any + } } export interface QueryOptions { diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index e1c065dd26..66dd269412 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -8,9 +8,24 @@ import { Operation, RelationshipsJson, } from "../../definitions/datasource" +import { isIsoDateString } from "../utils" type KnexQuery = Knex.QueryBuilder | Knex +function parseBody(body: any) { + for (let [key, value] of Object.entries(body)) { + if (typeof value !== "string") { + continue + } + if (isIsoDateString(value)) { + body[key] = new Date(value) + } else if (!isNaN(parseFloat(value))) { + body[key] = parseFloat(value) + } + } + return body +} + // right now we only do filters on the specific table being queried function addFilters( tableName: string, @@ -119,11 +134,12 @@ function buildCreate( ): KnexQuery { const { endpoint, body } = json let query: KnexQuery = knex(endpoint.entityId) + const parsedBody = parseBody(body) // mysql can't use returning if (opts.disableReturning) { - return query.insert(body) + return query.insert(parsedBody) } else { - return query.insert(body).returning("*") + return query.insert(parsedBody).returning("*") } } @@ -173,12 +189,13 @@ function buildUpdate( ): KnexQuery { const { endpoint, body, filters } = json let query: KnexQuery = knex(endpoint.entityId) + const parsedBody = parseBody(body) query = addFilters(endpoint.entityId, query, filters) // mysql can't use returning if (opts.disableReturning) { - return query.update(body) + return query.update(parsedBody) } else { - return query.update(body).returning("*") + return query.update(parsedBody).returning("*") } } diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts index 9567fa7054..fab151fc0d 100644 --- a/packages/server/src/integrations/mysql.ts +++ b/packages/server/src/integrations/mysql.ts @@ -29,6 +29,7 @@ module MySQLModule { blob: FieldTypes.LONGFORM, enum: FieldTypes.STRING, varchar: FieldTypes.STRING, + float: FieldTypes.NUMBER, int: FieldTypes.NUMBER, numeric: FieldTypes.NUMBER, bigint: FieldTypes.NUMBER, diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts index c114acf6b5..85d1b44b26 100644 --- a/packages/server/src/integrations/postgres.ts +++ b/packages/server/src/integrations/postgres.ts @@ -92,7 +92,7 @@ module PostgresModule { async function internalQuery(client: any, query: SqlQuery) { try { - return await client.query(query.sql, query.bindings || {}) + return await client.query(query.sql, query.bindings || []) } catch (err) { throw new Error(err) } diff --git a/packages/server/src/integrations/tests/postgres.spec.js b/packages/server/src/integrations/tests/postgres.spec.js index f973f69101..4ce5f12e96 100644 --- a/packages/server/src/integrations/tests/postgres.spec.js +++ b/packages/server/src/integrations/tests/postgres.spec.js @@ -20,7 +20,7 @@ describe("Postgres Integration", () => { await config.integration.create({ sql }) - expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) + expect(pg.queryMock).toHaveBeenCalledWith(sql, []) }) it("calls the read method with the correct params", async () => { @@ -28,7 +28,7 @@ describe("Postgres Integration", () => { await config.integration.read({ sql }) - expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) + expect(pg.queryMock).toHaveBeenCalledWith(sql, []) }) it("calls the update method with the correct params", async () => { @@ -36,7 +36,7 @@ describe("Postgres Integration", () => { const response = await config.integration.update({ sql }) - expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) + expect(pg.queryMock).toHaveBeenCalledWith(sql, []) }) it("calls the delete method with the correct params", async () => { @@ -44,7 +44,7 @@ describe("Postgres Integration", () => { await config.integration.delete({ sql }) - expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) + expect(pg.queryMock).toHaveBeenCalledWith(sql, []) }) describe("no rows returned", () => { diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index d0af0e99a9..03751bb467 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -68,3 +68,11 @@ export function isSQL(datasource: Datasource): boolean { const SQL = [SourceNames.POSTGRES, SourceNames.SQL_SERVER, SourceNames.MYSQL] return SQL.indexOf(datasource.source) !== -1 } + +export function isIsoDateString(str: string) { + if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str)) { + return false + } + let d = new Date(str) + return d.toISOString() === str +} diff --git a/packages/server/src/utilities/fileSystem/clientLibrary.js b/packages/server/src/utilities/fileSystem/clientLibrary.js new file mode 100644 index 0000000000..3f0d31f257 --- /dev/null +++ b/packages/server/src/utilities/fileSystem/clientLibrary.js @@ -0,0 +1,154 @@ +const { join } = require("path") +const { ObjectStoreBuckets } = require("../../constants") +const fs = require("fs") +const { upload, retrieveToTmp, streamUpload } = require("./utilities") +const { resolve } = require("../centralPath") +const env = require("../../environment") +const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..") + +/** + * Client library paths in the object store: + * Previously, the entire standard-components package was downloaded from NPM + * as a tarball and extracted to the object store, even though only the manifest + * was ever needed. Therefore we need to support old apps which may still have + * the manifest at this location for the first update. + * + * The new paths for the in-use version are: + * {appId}/manifest.json + * {appId}/budibase-client.js + * + * The paths for the backups are: + * {appId}/manifest.json.bak + * {appId}/budibase-client.js.bak + * + * We don't rely on NPM at all any more, as when updating to the latest version + * we pull both the manifest and client bundle from the server's dependencies + * in the local file system. + */ + +/** + * Backs up the current client library version by copying both the manifest + * and client bundle to .bak extensions in the object store. Only the one + * previous version is stored as a backup, which can be reverted to. + * @param appId The app ID to backup + * @returns {Promise} + */ +exports.backupClientLibrary = async appId => { + // Copy existing manifest to tmp + let tmpManifestPath + try { + // Try to load the manifest from the new file location + tmpManifestPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "manifest.json") + ) + } catch (error) { + // Fallback to loading it from the old location for old apps + tmpManifestPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join( + appId, + "node_modules", + "budibase", + "standard-components", + "package", + "manifest.json" + ) + ) + } + + // Copy existing client lib to tmp + const tmpClientPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "budibase-client.js") + ) + + // Upload manifest and client library as backups + const manifestUpload = upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "manifest.json.bak"), + path: tmpManifestPath, + type: "application/json", + }) + const clientUpload = upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "budibase-client.js.bak"), + path: tmpClientPath, + type: "application/javascript", + }) + await Promise.all([manifestUpload, clientUpload]) +} + +/** + * Uploads the latest version of the component manifest and the client library + * to the object store, overwriting the existing version. + * @param appId The app ID to update + * @returns {Promise} + */ +exports.updateClientLibrary = async appId => { + let manifest, client + + if (env.isDev()) { + // Load the symlinked version in dev which is always the newest + manifest = require.resolve("@budibase/standard-components/manifest.json") + client = require.resolve("@budibase/client") + } else { + // Load the bundled version in prod + manifest = resolve(TOP_LEVEL_PATH, "client", "manifest.json") + client = resolve(TOP_LEVEL_PATH, "client", "budibase-client.js") + } + + // Upload latest manifest and client library + const manifestUpload = streamUpload( + ObjectStoreBuckets.APPS, + join(appId, "manifest.json"), + fs.createReadStream(manifest), + { + ContentType: "application/json", + } + ) + const clientUpload = streamUpload( + ObjectStoreBuckets.APPS, + join(appId, "budibase-client.js"), + fs.createReadStream(client), + { + ContentType: "application/javascript", + } + ) + await Promise.all([manifestUpload, clientUpload]) +} + +/** + * Reverts the version of the client library and manifest to the previously + * used version for an app. + * @param appId The app ID to revert + * @returns {Promise} + */ +exports.revertClientLibrary = async appId => { + // Copy backups manifest to tmp directory + const tmpManifestPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "manifest.json.bak") + ) + + // Copy backup client lib to tmp + const tmpClientPath = await retrieveToTmp( + ObjectStoreBuckets.APPS, + join(appId, "budibase-client.js.bak") + ) + + // Upload backups as new versions + const manifestUpload = upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "manifest.json"), + path: tmpManifestPath, + type: "application/json", + }) + const clientUpload = upload({ + bucket: ObjectStoreBuckets.APPS, + filename: join(appId, "budibase-client.js"), + path: tmpClientPath, + type: "application/javascript", + }) + await Promise.all([manifestUpload, clientUpload]) +} diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index afacbf8cdf..b83ff03854 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -13,7 +13,7 @@ const { deleteFolder, downloadTarball, } = require("./utilities") -const { downloadLibraries, uploadClientLibrary } = require("./newApp") +const { updateClientLibrary } = require("./clientLibrary") const download = require("download") const env = require("../../environment") const { homedir } = require("os") @@ -139,13 +139,12 @@ exports.performBackup = async (appId, backupName) => { } /** - * Downloads required libraries and creates a new path in the object store. + * Uploads the latest client library to the object store. * @param {string} appId The ID of the app which is being created. * @return {Promise} once promise completes app resources should be ready in object store. */ exports.createApp = async appId => { - await downloadLibraries(appId) - await uploadClientLibrary(appId) + await updateClientLibrary(appId) } /** @@ -193,8 +192,17 @@ exports.getComponentLibraryManifest = async (appId, library) => { delete require.cache[require.resolve(path)] return require(path) } - const path = join(appId, "node_modules", library, "package", filename) - let resp = await retrieve(ObjectStoreBuckets.APPS, path) + + let resp + try { + // Try to load the manifest from the new file location + const path = join(appId, filename) + resp = await retrieve(ObjectStoreBuckets.APPS, path) + } catch (error) { + // Fallback to loading it from the old location for old apps + const path = join(appId, "node_modules", library, "package", filename) + resp = await retrieve(ObjectStoreBuckets.APPS, path) + } if (typeof resp !== "string") { resp = resp.toString("utf8") } diff --git a/packages/server/src/utilities/fileSystem/newApp.js b/packages/server/src/utilities/fileSystem/newApp.js deleted file mode 100644 index 735f0d523e..0000000000 --- a/packages/server/src/utilities/fileSystem/newApp.js +++ /dev/null @@ -1,36 +0,0 @@ -const packageJson = require("../../../package.json") -const { join } = require("path") -const { ObjectStoreBuckets } = require("../../constants") -const { streamUpload, downloadTarball } = require("./utilities") -const fs = require("fs") - -const BUCKET_NAME = ObjectStoreBuckets.APPS - -// can't really test this due to the downloading nature of it, wouldn't be a great test case -/* istanbul ignore next */ -exports.downloadLibraries = async appId => { - const LIBRARIES = ["standard-components"] - - const paths = {} - // Need to download tarballs directly from NPM as our users may not have node on their machine - for (let lib of LIBRARIES) { - // download tarball - const registryUrl = `https://registry.npmjs.org/@budibase/${lib}/-/${lib}-${packageJson.version}.tgz` - const path = join(appId, "node_modules", "@budibase", lib) - paths[`@budibase/${lib}`] = await downloadTarball( - registryUrl, - BUCKET_NAME, - path - ) - } - return paths -} - -exports.uploadClientLibrary = async appId => { - const sourcepath = require.resolve("@budibase/client") - const destPath = join(appId, "budibase-client.js") - - await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath), { - ContentType: "application/javascript", - }) -} diff --git a/packages/server/src/utilities/usageQuota.js b/packages/server/src/utilities/usageQuota.js index d042d290d5..bfe71a4093 100644 --- a/packages/server/src/utilities/usageQuota.js +++ b/packages/server/src/utilities/usageQuota.js @@ -85,7 +85,7 @@ exports.update = async (apiKey, property, usage) => { await apiKeyTable.put({ item: keyObj }) return } - // we have infact breached the reset period + // we have in fact breached the reset period else if (keyObj && keyObj.quotaReset <= Date.now()) { // update the quota reset period and reset the values for all properties keyObj.quotaReset = getNewQuotaReset() diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index be66a4ce87..2410ba56b5 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1,4 +1,8 @@ { + "features": { + "spectrumThemes": true, + "intelligentLoading": true + }, "layout": { "name": "Layout", "description": "This component is specific only to layouts", @@ -27,7 +31,7 @@ "type": "select", "label": "Width", "key": "width", - "options": ["Small", "Medium", "Large"], + "options": ["Small", "Medium", "Large", "Max"], "defaultValue": "Large" }, { diff --git a/packages/standard-components/package.json b/packages/standard-components/package.json index 0e9ab78696..a1e2a46260 100644 --- a/packages/standard-components/package.json +++ b/packages/standard-components/package.json @@ -29,12 +29,12 @@ "keywords": [ "svelte" ], - "version": "0.9.71", + "version": "0.9.74", "license": "MIT", "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc", "dependencies": { - "@budibase/bbui": "^0.9.71", "@spectrum-css/card": "^3.0.3", + "@budibase/bbui": "^0.9.74", "@spectrum-css/link": "^3.1.3", "@spectrum-css/page": "^3.0.1", "@spectrum-css/typography": "^3.0.2", diff --git a/packages/standard-components/src/Layout.svelte b/packages/standard-components/src/Layout.svelte index df0bd19525..24c9709895 100644 --- a/packages/standard-components/src/Layout.svelte +++ b/packages/standard-components/src/Layout.svelte @@ -20,6 +20,7 @@ None: "none", } const widthClasses = { + Max: "max", Large: "l", Medium: "m", Small: "s", @@ -178,6 +179,9 @@ position: relative; padding: 32px; } + .layout--none .main { + padding: 0; + } .size--s { width: 800px; } @@ -187,6 +191,9 @@ .size--l { width: 1400px; } + .size--max { + width: 100%; + } /* Nav components */ .burger { diff --git a/packages/standard-components/src/helpers.js b/packages/standard-components/src/helpers.js index 07a8081058..4a47d4e538 100644 --- a/packages/standard-components/src/helpers.js +++ b/packages/standard-components/src/helpers.js @@ -22,7 +22,7 @@ export const generateID = (size = 21) => { // It is incorrect to use bytes exceeding the alphabet size. // The following mask reduces the random byte in the 0-255 value // range to the 0-63 value range. Therefore, adding hacks, such - // as empty string fallback or magic numbers, is unneccessary because + // as empty string fallback or magic numbers, is unnecessary because // the bitmask trims bytes down to the alphabet size. let byte = bytes[size] & 63 if (byte < 36) { diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index a37f8fcc59..f75a11cf4b 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "0.9.71", + "version": "0.9.74", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/string-templates/src/processors/preprocessor.js b/packages/string-templates/src/processors/preprocessor.js index ee3a3a9730..6f6537674a 100644 --- a/packages/string-templates/src/processors/preprocessor.js +++ b/packages/string-templates/src/processors/preprocessor.js @@ -28,7 +28,7 @@ module.exports.processors = [ let startBraceIdx = statement.indexOf("[") let lastIdx = 0 while (startBraceIdx !== -1) { - // if the character previous to the literal specifier is alpha-numeric this should happen + // if the character previous to the literal specifier is alphanumeric this should happen if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) { statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[") } diff --git a/packages/worker/package.json b/packages/worker/package.json index dad5a2c112..134eb55593 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "0.9.71", + "version": "0.9.74", "description": "Budibase background service", "main": "src/index.js", "repository": { @@ -21,8 +21,8 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.71", - "@budibase/string-templates": "^0.9.71", + "@budibase/auth": "^0.9.74", + "@budibase/string-templates": "^0.9.74", "@koa/router": "^8.0.0", "aws-sdk": "^2.811.0", "bcryptjs": "^2.4.3", diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index 534456fe15..dc13bb948a 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -173,7 +173,7 @@ exports.sendEmail = async ( } /** - * Given an SMTP configuration this runs it through nodemailer to see if it is infact functional. + * Given an SMTP configuration this runs it through nodemailer to see if it is in fact functional. * @param {object} config an SMTP configuration - this is based on the nodemailer API. * @return {Promise} returns true if the configuration is valid. */