diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte index a500c6a5b3..8454f3b3d5 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte @@ -28,6 +28,9 @@ let importModal $: showImportButton = false + $: customIntegrations = Object.entries(integrations).filter( + entry => entry[1].custom + ) checkShowImport() @@ -163,17 +166,19 @@ /> {/each} - Custom data source -
- {#each Object.entries(integrations).filter(entry => entry[1].custom) as [integrationType, schema]} - selectIntegration(evt.detail)} - {schema} - bind:integrationType - {integration} - /> - {/each} -
+ {#if customIntegrations.length > 0} + Custom data source +
+ {#each customIntegrations as [integrationType, schema]} + selectIntegration(evt.detail)} + {schema} + bind:integrationType + {integration} + /> + {/each} +
+ {/if} diff --git a/packages/cli/src/plugins/index.js b/packages/cli/src/plugins/index.js index 3ee2463a31..643406bc4d 100644 --- a/packages/cli/src/plugins/index.js +++ b/packages/cli/src/plugins/index.js @@ -9,6 +9,19 @@ const { runPkgCommand } = require("../exec") const { join } = require("path") const { success, error, info } = require("../utils") +function checkInPlugin() { + if (!fs.existsSync("package.json")) { + throw new Error( + "Please run in a plugin directory - must contain package.json" + ) + } + if (!fs.existsSync("schema.json")) { + throw new Error( + "Please run in a plugin directory - must contain schema.json" + ) + } +} + async function init(opts) { const type = opts["init"] || opts if (!type || !PLUGIN_TYPES_ARR.includes(type)) { @@ -35,13 +48,15 @@ async function init(opts) { // get the skeleton console.log(info("Retrieving project...")) await getSkeleton(type, name) - await fleshOutSkeleton(name, desc, version) + await fleshOutSkeleton(type, name, desc, version) console.log(info("Installing dependencies...")) await runPkgCommand("install", join(process.cwd(), name)) console.log(info(`Plugin created in directory "${name}"`)) } async function verify() { + // will throw errors if not acceptable + checkInPlugin() console.log(info("Verifying plugin...")) const schema = fs.readFileSync("schema.json", "utf8") const pkg = fs.readFileSync("package.json", "utf8") diff --git a/packages/cli/src/plugins/skeleton.js b/packages/cli/src/plugins/skeleton.js index a1d9101c6f..76b9aa2d8a 100644 --- a/packages/cli/src/plugins/skeleton.js +++ b/packages/cli/src/plugins/skeleton.js @@ -6,7 +6,7 @@ const { join } = require("path") const tar = require("tar") const { processStringSync } = require("@budibase/string-templates") -const HBS_FILES = ["package.json.hbs", "schema.json.hbs"] +const HBS_FILES = ["package.json.hbs", "schema.json.hbs", "README.md.hbs"] async function getSkeletonUrl(type) { const resp = await fetch( @@ -40,7 +40,7 @@ exports.getSkeleton = async (type, name) => { fs.rmSync(tarballFile) } -exports.fleshOutSkeleton = async (name, description, version) => { +exports.fleshOutSkeleton = async (type, name, description, version) => { for (let file of HBS_FILES) { const oldFile = join(name, file), newFile = join(name, file.substring(0, file.length - 4)) diff --git a/packages/cli/src/plugins/validate.js b/packages/cli/src/plugins/validate.js index 4c2fa6ba53..a6b4555cbd 100644 --- a/packages/cli/src/plugins/validate.js +++ b/packages/cli/src/plugins/validate.js @@ -22,6 +22,8 @@ function validateComponent(schema) { const validator = joi.object({ type: joi.string().allow("component").required(), metadata: joi.object().unknown(true).required(), + hash: joi.string().optional(), + version: joi.string().optional(), schema: joi .object({ name: joi.string().required(), @@ -53,6 +55,8 @@ function validateDatasource(schema) { const validator = joi.object({ type: joi.string().allow("datasource").required(), metadata: joi.object().unknown(true).required(), + hash: joi.string().optional(), + version: joi.string().optional(), schema: joi.object({ docs: joi.string(), friendlyName: joi.string().required(), diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index a830fd5518..8a4a412bc6 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -17,15 +17,12 @@ const bullboard = require("./automations/bullboard") const { logAlert } = require("@budibase/backend-core/logging") const { pinoSettings } = require("@budibase/backend-core") const { Thread } = require("./threads") -const chokidar = require("chokidar") const fs = require("fs") -const path = require("path") import redis from "./utilities/redis" import * as migrations from "./migrations" import { events, installation, tenancy } from "@budibase/backend-core" import { createAdminUser, getChecklist } from "./utilities/workerRequests" -import { processPlugin } from "./api/controllers/plugin" -import { DEFAULT_TENANT_ID } from "@budibase/backend-core/constants" +import { watch } from "./watch" const app = new Koa() @@ -144,28 +141,7 @@ module.exports = server.listen(env.PORT || 0, async () => { env.PLUGINS_DIR && fs.existsSync(env.PLUGINS_DIR) ) { - const watchPath = path.join(env.PLUGINS_DIR, "./**/*.tar.gz") - chokidar - .watch(watchPath, { - ignored: "**/node_modules", - awaitWriteFinish: true, - }) - .on("all", async (event: string, path: string) => { - // Sanity checks - if (!path?.endsWith(".tar.gz") || !fs.existsSync(path)) { - return - } - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - try { - const split = path.split("/") - const name = split[split.length - 1] - console.log("Importing plugin:", path) - await processPlugin({ name, path }) - } catch (err) { - console.log("Failed to import plugin:", err) - } - }) - }) + watch() } // check for version updates diff --git a/packages/server/src/integrations/index.ts b/packages/server/src/integrations/index.ts index b9523061a4..7b730a1764 100644 --- a/packages/server/src/integrations/index.ts +++ b/packages/server/src/integrations/index.ts @@ -93,7 +93,11 @@ module.exports = { for (let plugin of plugins) { if (plugin.name === integration) { // need to use commonJS require due to its dynamic runtime nature - return getDatasourcePlugin(plugin.name, plugin.jsUrl) + return getDatasourcePlugin( + plugin.name, + plugin.jsUrl, + plugin.schema?.hash + ) } } }, diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index 0d2f665e02..821e905fbc 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -352,13 +352,21 @@ exports.extractPluginTarball = async file => { return { metadata, directory: path } } -exports.getDatasourcePlugin = async (name, url) => { +exports.getDatasourcePlugin = async (name, url, hash) => { if (!fs.existsSync(DATASOURCE_PATH)) { fs.mkdirSync(DATASOURCE_PATH) } const filename = join(DATASOURCE_PATH, name) + const metadataName = `${filename}.bbmetadata` if (fs.existsSync(filename)) { - return require(filename) + const currentHash = fs.readFileSync(metadataName, "utf8") + // if hash is the same return the file, otherwise remove it and re-download + if (currentHash === hash) { + return require(filename) + } else { + console.log(`Updating plugin: ${name}`) + fs.unlinkSync(filename) + } } const fullUrl = checkSlashesInUrl( `${env.MINIO_URL}/${ObjectStoreBuckets.PLUGINS}/${url}` @@ -367,6 +375,7 @@ exports.getDatasourcePlugin = async (name, url) => { if (response.status === 200) { const content = await response.text() fs.writeFileSync(filename, content) + fs.writeFileSync(metadataName, hash) require(filename) } else { throw new Error( diff --git a/packages/server/src/watch.ts b/packages/server/src/watch.ts new file mode 100644 index 0000000000..3d3f4280da --- /dev/null +++ b/packages/server/src/watch.ts @@ -0,0 +1,32 @@ +import path from "path" +import * as env from "./environment" +import chokidar from "chokidar" +import fs from "fs" +import { tenancy } from "@budibase/backend-core" +import { DEFAULT_TENANT_ID } from "@budibase/backend-core/constants" +import { processPlugin } from "./api/controllers/plugin" + +export function watch() { + const watchPath = path.join(env.PLUGINS_DIR, "./**/*.tar.gz") + chokidar + .watch(watchPath, { + ignored: "**/node_modules", + awaitWriteFinish: true, + }) + .on("all", async (event: string, path: string) => { + // Sanity checks + if (!path?.endsWith(".tar.gz") || !fs.existsSync(path)) { + return + } + await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { + try { + const split = path.split("/") + const name = split[split.length - 1] + console.log("Importing plugin:", path) + await processPlugin({ name, path }) + } catch (err) { + console.log("Failed to import plugin:", err) + } + }) + }) +}