🐛 fix(database): remove content validation limits for agent cron jobs (#11444)

* 🐛 fix(database): remove content validation limits for agent cron jobs

- Remove min(1) validation to allow empty content
- Remove max(2000) validation to allow unlimited content length
- Content can now be empty when using editData for rich content

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor(types): move agentCronJob schemas to types package

- Create manual Zod schemas in @lobechat/types instead of using createInsertSchema
- Define InsertAgentCronJobSchema and UpdateAgentCronJobSchema manually
- Re-export types from database schema for backward compatibility
- Update router to use new schema imports from @lobechat/types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-01-12 20:57:37 +08:00
committed by GitHub
parent 5c102b559d
commit 04a28d3938
4 changed files with 132 additions and 114 deletions

View File

@@ -1,7 +1,6 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { type ExecutionConditions } from '@lobechat/types';
import { boolean, index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { createInsertSchema } from 'drizzle-zod';
import { z } from 'zod';
import { idGenerator } from '../utils/idGenerator';
import { timestamps } from './_helpers';
@@ -9,16 +8,6 @@ import { agents } from './agent';
import { chatGroups } from './chatGroup';
import { users } from './user';
// Execution conditions type for JSONB field
export interface ExecutionConditions {
maxExecutionsPerDay?: number;
timeRange?: {
end: string; // "18:00"
start: string; // "09:00"
};
weekdays?: number[]; // [1,2,3,4,5] (Monday=1, Sunday=0)
}
// Agent cron jobs table - supports multiple cron jobs per agent
export const agentCronJobs = pgTable(
'agent_cron_jobs',
@@ -74,98 +63,11 @@ export const agentCronJobs = pgTable(
],
);
// Validation schemas
export const cronPatternSchema = z
.string()
.regex(
/^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$/,
'Invalid cron pattern',
);
// Minimum 30 minutes validation (using standard cron format)
export const minimumIntervalSchema = z.string().refine((pattern) => {
// Standard cron format: minute hour day month weekday
const allowedPatterns = [
'*/30 * * * *', // Every 30 minutes
'0 * * * *', // Every hour
'0 */2 * * *', // Every 2 hours
'0 */3 * * *', // Every 3 hours
'0 */4 * * *', // Every 4 hours
'0 */6 * * *', // Every 6 hours
'0 */8 * * *', // Every 8 hours
'0 */12 * * *', // Every 12 hours
'0 0 * * *', // Daily at midnight
'0 0 * * 0', // Weekly on Sunday
'0 0 1 * *', // Monthly on 1st
];
// Check if it matches allowed patterns
if (allowedPatterns.includes(pattern)) {
return true;
}
// Parse pattern to validate minimum 30-minute interval
const parts = pattern.split(' ');
if (parts.length !== 5) {
return false;
}
const [minute, hour] = parts;
// Allow minute intervals >= 30 (e.g., */30, */45, */60)
if (minute.startsWith('*/')) {
const interval = parseInt(minute.slice(2));
if (!isNaN(interval) && interval >= 30) {
return true;
}
}
// Allow hourly patterns: 0 */N * * * where N >= 1
if (minute === '0' && hour.startsWith('*/')) {
const interval = parseInt(hour.slice(2));
if (!isNaN(interval) && interval >= 1) {
return true;
}
}
// Allow specific hour patterns: 0 N * * * (runs once per day)
if (minute === '0' && /^\d+$/.test(hour)) {
const h = parseInt(hour);
if (!isNaN(h) && h >= 0 && h <= 23) {
return true;
}
}
return false;
}, 'Minimum execution interval is 30 minutes');
export const executionConditionsSchema = z
.object({
maxExecutionsPerDay: z.number().min(1).max(100).optional(),
timeRange: z
.object({
end: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
start: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
})
.optional(),
weekdays: z.array(z.number().min(0).max(6)).optional(),
})
.optional();
export const insertAgentCronJobSchema = createInsertSchema(agentCronJobs, {
cronPattern: minimumIntervalSchema,
content: z.string().min(1).max(2000),
editData: z.record(z.any()).optional(), // Allow any JSON structure for rich content
name: z.string().max(100).optional(),
description: z.string().max(500).optional(),
maxExecutions: z.number().min(1).max(10_000).optional(),
executionConditions: executionConditionsSchema,
});
export const updateAgentCronJobSchema = insertAgentCronJobSchema.partial();
// Type exports
export type NewAgentCronJob = typeof agentCronJobs.$inferInsert;
export type AgentCronJob = typeof agentCronJobs.$inferSelect;
export type CreateAgentCronJobData = z.infer<typeof insertAgentCronJobSchema>;
export type UpdateAgentCronJobData = z.infer<typeof updateAgentCronJobSchema>;
// Re-export types from types package for consumers
export type { ExecutionConditions } from '@lobechat/types';
export type { InsertAgentCronJob as CreateAgentCronJobData } from '@lobechat/types';
export type { UpdateAgentCronJob as UpdateAgentCronJobData } from '@lobechat/types';

View File

@@ -0,0 +1,115 @@
import { z } from 'zod';
// Execution conditions type
export interface ExecutionConditions {
maxExecutionsPerDay?: number;
timeRange?: {
end: string; // "18:00"
start: string; // "09:00"
};
weekdays?: number[]; // [1,2,3,4,5] (Monday=1, Sunday=0)
}
// Cron pattern validation schema
export const cronPatternSchema = z
.string()
.regex(
/^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$/,
'Invalid cron pattern',
);
// Minimum 30 minutes validation (using standard cron format)
export const minimumIntervalSchema = z.string().refine((pattern) => {
// Standard cron format: minute hour day month weekday
const allowedPatterns = [
'*/30 * * * *', // Every 30 minutes
'0 * * * *', // Every hour
'0 */2 * * *', // Every 2 hours
'0 */3 * * *', // Every 3 hours
'0 */4 * * *', // Every 4 hours
'0 */6 * * *', // Every 6 hours
'0 */8 * * *', // Every 8 hours
'0 */12 * * *', // Every 12 hours
'0 0 * * *', // Daily at midnight
'0 0 * * 0', // Weekly on Sunday
'0 0 1 * *', // Monthly on 1st
];
// Check if it matches allowed patterns
if (allowedPatterns.includes(pattern)) {
return true;
}
// Parse pattern to validate minimum 30-minute interval
const parts = pattern.split(' ');
if (parts.length !== 5) {
return false;
}
const [minute, hour] = parts;
// Allow minute intervals >= 30 (e.g., */30, */45, */60)
if (minute.startsWith('*/')) {
const interval = parseInt(minute.slice(2));
if (!isNaN(interval) && interval >= 30) {
return true;
}
}
// Allow hourly patterns: 0 */N * * * where N >= 1
if (minute === '0' && hour.startsWith('*/')) {
const interval = parseInt(hour.slice(2));
if (!isNaN(interval) && interval >= 1) {
return true;
}
}
// Allow specific hour patterns: 0 N * * * (runs once per day)
if (minute === '0' && /^\d+$/.test(hour)) {
const h = parseInt(hour);
if (!isNaN(h) && h >= 0 && h <= 23) {
return true;
}
}
return false;
}, 'Minimum execution interval is 30 minutes');
// Execution conditions schema
export const ExecutionConditionsSchema = z
.object({
maxExecutionsPerDay: z.number().min(1).max(100).optional(),
timeRange: z
.object({
end: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
start: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
})
.optional(),
weekdays: z.array(z.number().min(0).max(6)).optional(),
})
.optional();
// Insert schema for creating agent cron jobs
export const InsertAgentCronJobSchema = z.object({
agentId: z.string(),
content: z.string(), // Allow empty content (when using editData for rich content)
cronPattern: minimumIntervalSchema,
description: z.string().optional().nullable(),
editData: z.record(z.string(), z.any()).optional().nullable(),
enabled: z.boolean().optional().nullable(),
executionConditions: ExecutionConditionsSchema.nullable(),
groupId: z.string().optional().nullable(),
id: z.string().optional(),
maxExecutions: z.number().min(1).max(10_000).optional().nullable(),
name: z.string().optional().nullable(),
remainingExecutions: z.number().optional().nullable(),
timezone: z.string().optional().nullable(),
userId: z.string().optional(),
});
// Update schema (all fields optional)
export const UpdateAgentCronJobSchema = InsertAgentCronJobSchema.partial();
// Type exports
export type InsertAgentCronJob = z.infer<typeof InsertAgentCronJobSchema>;
export type UpdateAgentCronJob = z.infer<typeof UpdateAgentCronJobSchema>;

View File

@@ -1,4 +1,5 @@
export * from './agent';
export * from './agentCronJob';
export * from './agentGroup';
export * from './aiChat';
export * from './aiProvider';

View File

@@ -1,13 +1,13 @@
import {
type InsertAgentCronJob,
InsertAgentCronJobSchema,
type UpdateAgentCronJob,
UpdateAgentCronJobSchema,
} from '@lobechat/types';
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { AgentCronJobModel } from '@/database/models/agentCronJob';
import {
type CreateAgentCronJobData,
type UpdateAgentCronJobData,
insertAgentCronJobSchema,
updateAgentCronJobSchema,
} from '@/database/schemas/agentCronJob';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
@@ -31,7 +31,7 @@ const batchUpdateStatusSchema = z.object({
});
// Create input schema for tRPC that omits server-managed fields
const createAgentCronJobInputSchema = insertAgentCronJobSchema.omit({
const createAgentCronJobInputSchema = InsertAgentCronJobSchema.omit({
userId: true, // Provided by authentication context
});
@@ -81,7 +81,7 @@ export const agentCronJobRouter = router({
const cronJobModel = new AgentCronJobModel(db, userId);
// Add userId to the input data since it's provided by authentication context
const cronJobData = { ...input, userId };
const cronJob = await cronJobModel.create(cronJobData as CreateAgentCronJobData);
const cronJob = await cronJobModel.create(cronJobData as InsertAgentCronJob);
return {
data: cronJob,
@@ -327,7 +327,7 @@ export const agentCronJobRouter = router({
update: agentCronJobProcedure
.input(
z.object({
data: updateAgentCronJobSchema,
data: UpdateAgentCronJobSchema,
id: z.string(),
}),
)
@@ -337,7 +337,7 @@ export const agentCronJobRouter = router({
try {
const cronJobModel = new AgentCronJobModel(db, userId);
const cronJob = await cronJobModel.update(id, data as UpdateAgentCronJobData);
const cronJob = await cronJobModel.update(id, data as UpdateAgentCronJob);
if (!cronJob) {
throw new TRPCError({