diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs
index 62d896a..622c082 100644
--- a/apps/docs/next.config.mjs
+++ b/apps/docs/next.config.mjs
@@ -18,12 +18,20 @@ const config = {
},
],
},
+ experimental: {
+ turbo: {
+ resolveAlias: {
+ 'fumadocs-ui/components/callout': 'fumadocs-ui/dist/components/callout.js',
+ },
+ },
+ },
webpack: (config, { isServer }) => {
// Resolve the fumadocs virtual collection import to the local .source directory
config.resolve = config.resolve || {};
config.resolve.alias = {
...(config.resolve.alias || {}),
'fumadocs-mdx:collections': path.resolve(__dirname, '.source'),
+ 'fumadocs-ui/components/callout$': path.resolve(__dirname, '../../node_modules/fumadocs-ui/dist/components/callout.js'),
};
return config;
},
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 8f9e1f4..b9e62e5 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -5,7 +5,7 @@
"description": "ObjectStack Protocol Documentation Site",
"scripts": {
"site:dev": "next dev",
- "build": "next build",
+ "build": "NEXT_PRIVATE_BUILD_WORKER=1 next build",
"site:start": "next start",
"site:lint": "next lint"
},
diff --git a/content/docs/guides/field-types.cn.mdx b/content/docs/guides/field-types.cn.mdx
index df623d3..d149ad6 100644
--- a/content/docs/guides/field-types.cn.mdx
+++ b/content/docs/guides/field-types.cn.mdx
@@ -3,7 +3,6 @@ title: 字段类型参考
description: ObjectStack 所有字段类型的完整指南,包含示例和配置选项
---
-import { Callout } from 'fumadocs-ui/components/callout';
# 字段类型参考
@@ -66,9 +65,9 @@ email: Field.email({
})
```
-
+> ℹ️ **Info:**
自动验证邮箱格式:`user@domain.com`
-
+
---
@@ -107,9 +106,9 @@ api_key: Field.password({
})
```
-
+> ⚠️ **Warning:**
密码字段在 UI 中自动掩码,如果 `encryption: true` 则加密存储
-
+
---
@@ -137,9 +136,9 @@ html_content: Field.html({
})
```
-
+> ⚠️ **Warning:**
渲染前务必净化 HTML 内容,防止 XSS 攻击
-
+
---
@@ -370,11 +369,11 @@ account: Field.masterDetail('account', {
})
```
-
+> ℹ️ **Info:**
**主从关系 vs 查找:**
- **主从关系:** 紧密耦合,级联删除,子记录继承安全性
- **查找:** 松散耦合,删除时置空,独立安全性
-
+
---
@@ -451,9 +450,9 @@ days_open: Field.formula({
})
```
-
+> ℹ️ **Info:**
公式字段自动计算且只读。可用函数请参见 [公式函数](/docs/references/data/formulas)。
-
+
---
diff --git a/content/docs/guides/field-types.mdx b/content/docs/guides/field-types.mdx
index be8a9c3..b088c75 100644
--- a/content/docs/guides/field-types.mdx
+++ b/content/docs/guides/field-types.mdx
@@ -3,7 +3,6 @@ title: Field Types Reference
description: Complete guide to all ObjectStack field types with examples and configuration options
---
-import { Callout } from 'fumadocs-ui/components/callout';
# Field Types Reference
@@ -66,9 +65,9 @@ email: Field.email({
})
```
-
+> ℹ️ **Info:**
Automatically validates email format: `user@domain.com`
-
+
---
@@ -107,9 +106,9 @@ api_key: Field.password({
})
```
-
+> ⚠️ **Warning:**
Password fields are automatically masked in UI and encrypted if `encryption: true`
-
+
---
@@ -137,9 +136,9 @@ html_content: Field.html({
})
```
-
+> ⚠️ **Warning:**
Always sanitize HTML content before rendering to prevent XSS attacks
-
+
---
@@ -370,11 +369,11 @@ account: Field.masterDetail('account', {
})
```
-
+> ℹ️ **Info:**
**Master-Detail vs Lookup:**
- **Master-Detail:** Tight coupling, cascade deletes, child inherits security
- **Lookup:** Loose coupling, set null on delete, independent security
-
+
---
@@ -451,9 +450,9 @@ days_open: Field.formula({
})
```
-
+> ℹ️ **Info:**
Formula fields are automatically calculated and readonly. See [Formula Functions](/docs/references/data/formulas) for available functions.
-
+
---
diff --git a/content/docs/guides/view-configuration.mdx b/content/docs/guides/view-configuration.mdx
index dad717a..b8fd77b 100644
--- a/content/docs/guides/view-configuration.mdx
+++ b/content/docs/guides/view-configuration.mdx
@@ -3,7 +3,6 @@ title: View Configuration
description: Complete guide to configuring Grid, Kanban, Calendar, Gantt views and forms in ObjectStack
---
-import { Callout } from 'fumadocs-ui/components/callout';
# View Configuration Guide
diff --git a/content/docs/guides/workflows-validation.mdx b/content/docs/guides/workflows-validation.mdx
index 8d1fbe3..9872f2c 100644
--- a/content/docs/guides/workflows-validation.mdx
+++ b/content/docs/guides/workflows-validation.mdx
@@ -3,7 +3,6 @@ title: Validation Rules & Workflows
description: Complete guide to validation rules and workflow automation in ObjectStack
---
-import { Callout } from 'fumadocs-ui/components/callout';
# Validation Rules & Workflows
@@ -73,9 +72,9 @@ export const Opportunity = ObjectSchema.create({
});
```
-
+> ℹ️ **Info:**
**Important:** The `condition` is inverted logic - it defines when validation **FAILS**. If the condition evaluates to `TRUE`, the validation error is shown.
-
+
### Expression Examples
diff --git a/content/docs/index.cn.mdx b/content/docs/index.cn.mdx
index e76466e..e2e3fbd 100644
--- a/content/docs/index.cn.mdx
+++ b/content/docs/index.cn.mdx
@@ -23,7 +23,7 @@ import { Book, Compass, FileText, Layers } from 'lucide-react';
icon={}
title="概念"
href="/docs/concepts/manifesto"
- description="理解"意图优于实现"和"本地优先"架构的理念。"
+ description="理解「意图优于实现」和「本地优先」架构的理念。"
/>
}
diff --git a/packages/spec/src/data/query.test.ts b/packages/spec/src/data/query.test.ts
index 281e85e..fe38c95 100644
--- a/packages/spec/src/data/query.test.ts
+++ b/packages/spec/src/data/query.test.ts
@@ -1617,3 +1617,341 @@ describe('QuerySchema - Complex Queries', () => {
expect(() => QuerySchema.parse(query)).not.toThrow();
});
});
+
+describe('QuerySchema - Edge Cases and Null Handling', () => {
+ it('should handle null values in filter expressions', () => {
+ const query: QueryAST = {
+ object: 'account',
+ fields: ['name'],
+ filters: ['deleted_at', 'is_null', null],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle undefined for optional fields', () => {
+ const query: QueryAST = {
+ object: 'account',
+ fields: undefined,
+ filters: undefined,
+ sort: undefined,
+ aggregations: undefined,
+ joins: undefined,
+ groupBy: undefined,
+ having: undefined,
+ windowFunctions: undefined,
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle empty arrays', () => {
+ const query: QueryAST = {
+ object: 'account',
+ fields: [],
+ aggregations: [],
+ joins: [],
+ windowFunctions: [],
+ groupBy: [],
+ sort: [],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle zero and negative values in pagination', () => {
+ const queries = [
+ { object: 'account', top: 0, skip: 0 },
+ { object: 'account', top: 1, skip: 0 },
+ { object: 'account', top: 100, skip: 1000 },
+ ];
+
+ queries.forEach(query => {
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+ });
+
+ it('should handle complex nested null filters', () => {
+ const query: QueryAST = {
+ object: 'order',
+ fields: ['id'],
+ filters: [
+ ['approved_at', 'is_null', null],
+ 'and',
+ ['rejected_at', 'is_null', null],
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle optional alias in field nodes', () => {
+ const query: QueryAST = {
+ object: 'account',
+ fields: [
+ 'name',
+ { field: 'owner', fields: ['name', 'email'] },
+ { field: 'manager', fields: ['name'], alias: 'mgr' },
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle aggregation without field for COUNT', () => {
+ const query: QueryAST = {
+ object: 'order',
+ aggregations: [
+ { function: 'count', alias: 'total_count' },
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle optional distinct flag in aggregation', () => {
+ const query: QueryAST = {
+ object: 'order',
+ aggregations: [
+ { function: 'count', field: 'customer_id', alias: 'unique_customers', distinct: true },
+ { function: 'sum', field: 'amount', alias: 'total_amount' }, // distinct undefined
+ ],
+ groupBy: ['region'],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle optional properties in window functions', () => {
+ const query: QueryAST = {
+ object: 'sales',
+ fields: ['amount'],
+ windowFunctions: [
+ {
+ function: 'row_number',
+ alias: 'row_num',
+ over: {
+ // partitionBy and orderBy are optional
+ },
+ },
+ {
+ function: 'sum',
+ field: 'amount',
+ alias: 'total',
+ over: {
+ partitionBy: ['region'],
+ // orderBy is optional
+ },
+ },
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle optional frame in window specification', () => {
+ const query: QueryAST = {
+ object: 'transactions',
+ fields: ['amount'],
+ windowFunctions: [
+ {
+ function: 'sum',
+ field: 'amount',
+ alias: 'running_total',
+ over: {
+ orderBy: [{ field: 'date', order: 'asc' }],
+ frame: {
+ type: 'rows',
+ start: 'UNBOUNDED PRECEDING',
+ end: 'CURRENT ROW',
+ },
+ },
+ },
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle optional subquery in joins', () => {
+ const query: QueryAST = {
+ object: 'customer',
+ joins: [
+ {
+ type: 'left',
+ object: 'order',
+ on: ['customer.id', '=', 'order.customer_id'],
+ },
+ {
+ type: 'inner',
+ object: 'filtered_orders',
+ alias: 'fo',
+ on: ['customer.id', '=', 'fo.customer_id'],
+ subquery: {
+ object: 'order',
+ fields: ['customer_id', 'amount'],
+ filters: ['amount', '>', 1000],
+ },
+ },
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should reject invalid object type', () => {
+ expect(() => QuerySchema.parse({
+ object: 123, // Should be string
+ fields: ['name'],
+ })).toThrow();
+ });
+
+ it('should reject invalid field types in array', () => {
+ expect(() => QuerySchema.parse({
+ object: 'account',
+ fields: [123, 456], // Should be strings or objects
+ })).toThrow();
+ });
+
+ it('should reject invalid aggregation function', () => {
+ expect(() => QuerySchema.parse({
+ object: 'order',
+ aggregations: [
+ { function: 'invalid_func', alias: 'test' },
+ ],
+ })).toThrow();
+ });
+
+ it('should reject invalid join type', () => {
+ expect(() => QuerySchema.parse({
+ object: 'order',
+ joins: [
+ {
+ type: 'invalid_join',
+ object: 'customer',
+ on: ['order.customer_id', '=', 'customer.id'],
+ },
+ ],
+ })).toThrow();
+ });
+
+ it('should reject invalid window function', () => {
+ expect(() => QuerySchema.parse({
+ object: 'sales',
+ windowFunctions: [
+ {
+ function: 'invalid_window_func',
+ alias: 'test',
+ over: {},
+ },
+ ],
+ })).toThrow();
+ });
+
+ it('should reject invalid sort order', () => {
+ expect(() => QuerySchema.parse({
+ object: 'account',
+ sort: [{ field: 'name', order: 'invalid' }],
+ })).toThrow();
+ });
+});
+
+describe('QuerySchema - Type Coercion Edge Cases', () => {
+ it('should handle various data types in filter values', () => {
+ const queries = [
+ { object: 'account', filters: ['age', '>', 18] }, // number
+ { object: 'account', filters: ['active', '=', true] }, // boolean
+ { object: 'account', filters: ['name', '=', 'John'] }, // string
+ { object: 'account', filters: ['tags', 'in', ['a', 'b', 'c']] }, // array
+ { object: 'account', filters: ['value', 'between', [0, 100]] }, // array
+ ];
+
+ queries.forEach(query => {
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+ });
+
+ it('should handle boolean flags', () => {
+ const query: QueryAST = {
+ object: 'account',
+ fields: ['name'],
+ distinct: true,
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+
+ const query2: QueryAST = {
+ object: 'account',
+ fields: ['name'],
+ distinct: false,
+ };
+
+ expect(() => QuerySchema.parse(query2)).not.toThrow();
+ });
+
+ it('should handle default sort order', () => {
+ const query: QueryAST = {
+ object: 'account',
+ sort: [{ field: 'name' }], // order defaults to 'asc'
+ };
+
+ const result = QuerySchema.parse(query);
+ expect(result.sort?.[0].order).toBe('asc');
+ });
+
+ it('should handle mixed field types', () => {
+ const query: QueryAST = {
+ object: 'account',
+ fields: [
+ 'simple_field',
+ {
+ field: 'related_field',
+ fields: ['nested_field'],
+ alias: 'rel',
+ },
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle deeply nested filters', () => {
+ const query: QueryAST = {
+ object: 'order',
+ filters: [
+ [
+ ['status', '=', 'active'],
+ 'and',
+ ['amount', '>', 100],
+ ],
+ 'or',
+ [
+ ['priority', '=', 'high'],
+ 'and',
+ ['urgent', '=', true],
+ ],
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+
+ it('should handle complex having clauses', () => {
+ const query: QueryAST = {
+ object: 'order',
+ fields: ['customer_id'],
+ aggregations: [
+ { function: 'count', alias: 'order_count' },
+ { function: 'sum', field: 'amount', alias: 'total' },
+ ],
+ groupBy: ['customer_id'],
+ having: [
+ ['order_count', '>', 5],
+ 'and',
+ ['total', '>', 1000],
+ ],
+ };
+
+ expect(() => QuerySchema.parse(query)).not.toThrow();
+ });
+});
diff --git a/packages/spec/src/data/validation.test.ts b/packages/spec/src/data/validation.test.ts
index 159e418..fd3b368 100644
--- a/packages/spec/src/data/validation.test.ts
+++ b/packages/spec/src/data/validation.test.ts
@@ -5,6 +5,10 @@ import {
UniquenessValidationSchema,
StateMachineValidationSchema,
FormatValidationSchema,
+ CrossFieldValidationSchema,
+ AsyncValidationSchema,
+ CustomValidatorSchema,
+ ConditionalValidationSchema,
type ValidationRule,
} from './validation.zod';
@@ -1077,3 +1081,454 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
});
});
});
+
+describe('ValidationRuleSchema - Edge Cases and Null Handling', () => {
+ it('should handle null and undefined in optional fields', () => {
+ const validation = {
+ type: 'script' as const,
+ name: 'test_validation',
+ message: 'Test message',
+ condition: 'amount > 0',
+ active: undefined, // Should default to true
+ severity: undefined, // Should default to 'error'
+ };
+
+ const result = ScriptValidationSchema.parse(validation);
+ expect(result.active).toBe(true);
+ expect(result.severity).toBe('error');
+ });
+
+ it('should handle empty arrays in UniquenessValidation', () => {
+ expect(() => UniquenessValidationSchema.parse({
+ type: 'unique',
+ name: 'test_unique',
+ message: 'Must be unique',
+ fields: [], // Empty array should be valid but probably not useful
+ })).not.toThrow();
+ });
+
+ it('should handle undefined scope in UniquenessValidation', () => {
+ const validation = {
+ type: 'unique' as const,
+ name: 'unique_email',
+ message: 'Email must be unique',
+ fields: ['email'],
+ scope: undefined,
+ caseSensitive: undefined, // Should default to true
+ };
+
+ const result = UniquenessValidationSchema.parse(validation);
+ expect(result.caseSensitive).toBe(true);
+ });
+
+ it('should handle empty state transitions', () => {
+ const validation = {
+ type: 'state_machine' as const,
+ name: 'state_validation',
+ message: 'Invalid state transition',
+ field: 'status',
+ transitions: {
+ 'draft': [],
+ 'published': ['draft', 'archived'],
+ },
+ };
+
+ expect(() => StateMachineValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle undefined regex and format in FormatValidation', () => {
+ const validation = {
+ type: 'format' as const,
+ name: 'format_check',
+ message: 'Invalid format',
+ field: 'email',
+ format: 'email' as const,
+ regex: undefined,
+ };
+
+ expect(() => FormatValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle various format types', () => {
+ const formats = ['email', 'url', 'phone', 'json'] as const;
+
+ formats.forEach(format => {
+ const validation = {
+ type: 'format' as const,
+ name: `format_${format}`,
+ message: `Invalid ${format}`,
+ field: 'test_field',
+ format,
+ };
+
+ expect(() => FormatValidationSchema.parse(validation)).not.toThrow();
+ });
+ });
+
+ it('should handle empty fields array in CrossFieldValidation', () => {
+ const validation = {
+ type: 'cross_field' as const,
+ name: 'test_cross_field',
+ message: 'Validation failed',
+ condition: 'true',
+ fields: [], // Empty array
+ };
+
+ expect(() => CrossFieldValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle undefined optional fields in AsyncValidation', () => {
+ const validation = {
+ type: 'async' as const,
+ name: 'async_validation',
+ message: 'Validation failed',
+ field: 'email',
+ validatorUrl: '/api/validate',
+ validatorFunction: undefined,
+ debounce: undefined,
+ params: undefined,
+ };
+
+ const result = AsyncValidationSchema.parse(validation);
+ expect(result.timeout).toBe(5000); // Default timeout
+ });
+
+ it('should handle custom timeout in AsyncValidation', () => {
+ const validation = {
+ type: 'async' as const,
+ name: 'async_validation',
+ message: 'Validation failed',
+ field: 'email',
+ validatorUrl: '/api/validate',
+ timeout: 10000,
+ };
+
+ const result = AsyncValidationSchema.parse(validation);
+ expect(result.timeout).toBe(10000);
+ });
+
+ it('should handle undefined field in CustomValidator', () => {
+ const validation = {
+ type: 'custom' as const,
+ name: 'custom_validation',
+ message: 'Validation failed',
+ field: undefined, // Optional for record-level validation
+ validatorFunction: 'validateRecord',
+ params: undefined,
+ };
+
+ expect(() => CustomValidatorSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle empty params object', () => {
+ const validation = {
+ type: 'custom' as const,
+ name: 'custom_validation',
+ message: 'Validation failed',
+ validatorFunction: 'validateRecord',
+ params: {},
+ };
+
+ expect(() => CustomValidatorSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle undefined otherwise in ConditionalValidation', () => {
+ const validation = {
+ type: 'conditional' as const,
+ name: 'conditional_validation',
+ message: 'Conditional validation',
+ when: 'type = "special"',
+ then: {
+ type: 'script' as const,
+ name: 'special_validation',
+ message: 'Special type validation',
+ condition: 'amount > 100',
+ },
+ otherwise: undefined,
+ };
+
+ expect(() => ConditionalValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should reject missing required fields', () => {
+ expect(() => ScriptValidationSchema.parse({
+ type: 'script',
+ // Missing name, message, and condition
+ })).toThrow();
+ });
+
+ it('should reject invalid validation type', () => {
+ expect(() => ValidationRuleSchema.parse({
+ type: 'invalid_type',
+ name: 'test',
+ message: 'Test',
+ })).toThrow();
+ });
+
+ it('should reject invalid severity level', () => {
+ expect(() => ScriptValidationSchema.parse({
+ type: 'script',
+ name: 'test',
+ message: 'Test',
+ condition: 'true',
+ severity: 'invalid',
+ })).toThrow();
+ });
+});
+
+describe('ValidationRuleSchema - Type Coercion Edge Cases', () => {
+ it('should handle boolean active flag', () => {
+ const testCases = [
+ { active: true, expected: true },
+ { active: false, expected: false },
+ ];
+
+ testCases.forEach(({ active, expected }) => {
+ const validation = {
+ type: 'script' as const,
+ name: 'test',
+ message: 'Test',
+ condition: 'true',
+ active,
+ };
+
+ const result = ScriptValidationSchema.parse(validation);
+ expect(result.active).toBe(expected);
+ });
+ });
+
+ it('should handle caseSensitive boolean in uniqueness validation', () => {
+ const validation = {
+ type: 'unique' as const,
+ name: 'unique_test',
+ message: 'Must be unique',
+ fields: ['field1'],
+ caseSensitive: false,
+ };
+
+ const result = UniquenessValidationSchema.parse(validation);
+ expect(result.caseSensitive).toBe(false);
+ });
+
+ it('should handle distinct boolean in aggregation', () => {
+ const validation = {
+ type: 'async' as const,
+ name: 'async_test',
+ message: 'Validation failed',
+ field: 'test',
+ validatorUrl: '/api/validate',
+ debounce: 500,
+ timeout: 3000,
+ };
+
+ const result = AsyncValidationSchema.parse(validation);
+ expect(result.debounce).toBe(500);
+ expect(result.timeout).toBe(3000);
+ });
+
+ it('should handle various param types', () => {
+ const paramsTests = [
+ { params: { key: 'value' } },
+ { params: { nested: { key: 'value' } } },
+ { params: { array: [1, 2, 3] } },
+ { params: { boolean: true, number: 42, string: 'test' } },
+ ];
+
+ paramsTests.forEach(({ params }) => {
+ const validation = {
+ type: 'custom' as const,
+ name: 'custom_test',
+ message: 'Test',
+ validatorFunction: 'validate',
+ params,
+ };
+
+ expect(() => CustomValidatorSchema.parse(validation)).not.toThrow();
+ });
+ });
+
+ it('should handle nested conditional validations', () => {
+ const validation = {
+ type: 'conditional' as const,
+ name: 'nested_conditional',
+ message: 'Nested conditional',
+ when: 'type = "A"',
+ then: {
+ type: 'conditional' as const,
+ name: 'inner_conditional',
+ message: 'Inner conditional',
+ when: 'subtype = "B"',
+ then: {
+ type: 'script' as const,
+ name: 'final_validation',
+ message: 'Final validation',
+ condition: 'value > 0',
+ },
+ },
+ };
+
+ expect(() => ValidationRuleSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle complex state machine transitions', () => {
+ const validation = {
+ type: 'state_machine' as const,
+ name: 'complex_state',
+ message: 'Invalid state transition',
+ field: 'status',
+ transitions: {
+ 'draft': ['review', 'cancelled'],
+ 'review': ['approved', 'rejected', 'draft'],
+ 'approved': ['published', 'draft'],
+ 'rejected': ['draft'],
+ 'published': ['archived'],
+ 'archived': [],
+ 'cancelled': [],
+ },
+ };
+
+ expect(() => StateMachineValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle format validation with regex', () => {
+ const validation = {
+ type: 'format' as const,
+ name: 'regex_format',
+ message: 'Invalid format',
+ field: 'custom_field',
+ regex: '^[A-Z]{3}-\\d{4}$', // Pattern like ABC-1234
+ };
+
+ expect(() => FormatValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle cross-field validation with complex conditions', () => {
+ const validation = {
+ type: 'cross_field' as const,
+ name: 'complex_cross_field',
+ message: 'Complex validation failed',
+ condition: '(end_date > start_date) AND (amount >= min_amount) AND (amount <= max_amount)',
+ fields: ['start_date', 'end_date', 'amount', 'min_amount', 'max_amount'],
+ };
+
+ expect(() => CrossFieldValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle async validation with all optional fields', () => {
+ const validation = {
+ type: 'async' as const,
+ name: 'comprehensive_async',
+ message: 'Async validation failed',
+ field: 'email',
+ validatorUrl: '/api/validate/email',
+ validatorFunction: 'validateEmail',
+ timeout: 2000,
+ debounce: 300,
+ params: {
+ checkDomain: true,
+ allowDisposable: false,
+ },
+ };
+
+ expect(() => AsyncValidationSchema.parse(validation)).not.toThrow();
+ });
+});
+
+describe('ValidationRuleSchema - Boundary Conditions', () => {
+ it('should handle very long validation names', () => {
+ const validation = {
+ type: 'script' as const,
+ name: 'very_long_validation_name_that_follows_snake_case_convention',
+ message: 'Test',
+ condition: 'true',
+ };
+
+ expect(() => ScriptValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle very long messages', () => {
+ const longMessage = 'This is a very long validation message that provides detailed information about what went wrong and how to fix it. '.repeat(10);
+
+ const validation = {
+ type: 'script' as const,
+ name: 'test_long_message',
+ message: longMessage,
+ condition: 'true',
+ };
+
+ expect(() => ScriptValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle large number of fields in uniqueness validation', () => {
+ const validation = {
+ type: 'unique' as const,
+ name: 'composite_unique',
+ message: 'Combination must be unique',
+ fields: ['field1', 'field2', 'field3', 'field4', 'field5', 'field6', 'field7', 'field8'],
+ };
+
+ expect(() => UniquenessValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle large number of state transitions', () => {
+ const transitions: Record = {};
+ for (let i = 0; i < 20; i++) {
+ transitions[`state_${i}`] = [`state_${(i + 1) % 20}`];
+ }
+
+ const validation = {
+ type: 'state_machine' as const,
+ name: 'large_state_machine',
+ message: 'Invalid transition',
+ field: 'status',
+ transitions,
+ };
+
+ expect(() => StateMachineValidationSchema.parse(validation)).not.toThrow();
+ });
+
+ it('should handle extreme timeout values', () => {
+ const testCases = [
+ { timeout: 100 }, // Very short
+ { timeout: 5000 }, // Default
+ { timeout: 30000 }, // Long
+ { timeout: 60000 }, // Very long
+ ];
+
+ testCases.forEach(({ timeout }) => {
+ const validation = {
+ type: 'async' as const,
+ name: 'timeout_test',
+ message: 'Test',
+ field: 'test',
+ validatorUrl: '/api/validate',
+ timeout,
+ };
+
+ expect(() => AsyncValidationSchema.parse(validation)).not.toThrow();
+ });
+ });
+
+ it('should handle debounce edge cases', () => {
+ const testCases = [
+ { debounce: 0 },
+ { debounce: 100 },
+ { debounce: 500 },
+ { debounce: 1000 },
+ { debounce: 5000 },
+ ];
+
+ testCases.forEach(({ debounce }) => {
+ const validation = {
+ type: 'async' as const,
+ name: 'debounce_test',
+ message: 'Test',
+ field: 'test',
+ validatorUrl: '/api/validate',
+ debounce,
+ };
+
+ expect(() => AsyncValidationSchema.parse(validation)).not.toThrow();
+ });
+ });
+});