v0.5.39: notion, workflow variables fixes

This commit is contained in:
Waleed
2025-12-20 20:44:00 -08:00
committed by GitHub
30 changed files with 469 additions and 311 deletions

View File

@@ -37,7 +37,7 @@ import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-
import type { GenerationType } from '@/blocks/types'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
const logger = createLogger('Code')
@@ -602,7 +602,7 @@ export function Code({
const inner = reference.slice(1, -1)
const [prefix] = inner.split('.')
const normalizedPrefix = normalizeBlockName(prefix)
const normalizedPrefix = normalizeName(prefix)
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
return true

View File

@@ -33,7 +33,7 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('ConditionInput')
@@ -139,7 +139,7 @@ export function ConditionInput({
const inner = reference.slice(1, -1)
const [prefix] = inner.split('.')
const normalizedPrefix = normalizeBlockName(prefix)
const normalizedPrefix = normalizeName(prefix)
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
return true

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from 'react'
import { splitReferenceSegment } from '@/lib/workflows/sanitization/references'
import { REFERENCE } from '@/executor/constants'
import { createCombinedPattern } from '@/executor/utils/reference-validation'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
export interface HighlightContext {
accessiblePrefixes?: Set<string>
@@ -31,7 +31,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
const inner = reference.slice(1, -1)
const [prefix] = inner.split('.')
const normalizedPrefix = normalizeBlockName(prefix)
const normalizedPrefix = normalizeName(prefix)
if (SYSTEM_PREFIXES.has(normalizedPrefix)) {
return true

View File

@@ -34,6 +34,7 @@ import { useVariablesStore } from '@/stores/panel/variables/store'
import type { Variable } from '@/stores/panel/variables/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { normalizeName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState } from '@/stores/workflows/workflow/types'
import { getTool } from '@/tools/utils'
@@ -117,20 +118,6 @@ const TAG_PREFIXES = {
VARIABLE: 'variable.',
} as const
/**
* Normalizes a block name by removing spaces and converting to lowercase
*/
const normalizeBlockName = (blockName: string): string => {
return blockName.replace(/\s+/g, '').toLowerCase()
}
/**
* Normalizes a variable name by removing spaces
*/
const normalizeVariableName = (variableName: string): string => {
return variableName.replace(/\s+/g, '')
}
/**
* Ensures the root tag is present in the tags array
*/
@@ -521,7 +508,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
if (sourceBlock.type === 'loop' || sourceBlock.type === 'parallel') {
const mockConfig = { outputs: { results: 'array' } }
const blockName = sourceBlock.name || sourceBlock.type
const normalizedBlockName = normalizeBlockName(blockName)
const normalizedBlockName = normalizeName(blockName)
const outputPaths = generateOutputPaths(mockConfig.outputs)
const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
@@ -542,7 +529,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}
const blockName = sourceBlock.name || sourceBlock.type
const normalizedBlockName = normalizeBlockName(blockName)
const normalizedBlockName = normalizeName(blockName)
const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId)
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
@@ -735,12 +722,12 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
)
const variableTags = validVariables.map(
(variable: Variable) => `${TAG_PREFIXES.VARIABLE}${normalizeVariableName(variable.name)}`
(variable: Variable) => `${TAG_PREFIXES.VARIABLE}${normalizeName(variable.name)}`
)
const variableInfoMap = validVariables.reduce(
(acc, variable) => {
const tagName = `${TAG_PREFIXES.VARIABLE}${normalizeVariableName(variable.name)}`
const tagName = `${TAG_PREFIXES.VARIABLE}${normalizeName(variable.name)}`
acc[tagName] = {
type: variable.type,
id: variable.id,
@@ -865,7 +852,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const mockConfig = { outputs: { results: 'array' } }
const blockName = accessibleBlock.name || accessibleBlock.type
const normalizedBlockName = normalizeBlockName(blockName)
const normalizedBlockName = normalizeName(blockName)
const outputPaths = generateOutputPaths(mockConfig.outputs)
let blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
@@ -885,7 +872,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}
const blockName = accessibleBlock.name || accessibleBlock.type
const normalizedBlockName = normalizeBlockName(blockName)
const normalizedBlockName = normalizeName(blockName)
const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId)
const responseFormatValue = mergedSubBlocks?.responseFormat?.value

View File

@@ -9,7 +9,7 @@ import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/co
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState } from '@/stores/workflows/workflow/types'
@@ -110,7 +110,7 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId
const inner = reference.slice(1, -1)
const [prefix] = inner.split('.')
const normalizedPrefix = normalizeBlockName(prefix)
const normalizedPrefix = normalizeName(prefix)
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
return true

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/sanitization/references'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
@@ -53,10 +53,10 @@ export function useAccessibleReferencePrefixes(blockId?: string | null): Set<str
const prefixes = new Set<string>()
accessibleIds.forEach((id) => {
prefixes.add(normalizeBlockName(id))
prefixes.add(normalizeName(id))
const block = blocks[id]
if (block?.name) {
prefixes.add(normalizeBlockName(block.name))
prefixes.add(normalizeName(block.name))
}
})

View File

@@ -52,6 +52,17 @@ interface UIEnvironmentVariable {
id?: number
}
/**
* Validates an environment variable key.
* Returns an error message if invalid, undefined if valid.
*/
function validateEnvVarKey(key: string): string | undefined {
if (!key) return undefined
if (key.includes(' ')) return 'Spaces are not allowed'
if (!ENV_VAR_PATTERN.test(key)) return 'Only letters, numbers, and underscores allowed'
return undefined
}
interface EnvironmentVariablesProps {
registerBeforeLeaveHandler?: (handler: (onProceed: () => void) => void) => void
}
@@ -222,6 +233,10 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
return envVars.some((envVar) => !!envVar.key && Object.hasOwn(workspaceVars, envVar.key))
}, [envVars, workspaceVars])
const hasInvalidKeys = useMemo(() => {
return envVars.some((envVar) => !!envVar.key && validateEnvVarKey(envVar.key))
}, [envVars])
useEffect(() => {
hasChangesRef.current = hasChanges
}, [hasChanges])
@@ -551,6 +566,7 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
const renderEnvVarRow = useCallback(
(envVar: UIEnvironmentVariable, originalIndex: number) => {
const isConflict = !!envVar.key && Object.hasOwn(workspaceVars, envVar.key)
const keyError = validateEnvVarKey(envVar.key)
const maskedValueStyle =
focusedValueIndex !== originalIndex && !isConflict
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
@@ -571,7 +587,7 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
spellCheck='false'
readOnly
onFocus={(e) => e.target.removeAttribute('readOnly')}
className={`h-9 ${isConflict ? conflictClassName : ''}`}
className={`h-9 ${isConflict ? conflictClassName : ''} ${keyError ? 'border-[var(--text-error)]' : ''}`}
/>
<div />
<EmcnInput
@@ -627,7 +643,12 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
</Tooltip.Root>
</div>
</div>
{isConflict && (
{keyError && (
<div className='col-span-3 mt-[4px] text-[12px] text-[var(--text-error)] leading-tight'>
{keyError}
</div>
)}
{isConflict && !keyError && (
<div className='col-span-3 mt-[4px] text-[12px] text-[var(--text-error)] leading-tight'>
Workspace variable with the same name overrides this. Rename your personal key to use
it.
@@ -707,14 +728,17 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
<Tooltip.Trigger asChild>
<Button
onClick={handleSave}
disabled={isLoading || !hasChanges || hasConflicts}
disabled={isLoading || !hasChanges || hasConflicts || hasInvalidKeys}
variant='primary'
className={`${PRIMARY_BUTTON_STYLES} ${hasConflicts ? 'cursor-not-allowed opacity-50' : ''}`}
className={`${PRIMARY_BUTTON_STYLES} ${hasConflicts || hasInvalidKeys ? 'cursor-not-allowed opacity-50' : ''}`}
>
Save
</Button>
</Tooltip.Trigger>
{hasConflicts && <Tooltip.Content>Resolve all conflicts before saving</Tooltip.Content>}
{hasInvalidKeys && !hasConflicts && (
<Tooltip.Content>Fix invalid variable names before saving</Tooltip.Content>
)}
</Tooltip.Root>
</div>
@@ -808,8 +832,8 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
{hasConflicts
? 'You have unsaved changes, but conflicts must be resolved before saving. You can discard your changes to close the modal.'
{hasConflicts || hasInvalidKeys
? `You have unsaved changes, but ${hasConflicts ? 'conflicts must be resolved' : 'invalid variable names must be fixed'} before saving. You can discard your changes to close the modal.`
: 'You have unsaved changes. Do you want to save them before closing?'}
</p>
</ModalBody>
@@ -817,18 +841,22 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
<Button variant='default' onClick={handleCancel}>
Discard Changes
</Button>
{hasConflicts ? (
{hasConflicts || hasInvalidKeys ? (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
disabled={true}
variant='primary'
className='cursor-not-allowed opacity-50'
className={`${PRIMARY_BUTTON_STYLES} cursor-not-allowed opacity-50`}
>
Save Changes
</Button>
</Tooltip.Trigger>
<Tooltip.Content>Resolve all conflicts before saving</Tooltip.Content>
<Tooltip.Content>
{hasConflicts
? 'Resolve all conflicts before saving'
: 'Fix invalid variable names before saving'}
</Tooltip.Content>
</Tooltip.Root>
) : (
<Button onClick={handleSave} variant='primary' className={PRIMARY_BUTTON_STYLES}>

View File

@@ -35,7 +35,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
title: 'Notion Account',
type: 'oauth-input',
serviceId: 'notion',
requiredScopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
placeholder: 'Select Notion account',
required: true,
},

View File

@@ -16,7 +16,7 @@ import {
import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types'
import { collectBlockData } from '@/executor/utils/block-data'
import type { SerializedBlock } from '@/serializer/types'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
import { executeTool } from '@/tools'
const logger = createLogger('HumanInTheLoopBlockHandler')
@@ -591,7 +591,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
if (pauseBlockName) {
blockNameMappingWithPause[pauseBlockName] = pauseBlockId
blockNameMappingWithPause[normalizeBlockName(pauseBlockName)] = pauseBlockId
blockNameMappingWithPause[normalizeName(pauseBlockName)] = pauseBlockId
}
const notificationPromises = tools.map<Promise<NotificationToolResult>>(async (toolConfig) => {

View File

@@ -5,7 +5,7 @@ import {
type Resolver,
} from '@/executor/variables/resolvers/reference'
import type { SerializedWorkflow } from '@/serializer/types'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
export class BlockResolver implements Resolver {
private blockByNormalizedName: Map<string, string>
@@ -15,7 +15,7 @@ export class BlockResolver implements Resolver {
for (const block of workflow.blocks) {
this.blockByNormalizedName.set(block.id, block.id)
if (block.metadata?.name) {
const normalized = normalizeBlockName(block.metadata.name)
const normalized = normalizeName(block.metadata.name)
this.blockByNormalizedName.set(normalized, block.id)
}
}
@@ -83,7 +83,7 @@ export class BlockResolver implements Resolver {
if (this.blockByNormalizedName.has(name)) {
return this.blockByNormalizedName.get(name)
}
const normalized = normalizeBlockName(name)
const normalized = normalizeName(name)
return this.blockByNormalizedName.get(normalized)
}

View File

@@ -0,0 +1,219 @@
import { describe, expect, it, vi } from 'vitest'
import type { ResolutionContext } from './reference'
import { WorkflowResolver } from './workflow'
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}))
vi.mock('@/lib/workflows/variables/variable-manager', () => ({
VariableManager: {
resolveForExecution: vi.fn((value) => value),
},
}))
/**
* Creates a minimal ResolutionContext for testing.
* The WorkflowResolver only uses context.executionContext.workflowVariables,
* so we only need to provide that field.
*/
function createTestContext(workflowVariables: Record<string, any>): ResolutionContext {
return {
executionContext: { workflowVariables },
executionState: {},
currentNodeId: 'test-node',
} as ResolutionContext
}
describe('WorkflowResolver', () => {
describe('canResolve', () => {
it.concurrent('should return true for variable references', () => {
const resolver = new WorkflowResolver({})
expect(resolver.canResolve('<variable.myvar>')).toBe(true)
expect(resolver.canResolve('<variable.test>')).toBe(true)
})
it.concurrent('should return false for non-variable references', () => {
const resolver = new WorkflowResolver({})
expect(resolver.canResolve('<block.output>')).toBe(false)
expect(resolver.canResolve('<loop.index>')).toBe(false)
expect(resolver.canResolve('plain text')).toBe(false)
})
})
describe('resolve with normalized matching', () => {
it.concurrent('should resolve variable with exact name match', () => {
const variables = {
'var-1': { id: 'var-1', name: 'myvar', type: 'plain', value: 'test-value' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.myvar>', createTestContext(variables))
expect(result).toBe('test-value')
})
it.concurrent('should resolve variable with normalized name (lowercase)', () => {
const variables = {
'var-1': { id: 'var-1', name: 'MyVar', type: 'plain', value: 'test-value' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.myvar>', createTestContext(variables))
expect(result).toBe('test-value')
})
it.concurrent('should resolve variable with normalized name (spaces removed)', () => {
const variables = {
'var-1': { id: 'var-1', name: 'My Variable', type: 'plain', value: 'test-value' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.myvariable>', createTestContext(variables))
expect(result).toBe('test-value')
})
it.concurrent(
'should resolve variable with fully normalized name (JIRA TEAM UUID case)',
() => {
const variables = {
'var-1': { id: 'var-1', name: 'JIRA TEAM UUID', type: 'plain', value: 'uuid-123' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.jirateamuuid>', createTestContext(variables))
expect(result).toBe('uuid-123')
}
)
it.concurrent('should resolve variable regardless of reference case', () => {
const variables = {
'var-1': { id: 'var-1', name: 'jirateamuuid', type: 'plain', value: 'uuid-123' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.JIRATEAMUUID>', createTestContext(variables))
expect(result).toBe('uuid-123')
})
it.concurrent('should resolve by variable ID (exact match)', () => {
const variables = {
'my-uuid-id': { id: 'my-uuid-id', name: 'Some Name', type: 'plain', value: 'id-value' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.my-uuid-id>', createTestContext(variables))
expect(result).toBe('id-value')
})
it.concurrent('should return undefined for non-existent variable', () => {
const variables = {
'var-1': { id: 'var-1', name: 'existing', type: 'plain', value: 'test' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.nonexistent>', createTestContext(variables))
expect(result).toBeUndefined()
})
it.concurrent('should handle nested path access', () => {
const variables = {
'var-1': {
id: 'var-1',
name: 'config',
type: 'object',
value: { nested: { value: 'deep' } },
},
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve(
'<variable.config.nested.value>',
createTestContext(variables)
)
expect(result).toBe('deep')
})
it.concurrent('should resolve with mixed case and spaces in reference', () => {
const variables = {
'var-1': { id: 'var-1', name: 'api key', type: 'plain', value: 'secret-key' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.APIKEY>', createTestContext(variables))
expect(result).toBe('secret-key')
})
it.concurrent('should handle real-world variable naming patterns', () => {
const testCases = [
{ varName: 'User ID', refName: 'userid', value: 'user-123' },
{ varName: 'API Key', refName: 'apikey', value: 'key-456' },
{ varName: 'STRIPE SECRET KEY', refName: 'stripesecretkey', value: 'sk_test' },
{ varName: 'Database URL', refName: 'databaseurl', value: 'postgres://...' },
]
for (const { varName, refName, value } of testCases) {
const variables = {
'var-1': { id: 'var-1', name: varName, type: 'plain', value },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve(`<variable.${refName}>`, createTestContext(variables))
expect(result).toBe(value)
}
})
})
describe('edge cases', () => {
it.concurrent('should handle empty workflow variables', () => {
const resolver = new WorkflowResolver({})
const result = resolver.resolve('<variable.anyvar>', createTestContext({}))
expect(result).toBeUndefined()
})
it.concurrent('should handle invalid reference format', () => {
const resolver = new WorkflowResolver({})
const result = resolver.resolve('<variable>', createTestContext({}))
expect(result).toBeUndefined()
})
it.concurrent('should handle null variable values in the map', () => {
const variables: Record<string, any> = {
'var-1': null,
'var-2': { id: 'var-2', name: 'valid', type: 'plain', value: 'exists' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.valid>', createTestContext(variables))
expect(result).toBe('exists')
})
it.concurrent('should handle variable with empty name', () => {
const variables = {
'var-1': { id: 'var-1', name: '', type: 'plain', value: 'empty-name' },
}
const resolver = new WorkflowResolver(variables)
// Empty name normalizes to empty string, which matches "<variable.>" reference
const result = resolver.resolve('<variable.>', createTestContext(variables))
expect(result).toBe('empty-name')
})
it.concurrent('should prefer name match over ID match when both could apply', () => {
const variables = {
apikey: { id: 'apikey', name: 'different', type: 'plain', value: 'by-id' },
'var-2': { id: 'var-2', name: 'apikey', type: 'plain', value: 'by-name' },
}
const resolver = new WorkflowResolver(variables)
const result = resolver.resolve('<variable.apikey>', createTestContext(variables))
expect(['by-id', 'by-name']).toContain(result)
})
})
})

View File

@@ -6,6 +6,7 @@ import {
type ResolutionContext,
type Resolver,
} from '@/executor/variables/resolvers/reference'
import { normalizeName } from '@/stores/workflows/utils'
const logger = createLogger('WorkflowResolver')
@@ -32,12 +33,17 @@ export class WorkflowResolver implements Resolver {
}
const [_, variableName, ...pathParts] = parts
const normalizedRefName = normalizeName(variableName)
const workflowVars = context.executionContext.workflowVariables || this.workflowVariables
for (const varObj of Object.values(workflowVars)) {
const v = varObj as any
if (v && (v.name === variableName || v.id === variableName)) {
if (!v) continue
// Match by normalized name or exact ID
const normalizedVarName = v.name ? normalizeName(v.name) : ''
if (normalizedVarName === normalizedRefName || v.id === variableName) {
const normalizedType = (v.type === 'string' ? 'plain' : v.type) || 'plain'
let value: any
try {

View File

@@ -1288,7 +1288,6 @@ export function useCollaborativeWorkflow() {
},
workflowId: activeWorkflowId || '',
userId: session?.user?.id || 'unknown',
immediate: true,
})
},
[

View File

@@ -212,7 +212,6 @@ export const auth = betterAuth({
'github',
'email-password',
'confluence',
// 'supabase',
'x',
'notion',
'microsoft',
@@ -950,56 +949,6 @@ export const auth = betterAuth({
},
},
// Supabase provider (unused)
// {
// providerId: 'supabase',
// clientId: env.SUPABASE_CLIENT_ID as string,
// clientSecret: env.SUPABASE_CLIENT_SECRET as string,
// authorizationUrl: 'https://api.supabase.com/v1/oauth/authorize',
// tokenUrl: 'https://api.supabase.com/v1/oauth/token',
// userInfoUrl: 'https://dummy-not-used.supabase.co',
// scopes: ['database.read', 'database.write', 'projects.read'],
// responseType: 'code',
// pkce: true,
// redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/supabase`,
// getUserInfo: async (tokens) => {
// try {
// logger.info('Creating Supabase user profile from token data')
// let userId = 'supabase-user'
// if (tokens.idToken) {
// try {
// const decodedToken = JSON.parse(
// Buffer.from(tokens.idToken.split('.')[1], 'base64').toString()
// )
// if (decodedToken.sub) {
// userId = decodedToken.sub
// }
// } catch (e) {
// logger.warn('Failed to decode Supabase ID token', {
// error: e,
// })
// }
// }
// const uniqueId = `${userId}-${Date.now()}`
// const now = new Date()
// return {
// id: uniqueId,
// name: 'Supabase User',
// email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@supabase.user`,
// emailVerified: false,
// createdAt: now,
// updatedAt: now,
// }
// } catch (error) {
// logger.error('Error creating Supabase user profile:', { error })
// return null
// }
// },
// },
// X provider
{
providerId: 'x',
@@ -1133,57 +1082,6 @@ export const auth = betterAuth({
},
},
// Discord provider (unused)
// {
// providerId: 'discord',
// clientId: env.DISCORD_CLIENT_ID as string,
// clientSecret: env.DISCORD_CLIENT_SECRET as string,
// authorizationUrl: 'https://discord.com/api/oauth2/authorize',
// tokenUrl: 'https://discord.com/api/oauth2/token',
// userInfoUrl: 'https://discord.com/api/users/@me',
// scopes: ['identify', 'bot', 'messages.read', 'guilds', 'guilds.members.read'],
// responseType: 'code',
// accessType: 'offline',
// authentication: 'basic',
// prompt: 'consent',
// redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/discord`,
// getUserInfo: async (tokens) => {
// try {
// const response = await fetch('https://discord.com/api/users/@me', {
// headers: {
// Authorization: `Bearer ${tokens.accessToken}`,
// },
// })
// if (!response.ok) {
// logger.error('Error fetching Discord user info:', {
// status: response.status,
// statusText: response.statusText,
// })
// return null
// }
// const profile = await response.json()
// const now = new Date()
// return {
// id: profile.id,
// name: profile.username || 'Discord User',
// email: profile.email || `${profile.id}@discord.user`,
// image: profile.avatar
// ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`
// : undefined,
// emailVerified: profile.verified || false,
// createdAt: now,
// updatedAt: now,
// }
// } catch (error) {
// logger.error('Error in Discord getUserInfo:', { error })
// return null
// }
// },
// },
// Jira provider
{
providerId: 'jira',
@@ -1323,7 +1221,6 @@ export const auth = betterAuth({
authorizationUrl: 'https://api.notion.com/v1/oauth/authorize',
tokenUrl: 'https://api.notion.com/v1/oauth/token',
userInfoUrl: 'https://api.notion.com/v1/users/me',
scopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
responseType: 'code',
pkce: false,
accessType: 'offline',

View File

@@ -3,7 +3,6 @@ import {
AirtableIcon,
AsanaIcon,
ConfluenceIcon,
// DiscordIcon,
DropboxIcon,
GithubIcon,
GmailIcon,
@@ -32,7 +31,6 @@ import {
ShopifyIcon,
SlackIcon,
SpotifyIcon,
// SupabaseIcon,
TrelloIcon,
WealthboxIcon,
WebflowIcon,
@@ -49,12 +47,10 @@ export type OAuthProvider =
| 'google'
| 'github'
| 'x'
// | 'supabase'
| 'confluence'
| 'airtable'
| 'notion'
| 'jira'
// | 'discord'
| 'dropbox'
| 'microsoft'
| 'linear'
@@ -86,12 +82,10 @@ export type OAuthService =
| 'google-groups'
| 'github'
| 'x'
// | 'supabase'
| 'confluence'
| 'airtable'
| 'notion'
| 'jira'
// | 'discord'
| 'dropbox'
| 'microsoft-excel'
| 'microsoft-teams'
@@ -388,23 +382,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'x',
},
// supabase: {
// id: 'supabase',
// name: 'Supabase',
// icon: (props) => SupabaseIcon(props),
// services: {
// supabase: {
// id: 'supabase',
// name: 'Supabase',
// description: 'Connect to your Supabase projects and manage data.',
// providerId: 'supabase',
// icon: (props) => SupabaseIcon(props),
// baseProviderIcon: (props) => SupabaseIcon(props),
// scopes: ['database.read', 'database.write', 'projects.read'],
// },
// },
// defaultService: 'supabase',
// },
confluence: {
id: 'confluence',
name: 'Confluence',
@@ -518,23 +495,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'airtable',
},
// discord: {
// id: 'discord',
// name: 'Discord',
// icon: (props) => DiscordIcon(props),
// services: {
// discord: {
// id: 'discord',
// name: 'Discord',
// description: 'Read and send messages to Discord channels and interact with servers.',
// providerId: 'discord',
// icon: (props) => DiscordIcon(props),
// baseProviderIcon: (props) => DiscordIcon(props),
// scopes: ['identify', 'bot', 'messages.read', 'guilds', 'guilds.members.read'],
// },
// },
// defaultService: 'discord',
// },
notion: {
id: 'notion',
name: 'Notion',
@@ -547,7 +507,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
providerId: 'notion',
icon: (props) => NotionIcon(props),
baseProviderIcon: (props) => NotionIcon(props),
scopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
scopes: [],
},
},
defaultService: 'notion',
@@ -1272,18 +1232,6 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
useBasicAuth: false,
}
}
// case 'discord': {
// const { clientId, clientSecret } = getCredentials(
// env.DISCORD_CLIENT_ID,
// env.DISCORD_CLIENT_SECRET
// )
// return {
// tokenEndpoint: 'https://discord.com/api/v10/oauth2/token',
// clientId,
// clientSecret,
// useBasicAuth: true,
// }
// }
case 'microsoft': {
const { clientId, clientSecret } = getCredentials(
env.MICROSOFT_CLIENT_ID,

View File

@@ -24,7 +24,6 @@ interface EnqueueWorkflowOperationArgs {
target: string
payload: any
workflowId: string
immediate?: boolean
operationId?: string
}
@@ -37,7 +36,6 @@ export async function enqueueWorkflowOperation({
target,
payload,
workflowId,
immediate = false,
operationId,
}: EnqueueWorkflowOperationArgs): Promise<string> {
const userId = await resolveUserId()
@@ -52,7 +50,6 @@ export async function enqueueWorkflowOperation({
},
workflowId,
userId,
immediate,
})
logger.debug('Queued workflow operation', {
@@ -60,7 +57,6 @@ export async function enqueueWorkflowOperation({
operation,
target,
operationId: opId,
immediate,
})
return opId
@@ -69,7 +65,6 @@ export async function enqueueWorkflowOperation({
interface EnqueueReplaceStateArgs {
workflowId: string
state: WorkflowState
immediate?: boolean
operationId?: string
}
@@ -79,7 +74,6 @@ interface EnqueueReplaceStateArgs {
export async function enqueueReplaceWorkflowState({
workflowId,
state,
immediate,
operationId,
}: EnqueueReplaceStateArgs): Promise<string> {
return enqueueWorkflowOperation({
@@ -87,7 +81,6 @@ export async function enqueueReplaceWorkflowState({
operation: 'replace-state',
target: 'workflow',
payload: { state },
immediate,
operationId,
})
}

View File

@@ -1,4 +1,4 @@
import { normalizeBlockName } from '@/stores/workflows/utils'
import { normalizeName } from '@/stores/workflows/utils'
export const SYSTEM_REFERENCE_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable'])
@@ -111,7 +111,7 @@ export function extractReferencePrefixes(value: string): Array<{ raw: string; pr
continue
}
const normalized = normalizeBlockName(rawPrefix)
const normalized = normalizeName(rawPrefix)
references.push({ raw: referenceSegment, prefix: normalized })
}

View File

@@ -15,7 +15,6 @@ export interface QueuedOperation {
retryCount: number
status: 'pending' | 'processing' | 'confirmed' | 'failed'
userId: string
immediate?: boolean // Flag for immediate processing (skips debouncing)
}
interface OperationQueueState {

View File

@@ -1,9 +1,11 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console/logger'
import { useOperationQueueStore } from '@/stores/operation-queue/store'
import type { Variable, VariablesStore } from '@/stores/panel/variables/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { normalizeName } from '@/stores/workflows/utils'
const logger = createLogger('VariablesStore')
@@ -177,49 +179,72 @@ export const useVariablesStore = create<VariablesStore>()(
if (activeWorkflowId) {
const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
const updatedWorkflowValues = { ...workflowValues }
const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> =
[]
const oldVarName = normalizeName(oldVariableName)
const newVarName = normalizeName(newName)
const regex = new RegExp(`<variable\\.${oldVarName}>`, 'gi')
const updateReferences = (value: any, pattern: RegExp, replacement: string): any => {
if (typeof value === 'string') {
return pattern.test(value) ? value.replace(pattern, replacement) : value
}
if (Array.isArray(value)) {
return value.map((item) => updateReferences(item, pattern, replacement))
}
if (value !== null && typeof value === 'object') {
const result = { ...value }
for (const key in result) {
result[key] = updateReferences(result[key], pattern, replacement)
}
return result
}
return value
}
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
Object.entries(blockValues as Record<string, any>).forEach(
([subBlockId, value]) => {
const oldVarName = oldVariableName.replace(/\s+/g, '').toLowerCase()
const newVarName = newName.replace(/\s+/g, '').toLowerCase()
const regex = new RegExp(`<variable\.${oldVarName}>`, 'gi')
const updatedValue = updateReferences(value, regex, `<variable.${newVarName}>`)
updatedWorkflowValues[blockId][subBlockId] = updateReferences(
value,
regex,
`<variable.${newVarName}>`
)
function updateReferences(value: any, regex: RegExp, replacement: string): any {
if (typeof value === 'string') {
return regex.test(value) ? value.replace(regex, replacement) : value
if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
if (!updatedWorkflowValues[blockId]) {
updatedWorkflowValues[blockId] = { ...workflowValues[blockId] }
}
if (Array.isArray(value)) {
return value.map((item) => updateReferences(item, regex, replacement))
}
if (value !== null && typeof value === 'object') {
const result = { ...value }
for (const key in result) {
result[key] = updateReferences(result[key], regex, replacement)
}
return result
}
return value
updatedWorkflowValues[blockId][subBlockId] = updatedValue
changedSubBlocks.push({ blockId, subBlockId, value: updatedValue })
}
}
)
})
// Update local state
useSubBlockStore.setState({
workflowValues: {
...subBlockStore.workflowValues,
[activeWorkflowId]: updatedWorkflowValues,
},
})
// Queue operations for persistence via socket
const operationQueue = useOperationQueueStore.getState()
for (const { blockId, subBlockId, value } of changedSubBlocks) {
operationQueue.addToQueue({
id: crypto.randomUUID(),
operation: {
operation: 'subblock-update',
target: 'subblock',
payload: { blockId, subblockId: subBlockId, value },
},
workflowId: activeWorkflowId,
userId: 'system',
})
}
}
}
}

View File

@@ -499,7 +499,6 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
await enqueueReplaceWorkflowState({
workflowId: activeWorkflowId,
state: baselineWorkflow,
immediate: true,
})
// Persist to database

View File

@@ -0,0 +1,93 @@
import { describe, expect, it } from 'vitest'
import { normalizeName } from './utils'
describe('normalizeName', () => {
it.concurrent('should convert to lowercase', () => {
expect(normalizeName('MyVariable')).toBe('myvariable')
expect(normalizeName('UPPERCASE')).toBe('uppercase')
expect(normalizeName('MixedCase')).toBe('mixedcase')
})
it.concurrent('should remove spaces', () => {
expect(normalizeName('my variable')).toBe('myvariable')
expect(normalizeName('my variable')).toBe('myvariable')
expect(normalizeName(' spaced ')).toBe('spaced')
})
it.concurrent('should handle both lowercase and space removal', () => {
expect(normalizeName('JIRA TEAM UUID')).toBe('jirateamuuid')
expect(normalizeName('My Block Name')).toBe('myblockname')
expect(normalizeName('API 1')).toBe('api1')
})
it.concurrent('should handle edge cases', () => {
expect(normalizeName('')).toBe('')
expect(normalizeName(' ')).toBe('')
expect(normalizeName('a')).toBe('a')
expect(normalizeName('already_normalized')).toBe('already_normalized')
})
it.concurrent('should preserve non-space special characters', () => {
expect(normalizeName('my-variable')).toBe('my-variable')
expect(normalizeName('my_variable')).toBe('my_variable')
expect(normalizeName('my.variable')).toBe('my.variable')
})
it.concurrent('should handle tabs and newlines as whitespace', () => {
expect(normalizeName('my\tvariable')).toBe('myvariable')
expect(normalizeName('my\nvariable')).toBe('myvariable')
expect(normalizeName('my\r\nvariable')).toBe('myvariable')
})
it.concurrent('should handle unicode characters', () => {
expect(normalizeName('Café')).toBe('café')
expect(normalizeName('日本語')).toBe('日本語')
})
it.concurrent('should normalize block names correctly', () => {
expect(normalizeName('Agent 1')).toBe('agent1')
expect(normalizeName('API Block')).toBe('apiblock')
expect(normalizeName('My Custom Block')).toBe('mycustomblock')
})
it.concurrent('should normalize variable names correctly', () => {
expect(normalizeName('jira1')).toBe('jira1')
expect(normalizeName('JIRA TEAM UUID')).toBe('jirateamuuid')
expect(normalizeName('My Variable')).toBe('myvariable')
})
it.concurrent('should produce consistent results for references', () => {
const originalName = 'JIRA TEAM UUID'
const normalized1 = normalizeName(originalName)
const normalized2 = normalizeName(originalName)
expect(normalized1).toBe(normalized2)
expect(normalized1).toBe('jirateamuuid')
})
it.concurrent('should allow matching block references to variable references', () => {
const name = 'API Block'
const blockRef = `<${normalizeName(name)}.output>`
const varRef = `<variable.${normalizeName(name)}>`
expect(blockRef).toBe('<apiblock.output>')
expect(varRef).toBe('<variable.apiblock>')
})
it.concurrent('should handle real-world naming patterns consistently', () => {
const realWorldNames = [
{ input: 'User ID', expected: 'userid' },
{ input: 'API Key', expected: 'apikey' },
{ input: 'OAuth Token', expected: 'oauthtoken' },
{ input: 'Database URL', expected: 'databaseurl' },
{ input: 'STRIPE SECRET KEY', expected: 'stripesecretkey' },
{ input: 'openai api key', expected: 'openaiapikey' },
{ input: 'Customer Name', expected: 'customername' },
{ input: 'Order Total', expected: 'ordertotal' },
]
for (const { input, expected } of realWorldNames) {
expect(normalizeName(input)).toBe(expected)
}
})
})

View File

@@ -2,11 +2,12 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types'
/**
* Normalizes a block name for comparison by converting to lowercase and removing spaces
* @param name - The block name to normalize
* Normalizes a name for comparison by converting to lowercase and removing spaces.
* Used for both block names and variable names to ensure consistent matching.
* @param name - The name to normalize
* @returns The normalized name
*/
export function normalizeBlockName(name: string): string {
export function normalizeName(name: string): string {
return name.toLowerCase().replace(/\s+/g, '')
}
@@ -20,7 +21,7 @@ export function normalizeBlockName(name: string): string {
export function getUniqueBlockName(baseName: string, existingBlocks: Record<string, any>): string {
// Special case: Start blocks should always be named "Start" without numbers
// This applies to both "Start" and "Starter" base names
const normalizedBaseName = normalizeBlockName(baseName)
const normalizedBaseName = normalizeName(baseName)
if (normalizedBaseName === 'start' || normalizedBaseName === 'starter') {
return 'Start'
}
@@ -28,13 +29,13 @@ export function getUniqueBlockName(baseName: string, existingBlocks: Record<stri
const baseNameMatch = baseName.match(/^(.*?)(\s+\d+)?$/)
const namePrefix = baseNameMatch ? baseNameMatch[1].trim() : baseName
const normalizedBase = normalizeBlockName(namePrefix)
const normalizedBase = normalizeName(namePrefix)
const existingNumbers = Object.values(existingBlocks)
.filter((block) => {
const blockNameMatch = block.name?.match(/^(.*?)(\s+\d+)?$/)
const blockPrefix = blockNameMatch ? blockNameMatch[1].trim() : block.name
return blockPrefix && normalizeBlockName(blockPrefix) === normalizedBase
return blockPrefix && normalizeName(blockPrefix) === normalizedBase
})
.map((block) => {
const match = block.name?.match(/(\d+)$/)
@@ -65,45 +66,34 @@ export function mergeSubblockState(
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
const subBlockStore = useSubBlockStore.getState()
// Get all the values stored in the subblock store for this workflow
const workflowSubblockValues = workflowId ? subBlockStore.workflowValues[workflowId] || {} : {}
return Object.entries(blocksToProcess).reduce(
(acc, [id, block]) => {
// Skip if block is undefined
if (!block) {
return acc
}
// Initialize subBlocks if not present
const blockSubBlocks = block.subBlocks || {}
// Get stored values for this block
const blockValues = workflowSubblockValues[id] || {}
// Create a deep copy of the block's subBlocks to maintain structure
const mergedSubBlocks = Object.entries(blockSubBlocks).reduce(
(subAcc, [subBlockId, subBlock]) => {
// Skip if subBlock is undefined
if (!subBlock) {
return subAcc
}
// Get the stored value for this subblock
let storedValue = null
// If workflowId is provided, use it to get the value
if (workflowId) {
// Try to get the value from the subblock store for this specific workflow
if (blockValues[subBlockId] !== undefined) {
storedValue = blockValues[subBlockId]
}
} else {
// Fall back to the active workflow if no workflowId is provided
storedValue = subBlockStore.getValue(id, subBlockId)
}
// Create a new subblock object with the same structure but updated value
subAcc[subBlockId] = {
...subBlock,
value: storedValue !== undefined && storedValue !== null ? storedValue : subBlock.value,
@@ -200,7 +190,24 @@ export async function mergeSubblockStateAsync(
subBlockEntries.filter((entry): entry is readonly [string, SubBlockState] => entry !== null)
) as Record<string, SubBlockState>
// Return the full block state with updated subBlocks
// Add any values that exist in the store but aren't in the block structure
// This handles cases where block config has been updated but values still exist
// IMPORTANT: This includes runtime subblock IDs like webhookId, triggerPath, etc.
if (workflowId) {
const workflowValues = subBlockStore.workflowValues[workflowId]
const blockValues = workflowValues?.[id] || {}
Object.entries(blockValues).forEach(([subBlockId, value]) => {
if (!mergedSubBlocks[subBlockId] && value !== null && value !== undefined) {
mergedSubBlocks[subBlockId] = {
id: subBlockId,
type: 'short-input',
value: value,
}
}
})
}
// Return the full block state with updated subBlocks (including orphaned values)
return [
id,
{

View File

@@ -9,11 +9,7 @@ import type { SubBlockConfig } from '@/blocks/types'
import { isAnnotationOnlyBlock } from '@/executor/constants'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import {
getUniqueBlockName,
mergeSubblockState,
normalizeBlockName,
} from '@/stores/workflows/utils'
import { getUniqueBlockName, mergeSubblockState, normalizeName } from '@/stores/workflows/utils'
import type {
Position,
SubBlockState,
@@ -676,7 +672,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
if (!oldBlock) return { success: false, changedSubblocks: [] }
// Check for normalized name collisions
const normalizedNewName = normalizeBlockName(name)
const normalizedNewName = normalizeName(name)
const currentBlocks = get().blocks
// Find any other block with the same normalized name
@@ -684,7 +680,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
return (
blockId !== id && // Different block
block.name && // Has a name
normalizeBlockName(block.name) === normalizedNewName // Same normalized name
normalizeName(block.name) === normalizedNewName // Same normalized name
)
})

View File

@@ -75,16 +75,10 @@ export const notionCreateDatabaseTool: ToolConfig<NotionCreateDatabaseParams, No
}
}
// Format parent ID
const formattedParentId = params.parentId.replace(
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
'$1-$2-$3-$4-$5'
)
const body = {
parent: {
type: 'page_id',
page_id: formattedParentId,
page_id: params.parentId,
},
title: [
{

View File

@@ -54,21 +54,13 @@ export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResp
}
},
body: (params: NotionCreatePageParams) => {
// Format parent ID with hyphens if needed
const formattedParentId = params.parentId.replace(
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
'$1-$2-$3-$4-$5'
)
// Prepare the body for page parent
const body: any = {
parent: {
type: 'page_id',
page_id: formattedParentId,
page_id: params.parentId,
},
}
// Add title if provided
if (params.title) {
body.properties = {
title: {
@@ -87,7 +79,6 @@ export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResp
body.properties = {}
}
// Add content if provided
if (params.content) {
body.children = [
{
@@ -115,7 +106,6 @@ export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResp
const data = await response.json()
let pageTitle = 'Untitled'
// Try to extract the title from properties
if (data.properties?.title) {
const titleProperty = data.properties.title
if (

View File

@@ -48,11 +48,7 @@ export const notionQueryDatabaseTool: ToolConfig<NotionQueryDatabaseParams, Noti
request: {
url: (params: NotionQueryDatabaseParams) => {
const formattedId = params.databaseId.replace(
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
'$1-$2-$3-$4-$5'
)
return `https://api.notion.com/v1/databases/${formattedId}/query`
return `https://api.notion.com/v1/databases/${params.databaseId}/query`
},
method: 'POST',
headers: (params: NotionQueryDatabaseParams) => {

View File

@@ -29,11 +29,7 @@ export const notionReadTool: ToolConfig<NotionReadParams, NotionResponse> = {
request: {
url: (params: NotionReadParams) => {
// Format page ID with hyphens if needed
const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
// Use the page endpoint to get page properties
return `https://api.notion.com/v1/pages/${formattedId}`
return `https://api.notion.com/v1/pages/${params.pageId}`
},
method: 'GET',
headers: (params: NotionReadParams) => {
@@ -85,12 +81,9 @@ export const notionReadTool: ToolConfig<NotionReadParams, NotionResponse> = {
}
}
// Format page ID for blocks endpoint
const formattedId = pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
// Fetch page content using blocks endpoint
const blocksResponse = await fetch(
`https://api.notion.com/v1/blocks/${formattedId}/children?page_size=100`,
`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`,
{
method: 'GET',
headers: {

View File

@@ -34,13 +34,7 @@ export const notionReadDatabaseTool: ToolConfig<NotionReadDatabaseParams, Notion
request: {
url: (params: NotionReadDatabaseParams) => {
// Format database ID with hyphens if needed
const formattedId = params.databaseId.replace(
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
'$1-$2-$3-$4-$5'
)
return `https://api.notion.com/v1/databases/${formattedId}`
return `https://api.notion.com/v1/databases/${params.databaseId}`
},
method: 'GET',
headers: (params: NotionReadDatabaseParams) => {

View File

@@ -35,9 +35,7 @@ export const notionUpdatePageTool: ToolConfig<NotionUpdatePageParams, NotionResp
request: {
url: (params: NotionUpdatePageParams) => {
// Format page ID with hyphens if needed
const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
return `https://api.notion.com/v1/pages/${formattedId}`
return `https://api.notion.com/v1/pages/${params.pageId}`
},
method: 'PATCH',
headers: (params: NotionUpdatePageParams) => {

View File

@@ -35,9 +35,7 @@ export const notionWriteTool: ToolConfig<NotionWriteParams, NotionResponse> = {
request: {
url: (params: NotionWriteParams) => {
// Format page ID with hyphens if needed
const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
return `https://api.notion.com/v1/blocks/${formattedId}/children`
return `https://api.notion.com/v1/blocks/${params.pageId}/children`
},
method: 'PATCH',
headers: (params: NotionWriteParams) => {