merge from master

This commit is contained in:
Martin McKeaveney 2022-03-08 12:34:27 +01:00
commit 43348fd54f
22 changed files with 2904 additions and 168 deletions

View File

@ -55,7 +55,7 @@ http {
add_header X-Frame-Options SAMEORIGIN always; add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always; add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" 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 # upstreams
set $apps {{ apps }}; set $apps {{ apps }};

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.76-alpha.3", "version": "1.0.80",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -64,10 +64,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.76-alpha.3", "@budibase/bbui": "^1.0.80",
"@budibase/client": "^1.0.76-alpha.3", "@budibase/client": "^1.0.80",
"@budibase/frontend-core": "^1.0.76-alpha.3", "@budibase/frontend-core": "^1.0.80",
"@budibase/string-templates": "^1.0.76-alpha.3", "@budibase/string-templates": "^1.0.80",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -57,7 +57,8 @@
{data} {data}
{loading} {loading}
{type} {type}
allowEditing={!view?.calculation} allowEditing={false}
rowCount={10}
bind:hideAutocolumns bind:hideAutocolumns
> >
<ViewFilterButton {view} /> <ViewFilterButton {view} />

View File

@ -134,8 +134,9 @@
// Remove all iframe event listeners on component destroy // Remove all iframe event listeners on component destroy
onDestroy(() => { onDestroy(() => {
window.removeEventListener("message", receiveMessage)
if (iframe.contentWindow) { if (iframe.contentWindow) {
window.removeEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) { if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB // Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener( iframe.contentWindow.removeEventListener(

View File

@ -60,7 +60,7 @@ export function createQueriesStore() {
}) })
return savedQuery return savedQuery
}, },
import: async (data, datasourceId) => { import: async ({ data, datasourceId }) => {
return await API.importQueries({ return await API.importQueries({
datasourceId, datasourceId,
data, data,

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,18 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.76-alpha.3", "@budibase/bbui": "^1.0.80",
"@budibase/frontend-core": "^1.0.76-alpha.3", "@budibase/frontend-core": "^1.0.80",
"@budibase/string-templates": "^1.0.76-alpha.3", "@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/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -81,7 +81,10 @@
loading = false loading = false
return res return res
} catch (error) { } catch (error) {
notificationStore.actions.error(`Error uploading file: ${error}`) notificationStore.actions.error(
`Error uploading file: ${error?.message || error}`
)
loading = false
} }
} }

View File

@ -127,12 +127,16 @@ const queryExecutionHandler = async action => {
// Trigger a notification and invalidate the datasource as long as this // Trigger a notification and invalidate the datasource as long as this
// was not a readable query // was not a readable query
if (!query.readable) { if (!query.readable) {
API.notifications.error.success("Query executed successfully") notificationStore.actions.success("Query executed successfully")
await dataSourceStore.actions.invalidateDataSource(query.datasourceId) await dataSourceStore.actions.invalidateDataSource(query.datasourceId)
} }
return { result } return { result }
} catch (error) { } catch (error) {
notificationStore.actions.error(
"An error occurred while executing the query"
)
// Abort next actions // Abort next actions
return false return false
} }

2689
packages/client/stats.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.76-alpha.3", "@budibase/bbui": "^1.0.80",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -1,61 +1,66 @@
export const buildAttachmentEndpoints = API => ({ 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,
})
},
/** /**
* Generates a signed URL to upload a file to an external datasource. * Generates a signed URL to upload a file to an external datasource.
* @param datasourceId the ID of the datasource to upload to * @param datasourceId the ID of the datasource to upload to
* @param bucket the name of the bucket to upload to * @param bucket the name of the bucket to upload to
* @param key the name of the file 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({ return await API.post({
url: `/api/attachments/${datasourceId}/url`, url: `/api/attachments/${datasourceId}/url`,
body: { bucket, key }, body: { bucket, key },
}) })
}, }
/** return {
* Uploads a file to an external datasource. getSignedDatasourceURL,
* @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 * Uploads an attachment to the server.
* @param data the file to upload * @param data the attachment to upload
*/ * @param tableId the table ID to upload to
externalUpload: async ({ datasourceId, bucket, key, data }) => { */
const { signedUrl, publicUrl } = await API.getSignedDatasourceURL({ uploadAttachment: async ({ data, tableId }) => {
datasourceId, return await API.post({
bucket, url: `/api/attachments/${tableId}/upload`,
key, body: data,
}) json: false,
await API.put({ })
url: signedUrl, },
body: data,
json: false, /**
external: true, * Uploads an attachment to the server as a builder user from the builder.
}) * @param data the data to upload
return { publicUrl } */
}, 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 }
},
}
}

View File

@ -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
}

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -73,9 +73,9 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.76-alpha.3", "@budibase/backend-core": "^1.0.80",
"@budibase/client": "^1.0.76-alpha.3", "@budibase/client": "^1.0.80",
"@budibase/string-templates": "^1.0.76-alpha.3", "@budibase/string-templates": "^1.0.80",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -16,7 +16,7 @@ import {
import { DatasourcePlus } from "./base/datasourcePlus" import { DatasourcePlus } from "./base/datasourcePlus"
module MySQLModule { module MySQLModule {
const mysql = require("mysql2") const mysql = require("mysql2/promise")
const Sql = require("./base/sql") const Sql = require("./base/sql")
interface MySQLConfig { interface MySQLConfig {
@ -29,7 +29,7 @@ module MySQLModule {
} }
const SCHEMA: Integration = { const SCHEMA: Integration = {
docs: "https://github.com/mysqljs/mysql", docs: "https://github.com/sidorares/node-mysql2",
plus: true, plus: true,
friendlyName: "MySQL", friendlyName: "MySQL",
description: description:
@ -80,36 +80,9 @@ module MySQLModule {
}, },
} }
function internalQuery(
client: any,
query: SqlQuery,
connect: boolean = true
): Promise<any[] | any> {
// 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 { class MySQLIntegration extends Sql implements DatasourcePlus {
private config: MySQLConfig private config: MySQLConfig
private readonly client: any private client: any
public tables: Record<string, Table> = {} public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
@ -119,93 +92,127 @@ module MySQLModule {
if (config.ssl && Object.keys(config.ssl).length === 0) { if (config.ssl && Object.keys(config.ssl).length === 0) {
delete config.ssl 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<any[] | any> {
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<string, Table>) { async buildSchema(datasourceId: string, entities: Record<string, Table>) {
const tables: { [key: string]: Table } = {} const tables: { [key: string]: Table } = {}
const database = this.config.database const database = this.config.database
this.client.connect() await this.connect()
// get the tables first try {
const tablesResp = await internalQuery( // get the tables first
this.client, const tablesResp = await this.internalQuery(
{ sql: "SHOW TABLES;" }, { 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}\`;` },
false false
) )
for (let column of descResp) { const tableNames = tablesResp.map(
const columnName = column.Field (obj: any) =>
if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) { obj[`Tables_in_${database}`] ||
primaryKeys.push(columnName) 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 = { if (!tables[tableName]) {
presence: column.Null !== "YES", tables[tableName] = {
} _id: buildExternalTableId(datasourceId, tableName),
const isAuto: boolean = primary: primaryKeys,
typeof column.Extra === "string" && name: tableName,
(column.Extra === "auto_increment" || schema,
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,
} }
} }
} finally {
await this.disconnect()
} }
this.client.end()
const final = finaliseExternalTables(tables, entities) const final = finaliseExternalTables(tables, entities)
this.tables = final.tables this.tables = final.tables
this.schemaErrors = final.errors this.schemaErrors = final.errors
} }
async create(query: SqlQuery | string) { 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 }] return results.length ? results : [{ created: true }]
} }
async read(query: SqlQuery | string) { async read(query: SqlQuery | string) {
return internalQuery(this.client, getSqlQuery(query)) return this.internalQuery(getSqlQuery(query))
} }
async update(query: SqlQuery | string) { 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 }] return results.length ? results : [{ updated: true }]
} }
async delete(query: SqlQuery | string) { 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 }] return results.length ? results : [{ deleted: true }]
} }
async query(json: QueryJson) { async query(json: QueryJson) {
this.client.connect() await this.connect()
const queryFn = (query: any) => internalQuery(this.client, query, false) try {
const output = await this.queryWithReturning(json, queryFn) const queryFn = (query: any) => this.internalQuery(query, false)
this.client.end() return await this.queryWithReturning(json, queryFn)
return output } finally {
await this.disconnect()
}
} }
} }

View File

@ -19,7 +19,7 @@ describe("MySQL Integration", () => {
await config.integration.create({ await config.integration.create({
sql 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 () => { it("calls the read method with the correct params", async () => {
@ -27,7 +27,7 @@ describe("MySQL Integration", () => {
await config.integration.read({ await config.integration.read({
sql 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 () => { it("calls the update method with the correct params", async () => {
@ -35,7 +35,7 @@ describe("MySQL Integration", () => {
await config.integration.update({ await config.integration.update({
sql 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 () => { it("calls the delete method with the correct params", async () => {
@ -43,7 +43,7 @@ describe("MySQL Integration", () => {
await config.integration.delete({ await config.integration.delete({
sql sql
}) })
expect(config.integration.client.query).toHaveBeenCalledWith(sql, {}, expect.any(Function)) expect(config.integration.client.query).toHaveBeenCalledWith(sql, [])
}) })
describe("no rows returned", () => { describe("no rows returned", () => {

View File

@ -40,7 +40,7 @@ const SQL_TYPE_MAP = {
export enum SqlClients { export enum SqlClients {
MS_SQL = "mssql", MS_SQL = "mssql",
POSTGRES = "pg", POSTGRES = "pg",
MY_SQL = "mysql", MY_SQL = "mysql2",
ORACLE = "oracledb", ORACLE = "oracledb",
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.76-alpha.3", "version": "1.0.80",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -34,8 +34,8 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "^1.0.76-alpha.3", "@budibase/backend-core": "^1.0.80",
"@budibase/string-templates": "^1.0.76-alpha.3", "@budibase/string-templates": "^1.0.80",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0", "@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",