Merge pull request #15619 from Budibase/plugin-typing

Improve typing around plugins.
This commit is contained in:
Sam Rose 2025-02-27 09:49:42 +00:00 committed by GitHub
commit 0a8cf31dff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 94 additions and 76 deletions

@ -1 +1 @@
Subproject commit 45f5673d5e5ab3c22deb6663cea2e31a628aa133 Subproject commit e3843dd4eaced68ae063355b77df200dbc789c98

View File

@ -11,6 +11,7 @@ import {
UploadPluginResponse, UploadPluginResponse,
FetchPluginResponse, FetchPluginResponse,
DeletePluginResponse, DeletePluginResponse,
PluginMetadata,
} from "@budibase/types" } from "@budibase/types"
import env from "../../../environment" import env from "../../../environment"
import { clientAppSocket } from "../../../websockets" import { clientAppSocket } from "../../../websockets"
@ -53,10 +54,11 @@ export async function create(
const { source, url, headers, githubToken } = ctx.request.body const { source, url, headers, githubToken } = ctx.request.body
try { try {
let metadata let metadata: PluginMetadata
let directory let directory: string
// Generating random name as a backup and needed for url // 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) { switch (source) {
case PluginSource.NPM: { case PluginSource.NPM: {
@ -81,12 +83,14 @@ export async function create(
directory = directoryUrl directory = directoryUrl
break break
} }
default:
ctx.throw(400, "Invalid source")
} }
pluginCore.validate(metadata?.schema) pluginCore.validate(metadata.schema)
// Only allow components in cloud // 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( throw new Error(
"Only component plugins are supported outside of self-host" "Only component plugins are supported outside of self-host"
) )

View File

@ -7,7 +7,7 @@ import { default as queries } from "./app/queries"
import { default as rows } from "./app/rows" import { default as rows } from "./app/rows"
import { default as links } from "./app/links" import { default as links } from "./app/links"
import { default as users } from "./users" 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 views from "./app/views"
import * as permissions from "./app/permissions" import * as permissions from "./app/permissions"
import * as rowActions from "./app/rowActions" import * as rowActions from "./app/rowActions"

View File

@ -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 { export async function fetch(type?: PluginType): Promise<Plugin[]> {
...plugins, 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
} }

View File

@ -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<Plugin[]> {
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
}

View File

@ -1,4 +1,4 @@
import { Plugin } from "@budibase/types" import { Plugin, PluginUpload } from "@budibase/types"
import { budibaseTempDir } from "../budibaseDir" import { budibaseTempDir } from "../budibaseDir"
import fs from "fs" import fs from "fs"
import { join } from "path" import { join } from "path"
@ -8,31 +8,31 @@ import stream from "stream"
const DATASOURCE_PATH = join(budibaseTempDir(), "datasource") const DATASOURCE_PATH = join(budibaseTempDir(), "datasource")
const AUTOMATION_PATH = join(budibaseTempDir(), "automation") const AUTOMATION_PATH = join(budibaseTempDir(), "automation")
export const getPluginMetadata = async (path: string) => { export const getPluginMetadata = async (
let metadata: any = {} path: string
): Promise<PluginUpload> => {
let pkg: any
let schema: any
try { try {
const pkg = fs.readFileSync(join(path, "package.json"), "utf8") pkg = JSON.parse(fs.readFileSync(join(path, "package.json"), "utf8"))
const schema = fs.readFileSync(join(path, "schema.json"), "utf8") schema = JSON.parse(fs.readFileSync(join(path, "schema.json"), "utf8"))
if (!pkg.name) {
metadata.schema = JSON.parse(schema) throw new Error("package.json is missing 'name'.")
metadata.package = JSON.parse(pkg) }
if (!pkg.version) {
if ( throw new Error("package.json is missing 'version'.")
!metadata.package.name || }
!metadata.package.version || if (!pkg.description) {
!metadata.package.description throw new Error("package.json is missing 'description'.")
) {
throw new Error(
"package.json is missing one of 'name', 'version' or 'description'."
)
} }
} catch (err: any) { } catch (err: any) {
throw new Error( 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) { async function getPluginImpl(path: string, plugin: Plugin) {

View File

@ -3,7 +3,8 @@ import env from "./environment"
import chokidar from "chokidar" import chokidar from "chokidar"
import fs from "fs" import fs from "fs"
import { constants, tenancy } from "@budibase/backend-core" 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() { export function watch() {
const watchPath = path.join(env.PLUGINS_DIR, "./**/*.tar.gz") const watchPath = path.join(env.PLUGINS_DIR, "./**/*.tar.gz")
@ -27,7 +28,7 @@ export function watch() {
const split = path.split("/") const split = path.split("/")
const name = split[split.length - 1] const name = split[split.length - 1]
console.log("Importing plugin:", path) console.log("Importing plugin:", path)
await pluginsSdk.processUploaded({ name, path }) await processUploaded({ name, path }, PluginSource.FILE)
} catch (err: any) { } catch (err: any) {
const message = err?.message ? err?.message : err const message = err?.message ? err?.message : err
console.error("Failed to import plugin:", message) console.error("Failed to import plugin:", message)

View File

@ -24,10 +24,7 @@ export interface Plugin extends Document {
source: PluginSource source: PluginSource
package: { [key: string]: any } package: { [key: string]: any }
hash: string hash: string
schema: { schema: PluginSchema
type: PluginType
[key: string]: any
}
iconFileName?: string iconFileName?: string
// Populated on read // Populated on read
jsUrl?: string jsUrl?: string
@ -36,3 +33,24 @@ export interface Plugin extends Document {
} }
export const PLUGIN_TYPE_ARR = Object.values(PluginType) 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
}