Update schedule management with shell type support and version upgrades

- Updated the `drizzle-zod` package to version 0.7.1 across the project for enhanced schema validation.
- Introduced a new `shellType` column in the `schedule` schema to specify the shell used for executing commands, defaulting to 'bash'.
- Enhanced the `HandleSchedules` component to include the new `shellType` field, allowing users to select the shell type when creating or updating schedules.
- Updated the `runCommand` utility to utilize the selected shell type when executing commands, improving flexibility in command execution.
- Refactored the API to accommodate the new `shellType` in schedule creation and updates, ensuring proper handling of the new field.
This commit is contained in:
Mauricio Siu 2025-05-02 15:56:40 -06:00
parent f2bb01c800
commit 2c90103823
11 changed files with 5766 additions and 239 deletions

View File

@ -13,7 +13,7 @@ import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { Clock, Terminal, Info } from "lucide-react"; import { Clock, Terminal, Info, PlusCircle, PenBoxIcon } from "lucide-react";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -28,6 +28,15 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { toast } from "sonner";
const commonCronExpressions = [ const commonCronExpressions = [
{ label: "Every minute", value: "* * * * *" }, { label: "Every minute", value: "* * * * *" },
@ -47,26 +56,16 @@ const formSchema = z.object({
}); });
interface Props { interface Props {
applicationId: string; applicationId?: string;
onSuccess?: () => void;
defaultValues?: {
name: string;
cronExpression: string;
command: string;
};
scheduleId?: string; scheduleId?: string;
} }
export const HandleSchedules = ({ export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
applicationId, const [isOpen, setIsOpen] = useState(false);
onSuccess, const utils = api.useUtils();
defaultValues,
scheduleId,
}: Props) => {
const utils = api.useContext();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: defaultValues || { defaultValues: {
name: "", name: "",
cronExpression: "", cronExpression: "",
command: "", command: "",
@ -74,166 +73,198 @@ export const HandleSchedules = ({
}, },
}); });
const { mutate: createSchedule, isLoading: isCreating } = const { data: schedule } = api.schedule.one.useQuery(
api.schedule.create.useMutation({ { scheduleId: scheduleId || "" },
onSuccess: () => { { enabled: !!scheduleId },
utils.schedule.list.invalidate({ applicationId }); );
form.reset();
onSuccess?.();
},
});
const { mutate: updateSchedule, isLoading: isUpdating } = useEffect(() => {
api.schedule.update.useMutation({
onSuccess: () => {
utils.schedule.list.invalidate({ applicationId });
onSuccess?.();
},
});
const isLoading = isCreating || isUpdating;
const onSubmit = (values: z.infer<typeof formSchema>) => {
if (scheduleId) { if (scheduleId) {
updateSchedule({ form.reset({
...values, name: schedule?.name,
scheduleId, cronExpression: schedule?.cronExpression,
applicationId, command: schedule?.command,
}); enabled: schedule?.enabled,
} else {
createSchedule({
...values,
applicationId,
}); });
} }
}, [form, form.reset, schedule]);
const { mutateAsync, isLoading } = scheduleId
? api.schedule.update.useMutation()
: api.schedule.create.useMutation();
const onSubmit = async (values: z.infer<typeof formSchema>) => {
await mutateAsync({
...values,
...(scheduleId && { scheduleId }),
...(applicationId && { applicationId }),
})
.then(() => {
toast.success(
`Schedule ${scheduleId ? "updated" : "created"} successfully`,
);
utils.schedule.list.invalidate({ applicationId });
setIsOpen(false);
})
.catch((error) => {
toast.error(
error instanceof Error ? error.message : "An unknown error occurred",
);
});
}; };
return ( return (
<Form {...form}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <DialogTrigger asChild>
<FormField {scheduleId ? (
control={form.control} <Button
name="name" variant="ghost"
render={({ field }) => ( size="icon"
<FormItem> className="group hover:bg-blue-500/10 "
<FormLabel className="flex items-center gap-2"> >
<Clock className="w-4 h-4" /> <PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
Task Name </Button>
</FormLabel> ) : (
<FormControl> <Button>
<Input placeholder="Daily Database Backup" {...field} /> <PlusCircle className="w-4 h-4" />
</FormControl> Add Schedule
<FormDescription> </Button>
A descriptive name for your scheduled task )}
</FormDescription> </DialogTrigger>
<FormMessage /> <DialogContent>
</FormItem> {scheduleId}
)} <DialogHeader>
/> <DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Task Name
</FormLabel>
<FormControl>
<Input placeholder="Daily Database Backup" {...field} />
</FormControl>
<FormDescription>
A descriptive name for your scheduled task
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="cronExpression" name="cronExpression"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel className="flex items-center gap-2">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
Schedule Schedule
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" /> <Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> <p>
Cron expression format: minute hour day month weekday Cron expression format: minute hour day month
</p> weekday
<p>Example: 0 0 * * * (daily at midnight)</p> </p>
</TooltipContent> <p>Example: 0 0 * * * (daily at midnight)</p>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</FormLabel> </TooltipProvider>
<Select </FormLabel>
onValueChange={(value) => field.onChange(value)} <Select
value={field.value} onValueChange={(value) => field.onChange(value)}
> value={field.value}
<FormControl> >
<SelectTrigger> <FormControl>
<SelectValue placeholder="Select or type a cron expression" /> <SelectTrigger>
</SelectTrigger> <SelectValue placeholder="Select or type a cron expression" />
</FormControl> </SelectTrigger>
<SelectContent> </FormControl>
{commonCronExpressions.map((expr) => ( <SelectContent>
<SelectItem key={expr.value} value={expr.value}> {commonCronExpressions.map((expr) => (
{expr.label} ({expr.value}) <SelectItem key={expr.value} value={expr.value}>
</SelectItem> {expr.label} ({expr.value})
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
<FormControl> </Select>
<Input <FormControl>
placeholder="Custom cron expression (e.g., 0 0 * * *)" <Input
{...field} placeholder="Custom cron expression (e.g., 0 0 * * *)"
className="mt-2" {...field}
/> className="mt-2"
</FormControl> />
<FormDescription> </FormControl>
Choose a predefined schedule or enter a custom cron expression <FormDescription>
</FormDescription> Choose a predefined schedule or enter a custom cron
<FormMessage /> expression
</FormItem> </FormDescription>
)} <FormMessage />
/> </FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="command" name="command"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel className="flex items-center gap-2">
<Terminal className="w-4 h-4" /> <Terminal className="w-4 h-4" />
Command Command
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="docker exec my-container npm run backup" placeholder="docker exec my-container npm run backup"
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The command to execute in your container The command to execute in your container
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="enabled" name="enabled"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel className="flex items-center gap-2">
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
Enabled Enabled
</FormLabel> </FormLabel>
</FormItem> </FormItem>
)} )}
/> />
<Button type="submit" disabled={isLoading} className="w-full"> <Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? ( {isLoading ? (
<> <>
<Clock className="mr-2 h-4 w-4 animate-spin" /> <Clock className="mr-2 h-4 w-4 animate-spin" />
{scheduleId ? "Updating..." : "Creating..."} {scheduleId ? "Updating..." : "Creating..."}
</> </>
) : ( ) : (
<>{scheduleId ? "Update" : "Create"} Schedule</> <>{scheduleId ? "Update" : "Create"} Schedule</>
)} )}
</Button> </Button>
</form> </form>
</Form> </Form>
</DialogContent>
</Dialog>
); );
}; };

View File

@ -1,11 +1,4 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { import {
Table, Table,
TableBody, TableBody,
@ -15,9 +8,8 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useState } from "react";
import { HandleSchedules } from "./handle-schedules"; import { HandleSchedules } from "./handle-schedules";
import { PlusCircle, Clock, Terminal, Trash2, Edit2 } from "lucide-react"; import { Clock, Terminal, Trash2 } from "lucide-react";
import { import {
Card, Card,
CardContent, CardContent,
@ -34,14 +26,6 @@ interface Props {
} }
export const ShowSchedules = ({ applicationId }: Props) => { export const ShowSchedules = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [editingSchedule, setEditingSchedule] = useState<{
scheduleId: string;
name: string;
cronExpression: string;
command: string;
} | null>(null);
const { data: schedules } = api.schedule.list.useQuery({ const { data: schedules } = api.schedule.list.useQuery({
applicationId, applicationId,
}); });
@ -60,11 +44,6 @@ export const ShowSchedules = ({ applicationId }: Props) => {
const utils = api.useContext(); const utils = api.useContext();
const onClose = () => {
setIsOpen(false);
setEditingSchedule(null);
};
return ( return (
<Card className="border px-4 shadow-none bg-transparent"> <Card className="border px-4 shadow-none bg-transparent">
<CardHeader className="px-0"> <CardHeader className="px-0">
@ -78,27 +57,7 @@ export const ShowSchedules = ({ applicationId }: Props) => {
</CardDescription> </CardDescription>
</div> </div>
<Dialog open={isOpen} onOpenChange={setIsOpen}> <HandleSchedules applicationId={applicationId} />
<DialogTrigger asChild>
<Button className="gap-2">
<PlusCircle className="w-4 h-4" />
New Schedule
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingSchedule ? "Edit" : "Create"} Schedule
</DialogTitle>
</DialogHeader>
<HandleSchedules
applicationId={applicationId}
onSuccess={onClose}
defaultValues={editingSchedule || undefined}
scheduleId={editingSchedule?.scheduleId}
/>
</DialogContent>
</Dialog>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-0"> <CardContent className="px-0">
@ -146,7 +105,7 @@ export const ShowSchedules = ({ applicationId }: Props) => {
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<ShowSchedulesLogs <ShowSchedulesLogs
deployments={deployments} deployments={deployments || []}
serverId={application.serverId || undefined} serverId={application.serverId || undefined}
/> />
<Button <Button
@ -170,15 +129,12 @@ export const ShowSchedules = ({ applicationId }: Props) => {
> >
Run Manual Schedule Run Manual Schedule
</Button> </Button>
<Button
onClick={() => { <HandleSchedules
setEditingSchedule(schedule); scheduleId={schedule.scheduleId}
setIsOpen(true); applicationId={applicationId}
}} />
>
<Edit2 className="w-4 h-4" />
<span className="sr-only">Edit</span>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"

View File

@ -0,0 +1,2 @@
CREATE TYPE "public"."shellType" AS ENUM('bash', 'sh');--> statement-breakpoint
ALTER TABLE "schedule" ADD COLUMN "shellType" "shellType" DEFAULT 'bash' NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@ -645,6 +645,13 @@
"when": 1746180131377, "when": 1746180131377,
"tag": "0091_amused_warlock", "tag": "0091_amused_warlock",
"breakpoints": true "breakpoints": true
},
{
"idx": 92,
"version": "7",
"when": 1746221961240,
"tag": "0092_safe_scarlet_witch",
"breakpoints": true
} }
] ]
} }

View File

@ -104,7 +104,7 @@
"dockerode": "4.0.2", "dockerode": "4.0.2",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"drizzle-orm": "^0.39.1", "drizzle-orm": "^0.39.1",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.7.1",
"fancy-ansi": "^0.1.3", "fancy-ansi": "^0.1.3",
"hi-base32": "^0.5.1", "hi-base32": "^0.5.1",
"i18next": "^23.16.4", "i18next": "^23.16.4",

View File

@ -3,6 +3,7 @@ import { z } from "zod";
import { import {
createScheduleSchema, createScheduleSchema,
schedules, schedules,
updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule"; } from "@dokploy/server/db/schema/schedule";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
@ -13,15 +14,16 @@ export const scheduleRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
.input(createScheduleSchema) .input(createScheduleSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { scheduleId, ...rest } = input;
const [schedule] = await ctx.db const [schedule] = await ctx.db
.insert(schedules) .insert(schedules)
.values(input) .values(rest)
.returning(); .returning();
return schedule; return schedule;
}), }),
update: protectedProcedure update: protectedProcedure
.input(createScheduleSchema.extend({ scheduleId: z.string() })) .input(updateScheduleSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { scheduleId, ...rest } = input; const { scheduleId, ...rest } = input;
const [schedule] = await ctx.db const [schedule] = await ctx.db

View File

@ -54,7 +54,7 @@
"dockerode": "4.0.2", "dockerode": "4.0.2",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"drizzle-orm": "^0.39.1", "drizzle-orm": "^0.39.1",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.7.1",
"hi-base32": "^0.5.1", "hi-base32": "^0.5.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"lodash": "4.17.21", "lodash": "4.17.21",

View File

@ -1,12 +1,14 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, pgTable, text } from "drizzle-orm/pg-core"; import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema, createUpdateSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { applications } from "./application"; import { applications } from "./application";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
export const schedules = pgTable("schedule", { export const schedules = pgTable("schedule", {
scheduleId: text("scheduleId") scheduleId: text("scheduleId")
.notNull() .notNull()
@ -17,6 +19,7 @@ export const schedules = pgTable("schedule", {
appName: text("appName") appName: text("appName")
.notNull() .notNull()
.$defaultFn(() => generateAppName("schedule")), .$defaultFn(() => generateAppName("schedule")),
shellType: shellTypes("shellType").notNull().default("bash"),
command: text("command").notNull(), command: text("command").notNull(),
applicationId: text("applicationId") applicationId: text("applicationId")
.notNull() .notNull()
@ -45,3 +48,7 @@ export const createScheduleSchema = createInsertSchema(schedules, {
command: z.string().min(1), command: z.string().min(1),
applicationId: z.string().min(1), applicationId: z.string().min(1),
}); });
export const updateScheduleSchema = createUpdateSchema(schedules).extend({
scheduleId: z.string().min(1),
});

View File

@ -19,7 +19,8 @@ export const scheduleJob = (schedule: Schedule) => {
}; };
export const runCommand = async (scheduleId: string) => { export const runCommand = async (scheduleId: string) => {
const { application, command } = await findScheduleById(scheduleId); const { application, command, shellType } =
await findScheduleById(scheduleId);
const isServer = !!application.serverId; const isServer = !!application.serverId;
@ -42,7 +43,8 @@ export const runCommand = async (scheduleId: string) => {
application.serverId, application.serverId,
` `
set -e set -e
docker exec ${containerId} sh -c "${command}" || { echo "Running command: docker exec ${containerId} ${shellType} -c \"${command}\"" >> ${deployment.logPath};
docker exec ${containerId} ${shellType} -c "${command}" || {
echo "❌ Command failed" >> ${deployment.logPath}; echo "❌ Command failed" >> ${deployment.logPath};
exit 1; exit 1;
} }
@ -56,10 +58,12 @@ export const runCommand = async (scheduleId: string) => {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
try { try {
writeStream.write(`${command}\n`); writeStream.write(
`docker exec ${containerId} ${shellType} -c "${command}"\n`,
);
await spawnAsync( await spawnAsync(
"docker", "docker",
["exec", containerId, "sh", "-c", command], ["exec", containerId, shellType, "-c", command],
(data) => { (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);

View File

@ -302,8 +302,8 @@ importers:
specifier: ^0.39.1 specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.7.1
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) version: 0.7.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
fancy-ansi: fancy-ansi:
specifier: ^0.1.3 specifier: ^0.1.3
version: 0.1.3 version: 0.1.3
@ -664,8 +664,8 @@ importers:
specifier: ^0.39.1 specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.7.1
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) version: 0.7.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
hi-base32: hi-base32:
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1 version: 0.5.1
@ -4549,11 +4549,11 @@ packages:
sqlite3: sqlite3:
optional: true optional: true
drizzle-zod@0.5.1: drizzle-zod@0.7.1:
resolution: {integrity: sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A==} resolution: {integrity: sha512-nZzALOdz44/AL2U005UlmMqaQ1qe5JfanvLujiTHiiT8+vZJTBFhj3pY4Vk+L6UWyKFfNmLhk602Hn4kCTynKQ==}
peerDependencies: peerDependencies:
drizzle-orm: '>=0.23.13' drizzle-orm: '>=0.36.0'
zod: '*' zod: '>=3.0.0'
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -11191,7 +11191,7 @@ snapshots:
react: 18.2.0 react: 18.2.0
sqlite3: 5.1.7 sqlite3: 5.1.7
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8): drizzle-zod@0.7.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
dependencies: dependencies:
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
zod: 3.23.8 zod: 3.23.8