diff --git a/hosting/nginx.prod.conf.hbs b/hosting/nginx.prod.conf.hbs index f446c928fb..23ef1651d1 100644 --- a/hosting/nginx.prod.conf.hbs +++ b/hosting/nginx.prod.conf.hbs @@ -55,7 +55,7 @@ http { add_header X-Frame-Options SAMEORIGIN always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self'; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io ; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self' https:; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always; # upstreams set $apps {{ apps }}; diff --git a/lerna.json b/lerna.json index f7afef9948..2f375fcec0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.76-alpha.3", + "version": "1.0.80", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index bcacc06ab8..56e184989c 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "description": "Budibase backend core libraries used in server and worker", "main": "src/index.js", "author": "Budibase", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index eb1c3da6f5..b0dd85e75d 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": "1.0.76-alpha.3", + "version": "1.0.80", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/builder/package.json b/packages/builder/package.json index 3ca2a167b8..c4f4a74854 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "license": "GPL-3.0", "private": true, "scripts": { @@ -64,10 +64,10 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.76-alpha.3", - "@budibase/client": "^1.0.76-alpha.3", - "@budibase/frontend-core": "^1.0.76-alpha.3", - "@budibase/string-templates": "^1.0.76-alpha.3", + "@budibase/bbui": "^1.0.80", + "@budibase/client": "^1.0.80", + "@budibase/frontend-core": "^1.0.80", + "@budibase/string-templates": "^1.0.80", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte index 10c6703623..cf525f3ef3 100644 --- a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte @@ -57,7 +57,8 @@ {data} {loading} {type} - allowEditing={!view?.calculation} + allowEditing={false} + rowCount={10} bind:hideAutocolumns > diff --git a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte index 2a886fab0c..de50e0cfb2 100644 --- a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte @@ -134,8 +134,9 @@ // Remove all iframe event listeners on component destroy onDestroy(() => { + window.removeEventListener("message", receiveMessage) + if (iframe.contentWindow) { - window.removeEventListener("message", receiveMessage) if (!$store.clientFeatures.messagePassing) { // Legacy - remove in later versions of BB iframe.contentWindow.removeEventListener( diff --git a/packages/builder/src/stores/backend/queries.js b/packages/builder/src/stores/backend/queries.js index 6e30cb21f8..e5b79fb165 100644 --- a/packages/builder/src/stores/backend/queries.js +++ b/packages/builder/src/stores/backend/queries.js @@ -60,7 +60,7 @@ export function createQueriesStore() { }) return savedQuery }, - import: async (data, datasourceId) => { + import: async ({ data, datasourceId }) => { return await API.importQueries({ datasourceId, data, diff --git a/packages/cli/package.json b/packages/cli/package.json index 49b4376d7d..c5433bd30c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "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 1eed59e1f5..74b488c730 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,18 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^1.0.76-alpha.3", - "@budibase/frontend-core": "^1.0.76-alpha.3", - "@budibase/string-templates": "^1.0.76-alpha.3", + "@budibase/bbui": "^1.0.80", + "@budibase/frontend-core": "^1.0.80", + "@budibase/string-templates": "^1.0.80", + "regexparam": "^1.3.0", + "rollup-plugin-polyfill-node": "^0.8.0", + "shortid": "^2.2.15", + "svelte-spa-router": "^3.0.5" + }, + "devDependencies": { + "@rollup/plugin-alias": "^3.1.5", + "@rollup/plugin-commonjs": "^18.0.0", + "@rollup/plugin-node-resolve": "^11.2.1", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/client/src/components/app/forms/S3Upload.svelte b/packages/client/src/components/app/forms/S3Upload.svelte index ab66755b6e..031c49f4b4 100644 --- a/packages/client/src/components/app/forms/S3Upload.svelte +++ b/packages/client/src/components/app/forms/S3Upload.svelte @@ -81,7 +81,10 @@ loading = false return res } catch (error) { - notificationStore.actions.error(`Error uploading file: ${error}`) + notificationStore.actions.error( + `Error uploading file: ${error?.message || error}` + ) + loading = false } } diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 72c8f9c083..fec966725b 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -127,12 +127,16 @@ const queryExecutionHandler = async action => { // Trigger a notification and invalidate the datasource as long as this // was not a readable query if (!query.readable) { - API.notifications.error.success("Query executed successfully") + notificationStore.actions.success("Query executed successfully") await dataSourceStore.actions.invalidateDataSource(query.datasourceId) } return { result } } catch (error) { + notificationStore.actions.error( + "An error occurred while executing the query" + ) + // Abort next actions return false } diff --git a/packages/client/stats.html b/packages/client/stats.html new file mode 100644 index 0000000000..4cab61a146 --- /dev/null +++ b/packages/client/stats.html @@ -0,0 +1,2689 @@ + + + + + + + + RollUp Visualizer + + + +
+ + + + + diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 91ac12da2b..3cfc0837e0 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "^1.0.76-alpha.3", + "@budibase/bbui": "^1.0.80", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/api/attachments.js b/packages/frontend-core/src/api/attachments.js index 2077c4f7ef..e2eb348f9d 100644 --- a/packages/frontend-core/src/api/attachments.js +++ b/packages/frontend-core/src/api/attachments.js @@ -1,61 +1,66 @@ -export const buildAttachmentEndpoints = API => ({ - /** - * Uploads an attachment to the server. - * @param data the attachment to upload - * @param tableId the table ID to upload to - */ - uploadAttachment: async ({ data, tableId }) => { - return await API.post({ - url: `/api/attachments/${tableId}/upload`, - body: data, - json: false, - }) - }, - - /** - * Uploads an attachment to the server as a builder user from the builder. - * @param data the data to upload - */ - uploadBuilderAttachment: async data => { - return await API.post({ - url: "/api/attachments/process", - body: data, - json: false, - }) - }, - +export const buildAttachmentEndpoints = API => { /** * Generates a signed URL to upload a file to an external datasource. * @param datasourceId the ID of the datasource to upload to * @param bucket the name of the bucket to upload to * @param key the name of the file to upload to */ - getSignedDatasourceURL: async ({ datasourceId, bucket, key }) => { + const getSignedDatasourceURL = async ({ datasourceId, bucket, key }) => { return await API.post({ url: `/api/attachments/${datasourceId}/url`, body: { bucket, key }, }) - }, + } - /** - * Uploads a file to an external datasource. - * @param datasourceId the ID of the datasource to upload to - * @param bucket the name of the bucket to upload to - * @param key the name of the file to upload to - * @param data the file to upload - */ - externalUpload: async ({ datasourceId, bucket, key, data }) => { - const { signedUrl, publicUrl } = await API.getSignedDatasourceURL({ - datasourceId, - bucket, - key, - }) - await API.put({ - url: signedUrl, - body: data, - json: false, - external: true, - }) - return { publicUrl } - }, -}) + return { + getSignedDatasourceURL, + + /** + * Uploads an attachment to the server. + * @param data the attachment to upload + * @param tableId the table ID to upload to + */ + uploadAttachment: async ({ data, tableId }) => { + return await API.post({ + url: `/api/attachments/${tableId}/upload`, + body: data, + json: false, + }) + }, + + /** + * Uploads an attachment to the server as a builder user from the builder. + * @param data the data to upload + */ + uploadBuilderAttachment: async data => { + return await API.post({ + url: "/api/attachments/process", + body: data, + json: false, + }) + }, + + /** + * Uploads a file to an external datasource. + * @param datasourceId the ID of the datasource to upload to + * @param bucket the name of the bucket to upload to + * @param key the name of the file to upload to + * @param data the file to upload + */ + externalUpload: async ({ datasourceId, bucket, key, data }) => { + console.log(API) + const { signedUrl, publicUrl } = await getSignedDatasourceURL({ + datasourceId, + bucket, + key, + }) + await API.put({ + url: signedUrl, + body: data, + json: false, + external: true, + }) + return { publicUrl } + }, + } +} diff --git a/packages/server/__mocks__/mysql2/promise.ts b/packages/server/__mocks__/mysql2/promise.ts new file mode 100644 index 0000000000..8a8fb7fcf0 --- /dev/null +++ b/packages/server/__mocks__/mysql2/promise.ts @@ -0,0 +1,17 @@ +module MySQLMock { + const mysql: any = {} + + const client = { + connect: jest.fn(), + end: jest.fn(), + query: jest.fn(async () => { + return [[]] + }), + } + + mysql.createConnection = jest.fn(async () => { + return client + }) + + module.exports = mysql +} diff --git a/packages/server/package.json b/packages/server/package.json index 7dafc8396d..e17e29864f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -73,9 +73,9 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "^10.0.3", - "@budibase/backend-core": "^1.0.76-alpha.3", - "@budibase/client": "^1.0.76-alpha.3", - "@budibase/string-templates": "^1.0.76-alpha.3", + "@budibase/backend-core": "^1.0.80", + "@budibase/client": "^1.0.80", + "@budibase/string-templates": "^1.0.80", "@bull-board/api": "^3.7.0", "@bull-board/koa": "^3.7.0", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts index 24a55a273d..82377aa21a 100644 --- a/packages/server/src/integrations/mysql.ts +++ b/packages/server/src/integrations/mysql.ts @@ -16,7 +16,7 @@ import { import { DatasourcePlus } from "./base/datasourcePlus" module MySQLModule { - const mysql = require("mysql2") + const mysql = require("mysql2/promise") const Sql = require("./base/sql") interface MySQLConfig { @@ -29,7 +29,7 @@ module MySQLModule { } const SCHEMA: Integration = { - docs: "https://github.com/mysqljs/mysql", + docs: "https://github.com/sidorares/node-mysql2", plus: true, friendlyName: "MySQL", description: @@ -80,36 +80,9 @@ module MySQLModule { }, } - function internalQuery( - client: any, - query: SqlQuery, - connect: boolean = true - ): Promise { - // Node MySQL is callback based, so we must wrap our call in a promise - return new Promise((resolve, reject) => { - if (connect) { - client.connect() - } - return client.query( - query.sql, - query.bindings || {}, - (error: any, results: object[]) => { - if (error) { - reject(error) - } else { - resolve(results) - } - if (connect) { - client.end() - } - } - ) - }) - } - class MySQLIntegration extends Sql implements DatasourcePlus { private config: MySQLConfig - private readonly client: any + private client: any public tables: Record = {} public schemaErrors: Record = {} @@ -119,93 +92,127 @@ module MySQLModule { if (config.ssl && Object.keys(config.ssl).length === 0) { delete config.ssl } - this.client = mysql.createConnection(config) + this.config = config + } + + async connect() { + this.client = await mysql.createConnection(this.config) + } + + async disconnect() { + await this.client.end() + } + + async internalQuery( + query: SqlQuery, + connect: boolean = true + ): Promise { + try { + if (connect) { + await this.connect() + } + // Node MySQL is callback based, so we must wrap our call in a promise + const response = await this.client.query( + query.sql, + query.bindings || [] + ) + return response[0] + } finally { + if (connect) { + await this.disconnect() + } + } } async buildSchema(datasourceId: string, entities: Record) { const tables: { [key: string]: Table } = {} const database = this.config.database - this.client.connect() + await this.connect() - // get the tables first - const tablesResp = await internalQuery( - this.client, - { sql: "SHOW TABLES;" }, - false - ) - const tableNames = tablesResp.map( - (obj: any) => - obj[`Tables_in_${database}`] || - obj[`Tables_in_${database.toLowerCase()}`] - ) - for (let tableName of tableNames) { - const primaryKeys = [] - const schema: TableSchema = {} - const descResp = await internalQuery( - this.client, - { sql: `DESCRIBE \`${tableName}\`;` }, + try { + // get the tables first + const tablesResp = await this.internalQuery( + { sql: "SHOW TABLES;" }, false ) - for (let column of descResp) { - const columnName = column.Field - if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) { - primaryKeys.push(columnName) + const tableNames = tablesResp.map( + (obj: any) => + obj[`Tables_in_${database}`] || + obj[`Tables_in_${database.toLowerCase()}`] + ) + for (let tableName of tableNames) { + const primaryKeys = [] + const schema: TableSchema = {} + const descResp = await this.internalQuery( + { sql: `DESCRIBE \`${tableName}\`;` }, + false + ) + for (let column of descResp) { + const columnName = column.Field + if ( + column.Key === "PRI" && + primaryKeys.indexOf(column.Key) === -1 + ) { + primaryKeys.push(columnName) + } + const constraints = { + presence: column.Null !== "YES", + } + const isAuto: boolean = + typeof column.Extra === "string" && + (column.Extra === "auto_increment" || + column.Extra.toLowerCase().includes("generated")) + schema[columnName] = { + name: columnName, + autocolumn: isAuto, + type: convertSqlType(column.Type), + constraints, + } } - const constraints = { - presence: column.Null !== "YES", - } - const isAuto: boolean = - typeof column.Extra === "string" && - (column.Extra === "auto_increment" || - column.Extra.toLowerCase().includes("generated")) - schema[columnName] = { - name: columnName, - autocolumn: isAuto, - type: convertSqlType(column.Type), - constraints, - } - } - if (!tables[tableName]) { - tables[tableName] = { - _id: buildExternalTableId(datasourceId, tableName), - primary: primaryKeys, - name: tableName, - schema, + if (!tables[tableName]) { + tables[tableName] = { + _id: buildExternalTableId(datasourceId, tableName), + primary: primaryKeys, + name: tableName, + schema, + } } } + } finally { + await this.disconnect() } - - this.client.end() const final = finaliseExternalTables(tables, entities) this.tables = final.tables this.schemaErrors = final.errors } async create(query: SqlQuery | string) { - const results = await internalQuery(this.client, getSqlQuery(query)) + const results = await this.internalQuery(getSqlQuery(query)) return results.length ? results : [{ created: true }] } async read(query: SqlQuery | string) { - return internalQuery(this.client, getSqlQuery(query)) + return this.internalQuery(getSqlQuery(query)) } async update(query: SqlQuery | string) { - const results = await internalQuery(this.client, getSqlQuery(query)) + const results = await this.internalQuery(getSqlQuery(query)) return results.length ? results : [{ updated: true }] } async delete(query: SqlQuery | string) { - const results = await internalQuery(this.client, getSqlQuery(query)) + const results = await this.internalQuery(getSqlQuery(query)) return results.length ? results : [{ deleted: true }] } async query(json: QueryJson) { - this.client.connect() - const queryFn = (query: any) => internalQuery(this.client, query, false) - const output = await this.queryWithReturning(json, queryFn) - this.client.end() - return output + await this.connect() + try { + const queryFn = (query: any) => this.internalQuery(query, false) + return await this.queryWithReturning(json, queryFn) + } finally { + await this.disconnect() + } } } diff --git a/packages/server/src/integrations/tests/mysql.spec.js b/packages/server/src/integrations/tests/mysql.spec.js index 47ca3688f0..8304771d5d 100644 --- a/packages/server/src/integrations/tests/mysql.spec.js +++ b/packages/server/src/integrations/tests/mysql.spec.js @@ -19,7 +19,7 @@ describe("MySQL Integration", () => { await config.integration.create({ sql }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, {}, expect.any(Function)) + expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) }) it("calls the read method with the correct params", async () => { @@ -27,7 +27,7 @@ describe("MySQL Integration", () => { await config.integration.read({ sql }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, {}, expect.any(Function)) + expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) }) it("calls the update method with the correct params", async () => { @@ -35,7 +35,7 @@ describe("MySQL Integration", () => { await config.integration.update({ sql }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, {}, expect.any(Function)) + expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) }) it("calls the delete method with the correct params", async () => { @@ -43,7 +43,7 @@ describe("MySQL Integration", () => { await config.integration.delete({ sql }) - expect(config.integration.client.query).toHaveBeenCalledWith(sql, {}, expect.any(Function)) + expect(config.integration.client.query).toHaveBeenCalledWith(sql, []) }) describe("no rows returned", () => { diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index 1341f5abca..26e35f300f 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -40,7 +40,7 @@ const SQL_TYPE_MAP = { export enum SqlClients { MS_SQL = "mssql", POSTGRES = "pg", - MY_SQL = "mysql", + MY_SQL = "mysql2", ORACLE = "oracledb", } diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 2c399a7dac..73649597fb 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/worker/package.json b/packages/worker/package.json index 14c8d7e402..e39c9ed096 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.0.76-alpha.3", + "version": "1.0.80", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -34,8 +34,8 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "^1.0.76-alpha.3", - "@budibase/string-templates": "^1.0.76-alpha.3", + "@budibase/backend-core": "^1.0.80", + "@budibase/string-templates": "^1.0.80", "@koa/router": "^8.0.0", "@sentry/node": "^6.0.0", "@techpass/passport-openidconnect": "^0.3.0",