✨ Introducing The Forge (#1072)
The Forge allows anyone to easily create their own Typebot Block. Closes #380
This commit is contained in:
133
packages/forge/core/index.ts
Normal file
133
packages/forge/core/index.ts
Normal 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'
|
||||
16
packages/forge/core/package.json
Normal file
16
packages/forge/core/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
5
packages/forge/core/tsconfig.json
Normal file
5
packages/forge/core/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
133
packages/forge/core/types.ts
Normal file
133
packages/forge/core/types.ts
Normal 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
|
||||
48
packages/forge/core/zod/extendWithTypebotLayout.ts
Normal file
48
packages/forge/core/zod/extendWithTypebotLayout.ts
Normal 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
|
||||
}
|
||||
}
|
||||
10
packages/forge/core/zod/index.ts
Normal file
10
packages/forge/core/zod/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
extendWithTypebotLayout,
|
||||
ZodLayoutMetadata,
|
||||
} from './extendWithTypebotLayout'
|
||||
|
||||
extendWithTypebotLayout(z)
|
||||
|
||||
export { z }
|
||||
export type { ZodLayoutMetadata }
|
||||
Reference in New Issue
Block a user