2
0

Introducing The Forge (#1072)

The Forge allows anyone to easily create their own Typebot Block.

Closes #380
This commit is contained in:
Baptiste Arnaud
2023-12-13 10:22:02 +01:00
committed by GitHub
parent c373108b55
commit 5e019bbb22
184 changed files with 42659 additions and 37411 deletions

View File

@@ -0,0 +1,133 @@
import { AuthDefinition, BlockDefinition, ActionDefinition } from './types'
import { z } from './zod'
export const variableStringSchema = z.custom<`{{${string}}}`>((val) =>
/^{{.+}}$/g.test(val as string)
)
export const createAuth = <A extends AuthDefinition>(authDefinition: A) =>
authDefinition
export const createBlock = <
I extends string,
A extends AuthDefinition,
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
): BlockDefinition<I, A, O> => blockDefinition
export const createAction = <
A extends AuthDefinition,
BaseOptions extends z.ZodObject<any>,
O extends z.ZodObject<any>
>(
actionDefinition: {
auth?: A
baseOptions?: BaseOptions
} & ActionDefinition<A, BaseOptions, O>
) => actionDefinition
export const parseBlockSchema = <
I extends string,
A extends AuthDefinition,
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
) => {
const options = z.discriminatedUnion('action', [
blockDefinition.options
? blockDefinition.options.extend({
credentialsId: z.string().optional(),
action: z.undefined(),
})
: z.object({
credentialsId: z.string().optional(),
action: z.undefined(),
}),
...blockDefinition.actions.map((action) =>
blockDefinition.options
? (blockDefinition.options
.extend({
credentialsId: z.string().optional(),
})
.extend({
action: z.literal(action.name),
})
.merge(action.options ?? z.object({})) as any)
: z
.object({
credentialsId: z.string().optional(),
})
.extend({
action: z.literal(action.name),
})
.merge(action.options ?? z.object({}))
),
])
return z.object({
id: z.string(),
outgoingEdgeId: z.string().optional(),
type: z.literal(blockDefinition.id),
options: options.optional(),
})
}
export const parseBlockCredentials = <
I extends string,
A extends AuthDefinition,
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
) => {
if (!blockDefinition.auth) throw new Error('Block has no auth definition')
return z.object({
id: z.string(),
type: z.literal(blockDefinition.id),
createdAt: z.date(),
workspaceId: z.string(),
name: z.string(),
iv: z.string(),
data: blockDefinition.auth.schema,
})
}
export const option = {
object: <T extends z.ZodRawShape>(schema: T) => z.object(schema),
literal: <T extends string>(value: T) => z.literal(value),
string: z.string().optional(),
enum: <T extends string>(values: readonly [T, ...T[]]) =>
z.enum(values).optional(),
number: z.number().or(variableStringSchema).optional(),
array: <T extends z.ZodTypeAny>(schema: T) => z.array(schema).optional(),
discriminatedUnion: <
T extends string,
J extends [
z.ZodDiscriminatedUnionOption<T>,
...z.ZodDiscriminatedUnionOption<T>[]
]
>(
field: T,
schemas: J
) =>
// @ts-expect-error
z.discriminatedUnion<T, J>(field, [
z.object({ [field]: z.undefined() }),
...schemas,
]),
saveResponseArray: <I extends readonly [string, ...string[]]>(items: I) =>
z
.array(
z.object({
item: z.enum(items).optional().layout({
placeholder: 'Select a response',
defaultValue: items[0],
}),
variableId: z.string().optional().layout({
input: 'variableDropdown',
}),
})
)
.optional(),
}
export type * from './types'

View File

@@ -0,0 +1,16 @@
{
"name": "@typebot.io/forge",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"zod": "3.22.4"
},
"devDependencies": {
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15"
}
}

View File

@@ -0,0 +1,5 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,133 @@
import { SVGProps } from 'react'
import { z } from './zod'
export type VariableStore = {
get: (variableId: string) => string | (string | null)[] | null | undefined
set: (variableId: string, value: unknown) => void
parse: (value: string) => string
}
export type LogsStore = {
add: (
log:
| string
| {
status: 'error' | 'success' | 'info'
description: string
details?: unknown
}
) => void
}
export type FunctionToExecute = {
args: Record<string, string | number | null>
content: string
}
export type ReadOnlyVariableStore = Omit<VariableStore, 'set'>
export type ActionDefinition<
A extends AuthDefinition,
BaseOptions extends z.ZodObject<any>,
Options extends z.ZodObject<any> = z.ZodObject<{}>
> = {
name: string
fetchers?: FetcherDefinition<A, z.infer<BaseOptions> & z.infer<Options>>[]
options?: Options
getSetVariableIds?: (options: z.infer<Options>) => string[]
run?: {
server?: (params: {
credentials: CredentialsFromAuthDef<A>
options: z.infer<BaseOptions> & z.infer<Options>
variables: VariableStore
logs: LogsStore
}) => Promise<void> | void
/**
* Used to stream a text bubble. Will only be used if the block following the integration block is a text bubble containing the variable returned by `getStreamVariableId`.
*/
stream?: {
getStreamVariableId: (options: z.infer<Options>) => string | undefined
run: (params: {
credentials: CredentialsFromAuthDef<A>
options: z.infer<BaseOptions> & z.infer<Options>
variables: ReadOnlyVariableStore
}) => Promise<ReadableStream<any> | undefined>
}
web?: {
displayEmbedBubble?: {
waitForEvent?: {
getSaveVariableId?: (
options: z.infer<BaseOptions> & z.infer<Options>
) => string | undefined
parseFunction: (params: {
options: z.infer<BaseOptions> & z.infer<Options>
}) => FunctionToExecute
}
parseInitFunction: (params: {
options: z.infer<BaseOptions> & z.infer<Options>
}) => FunctionToExecute
}
parseFunction?: (params: {
options: z.infer<BaseOptions> & z.infer<Options>
}) => FunctionToExecute
}
}
}
export type FetcherDefinition<A extends AuthDefinition, T = {}> = {
id: string
/**
* List of option keys to determine if the fetcher should be re-executed whenever these options are updated.
*/
dependencies: (keyof T)[]
fetch: (params: {
credentials: CredentialsFromAuthDef<A>
options: T
}) => Promise<(string | { label: string; value: string })[]>
}
export type AuthDefinition = {
type: 'encryptedCredentials'
name: string
schema: z.ZodObject<any>
}
export type CredentialsFromAuthDef<A extends AuthDefinition> = A extends {
type: 'encryptedCredentials'
schema: infer S extends z.ZodObject<any>
}
? z.infer<S>
: never
export type BlockDefinition<
Id extends string,
Auth extends AuthDefinition,
Options extends z.ZodObject<any>
> = {
id: Id
name: string
fullName?: string
/**
* Keywords used when searching for a block.
*/
tags?: string[]
LightLogo: (props: SVGProps<SVGSVGElement>) => JSX.Element
DarkLogo?: (props: SVGProps<SVGSVGElement>) => JSX.Element
docsUrl?: string
auth?: Auth
options?: Options | undefined
fetchers?: FetcherDefinition<Auth, Options>[]
isDisabledInPreview?: boolean
actions: ActionDefinition<Auth, Options>[]
}
export type FetchItemsParams<T> = T extends ActionDefinition<
infer A,
infer BaseOptions,
infer Options
>
? {
credentials: CredentialsFromAuthDef<A>
options: BaseOptions & Options
}
: never

View File

@@ -0,0 +1,48 @@
import { ZodArray, ZodDate, ZodOptional, ZodString, ZodTypeAny, z } from 'zod'
type OptionableZodType<T extends ZodTypeAny> = T | ZodOptional<T>
export interface ZodLayoutMetadata<
T extends ZodTypeAny,
TInferred = z.input<T> | z.output<T>
> {
accordion?: string
label?: string
input?: 'variableDropdown' | 'textarea' | 'password'
defaultValue?: T extends ZodDate ? string : TInferred
placeholder?: string
helperText?: string
direction?: 'row' | 'column'
isRequired?: boolean
withVariableButton?: boolean
fetcher?: T extends OptionableZodType<ZodString> ? string : never
itemLabel?: T extends OptionableZodType<ZodArray<any>> ? string : never
isOrdered?: T extends OptionableZodType<ZodArray<any>> ? boolean : never
isHidden?: boolean
moreInfoTooltip?: string
}
declare module 'zod' {
interface ZodType<Output, Def extends ZodTypeDef, Input = Output> {
layout<T extends ZodTypeAny>(this: T, metadata: ZodLayoutMetadata<T>): T
}
interface ZodTypeDef {
layout?: ZodLayoutMetadata<ZodTypeAny>
}
}
export const extendWithTypebotLayout = (zod: typeof z) => {
if (typeof zod.ZodType.prototype.layout !== 'undefined') {
return
}
zod.ZodType.prototype.layout = function (layout) {
const result = new (this as any).constructor({
...this._def,
layout,
})
return result
}
}

View File

@@ -0,0 +1,10 @@
import { z } from 'zod'
import {
extendWithTypebotLayout,
ZodLayoutMetadata,
} from './extendWithTypebotLayout'
extendWithTypebotLayout(z)
export { z }
export type { ZodLayoutMetadata }