chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
import * as client from '@botpress/client'
import * as sdk from '@botpress/sdk'
import * as errors from '../errors'
import * as utils from '../utils'
import * as types from './types'
export const prepareCreateBotBody = async (bot: sdk.BotDefinition): Promise<types.CreateBotRequestBody> => ({
user: bot.user,
conversation: bot.conversation,
message: bot.message,
recurringEvents: bot.recurringEvents,
actions: bot.actions
? await utils.records.mapValuesAsync(bot.actions, async (action, actionName) => ({
...action,
input: {
...action.input,
schema: await utils.schema
.mapZodToJsonSchema(action.input, {
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(
thrown,
`Failed to convert ZUI to JSON schema for bot action ${actionName} input`
)
}),
},
output: {
...action.output,
schema: await utils.schema
.mapZodToJsonSchema(action.output, {
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(
thrown,
`Failed to convert ZUI to JSON schema for bot action ${actionName} output`
)
}),
},
}))
: undefined,
configuration: bot.configuration
? {
...bot.configuration,
schema: await utils.schema
.mapZodToJsonSchema(bot.configuration, {
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, 'Failed to convert ZUI to JSON schema for bot configuration')
}),
}
: undefined,
events: bot.events
? await utils.records.mapValuesAsync(bot.events, async (event, eventName) => ({
...event,
schema: await utils.schema
.mapZodToJsonSchema(event, {
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(
thrown,
`Failed to convert ZUI to JSON schema for bot event ${eventName}`
)
}),
}))
: undefined,
states: bot.states
? (utils.records.filterValues(
await utils.records.mapValuesAsync(bot.states, async (state, stateName) => ({
...state,
schema: await utils.schema
.mapZodToJsonSchema(state, {
useLegacyZuiTransformer: bot.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: bot.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(
thrown,
`Failed to convert ZUI to JSON schema for bot state ${stateName}`
)
}),
})),
({ type }) => type !== 'workflow'
) as types.CreateBotRequestBody['states'])
: undefined,
tags: bot.attributes,
})
export const prepareUpdateBotBody = (
localBot: types.UpdateBotRequestBody,
remoteBot: client.Bot
): types.UpdateBotRequestBody => ({
...localBot,
states: utils.records.setNullOnMissingValues(localBot.states, remoteBot.states),
recurringEvents: utils.records.setNullOnMissingValues(localBot.recurringEvents, remoteBot.recurringEvents),
events: utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localBot.events, remoteBot.events),
remoteItems: remoteBot.events,
}),
actions: utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localBot.actions, remoteBot.actions),
remoteItems: remoteBot.actions,
}),
user: {
...localBot.user,
tags: utils.records.setNullOnMissingValues(localBot.user?.tags, remoteBot.user?.tags),
},
conversation: {
...localBot.conversation,
tags: utils.records.setNullOnMissingValues(localBot.conversation?.tags, remoteBot.conversation?.tags),
},
message: {
...localBot.message,
tags: utils.records.setNullOnMissingValues(localBot.message?.tags, remoteBot.message?.tags),
},
integrations: utils.integrations.prepareIntegrationsUpdate(
utils.records.setNullOnMissingValues(localBot.integrations, remoteBot.integrations),
remoteBot.integrations
),
plugins: utils.records.setNullOnMissingValues(localBot.plugins, remoteBot.plugins),
tags: localBot.tags, // TODO: allow removing bot tags (aka attributes) by setting to null
})
+348
View File
@@ -0,0 +1,348 @@
import * as client from '@botpress/client'
import semver from 'semver'
import yn from 'yn'
import * as errors from '../errors'
import type { Logger } from '../logger'
import { formatPackageRef, ApiPackageRef, NamePackageRef } from '../package-ref'
import * as utils from '../utils'
import * as paging from './paging'
import * as retry from './retry'
import {
ApiClientProps,
PublicOrUnlistedIntegration,
PrivateIntegration,
PublicOrPrivateIntegration,
PublicInterface,
PrivateInterface,
PublicOrPrivateInterface,
PrivatePlugin,
PublicPlugin,
PublicOrPrivatePlugin,
BotSummary,
} from './types'
export * from './types'
/**
* This class is used to wrap the Botpress API and provide a more convenient way to interact with it.
*/
export class ApiClient {
public readonly client: client.Client
public readonly url: string
public readonly token: string
public readonly workspaceId: string
public readonly botId?: string
public readonly extraHeaders: Record<string, string>
public static newClient = (props: ApiClientProps, logger: Logger) => new ApiClient(props, logger)
public constructor(
props: ApiClientProps,
private _logger: Logger
) {
const { apiUrl, token, workspaceId, botId, extraHeaders = {} } = props
this.client = new client.Client({
apiUrl,
token,
workspaceId,
botId,
retry: retry.config,
headers: { 'x-multiple-integrations': 'true', ...extraHeaders },
})
this.url = apiUrl
this.token = token
this.workspaceId = workspaceId
this.botId = botId
this.extraHeaders = extraHeaders
}
public get isBotpressWorkspace(): boolean {
// this environment variable is undocumented and only used internally for dev purposes
const isBotpressWorkspace = yn(process.env.BP_IS_BOTPRESS_WORKSPACE)
if (isBotpressWorkspace !== undefined) {
return isBotpressWorkspace
}
return [
'6a76fa10-e150-4ff6-8f59-a300feec06c1',
'95de33eb-1551-4af9-9088-e5dcb02efd09',
'11111111-1111-1111-aaaa-111111111111',
].includes(this.workspaceId)
}
public async safeListTables(req: client.ClientInputs['listTables']): Promise<
| {
success: true
tables: client.ClientOutputs['listTables']['tables']
}
| {
success: false
error: Error
}
> {
try {
const result = await this.client.listTables(req)
return { success: true, tables: result.tables }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
return { success: false, error }
}
}
public async getWorkspace(): Promise<client.ClientOutputs['getWorkspace']> {
return this.client.getWorkspace({ id: this.workspaceId })
}
public async findWorkspaceByHandle(handle: string): Promise<client.ClientOutputs['getWorkspace'] | undefined> {
const { workspaces } = await this.client.listWorkspaces({ handle })
return workspaces[0] // There should be only one workspace with a given handle
}
public withExtraHeaders(headers: Record<string, string>): ApiClient {
return ApiClient.newClient(
{
apiUrl: this.url,
token: this.token,
workspaceId: this.workspaceId,
botId: this.botId,
extraHeaders: { ...this.extraHeaders, ...headers },
},
this._logger
)
}
public switchWorkspace(workspaceId: string): ApiClient {
return ApiClient.newClient(
{ apiUrl: this.url, token: this.token, workspaceId, extraHeaders: this.extraHeaders },
this._logger
)
}
public switchBot(botId: string): ApiClient {
return ApiClient.newClient(
{ apiUrl: this.url, token: this.token, botId, workspaceId: this.workspaceId, extraHeaders: this.extraHeaders },
this._logger
)
}
public async updateWorkspace(
props: utils.types.SafeOmit<client.ClientInputs['updateWorkspace'], 'id'>
): Promise<client.ClientOutputs['updateWorkspace']> {
return this.client.updateWorkspace({ id: this.workspaceId, ...props })
}
public async getPublicOrPrivateIntegration(ref: ApiPackageRef): Promise<PublicOrPrivateIntegration> {
const integration = await this.findPublicOrPrivateIntegration(ref)
if (!integration) {
throw new Error(`Integration "${formatPackageRef(ref)}" not found`)
}
return integration
}
public async findPublicOrPrivateIntegration(ref: ApiPackageRef): Promise<PublicOrPrivateIntegration | undefined> {
const formatted = formatPackageRef(ref)
const privateIntegration = await this.findPrivateIntegration(ref)
if (privateIntegration) {
this._logger.debug(`Found integration "${formatted}" in workspace`)
return privateIntegration
}
const publicIntegration = await this.findPublicIntegration(ref)
if (publicIntegration) {
this._logger.debug(`Found integration "${formatted}" in hub`)
return publicIntegration
}
return
}
public async findPrivateIntegration(ref: ApiPackageRef): Promise<PrivateIntegration | undefined> {
const { workspaceId } = this
if (ref.type === 'id') {
return this.client
.getIntegration(ref)
.then((r) => ({ ...r.integration, workspaceId }))
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
return this.client
.getIntegrationByName(ref)
.then((r) => ({ ...r.integration, workspaceId }))
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
public async findPublicIntegration(ref: ApiPackageRef): Promise<PublicOrUnlistedIntegration | undefined> {
if (ref.type === 'id') {
return this.client
.getPublicIntegrationById(ref)
.then((r) => ({ ...r.integration, visibility: r.integration.visibility as 'public' | 'unlisted' }) as const)
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
return this.client
.getPublicIntegration(ref)
.then((r) => ({ ...r.integration, visibility: r.integration.visibility as 'public' | 'unlisted' }) as const)
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
public async findPublicOrPrivateInterface(ref: ApiPackageRef): Promise<PublicOrPrivateInterface | undefined> {
const formatted = formatPackageRef(ref)
const privateInterface = await this.findPrivateInterface(ref)
if (privateInterface) {
this._logger.debug(`Found interface "${formatted}" in workspace`)
return privateInterface
}
const publicInterface = await this.findPublicInterface(ref)
if (publicInterface) {
this._logger.debug(`Found interface "${formatted}" in hub`)
return publicInterface
}
return
}
public async findPrivateInterface(ref: ApiPackageRef): Promise<PrivateInterface | undefined> {
const { workspaceId } = this
if (ref.type === 'id') {
return this.client
.getInterface(ref)
.then((r) => ({ ...r.interface, workspaceId }))
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
return this.client
.getInterfaceByName(ref)
.then((r) => ({ ...r.interface, workspaceId }))
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
public async getPublicInterface(ref: ApiPackageRef): Promise<PublicInterface> {
const intrface = await this.findPublicInterface(ref)
if (!intrface) {
throw new Error(`Interface "${formatPackageRef(ref)}" not found`)
}
return intrface
}
public async findPublicInterface(ref: ApiPackageRef): Promise<PublicInterface | undefined> {
if (ref.type === 'id') {
return this.client
.getPublicInterfaceById(ref)
.then((r) => ({ ...r.interface, public: true }) as const)
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
return this.client
.getPublicInterface(ref)
.then((r) => ({ ...r.interface, public: true }) as const)
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
public async findPublicPlugin(ref: ApiPackageRef): Promise<PublicPlugin | undefined> {
if (ref.type === 'id') {
return this.client
.getPublicPluginById(ref)
.then((r) => ({ ...r.plugin, public: true }) as const)
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
return this.client
.getPublicPlugin(ref)
.then((r) => ({ ...r.plugin, public: true }) as const)
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
public async getPublicOrPrivatePlugin(ref: ApiPackageRef): Promise<PublicOrPrivatePlugin> {
const plugin = await this.findPublicOrPrivatePlugin(ref)
if (!plugin) {
throw new Error(`Plugin "${formatPackageRef(ref)}" not found`)
}
return plugin
}
public async findPublicOrPrivatePlugin(ref: ApiPackageRef): Promise<PublicOrPrivatePlugin | undefined> {
const formatted = formatPackageRef(ref)
const privatePlugin = await this.findPrivatePlugin(ref)
if (privatePlugin) {
this._logger.debug(`Found plugin "${formatted}" in workspace`)
return privatePlugin
}
const publicPlugin = await this.findPublicPlugin(ref)
if (publicPlugin) {
this._logger.debug(`Found plugin "${formatted}" in hub`)
return publicPlugin
}
return
}
public async findPrivatePlugin(ref: ApiPackageRef): Promise<PrivatePlugin | undefined> {
const { workspaceId } = this
if (ref.type === 'id') {
return this.client
.getPlugin(ref)
.then((r) => ({ ...r.plugin, workspaceId }))
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
return this.client
.getPluginByName(ref)
.then((r) => ({ ...r.plugin, workspaceId }))
.catch(this._returnUndefinedOnError('ResourceNotFound'))
}
public async testLogin(): Promise<void> {
await this.client.listBots({})
}
public listAllPages = paging.listAllPages
public async findPreviousIntegrationVersion(ref: NamePackageRef): Promise<PublicOrPrivateIntegration | undefined> {
const isValidSemverVersion = semver.valid(ref.version)
// Sanity check (this should never happen):
if (!isValidSemverVersion) {
throw new errors.BotpressCLIError(`Invalid version "${ref.version}" for integration "${ref.name}"`)
}
return this.findPublicOrPrivateIntegration({ ...ref, version: `<${ref.version}` })
}
public async findBotByName(name: string): Promise<BotSummary | undefined> {
// api does not allow filtering bots by name
const allBots = await this.listAllPages(this.client.listBots, (r) => r.bots)
return allBots.find((b) => b.name === name)
}
private _returnUndefinedOnError =
(type: client.ApiError['type']) =>
(thrown: any): undefined => {
if (client.isApiError(thrown) && thrown.type === type) {
return
}
throw thrown
}
public async getOrGenerateShareableId(
botId: string,
integrationId: string,
integrationAlias: string
): Promise<string> {
const { shareableId, isExpired } = await this.client
.getIntegrationShareableId({
botId,
integrationId,
integrationInstanceAlias: integrationAlias,
})
.catch(() => ({ shareableId: undefined, isExpired: true }))
if (shareableId && !isExpired) {
return shareableId
}
const { shareableId: newShareableId } = await this.client.createIntegrationShareableId({
botId,
integrationId,
integrationInstanceAlias: integrationAlias,
})
return newShareableId
}
}
+6
View File
@@ -0,0 +1,6 @@
export * from './client'
export * from './types'
export * from './integration-body'
export * from './interface-body'
export * from './bot-body'
export * from './plugin-body'
+243
View File
@@ -0,0 +1,243 @@
import * as client from '@botpress/client'
import * as sdk from '@botpress/sdk'
import * as errors from '../errors'
import * as utils from '../utils'
import * as types from './types'
export const prepareCreateIntegrationBody = async (
integration: sdk.IntegrationDefinition
): Promise<types.CreateIntegrationRequestBody> => {
const base = `Failed to convert ZUI to JSON schema for integration ${integration.name}`
return {
name: integration.name,
version: integration.version,
title: 'title' in integration ? integration.title : undefined,
description: 'description' in integration ? integration.description : undefined,
user: integration.user,
events: integration.events
? await utils.records.mapValuesAsync(integration.events, async (event, eventName) => ({
...event,
schema: await utils.schema
.mapZodToJsonSchema(event, {
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`)
}),
}))
: undefined,
actions: integration.actions
? await utils.records.mapValuesAsync(integration.actions, async (action, actionName) => ({
...action,
input: {
...action.input,
schema: await utils.schema
.mapZodToJsonSchema(action.input, {
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`)
}),
},
output: {
...action.output,
schema: await utils.schema
.mapZodToJsonSchema(action.output, {
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`)
}),
},
}))
: undefined,
channels: integration.channels
? await utils.records.mapValuesAsync(integration.channels, async (channel, channelName) => ({
...channel,
messages: await utils.records.mapValuesAsync(channel.messages, async (message, messageName) => ({
...message,
schema: await utils.schema
.mapZodToJsonSchema(message, {
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(
thrown,
`${base} for channel ${channelName} for message ${messageName}`
)
}),
})),
}))
: undefined,
states: integration.states
? await utils.records.mapValuesAsync(integration.states, async (state, stateName) => ({
...state,
schema: await utils.schema
.mapZodToJsonSchema(state, {
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for state ${stateName}`)
}),
}))
: undefined,
entities: integration.entities
? await utils.records.mapValuesAsync(integration.entities, async (entity, entityName) => ({
...entity,
schema: await utils.schema
.mapZodToJsonSchema(entity, {
useLegacyZuiTransformer: integration.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: integration.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for entity ${entityName}`)
}),
}))
: undefined,
attributes: integration.attributes,
extraOperations: '__advanced' in integration ? integration.__advanced?.extraOperations : undefined,
}
}
type UpdateIntegrationChannelsBody = NonNullable<types.UpdateIntegrationRequestBody['channels']>
type UpdateIntegrationChannelBody = UpdateIntegrationChannelsBody[string]
type Channels = client.Integration['channels']
type Channel = client.Integration['channels'][string]
export const prepareUpdateIntegrationBody = (
localIntegration: types.UpdateIntegrationRequestBody,
remoteIntegration: client.Integration
): types.UpdateIntegrationRequestBody => {
const actions = utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localIntegration.actions, remoteIntegration.actions),
remoteItems: remoteIntegration.actions,
})
const events = utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localIntegration.events, remoteIntegration.events),
remoteItems: remoteIntegration.events,
})
const states = utils.records.setNullOnMissingValues(localIntegration.states, remoteIntegration.states)
const entities = utils.records.setNullOnMissingValues(localIntegration.entities, remoteIntegration.entities)
const user = {
...localIntegration.user,
tags: utils.records.setNullOnMissingValues(localIntegration.user?.tags, remoteIntegration.user?.tags),
}
const channels = _prepareUpdateIntegrationChannelsBody(localIntegration.channels ?? {}, remoteIntegration.channels)
const interfaces = utils.records.setNullOnMissingValues(localIntegration.interfaces, remoteIntegration.interfaces)
const configurations = utils.records.setNullOnMissingValues(
localIntegration.configurations,
remoteIntegration.configurations
)
const readme = localIntegration.readme
const icon = localIntegration.icon
const attributes = utils.records.setNullOnMissingValues(localIntegration.attributes, remoteIntegration.attributes)
const extraOperations = localIntegration.extraOperations
return {
..._maybeRemoveVrlScripts(localIntegration, remoteIntegration),
actions,
events,
states,
entities,
user,
channels,
interfaces,
configurations,
readme,
icon,
attributes,
extraOperations,
}
}
const _maybeRemoveVrlScripts = (
localIntegration: types.UpdateIntegrationRequestBody,
remoteIntegration: client.Integration
): types.UpdateIntegrationRequestBody => {
const newIntegration = structuredClone(localIntegration)
if (
remoteIntegration.configuration?.identifier?.linkTemplateScript &&
!localIntegration.configuration?.identifier?.linkTemplateScript
) {
newIntegration.configuration ??= remoteIntegration.configuration
newIntegration.configuration.identifier ??= remoteIntegration.configuration.identifier
newIntegration.configuration.identifier.linkTemplateScript = null
newIntegration.configuration.identifier.required = false
}
if (remoteIntegration.identifier.extractScript && !localIntegration.identifier?.extractScript) {
newIntegration.identifier ??= remoteIntegration.identifier
newIntegration.identifier.extractScript = null
}
if (remoteIntegration.identifier.fallbackHandlerScript && !localIntegration.identifier?.fallbackHandlerScript) {
newIntegration.identifier ??= remoteIntegration.identifier
newIntegration.identifier.fallbackHandlerScript = null
}
for (const configName of Object.keys(localIntegration.configurations ?? {})) {
if (
remoteIntegration.configurations[configName]?.identifier.linkTemplateScript &&
!localIntegration.configurations?.[configName]?.identifier?.linkTemplateScript
) {
newIntegration.configurations ??= remoteIntegration.configurations
newIntegration.configurations[configName] ??= remoteIntegration.configurations[configName]
newIntegration.configurations[configName].identifier ??= remoteIntegration.configurations[configName].identifier
newIntegration.configurations[configName].identifier.linkTemplateScript = null
newIntegration.configurations[configName].identifier.required = false
}
}
return newIntegration
}
const _prepareUpdateIntegrationChannelsBody = (
localChannels: UpdateIntegrationChannelsBody,
remoteChannels: Channels
): UpdateIntegrationChannelsBody => {
const channelBody: UpdateIntegrationChannelsBody = {}
const zipped = utils.records.zipObjects(localChannels, remoteChannels)
for (const [channelName, [localChannel, remoteChannel]] of Object.entries(zipped)) {
if (localChannel && remoteChannel) {
// channel has to be updated
channelBody[channelName] = _prepareUpdateIntegrationChannelBody(localChannel, remoteChannel)
} else if (localChannel) {
// channel has to be created
channelBody[channelName] = localChannel
continue
} else if (remoteChannel) {
// channel has to be deleted
channelBody[channelName] = null
continue
}
}
return channelBody
}
const _prepareUpdateIntegrationChannelBody = (
localChannel: UpdateIntegrationChannelBody,
remoteChannel: Channel
): UpdateIntegrationChannelBody => ({
...localChannel,
messages: utils.records.setNullOnMissingValues(localChannel?.messages, remoteChannel.messages),
message: {
...localChannel?.message,
tags: utils.records.setNullOnMissingValues(localChannel?.message?.tags, remoteChannel.message.tags),
},
conversation: {
...localChannel?.conversation,
tags: utils.records.setNullOnMissingValues(localChannel?.conversation?.tags, remoteChannel.conversation.tags),
},
})
+128
View File
@@ -0,0 +1,128 @@
import * as client from '@botpress/client'
import * as sdk from '@botpress/sdk'
import * as errors from '../errors'
import * as utils from '../utils'
import * as types from './types'
export const prepareCreateInterfaceBody = async (
intrface: sdk.InterfaceDefinition
): Promise<types.CreateInterfaceRequestBody> => {
const base = `Failed to convert ZUI to JSON schema for interface ${intrface.name}`
return {
name: intrface.name,
version: intrface.version,
title: 'title' in intrface ? intrface.title : undefined,
description: 'description' in intrface ? intrface.description : undefined,
entities: intrface.entities
? await utils.records.mapValuesAsync(intrface.entities, async (entity, entityName) => ({
...entity,
schema: await utils.schema
.mapZodToJsonSchema(entity, {
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for entity ${entityName}`)
}),
}))
: {},
events: intrface.events
? await utils.records.mapValuesAsync(intrface.events, async (event, eventName) => ({
...event,
schema: await utils.schema
.mapZodToJsonSchema(event, {
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`)
}),
}))
: {},
actions: intrface.actions
? await utils.records.mapValuesAsync(intrface.actions, async (action, actionName) => ({
...action,
input: {
...action.input,
schema: await utils.schema
.mapZodToJsonSchema(action.input, {
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`)
}),
},
output: {
...action.output,
schema: await utils.schema
.mapZodToJsonSchema(action.output, {
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`)
}),
},
}))
: {},
channels: intrface.channels
? await utils.records.mapValuesAsync(intrface.channels, async (channel, channelName) => ({
...channel,
messages: await utils.records.mapValuesAsync(channel.messages, async (message, messageName) => ({
...message,
schema: await utils.schema
.mapZodToJsonSchema(message, {
useLegacyZuiTransformer: intrface.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: intrface.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(
thrown,
`${base} for channel ${channelName} for message ${messageName}`
)
}),
})),
}))
: {},
attributes: intrface.attributes,
}
}
export const prepareUpdateInterfaceBody = (
localInterface: types.CreateInterfaceRequestBody & { id: string },
remoteInterface: client.Interface
): types.UpdateInterfaceRequestBody => {
const actions = utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localInterface.actions, remoteInterface.actions),
remoteItems: remoteInterface.actions,
})
const events = utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localInterface.events, remoteInterface.events),
remoteItems: remoteInterface.events,
})
const entities = utils.records.setNullOnMissingValues(localInterface.entities, remoteInterface.entities)
const currentChannels: types.UpdateInterfaceRequestBody['channels'] = localInterface.channels
? utils.records.mapValues(localInterface.channels, (channel, channelName) => ({
...channel,
messages: utils.records.setNullOnMissingValues(
channel?.messages,
remoteInterface.channels[channelName]?.messages
),
}))
: undefined
const channels = utils.records.setNullOnMissingValues(currentChannels, remoteInterface.channels)
const attributes = utils.records.setNullOnMissingValues(localInterface.attributes, remoteInterface.attributes)
return {
...localInterface,
entities,
actions,
events,
channels,
attributes,
}
}
+21
View File
@@ -0,0 +1,21 @@
export type PageLister<R extends object> = (t: { nextToken?: string }) => Promise<R & { meta: { nextToken?: string } }>
export async function listAllPages<R extends object>(lister: PageLister<R>): Promise<R[]>
export async function listAllPages<R extends object, M>(lister: PageLister<R>, mapper?: (r: R) => M[]): Promise<M[]>
export async function listAllPages<R extends object, M>(lister: PageLister<R>, mapper?: (r: R) => M[]) {
let nextToken: string | undefined
const all: R[] = []
do {
const { meta, ...r } = await lister({ nextToken })
all.push(r as R)
nextToken = meta.nextToken
} while (nextToken)
if (!mapper) {
return all
}
const mapped: M[] = all.flatMap((r) => mapper(r))
return mapped
}
+133
View File
@@ -0,0 +1,133 @@
import * as client from '@botpress/client'
import * as sdk from '@botpress/sdk'
import * as errors from '../errors'
import * as utils from '../utils'
import * as types from './types'
export const prepareCreatePluginBody = async (plugin: sdk.PluginDefinition): Promise<types.CreatePluginRequestBody> => {
const base = `Failed to convert ZUI to JSON schema for plugin ${plugin.name}`
return {
name: plugin.name,
version: plugin.version,
title: 'title' in plugin ? plugin.title : undefined,
description: 'description' in plugin ? plugin.description : undefined,
user: {
tags: plugin.user?.tags ?? {},
},
conversation: {
tags: plugin.conversation?.tags ?? {},
},
message: {
tags: plugin.message?.tags ?? {},
},
configuration: plugin.configuration
? {
...plugin.configuration,
schema: await utils.schema
.mapZodToJsonSchema(plugin.configuration, {
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for configuration`)
}),
}
: undefined,
events: plugin.events
? await utils.records.mapValuesAsync(plugin.events, async (event, eventName) => ({
...event,
schema: await utils.schema
.mapZodToJsonSchema(event, {
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for event ${eventName}`)
}),
}))
: undefined,
actions: plugin.actions
? await utils.records.mapValuesAsync(plugin.actions, async (action, actionName) => ({
...action,
input: {
...action.input,
schema: await utils.schema
.mapZodToJsonSchema(action.input, {
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} input`)
}),
},
output: {
...action.output,
schema: await utils.schema
.mapZodToJsonSchema(action.output, {
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for action ${actionName} output`)
}),
},
}))
: undefined,
states: plugin.states
? (utils.records.filterValues(
await utils.records.mapValuesAsync(plugin.states, async (state, stateName) => ({
...state,
schema: await utils.schema
.mapZodToJsonSchema(state, {
useLegacyZuiTransformer: plugin.__advanced?.useLegacyZuiTransformer,
toJSONSchemaOptions: plugin.__advanced?.toJSONSchemaOptions,
})
.catch((thrown) => {
throw errors.BotpressCLIError.wrap(thrown, `${base} for state ${stateName}`)
}),
})),
({ type }) => type !== 'workflow'
) as types.CreatePluginRequestBody['states'])
: undefined,
attributes: plugin.attributes,
}
}
export const prepareUpdatePluginBody = (
localPlugin: types.UpdatePluginRequestBody,
remotePlugin: client.Plugin
): types.UpdatePluginRequestBody => {
const actions = utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localPlugin.actions, remotePlugin.actions),
remoteItems: remotePlugin.actions,
})
const events = utils.attributes.prepareAttributeUpdateBody({
localItems: utils.records.setNullOnMissingValues(localPlugin.events, remotePlugin.events),
remoteItems: remotePlugin.events,
})
const states = utils.records.setNullOnMissingValues(localPlugin.states, remotePlugin.states)
const attributes = utils.records.setNullOnMissingValues(localPlugin.attributes, remotePlugin.attributes)
const dependencies: types.UpdatePluginRequestBody['dependencies'] = {
integrations: utils.records.setNullOnMissingValues(
localPlugin.dependencies?.integrations,
remotePlugin.dependencies?.integrations
),
interfaces: utils.records.setNullOnMissingValues(
localPlugin.dependencies?.interfaces,
remotePlugin.dependencies?.interfaces
),
}
// TODO: set null to conversation, user and message tags that are removed
return {
...localPlugin,
actions,
events,
states,
attributes,
dependencies,
}
}
+48
View File
@@ -0,0 +1,48 @@
import * as client from '@botpress/client'
// TODO: we probably shouldnt retry on 500 errors, but this is a temporary fix for the botpress repo CI
const HTTP_STATUS_TO_RETRY_ON = [429, 500, 502, 503, 504]
function getRetryAfterMs(error: Parameters<NonNullable<client.RetryConfig['retryDelay']>>[1]): number | undefined {
const headers = error?.response?.headers
if (!headers) return undefined
const headerNames = ['retry-after', 'ratelimit-reset', 'x-ratelimit-reset']
for (const name of headerNames) {
const raw = headers[name]
if (!raw) continue
const value = String(raw)
// HTTP-date format (e.g. "Mon, 28 Apr 2026 12:00:00 GMT")
if (value.includes(' ')) {
const futureDate = new Date(value)
if (!isNaN(futureDate.getTime())) {
return Math.max(0, futureDate.getTime() - Date.now())
}
continue
}
// Seconds format (e.g. "120")
const seconds = parseInt(value, 10)
if (!isNaN(seconds) && seconds >= 0) {
return seconds * 1000
}
}
return undefined
}
export const config: client.RetryConfig = {
retries: 3,
retryCondition: (err) =>
client.axiosRetry.isNetworkOrIdempotentRequestError(err) ||
HTTP_STATUS_TO_RETRY_ON.includes(err.response?.status ?? 0),
retryDelay: (retryCount, error) => {
if (error?.response?.status === 429) {
const retryAfterMs = getRetryAfterMs(error)
if (retryAfterMs !== undefined) {
return retryAfterMs
}
}
return Math.max(retryCount, 1) * 1000
},
}
+62
View File
@@ -0,0 +1,62 @@
import * as client from '@botpress/client'
import { Logger } from '../logger'
import { SafeOmit, Merge } from '../utils/type-utils'
import { ApiClient } from './client'
export type ApiClientProps = {
apiUrl: string
token: string
workspaceId: string
botId?: string
extraHeaders?: Record<string, string>
}
export type ApiClientFactory = {
newClient: (props: ApiClientProps, logger: Logger) => ApiClient
}
export type PublicOrUnlistedIntegration = client.Integration & { visibility: 'public' | 'unlisted' }
export type PrivateIntegration = client.Integration & { workspaceId: string }
export type PublicOrPrivateIntegration = client.Integration & { workspaceId?: string }
export type IntegrationSummary = (
| client.ClientOutputs['listIntegrations']
| client.ClientOutputs['listPublicIntegrations']
)['integrations'][number]
export type BotSummary = client.ClientOutputs['listBots']['bots'][number]
export type PublicInterface = client.Interface & { public: true }
export type PrivateInterface = client.Interface & { workspaceId: string }
export type PublicOrPrivateInterface = client.Interface & { workspaceId?: string }
export type PublicPlugin = client.Plugin & { public: true }
export type PrivatePlugin = client.Plugin & { workspaceId: string }
export type PublicOrPrivatePlugin = client.Plugin & { workspaceId?: string }
export type CreateBotRequestBody = client.ClientInputs['createBot']
export type UpdateBotRequestBody = client.ClientInputs['updateBot']
export type CreateIntegrationRequestBody = client.ClientInputs['createIntegration']
export type UpdateIntegrationRequestBody = client.ClientInputs['updateIntegration']
export type CreateInterfaceRequestBody = client.ClientInputs['createInterface']
export type UpdateInterfaceRequestBody = client.ClientInputs['updateInterface']
type PluginDependency = client.Plugin['dependencies']['integrations'][string]
export type CreatePluginRequestBody = Merge<
SafeOmit<client.ClientInputs['createPlugin'], 'code'>,
{
dependencies?: {
integrations?: Record<string, PluginDependency>
interfaces?: Record<string, PluginDependency>
}
}
>
export type UpdatePluginRequestBody = Merge<
client.ClientInputs['updatePlugin'],
{
dependencies?: {
integrations?: Record<string, PluginDependency | null>
interfaces?: Record<string, PluginDependency | null>
}
}
>