Compare commits

...

21 Commits

Author SHA1 Message Date
Stephen Zhou
ff83828263 update 2026-02-06 18:42:38 +08:00
Stephen Zhou
82f472d0d0 update 2026-02-06 18:39:15 +08:00
Stephen Zhou
149a5231f0 update 2026-02-06 18:38:19 +08:00
Stephen Zhou
6e4d082cbf update 2026-02-06 18:34:43 +08:00
Stephen Zhou
32f265b9f0 clean default 2026-02-06 18:27:29 +08:00
Stephen Zhou
c183d653a1 remove @next/bundle-analyzer 2026-02-06 18:21:14 +08:00
Stephen Zhou
c71e89f83a better define 2026-02-06 18:17:47 +08:00
Stephen Zhou
d07da73fab update 2026-02-06 17:35:00 +08:00
Stephen Zhou
b58ed66d1c update 2026-02-06 17:17:22 +08:00
Stephen Zhou
dfa0062e97 update 2026-02-06 16:58:41 +08:00
Stephen Zhou
feb4ab8eb3 update 2026-02-06 16:56:18 +08:00
Stephen Zhou
72ad187af2 update 2026-02-06 16:49:02 +08:00
yyh
a4321e24a1 Update web/env.ts 2026-02-06 14:54:33 +08:00
yyh
23b6f33bd3 Merge branch 'main' into 2-6-type-safe-env 2026-02-06 14:49:17 +08:00
Stephen Zhou
f95322ef9c update 2026-02-06 14:29:40 +08:00
Stephen Zhou
cef8058a8f update 2026-02-06 13:52:07 +08:00
Stephen Zhou
5a1a3bb859 use dataset for client 2026-02-06 13:51:40 +08:00
Stephen Zhou
3371fa7861 update 2026-02-06 13:47:13 +08:00
Stephen Zhou
97ecde5389 update 2026-02-06 13:43:45 +08:00
Stephen Zhou
5b22d5026b nev 2026-02-06 11:34:00 +08:00
Stephen Zhou
7d34faaf74 refactor: type safe env 2026-02-06 11:15:19 +08:00
35 changed files with 422 additions and 865 deletions

View File

@@ -1,261 +0,0 @@
/**
* MAX_PARALLEL_LIMIT Configuration Bug Test
*
* This test reproduces and verifies the fix for issue #23083:
* MAX_PARALLEL_LIMIT environment variable does not take effect in iteration panel
*/
import { render, screen } from '@testing-library/react'
import * as React from 'react'
// Mock environment variables before importing constants
const originalEnv = process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
// Test with different environment values
function setupEnvironment(value?: string) {
if (value)
process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT = value
else
delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
// Clear module cache to force re-evaluation
vi.resetModules()
}
function restoreEnvironment() {
if (originalEnv)
process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT = originalEnv
else
delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
vi.resetModules()
}
// Mock i18next with proper implementation
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
if (key.includes('MaxParallelismTitle'))
return 'Max Parallelism'
if (key.includes('MaxParallelismDesc'))
return 'Maximum number of parallel executions'
if (key.includes('parallelMode'))
return 'Parallel Mode'
if (key.includes('parallelPanelDesc'))
return 'Enable parallel execution'
if (key.includes('errorResponseMethod'))
return 'Error Response Method'
return key
},
}),
initReactI18next: {
type: '3rdParty',
init: vi.fn(),
},
}))
// Mock i18next module completely to prevent initialization issues
vi.mock('i18next', () => ({
use: vi.fn().mockReturnThis(),
init: vi.fn().mockReturnThis(),
t: vi.fn(key => key),
isInitialized: true,
}))
// Mock the useConfig hook
vi.mock('@/app/components/workflow/nodes/iteration/use-config', () => ({
default: () => ({
inputs: {
is_parallel: true,
parallel_nums: 5,
error_handle_mode: 'terminated',
},
changeParallel: vi.fn(),
changeParallelNums: vi.fn(),
changeErrorHandleMode: vi.fn(),
}),
}))
// Mock other components
vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
default: function MockVarReferencePicker() {
return <div data-testid="var-reference-picker">VarReferencePicker</div>
},
}))
vi.mock('@/app/components/workflow/nodes/_base/components/split', () => ({
default: function MockSplit() {
return <div data-testid="split">Split</div>
},
}))
vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({
default: function MockField({ title, children }: { title: string, children: React.ReactNode }) {
return (
<div data-testid="field">
<label>{title}</label>
{children}
</div>
)
},
}))
const getParallelControls = () => ({
numberInput: screen.getByRole('spinbutton'),
slider: screen.getByRole('slider'),
})
describe('MAX_PARALLEL_LIMIT Configuration Bug', () => {
const mockNodeData = {
id: 'test-iteration-node',
type: 'iteration' as const,
data: {
title: 'Test Iteration',
desc: 'Test iteration node',
iterator_selector: ['test'],
output_selector: ['output'],
is_parallel: true,
parallel_nums: 5,
error_handle_mode: 'terminated' as const,
},
}
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
restoreEnvironment()
})
afterAll(() => {
restoreEnvironment()
})
describe('Environment Variable Parsing', () => {
it('should parse MAX_PARALLEL_LIMIT from NEXT_PUBLIC_MAX_PARALLEL_LIMIT environment variable', async () => {
setupEnvironment('25')
const { MAX_PARALLEL_LIMIT } = await import('@/config')
expect(MAX_PARALLEL_LIMIT).toBe(25)
})
it('should fallback to default when environment variable is not set', async () => {
setupEnvironment() // No environment variable
const { MAX_PARALLEL_LIMIT } = await import('@/config')
expect(MAX_PARALLEL_LIMIT).toBe(10)
})
it('should handle invalid environment variable values', async () => {
setupEnvironment('invalid')
const { MAX_PARALLEL_LIMIT } = await import('@/config')
// Should fall back to default when parsing fails
expect(MAX_PARALLEL_LIMIT).toBe(10)
})
it('should handle empty environment variable', async () => {
setupEnvironment('')
const { MAX_PARALLEL_LIMIT } = await import('@/config')
// Should fall back to default when empty
expect(MAX_PARALLEL_LIMIT).toBe(10)
})
// Edge cases for boundary values
it('should clamp MAX_PARALLEL_LIMIT to MIN when env is 0 or negative', async () => {
setupEnvironment('0')
let { MAX_PARALLEL_LIMIT } = await import('@/config')
expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default
setupEnvironment('-5')
;({ MAX_PARALLEL_LIMIT } = await import('@/config'))
expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default
})
it('should handle float numbers by parseInt behavior', async () => {
setupEnvironment('12.7')
const { MAX_PARALLEL_LIMIT } = await import('@/config')
// parseInt truncates to integer
expect(MAX_PARALLEL_LIMIT).toBe(12)
})
})
describe('UI Component Integration (Main Fix Verification)', () => {
it('should render iteration panel with environment-configured max value', async () => {
// Set environment variable to a different value
setupEnvironment('30')
// Import Panel after setting environment
const Panel = await import('@/app/components/workflow/nodes/iteration/panel').then(mod => mod.default)
const { MAX_PARALLEL_LIMIT } = await import('@/config')
render(
<Panel
id="test-node"
// @ts-expect-error key type mismatch
data={mockNodeData.data}
/>,
)
// Behavior-focused assertion: UI max should equal MAX_PARALLEL_LIMIT
const { numberInput, slider } = getParallelControls()
expect(numberInput).toHaveAttribute('max', String(MAX_PARALLEL_LIMIT))
expect(slider).toHaveAttribute('aria-valuemax', String(MAX_PARALLEL_LIMIT))
// Verify the actual values
expect(MAX_PARALLEL_LIMIT).toBe(30)
expect(numberInput.getAttribute('max')).toBe('30')
expect(slider.getAttribute('aria-valuemax')).toBe('30')
})
it('should maintain UI consistency with different environment values', async () => {
setupEnvironment('15')
const Panel = await import('@/app/components/workflow/nodes/iteration/panel').then(mod => mod.default)
const { MAX_PARALLEL_LIMIT } = await import('@/config')
render(
<Panel
id="test-node"
// @ts-expect-error key type mismatch
data={mockNodeData.data}
/>,
)
// Both input and slider should use the same max value from MAX_PARALLEL_LIMIT
const { numberInput, slider } = getParallelControls()
expect(numberInput.getAttribute('max')).toBe(slider.getAttribute('aria-valuemax'))
expect(numberInput.getAttribute('max')).toBe(String(MAX_PARALLEL_LIMIT))
})
})
describe('Legacy Constant Verification (For Transition Period)', () => {
// Marked as transition/deprecation tests
it('should maintain MAX_ITERATION_PARALLEL_NUM for backward compatibility', async () => {
const { MAX_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants')
expect(typeof MAX_ITERATION_PARALLEL_NUM).toBe('number')
expect(MAX_ITERATION_PARALLEL_NUM).toBe(10) // Hardcoded legacy value
})
it('should demonstrate MAX_PARALLEL_LIMIT vs legacy constant difference', async () => {
setupEnvironment('50')
const { MAX_PARALLEL_LIMIT } = await import('@/config')
const { MAX_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants')
// MAX_PARALLEL_LIMIT is configurable, MAX_ITERATION_PARALLEL_NUM is not
expect(MAX_PARALLEL_LIMIT).toBe(50)
expect(MAX_ITERATION_PARALLEL_NUM).toBe(10)
expect(MAX_PARALLEL_LIMIT).not.toBe(MAX_ITERATION_PARALLEL_NUM)
})
})
describe('Constants Validation', () => {
it('should validate that required constants exist and have correct types', async () => {
const { MAX_PARALLEL_LIMIT } = await import('@/config')
const { MIN_ITERATION_PARALLEL_NUM } = await import('@/app/components/workflow/constants')
expect(typeof MAX_PARALLEL_LIMIT).toBe('number')
expect(typeof MIN_ITERATION_PARALLEL_NUM).toBe('number')
expect(MAX_PARALLEL_LIMIT).toBeGreaterThanOrEqual(MIN_ITERATION_PARALLEL_NUM)
})
})
})

View File

@@ -1,5 +1,5 @@
import type { RemixiconComponentType } from '@remixicon/react'
import { z } from 'zod'
import * as z from 'zod'
export const InputTypeEnum = z.enum([
'text-input',

View File

@@ -1,6 +1,6 @@
import type { ZodNumber, ZodSchema, ZodString } from 'zod'
import type { BaseConfiguration } from './types'
import { z } from 'zod'
import * as z from 'zod'
import { BaseFieldType } from './types'
export const generateZodSchema = (fields: BaseConfiguration[]) => {

View File

@@ -1,4 +1,4 @@
import { z } from 'zod'
import * as z from 'zod'
const ContactMethod = z.union([
z.literal('email'),
@@ -22,10 +22,10 @@ export const UserSchema = z.object({
.min(3, 'Surname must be at least 3 characters long')
.regex(/^[A-Z]/, 'Surname must start with a capital letter'),
isAcceptingTerms: z.boolean().refine(val => val, {
message: 'You must accept the terms and conditions',
error: 'You must accept the terms and conditions',
}),
contact: z.object({
email: z.string().email('Invalid email address'),
email: z.email('Invalid email address'),
phone: z.string().optional(),
preferredContactMethod: ContactMethod,
}),

View File

@@ -1,6 +1,6 @@
import type { ZodSchema, ZodString } from 'zod'
import type { InputFieldConfiguration } from './types'
import { z } from 'zod'
import * as z from 'zod'
import { SupportedFileTypes, TransferMethod } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/schema'
import { InputFieldType } from './types'

View File

@@ -2,6 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { env } from '@/env'
import ParamItem from '.'
type Props = {
@@ -11,12 +12,7 @@ type Props = {
enable: boolean
}
const maxTopK = (() => {
const configValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-top-k-max-value') || '', 10)
if (configValue && !isNaN(configValue))
return configValue
return 10
})()
const maxTopK = env.NEXT_PUBLIC_TOP_K_MAX_VALUE
const VALUE_LIMIT = {
default: 2,
step: 1,

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import { noop } from 'es-toolkit/function'
import { z } from 'zod'
import * as z from 'zod'
import withValidation from '.'
describe('withValidation HOC', () => {

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { z } from 'zod'
import * as z from 'zod'
import withValidation from '.'
// Sample components to wrap with validation
@@ -65,7 +65,7 @@ const ProductCard = ({ name, price, category, inStock }: ProductCardProps) => {
// Create validated versions
const userSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
email: z.email('Invalid email'),
age: z.number().min(0).max(150),
})
@@ -371,7 +371,7 @@ export const ConfigurationValidation: Story = {
)
const configSchema = z.object({
apiUrl: z.string().url('Must be valid URL'),
apiUrl: z.url('Must be valid URL'),
timeout: z.number().min(0).max(30000),
retries: z.number().min(0).max(5),
debug: z.boolean(),
@@ -430,7 +430,7 @@ export const UsageDocumentation: Story = {
<div>
<h4 className="mb-2 text-sm font-semibold text-gray-900">Usage Example</h4>
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-xs text-gray-100">
{`import { z } from 'zod'
{`import * as z from 'zod'
import withValidation from './withValidation'
// Define your component

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { InputNumber } from '@/app/components/base/input-number'
import Tooltip from '@/app/components/base/tooltip'
import { env } from '@/env'
const TextLabel: FC<PropsWithChildren> = (props) => {
return <label className="text-xs font-semibold leading-none text-text-secondary">{props.children}</label>
@@ -46,7 +47,7 @@ export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) =>
}
export const MaxLengthInput: FC<InputNumberProps> = (props) => {
const maxValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10)
const maxValue = env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH
const { t } = useTranslation()
return (

View File

@@ -1,5 +1,6 @@
import type { ParentMode, PreProcessingRule, ProcessRule, Rules, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import { useCallback, useRef, useState } from 'react'
import { env } from '@/env'
import { ChunkingMode, ProcessMode } from '@/models/datasets'
import escape from './escape'
import unescape from './unescape'
@@ -8,10 +9,7 @@ import unescape from './unescape'
export const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n'
export const DEFAULT_MAXIMUM_CHUNK_LENGTH = 1024
export const DEFAULT_OVERLAP = 50
export const MAXIMUM_CHUNK_TOKEN_LENGTH = Number.parseInt(
globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000',
10,
)
export const MAXIMUM_CHUNK_TOKEN_LENGTH = env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH
export type ParentChildConfig = {
chunkForContext: ParentMode

View File

@@ -1,7 +1,7 @@
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { z } from 'zod'
import * as z from 'zod'
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
import Toast from '@/app/components/base/toast'
import Actions from './actions'
@@ -53,7 +53,7 @@ const createFailingSchema = () => {
issues: [{ path: ['field1'], message: 'is required' }],
},
}),
} as unknown as z.ZodSchema
} as unknown as z.ZodType
}
// ==========================================

View File

@@ -28,6 +28,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
import { useDocLink } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { env } from '@/env'
import { useLogout } from '@/service/use-common'
import { cn } from '@/utils/classnames'
import AccountAbout from '../account-about'
@@ -178,7 +179,7 @@ export default function AppSelector() {
</Link>
</MenuItem>
{
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
<MenuItem>
<div
className={cn(itemClassName, 'justify-between', 'data-[active]:bg-state-base-hover')}

View File

@@ -3,6 +3,7 @@
import { SerwistProvider } from '@serwist/turbopack/react'
import { useEffect } from 'react'
import { IS_DEV } from '@/config'
import { env } from '@/env'
import { isClient } from '@/utils/client'
export function PWAProvider({ children }: { children: React.ReactNode }) {
@@ -10,7 +11,7 @@ export function PWAProvider({ children }: { children: React.ReactNode }) {
return <DisabledPWAProvider>{children}</DisabledPWAProvider>
}
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
const basePath = env.NEXT_PUBLIC_BASE_PATH
const swUrl = `${basePath}/serwist/sw.js`
return (

View File

@@ -1,6 +1,6 @@
import type { TFunction } from 'i18next'
import type { SchemaOptions } from './types'
import { z } from 'zod'
import * as z from 'zod'
import { InputTypeEnum } from '@/app/components/base/form/components/field/input-type-select/types'
import { MAX_VAR_KEY_LENGTH } from '@/config'
import { PipelineInputVarType } from '@/models/pipeline'
@@ -41,49 +41,47 @@ export const createInputFieldSchema = (type: PipelineInputVarType, t: TFunction,
tooltips: z.string().optional(),
})
if (type === PipelineInputVarType.textInput || type === PipelineInputVarType.paragraph) {
return z.object({
return z.looseObject({
maxLength: z.number().min(1).max(TEXT_MAX_LENGTH),
default: z.string().optional(),
}).merge(commonSchema).passthrough()
}).extend(commonSchema.shape)
}
if (type === PipelineInputVarType.number) {
return z.object({
return z.looseObject({
default: z.number().optional(),
unit: z.string().optional(),
placeholder: z.string().optional(),
}).merge(commonSchema).passthrough()
}).extend(commonSchema.shape)
}
if (type === PipelineInputVarType.select) {
return z.object({
options: z.array(z.string()).nonempty({
message: t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' }),
}).refine(
return z.looseObject({
options: z.tuple([z.string()], z.string()).refine(
arr => new Set(arr).size === arr.length,
{
message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }),
},
),
default: z.string().optional(),
}).merge(commonSchema).passthrough()
}).extend(commonSchema.shape)
}
if (type === PipelineInputVarType.singleFile) {
return z.object({
return z.looseObject({
allowedFileUploadMethods: z.array(TransferMethod),
allowedTypesAndExtensions: z.object({
allowedTypesAndExtensions: z.looseObject({
allowedFileExtensions: z.array(z.string()).optional(),
allowedFileTypes: z.array(SupportedFileTypes),
}),
}).merge(commonSchema).passthrough()
}).extend(commonSchema.shape)
}
if (type === PipelineInputVarType.multiFiles) {
return z.object({
return z.looseObject({
allowedFileUploadMethods: z.array(TransferMethod),
allowedTypesAndExtensions: z.object({
allowedTypesAndExtensions: z.looseObject({
allowedFileExtensions: z.array(z.string()).optional(),
allowedFileTypes: z.array(SupportedFileTypes),
}),
maxLength: z.number().min(1).max(maxFileUploadLimit),
}).merge(commonSchema).passthrough()
}).extend(commonSchema.shape)
}
return commonSchema.passthrough()
return z.looseObject(commonSchema.shape)
}

View File

@@ -4,15 +4,16 @@ import * as Sentry from '@sentry/react'
import { useEffect } from 'react'
import { IS_DEV } from '@/config'
import { env } from '@/env'
const SentryInitializer = ({
children,
}: { children: React.ReactElement }) => {
useEffect(() => {
const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
if (!IS_DEV && SENTRY_DSN) {
const sentryDsn = env.NEXT_PUBLIC_SENTRY_DSN
if (!IS_DEV && sentryDsn) {
Sentry.init({
dsn: SENTRY_DSN,
dsn: sentryDsn,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import { InputNumber } from '@/app/components/base/input-number'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import { env } from '@/env'
export type TopKAndScoreThresholdProps = {
topK: number
@@ -15,12 +16,7 @@ export type TopKAndScoreThresholdProps = {
hiddenScoreThreshold?: boolean
}
const maxTopK = (() => {
const configValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-top-k-max-value') || '', 10)
if (configValue && !isNaN(configValue))
return configValue
return 10
})()
const maxTopK = env.NEXT_PUBLIC_TOP_K_MAX_VALUE
const TOP_K_VALUE_LIMIT = {
amount: 1,
min: 1,

View File

@@ -1,6 +1,6 @@
import type { ValidationError } from 'jsonschema'
import type { ArrayItems, Field, LLMNodeType } from './types'
import { z } from 'zod'
import * as z from 'zod'
import { draft07Validator, forbidBooleanProperties } from '@/utils/validators'
import { ArrayType, Type } from './types'

View File

@@ -1,4 +1,4 @@
import { z } from 'zod'
import * as z from 'zod'
const arrayStringSchemaParttern = z.array(z.string())
const arrayNumberSchemaParttern = z.array(z.number())
@@ -7,7 +7,7 @@ const arrayNumberSchemaParttern = z.array(z.number())
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()])
type Literal = z.infer<typeof literalSchema>
type Json = Literal | { [key: string]: Json } | Json[]
const jsonSchema: z.ZodType<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))
const jsonSchema: z.ZodType<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(z.string(), jsonSchema)]))
const arrayJsonSchema: z.ZodType<Json[]> = z.lazy(() => z.array(jsonSchema))
export const validateJSONSchema = (schema: any, type: string) => {

View File

@@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { z } from 'zod'
import * as z from 'zod'
import Button from '@/app/components/base/button'
import { formContext, useAppForm } from '@/app/components/base/form'
import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator'
@@ -22,10 +22,10 @@ import Input from '../components/base/input'
import Loading from '../components/base/loading'
const accountFormSchema = z.object({
email: z
.string()
.min(1, { message: 'error.emailInValid' })
.email('error.emailInValid'),
email: z.email('error.emailInValid')
.min(1, {
error: 'error.emailInValid',
}),
})
const ForgotPasswordForm = () => {

View File

@@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { z } from 'zod'
import * as z from 'zod'
import Button from '@/app/components/base/button'
import { formContext, useAppForm } from '@/app/components/base/form'
import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator'
@@ -22,13 +22,15 @@ import { encryptPassword as encodePassword } from '@/utils/encryption'
import Loading from '../components/base/loading'
const accountFormSchema = z.object({
email: z
.string()
.min(1, { message: 'error.emailInValid' })
.email('error.emailInValid'),
name: z.string().min(1, { message: 'error.nameEmpty' }),
email: z.email('error.emailInValid')
.min(1, {
error: 'error.emailInValid',
}),
name: z.string().min(1, {
error: 'error.nameEmpty',
}),
password: z.string().min(8, {
message: 'error.passwordLengthInValid',
error: 'error.passwordLengthInValid',
}).regex(validPassword, 'error.passwordInvalid'),
})
@@ -197,7 +199,7 @@ const InstallForm = () => {
</div>
<div className={cn('mt-1 text-xs text-text-secondary', {
'text-red-400 !text-sm': passwordErrors && passwordErrors.length > 0,
'!text-sm text-red-400': passwordErrors && passwordErrors.length > 0,
})}
>
{t('error.passwordInvalid', { ns: 'login' })}

View File

@@ -5,8 +5,8 @@ import { Instrument_Serif } from 'next/font/google'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import GlobalPublicStoreProvider from '@/context/global-public-context'
import { TanstackQueryInitializer } from '@/context/query-client'
import { getDatasetMap } from '@/env'
import { getLocaleOnServer } from '@/i18n-config/server'
import { DatasetAttr } from '@/types/feature'
import { cn } from '@/utils/classnames'
import { ToastProvider } from './components/base/toast'
import BrowserInitializer from './components/browser-initializer'
@@ -39,40 +39,7 @@ const LocaleLayout = async ({
children: React.ReactNode
}) => {
const locale = await getLocaleOnServer()
const datasetMap: Record<DatasetAttr, string | undefined> = {
[DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX,
[DatasetAttr.DATA_PUBLIC_API_PREFIX]: process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX,
[DatasetAttr.DATA_MARKETPLACE_API_PREFIX]: process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX,
[DatasetAttr.DATA_MARKETPLACE_URL_PREFIX]: process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX,
[DatasetAttr.DATA_PUBLIC_EDITION]: process.env.NEXT_PUBLIC_EDITION,
[DatasetAttr.DATA_PUBLIC_AMPLITUDE_API_KEY]: process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY,
[DatasetAttr.DATA_PUBLIC_COOKIE_DOMAIN]: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
[DatasetAttr.DATA_PUBLIC_SUPPORT_MAIL_LOGIN]: process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN,
[DatasetAttr.DATA_PUBLIC_SENTRY_DSN]: process.env.NEXT_PUBLIC_SENTRY_DSN,
[DatasetAttr.DATA_PUBLIC_MAINTENANCE_NOTICE]: process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE,
[DatasetAttr.DATA_PUBLIC_SITE_ABOUT]: process.env.NEXT_PUBLIC_SITE_ABOUT,
[DatasetAttr.DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS]: process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS,
[DatasetAttr.DATA_PUBLIC_MAX_TOOLS_NUM]: process.env.NEXT_PUBLIC_MAX_TOOLS_NUM,
[DatasetAttr.DATA_PUBLIC_MAX_PARALLEL_LIMIT]: process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT,
[DatasetAttr.DATA_PUBLIC_TOP_K_MAX_VALUE]: process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE,
[DatasetAttr.DATA_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH]: process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH,
[DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT]: process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT,
[DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM]: process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM,
[DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH]: process.env.NEXT_PUBLIC_MAX_TREE_DEPTH,
[DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME]: process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL,
[DatasetAttr.DATA_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX]: process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_WIDGET_KEY]: process.env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN,
[DatasetAttr.DATA_PUBLIC_BATCH_CONCURRENCY]: process.env.NEXT_PUBLIC_BATCH_CONCURRENCY,
}
const datasetMap = getDatasetMap()
return (
<html lang={locale ?? 'en'} className={cn('h-full', instrumentSerif.variable)} suppressHydrationWarning>

View File

@@ -1,6 +1,7 @@
import { createSerwistRoute } from '@serwist/turbopack'
import { env } from '@/env'
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
const basePath = env.NEXT_PUBLIC_BASE_PATH
export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } = createSerwistRoute({
swSrc: 'app/sw.ts',

View File

@@ -1,101 +1,51 @@
import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { InputVarType } from '@/app/components/workflow/types'
import { env } from '@/env'
import { PromptRole } from '@/models/debug'
import { PipelineInputVarType } from '@/models/pipeline'
import { AgentStrategy } from '@/types/app'
import { DatasetAttr } from '@/types/feature'
import pkg from '../package.json'
const getBooleanConfig = (
envVar: string | undefined,
dataAttrKey: DatasetAttr,
defaultValue: boolean = true,
) => {
if (envVar !== undefined && envVar !== '')
return envVar === 'true'
const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey)
if (attrValue !== undefined && attrValue !== '')
return attrValue === 'true'
return defaultValue
}
const getNumberConfig = (
envVar: string | undefined,
dataAttrKey: DatasetAttr,
defaultValue: number,
) => {
if (envVar) {
const parsed = Number.parseInt(envVar)
if (!Number.isNaN(parsed) && parsed > 0)
return parsed
}
const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey)
if (attrValue) {
const parsed = Number.parseInt(attrValue)
if (!Number.isNaN(parsed) && parsed > 0)
return parsed
}
return defaultValue
}
const getStringConfig = (
envVar: string | undefined,
dataAttrKey: DatasetAttr,
defaultValue: string,
) => {
if (envVar)
return envVar
const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey)
if (attrValue)
return attrValue
return defaultValue
}
export const API_PREFIX = getStringConfig(
process.env.NEXT_PUBLIC_API_PREFIX,
DatasetAttr.DATA_API_PREFIX,
env.NEXT_PUBLIC_API_PREFIX,
'http://localhost:5001/console/api',
)
export const PUBLIC_API_PREFIX = getStringConfig(
process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX,
DatasetAttr.DATA_PUBLIC_API_PREFIX,
env.NEXT_PUBLIC_PUBLIC_API_PREFIX,
'http://localhost:5001/api',
)
export const MARKETPLACE_API_PREFIX = getStringConfig(
process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX,
DatasetAttr.DATA_MARKETPLACE_API_PREFIX,
env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX,
'http://localhost:5002/api',
)
export const MARKETPLACE_URL_PREFIX = getStringConfig(
process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX,
DatasetAttr.DATA_MARKETPLACE_URL_PREFIX,
env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX,
'',
)
const EDITION = getStringConfig(
process.env.NEXT_PUBLIC_EDITION,
DatasetAttr.DATA_PUBLIC_EDITION,
'SELF_HOSTED',
)
const EDITION = env.NEXT_PUBLIC_EDITION
export const IS_CE_EDITION = EDITION === 'SELF_HOSTED'
export const IS_CLOUD_EDITION = EDITION === 'CLOUD'
export const AMPLITUDE_API_KEY = getStringConfig(
process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY,
DatasetAttr.DATA_PUBLIC_AMPLITUDE_API_KEY,
env.NEXT_PUBLIC_AMPLITUDE_API_KEY,
'',
)
export const IS_DEV = process.env.NODE_ENV === 'development'
export const IS_PROD = process.env.NODE_ENV === 'production'
export const IS_DEV = env.NODE_ENV === 'development'
export const IS_PROD = env.NODE_ENV === 'production'
export const SUPPORT_MAIL_LOGIN = !!(
process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN
|| globalThis.document?.body?.getAttribute('data-public-support-mail-login')
)
export const SUPPORT_MAIL_LOGIN = env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN
export const TONE_LIST = [
{
@@ -161,16 +111,11 @@ export const getMaxToken = (modelId: string) => {
export const LOCALE_COOKIE_NAME = 'locale'
const COOKIE_DOMAIN = getStringConfig(
process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
DatasetAttr.DATA_PUBLIC_COOKIE_DOMAIN,
env.NEXT_PUBLIC_COOKIE_DOMAIN,
'',
).trim()
export const BATCH_CONCURRENCY = getNumberConfig(
process.env.NEXT_PUBLIC_BATCH_CONCURRENCY,
DatasetAttr.DATA_PUBLIC_BATCH_CONCURRENCY,
5, // default
)
export const BATCH_CONCURRENCY = env.NEXT_PUBLIC_BATCH_CONCURRENCY
export const CSRF_COOKIE_NAME = () => {
if (COOKIE_DOMAIN)
@@ -342,112 +287,62 @@ export const VAR_REGEX
export const resetReg = () => (VAR_REGEX.lastIndex = 0)
export const DISABLE_UPLOAD_IMAGE_AS_ICON
= process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true'
= env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON
export const GITHUB_ACCESS_TOKEN
= process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN || ''
= env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN
export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl'
export const FULL_DOC_PREVIEW_LENGTH = 50
export const JSON_SCHEMA_MAX_DEPTH = 10
export const MAX_TOOLS_NUM = getNumberConfig(
process.env.NEXT_PUBLIC_MAX_TOOLS_NUM,
DatasetAttr.DATA_PUBLIC_MAX_TOOLS_NUM,
10,
)
export const MAX_PARALLEL_LIMIT = getNumberConfig(
process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT,
DatasetAttr.DATA_PUBLIC_MAX_PARALLEL_LIMIT,
10,
)
export const TEXT_GENERATION_TIMEOUT_MS = getNumberConfig(
process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS,
DatasetAttr.DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS,
60000,
)
export const LOOP_NODE_MAX_COUNT = getNumberConfig(
process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT,
DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT,
100,
)
export const MAX_ITERATIONS_NUM = getNumberConfig(
process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM,
DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM,
99,
)
export const MAX_TREE_DEPTH = getNumberConfig(
process.env.NEXT_PUBLIC_MAX_TREE_DEPTH,
DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH,
50,
)
export const MAX_TOOLS_NUM = env.NEXT_PUBLIC_MAX_TOOLS_NUM
export const MAX_PARALLEL_LIMIT = env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
export const TEXT_GENERATION_TIMEOUT_MS = env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS
export const LOOP_NODE_MAX_COUNT = env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT
export const MAX_ITERATIONS_NUM = env.NEXT_PUBLIC_MAX_ITERATIONS_NUM
export const MAX_TREE_DEPTH = env.NEXT_PUBLIC_MAX_TREE_DEPTH
export const ALLOW_UNSAFE_DATA_SCHEME = getBooleanConfig(
process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME,
DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME,
false,
)
export const ENABLE_WEBSITE_JINAREADER = getBooleanConfig(
process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER,
DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER,
true,
)
export const ENABLE_WEBSITE_FIRECRAWL = getBooleanConfig(
process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL,
DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL,
true,
)
export const ENABLE_WEBSITE_WATERCRAWL = getBooleanConfig(
process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL,
DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL,
false,
)
export const ENABLE_SINGLE_DOLLAR_LATEX = getBooleanConfig(
process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX,
DatasetAttr.DATA_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX,
false,
)
export const ALLOW_UNSAFE_DATA_SCHEME = env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME
export const ENABLE_WEBSITE_JINAREADER = env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER
export const ENABLE_WEBSITE_FIRECRAWL = env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL
export const ENABLE_WEBSITE_WATERCRAWL = env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL
export const ENABLE_SINGLE_DOLLAR_LATEX = env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX
export const VALUE_SELECTOR_DELIMITER = '@@@'
export const validPassword = /^(?=.*[a-z])(?=.*\d)\S{8,}$/i
export const ZENDESK_WIDGET_KEY = getStringConfig(
process.env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY,
DatasetAttr.NEXT_PUBLIC_ZENDESK_WIDGET_KEY,
env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY,
'',
)
export const ZENDESK_FIELD_IDS = {
ENVIRONMENT: getStringConfig(
process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT,
DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT,
env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT,
'',
),
VERSION: getStringConfig(
process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION,
DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION,
env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION,
'',
),
EMAIL: getStringConfig(
process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL,
DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL,
env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL,
'',
),
WORKSPACE_ID: getStringConfig(
process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID,
DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID,
env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID,
'',
),
PLAN: getStringConfig(
process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN,
DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN,
env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN,
'',
),
}
export const APP_VERSION = pkg.version
export const IS_MARKETPLACE = globalThis.document?.body?.getAttribute('data-is-marketplace') === 'true'
export const IS_MARKETPLACE = env.NEXT_PUBLIC_IS_MARKETPLACE
export const RAG_PIPELINE_PREVIEW_CHUNK_NUM = 20

View File

@@ -10,6 +10,7 @@ import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { ZENDESK_FIELD_IDS } from '@/config'
import { env } from '@/env'
import {
useCurrentWorkspace,
useLangGeniusVersion,
@@ -204,7 +205,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
}}
>
<div className="flex h-full flex-col overflow-y-auto">
{globalThis.document?.body?.getAttribute('data-public-maintenance-notice') && <MaintenanceNotice />}
{env.NEXT_PUBLIC_MAINTENANCE_NOTICE && <MaintenanceNotice />}
<div className="relative flex grow flex-col overflow-y-auto overflow-x-hidden bg-background-body">
{children}
</div>

233
web/env.ts Normal file
View File

@@ -0,0 +1,233 @@
import type { CamelCase, Replace } from 'string-ts'
import { createEnv } from '@t3-oss/env-nextjs'
import { concat, kebabCase, length, slice } from 'string-ts'
import * as z from 'zod'
import { isClient, isServer } from './utils/client'
import { ObjectFromEntries, ObjectKeys } from './utils/object'
const CLIENT_ENV_PREFIX = 'NEXT_PUBLIC_'
type ClientSchema = Record<`${typeof CLIENT_ENV_PREFIX}${string}`, z.ZodType>
const coercedBoolean = z.string().transform(s => s !== 'false' && s !== '0')
const coercedNumber = z.coerce.number().int().positive()
/// keep-sorted
const clientSchema = {
/**
* Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
*/
NEXT_PUBLIC_ALLOW_EMBED: coercedBoolean.default(false),
/**
* Allow rendering unsafe URLs which have "data:" scheme.
*/
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME: coercedBoolean.default(false),
/**
* The API key of amplitude
*/
NEXT_PUBLIC_AMPLITUDE_API_KEY: z.string().optional(),
/**
* The base URL of console application, refers to the Console base URL of WEB service if console domain is
* different from api or web app domain.
* example: http://cloud.dify.ai/console/api
*/
NEXT_PUBLIC_API_PREFIX: z.url().optional(),
/**
* The base path for the application
*/
NEXT_PUBLIC_BASE_PATH: z.string().regex(/^\/.*[^/]$/).or(z.literal('')).default(''),
/**
* number of concurrency
*/
NEXT_PUBLIC_BATCH_CONCURRENCY: coercedNumber.default(5),
/**
* When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1.
*/
NEXT_PUBLIC_COOKIE_DOMAIN: z.string().optional(),
/**
* CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
*/
NEXT_PUBLIC_CSP_WHITELIST: z.string().optional(),
/**
* For production release, change this to PRODUCTION
*/
NEXT_PUBLIC_DEPLOY_ENV: z.enum(['DEVELOPMENT', 'PRODUCTION', 'TESTING']).optional(),
NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON: coercedBoolean.default(false),
/**
* The deployment edition, SELF_HOSTED
*/
NEXT_PUBLIC_EDITION: z.enum(['SELF_HOSTED', 'CLOUD']).default('SELF_HOSTED'),
/**
* Enable inline LaTeX rendering with single dollar signs ($...$)
* Default is false for security reasons to prevent conflicts with regular text
*/
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX: coercedBoolean.default(false),
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: coercedBoolean.default(true),
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: coercedBoolean.default(true),
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: coercedBoolean.default(false),
/**
* Github Access Token, used for invoking Github API
*/
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN: z.string().default(''),
/**
* The maximum number of tokens for segmentation
*/
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: coercedNumber.default(4000),
NEXT_PUBLIC_IS_MARKETPLACE: coercedBoolean.default(false),
/**
* Maximum loop count in the workflow
*/
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: coercedNumber.default(100),
NEXT_PUBLIC_MAINTENANCE_NOTICE: z.string().optional(),
/**
* The API PREFIX for MARKETPLACE
*/
NEXT_PUBLIC_MARKETPLACE_API_PREFIX: z.url().optional(),
/**
* The URL for MARKETPLACE
*/
NEXT_PUBLIC_MARKETPLACE_URL_PREFIX: z.url().optional(),
/**
* The maximum number of iterations for agent setting
*/
NEXT_PUBLIC_MAX_ITERATIONS_NUM: coercedNumber.default(99),
/**
* Maximum number of Parallelism branches in the workflow
*/
NEXT_PUBLIC_MAX_PARALLEL_LIMIT: coercedNumber.default(10),
/**
* Maximum number of tools in the agent/workflow
*/
NEXT_PUBLIC_MAX_TOOLS_NUM: coercedNumber.default(10),
/**
* The maximum number of tree node depth for workflow
*/
NEXT_PUBLIC_MAX_TREE_DEPTH: coercedNumber.default(50),
/**
* The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
* console or api domain.
* example: http://udify.app/api
*/
NEXT_PUBLIC_PUBLIC_API_PREFIX: z.url().optional(),
/**
* SENTRY
*/
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
NEXT_PUBLIC_SITE_ABOUT: z.string().optional(),
NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: coercedBoolean.default(false),
/**
* The timeout for the text generation in millisecond
*/
NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS: coercedNumber.default(60000),
/**
* The maximum number of top-k value for RAG.
*/
NEXT_PUBLIC_TOP_K_MAX_VALUE: coercedNumber.default(10),
/**
* Disable Upload Image as WebApp icon default is false
*/
NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON: coercedBoolean.default(false),
NEXT_PUBLIC_WEB_PREFIX: z.url().optional(),
NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL: z.string().optional(),
NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT: z.string().optional(),
NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN: z.string().optional(),
NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION: z.string().optional(),
NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID: z.string().optional(),
NEXT_PUBLIC_ZENDESK_WIDGET_KEY: z.string().optional(),
} satisfies ClientSchema
export const env = createEnv({
server: {
/**
* Maximum length of segmentation tokens for indexing
*/
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: coercedNumber.default(4000),
/**
* Disable Next.js Telemetry (https://nextjs.org/telemetry)
*/
NEXT_TELEMETRY_DISABLED: coercedBoolean.optional(),
PORT: coercedNumber.default(3000),
/**
* The timeout for the text generation in millisecond
*/
TEXT_GENERATION_TIMEOUT_MS: coercedNumber.default(60000),
},
shared: {
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
},
client: clientSchema,
experimental__runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_ALLOW_EMBED: isServer ? process.env.NEXT_PUBLIC_ALLOW_EMBED : getRuntimeEnvFromBody('allowEmbed'),
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME: isServer ? process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME : getRuntimeEnvFromBody('allowUnsafeDataScheme'),
NEXT_PUBLIC_AMPLITUDE_API_KEY: isServer ? process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY : getRuntimeEnvFromBody('amplitudeApiKey'),
NEXT_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('apiPrefix'),
NEXT_PUBLIC_BASE_PATH: isServer ? process.env.NEXT_PUBLIC_BASE_PATH : getRuntimeEnvFromBody('basePath'),
NEXT_PUBLIC_BATCH_CONCURRENCY: isServer ? process.env.NEXT_PUBLIC_BATCH_CONCURRENCY : getRuntimeEnvFromBody('batchConcurrency'),
NEXT_PUBLIC_COOKIE_DOMAIN: isServer ? process.env.NEXT_PUBLIC_COOKIE_DOMAIN : getRuntimeEnvFromBody('cookieDomain'),
NEXT_PUBLIC_CSP_WHITELIST: isServer ? process.env.NEXT_PUBLIC_CSP_WHITELIST : getRuntimeEnvFromBody('cspWhitelist'),
NEXT_PUBLIC_DEPLOY_ENV: isServer ? process.env.NEXT_PUBLIC_DEPLOY_ENV : getRuntimeEnvFromBody('deployEnv'),
NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON: isServer ? process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON : getRuntimeEnvFromBody('disableUploadImageAsIcon'),
NEXT_PUBLIC_EDITION: isServer ? process.env.NEXT_PUBLIC_EDITION : getRuntimeEnvFromBody('edition'),
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX: isServer ? process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX : getRuntimeEnvFromBody('enableSingleDollarLatex'),
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL : getRuntimeEnvFromBody('enableWebsiteFirecrawl'),
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER : getRuntimeEnvFromBody('enableWebsiteJinareader'),
NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL: isServer ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL : getRuntimeEnvFromBody('enableWebsiteWatercrawl'),
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN: isServer ? process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN : getRuntimeEnvFromBody('githubAccessToken'),
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: isServer ? process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH : getRuntimeEnvFromBody('indexingMaxSegmentationTokensLength'),
NEXT_PUBLIC_IS_MARKETPLACE: isServer ? process.env.NEXT_PUBLIC_IS_MARKETPLACE : getRuntimeEnvFromBody('isMarketplace'),
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: isServer ? process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT : getRuntimeEnvFromBody('loopNodeMaxCount'),
NEXT_PUBLIC_MAINTENANCE_NOTICE: isServer ? process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE : getRuntimeEnvFromBody('maintenanceNotice'),
NEXT_PUBLIC_MARKETPLACE_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX : getRuntimeEnvFromBody('marketplaceApiPrefix'),
NEXT_PUBLIC_MARKETPLACE_URL_PREFIX: isServer ? process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX : getRuntimeEnvFromBody('marketplaceUrlPrefix'),
NEXT_PUBLIC_MAX_ITERATIONS_NUM: isServer ? process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM : getRuntimeEnvFromBody('maxIterationsNum'),
NEXT_PUBLIC_MAX_PARALLEL_LIMIT: isServer ? process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT : getRuntimeEnvFromBody('maxParallelLimit'),
NEXT_PUBLIC_MAX_TOOLS_NUM: isServer ? process.env.NEXT_PUBLIC_MAX_TOOLS_NUM : getRuntimeEnvFromBody('maxToolsNum'),
NEXT_PUBLIC_MAX_TREE_DEPTH: isServer ? process.env.NEXT_PUBLIC_MAX_TREE_DEPTH : getRuntimeEnvFromBody('maxTreeDepth'),
NEXT_PUBLIC_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('publicApiPrefix'),
NEXT_PUBLIC_SENTRY_DSN: isServer ? process.env.NEXT_PUBLIC_SENTRY_DSN : getRuntimeEnvFromBody('sentryDsn'),
NEXT_PUBLIC_SITE_ABOUT: isServer ? process.env.NEXT_PUBLIC_SITE_ABOUT : getRuntimeEnvFromBody('siteAbout'),
NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: isServer ? process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN : getRuntimeEnvFromBody('supportMailLogin'),
NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS: isServer ? process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS : getRuntimeEnvFromBody('textGenerationTimeoutMs'),
NEXT_PUBLIC_TOP_K_MAX_VALUE: isServer ? process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE : getRuntimeEnvFromBody('topKMaxValue'),
NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON: isServer ? process.env.NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON : getRuntimeEnvFromBody('uploadImageAsIcon'),
NEXT_PUBLIC_WEB_PREFIX: isServer ? process.env.NEXT_PUBLIC_WEB_PREFIX : getRuntimeEnvFromBody('webPrefix'),
NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL : getRuntimeEnvFromBody('zendeskFieldIdEmail'),
NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT : getRuntimeEnvFromBody('zendeskFieldIdEnvironment'),
NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN : getRuntimeEnvFromBody('zendeskFieldIdPlan'),
NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION : getRuntimeEnvFromBody('zendeskFieldIdVersion'),
NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID: isServer ? process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID : getRuntimeEnvFromBody('zendeskFieldIdWorkspaceId'),
NEXT_PUBLIC_ZENDESK_WIDGET_KEY: isServer ? process.env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY : getRuntimeEnvFromBody('zendeskWidgetKey'),
},
emptyStringAsUndefined: true,
})
type ClientEnvKey = keyof typeof clientSchema
type DatasetKey = CamelCase<Replace<ClientEnvKey, typeof CLIENT_ENV_PREFIX>>
/**
* Browser-only function to get runtime env value from HTML body dataset.
*/
function getRuntimeEnvFromBody(key: DatasetKey) {
if (typeof window === 'undefined') {
throw new TypeError('getRuntimeEnvFromBody can only be called in the browser')
}
const value = document.body.dataset[key]
return value || undefined
}
/**
* Server-only function to get dataset map for embedding into the HTML body.
*/
export function getDatasetMap() {
if (isClient) {
throw new TypeError('getDatasetMap can only be called on the server')
}
return ObjectFromEntries(
ObjectKeys(clientSchema)
.map(envKey => [
concat('data-', kebabCase(slice(envKey, length(CLIENT_ENV_PREFIX)))),
env[envKey],
]),
)
}

View File

@@ -1614,11 +1614,6 @@
"count": 1
}
},
"app/components/base/param-item/top-k-item.tsx": {
"unicorn/prefer-number-properties": {
"count": 1
}
},
"app/components/base/portal-to-follow-elem/index.tsx": {
"react-refresh/only-export-components": {
"count": 2
@@ -4055,11 +4050,6 @@
"count": 4
}
},
"app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx": {
"unicorn/prefer-number-properties": {
"count": 1
}
},
"app/components/workflow/nodes/knowledge-base/components/retrieval-setting/type.ts": {
"ts/no-explicit-any": {
"count": 2
@@ -4912,11 +4902,6 @@
"count": 7
}
},
"app/install/installForm.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/reset-password/layout.tsx": {
"ts/no-explicit-any": {
"count": 1

View File

@@ -1,10 +1,9 @@
import type { NextConfig } from 'next'
import process from 'node:process'
import withBundleAnalyzerInit from '@next/bundle-analyzer'
import createMDX from '@next/mdx'
import { codeInspectorPlugin } from 'code-inspector-plugin'
import { env } from './env'
const isDev = process.env.NODE_ENV === 'development'
const isDev = env.NODE_ENV === 'development'
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
@@ -17,20 +16,17 @@ const withMDX = createMDX({
// providerImportSource: "@mdx-js/react",
},
})
const withBundleAnalyzer = withBundleAnalyzerInit({
enabled: process.env.ANALYZE === 'true',
})
// the default url to prevent parse url error when running jest
const hasSetWebPrefix = process.env.NEXT_PUBLIC_WEB_PREFIX
const port = process.env.PORT || 3000
const hasSetWebPrefix = env.NEXT_PUBLIC_WEB_PREFIX
const port = env.PORT
const locImageURLs = !hasSetWebPrefix ? [new URL(`http://localhost:${port}/**`), new URL(`http://127.0.0.1:${port}/**`)] : []
const remoteImageURLs = ([hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_WEB_PREFIX}/**`) : '', ...locImageURLs].filter(item => !!item)) as URL[]
const remoteImageURLs = ([hasSetWebPrefix ? new URL(`${env.NEXT_PUBLIC_WEB_PREFIX}/**`) : '', ...locImageURLs].filter(item => !!item)) as URL[]
const nextConfig: NextConfig = {
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
basePath: env.NEXT_PUBLIC_BASE_PATH,
serverExternalPackages: ['esbuild'],
transpilePackages: ['echarts', 'zrender'],
transpilePackages: ['@t3-oss/env-core', '@t3-oss/env-nextjs', 'echarts', 'zrender'],
turbopack: {
rules: codeInspectorPlugin({
bundler: 'turbopack',
@@ -72,4 +68,4 @@ const nextConfig: NextConfig = {
},
}
export default withBundleAnalyzer(withMDX(nextConfig))
export default withMDX(nextConfig)

View File

@@ -54,7 +54,7 @@
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build",
"preinstall": "npx only-allow pnpm",
"analyze": "ANALYZE=true pnpm build",
"analyze": "next experimental-analyze",
"knip": "knip"
},
"dependencies": {
@@ -82,6 +82,7 @@
"@remixicon/react": "4.7.0",
"@sentry/react": "8.55.0",
"@svgdotjs/svg.js": "3.2.5",
"@t3-oss/env-nextjs": "0.13.10",
"@tailwindcss/typography": "0.5.19",
"@tanstack/react-form": "1.23.7",
"@tanstack/react-query": "5.90.5",
@@ -159,7 +160,7 @@
"ufo": "1.6.3",
"use-context-selector": "2.0.0",
"uuid": "10.0.0",
"zod": "3.25.76",
"zod": "4.3.6",
"zundo": "2.3.0",
"zustand": "5.0.9"
},
@@ -169,7 +170,6 @@
"@eslint-react/eslint-plugin": "2.9.4",
"@mdx-js/loader": "3.1.1",
"@mdx-js/react": "3.1.1",
"@next/bundle-analyzer": "16.1.5",
"@next/eslint-plugin-next": "16.1.6",
"@next/mdx": "16.1.5",
"@rgrove/parse-xml": "4.2.0",

184
web/pnpm-lock.yaml generated
View File

@@ -125,6 +125,9 @@ importers:
'@svgdotjs/svg.js':
specifier: 3.2.5
version: 3.2.5
'@t3-oss/env-nextjs':
specifier: 0.13.10
version: 0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)
'@tailwindcss/typography':
specifier: 0.5.19
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
@@ -357,8 +360,8 @@ importers:
specifier: 10.0.0
version: 10.0.0
zod:
specifier: 3.25.76
version: 3.25.76
specifier: 4.3.6
version: 4.3.6
zundo:
specifier: 2.3.0
version: 2.3.0(zustand@5.0.9(@types/react@19.2.9)(immer@11.1.0)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))
@@ -381,9 +384,6 @@ importers:
'@mdx-js/react':
specifier: 3.1.1
version: 3.1.1(@types/react@19.2.9)(react@19.2.4)
'@next/bundle-analyzer':
specifier: 16.1.5
version: 16.1.5
'@next/eslint-plugin-next':
specifier: 16.1.6
version: 16.1.6
@@ -921,10 +921,6 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
@@ -1725,9 +1721,6 @@ packages:
'@neoconfetti/react@1.0.0':
resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==}
'@next/bundle-analyzer@16.1.5':
resolution: {integrity: sha512-/iPMrxbvgMZQX1huKZu+rnh7bxo2m5/o0PpOWLMRcAlQ2METpZ7/a3SP/aXFePZAyrQpgpndTldXW3LxPXM/KA==}
'@next/env@16.0.0':
resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==}
@@ -2817,6 +2810,40 @@ packages:
'@swc/types@0.1.25':
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
'@t3-oss/env-core@0.13.10':
resolution: {integrity: sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g==}
peerDependencies:
arktype: ^2.1.0
typescript: '>=5.0.0'
valibot: ^1.0.0-beta.7 || ^1.0.0
zod: ^3.24.0 || ^4.0.0
peerDependenciesMeta:
arktype:
optional: true
typescript:
optional: true
valibot:
optional: true
zod:
optional: true
'@t3-oss/env-nextjs@0.13.10':
resolution: {integrity: sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ==}
peerDependencies:
arktype: ^2.1.0
typescript: '>=5.0.0'
valibot: ^1.0.0-beta.7 || ^1.0.0
zod: ^3.24.0 || ^4.0.0
peerDependenciesMeta:
arktype:
optional: true
typescript:
optional: true
valibot:
optional: true
zod:
optional: true
'@tailwindcss/typography@0.5.19':
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
peerDependencies:
@@ -3577,10 +3604,6 @@ packages:
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn-walk@8.3.4:
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
engines: {node: '>=0.4.0'}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
@@ -4207,9 +4230,6 @@ packages:
dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -4309,9 +4329,6 @@ packages:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
echarts-for-react@3.0.5:
resolution: {integrity: sha512-YpEI5Ty7O/2nvCfQ7ybNa+S90DwE8KYZWacGvJW4luUqywP7qStQ+pxDlYOmr4jGDu10mhEkiAuMKcUlT4W5vg==}
peerDependencies:
@@ -4956,10 +4973,6 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
gzip-size@6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
engines: {node: '>=10'}
hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
@@ -5230,10 +5243,6 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@@ -5956,10 +5965,6 @@ packages:
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -6686,10 +6691,6 @@ packages:
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
sirv@2.0.4:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
engines: {node: '>= 10'}
sirv@3.0.2:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'}
@@ -7392,11 +7393,6 @@ packages:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
webpack-bundle-analyzer@4.10.1:
resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==}
engines: {node: '>= 10.13.0'}
hasBin: true
webpack-sources@3.3.3:
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
engines: {node: '>=10.13.0'}
@@ -7463,18 +7459,6 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@7.5.10:
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
engines: {node: '>=8.3.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
@@ -7538,9 +7522,6 @@ packages:
peerDependencies:
zod: ^3.25.0 || ^4.0.0
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
@@ -8047,8 +8028,6 @@ snapshots:
'@csstools/css-tokenizer@3.0.4': {}
'@discoveryjs/json-ext@0.5.7': {}
'@emnapi/core@1.8.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@@ -8239,7 +8218,7 @@ snapshots:
eslint: 9.39.2(jiti@1.21.7)
ts-pattern: 5.9.0
typescript: 5.9.3
zod: 3.25.76
zod: 4.3.6
transitivePeerDependencies:
- supports-color
@@ -8932,13 +8911,6 @@ snapshots:
'@neoconfetti/react@1.0.0': {}
'@next/bundle-analyzer@16.1.5':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@16.0.0': {}
'@next/env@16.1.5': {}
@@ -9239,7 +9211,8 @@ snapshots:
'@pkgr/core@0.2.9': {}
'@polka/url@1.0.0-next.29': {}
'@polka/url@1.0.0-next.29':
optional: true
'@preact/signals-core@1.12.2': {}
@@ -9954,6 +9927,20 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
'@t3-oss/env-core@0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)':
optionalDependencies:
typescript: 5.9.3
valibot: 1.2.0(typescript@5.9.3)
zod: 4.3.6
'@t3-oss/env-nextjs@0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)':
dependencies:
'@t3-oss/env-core': 0.13.10(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(zod@4.3.6)
optionalDependencies:
typescript: 5.9.3
valibot: 1.2.0(typescript@5.9.3)
zod: 4.3.6
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
postcss-selector-parser: 6.0.10
@@ -10946,10 +10933,6 @@ snapshots:
dependencies:
acorn: 8.15.0
acorn-walk@8.3.4:
dependencies:
acorn: 8.15.0
acorn@8.15.0: {}
agent-base@7.1.4: {}
@@ -11576,8 +11559,6 @@ snapshots:
dayjs@1.11.19: {}
debounce@1.2.1: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -11653,8 +11634,6 @@ snapshots:
dotenv@16.6.1: {}
duplexer@0.1.2: {}
echarts-for-react@3.0.5(echarts@5.6.0)(react@19.2.4):
dependencies:
echarts: 5.6.0
@@ -11948,8 +11927,8 @@ snapshots:
'@babel/parser': 7.28.6
eslint: 9.39.2(jiti@1.21.7)
hermes-parser: 0.25.1
zod: 3.25.76
zod-validation-error: 4.0.2(zod@3.25.76)
zod: 4.3.6
zod-validation-error: 4.0.2(zod@4.3.6)
transitivePeerDependencies:
- supports-color
@@ -12495,10 +12474,6 @@ snapshots:
graphemer@1.4.0: {}
gzip-size@6.0.0:
dependencies:
duplexer: 0.1.2
hachure-fill@0.5.2: {}
has-flag@4.0.0: {}
@@ -12816,8 +12791,6 @@ snapshots:
is-plain-obj@4.1.0: {}
is-plain-object@5.0.0: {}
is-potential-custom-element-name@1.0.1: {}
is-stream@3.0.0: {}
@@ -13703,7 +13676,8 @@ snapshots:
mri@1.2.0: {}
mrmime@2.0.1: {}
mrmime@2.0.1:
optional: true
ms@2.1.3: {}
@@ -13824,8 +13798,6 @@ snapshots:
openapi-types@12.1.3: {}
opener@1.5.2: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -14753,12 +14725,6 @@ snapshots:
dependencies:
is-arrayish: 0.3.4
sirv@2.0.4:
dependencies:
'@polka/url': 1.0.0-next.29
mrmime: 2.0.1
totalist: 3.0.1
sirv@3.0.2:
dependencies:
'@polka/url': 1.0.0-next.29
@@ -15061,7 +15027,8 @@ snapshots:
dependencies:
eslint-visitor-keys: 5.0.0
totalist@3.0.1: {}
totalist@3.0.1:
optional: true
tough-cookie@6.0.0:
dependencies:
@@ -15449,25 +15416,6 @@ snapshots:
webidl-conversions@8.0.1: {}
webpack-bundle-analyzer@4.10.1:
dependencies:
'@discoveryjs/json-ext': 0.5.7
acorn: 8.15.0
acorn-walk: 8.3.4
commander: 7.2.0
debounce: 1.2.1
escape-string-regexp: 4.0.0
gzip-size: 6.0.0
html-escaper: 2.0.2
is-plain-object: 5.0.0
opener: 1.5.2
picocolors: 1.1.1
sirv: 2.0.4
ws: 7.5.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
webpack-sources@3.3.3:
optional: true
@@ -15557,8 +15505,6 @@ snapshots:
wrappy@1.0.2:
optional: true
ws@7.5.10: {}
ws@8.19.0: {}
wsl-utils@0.1.0:
@@ -15595,11 +15541,9 @@ snapshots:
zen-observable@0.8.15: {}
zod-validation-error@4.0.2(zod@3.25.76):
zod-validation-error@4.0.2(zod@4.3.6):
dependencies:
zod: 3.25.76
zod@3.25.76: {}
zod: 4.3.6
zod@4.3.6: {}

View File

@@ -1,13 +1,14 @@
import type { NextRequest } from 'next/server'
import { Buffer } from 'node:buffer'
import { NextResponse } from 'next/server'
import { env } from '@/env'
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com https://api2.amplitude.com *.amplitude.com'
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
// Chatbot page should be allowed to be embedded in iframe. It's a feature
if (process.env.NEXT_PUBLIC_ALLOW_EMBED !== 'true' && !pathname.startsWith('/chat') && !pathname.startsWith('/workflow') && !pathname.startsWith('/completion') && !pathname.startsWith('/webapp-signin'))
if (env.NEXT_PUBLIC_ALLOW_EMBED !== true && !pathname.startsWith('/chat') && !pathname.startsWith('/workflow') && !pathname.startsWith('/completion') && !pathname.startsWith('/webapp-signin'))
response.headers.set('X-Frame-Options', 'DENY')
return response
@@ -21,11 +22,11 @@ export function proxy(request: NextRequest) {
},
})
const isWhiteListEnabled = !!process.env.NEXT_PUBLIC_CSP_WHITELIST && process.env.NODE_ENV === 'production'
const isWhiteListEnabled = !!env.NEXT_PUBLIC_CSP_WHITELIST && env.NODE_ENV === 'production'
if (!isWhiteListEnabled)
return wrapResponseWithXFrameOptions(response, pathname)
const whiteList = `${process.env.NEXT_PUBLIC_CSP_WHITELIST} ${NECESSARY_DOMAIN}`
const whiteList = `${env.NEXT_PUBLIC_CSP_WHITELIST} ${NECESSARY_DOMAIN}`
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `'nonce-${nonce}'`

View File

@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const loadGetBaseURL = async (isClientValue: boolean) => {
vi.resetModules()
vi.doMock('@/utils/client', () => ({ isClient: isClientValue }))
vi.doMock('@/utils/client', () => ({ isClient: isClientValue, isServer: !isClientValue }))
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
// eslint-disable-next-line next/no-assign-module-variable
const module = await import('./client')

View File

@@ -107,37 +107,3 @@ export const defaultSystemFeatures: SystemFeatures = {
enable_trial_app: false,
enable_explore_banner: false,
}
export enum DatasetAttr {
DATA_API_PREFIX = 'data-api-prefix',
DATA_PUBLIC_API_PREFIX = 'data-public-api-prefix',
DATA_MARKETPLACE_API_PREFIX = 'data-marketplace-api-prefix',
DATA_MARKETPLACE_URL_PREFIX = 'data-marketplace-url-prefix',
DATA_PUBLIC_EDITION = 'data-public-edition',
DATA_PUBLIC_AMPLITUDE_API_KEY = 'data-public-amplitude-api-key',
DATA_PUBLIC_COOKIE_DOMAIN = 'data-public-cookie-domain',
DATA_PUBLIC_SUPPORT_MAIL_LOGIN = 'data-public-support-mail-login',
DATA_PUBLIC_SENTRY_DSN = 'data-public-sentry-dsn',
DATA_PUBLIC_MAINTENANCE_NOTICE = 'data-public-maintenance-notice',
DATA_PUBLIC_SITE_ABOUT = 'data-public-site-about',
DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS = 'data-public-text-generation-timeout-ms',
DATA_PUBLIC_MAX_TOOLS_NUM = 'data-public-max-tools-num',
DATA_PUBLIC_MAX_PARALLEL_LIMIT = 'data-public-max-parallel-limit',
DATA_PUBLIC_TOP_K_MAX_VALUE = 'data-public-top-k-max-value',
DATA_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH = 'data-public-indexing-max-segmentation-tokens-length',
DATA_PUBLIC_LOOP_NODE_MAX_COUNT = 'data-public-loop-node-max-count',
DATA_PUBLIC_MAX_ITERATIONS_NUM = 'data-public-max-iterations-num',
DATA_PUBLIC_MAX_TREE_DEPTH = 'data-public-max-tree-depth',
DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME = 'data-public-allow-unsafe-data-scheme',
DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER = 'data-public-enable-website-jinareader',
DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL = 'data-public-enable-website-firecrawl',
DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL = 'data-public-enable-website-watercrawl',
DATA_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX = 'data-public-enable-single-dollar-latex',
NEXT_PUBLIC_ZENDESK_WIDGET_KEY = 'next-public-zendesk-widget-key',
NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT = 'next-public-zendesk-field-id-environment',
NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION = 'next-public-zendesk-field-id-version',
NEXT_PUBLIC_ZENDESK_FIELD_ID_EMAIL = 'next-public-zendesk-field-id-email',
NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID = 'next-public-zendesk-field-id-workspace-id',
NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN = 'next-public-zendesk-field-id-plan',
DATA_PUBLIC_BATCH_CONCURRENCY = 'data-public-batch-concurrency',
}

7
web/utils/object.ts Normal file
View File

@@ -0,0 +1,7 @@
export function ObjectFromEntries<const T extends ReadonlyArray<readonly [PropertyKey, unknown]>>(entries: T): { [K in T[number]as K[0]]: K[1] } {
return Object.fromEntries(entries) as { [K in T[number]as K[0]]: K[1] }
}
export function ObjectKeys<const T extends Record<string, unknown>>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[]
}

View File

@@ -8,6 +8,7 @@ import {
} from '@/app/components/base/prompt-editor/constants'
import { InputVarType } from '@/app/components/workflow/types'
import { getMaxVarNameLength, MARKETPLACE_URL_PREFIX, MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW } from '@/config'
import { env } from '@/env'
const otherAllowedRegex = /^\w+$/
@@ -129,7 +130,7 @@ export const getVars = (value: string) => {
// Set the value of basePath
// example: /dify
export const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
export const basePath = env.NEXT_PUBLIC_BASE_PATH
export function getMarketplaceUrl(path: string, params?: Record<string, string | undefined>) {
const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) })

View File

@@ -1,173 +0,0 @@
import { z, ZodError } from 'zod'
describe('Zod Features', () => {
it('should support string', () => {
const stringSchema = z.string()
const numberLikeStringSchema = z.coerce.string() // 12 would be converted to '12'
const stringSchemaWithError = z.string({
required_error: 'Name is required',
invalid_type_error: 'Invalid name type, expected string',
})
const urlSchema = z.string().url()
const uuidSchema = z.string().uuid()
expect(stringSchema.parse('hello')).toBe('hello')
expect(() => stringSchema.parse(12)).toThrow()
expect(numberLikeStringSchema.parse('12')).toBe('12')
expect(numberLikeStringSchema.parse(12)).toBe('12')
expect(() => stringSchemaWithError.parse(undefined)).toThrow('Name is required')
expect(() => stringSchemaWithError.parse(12)).toThrow('Invalid name type, expected string')
expect(urlSchema.parse('https://dify.ai')).toBe('https://dify.ai')
expect(uuidSchema.parse('123e4567-e89b-12d3-a456-426614174000')).toBe('123e4567-e89b-12d3-a456-426614174000')
})
it('should support enum', () => {
enum JobStatus {
waiting = 'waiting',
processing = 'processing',
completed = 'completed',
}
expect(z.nativeEnum(JobStatus).parse(JobStatus.waiting)).toBe(JobStatus.waiting)
expect(z.nativeEnum(JobStatus).parse('completed')).toBe('completed')
expect(() => z.nativeEnum(JobStatus).parse('invalid')).toThrow()
})
it('should support number', () => {
const numberSchema = z.number()
const numberWithMin = z.number().gt(0) // alias min
const numberWithMinEqual = z.number().gte(0)
const numberWithMax = z.number().lt(100) // alias max
expect(numberSchema.parse(123)).toBe(123)
expect(numberWithMin.parse(50)).toBe(50)
expect(numberWithMinEqual.parse(0)).toBe(0)
expect(() => numberWithMin.parse(-1)).toThrow()
expect(numberWithMax.parse(50)).toBe(50)
expect(() => numberWithMax.parse(101)).toThrow()
})
it('should support boolean', () => {
const booleanSchema = z.boolean()
expect(booleanSchema.parse(true)).toBe(true)
expect(booleanSchema.parse(false)).toBe(false)
expect(() => booleanSchema.parse('true')).toThrow()
})
it('should support date', () => {
const dateSchema = z.date()
expect(dateSchema.parse(new Date('2023-01-01'))).toEqual(new Date('2023-01-01'))
})
it('should support object', () => {
const userSchema = z.object({
id: z.union([z.string(), z.number()]),
name: z.string(),
email: z.string().email(),
age: z.number().min(0).max(120).optional(),
})
type User = z.infer<typeof userSchema>
const validUser: User = {
id: 1,
name: 'John',
email: 'john@example.com',
age: 30,
}
expect(userSchema.parse(validUser)).toEqual(validUser)
})
it('should support object optional field', () => {
const userSchema = z.object({
name: z.string(),
optionalField: z.optional(z.string()),
})
type User = z.infer<typeof userSchema>
const user: User = {
name: 'John',
}
const userWithOptionalField: User = {
name: 'John',
optionalField: 'optional',
}
expect(userSchema.safeParse(user).success).toEqual(true)
expect(userSchema.safeParse(userWithOptionalField).success).toEqual(true)
})
it('should support object intersection', () => {
const Person = z.object({
name: z.string(),
})
const Employee = z.object({
role: z.string(),
})
const EmployedPerson = z.intersection(Person, Employee)
const validEmployedPerson = {
name: 'John',
role: 'Developer',
}
expect(EmployedPerson.parse(validEmployedPerson)).toEqual(validEmployedPerson)
})
it('should support record', () => {
const recordSchema = z.record(z.string(), z.number())
const validRecord = {
a: 1,
b: 2,
}
expect(recordSchema.parse(validRecord)).toEqual(validRecord)
})
it('should support array', () => {
const numbersSchema = z.array(z.number())
const stringArraySchema = z.string().array()
expect(numbersSchema.parse([1, 2, 3])).toEqual([1, 2, 3])
expect(stringArraySchema.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c'])
})
it('should support promise', async () => {
const promiseSchema = z.promise(z.string())
const validPromise = Promise.resolve('success')
await expect(promiseSchema.parse(validPromise)).resolves.toBe('success')
})
it('should support unions', () => {
const unionSchema = z.union([z.string(), z.number()])
expect(unionSchema.parse('success')).toBe('success')
expect(unionSchema.parse(404)).toBe(404)
})
it('should support functions', () => {
const functionSchema = z.function().args(z.string(), z.number(), z.optional(z.string())).returns(z.number())
const validFunction = (name: string, age: number, _optional?: string): number => {
return age
}
expect(functionSchema.safeParse(validFunction).success).toEqual(true)
})
it('should support undefined, null, any, and void', () => {
const undefinedSchema = z.undefined()
const nullSchema = z.null()
const anySchema = z.any()
expect(undefinedSchema.parse(undefined)).toBeUndefined()
expect(nullSchema.parse(null)).toBeNull()
expect(anySchema.parse('anything')).toBe('anything')
expect(anySchema.parse(3)).toBe(3)
})
it('should safeParse would not throw', () => {
expect(z.string().safeParse('abc').success).toBe(true)
expect(z.string().safeParse(123).success).toBe(false)
expect(z.string().safeParse(123).error).toBeInstanceOf(ZodError)
})
})