✨ Add NocoDB block (#1365)
#970 #997 Fully integrate NocoDB. Added all API Functions: - List Table Records - Create Table Records - Update Table Records - Delete Table Records - Read Table Record - Count Table Records - List Linked Records - Link Records - Unlink Records Optional Todo: - Save responses of non-get requests in a variable (error validation try-catch is added and logged so i do not think so it is much needed) You are free to implement any extra validation/function :D --------- Co-authored-by: Baptiste Arnaud <baptiste.arnaud95@gmail.com>
This commit is contained in:
committed by
GitHub
parent
3e4e7531f6
commit
a17781dfa6
@ -6,12 +6,14 @@ import { ZodObjectLayout } from './zodLayouts/ZodObjectLayout'
|
|||||||
import { ZodActionDiscriminatedUnion } from './zodLayouts/ZodActionDiscriminatedUnion'
|
import { ZodActionDiscriminatedUnion } from './zodLayouts/ZodActionDiscriminatedUnion'
|
||||||
import { useForgedBlock } from '../hooks/useForgedBlock'
|
import { useForgedBlock } from '../hooks/useForgedBlock'
|
||||||
import { ForgedBlock } from '@typebot.io/forge-repository/types'
|
import { ForgedBlock } from '@typebot.io/forge-repository/types'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: ForgedBlock
|
block: ForgedBlock
|
||||||
onOptionsChange: (options: BlockOptions) => void
|
onOptionsChange: (options: BlockOptions) => void
|
||||||
}
|
}
|
||||||
export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
|
export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
|
||||||
|
const [keySuffix, setKeySuffix] = useState<number>(0)
|
||||||
const { blockDef, blockSchema, actionDef } = useForgedBlock(
|
const { blockDef, blockSchema, actionDef } = useForgedBlock(
|
||||||
block.type,
|
block.type,
|
||||||
block.options?.action
|
block.options?.action
|
||||||
@ -32,7 +34,10 @@ export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
|
|||||||
const actionOptions = actionOptionsKeys.reduce(
|
const actionOptions = actionOptionsKeys.reduce(
|
||||||
(acc, key) => ({
|
(acc, key) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[key]: undefined,
|
[key]:
|
||||||
|
block.options[key] && typeof block.options[key] !== 'object'
|
||||||
|
? block.options[key]
|
||||||
|
: undefined,
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
@ -40,6 +45,7 @@ export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
|
|||||||
...updates,
|
...updates,
|
||||||
...actionOptions,
|
...actionOptions,
|
||||||
})
|
})
|
||||||
|
setKeySuffix((prev) => prev + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -85,6 +91,7 @@ export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ZodActionDiscriminatedUnion
|
<ZodActionDiscriminatedUnion
|
||||||
|
key={block.id + keySuffix}
|
||||||
schema={blockSchema.shape.options}
|
schema={blockSchema.shape.options}
|
||||||
blockDef={blockDef}
|
blockDef={blockDef}
|
||||||
blockOptions={block.options}
|
blockOptions={block.options}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NumberInput, TextInput, Textarea } from '@/components/inputs'
|
import { NumberInput, TextInput, Textarea } from '@/components/inputs'
|
||||||
import { z } from '@typebot.io/forge/zod'
|
import { z } from '@typebot.io/forge/zod'
|
||||||
import { ZodLayoutMetadata } from '@typebot.io/forge/zod'
|
import { ZodLayoutMetadata } from '@typebot.io/forge/zod'
|
||||||
|
import { evaluateIsHidden } from '@typebot.io/forge/zod/helpers/evaluateIsHidden'
|
||||||
import Markdown, { Components } from 'react-markdown'
|
import Markdown, { Components } from 'react-markdown'
|
||||||
import { ZodTypeAny } from 'zod'
|
import { ZodTypeAny } from 'zod'
|
||||||
import { ForgeSelectInput } from '../ForgeSelectInput'
|
import { ForgeSelectInput } from '../ForgeSelectInput'
|
||||||
@ -64,7 +65,7 @@ export const ZodFieldLayout = ({
|
|||||||
const innerSchema = getZodInnerSchema(schema)
|
const innerSchema = getZodInnerSchema(schema)
|
||||||
const layout = innerSchema._def.layout
|
const layout = innerSchema._def.layout
|
||||||
|
|
||||||
if (layout?.isHidden) return null
|
if (evaluateIsHidden(layout?.isHidden, blockOptions)) return null
|
||||||
|
|
||||||
switch (innerSchema._def.typeName) {
|
switch (innerSchema._def.typeName) {
|
||||||
case 'ZodObject':
|
case 'ZodObject':
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
ForgedBlock,
|
ForgedBlock,
|
||||||
} from '@typebot.io/forge-repository/types'
|
} from '@typebot.io/forge-repository/types'
|
||||||
import { getZodInnerSchema } from '../../helpers/getZodInnerSchema'
|
import { getZodInnerSchema } from '../../helpers/getZodInnerSchema'
|
||||||
|
import { evaluateIsHidden } from '@typebot.io/forge/zod/helpers/evaluateIsHidden'
|
||||||
|
|
||||||
export const ZodObjectLayout = ({
|
export const ZodObjectLayout = ({
|
||||||
schema,
|
schema,
|
||||||
@ -37,20 +38,23 @@ export const ZodObjectLayout = ({
|
|||||||
blockOptions?: ForgedBlock['options']
|
blockOptions?: ForgedBlock['options']
|
||||||
onDataChange: (value: any) => void
|
onDataChange: (value: any) => void
|
||||||
}): ReactNode[] => {
|
}): ReactNode[] => {
|
||||||
const layout = getZodInnerSchema(schema)._def.layout
|
const innerSchema = getZodInnerSchema(schema)
|
||||||
if (layout?.isHidden) return []
|
const shape =
|
||||||
return Object.keys(schema.shape).reduce<{
|
'shape' in innerSchema ? innerSchema.shape : innerSchema._def.shape()
|
||||||
|
const layout = innerSchema._def.layout
|
||||||
|
if (evaluateIsHidden(layout?.isHidden, blockOptions)) return []
|
||||||
|
return Object.keys(shape).reduce<{
|
||||||
nodes: ReactNode[]
|
nodes: ReactNode[]
|
||||||
accordionsCreated: string[]
|
accordionsCreated: string[]
|
||||||
}>(
|
}>(
|
||||||
(nodes, key, index) => {
|
(nodes, key, index) => {
|
||||||
if (ignoreKeys?.includes(key)) return nodes
|
if (ignoreKeys?.includes(key)) return nodes
|
||||||
const keySchema = getZodInnerSchema(schema.shape[key])
|
const keySchema = getZodInnerSchema(shape[key])
|
||||||
const layout = keySchema._def.layout as
|
const layout = keySchema._def.layout as
|
||||||
| ZodLayoutMetadata<ZodTypeAny>
|
| ZodLayoutMetadata<ZodTypeAny>
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
if (layout?.isHidden) return nodes
|
if (evaluateIsHidden(layout?.isHidden, blockOptions)) return nodes
|
||||||
if (
|
if (
|
||||||
layout &&
|
layout &&
|
||||||
layout.accordion &&
|
layout.accordion &&
|
||||||
@ -60,7 +64,7 @@ export const ZodObjectLayout = ({
|
|||||||
if (nodes.accordionsCreated.includes(layout.accordion)) return nodes
|
if (nodes.accordionsCreated.includes(layout.accordion)) return nodes
|
||||||
const accordionKeys = getObjectKeysWithSameAccordionAttr(
|
const accordionKeys = getObjectKeysWithSameAccordionAttr(
|
||||||
layout.accordion,
|
layout.accordion,
|
||||||
schema
|
shape
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
nodes: [
|
nodes: [
|
||||||
@ -77,7 +81,7 @@ export const ZodObjectLayout = ({
|
|||||||
{accordionKeys.map((accordionKey, idx) => (
|
{accordionKeys.map((accordionKey, idx) => (
|
||||||
<ZodFieldLayout
|
<ZodFieldLayout
|
||||||
key={accordionKey + idx}
|
key={accordionKey + idx}
|
||||||
schema={schema.shape[accordionKey]}
|
schema={shape[accordionKey]}
|
||||||
data={data?.[accordionKey]}
|
data={data?.[accordionKey]}
|
||||||
onDataChange={(val) =>
|
onDataChange={(val) =>
|
||||||
onDataChange({ ...data, [accordionKey]: val })
|
onDataChange({ ...data, [accordionKey]: val })
|
||||||
@ -118,12 +122,9 @@ export const ZodObjectLayout = ({
|
|||||||
).nodes
|
).nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
const getObjectKeysWithSameAccordionAttr = (
|
const getObjectKeysWithSameAccordionAttr = (accordion: string, shape: any) =>
|
||||||
accordion: string,
|
Object.keys(shape).reduce<string[]>((keys, currentKey) => {
|
||||||
schema: z.ZodObject<any>
|
const l = shape[currentKey]._def.layout as
|
||||||
) =>
|
|
||||||
Object.keys(schema.shape).reduce<string[]>((keys, currentKey) => {
|
|
||||||
const l = schema.shape[currentKey]._def.layout as
|
|
||||||
| ZodLayoutMetadata<ZodTypeAny>
|
| ZodLayoutMetadata<ZodTypeAny>
|
||||||
| undefined
|
| undefined
|
||||||
return !l?.accordion || l.accordion !== accordion
|
return !l?.accordion || l.accordion !== accordion
|
||||||
|
@ -2,11 +2,6 @@
|
|||||||
title: Anthropic
|
title: Anthropic
|
||||||
---
|
---
|
||||||
|
|
||||||
<Warning>
|
|
||||||
There is an ongoing issue with Anthropic block streaming capabilities. We are
|
|
||||||
working on a fix and will update this page once the issue is resolved.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
## Create Message
|
## Create Message
|
||||||
|
|
||||||
With the Anthropic block, you can create chat messages based on your user queries and display the answer back to your typebot using Claude AI.
|
With the Anthropic block, you can create chat messages based on your user queries and display the answer back to your typebot using Claude AI.
|
||||||
|
31
apps/docs/editor/blocks/integrations/nocodb.mdx
Normal file
31
apps/docs/editor/blocks/integrations/nocodb.mdx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: NocoDB
|
||||||
|
---
|
||||||
|
|
||||||
|
With the NocoDB block, you can create, update or get data from your NocoDB tables.
|
||||||
|
|
||||||
|
## How to find my `Table ID`?
|
||||||
|
|
||||||
|
To find your `Table ID`, you need to go to your NocoDB dashboard and click on the 3 dots button next to your table name.
|
||||||
|
|
||||||
|
<Frame>
|
||||||
|
<img
|
||||||
|
src="/images/blocks/integrations/nocodb-table-id.jpg"
|
||||||
|
alt="NocoDB table ID"
|
||||||
|
/>
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
## Search Records
|
||||||
|
|
||||||
|
This action allows you to search for existing records in a table. It requires your `Table ID` and can optionally take a `View ID` to search in a specific view.
|
||||||
|
|
||||||
|
<Frame>
|
||||||
|
<img
|
||||||
|
src="/images/blocks/integrations/nocodb.jpg"
|
||||||
|
alt="NocoDB block example"
|
||||||
|
/>
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
You can configure the filter to return `All`, `First`, `Last` or `Random` found records.
|
||||||
|
|
||||||
|
Then all you need to do is to map the found fields to variables that you can re-use on your bot.
|
BIN
apps/docs/images/blocks/integrations/nocodb-table-id.jpg
Normal file
BIN
apps/docs/images/blocks/integrations/nocodb-table-id.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
BIN
apps/docs/images/blocks/integrations/nocodb.jpg
Normal file
BIN
apps/docs/images/blocks/integrations/nocodb.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
@ -126,7 +126,8 @@
|
|||||||
"editor/blocks/integrations/mistral",
|
"editor/blocks/integrations/mistral",
|
||||||
"editor/blocks/integrations/elevenlabs",
|
"editor/blocks/integrations/elevenlabs",
|
||||||
"editor/blocks/integrations/anthropic",
|
"editor/blocks/integrations/anthropic",
|
||||||
"editor/blocks/integrations/dify-ai"
|
"editor/blocks/integrations/dify-ai",
|
||||||
|
"editor/blocks/integrations/nocodb"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -20532,6 +20532,242 @@
|
|||||||
"id",
|
"id",
|
||||||
"type"
|
"type"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"outgoingEdgeId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"nocodb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Search Records"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"viewId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"returnType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"All",
|
||||||
|
"First",
|
||||||
|
"Last",
|
||||||
|
"Random"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"comparisons": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Equal to",
|
||||||
|
"Not equal",
|
||||||
|
"Contains",
|
||||||
|
"Greater than",
|
||||||
|
"Less than",
|
||||||
|
"Is set",
|
||||||
|
"Is empty",
|
||||||
|
"Starts with",
|
||||||
|
"Ends with"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"joiner": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"AND",
|
||||||
|
"OR"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"comparisons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"responseMapping": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fieldName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"variableId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Create Record"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Update Existing Record"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"viewId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"comparisons": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Equal to",
|
||||||
|
"Not equal",
|
||||||
|
"Contains",
|
||||||
|
"Greater than",
|
||||||
|
"Less than",
|
||||||
|
"Is set",
|
||||||
|
"Is empty",
|
||||||
|
"Starts with",
|
||||||
|
"Ends with"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"joiner": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"AND",
|
||||||
|
"OR"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"comparisons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fieldName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Block"
|
"title": "Block"
|
||||||
|
@ -11513,6 +11513,242 @@
|
|||||||
"id",
|
"id",
|
||||||
"type"
|
"type"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"outgoingEdgeId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"nocodb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Search Records"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"viewId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"returnType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"All",
|
||||||
|
"First",
|
||||||
|
"Last",
|
||||||
|
"Random"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"comparisons": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Equal to",
|
||||||
|
"Not equal",
|
||||||
|
"Contains",
|
||||||
|
"Greater than",
|
||||||
|
"Less than",
|
||||||
|
"Is set",
|
||||||
|
"Is empty",
|
||||||
|
"Starts with",
|
||||||
|
"Ends with"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"joiner": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"AND",
|
||||||
|
"OR"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"comparisons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"responseMapping": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fieldName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"variableId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Create Record"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"credentialsId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Update Existing Record"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tableId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"viewId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"comparisons": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Equal to",
|
||||||
|
"Not equal",
|
||||||
|
"Contains",
|
||||||
|
"Greater than",
|
||||||
|
"Less than",
|
||||||
|
"Is set",
|
||||||
|
"Is empty",
|
||||||
|
"Starts with",
|
||||||
|
"Ends with"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"joiner": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"AND",
|
||||||
|
"OR"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"comparisons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fieldName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"action"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Block"
|
"title": "Block"
|
||||||
|
@ -29,9 +29,20 @@ export const createChatMessage = createAction({
|
|||||||
moreInfoTooltip:
|
moreInfoTooltip:
|
||||||
'The user identifier, defined by the developer, must ensure uniqueness within the app.',
|
'The user identifier, defined by the developer, must ensure uniqueness within the app.',
|
||||||
}),
|
}),
|
||||||
inputs: option.keyValueList.layout({
|
inputs: option
|
||||||
accordion: 'Inputs',
|
.array(
|
||||||
}),
|
option.object({
|
||||||
|
key: option.string.layout({
|
||||||
|
label: 'Key',
|
||||||
|
}),
|
||||||
|
value: option.string.layout({
|
||||||
|
label: 'Value',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.layout({
|
||||||
|
accordion: 'Inputs',
|
||||||
|
}),
|
||||||
responseMapping: option
|
responseMapping: option
|
||||||
.saveResponseArray(
|
.saveResponseArray(
|
||||||
['Answer', 'Conversation ID', 'Total Tokens'] as const,
|
['Answer', 'Conversation ID', 'Total Tokens'] as const,
|
||||||
|
61
packages/forge/blocks/nocodb/actions/createRecord.ts
Normal file
61
packages/forge/blocks/nocodb/actions/createRecord.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { createAction, option } from '@typebot.io/forge'
|
||||||
|
import { auth } from '../auth'
|
||||||
|
import ky, { HTTPError } from 'ky'
|
||||||
|
import { defaultBaseUrl } from '../constants'
|
||||||
|
import { parseRecordsCreateBody } from '../helpers/parseRecordCreateBody'
|
||||||
|
|
||||||
|
export const createRecord = createAction({
|
||||||
|
auth,
|
||||||
|
name: 'Create Record',
|
||||||
|
options: option.object({
|
||||||
|
tableId: option.string.layout({
|
||||||
|
label: 'Table ID',
|
||||||
|
isRequired: true,
|
||||||
|
helperText: 'Identifier of the table to create records in.',
|
||||||
|
}),
|
||||||
|
fields: option
|
||||||
|
.array(
|
||||||
|
option.object({
|
||||||
|
key: option.string.layout({
|
||||||
|
label: 'Field',
|
||||||
|
isRequired: true,
|
||||||
|
}),
|
||||||
|
value: option.string.layout({
|
||||||
|
label: 'Value',
|
||||||
|
isRequired: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.layout({
|
||||||
|
itemLabel: 'field',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
run: {
|
||||||
|
server: async ({
|
||||||
|
credentials: { baseUrl, apiKey },
|
||||||
|
options: { tableId, fields },
|
||||||
|
logs,
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
if (!fields || fields.length === 0) return
|
||||||
|
await ky.post(
|
||||||
|
`${baseUrl ?? defaultBaseUrl}/api/v2/tables/${tableId}/records`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'xc-token': apiKey,
|
||||||
|
},
|
||||||
|
json: parseRecordsCreateBody(fields),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HTTPError)
|
||||||
|
return logs.add({
|
||||||
|
status: 'error',
|
||||||
|
description: error.message,
|
||||||
|
details: await error.response.text(),
|
||||||
|
})
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
136
packages/forge/blocks/nocodb/actions/searchRecords.ts
Normal file
136
packages/forge/blocks/nocodb/actions/searchRecords.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { createAction, option } from '@typebot.io/forge'
|
||||||
|
import { auth } from '../auth'
|
||||||
|
import ky, { HTTPError } from 'ky'
|
||||||
|
import { ListTableRecordsResponse } from '../types'
|
||||||
|
import { isDefined } from '@typebot.io/lib'
|
||||||
|
import { parseSearchParams } from '../helpers/parseSearchParams'
|
||||||
|
import { convertFilterToWhereClause } from '../helpers/convertFilterToWhereClause'
|
||||||
|
import {
|
||||||
|
defaultBaseUrl,
|
||||||
|
defaultLimitForSearch,
|
||||||
|
filterOperators,
|
||||||
|
} from '../constants'
|
||||||
|
|
||||||
|
export const searchRecords = createAction({
|
||||||
|
auth,
|
||||||
|
name: 'Search Records',
|
||||||
|
options: option.object({
|
||||||
|
tableId: option.string.layout({
|
||||||
|
label: 'Table ID',
|
||||||
|
moreInfoTooltip:
|
||||||
|
'Can be found by clicking on the 3 dots next to the table name.',
|
||||||
|
isRequired: true,
|
||||||
|
}),
|
||||||
|
viewId: option.string.layout({
|
||||||
|
label: 'View ID',
|
||||||
|
moreInfoTooltip:
|
||||||
|
'Can be found by clicking on the 3 dots next to the view name.',
|
||||||
|
}),
|
||||||
|
returnType: option.enum(['All', 'First', 'Last', 'Random']).layout({
|
||||||
|
accordion: 'Filter',
|
||||||
|
defaultValue: 'All',
|
||||||
|
}),
|
||||||
|
filter: option
|
||||||
|
.filter({
|
||||||
|
operators: filterOperators,
|
||||||
|
isJoinerHidden: ({ filter }) =>
|
||||||
|
!filter?.comparisons || filter.comparisons.length < 2,
|
||||||
|
})
|
||||||
|
.layout({
|
||||||
|
accordion: 'Filter',
|
||||||
|
}),
|
||||||
|
responseMapping: option
|
||||||
|
.array(
|
||||||
|
option.object({
|
||||||
|
fieldName: option.string.layout({
|
||||||
|
label: 'Enter a field name',
|
||||||
|
}),
|
||||||
|
variableId: option.string.layout({
|
||||||
|
inputType: 'variableDropdown',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.layout({
|
||||||
|
accordion: 'Response Mapping',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getSetVariableIds: ({ responseMapping }) =>
|
||||||
|
responseMapping?.map((r) => r.variableId).filter(isDefined) ?? [],
|
||||||
|
run: {
|
||||||
|
server: async ({
|
||||||
|
credentials: { baseUrl, apiKey },
|
||||||
|
options: { tableId, responseMapping, filter, returnType, viewId },
|
||||||
|
variables,
|
||||||
|
logs,
|
||||||
|
}) => {
|
||||||
|
if (!apiKey) return logs.add('API key is required')
|
||||||
|
try {
|
||||||
|
const data = await ky
|
||||||
|
.get(
|
||||||
|
`${baseUrl ?? defaultBaseUrl}/api/v2/tables/${tableId}/records`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'xc-token': apiKey,
|
||||||
|
},
|
||||||
|
searchParams: parseSearchParams({
|
||||||
|
where: convertFilterToWhereClause(filter),
|
||||||
|
viewId,
|
||||||
|
limit: defaultLimitForSearch,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.json<ListTableRecordsResponse>()
|
||||||
|
|
||||||
|
let filterIndex: number | undefined = undefined
|
||||||
|
|
||||||
|
if (returnType && returnType !== 'All') {
|
||||||
|
const total = data.pageInfo.totalRows
|
||||||
|
if (returnType === 'First') {
|
||||||
|
filterIndex = 0
|
||||||
|
} else if (returnType === 'Last') {
|
||||||
|
filterIndex = total - 1
|
||||||
|
} else if (returnType === 'Random') {
|
||||||
|
filterIndex = Math.floor(Math.random() * total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredList =
|
||||||
|
isDefined(filterIndex) && data.list[filterIndex]
|
||||||
|
? [data.list[filterIndex]]
|
||||||
|
: data.list
|
||||||
|
|
||||||
|
if (filteredList.length === 0)
|
||||||
|
return logs.add({
|
||||||
|
status: 'info',
|
||||||
|
description: `Couldn't find any rows matching the filter`,
|
||||||
|
details: JSON.stringify(filter, null, 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
responseMapping?.forEach((mapping) => {
|
||||||
|
if (!mapping.fieldName || !mapping.variableId) return
|
||||||
|
if (!filteredList[0][mapping.fieldName]) {
|
||||||
|
logs.add(`Field ${mapping.fieldName} does not exist in the table`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = filteredList.map(
|
||||||
|
(item) => item[mapping.fieldName as string]
|
||||||
|
)
|
||||||
|
|
||||||
|
variables.set(
|
||||||
|
mapping.variableId,
|
||||||
|
items.length === 1 ? items[0] : items
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HTTPError)
|
||||||
|
return logs.add({
|
||||||
|
status: 'error',
|
||||||
|
description: error.message,
|
||||||
|
details: await error.response.text(),
|
||||||
|
})
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
103
packages/forge/blocks/nocodb/actions/updateExistingRecord.ts
Normal file
103
packages/forge/blocks/nocodb/actions/updateExistingRecord.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { createAction, option } from '@typebot.io/forge'
|
||||||
|
import { auth } from '../auth'
|
||||||
|
import ky, { HTTPError } from 'ky'
|
||||||
|
import { parseRecordsUpdateBody } from '../helpers/parseRecordsUpdateBody'
|
||||||
|
import {
|
||||||
|
defaultBaseUrl,
|
||||||
|
defaultLimitForSearch,
|
||||||
|
filterOperators,
|
||||||
|
} from '../constants'
|
||||||
|
import { parseSearchParams } from '../helpers/parseSearchParams'
|
||||||
|
import { convertFilterToWhereClause } from '../helpers/convertFilterToWhereClause'
|
||||||
|
import { ListTableRecordsResponse } from '../types'
|
||||||
|
|
||||||
|
export const updateExistingRecord = createAction({
|
||||||
|
auth,
|
||||||
|
name: 'Update Existing Record',
|
||||||
|
options: option.object({
|
||||||
|
tableId: option.string.layout({
|
||||||
|
label: 'Table ID',
|
||||||
|
isRequired: true,
|
||||||
|
moreInfoTooltip:
|
||||||
|
'Can be found by clicking on the 3 dots next to the table name.',
|
||||||
|
}),
|
||||||
|
viewId: option.string.layout({
|
||||||
|
label: 'View ID',
|
||||||
|
moreInfoTooltip:
|
||||||
|
'Can be found by clicking on the 3 dots next to the view name.',
|
||||||
|
}),
|
||||||
|
filter: option
|
||||||
|
.filter({
|
||||||
|
operators: filterOperators,
|
||||||
|
isJoinerHidden: ({ filter }) =>
|
||||||
|
!filter?.comparisons || filter.comparisons.length < 2,
|
||||||
|
})
|
||||||
|
.layout({
|
||||||
|
accordion: 'Select Records',
|
||||||
|
}),
|
||||||
|
updates: option
|
||||||
|
.array(
|
||||||
|
option.object({
|
||||||
|
fieldName: option.string.layout({
|
||||||
|
label: 'Enter a field name',
|
||||||
|
}),
|
||||||
|
value: option.string.layout({
|
||||||
|
placeholder: 'Enter a value',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.layout({
|
||||||
|
accordion: 'Updates',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
run: {
|
||||||
|
server: async ({
|
||||||
|
credentials: { baseUrl, apiKey },
|
||||||
|
options: { tableId, filter, viewId, updates },
|
||||||
|
logs,
|
||||||
|
}) => {
|
||||||
|
if (!apiKey) return logs.add('API key is required')
|
||||||
|
if (!updates) return logs.add('At least one update is required')
|
||||||
|
if (!filter?.comparisons || filter.comparisons.length === 0)
|
||||||
|
return logs.add('At least one filter is required')
|
||||||
|
try {
|
||||||
|
const listData = await ky
|
||||||
|
.get(
|
||||||
|
`${baseUrl ?? defaultBaseUrl}/api/v2/tables/${tableId}/records`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'xc-token': apiKey,
|
||||||
|
},
|
||||||
|
searchParams: parseSearchParams({
|
||||||
|
where: convertFilterToWhereClause(filter),
|
||||||
|
viewId,
|
||||||
|
limit: defaultLimitForSearch,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.json<ListTableRecordsResponse>()
|
||||||
|
|
||||||
|
await ky.patch(
|
||||||
|
`${baseUrl ?? defaultBaseUrl}/api/v2/tables/${tableId}/records`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'xc-token': apiKey,
|
||||||
|
},
|
||||||
|
json: parseRecordsUpdateBody(
|
||||||
|
listData.list.map((item) => item.Id),
|
||||||
|
updates
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HTTPError)
|
||||||
|
return logs.add({
|
||||||
|
status: 'error',
|
||||||
|
description: error.message,
|
||||||
|
details: await error.response.text(),
|
||||||
|
})
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
24
packages/forge/blocks/nocodb/auth.ts
Normal file
24
packages/forge/blocks/nocodb/auth.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { option, AuthDefinition } from '@typebot.io/forge'
|
||||||
|
import { defaultBaseUrl } from './constants'
|
||||||
|
|
||||||
|
export const auth = {
|
||||||
|
type: 'encryptedCredentials',
|
||||||
|
name: 'NocoDB account',
|
||||||
|
schema: option.object({
|
||||||
|
baseUrl: option.string.layout({
|
||||||
|
label: 'Base URL',
|
||||||
|
isRequired: true,
|
||||||
|
helperText: 'Change it only if you are self-hosting NocoDB.',
|
||||||
|
withVariableButton: false,
|
||||||
|
defaultValue: defaultBaseUrl,
|
||||||
|
}),
|
||||||
|
apiKey: option.string.layout({
|
||||||
|
label: 'API Token',
|
||||||
|
isRequired: true,
|
||||||
|
helperText:
|
||||||
|
'You can generate an API token [here](https://app.nocodb.com/#/account/tokens)',
|
||||||
|
inputType: 'password',
|
||||||
|
withVariableButton: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} satisfies AuthDefinition
|
15
packages/forge/blocks/nocodb/constants.ts
Normal file
15
packages/forge/blocks/nocodb/constants.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const defaultBaseUrl = 'https://app.nocodb.com'
|
||||||
|
|
||||||
|
export const filterOperators = [
|
||||||
|
'Equal to',
|
||||||
|
'Not equal',
|
||||||
|
'Contains',
|
||||||
|
'Greater than',
|
||||||
|
'Less than',
|
||||||
|
'Is set',
|
||||||
|
'Is empty',
|
||||||
|
'Starts with',
|
||||||
|
'Ends with',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const defaultLimitForSearch = 1000
|
@ -0,0 +1,50 @@
|
|||||||
|
// See `where`: https://docs.nocodb.com/0.109.7/developer-resources/rest-apis/#query-params
|
||||||
|
// Example: (colName,eq,colValue)~or(colName2,gt,colValue2)
|
||||||
|
|
||||||
|
import { isEmpty } from '@typebot.io/lib'
|
||||||
|
|
||||||
|
export const convertFilterToWhereClause = (
|
||||||
|
filter:
|
||||||
|
| {
|
||||||
|
comparisons: {
|
||||||
|
input?: string
|
||||||
|
operator?: string
|
||||||
|
value?: string
|
||||||
|
}[]
|
||||||
|
joiner?: 'AND' | 'OR'
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
): string | undefined => {
|
||||||
|
if (!filter || !filter.comparisons || filter.comparisons.length === 0) return
|
||||||
|
|
||||||
|
const where = filter.comparisons
|
||||||
|
.map((comparison) => {
|
||||||
|
if (!comparison.value) return ''
|
||||||
|
|
||||||
|
switch (comparison.operator) {
|
||||||
|
case 'Not equal':
|
||||||
|
return `(${comparison.input},ne,${comparison.value})`
|
||||||
|
case 'Contains':
|
||||||
|
return `(${comparison.input},like,%${comparison.value}%)`
|
||||||
|
case 'Greater than':
|
||||||
|
return `(${comparison.input},gt,${comparison.value})`
|
||||||
|
case 'Less than':
|
||||||
|
return `(${comparison.input},lt,${comparison.value})`
|
||||||
|
case 'Is set':
|
||||||
|
return `(${comparison.input},isnot,null)`
|
||||||
|
case 'Is empty':
|
||||||
|
return `(${comparison.input},is,null)`
|
||||||
|
case 'Starts with':
|
||||||
|
return `(${comparison.input},like,${comparison.value}%)`
|
||||||
|
case 'Ends with':
|
||||||
|
return `(${comparison.input},like,%${comparison.value})`
|
||||||
|
default:
|
||||||
|
return `(${comparison.input},eq,${comparison.value})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('~' + (filter.joiner === 'OR' ? 'or' : 'and'))
|
||||||
|
|
||||||
|
if (isEmpty(where)) return
|
||||||
|
return where
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
export const parseRecordsCreateBody = (
|
||||||
|
fields: { key?: string; value?: string }[]
|
||||||
|
): Record<string, any> => {
|
||||||
|
const record: Record<string, any> = {}
|
||||||
|
|
||||||
|
fields.forEach(({ key, value }) => {
|
||||||
|
if (!key || !value) return
|
||||||
|
record[key] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
export const parseRecordsUpdateBody = (
|
||||||
|
ids: string[],
|
||||||
|
updates: { fieldName?: string; value?: string }[]
|
||||||
|
): Record<string, any>[] =>
|
||||||
|
ids.map((id) => {
|
||||||
|
const record: Record<string, any> = {
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.forEach(({ fieldName, value }) => {
|
||||||
|
if (!fieldName) return
|
||||||
|
record[fieldName] = value ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
return record
|
||||||
|
})
|
@ -0,0 +1,8 @@
|
|||||||
|
export const parseSearchParams = (
|
||||||
|
records: Record<string, any>
|
||||||
|
): Record<string, string> => {
|
||||||
|
return Object.entries(records).reduce((acc, [key, value]) => {
|
||||||
|
if (value === null || value === undefined) return acc
|
||||||
|
return { ...acc, [key]: value.toString() }
|
||||||
|
}, {})
|
||||||
|
}
|
16
packages/forge/blocks/nocodb/index.ts
Normal file
16
packages/forge/blocks/nocodb/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createBlock } from '@typebot.io/forge'
|
||||||
|
import { NocodbLogo } from './logo'
|
||||||
|
import { auth } from './auth'
|
||||||
|
import { searchRecords } from './actions/searchRecords'
|
||||||
|
import { createRecord } from './actions/createRecord'
|
||||||
|
import { updateExistingRecord } from './actions/updateExistingRecord'
|
||||||
|
|
||||||
|
export const nocodbBlock = createBlock({
|
||||||
|
id: 'nocodb',
|
||||||
|
name: 'NocoDB',
|
||||||
|
docsUrl: 'https://docs.typebot.io/forge/blocks/nocodb',
|
||||||
|
tags: ['database'],
|
||||||
|
LightLogo: NocodbLogo,
|
||||||
|
auth,
|
||||||
|
actions: [searchRecords, createRecord, updateExistingRecord],
|
||||||
|
})
|
34
packages/forge/blocks/nocodb/logo.tsx
Normal file
34
packages/forge/blocks/nocodb/logo.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/** @jsxImportSource react */
|
||||||
|
|
||||||
|
export const NocodbLogo = (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
rx="4"
|
||||||
|
fill="url(#paint0_linear_1871_109536)"
|
||||||
|
></rect>
|
||||||
|
<path
|
||||||
|
d="M8.3335 15.1562L12.0047 18.8297V24.6454H8.3335V15.1562ZM23.7533 7.34649V24.0464C23.7533 24.3894 23.4738 24.6665 23.1309 24.6665C22.9665 24.6665 22.8092 24.6031 22.6917 24.4857L8.3335 11.5367V7.8726C8.3335 7.52968 8.61066 7.25253 8.95359 7.25253H8.98648C9.1509 7.25253 9.3106 7.31831 9.42569 7.4334L20.0798 16.6783V7.34649H23.7533Z"
|
||||||
|
fill="white"
|
||||||
|
></path>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_1871_109536"
|
||||||
|
x1="15.9976"
|
||||||
|
y1="42.7731"
|
||||||
|
x2="15.9976"
|
||||||
|
y2="-8.9707"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#4351E8"></stop>
|
||||||
|
<stop offset="1" stopColor="#2A1EA5"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
18
packages/forge/blocks/nocodb/package.json
Normal file
18
packages/forge/blocks/nocodb/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@typebot.io/nocodb-block",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.ts",
|
||||||
|
"keywords": [],
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"devDependencies": {
|
||||||
|
"@typebot.io/forge": "workspace:*",
|
||||||
|
"@typebot.io/lib": "workspace:*",
|
||||||
|
"@typebot.io/tsconfig": "workspace:*",
|
||||||
|
"@types/react": "18.2.15",
|
||||||
|
"typescript": "5.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ky": "1.2.3"
|
||||||
|
}
|
||||||
|
}
|
6
packages/forge/blocks/nocodb/schemas.ts
Normal file
6
packages/forge/blocks/nocodb/schemas.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Do not edit this file manually
|
||||||
|
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
|
||||||
|
import { nocodbBlock } from '.'
|
||||||
|
|
||||||
|
export const nocodbBlockSchema = parseBlockSchema(nocodbBlock)
|
||||||
|
export const nocodbCredentialsSchema = parseBlockCredentials(nocodbBlock)
|
11
packages/forge/blocks/nocodb/tsconfig.json
Normal file
11
packages/forge/blocks/nocodb/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "@typebot.io/tsconfig/base.json",
|
||||||
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
}
|
||||||
|
}
|
10
packages/forge/blocks/nocodb/types.ts
Normal file
10
packages/forge/blocks/nocodb/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type ListTableRecordsResponse = {
|
||||||
|
list: Array<Record<string, any>>
|
||||||
|
pageInfo: {
|
||||||
|
totalRows: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
isFirstPage: boolean
|
||||||
|
isLastPage: boolean
|
||||||
|
}
|
||||||
|
}
|
@ -110,18 +110,6 @@ export const option = {
|
|||||||
z.enum(values).optional(),
|
z.enum(values).optional(),
|
||||||
number: z.number().or(variableStringSchema).optional(),
|
number: z.number().or(variableStringSchema).optional(),
|
||||||
array: <T extends z.ZodTypeAny>(schema: T) => z.array(schema).optional(),
|
array: <T extends z.ZodTypeAny>(schema: T) => z.array(schema).optional(),
|
||||||
keyValueList: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
key: z.string().optional().layout({
|
|
||||||
label: 'Key',
|
|
||||||
}),
|
|
||||||
value: z.string().optional().layout({
|
|
||||||
label: 'Value',
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
discriminatedUnion: <
|
discriminatedUnion: <
|
||||||
T extends string,
|
T extends string,
|
||||||
J extends [
|
J extends [
|
||||||
@ -165,6 +153,49 @@ export const option = {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
filter: ({
|
||||||
|
operators = defaultFilterOperators,
|
||||||
|
isJoinerHidden,
|
||||||
|
}: {
|
||||||
|
operators?: readonly [string, ...string[]]
|
||||||
|
isJoinerHidden: (currentObj: Record<string, any>) => boolean
|
||||||
|
}) =>
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
comparisons: z.array(
|
||||||
|
z.object({
|
||||||
|
input: z.string().optional().layout({ label: 'Enter a field ' }),
|
||||||
|
operator: z
|
||||||
|
.enum(operators)
|
||||||
|
.optional()
|
||||||
|
.layout({ defaultValue: 'Equal to' }),
|
||||||
|
value: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.layout({ placeholder: 'Enter a value' }),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
joiner: z.enum(['AND', 'OR']).optional().layout({
|
||||||
|
placeholder: 'Select joiner',
|
||||||
|
isHidden: isJoinerHidden,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultFilterOperators = [
|
||||||
|
'Equal to',
|
||||||
|
'Not equal',
|
||||||
|
'Contains',
|
||||||
|
'Does not contain',
|
||||||
|
'Greater than',
|
||||||
|
'Less than',
|
||||||
|
'Is set',
|
||||||
|
'Is empty',
|
||||||
|
'Starts with',
|
||||||
|
'Ends with',
|
||||||
|
'Matches regex',
|
||||||
|
'Does not match regex',
|
||||||
|
] as const
|
||||||
|
|
||||||
export type * from './types'
|
export type * from './types'
|
||||||
|
@ -20,7 +20,7 @@ export interface ZodLayoutMetadata<
|
|||||||
itemLabel?: T extends OptionableZodType<ZodArray<any>> ? string : never
|
itemLabel?: T extends OptionableZodType<ZodArray<any>> ? string : never
|
||||||
isOrdered?: T extends OptionableZodType<ZodArray<any>> ? boolean : never
|
isOrdered?: T extends OptionableZodType<ZodArray<any>> ? boolean : never
|
||||||
moreInfoTooltip?: string
|
moreInfoTooltip?: string
|
||||||
isHidden?: boolean
|
isHidden?: boolean | ((currentObj: Record<string, any>) => boolean)
|
||||||
isDebounceDisabled?: boolean
|
isDebounceDisabled?: boolean
|
||||||
hiddenItems?: string[]
|
hiddenItems?: string[]
|
||||||
}
|
}
|
||||||
|
9
packages/forge/core/zod/helpers/evaluateIsHidden.ts
Normal file
9
packages/forge/core/zod/helpers/evaluateIsHidden.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const evaluateIsHidden = (
|
||||||
|
isHidden: boolean | ((obj: any) => boolean) | undefined,
|
||||||
|
obj: any
|
||||||
|
): boolean => {
|
||||||
|
if (typeof isHidden === 'function') {
|
||||||
|
return isHidden(obj)
|
||||||
|
}
|
||||||
|
return isHidden ?? false
|
||||||
|
}
|
@ -13,4 +13,5 @@ export const forgedBlockIds = [
|
|||||||
'anthropic',
|
'anthropic',
|
||||||
'together-ai',
|
'together-ai',
|
||||||
'open-router',
|
'open-router',
|
||||||
|
'nocodb',
|
||||||
] as const satisfies ForgedBlock['type'][]
|
] as const satisfies ForgedBlock['type'][]
|
||||||
|
@ -20,6 +20,8 @@ import { togetherAiBlock } from '@typebot.io/together-ai-block'
|
|||||||
import { togetherAiCredentialsSchema } from '@typebot.io/together-ai-block/schemas'
|
import { togetherAiCredentialsSchema } from '@typebot.io/together-ai-block/schemas'
|
||||||
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
|
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
|
||||||
import { zemanticAiCredentialsSchema } from '@typebot.io/zemantic-ai-block/schemas'
|
import { zemanticAiCredentialsSchema } from '@typebot.io/zemantic-ai-block/schemas'
|
||||||
|
import { nocodbBlock } from '@typebot.io/nocodb-block'
|
||||||
|
import { nocodbCredentialsSchema } from '@typebot.io/nocodb-block/schemas'
|
||||||
|
|
||||||
export const forgedCredentialsSchemas = {
|
export const forgedCredentialsSchemas = {
|
||||||
[openAIBlock.id]: openAICredentialsSchema,
|
[openAIBlock.id]: openAICredentialsSchema,
|
||||||
@ -33,4 +35,5 @@ export const forgedCredentialsSchemas = {
|
|||||||
[anthropicBlock.id]: anthropicCredentialsSchema,
|
[anthropicBlock.id]: anthropicCredentialsSchema,
|
||||||
[togetherAiBlock.id]: togetherAiCredentialsSchema,
|
[togetherAiBlock.id]: togetherAiCredentialsSchema,
|
||||||
[openRouterBlock.id]: openRouterCredentialsSchema,
|
[openRouterBlock.id]: openRouterCredentialsSchema,
|
||||||
|
[nocodbBlock.id]: nocodbCredentialsSchema,
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { chatNodeBlock } from '@typebot.io/chat-node-block'
|
|||||||
import { calComBlock } from '@typebot.io/cal-com-block'
|
import { calComBlock } from '@typebot.io/cal-com-block'
|
||||||
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
|
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
|
||||||
import { openAIBlock } from '@typebot.io/openai-block'
|
import { openAIBlock } from '@typebot.io/openai-block'
|
||||||
|
import { nocodbBlock } from '@typebot.io/nocodb-block'
|
||||||
|
|
||||||
export const forgedBlocks = {
|
export const forgedBlocks = {
|
||||||
[openAIBlock.id]: openAIBlock,
|
[openAIBlock.id]: openAIBlock,
|
||||||
@ -23,4 +24,5 @@ export const forgedBlocks = {
|
|||||||
[anthropicBlock.id]: anthropicBlock,
|
[anthropicBlock.id]: anthropicBlock,
|
||||||
[togetherAiBlock.id]: togetherAiBlock,
|
[togetherAiBlock.id]: togetherAiBlock,
|
||||||
[openRouterBlock.id]: openRouterBlock,
|
[openRouterBlock.id]: openRouterBlock,
|
||||||
|
[nocodbBlock.id]: nocodbBlock,
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"@typebot.io/elevenlabs-block": "workspace:*",
|
"@typebot.io/elevenlabs-block": "workspace:*",
|
||||||
"@typebot.io/anthropic-block": "workspace:*",
|
"@typebot.io/anthropic-block": "workspace:*",
|
||||||
"@typebot.io/together-ai-block": "workspace:*",
|
"@typebot.io/together-ai-block": "workspace:*",
|
||||||
"@typebot.io/open-router-block": "workspace:*"
|
"@typebot.io/open-router-block": "workspace:*",
|
||||||
|
"@typebot.io/nocodb-block": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ import { togetherAiBlock } from '@typebot.io/together-ai-block'
|
|||||||
import { togetherAiBlockSchema } from '@typebot.io/together-ai-block/schemas'
|
import { togetherAiBlockSchema } from '@typebot.io/together-ai-block/schemas'
|
||||||
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
|
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
|
||||||
import { zemanticAiBlockSchema } from '@typebot.io/zemantic-ai-block/schemas'
|
import { zemanticAiBlockSchema } from '@typebot.io/zemantic-ai-block/schemas'
|
||||||
|
import { nocodbBlock } from '@typebot.io/nocodb-block'
|
||||||
|
import { nocodbBlockSchema } from '@typebot.io/nocodb-block/schemas'
|
||||||
|
|
||||||
export const forgedBlockSchemas = {
|
export const forgedBlockSchemas = {
|
||||||
[openAIBlock.id]: openAIBlockSchema,
|
[openAIBlock.id]: openAIBlockSchema,
|
||||||
@ -34,4 +36,5 @@ export const forgedBlockSchemas = {
|
|||||||
[anthropicBlock.id]: anthropicBlockSchema,
|
[anthropicBlock.id]: anthropicBlockSchema,
|
||||||
[togetherAiBlock.id]: togetherAiBlockSchema,
|
[togetherAiBlock.id]: togetherAiBlockSchema,
|
||||||
[openRouterBlock.id]: openRouterBlockSchema,
|
[openRouterBlock.id]: openRouterBlockSchema,
|
||||||
|
[nocodbBlock.id]: nocodbBlockSchema,
|
||||||
}
|
}
|
||||||
|
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@ -1420,6 +1420,28 @@ importers:
|
|||||||
specifier: 5.4.5
|
specifier: 5.4.5
|
||||||
version: 5.4.5
|
version: 5.4.5
|
||||||
|
|
||||||
|
packages/forge/blocks/nocodb:
|
||||||
|
dependencies:
|
||||||
|
ky:
|
||||||
|
specifier: 1.2.3
|
||||||
|
version: 1.2.3
|
||||||
|
devDependencies:
|
||||||
|
'@typebot.io/forge':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../core
|
||||||
|
'@typebot.io/lib':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../lib
|
||||||
|
'@typebot.io/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../tsconfig
|
||||||
|
'@types/react':
|
||||||
|
specifier: 18.2.15
|
||||||
|
version: 18.2.15
|
||||||
|
typescript:
|
||||||
|
specifier: 5.3.2
|
||||||
|
version: 5.3.2
|
||||||
|
|
||||||
packages/forge/blocks/openRouter:
|
packages/forge/blocks/openRouter:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@typebot.io/forge':
|
'@typebot.io/forge':
|
||||||
@ -1596,6 +1618,9 @@ importers:
|
|||||||
'@typebot.io/mistral-block':
|
'@typebot.io/mistral-block':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../blocks/mistral
|
version: link:../blocks/mistral
|
||||||
|
'@typebot.io/nocodb-block':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../blocks/nocodb
|
||||||
'@typebot.io/open-router-block':
|
'@typebot.io/open-router-block':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../blocks/openRouter
|
version: link:../blocks/openRouter
|
||||||
@ -22392,6 +22417,12 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/typescript@5.3.2:
|
||||||
|
resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/typescript@5.4.5:
|
/typescript@5.4.5:
|
||||||
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
|
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
|
Reference in New Issue
Block a user