diff --git a/packages/server/src/api/controllers/component.js b/packages/server/src/api/controllers/component.js index 108abb38a4..601f39db34 100644 --- a/packages/server/src/api/controllers/component.js +++ b/packages/server/src/api/controllers/component.js @@ -2,6 +2,7 @@ const { DocumentType, getPluginParams } = require("../../db/utils") const { getComponentLibraryManifest } = require("../../utilities/fileSystem") const { getAppDB } = require("@budibase/backend-core/context") const { getGlobalDB } = require("@budibase/backend-core/tenancy") +const env = require("../../environment") exports.fetchAppComponentDefinitions = async function (ctx) { try { @@ -32,23 +33,26 @@ exports.fetchAppComponentDefinitions = async function (ctx) { } } - // Add custom components - const globalDB = getGlobalDB() - const response = await globalDB.allDocs( - getPluginParams(null, { - include_docs: true, - }) - ) - response.rows - .map(row => row.doc) - .filter(plugin => plugin.schema.type === "component") - .forEach(plugin => { - const fullComponentName = `plugin/${plugin.name}/${plugin.version}` - definitions[fullComponentName] = { - component: fullComponentName, - ...plugin.schema.schema, - } - }) + // for now custom components only supported in self-host + if (env.SELF_HOSTED) { + // Add custom components + const globalDB = getGlobalDB() + const response = await globalDB.allDocs( + getPluginParams(null, { + include_docs: true, + }) + ) + response.rows + .map(row => row.doc) + .filter(plugin => plugin.schema.type === "component") + .forEach(plugin => { + const fullComponentName = `plugin/${plugin.name}/${plugin.version}` + definitions[fullComponentName] = { + component: fullComponentName, + ...plugin.schema.schema, + } + }) + } ctx.body = definitions } catch (err) { diff --git a/packages/server/src/api/controllers/plugin.ts b/packages/server/src/api/controllers/plugin.ts index 93e569e0c8..ff606295a2 100644 --- a/packages/server/src/api/controllers/plugin.ts +++ b/packages/server/src/api/controllers/plugin.ts @@ -1,9 +1,10 @@ import { ObjectStoreBuckets } from "../../constants" -import { extractPluginTarball } from "../../utilities/fileSystem" +import { extractPluginTarball, loadJSFile } from "../../utilities/fileSystem" import { getGlobalDB } from "@budibase/backend-core/tenancy" import { generatePluginID, getPluginParams } from "../../db/utils" import { uploadDirectory } from "@budibase/backend-core/objectStore" import { PluginType, FileType } from "@budibase/types" +import env from "../../environment" export async function getPlugins(type?: PluginType) { const db = getGlobalDB() @@ -49,6 +50,9 @@ export async function fetch(ctx: any) { export async function destroy(ctx: any) {} export async function processPlugin(plugin: FileType) { + if (!env.SELF_HOSTED) { + throw new Error("Plugins not supported outside of self-host.") + } const db = getGlobalDB() const { metadata, directory } = await extractPluginTarball(plugin) const version = metadata.package.version, @@ -56,7 +60,7 @@ export async function processPlugin(plugin: FileType) { description = metadata.package.description // first open the tarball into tmp directory - const bucketPath = `${name}/${version}/` + const bucketPath = `${name}/` const files = await uploadDirectory( ObjectStoreBuckets.PLUGINS, directory, @@ -66,8 +70,20 @@ export async function processPlugin(plugin: FileType) { if (!jsFile) { throw new Error(`Plugin missing .js file.`) } + // validate the JS for a datasource + if (metadata.schema.type === PluginType.DATASOURCE) { + const js = loadJSFile(directory, jsFile.name) + // TODO: this isn't safe - but we need full node environment + // in future we should do this in a thread for safety + try { + eval(js) + } catch (err: any) { + const message = err?.message ? err.message : JSON.stringify(err) + throw new Error(`JS invalid: ${message}`) + } + } const jsFileName = jsFile.name - const pluginId = generatePluginID(name, version) + const pluginId = generatePluginID(name) // overwrite existing docs entirely if they exist let rev @@ -80,10 +96,10 @@ export async function processPlugin(plugin: FileType) { const doc = { _id: pluginId, _rev: rev, + ...metadata, name, version, description, - ...metadata, jsUrl: `${bucketPath}${jsFileName}`, } const response = await db.put(doc) diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index e9a6a4c0d5..64d206aeb8 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -371,8 +371,8 @@ exports.getMemoryViewParams = (otherProps = {}) => { return getDocParams(DocumentType.MEM_VIEW, null, otherProps) } -exports.generatePluginID = (name, version) => { - return `${DocumentType.PLUGIN}${SEPARATOR}${name}${SEPARATOR}${version}` +exports.generatePluginID = name => { + return `${DocumentType.PLUGIN}${SEPARATOR}${name}` } /** diff --git a/packages/server/src/integrations/index.ts b/packages/server/src/integrations/index.ts index 7b730a1764..007b7e91e6 100644 --- a/packages/server/src/integrations/index.ts +++ b/packages/server/src/integrations/index.ts @@ -66,18 +66,18 @@ if (environment.SELF_HOSTED) { DEFINITIONS[SourceName.GOOGLE_SHEETS] = googlesheets.schema } -function isIntegrationAvailable(integration: string) {} - module.exports = { getDefinitions: async () => { - const plugins = await getPlugins(PluginType.DATASOURCE) - // extract the actual schema from each custom const pluginSchemas: { [key: string]: Integration } = {} - for (let plugin of plugins) { - const sourceId = plugin.name - pluginSchemas[sourceId] = { - ...plugin.schema["schema"], - custom: true, + if (environment.SELF_HOSTED) { + const plugins = await getPlugins(PluginType.DATASOURCE) + // extract the actual schema from each custom + for (let plugin of plugins) { + const sourceId = plugin.name + pluginSchemas[sourceId] = { + ...plugin.schema["schema"], + custom: true, + } } } return { @@ -89,16 +89,19 @@ module.exports = { if (INTEGRATIONS[integration]) { return INTEGRATIONS[integration] } - const plugins = await getPlugins(PluginType.DATASOURCE) - 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, - plugin.schema?.hash - ) + if (environment.SELF_HOSTED) { + const plugins = await getPlugins(PluginType.DATASOURCE) + 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, + plugin.schema?.hash + ) + } } } + throw new Error("No datasource implementation found.") }, } diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index 9141d8eebf..e1ac74272b 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -103,6 +103,13 @@ exports.loadHandlebarsFile = path => { return fs.readFileSync(path, "utf8") } +/** + * Same as above just with a different name. + */ +exports.loadJSFile = (directory, name) => { + return fs.readFileSync(join(directory, name), "utf8") +} + /** * When return a file from the API need to write the file to the system temporarily so we * can create a read stream to send. diff --git a/packages/server/src/watch.ts b/packages/server/src/watch.ts index 9821d1127b..a97fda4138 100644 --- a/packages/server/src/watch.ts +++ b/packages/server/src/watch.ts @@ -29,8 +29,9 @@ export function watch() { const name = split[split.length - 1] console.log("Importing plugin:", path) await processPlugin({ name, path }) - } catch (err) { - console.log("Failed to import plugin:", err) + } catch (err: any) { + const message = err?.message ? err?.message : err + console.error("Failed to import plugin:", message) } }) })