diff --git a/packages/builder/src/stores/builder/screenComponent.ts b/packages/builder/src/stores/builder/screenComponent.ts index 310bf2172c..2b3dde91f8 100644 --- a/packages/builder/src/stores/builder/screenComponent.ts +++ b/packages/builder/src/stores/builder/screenComponent.ts @@ -81,11 +81,11 @@ export const screenComponentErrorList = derived( const errors: UIComponentError[] = [] function checkComponentErrors(component: Component, ancestors: string[]) { + errors.push(...getMissingAncestors(component, definitions, ancestors)) errors.push( ...getInvalidDatasources(screen, component, datasources, definitions) ) errors.push(...getMissingRequiredSettings(component, definitions)) - errors.push(...getMissingAncestors(component, definitions, ancestors)) for (const child of component._children || []) { checkComponentErrors(child, [...ancestors, component._component]) @@ -239,7 +239,10 @@ function getMissingAncestors( ancestors: string[] ): UIComponentError[] { const definition = definitions[component._component] - + if (ancestors.some(a => !a.startsWith(BudibasePrefix))) { + // We don't have a way to know what components are used within a plugin component + return [] + } if (!definition?.requiredAncestors?.length) { return [] } diff --git a/packages/client/src/index.js b/packages/client/src/index.js deleted file mode 100644 index f19c46a452..0000000000 --- a/packages/client/src/index.js +++ /dev/null @@ -1,142 +0,0 @@ -import ClientApp from "./components/ClientApp.svelte" -import UpdatingApp from "./components/UpdatingApp.svelte" -import { - builderStore, - appStore, - blockStore, - componentStore, - environmentStore, - dndStore, - eventStore, - hoverStore, - stateStore, - routeStore, -} from "./stores" -import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js" -import { get } from "svelte/store" -import { initWebsocket } from "./websocket.js" - -// Provide svelte and svelte/internal as globals for custom components -import * as svelte from "svelte" -import * as internal from "svelte/internal" - -window.svelte_internal = internal -window.svelte = svelte - -// Initialise spectrum icons -loadSpectrumIcons() - -let app - -const loadBudibase = async () => { - // Update builder store with any builder flags - builderStore.set({ - ...get(builderStore), - inBuilder: !!window["##BUDIBASE_IN_BUILDER##"], - layout: window["##BUDIBASE_PREVIEW_LAYOUT##"], - screen: window["##BUDIBASE_PREVIEW_SCREEN##"], - selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"], - previewId: window["##BUDIBASE_PREVIEW_ID##"], - theme: window["##BUDIBASE_PREVIEW_THEME##"], - customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"], - previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"], - navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"], - hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"], - usedPlugins: window["##BUDIBASE_USED_PLUGINS##"], - location: window["##BUDIBASE_LOCATION##"], - snippets: window["##BUDIBASE_SNIPPETS##"], - componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"], - }) - - // Set app ID - this window flag is set by both the preview and the real - // server rendered app HTML - appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"]) - - // Set the flag used to determine if the app is being loaded via an iframe - appStore.actions.setAppEmbedded( - window["##BUDIBASE_APP_EMBEDDED##"] === "true" - ) - - if (window.MIGRATING_APP) { - new UpdatingApp({ - target: window.document.body, - }) - return - } - - // Fetch environment info - if (!get(environmentStore)?.loaded) { - await environmentStore.actions.fetchEnvironment() - } - - // Register handler for runtime events from the builder - window.handleBuilderRuntimeEvent = (type, data) => { - if (!window["##BUDIBASE_IN_BUILDER##"]) { - return - } - if (type === "event-completed") { - eventStore.actions.resolveEvent(data) - } else if (type === "eject-block") { - const block = blockStore.actions.getBlock(data) - block?.eject() - } else if (type === "dragging-new-component") { - const { dragging, component } = data - if (dragging) { - const definition = - componentStore.actions.getComponentDefinition(component) - dndStore.actions.startDraggingNewComponent({ component, definition }) - } else { - dndStore.actions.reset() - } - } else if (type === "request-context") { - const { selectedComponentInstance, screenslotInstance } = - get(componentStore) - const instance = selectedComponentInstance || screenslotInstance - const context = instance?.getDataContext() - let stringifiedContext = null - try { - stringifiedContext = JSON.stringify(context) - } catch (error) { - // Ignore - invalid context - } - eventStore.actions.dispatchEvent("provide-context", { - context: stringifiedContext, - }) - } else if (type === "hover-component") { - hoverStore.actions.hoverComponent(data, false) - } else if (type === "builder-meta") { - builderStore.actions.setMetadata(data) - } else if (type === "builder-state") { - const [[key, value]] = Object.entries(data) - stateStore.actions.setValue(key, value) - } else if (type === "builder-url-test-data") { - const { route, testValue } = data - routeStore.actions.setTestUrlParams(route, testValue) - } - } - - // Register any custom components - if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) { - window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => { - componentStore.actions.registerCustomComponent(component) - }) - } - - // Make a callback available for custom component bundles to register - // themselves at runtime - window.registerCustomComponent = - componentStore.actions.registerCustomComponent - - // Initialise websocket - initWebsocket() - - // Create app if one hasn't been created yet - if (!app) { - app = new ClientApp({ - target: window.document.body, - }) - } -} - -// Attach to window so the HTML template can call this when it loads -window.loadBudibase = loadBudibase diff --git a/packages/pro b/packages/pro index 45f5673d5e..e3843dd4ea 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 45f5673d5e5ab3c22deb6663cea2e31a628aa133 +Subproject commit e3843dd4eaced68ae063355b77df200dbc789c98 diff --git a/packages/server/src/api/controllers/plugin/index.ts b/packages/server/src/api/controllers/plugin/index.ts index 2a4d69b671..6f7cceca10 100644 --- a/packages/server/src/api/controllers/plugin/index.ts +++ b/packages/server/src/api/controllers/plugin/index.ts @@ -11,6 +11,7 @@ import { UploadPluginResponse, FetchPluginResponse, DeletePluginResponse, + PluginMetadata, } from "@budibase/types" import env from "../../../environment" import { clientAppSocket } from "../../../websockets" @@ -53,10 +54,11 @@ export async function create( const { source, url, headers, githubToken } = ctx.request.body try { - let metadata - let directory + let metadata: PluginMetadata + let directory: string + // Generating random name as a backup and needed for url - let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000) + const name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000) switch (source) { case PluginSource.NPM: { @@ -81,12 +83,14 @@ export async function create( directory = directoryUrl break } + default: + ctx.throw(400, "Invalid source") } - pluginCore.validate(metadata?.schema) + pluginCore.validate(metadata.schema) // Only allow components in cloud - if (!env.SELF_HOSTED && metadata?.schema?.type !== PluginType.COMPONENT) { + if (!env.SELF_HOSTED && metadata.schema?.type !== PluginType.COMPONENT) { throw new Error( "Only component plugins are supported outside of self-host" ) diff --git a/packages/server/src/sdk/index.ts b/packages/server/src/sdk/index.ts index e3e88c25c4..6d6bf00ee0 100644 --- a/packages/server/src/sdk/index.ts +++ b/packages/server/src/sdk/index.ts @@ -7,7 +7,7 @@ import { default as queries } from "./app/queries" import { default as rows } from "./app/rows" import { default as links } from "./app/links" import { default as users } from "./users" -import { default as plugins } from "./plugins" +import * as plugins from "./plugins" import * as views from "./app/views" import * as permissions from "./app/permissions" import * as rowActions from "./app/rowActions" diff --git a/packages/server/src/sdk/plugins/index.ts b/packages/server/src/sdk/plugins/index.ts index 863cfa0db6..03dd2be256 100644 --- a/packages/server/src/sdk/plugins/index.ts +++ b/packages/server/src/sdk/plugins/index.ts @@ -1,5 +1,41 @@ -import * as plugins from "./plugins" +import { KoaFile, Plugin, PluginSource, PluginType } from "@budibase/types" +import { + db as dbCore, + objectStore, + plugins as pluginCore, + tenancy, +} from "@budibase/backend-core" +import { fileUpload } from "../../api/controllers/plugin/file" +import env from "../../environment" +import { clientAppSocket } from "../../websockets" +import { sdk as pro } from "@budibase/pro" -export default { - ...plugins, +export async function fetch(type?: PluginType): Promise { + const db = tenancy.getGlobalDB() + const response = await db.allDocs( + dbCore.getPluginParams(null, { + include_docs: true, + }) + ) + let plugins = response.rows.map((row: any) => row.doc) as Plugin[] + plugins = await objectStore.enrichPluginURLs(plugins) + if (type) { + return plugins.filter((plugin: Plugin) => plugin.schema?.type === type) + } else { + return plugins + } +} + +export async function processUploaded(plugin: KoaFile, source: PluginSource) { + const { metadata, directory } = await fileUpload(plugin) + pluginCore.validate(metadata.schema) + + // Only allow components in cloud + if (!env.SELF_HOSTED && metadata.schema?.type !== PluginType.COMPONENT) { + throw new Error("Only component plugins are supported outside of self-host") + } + + const doc = await pro.plugins.storePlugin(metadata, directory, source) + clientAppSocket?.emit("plugin-update", { name: doc.name, hash: doc.hash }) + return doc } diff --git a/packages/server/src/sdk/plugins/plugins.ts b/packages/server/src/sdk/plugins/plugins.ts deleted file mode 100644 index bff24dcef7..0000000000 --- a/packages/server/src/sdk/plugins/plugins.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { KoaFile, Plugin, PluginSource, PluginType } from "@budibase/types" -import { - db as dbCore, - objectStore, - plugins as pluginCore, - tenancy, -} from "@budibase/backend-core" -import { fileUpload } from "../../api/controllers/plugin/file" -import env from "../../environment" -import { clientAppSocket } from "../../websockets" -import { sdk as pro } from "@budibase/pro" - -export async function fetch(type?: PluginType): Promise { - const db = tenancy.getGlobalDB() - const response = await db.allDocs( - dbCore.getPluginParams(null, { - include_docs: true, - }) - ) - let plugins = response.rows.map((row: any) => row.doc) as Plugin[] - plugins = await objectStore.enrichPluginURLs(plugins) - if (type) { - return plugins.filter((plugin: Plugin) => plugin.schema?.type === type) - } else { - return plugins - } -} - -export async function processUploaded(plugin: KoaFile, source?: PluginSource) { - const { metadata, directory } = await fileUpload(plugin) - pluginCore.validate(metadata?.schema) - - // Only allow components in cloud - if (!env.SELF_HOSTED && metadata?.schema?.type !== PluginType.COMPONENT) { - throw new Error("Only component plugins are supported outside of self-host") - } - - const doc = await pro.plugins.storePlugin(metadata, directory, source) - clientAppSocket?.emit("plugin-update", { name: doc.name, hash: doc.hash }) - return doc -} diff --git a/packages/server/src/utilities/fileSystem/plugin.ts b/packages/server/src/utilities/fileSystem/plugin.ts index 2949daef61..15238637c1 100644 --- a/packages/server/src/utilities/fileSystem/plugin.ts +++ b/packages/server/src/utilities/fileSystem/plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@budibase/types" +import { Plugin, PluginUpload } from "@budibase/types" import { budibaseTempDir } from "../budibaseDir" import fs from "fs" import { join } from "path" @@ -8,31 +8,31 @@ import stream from "stream" const DATASOURCE_PATH = join(budibaseTempDir(), "datasource") const AUTOMATION_PATH = join(budibaseTempDir(), "automation") -export const getPluginMetadata = async (path: string) => { - let metadata: any = {} +export const getPluginMetadata = async ( + path: string +): Promise => { + let pkg: any + let schema: any try { - const pkg = fs.readFileSync(join(path, "package.json"), "utf8") - const schema = fs.readFileSync(join(path, "schema.json"), "utf8") - - metadata.schema = JSON.parse(schema) - metadata.package = JSON.parse(pkg) - - if ( - !metadata.package.name || - !metadata.package.version || - !metadata.package.description - ) { - throw new Error( - "package.json is missing one of 'name', 'version' or 'description'." - ) + pkg = JSON.parse(fs.readFileSync(join(path, "package.json"), "utf8")) + schema = JSON.parse(fs.readFileSync(join(path, "schema.json"), "utf8")) + if (!pkg.name) { + throw new Error("package.json is missing 'name'.") + } + if (!pkg.version) { + throw new Error("package.json is missing 'version'.") + } + if (!pkg.description) { + throw new Error("package.json is missing 'description'.") } } catch (err: any) { throw new Error( - `Unable to process schema.json/package.json in plugin. ${err.message}` + `Unable to process schema.json/package.json in plugin. ${err.message}`, + { cause: err } ) } - return { metadata, directory: path } + return { metadata: { package: pkg, schema }, directory: path } } async function getPluginImpl(path: string, plugin: Plugin) { diff --git a/packages/server/src/watch.ts b/packages/server/src/watch.ts index 8f3fe22023..16b8800887 100644 --- a/packages/server/src/watch.ts +++ b/packages/server/src/watch.ts @@ -3,7 +3,8 @@ import env from "./environment" import chokidar from "chokidar" import fs from "fs" import { constants, tenancy } from "@budibase/backend-core" -import pluginsSdk from "./sdk/plugins" +import { processUploaded } from "./sdk/plugins" +import { PluginSource } from "@budibase/types" export function watch() { const watchPath = path.join(env.PLUGINS_DIR, "./**/*.tar.gz") @@ -27,7 +28,7 @@ export function watch() { const split = path.split("/") const name = split[split.length - 1] console.log("Importing plugin:", path) - await pluginsSdk.processUploaded({ name, path }) + await processUploaded({ name, path }, PluginSource.FILE) } catch (err: any) { const message = err?.message ? err?.message : err console.error("Failed to import plugin:", message) diff --git a/packages/types/src/documents/global/plugin.ts b/packages/types/src/documents/global/plugin.ts index eeddc04e58..56611d2d24 100644 --- a/packages/types/src/documents/global/plugin.ts +++ b/packages/types/src/documents/global/plugin.ts @@ -24,10 +24,7 @@ export interface Plugin extends Document { source: PluginSource package: { [key: string]: any } hash: string - schema: { - type: PluginType - [key: string]: any - } + schema: PluginSchema iconFileName?: string // Populated on read jsUrl?: string @@ -36,3 +33,24 @@ export interface Plugin extends Document { } export const PLUGIN_TYPE_ARR = Object.values(PluginType) + +export interface PluginSchema { + type: PluginType + [key: string]: any +} + +interface Package { + name: string + version: string + description: string +} + +export interface PluginMetadata { + schema: PluginSchema + package: Package +} + +export interface PluginUpload { + metadata: PluginMetadata + directory: string +}