From e7264f8cee559f339cfb77a1e5a495706953b797 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Mar 2025 11:41:07 +0100 Subject: [PATCH 01/55] Setup page for fertilizers --- fdm-app/app/components/custom/sidebar-app.tsx | 26 +++-- .../farm.$b_id_farm.fertilizers._index.tsx | 95 +++++++++++++++++++ 2 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx diff --git a/fdm-app/app/components/custom/sidebar-app.tsx b/fdm-app/app/components/custom/sidebar-app.tsx index 0b57cd442..433ffd033 100644 --- a/fdm-app/app/components/custom/sidebar-app.tsx +++ b/fdm-app/app/components/custom/sidebar-app.tsx @@ -36,6 +36,7 @@ import { Scale, Send, Settings, + Shapes, Sparkles, Sprout, Square, @@ -97,6 +98,12 @@ export function SidebarApp(props: SideBarAppType) { } else { atlasLink = undefined } + let fertilizersLink: string | undefined + if (farmId) { + fertilizersLink = `/farm/${farmId}/fertilizers` + } else { + fertilizersLink = undefined + } const nutrienBalanceLink = undefined const omBalanceLink = undefined @@ -210,14 +217,21 @@ export function SidebarApp(props: SideBarAppType) { */} - {/* + - - - Meststoffen - + {fertilizersLink ? ( + + + Meststoffen + + ) : ( + + + Meststoffen + + )} - */} + {/* diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx new file mode 100644 index 000000000..d8859f310 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx @@ -0,0 +1,95 @@ +import { FarmHeader } from "@/components/custom/farm/farm-header" +import { FarmTitle } from "@/components/custom/farm/farm-title" +import { SidebarInset } from "@/components/ui/sidebar" +import { getSession } from "@/lib/auth.server" +import { handleLoaderError } from "@/lib/error" +import { fdm } from "@/lib/fdm.server" +import { getFarm, getFarms } from "@svenvw/fdm-core" +import { + type LoaderFunctionArgs, + Outlet, + data, + useLoaderData, +} from "react-router" + + + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + // Get the farm id + const b_id_farm = params.b_id_farm + if (!b_id_farm) { + throw data("invalid: b_id_farm", { + status: 400, + statusText: "invalid: b_id_farm", + }) + } + + // Get the session + const session = await getSession(request) + + // Get details of farm + const farm = await getFarm(fdm, session.principal_id, b_id_farm) + if (!farm) { + throw data("not found: b_id_farm", { + status: 404, + statusText: "not found: b_id_farm", + }) + } + + // Get a list of possible farms of the user + const farms = await getFarms(fdm, session.principal_id) + if (!farms || farms.length === 0) { + throw data("not found: farms", { + status: 404, + statusText: "not found: farms", + }) + } + + const farmOptions = farms.map((farm) => { + return { + b_id_farm: farm.b_id_farm, + b_name_farm: farm.b_name_farm, + } + }) + + // Return user information from loader + return { + farm: farm, + b_id_farm: b_id_farm, + farmOptions: farmOptions, + } + } catch (error) { + throw handleLoaderError(error) + } +} + +/** + * Renders the layout for managing farm settings. + * + * This component displays a sidebar that includes the farm header, navigation options, and a link to farm fields. + * It also renders a main section containing the farm title, description, nested routes via an Outlet, and a notification toaster. + */ +export default function FarmFertilizersBlock() { + const loaderData = useLoaderData() + + return ( + + +
+ + +
+
+ ) +} From 6d161c0d52b40fe3173900a41574be7fc67e226a Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:15:09 +0100 Subject: [PATCH 02/55] Setup a table for fertilizers --- .../components/custom/fertilizer/columns.tsx | 28 ++++ .../components/custom/fertilizer/table.tsx | 84 ++++++++++++ fdm-app/app/components/ui/table.tsx | 120 ++++++++++++++++++ .../farm.$b_id_farm.fertilizers._index.tsx | 25 +++- fdm-app/package.json | 1 + pnpm-lock.yaml | 22 ++++ 6 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 fdm-app/app/components/custom/fertilizer/columns.tsx create mode 100644 fdm-app/app/components/custom/fertilizer/table.tsx create mode 100644 fdm-app/app/components/ui/table.tsx diff --git a/fdm-app/app/components/custom/fertilizer/columns.tsx b/fdm-app/app/components/custom/fertilizer/columns.tsx new file mode 100644 index 000000000..7804dc585 --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -0,0 +1,28 @@ +import type { ColumnDef } from "@tanstack/react-table" + +export type Fertilizer = { + p_id: string + p_name_nl: string + p_n_rt: number | null + p_p_rt: number | null + p_k_rt: number | null +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "p_id", + header: "ID", + }, + { + accessorKey: "p_name_nl", + header: "Naam", + }, + { + accessorKey: "p_n_rt", + header: "N", + }, + { + accessorKey: "p_p_rt", + header: "P", + }, +] diff --git a/fdm-app/app/components/custom/fertilizer/table.tsx b/fdm-app/app/components/custom/fertilizer/table.tsx new file mode 100644 index 000000000..4ff7f439b --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/table.tsx @@ -0,0 +1,84 @@ +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + Geen resultaten. + + + )} + +
+
+ ) +} diff --git a/fdm-app/app/components/ui/table.tsx b/fdm-app/app/components/ui/table.tsx new file mode 100644 index 000000000..a09f16e93 --- /dev/null +++ b/fdm-app/app/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "~/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx index d8859f310..62cdc6079 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx @@ -1,10 +1,17 @@ import { FarmHeader } from "@/components/custom/farm/farm-header" import { FarmTitle } from "@/components/custom/farm/farm-title" +import { columns, Fertilizer } from "@/components/custom/fertilizer/columns" +import { DataTable } from "@/components/custom/fertilizer/table" import { SidebarInset } from "@/components/ui/sidebar" import { getSession } from "@/lib/auth.server" import { handleLoaderError } from "@/lib/error" import { fdm } from "@/lib/fdm.server" -import { getFarm, getFarms } from "@svenvw/fdm-core" +import { + getFarm, + getFarms, + getFertilizer, + getFertilizers, +} from "@svenvw/fdm-core" import { type LoaderFunctionArgs, Outlet, @@ -12,8 +19,6 @@ import { useLoaderData, } from "react-router" - - export async function loader({ request, params }: LoaderFunctionArgs) { try { // Get the farm id @@ -53,11 +58,20 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } }) + // Get the available fertilizers + const fertilizers: Fertilizer[] = await getFertilizers( + fdm, + session.principal_id, + b_id_farm, + ) + // console.log(fertilizers) + // Return user information from loader return { farm: farm, b_id_farm: b_id_farm, farmOptions: farmOptions, + fertilizers: fertilizers, } } catch (error) { throw handleLoaderError(error) @@ -88,7 +102,10 @@ export default function FarmFertilizersBlock() { title={"Meststoffen"} description={"Beheer de meststoffen van dit bedrijf"} /> - + + {/*
*/} + + {/*
*/} ) diff --git a/fdm-app/package.json b/fdm-app/package.json index 295d7dad6..6f810c5af 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -37,6 +37,7 @@ "@sentry/vite-plugin": "^3.2.2", "@svenvw/fdm-calculator": "workspace:^", "@svenvw/fdm-core": "workspace:^", + "@tanstack/react-table": "^8.21.2", "@turf/centroid": "^7.2.0", "better-auth": "catalog:", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d81a0d80..95aaa6de2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: '@svenvw/fdm-core': specifier: workspace:^ version: link:../fdm-core + '@tanstack/react-table': + specifier: ^8.21.2 + version: 8.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@turf/centroid': specifier: ^7.2.0 version: 7.2.0 @@ -3638,6 +3641,17 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} + '@tanstack/react-table@8.21.2': + resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.2': + resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} + engines: {node: '>=12'} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -13212,6 +13226,14 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@tanstack/react-table@8.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/table-core': 8.21.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/table-core@8.21.2': {} + '@trysound/sax@0.2.0': {} '@turf/centroid@7.2.0': From 404250b6aec865b82bd33b2c329f9ab4e4465dc9 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:34:10 +0100 Subject: [PATCH 03/55] Add header options --- .../custom/fertilizer/column-header.tsx | 66 +++++++++ .../components/custom/fertilizer/columns.tsx | 11 +- .../components/custom/fertilizer/table.tsx | 135 +++++++++++------- .../farm.$b_id_farm.fertilizers._index.tsx | 26 ++-- 4 files changed, 176 insertions(+), 62 deletions(-) create mode 100644 fdm-app/app/components/custom/fertilizer/column-header.tsx diff --git a/fdm-app/app/components/custom/fertilizer/column-header.tsx b/fdm-app/app/components/custom/fertilizer/column-header.tsx new file mode 100644 index 000000000..279503ebd --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/column-header.tsx @@ -0,0 +1,66 @@ +import type { Column } from "@tanstack/react-table" +import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + column: Column + title: string +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort()) { + return
{title}
+ } + + return ( +
+ + + + + + column.toggleSorting(false)}> + + Oplopend + + column.toggleSorting(true)}> + + Aflopend + + + column.toggleVisibility(false)}> + + Verberg + + + +
+ ) +} diff --git a/fdm-app/app/components/custom/fertilizer/columns.tsx b/fdm-app/app/components/custom/fertilizer/columns.tsx index 7804dc585..68b8c3240 100644 --- a/fdm-app/app/components/custom/fertilizer/columns.tsx +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -1,4 +1,7 @@ +import { Button } from "@/components/ui/button" import type { ColumnDef } from "@tanstack/react-table" +import { ArrowUpDown } from "lucide-react" +import { DataTableColumnHeader } from "./column-header" export type Fertilizer = { p_id: string @@ -19,10 +22,14 @@ export const columns: ColumnDef[] = [ }, { accessorKey: "p_n_rt", - header: "N", + header: ({ column }) => { + return + }, }, { accessorKey: "p_p_rt", - header: "P", + header: ({ column }) => { + return + }, }, ] diff --git a/fdm-app/app/components/custom/fertilizer/table.tsx b/fdm-app/app/components/custom/fertilizer/table.tsx index 4ff7f439b..02d3a61d9 100644 --- a/fdm-app/app/components/custom/fertilizer/table.tsx +++ b/fdm-app/app/components/custom/fertilizer/table.tsx @@ -1,7 +1,11 @@ import { type ColumnDef, + ColumnFiltersState, flexRender, getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + SortingState, useReactTable, } from "@tanstack/react-table" import { @@ -12,6 +16,8 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { useState } from "react" +import { Input } from "@/components/ui/input" interface DataTableProps { columns: ColumnDef[] @@ -22,63 +28,94 @@ export function DataTable({ columns, data, }: DataTableProps) { + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + }, }) return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext(), - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} +
+
+ + table + .getColumn("p_name_nl") + ?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + Geen resultaten. + - )) - ) : ( - - - Geen resultaten. - - - )} - -
+ )} + +
+
) } diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx index 62cdc6079..1313f32b3 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx @@ -1,17 +1,15 @@ import { FarmHeader } from "@/components/custom/farm/farm-header" import { FarmTitle } from "@/components/custom/farm/farm-title" -import { columns, Fertilizer } from "@/components/custom/fertilizer/columns" +import { + columns, + type Fertilizer, +} from "@/components/custom/fertilizer/columns" import { DataTable } from "@/components/custom/fertilizer/table" import { SidebarInset } from "@/components/ui/sidebar" import { getSession } from "@/lib/auth.server" import { handleLoaderError } from "@/lib/error" import { fdm } from "@/lib/fdm.server" -import { - getFarm, - getFarms, - getFertilizer, - getFertilizers, -} from "@svenvw/fdm-core" +import { getFarm, getFarms, getFertilizers } from "@svenvw/fdm-core" import { type LoaderFunctionArgs, Outlet, @@ -102,10 +100,16 @@ export default function FarmFertilizersBlock() { title={"Meststoffen"} description={"Beheer de meststoffen van dit bedrijf"} /> - - {/*
*/} - - {/*
*/} +
+
+
+ +
+
+
) From 4e5bf7a58bc9a8889592dcee1b08219db5fa052c Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:56:06 +0100 Subject: [PATCH 04/55] Add button to go to page with details about fertilizer --- .../components/custom/fertilizer/columns.tsx | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/components/custom/fertilizer/columns.tsx b/fdm-app/app/components/custom/fertilizer/columns.tsx index 68b8c3240..c9ebcd712 100644 --- a/fdm-app/app/components/custom/fertilizer/columns.tsx +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -1,7 +1,15 @@ -import { Button } from "@/components/ui/button" import type { ColumnDef } from "@tanstack/react-table" -import { ArrowUpDown } from "lucide-react" +import { + SquareArrowOutUpRight, +} from "lucide-react" import { DataTableColumnHeader } from "./column-header" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { NavLink } from "react-router" export type Fertilizer = { p_id: string @@ -32,4 +40,23 @@ export const columns: ColumnDef[] = [ return }, }, + { + accessorKey: "Opties", + cell: ({ row }) => { + const fertilizer = row.original + + return ( + + + + + + + + {`Bekijk details over ${fertilizer.p_name_nl}`} + + + ) + }, + }, ] From 1bacbc21063e4f81453edbbd3da8e630e8e1ec44 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:53:26 +0100 Subject: [PATCH 05/55] Add page for to see details of fertilizer --- .../custom/fertilizer/formschema.tsx | 28 + .../farm.$b_id_farm.fertilizers.$p_id.tsx | 578 ++++++++++++++++++ 2 files changed, 606 insertions(+) create mode 100644 fdm-app/app/components/custom/fertilizer/formschema.tsx create mode 100644 fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx diff --git a/fdm-app/app/components/custom/fertilizer/formschema.tsx b/fdm-app/app/components/custom/fertilizer/formschema.tsx new file mode 100644 index 000000000..bd0d56f6f --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/formschema.tsx @@ -0,0 +1,28 @@ +import { z } from "zod" + +export const FormSchema = z.object({ + p_n_rt: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), + p_p_rt: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), + p_k_rt: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), + p_om: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), + p_c_rt: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), + p_s_rt: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), + p_ca_rt: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), + p_mg_rt: z.coerce.number({ + invalid_type_error: "Ongeldige waarde", + }), +}) diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx new file mode 100644 index 000000000..614aeebdb --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -0,0 +1,578 @@ +import { FarmHeader } from "@/components/custom/farm/farm-header" +import { FarmTitle } from "@/components/custom/farm/farm-title" +import { FormSchema } from "@/components/custom/fertilizer/formschema" +import { Badge } from "@/components/ui/badge" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { SidebarInset } from "@/components/ui/sidebar" +import { getSession } from "@/lib/auth.server" +import { handleLoaderError } from "@/lib/error" +import { fdm } from "@/lib/fdm.server" +import { zodResolver } from "@hookform/resolvers/zod" +import { getFarm, getFarms, getFertilizer } from "@svenvw/fdm-core" +import { useEffect } from "react" +import { + Form, + type LoaderFunctionArgs, + data, + useLoaderData, +} from "react-router" +import { RemixFormProvider, useRemixForm } from "remix-hook-form" +import type { z } from "zod" + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + // Get the farm id + const b_id_farm = params.b_id_farm + if (!b_id_farm) { + throw data("invalid: b_id_farm", { + status: 400, + statusText: "invalid: b_id_farm", + }) + } + + // Get the fertilizer id + const p_id = params.p_id + if (!p_id) { + throw data("invalid: p_id", { + status: 400, + statusText: "invalid: p_id", + }) + } + + // Get the session + const session = await getSession(request) + + // Get details of farm + const farm = await getFarm(fdm, session.principal_id, b_id_farm) + if (!farm) { + throw data("not found: b_id_farm", { + status: 404, + statusText: "not found: b_id_farm", + }) + } + + // Get a list of possible farms of the user + const farms = await getFarms(fdm, session.principal_id) + if (!farms || farms.length === 0) { + throw data("not found: farms", { + status: 404, + statusText: "not found: farms", + }) + } + + const farmOptions = farms.map((farm) => { + return { + b_id_farm: farm.b_id_farm, + b_name_farm: farm.b_name_farm, + } + }) + + // Get the available fertilizers + const fertilizer = await getFertilizer(fdm, p_id) + + // Return user information from loader + return { + farm: farm, + b_id_farm: b_id_farm, + farmOptions: farmOptions, + fertilizer: fertilizer, + editable: true, + } + } catch (error) { + throw handleLoaderError(error) + } +} + +/** + * Renders the layout for managing farm settings. + * + * This component displays a sidebar that includes the farm header, navigation options, and a link to farm fields. + * It also renders a main section containing the farm title, description, nested routes via an Outlet, and a notification toaster. + */ +export default function FarmFertilizerBlock() { + const loaderData = useLoaderData() + const fertilizer = loaderData.fertilizer + + const form = useRemixForm>({ + mode: "onTouched", + resolver: zodResolver(FormSchema), + defaultValues: { + p_n_rt: fertilizer.p_n_rt, + p_p_rt: fertilizer.p_p_rt, + p_k_rt: fertilizer.p_k_rt, + p_om: fertilizer.p_om, + p_c_rt: fertilizer.p_c_rt, + p_s_rt: fertilizer.p_s_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_mg_rt: fertilizer.p_mg_rt, + }, + }) + + useEffect(() => { + form.reset({ + p_n_rt: fertilizer.p_n_rt, + p_p_rt: fertilizer.p_p_rt, + p_k_rt: fertilizer.p_k_rt, + p_om: fertilizer.p_om, + p_c_rt: fertilizer.p_c_rt, + p_s_rt: fertilizer.p_s_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_mg_rt: fertilizer.p_mg_rt, + }) + }, [fertilizer, form.reset]) + + return ( + + +
+ +
+ +
+
+
+ + + + Algemene informatie + + + Details over de meststof + + + +
+ + Naam + + + {fertilizer.p_name_nl} + +
+
+ + Bron + + + {fertilizer.p_source} + +
+ {fertilizer.p_description && ( +
+ + Omschrijving + + + { + fertilizer.p_description + } + +
+ )} + {fertilizer.p_type_manure && ( +
+ + Type + + + Mest + +
+ )} + {fertilizer.p_type_mineral && ( +
+ + Type + + + Mineraal + +
+ )} + {fertilizer.p_type_compost && ( +
+ + Type + + + Compost + +
+ )} +
+
+ + + Samenstelling + + De gehalten van deze meststof + + + + {fertilizer.p_n_rt !== null && ( +
+ + Stikstof (N) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_n_rt + } + + )} + + g N / kg + +
+
+ )} + {fertilizer.p_p_rt !== null && ( +
+ + Fosfor (P) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_p_rt + } + + )} + + {" "} + g P / kg + +
+
+ )} + {fertilizer.p_k_rt !== null && ( +
+ + Kalium (K) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_k_rt + } + + )} + + {" "} + g K / kg + +
+
+ )} + {fertilizer.p_om !== null && ( +
+ + Organische stof (OS) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_om + } + + )} + + {" "} + g OS / kg + +
+
+ )} + {fertilizer.p_c_rt !== null && ( +
+ + Koolstof (C) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_c_rt + } + + )} + + {" "} + g C / kg + +
+
+ )} + {fertilizer.p_s_rt !== null && ( +
+ + Zwavel (S) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_s_rt + } + + )} + + {" "} + g SO3 / kg + +
+
+ )} + {fertilizer.p_ca_rt !== null && ( +
+ + Calcium (Ca) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_ca_rt + } + + )} + + {" "} + g Ca / kg + +
+
+ )} + {fertilizer.p_mg_rt !== null && ( +
+ + Magnesium (Mg) + +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_mg_rt + } + + )} + + {" "} + g Mg / kg + +
+
+ )} +
+
+
+
+
+
+
+
+
+ ) +} From 542f55b8002389a47533f955a59c4bdc8cdd79c3 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:13:45 +0100 Subject: [PATCH 06/55] When farm is created enable fertilizer catalogue with custom fertilizers for that specific farm --- .changeset/light-rockets-fly.md | 5 +++++ fdm-app/app/routes/farm.create._index.tsx | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/light-rockets-fly.md diff --git a/.changeset/light-rockets-fly.md b/.changeset/light-rockets-fly.md new file mode 100644 index 000000000..618351a5b --- /dev/null +++ b/.changeset/light-rockets-fly.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +When farm is created enable fertilizer catalogue with custom fertilizers for that specific farm diff --git a/fdm-app/app/routes/farm.create._index.tsx b/fdm-app/app/routes/farm.create._index.tsx index 3df2f86ae..41628384a 100644 --- a/fdm-app/app/routes/farm.create._index.tsx +++ b/fdm-app/app/routes/farm.create._index.tsx @@ -125,6 +125,13 @@ export async function action({ request }: ActionFunctionArgs) { b_id_farm, "srm", ) + // Enable catalogue with custom user fertilizers + await enableFertilizerCatalogue( + fdm, + session.principal_id, + b_id_farm, + b_id_farm, + ) await enableCultivationCatalogue( fdm, session.principal_id, From 7e6623136d276b6aa5ca99b8f4540e8ccc10e6c7 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:45:40 +0100 Subject: [PATCH 07/55] Add function `hashFertilizer` to create hash for fertilizer --- .changeset/dirty-pumas-cry.md | 5 ++ fdm-data/src/fertilizers/catalogues/srm.ts | 6 +- fdm-data/src/fertilizers/d.ts | 94 +++++++++++----------- fdm-data/src/fertilizers/hash.ts | 9 +++ fdm-data/src/index.ts | 3 +- 5 files changed, 65 insertions(+), 52 deletions(-) create mode 100644 .changeset/dirty-pumas-cry.md create mode 100644 fdm-data/src/fertilizers/hash.ts diff --git a/.changeset/dirty-pumas-cry.md b/.changeset/dirty-pumas-cry.md new file mode 100644 index 000000000..2e79bee7c --- /dev/null +++ b/.changeset/dirty-pumas-cry.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-data": minor +--- + +Add function `hashFertilizer` to create hash for fertilizer diff --git a/fdm-data/src/fertilizers/catalogues/srm.ts b/fdm-data/src/fertilizers/catalogues/srm.ts index d5d09dcbb..5a3a150a2 100644 --- a/fdm-data/src/fertilizers/catalogues/srm.ts +++ b/fdm-data/src/fertilizers/catalogues/srm.ts @@ -1,8 +1,6 @@ import type { CatalogueFertilizer, CatalogueFertilizerItem } from "../d" +import { hashFertilizer } from "../hash" import srm from "./srm.json" -import xxhash from "xxhash-wasm" - -const { h32ToString } = await xxhash() /** * Retrieves the SRM (Sluiting Regionale Kringlopen) fertilizer catalogue. @@ -75,7 +73,7 @@ export function getCatalogueSrm(): CatalogueFertilizer { } // Hash the item - item.hash = h32ToString(JSON.stringify(item)) + item.hash = hashFertilizer(item) return item }) diff --git a/fdm-data/src/fertilizers/d.ts b/fdm-data/src/fertilizers/d.ts index 3322684cf..f5c3a3162 100644 --- a/fdm-data/src/fertilizers/d.ts +++ b/fdm-data/src/fertilizers/d.ts @@ -1,55 +1,55 @@ export type CatalogueFertilizerName = "srm" export interface CatalogueFertilizerItem { - p_source: CatalogueFertilizerName + p_source: CatalogueFertilizerName | string p_id_catalogue: string p_name_nl: string - p_name_en: string | null - p_description: string | null - p_dm: number | null - p_density: number | null - p_om: number | null - p_a: number | null - p_hc: number | null - p_eom: number | null - p_eoc: number | null - p_c_rt: number | null - p_c_of: number | null - p_c_if: number | null - p_c_fr: number | null - p_cn_of: number | null - p_n_rt: number | null - p_n_if: number | null - p_n_of: number | null - p_n_wc: number | null - p_p_rt: number | null - p_k_rt: number | null - p_mg_rt: number | null - p_ca_rt: number | null - p_ne: number | null - p_s_rt: number | null - p_s_wc: number | null - p_cu_rt: number | null - p_zn_rt: number | null - p_na_rt: number | null - p_si_rt: number | null - p_b_rt: number | null - p_mn_rt: number | null - p_ni_rt: number | null - p_fe_rt: number | null - p_mo_rt: number | null - p_co_rt: number | null - p_as_rt: number | null - p_cd_rt: number | null - p_cr_rt: number | null - p_cr_vi: number | null - p_pb_rt: number | null - p_hg_rt: number | null - p_cl_cr: number | null - p_type_manure: boolean | null - p_type_mineral: boolean | null - p_type_compost: boolean | null - hash: string | null + p_name_en?: string | null | undefined + p_description?: string | null | undefined + p_dm?: number | null + p_density?: number | null + p_om?: number | null + p_a?: number | null + p_hc?: number | null + p_eom?: number | null + p_eoc?: number | null + p_c_rt?: number | null + p_c_of?: number | null + p_c_if?: number | null + p_c_fr?: number | null + p_cn_of?: number | null + p_n_rt?: number | null + p_n_if?: number | null + p_n_of?: number | null + p_n_wc?: number | null + p_p_rt?: number | null + p_k_rt?: number | null + p_mg_rt?: number | null + p_ca_rt?: number | null + p_ne?: number | null + p_s_rt?: number | null + p_s_wc?: number | null + p_cu_rt?: number | null + p_zn_rt?: number | null + p_na_rt?: number | null + p_si_rt?: number | null + p_b_rt?: number | null + p_mn_rt?: number | null + p_ni_rt?: number | null + p_fe_rt?: number | null + p_mo_rt?: number | null + p_co_rt?: number | null + p_as_rt?: number | null + p_cd_rt?: number | null + p_cr_rt?: number | null + p_cr_vi?: number | null + p_pb_rt?: number | null + p_hg_rt?: number | null + p_cl_cr?: number | null + p_type_manure?: boolean | null + p_type_mineral?: boolean | null + p_type_compost?: boolean | null + hash?: string | null | undefined } export type CatalogueFertilizer = CatalogueFertilizerItem[] diff --git a/fdm-data/src/fertilizers/hash.ts b/fdm-data/src/fertilizers/hash.ts new file mode 100644 index 000000000..218de4c07 --- /dev/null +++ b/fdm-data/src/fertilizers/hash.ts @@ -0,0 +1,9 @@ +import type { CatalogueFertilizerItem } from "./d" +import xxhash from "xxhash-wasm" + +const { h32ToString } = await xxhash() + +export function hashFertilizer(fertilizer: CatalogueFertilizerItem) { + const hash = h32ToString(JSON.stringify(fertilizer)) + return hash +} diff --git a/fdm-data/src/index.ts b/fdm-data/src/index.ts index 1023c9216..ccc27fa66 100644 --- a/fdm-data/src/index.ts +++ b/fdm-data/src/index.ts @@ -11,5 +11,6 @@ * @packageDocumentation */ -export { getFertilizersCatalogue } from "./fertilizers" +export { getFertilizersCatalogue} from "./fertilizers" export { getCultivationCatalogue } from "./cultivations" +export { hashFertilizer} from "./fertilizers/hash" From c2404860dcd23fdee3685de5bddd84b84cbf8661 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:01:51 +0100 Subject: [PATCH 08/55] Adapt function `addFertilizerToCatalogue` to add custom fertilizers for a farm --- .changeset/bright-buckets-rush.md | 5 + fdm-core/src/cultivation.test.ts | 109 +++--- fdm-core/src/db/schema.ts | 2 +- fdm-core/src/fertilizer.test.ts | 533 +++++++++++++++--------------- fdm-core/src/fertilizer.ts | 33 +- 5 files changed, 360 insertions(+), 322 deletions(-) create mode 100644 .changeset/bright-buckets-rush.md diff --git a/.changeset/bright-buckets-rush.md b/.changeset/bright-buckets-rush.md new file mode 100644 index 000000000..3a5221fca --- /dev/null +++ b/.changeset/bright-buckets-rush.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Adapt function `addFertilizerToCatalogue` to add custom fertilizers for a farm diff --git a/fdm-core/src/cultivation.test.ts b/fdm-core/src/cultivation.test.ts index da6702301..7dda3a37b 100644 --- a/fdm-core/src/cultivation.test.ts +++ b/fdm-core/src/cultivation.test.ts @@ -469,7 +469,6 @@ describe("Cultivation Data Model", () => { let b_lu_catalogue: string let p_id: string let b_lu_source: string - let p_source: string beforeEach(async () => { const farmName = "Test Farm" @@ -492,13 +491,11 @@ describe("Cultivation Data Model", () => { b_id_farm, b_lu_source, ) - - p_source = "custom" await enableFertilizerCatalogue( fdm, principal_id, b_id_farm, - p_source, + b_id_farm, ) b_id = await addField( @@ -544,63 +541,65 @@ describe("Cultivation Data Model", () => { ) // Add fertilizer to catalogue (needed for fertilizer application) - const p_id_catalogue = createId() const p_name_nl = "Test Fertilizer" const p_name_en = "Test Fertilizer (EN)" const p_description = "This is a test fertilizer" const p_acquiring_amount = 1000 const p_acquiring_date = new Date() - await addFertilizerToCatalogue(fdm, { - p_id_catalogue, - p_source, - p_name_nl, - p_name_en, - p_description, - p_dm: 37, - p_density: 20, - p_om: 20, - p_a: 30, - p_hc: 40, - p_eom: 50, - p_eoc: 60, - p_c_rt: 70, - p_c_of: 80, - p_c_if: 90, - p_c_fr: 100, - p_cn_of: 110, - p_n_rt: 120, - p_n_if: 130, - p_n_of: 140, - p_n_wc: 150, - p_p_rt: 160, - p_k_rt: 170, - p_mg_rt: 180, - p_ca_rt: 190, - p_ne: 200, - p_s_rt: 210, - p_s_wc: 220, - p_cu_rt: 230, - p_zn_rt: 240, - p_na_rt: 250, - p_si_rt: 260, - p_b_rt: 270, - p_mn_rt: 280, - p_ni_rt: 290, - p_fe_rt: 300, - p_mo_rt: 310, - p_co_rt: 320, - p_as_rt: 330, - p_cd_rt: 340, - pr_cr_rt: 350, - p_cr_vi: 360, - p_pb_rt: 370, - p_hg_rt: 380, - p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, - }) + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) p_id = await addFertilizer( fdm, diff --git a/fdm-core/src/db/schema.ts b/fdm-core/src/db/schema.ts index 43a700274..8a0f076b7 100644 --- a/fdm-core/src/db/schema.ts +++ b/fdm-core/src/db/schema.ts @@ -181,7 +181,7 @@ export const fertilizersCatalogue = fdmSchema.table( { p_id_catalogue: text().primaryKey(), p_source: text().notNull(), - p_name_nl: text(), + p_name_nl: text().notNull(), p_name_en: text(), p_description: text(), p_dm: numericCasted(), diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index 51a383a12..0c451a859 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -24,15 +24,15 @@ import { updateFertilizerApplication, } from "./fertilizer" import { addField } from "./field" -import { createId } from "./id" -import { disableFertilizerCatalogue, enableFertilizerCatalogue } from "./catalogues" +import { + disableFertilizerCatalogue, + enableFertilizerCatalogue, +} from "./catalogues" describe("Fertilizer Data Model", () => { let fdm: FdmServerType - let p_id_catalogue: string let principal_id: string let b_id_farm: string - let p_source: string beforeEach(async () => { const host = inject("host") @@ -56,10 +56,7 @@ describe("Fertilizer Data Model", () => { farmPostalCode, ) - p_source = "custom" - await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source) - - p_id_catalogue = createId() + await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, b_id_farm) }) afterAll(async () => {}) @@ -78,56 +75,59 @@ describe("Fertilizer Data Model", () => { const p_name_nl = "Test Fertilizer" const p_name_en = "Test Fertilizer (EN)" const p_description = "This is a test fertilizer" - await addFertilizerToCatalogue(fdm, { - p_id_catalogue, - p_source, - p_name_nl, - p_name_en, - p_description, - p_dm: 37, - p_density: 20, - p_om: 20, - p_a: 30, - p_hc: 40, - p_eom: 50, - p_eoc: 60, - p_c_rt: 70, - p_c_of: 80, - p_c_if: 90, - p_c_fr: 100, - p_cn_of: 110, - p_n_rt: 120, - p_n_if: 130, - p_n_of: 140, - p_n_wc: 150, - p_p_rt: 160, - p_k_rt: 170, - p_mg_rt: 180, - p_ca_rt: 190, - p_ne: 200, - p_s_rt: 210, - p_s_wc: 220, - p_cu_rt: 230, - p_zn_rt: 240, - p_na_rt: 250, - p_si_rt: 260, - p_b_rt: 270, - p_mn_rt: 280, - p_ni_rt: 290, - p_fe_rt: 300, - p_mo_rt: 310, - p_co_rt: 320, - p_as_rt: 330, - p_cd_rt: 340, - pr_cr_rt: 350, - p_cr_vi: 360, - p_pb_rt: 370, - p_hg_rt: 380, - p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, - }) + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) const fertilizers = await getFertilizersFromCatalogue( fdm, @@ -139,7 +139,7 @@ describe("Fertilizer Data Model", () => { (f) => f.p_id_catalogue === p_id_catalogue, ) expect(fertilizer).toBeDefined() - expect(fertilizer?.p_source).toBe(p_source) + expect(fertilizer?.p_source).toBe(b_id_farm) expect(fertilizer?.p_name_nl).toBe(p_name_nl) expect(fertilizer?.p_name_en).toBe(p_name_en) expect(fertilizer?.p_description).toBe(p_description) @@ -150,56 +150,59 @@ describe("Fertilizer Data Model", () => { const p_name_nl = "Test Fertilizer" const p_name_en = "Test Fertilizer (EN)" const p_description = "This is a test fertilizer" - await addFertilizerToCatalogue(fdm, { - p_id_catalogue, - p_source, - p_name_nl, - p_name_en, - p_description, - p_dm: 37, - p_density: 20, - p_om: 20, - p_a: 30, - p_hc: 40, - p_eom: 50, - p_eoc: 60, - p_c_rt: 70, - p_c_of: 80, - p_c_if: 90, - p_c_fr: 100, - p_cn_of: 110, - p_n_rt: 120, - p_n_if: 130, - p_n_of: 140, - p_n_wc: 150, - p_p_rt: 160, - p_k_rt: 170, - p_mg_rt: 180, - p_ca_rt: 190, - p_ne: 200, - p_s_rt: 210, - p_s_wc: 220, - p_cu_rt: 230, - p_zn_rt: 240, - p_na_rt: 250, - p_si_rt: 260, - p_b_rt: 270, - p_mn_rt: 280, - p_ni_rt: 290, - p_fe_rt: 300, - p_mo_rt: 310, - p_co_rt: 320, - p_as_rt: 330, - p_cd_rt: 340, - pr_cr_rt: 350, - p_cr_vi: 360, - p_pb_rt: 370, - p_hg_rt: 380, - p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, - }) + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) const p_acquiring_amount = 1000 const p_acquiring_date = new Date() @@ -222,56 +225,59 @@ describe("Fertilizer Data Model", () => { const p_name_nl = "Test Fertilizer" const p_name_en = "Test Fertilizer (EN)" const p_description = "This is a test fertilizer" - await addFertilizerToCatalogue(fdm, { - p_id_catalogue, - p_source, - p_name_nl, - p_name_en, - p_description, - p_dm: 37, - p_density: 20, - p_om: 20, - p_a: 30, - p_hc: 40, - p_eom: 50, - p_eoc: 60, - p_c_rt: 70, - p_c_of: 80, - p_c_if: 90, - p_c_fr: 100, - p_cn_of: 110, - p_n_rt: 120, - p_n_if: 130, - p_n_of: 140, - p_n_wc: 150, - p_p_rt: 160, - p_k_rt: 170, - p_mg_rt: 180, - p_ca_rt: 190, - p_ne: 200, - p_s_rt: 210, - p_s_wc: 220, - p_cu_rt: 230, - p_zn_rt: 240, - p_na_rt: 250, - p_si_rt: 260, - p_b_rt: 270, - p_mn_rt: 280, - p_ni_rt: 290, - p_fe_rt: 300, - p_mo_rt: 310, - p_co_rt: 320, - p_as_rt: 330, - p_cd_rt: 340, - pr_cr_rt: 350, - p_cr_vi: 360, - p_pb_rt: 370, - p_hg_rt: 380, - p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, - }) + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) const p_acquiring_amount = 1000 const p_acquiring_date = new Date() @@ -307,56 +313,59 @@ describe("Fertilizer Data Model", () => { const p_name_nl = "Test Fertilizer" const p_name_en = "Test Fertilizer (EN)" const p_description = "This is a test fertilizer" - await addFertilizerToCatalogue(fdm, { - p_id_catalogue, - p_source, - p_name_nl, - p_name_en, - p_description, - p_dm: 37, - p_density: 20, - p_om: 20, - p_a: 30, - p_hc: 40, - p_eom: 50, - p_eoc: 60, - p_c_rt: 70, - p_c_of: 80, - p_c_if: 90, - p_c_fr: 100, - p_cn_of: 110, - p_n_rt: 120, - p_n_if: 130, - p_n_of: 140, - p_n_wc: 150, - p_p_rt: 160, - p_k_rt: 170, - p_mg_rt: 180, - p_ca_rt: 190, - p_ne: 200, - p_s_rt: 210, - p_s_wc: 220, - p_cu_rt: 230, - p_zn_rt: 240, - p_na_rt: 250, - p_si_rt: 260, - p_b_rt: 270, - p_mn_rt: 280, - p_ni_rt: 290, - p_fe_rt: 300, - p_mo_rt: 310, - p_co_rt: 320, - p_as_rt: 330, - p_cd_rt: 340, - pr_cr_rt: 350, - p_cr_vi: 360, - p_pb_rt: 370, - p_hg_rt: 380, - p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, - }) + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) const p_acquiring_amount = 1000 const p_acquiring_date = new Date() @@ -385,7 +394,7 @@ describe("Fertilizer Data Model", () => { fdm, principal_id, b_id_farm, - p_source, + b_id_farm ) const fertilizersWithNoCatalogue = @@ -438,60 +447,62 @@ describe("Fertilizer Data Model", () => { ) // Add fertilizer to catalogue - p_id_catalogue = createId() const p_name_nl = "Test Fertilizer" const p_name_en = "Test Fertilizer (EN)" const p_description = "This is a test fertilizer" - await addFertilizerToCatalogue(fdm, { - p_id_catalogue, - p_source, - p_name_nl, - p_name_en, - p_description, - p_dm: 37, - p_density: 20, - p_om: 20, - p_a: 30, - p_hc: 40, - p_eom: 50, - p_eoc: 60, - p_c_rt: 70, - p_c_of: 80, - p_c_if: 90, - p_c_fr: 100, - p_cn_of: 110, - p_n_rt: 120, - p_n_if: 130, - p_n_of: 140, - p_n_wc: 150, - p_p_rt: 160, - p_k_rt: 170, - p_mg_rt: 180, - p_ca_rt: 190, - p_ne: 200, - p_s_rt: 210, - p_s_wc: 220, - p_cu_rt: 230, - p_zn_rt: 240, - p_na_rt: 250, - p_si_rt: 260, - p_b_rt: 270, - p_mn_rt: 280, - p_ni_rt: 290, - p_fe_rt: 300, - p_mo_rt: 310, - p_co_rt: 320, - p_as_rt: 330, - p_cd_rt: 340, - pr_cr_rt: 350, - p_cr_vi: 360, - p_pb_rt: 370, - p_hg_rt: 380, - p_cl_rt: 390, - p_type_manure: true, - p_type_mineral: false, - p_type_compost: false, - }) + const p_id_catalogue = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) const p_acquiring_amount = 1000 const p_acquiring_date = new Date() diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 259698197..a29e16feb 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -10,6 +10,7 @@ import type { getFertilizerApplicationType, getFertilizerType, } from "./fertilizer.d" +import { hashFertilizer } from "@svenvw/fdm-data" /** * Retrieves all fertilizers from the enabled catalogues for a farm. @@ -72,9 +73,11 @@ export async function getFertilizersFromCatalogue( } /** - * Adds a new fertilizer to the catalogue. + * Adds a new custom fertilizer to the catalogue of a farm. * * @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}. + * @param principal_id The ID of the principal making the request. + * @param b_id_farm The ID of the farm. * @param properties The properties of the fertilizer to add. * @returns A Promise that resolves when the fertilizer has been added. * @throws If adding the fertilizer fails. @@ -82,9 +85,9 @@ export async function getFertilizersFromCatalogue( */ export async function addFertilizerToCatalogue( fdm: FdmType, + principal_id: PrincipalId, + b_id_farm: schema.farmsTypeInsert["b_id_farm"], properties: { - p_id_catalogue: schema.fertilizersCatalogueTypeInsert["p_id_catalogue"] - p_source: schema.fertilizersCatalogueTypeInsert["p_source"] p_name_nl: schema.fertilizersCatalogueTypeInsert["p_name_nl"] p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] p_description: schema.fertilizersCatalogueTypeInsert["p_description"] @@ -132,10 +135,30 @@ export async function addFertilizerToCatalogue( p_type_mineral: schema.fertilizersCatalogueTypeInsert["p_type_mineral"] p_type_compost: schema.fertilizersCatalogueTypeInsert["p_type_compost"] }, -): Promise { +): Promise { try { + await checkPermission( + fdm, + "farm", + "write", + b_id_farm, + principal_id, + "addFertilizerToCatalogue", + ) + + const p_id_catalogue = createId() + const input: schema.fertilizersCatalogueTypeInsert = { + ...properties, + p_id_catalogue: p_id_catalogue, + p_source: b_id_farm, + hash: null, + } + input.hash = hashFertilizer(input) + // Insert the farm in the db - await fdm.insert(schema.fertilizersCatalogue).values(properties) + await fdm.insert(schema.fertilizersCatalogue).values(input) + + return p_id_catalogue } catch (err) { throw handleError(err, "Exception for addFertilizerToCatalogue", { properties, From a259ff6e5c61c5e29cfcdace19f061625be876f8 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:03:33 +0100 Subject: [PATCH 09/55] Add to the output of `getFertilizer` the values for `p_type_*` --- .changeset/red-oranges-drop.md | 5 +++++ fdm-core/src/fertilizer.ts | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/red-oranges-drop.md diff --git a/.changeset/red-oranges-drop.md b/.changeset/red-oranges-drop.md new file mode 100644 index 000000000..bda6e7e72 --- /dev/null +++ b/.changeset/red-oranges-drop.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Add to the output of `getFertilizer` the values for `p_type_*` diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index a29e16feb..74491a31d 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -296,6 +296,9 @@ export async function getFertilizer( p_pb_rt: schema.fertilizersCatalogue.p_pb_rt, p_hg_rt: schema.fertilizersCatalogue.p_hg_rt, p_cl_cr: schema.fertilizersCatalogue.p_cl_cr, + p_type_manure: schema.fertilizersCatalogue.p_type_manure, + p_type_mineral: schema.fertilizersCatalogue.p_type_mineral, + p_type_compost: schema.fertilizersCatalogue.p_type_compost, }) .from(schema.fertilizers) .leftJoin( From a52796a5121429dc6c41de2a4f2518fa9c1a9e30 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:28:45 +0100 Subject: [PATCH 10/55] Add function `updateFertilizerFromCatalogue` to alter properties of custom fertilizer --- .changeset/funny-dancers-rush.md | 5 + fdm-core/src/fertilizer.test.ts | 291 ++++++++++++++++++++++++++++++- fdm-core/src/fertilizer.ts | 114 +++++++++++- 3 files changed, 408 insertions(+), 2 deletions(-) create mode 100644 .changeset/funny-dancers-rush.md diff --git a/.changeset/funny-dancers-rush.md b/.changeset/funny-dancers-rush.md new file mode 100644 index 000000000..726342b7e --- /dev/null +++ b/.changeset/funny-dancers-rush.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Add function `updateFertilizerFromCatalogue` to alter properties of custom fertilizer diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index 0c451a859..2f050bdbb 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -22,12 +22,14 @@ import { removeFertilizer, removeFertilizerApplication, updateFertilizerApplication, + updateFertilizerFromCatalogue, } from "./fertilizer" import { addField } from "./field" import { disableFertilizerCatalogue, enableFertilizerCatalogue, } from "./catalogues" +import { createId } from "./id" describe("Fertilizer Data Model", () => { let fdm: FdmServerType @@ -394,7 +396,7 @@ describe("Fertilizer Data Model", () => { fdm, principal_id, b_id_farm, - b_id_farm + b_id_farm, ) const fertilizersWithNoCatalogue = @@ -404,6 +406,293 @@ describe("Fertilizer Data Model", () => { }) }) + describe("updateFertilizerFromCatalogue", () => { + let p_id_catalogue: string + + beforeEach(async () => { + // Add a fertilizer to the catalogue + p_id_catalogue = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl: "Test Fertilizer", + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) + }) + + it("should update an existing fertilizer in the catalogue", async () => { + const updatedProperties = { + p_name_nl: "Updated Test Fertilizer", + p_description: "This is an updated test fertilizer", + p_dm: 50, + } + + await updateFertilizerFromCatalogue( + fdm, + principal_id, + b_id_farm, + p_id_catalogue, + updatedProperties, + ) + + const fertilizers = await getFertilizersFromCatalogue( + fdm, + principal_id, + b_id_farm, + ) + const updatedFertilizer = fertilizers.find( + (f) => f.p_id_catalogue === p_id_catalogue, + ) + expect(updatedFertilizer).toBeDefined() + expect(updatedFertilizer?.p_name_nl).toBe( + updatedProperties.p_name_nl, + ) + expect(updatedFertilizer?.p_description).toBe( + updatedProperties.p_description, + ) + expect(updatedFertilizer?.p_dm).toBe(updatedProperties.p_dm) + }) + + it("should throw an error if fertilizer does not exist in catalogue", async () => { + const nonExistingCatalogueId = createId() + const updatedProperties = { + p_name_nl: "Updated Test Fertilizer", + } + + await expect( + updateFertilizerFromCatalogue( + fdm, + principal_id, + b_id_farm, + nonExistingCatalogueId, + updatedProperties, + ), + ).rejects.toThrow("Exception for updateFertilizerFromCatalogue") + }) + + it("should update a fertilizer with a subset of properties", async () => { + const updatedProperties = { + p_name_nl: "Updated Name Only", + } + + await updateFertilizerFromCatalogue( + fdm, + principal_id, + b_id_farm, + p_id_catalogue, + updatedProperties, + ) + + const fertilizers = await getFertilizersFromCatalogue( + fdm, + principal_id, + b_id_farm, + ) + const updatedFertilizer = fertilizers.find( + (f) => f.p_id_catalogue === p_id_catalogue, + ) + expect(updatedFertilizer).toBeDefined() + expect(updatedFertilizer?.p_name_nl).toBe( + updatedProperties.p_name_nl, + ) + // Check that other properties remain unchanged + expect(updatedFertilizer?.p_description).toBe( + "This is a test fertilizer", + ) + expect(updatedFertilizer?.p_dm).toBe(37) + }) + + it("should throw an error when updating with invalid principal ID", async () => { + const updatedProperties = { + p_name_nl: "Updated Test Fertilizer", + } + const invalidPrincipalId = "invalid-principal-id" + + await expect( + updateFertilizerFromCatalogue( + fdm, + invalidPrincipalId, + b_id_farm, + p_id_catalogue, + updatedProperties, + ), + ).rejects.toThrow( + "Principal does not have permission to perform this action", + ) + }) + it("should update hash after updating a fertilizer", async () => { + const updatedProperties = { + p_name_nl: "Updated Test Fertilizer", + p_description: "This is an updated test fertilizer", + p_dm: 50, + } + const fertilizersBefore = await getFertilizersFromCatalogue( + fdm, + principal_id, + b_id_farm, + ) + const fertilizerBefore = fertilizersBefore.find( + (f) => f.p_id_catalogue === p_id_catalogue, + ) + expect(fertilizerBefore).toBeDefined() + const hashBefore = fertilizerBefore?.hash + expect(hashBefore).toBeDefined() + await updateFertilizerFromCatalogue( + fdm, + principal_id, + b_id_farm, + p_id_catalogue, + updatedProperties, + ) + const fertilizersAfter = await getFertilizersFromCatalogue( + fdm, + principal_id, + b_id_farm, + ) + const fertilizerAfter = fertilizersAfter.find( + (f) => f.p_id_catalogue === p_id_catalogue, + ) + expect(fertilizerAfter).toBeDefined() + const hashAfter = fertilizerAfter?.hash + expect(hashAfter).toBeDefined() + + expect(hashBefore).not.toBe(hashAfter) + }) + it("should throw an error if updating a fertilizer of another farm", async () => { + const farmName = "Test Farm 2" + const farmBusinessId = "98765" + const farmAddress = "456 Farm Lane" + const farmPostalCode = "54321" + const b_id_farm2 = await addFarm( + fdm, + principal_id, + farmName, + farmBusinessId, + farmAddress, + farmPostalCode, + ) + await enableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm2, + b_id_farm2, + ) + + // Add a fertilizer to the catalogue + const p_id_catalogue2 = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm2, + { + p_name_nl: "Test Fertilizer 2", + p_name_en: "Test Fertilizer (EN) 2", + p_description: "This is a test fertilizer 2", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + pr_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + }, + ) + const updatedProperties = { + p_name_nl: "Updated Test Fertilizer", + } + await expect( + updateFertilizerFromCatalogue( + fdm, + principal_id, + b_id_farm, + p_id_catalogue2, + updatedProperties, + ), + ).rejects.toThrow("Exception for updateFertilizerFromCatalogue") + }) + }) + describe("Fertilizer Application", () => { let b_id: string let p_id: string diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 74491a31d..745e4ef91 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -1,4 +1,4 @@ -import { asc, desc, eq, inArray } from "drizzle-orm" +import { and, asc, desc, eq, inArray } from "drizzle-orm" import { createId } from "./id" import { checkPermission } from "./authorization" @@ -327,6 +327,118 @@ export async function getFertilizer( } } +/** + * Updates an existing fertilizer in the catalogue of a farm. + * + * @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}. + * @param principal_id The ID of the principal making the request. + * @param b_id_farm The ID of the farm. + * @param p_id_catalogue The ID of the fertilizer in the catalogue to update + * @param properties The properties of the fertilizer to update. + * @returns A Promise that resolves when the fertilizer has been updated. + * @throws If updating the fertilizer fails. + * @alpha + */ +export async function updateFertilizerFromCatalogue( + fdm: FdmType, + principal_id: PrincipalId, + b_id_farm: schema.farmsTypeInsert["b_id_farm"], + p_id_catalogue: schema.fertilizersCatalogueTypeInsert["p_id_catalogue"], + properties: Partial<{ + p_name_nl: schema.fertilizersCatalogueTypeInsert["p_name_nl"] + p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] + p_description: schema.fertilizersCatalogueTypeInsert["p_description"] + p_dm: schema.fertilizersCatalogueTypeInsert["p_dm"] + p_density: schema.fertilizersCatalogueTypeInsert["p_density"] + p_om: schema.fertilizersCatalogueTypeInsert["p_om"] + p_a: schema.fertilizersCatalogueTypeInsert["p_a"] + p_hc: schema.fertilizersCatalogueTypeInsert["p_hc"] + p_eom: schema.fertilizersCatalogueTypeInsert["p_eom"] + p_eoc: schema.fertilizersCatalogueTypeInsert["p_eoc"] + p_c_rt: schema.fertilizersCatalogueTypeInsert["p_c_rt"] + p_c_of: schema.fertilizersCatalogueTypeInsert["p_c_of"] + p_c_if: schema.fertilizersCatalogueTypeInsert["p_c_if"] + p_c_fr: schema.fertilizersCatalogueTypeInsert["p_c_fr"] + p_cn_of: schema.fertilizersCatalogueTypeInsert["p_cn_of"] + p_n_rt: schema.fertilizersCatalogueTypeInsert["p_n_rt"] + p_n_if: schema.fertilizersCatalogueTypeInsert["p_n_if"] + p_n_of: schema.fertilizersCatalogueTypeInsert["p_n_of"] + p_n_wc: schema.fertilizersCatalogueTypeInsert["p_n_wc"] + p_p_rt: schema.fertilizersCatalogueTypeInsert["p_p_rt"] + p_k_rt: schema.fertilizersCatalogueTypeInsert["p_k_rt"] + p_mg_rt: schema.fertilizersCatalogueTypeInsert["p_mg_rt"] + p_ca_rt: schema.fertilizersCatalogueTypeInsert["p_ca_rt"] + p_ne: schema.fertilizersCatalogueTypeInsert["p_ne"] + p_s_rt: schema.fertilizersCatalogueTypeInsert["p_s_rt"] + p_s_wc: schema.fertilizersCatalogueTypeInsert["p_s_wc"] + p_cu_rt: schema.fertilizersCatalogueTypeInsert["p_cu_rt"] + p_zn_rt: schema.fertilizersCatalogueTypeInsert["p_zn_rt"] + p_na_rt: schema.fertilizersCatalogueTypeInsert["p_na_rt"] + p_si_rt: schema.fertilizersCatalogueTypeInsert["p_si_rt"] + p_b_rt: schema.fertilizersCatalogueTypeInsert["p_b_rt"] + p_mn_rt: schema.fertilizersCatalogueTypeInsert["p_mn_rt"] + p_ni_rt: schema.fertilizersCatalogueTypeInsert["p_ni_rt"] + p_fe_rt: schema.fertilizersCatalogueTypeInsert["p_fe_rt"] + p_mo_rt: schema.fertilizersCatalogueTypeInsert["p_mo_rt"] + p_co_rt: schema.fertilizersCatalogueTypeInsert["p_co_rt"] + p_as_rt: schema.fertilizersCatalogueTypeInsert["p_as_rt"] + p_cd_rt: schema.fertilizersCatalogueTypeInsert["p_cd_rt"] + pr_cr_rt: schema.fertilizersCatalogueTypeInsert["p_cr_rt"] + p_cr_vi: schema.fertilizersCatalogueTypeInsert["p_cr_vi"] + p_pb_rt: schema.fertilizersCatalogueTypeInsert["p_pb_rt"] + p_hg_rt: schema.fertilizersCatalogueTypeInsert["p_hg_rt"] + p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_cr"] + p_type_manure: schema.fertilizersCatalogueTypeInsert["p_type_manure"] + p_type_mineral: schema.fertilizersCatalogueTypeInsert["p_type_mineral"] + p_type_compost: schema.fertilizersCatalogueTypeInsert["p_type_compost"] + }>, +): Promise { + try { + await checkPermission( + fdm, + "farm", + "write", + b_id_farm, + principal_id, + "updateFertilizerFromCatalogue", + ) + + const existingFertilizer = await fdm + .select() + .from(schema.fertilizersCatalogue) + .where( + and( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + p_id_catalogue, + ), + eq(schema.fertilizersCatalogue.p_source, b_id_farm), + ), + ) + if (existingFertilizer.length === 0) { + throw new Error("Fertilizer does not exist in catalogue") + } + const updatedProperties = { + ...existingFertilizer[0], + ...properties, + hash: null, + } + updatedProperties.hash = hashFertilizer(updatedProperties) + + await fdm + .update(schema.fertilizersCatalogue) + .set(updatedProperties) + .where( + eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), + ) + } catch (err) { + throw handleError(err, "Exception for updateFertilizerFromCatalogue", { + p_id_catalogue, + properties, + }) + } +} + /** * Retrieves fertilizer details for a specified farm. * From d8ae3d0487cacc77aed9b43939e3baa081aece83 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 27 Mar 2025 13:16:01 +0100 Subject: [PATCH 11/55] Fix --- fdm-calculator/src/doses/get-dose-field.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/fdm-calculator/src/doses/get-dose-field.test.ts b/fdm-calculator/src/doses/get-dose-field.test.ts index 66ebae170..edd976190 100644 --- a/fdm-calculator/src/doses/get-dose-field.test.ts +++ b/fdm-calculator/src/doses/get-dose-field.test.ts @@ -57,10 +57,7 @@ describe("getDoseForField", () => { new Date(), "lease", ) - p_id_catalogue = `p_test_fertilizer_${Math.round(Math.random() * 1000)}` - await addFertilizerToCatalogue(fdm, { - p_id_catalogue: p_id_catalogue, - p_source: "", + p_id_catalogue = await addFertilizerToCatalogue(fdm, principal_id, b_id_farm, { p_name_nl: "", p_name_en: "", p_description: "", From 1218ab79ed9b85cecb6ef2022feff692d9a48532 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 27 Mar 2025 13:27:45 +0100 Subject: [PATCH 12/55] Add `hashCultivation` to get the hash of a cultivation item --- .changeset/eleven-dancers-brush.md | 5 +++++ fdm-data/src/cultivations/catalogues/brp.ts | 3 ++- fdm-data/src/cultivations/hash.ts | 9 +++++++++ fdm-data/src/index.ts | 5 +++-- 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .changeset/eleven-dancers-brush.md create mode 100644 fdm-data/src/cultivations/hash.ts diff --git a/.changeset/eleven-dancers-brush.md b/.changeset/eleven-dancers-brush.md new file mode 100644 index 000000000..227dbafe9 --- /dev/null +++ b/.changeset/eleven-dancers-brush.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-data": minor +--- + +Add `hashCultivation` to get the hash of a cultivation item diff --git a/fdm-data/src/cultivations/catalogues/brp.ts b/fdm-data/src/cultivations/catalogues/brp.ts index 0de3d2c36..617fc8eaa 100644 --- a/fdm-data/src/cultivations/catalogues/brp.ts +++ b/fdm-data/src/cultivations/catalogues/brp.ts @@ -1,4 +1,5 @@ import type { CatalogueCultivation, CatalogueCultivationItem } from "../d" +import { hashCultivation } from "../hash" import brp from "./brp.json" import xxhash from "xxhash-wasm" @@ -40,7 +41,7 @@ export function getCatalogueBrp(): CatalogueCultivation { } // Hash the item - item.hash = h32ToString(JSON.stringify(item)) + item.hash = hashCultivation(item) return item }) diff --git a/fdm-data/src/cultivations/hash.ts b/fdm-data/src/cultivations/hash.ts new file mode 100644 index 000000000..072864fd5 --- /dev/null +++ b/fdm-data/src/cultivations/hash.ts @@ -0,0 +1,9 @@ +import type { CatalogueCultivationItem } from "./d" +import xxhash from "xxhash-wasm" + +const { h32ToString } = await xxhash() + +export function hashCultivation(cultivation: CatalogueCultivationItem) { + const hash = h32ToString(JSON.stringify(cultivation)) + return hash +} diff --git a/fdm-data/src/index.ts b/fdm-data/src/index.ts index ccc27fa66..3addf3e03 100644 --- a/fdm-data/src/index.ts +++ b/fdm-data/src/index.ts @@ -11,6 +11,7 @@ * @packageDocumentation */ -export { getFertilizersCatalogue} from "./fertilizers" +export { getFertilizersCatalogue } from "./fertilizers" export { getCultivationCatalogue } from "./cultivations" -export { hashFertilizer} from "./fertilizers/hash" +export { hashFertilizer } from "./fertilizers/hash" +export { hashCultivation } from "./cultivations/hash" From 2ef3870361f1b6d3bb34e260c500cf299acc0288 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Thu, 27 Mar 2025 13:33:28 +0100 Subject: [PATCH 13/55] Improve syncing --- fdm-core/src/catalogues.test.ts | 1 - fdm-core/src/catalogues.ts | 100 ++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index a65e89365..673ca896c 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -658,7 +658,6 @@ describe("Catalogues syncing", () => { ) expect(itemSynced[0].p_id_catalogue).toBeDefined() - expect(itemSynced[0].hash).toBe(item[0].hash) }) it("should update cultivation catalogue", async () => { diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index eeb99e322..a31c7354c 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -7,6 +7,8 @@ import { checkPermission } from "./authorization" import { getCultivationCatalogue, getFertilizersCatalogue, + hashCultivation, + hashFertilizer, } from "@svenvw/fdm-data" /** @@ -368,83 +370,93 @@ export async function isCultivationCatalogueEnabled( * @returns A promise that resolves when the synchronization is complete. */ export async function syncCatalogues(fdm: FdmType): Promise { - try { - // Sync fertilizers catalogue (SRM) - const srmCatalogue = getFertilizersCatalogue("srm") - for (const srmItem of srmCatalogue) { - const existingItem = await fdm - .select() + await syncFertilizerCatalogue(fdm) + await syncCultivationCatalogue(fdm) +} + +async function syncFertilizerCatalogue(fdm: FdmType) { + const srmCatalogue = getFertilizersCatalogue("srm") + await fdm.transaction(async (tx) => { + for (const item of srmCatalogue) { + const hash = hashFertilizer(item) + const existing = await tx + .select({ hash: schema.fertilizersCatalogue.hash }) .from(schema.fertilizersCatalogue) .where( eq( schema.fertilizersCatalogue.p_id_catalogue, - srmItem.p_id_catalogue, + item.p_id_catalogue, ), ) .limit(1) - - if (existingItem.length === 0) { - await fdm.insert(schema.fertilizersCatalogue).values(srmItem) - console.log( - `Inserted fertilizer catalogue item: ${srmItem.p_id_catalogue}`, - ) + if (existing.length === 0) { + //add the item if does not exist + await tx.insert(schema.fertilizersCatalogue).values({ + ...item, + hash: hash, + }) } else { - // Update item if different - if (srmItem.hash && srmItem.hash !== existingItem[0].hash) { - await fdm + // update the hash if it is undefined, null or different + if ( + existing[0].hash === null || + existing[0].hash === undefined || + existing[0].hash !== hash + ) { + await tx .update(schema.fertilizersCatalogue) - .set(srmItem) + .set({ hash: hash }) .where( eq( schema.fertilizersCatalogue.p_id_catalogue, - srmItem.p_id_catalogue, + item.p_id_catalogue, ), ) - console.log( - `Updated fertilizer catalogue item: ${srmItem.p_id_catalogue}`, - ) } } } + }) +} - // Sync cultivation catalogue (BRP) - const brpCatalogue = getCultivationCatalogue("brp") - for (const brpItem of brpCatalogue) { - const existingItem = await fdm - .select() +async function syncCultivationCatalogue(fdm: FdmType) { + const brpCatalogue = getCultivationCatalogue("brp") + + await fdm.transaction(async (tx) => { + for (const item of brpCatalogue) { + const hash = hashCultivation(item) + const existing = await tx + .select({ hash: schema.cultivationsCatalogue.hash }) .from(schema.cultivationsCatalogue) .where( eq( schema.cultivationsCatalogue.b_lu_catalogue, - brpItem.b_lu_catalogue, + item.b_lu_catalogue, ), ) .limit(1) - - if (existingItem.length === 0) { - await fdm.insert(schema.cultivationsCatalogue).values(brpItem) - console.log( - `Inserted cultivation catalogue item: ${brpItem.b_lu_catalogue}`, - ) + if (existing.length === 0) { + //add the item if does not exist + await tx.insert(schema.cultivationsCatalogue).values({ + ...item, + hash: hash, + }) } else { - // Update item if different - if (brpItem.hash && brpItem.hash !== existingItem[0].hash) { - await fdm + // update the hash if it is undefined, null or different + if ( + existing[0].hash === null || + existing[0].hash === undefined || + existing[0].hash !== hash + ) { + await tx .update(schema.cultivationsCatalogue) - .set(brpItem) + .set({ hash: hash }) .where( eq( schema.cultivationsCatalogue.b_lu_catalogue, - brpItem.b_lu_catalogue, + item.b_lu_catalogue, ), ) - console.log( - `Updated cultivation catalogue item: ${brpItem.b_lu_catalogue}`, - ) } } } - } catch (err) { - throw handleError(err, "Exception for syncCatalogues") - } + }) } From 226a3cc2145e89f4487f376c7cf6586a590371f3 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:40:36 +0100 Subject: [PATCH 14/55] Extend the output --- .changeset/red-oranges-drop.md | 2 +- fdm-core/src/fertilizer.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.changeset/red-oranges-drop.md b/.changeset/red-oranges-drop.md index bda6e7e72..6489b1202 100644 --- a/.changeset/red-oranges-drop.md +++ b/.changeset/red-oranges-drop.md @@ -2,4 +2,4 @@ "@svenvw/fdm-core": minor --- -Add to the output of `getFertilizer` the values for `p_type_*` +Add to the output of `getFertilizer` and `getFertilizers` the values for `p_type_*` and `p_source` diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 745e4ef91..9cbf3fb9a 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -261,6 +261,7 @@ export async function getFertilizer( const fertilizer = await fdm .select({ p_id: schema.fertilizers.p_id, + p_source: schema.fertilizersCatalogue.p_source, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_name_en: schema.fertilizersCatalogue.p_name_en, p_description: schema.fertilizersCatalogue.p_description, @@ -471,6 +472,7 @@ export async function getFertilizers( const fertilizers = await fdm .select({ p_id: schema.fertilizers.p_id, + p_source: schema.fertilizersCatalogue.p_source, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_name_en: schema.fertilizersCatalogue.p_name_en, p_description: schema.fertilizersCatalogue.p_description, @@ -506,6 +508,9 @@ export async function getFertilizers( p_pb_rt: schema.fertilizersCatalogue.p_pb_rt, p_hg_rt: schema.fertilizersCatalogue.p_hg_rt, p_cl_cr: schema.fertilizersCatalogue.p_cl_cr, + p_type_manure: schema.fertilizersCatalogue.p_type_manure, + p_type_mineral: schema.fertilizersCatalogue.p_type_mineral, + p_type_compost: schema.fertilizersCatalogue.p_type_compost, }) .from(schema.fertilizers) .leftJoin( From 17acee31e3f9e1acb9296079b71b31ef6d97fc1c Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:32:11 +0100 Subject: [PATCH 15/55] Improve layout of cards --- .../custom/fertilizer/formschema.tsx | 8 + .../farm.$b_id_farm.fertilizers.$p_id.tsx | 821 ++++++++++-------- 2 files changed, 466 insertions(+), 363 deletions(-) diff --git a/fdm-app/app/components/custom/fertilizer/formschema.tsx b/fdm-app/app/components/custom/fertilizer/formschema.tsx index bd0d56f6f..c080c724b 100644 --- a/fdm-app/app/components/custom/fertilizer/formschema.tsx +++ b/fdm-app/app/components/custom/fertilizer/formschema.tsx @@ -1,6 +1,14 @@ import { z } from "zod" export const FormSchema = z.object({ + p_name_nl: z.string({ + required_error: "Naam is verplicht", + invalid_type_error: "Ongeldige waarde", + }), + p_type: z.string({ + required_error: "Type is verplicht", + invalid_type_error: "Ongeldige waarde", + }), p_n_rt: z.coerce.number({ invalid_type_error: "Ongeldige waarde", }), diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index 614aeebdb..531187230 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -18,6 +18,13 @@ import { FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { SidebarInset } from "@/components/ui/sidebar" import { getSession } from "@/lib/auth.server" import { handleLoaderError } from "@/lib/error" @@ -85,13 +92,20 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get the available fertilizers const fertilizer = await getFertilizer(fdm, p_id) + // Set editable status + let editable = true + console.log(fertilizer.p_source) + if (fertilizer.p_source === b_id_farm) { + editable = true + } + // Return user information from loader return { farm: farm, b_id_farm: b_id_farm, farmOptions: farmOptions, fertilizer: fertilizer, - editable: true, + editable: editable, } } catch (error) { throw handleLoaderError(error) @@ -108,15 +122,27 @@ export default function FarmFertilizerBlock() { const loaderData = useLoaderData() const fertilizer = loaderData.fertilizer + fertilizer.p_type = "" + if (fertilizer.p_type_manure) { + fertilizer.p_type = "manure" + } else if (fertilizer.p_type_compost) { + fertilizer.p_type = "compost" + } else if (fertilizer.p_type_mineral) { + fertilizer.p_type = "mineral" + } + const form = useRemixForm>({ mode: "onTouched", resolver: zodResolver(FormSchema), defaultValues: { + p_name_nl: fertilizer.p_name_nl, + p_type: fertilizer.p_type, p_n_rt: fertilizer.p_n_rt, + p_n_wc: fertilizer.p_n_wc, p_p_rt: fertilizer.p_p_rt, p_k_rt: fertilizer.p_k_rt, p_om: fertilizer.p_om, - p_c_rt: fertilizer.p_c_rt, + p_eoc: fertilizer.p_eoc, p_s_rt: fertilizer.p_s_rt, p_ca_rt: fertilizer.p_ca_rt, p_mg_rt: fertilizer.p_mg_rt, @@ -125,11 +151,14 @@ export default function FarmFertilizerBlock() { useEffect(() => { form.reset({ + p_name_nl: fertilizer.p_name_nl, + p_type: fertilizer.p_type, p_n_rt: fertilizer.p_n_rt, + p_n_wc: fertilizer.p_n_wc, p_p_rt: fertilizer.p_p_rt, p_k_rt: fertilizer.p_k_rt, p_om: fertilizer.p_om, - p_c_rt: fertilizer.p_c_rt, + p_eoc: fertilizer.p_eoc, p_s_rt: fertilizer.p_s_rt, p_ca_rt: fertilizer.p_ca_rt, p_mg_rt: fertilizer.p_mg_rt, @@ -170,64 +199,132 @@ export default function FarmFertilizerBlock() { -
+
Naam - - {fertilizer.p_name_nl} - +
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + { + fertilizer.p_name_nl + } + + )} +
-
+
- Bron + Catalogus - - {fertilizer.p_source} + + {fertilizer.p_source !== + loaderData.b_id_farm ? ( + + { + loaderData.farm + .b_name_farm + } + + ) : ( + + { + fertilizer.p_source + } + + )}
- {fertilizer.p_description && ( -
- - Omschrijving - - - { - fertilizer.p_description - } - -
- )} - {fertilizer.p_type_manure && ( -
- - Type - - - Mest - -
- )} - {fertilizer.p_type_mineral && ( -
- - Type - - - Mineraal - -
- )} - {fertilizer.p_type_compost && ( -
- - Type +
+ + Type + + {loaderData.editable ? ( + ( + + + + + + )} + /> + ) : ( + + {fertilizer.p_type_manure ? ( + + Mest + + ) : null} + {fertilizer.p_type_compost ? ( + + Compost + + ) : null} + {fertilizer.p_type_mineral ? ( + + Kunstmest + + ) : null} - - Compost - -
- )} + )} +
@@ -238,333 +335,331 @@ export default function FarmFertilizerBlock() { - {fertilizer.p_n_rt !== null && ( -
- - Stikstof (N) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_n_rt - } - - )} - - g N / kg +
+ {/* Stikstof Row */} +
+ Stikstof +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_n_rt} -
+ )}
- )} - {fertilizer.p_p_rt !== null && ( -
- - Fosfor (P) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_p_rt - } - - )} - - {" "} - g P / kg +
+ g N / kg +
+ + {/* Stikstof, werkingscoëfficiënt Row */} +
+ Stikstof, + werkingscoëfficiënt +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_n_wc} -
+ )}
- )} - {fertilizer.p_k_rt !== null && ( -
- - Kalium (K) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_k_rt - } - - )} - - {" "} - g K / kg +
+ - +
+ {/* Fosfaat Row */} +
+ Fosfaat +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_p_rt} -
+ )}
- )} - {fertilizer.p_om !== null && ( -
- - Organische stof (OS) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_om - } - - )} - - {" "} - g OS / kg +
+ g P2O5 / kg +
+ + {/* Kalium Row */} +
+ Kalium +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_k_rt} -
+ )}
- )} - {fertilizer.p_c_rt !== null && ( -
- - Koolstof (C) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_c_rt - } - - )} - - {" "} - g C / kg +
+ g K2O / kg +
+ + {/* Organische stof Row */} +
+ Organische stof +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_om} -
+ )}
- )} - {fertilizer.p_s_rt !== null && ( -
- - Zwavel (S) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_s_rt - } - - )} - - {" "} - g SO3 / kg +
+ g OS / kg +
+ + {/* Koolstof, effectief Row */} +
+ Koolstof, effectief +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_eoc} -
+ )}
- )} - {fertilizer.p_ca_rt !== null && ( -
- - Calcium (Ca) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_ca_rt - } - - )} - - {" "} - g Ca / kg +
+ g EOC / kg +
+ + {/* Zwavel Row */} +
+ Zwavel +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_s_rt} -
+ )}
- )} - {fertilizer.p_mg_rt !== null && ( -
- - Magnesium (Mg) - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_mg_rt - } - - )} - - {" "} - g Mg / kg +
+ g SO3 / kg +
+ + {/* Calcium (Ca) Row */} +
+ Calcium +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_ca_rt} + + )} +
+
+ g CaO / kg +
+ + {/* Magnesium (Mg) Row */} +
+ Magnesium +
+
+ {loaderData.editable ? ( + ( + + + + + + + + )} + /> + ) : ( + + {fertilizer.p_mg_rt} -
+ )}
- )} +
+ g MgO / kg +
+
From 47b4c8072d44d2c4a08d099c0222d74444c18fa2 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:47:05 +0100 Subject: [PATCH 16/55] Add submit buttons --- .../farm.$b_id_farm.fertilizers.$p_id.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index 531187230..15938b728 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -1,11 +1,14 @@ import { FarmHeader } from "@/components/custom/farm/farm-header" import { FarmTitle } from "@/components/custom/farm/farm-title" import { FormSchema } from "@/components/custom/fertilizer/formschema" +import { LoadingSpinner } from "@/components/custom/loadingspinner" import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, + CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" @@ -326,6 +329,23 @@ export default function FarmFertilizerBlock() { )}
+ {loaderData.editable && ( + + + + )} @@ -661,6 +681,23 @@ export default function FarmFertilizerBlock() {
+ {loaderData.editable && ( + + + + )}
From 4e1e38f3c8e35d93df44b6265695715244e0e5b8 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:11:09 +0100 Subject: [PATCH 17/55] Fix missing output --- fdm-core/src/fertilizer.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 9cbf3fb9a..6407b2229 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -269,6 +269,18 @@ export async function getFertilizer( schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, p_picking_date: schema.fertilizerPicking.p_picking_date, + p_dm: schema.fertilizersCatalogue.p_dm, + p_density: schema.fertilizersCatalogue.p_density, + p_om: schema.fertilizersCatalogue.p_om, + p_a: schema.fertilizersCatalogue.p_a, + p_hc: schema.fertilizersCatalogue.p_hc, + p_eom: schema.fertilizersCatalogue.p_eom, + p_eoc: schema.fertilizersCatalogue.p_eoc, + p_c_rt: schema.fertilizersCatalogue.p_c_rt, + p_c_of: schema.fertilizersCatalogue.p_c_of, + p_c_if: schema.fertilizersCatalogue.p_c_if, + p_c_fr: schema.fertilizersCatalogue.p_c_fr, + p_cn_of: schema.fertilizersCatalogue.p_cn_of, p_n_rt: schema.fertilizersCatalogue.p_n_rt, p_n_if: schema.fertilizersCatalogue.p_n_if, p_n_of: schema.fertilizersCatalogue.p_n_of, @@ -480,6 +492,18 @@ export async function getFertilizers( schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, p_picking_date: schema.fertilizerPicking.p_picking_date, + p_dm: schema.fertilizersCatalogue.p_dm, + p_density: schema.fertilizersCatalogue.p_density, + p_om: schema.fertilizersCatalogue.p_om, + p_a: schema.fertilizersCatalogue.p_a, + p_hc: schema.fertilizersCatalogue.p_hc, + p_eom: schema.fertilizersCatalogue.p_eom, + p_eoc: schema.fertilizersCatalogue.p_eoc, + p_c_rt: schema.fertilizersCatalogue.p_c_rt, + p_c_of: schema.fertilizersCatalogue.p_c_of, + p_c_if: schema.fertilizersCatalogue.p_c_if, + p_c_fr: schema.fertilizersCatalogue.p_c_fr, + p_cn_of: schema.fertilizersCatalogue.p_cn_of, p_n_rt: schema.fertilizersCatalogue.p_n_rt, p_n_if: schema.fertilizersCatalogue.p_n_if, p_n_of: schema.fertilizersCatalogue.p_n_of, From 5c97d58c06c9188411b7d868b7cddee506a4e33f Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:13:24 +0100 Subject: [PATCH 18/55] Extend schema with min and max values --- .../custom/fertilizer/formschema.tsx | 114 ++++++++++++++---- 1 file changed, 90 insertions(+), 24 deletions(-) diff --git a/fdm-app/app/components/custom/fertilizer/formschema.tsx b/fdm-app/app/components/custom/fertilizer/formschema.tsx index c080c724b..7e5c3bf42 100644 --- a/fdm-app/app/components/custom/fertilizer/formschema.tsx +++ b/fdm-app/app/components/custom/fertilizer/formschema.tsx @@ -9,28 +9,94 @@ export const FormSchema = z.object({ required_error: "Type is verplicht", invalid_type_error: "Ongeldige waarde", }), - p_n_rt: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), - p_p_rt: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), - p_k_rt: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), - p_om: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), - p_c_rt: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), - p_s_rt: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), - p_ca_rt: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), - p_mg_rt: z.coerce.number({ - invalid_type_error: "Ongeldige waarde", - }), + p_n_rt: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000, { + message: "Waarde mag niet groter zijn dan 1000", + }), + p_n_wc: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1, { + message: "Waarde mag niet groter zijn dan 1", + }), + p_p_rt: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(4583, { + message: "Waarde mag niet groter zijn dan 4583", + }), + p_k_rt: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(2409.2, { + message: "Waarde mag niet groter zijn dan 2409.2", + }), + p_om: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000, { + message: "Waarde mag niet groter zijn dan 1000", + }), + p_eoc: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1000, { + message: "Waarde mag niet groter zijn dan 1000", + }), + p_s_rt: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(2497.2, { + message: "Waarde mag niet groter zijn dan 2497.2", + }), + p_ca_rt: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1399.2, { + message: "Waarde mag niet groter zijn dan 1399.2", + }), + p_mg_rt: z.coerce + .number({ + invalid_type_error: "Ongeldige waarde", + }) + .min(0, { + message: "Waarde mag niet negatief zijn", + }) + .max(1659, { + message: "Waarde mag niet groter zijn dan 1659", + }), }) From b5f2693c1d18fa0bb2a025364ebeb1d4cc88a74b Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:14:34 +0100 Subject: [PATCH 19/55] Typo --- fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index 15938b728..743361825 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -96,8 +96,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const fertilizer = await getFertilizer(fdm, p_id) // Set editable status - let editable = true - console.log(fertilizer.p_source) + let editable = false if (fertilizer.p_source === b_id_farm) { editable = true } @@ -242,7 +241,7 @@ export default function FarmFertilizerBlock() { Catalogus - {fertilizer.p_source !== + {fertilizer.p_source === loaderData.b_id_farm ? ( { From 6bd5f8781243b5cbc401780ae035457d2c9ca598 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:01:49 +0100 Subject: [PATCH 20/55] Include p_id_catalogue --- fdm-core/src/fertilizer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 6407b2229..1c9934f02 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -261,6 +261,7 @@ export async function getFertilizer( const fertilizer = await fdm .select({ p_id: schema.fertilizers.p_id, + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_source: schema.fertilizersCatalogue.p_source, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_name_en: schema.fertilizersCatalogue.p_name_en, @@ -484,6 +485,7 @@ export async function getFertilizers( const fertilizers = await fdm .select({ p_id: schema.fertilizers.p_id, + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_source: schema.fertilizersCatalogue.p_source, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_name_en: schema.fertilizersCatalogue.p_name_en, From cbef8d4ab741c1f5d90ff00c427be6f9afc06b61 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:02:09 +0100 Subject: [PATCH 21/55] Export updateFertilizerFromCatalogue --- fdm-core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index 509efdacb..000512498 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -25,6 +25,7 @@ export { addFarm, getFarm, getFarms, updateFarm } from "./farm" export { addField, getField, getFields, updateField } from "./field" export { addFertilizerToCatalogue, + updateFertilizerFromCatalogue, getFertilizersFromCatalogue, addFertilizer, removeFertilizer, From e0a09548dbee6a9cdd338374acb39c961c2a1614 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:02:55 +0100 Subject: [PATCH 22/55] Add header for fertilizers --- .../components/custom/farm/farm-header.tsx | 57 +++++++++++++++++++ fdm-app/app/components/custom/farm/farm.d.ts | 5 ++ .../farm.$b_id_farm.fertilizers.$p_id.tsx | 20 ++++++- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/components/custom/farm/farm-header.tsx b/fdm-app/app/components/custom/farm/farm-header.tsx index 9fdc249af..f289ed5c0 100644 --- a/fdm-app/app/components/custom/farm/farm-header.tsx +++ b/fdm-app/app/components/custom/farm/farm-header.tsx @@ -20,6 +20,7 @@ import { ChevronDown } from "lucide-react" import { NavLink } from "react-router" import type { FarmOptions, + FertilizerOption, FieldOptions, HeaderAction, LayerKey, @@ -33,6 +34,8 @@ interface FarmHeaderProps { b_id: string | undefined layerOptions: LayerOptions[] layerSelected: LayerKey | undefined + fertilizerOptions: FertilizerOption[] | undefined + p_id: string | undefined action: HeaderAction } @@ -43,6 +46,8 @@ export function FarmHeader({ b_id, layerOptions, layerSelected, + fertilizerOptions, + p_id, action, }: FarmHeaderProps) { return ( @@ -178,6 +183,58 @@ export function FarmHeader({ ) : null} + {fertilizerOptions && + fertilizerOptions.length > 0 ? ( + <> + + + + Meststof + + + + + + + {p_id && fertilizerOptions + ? (fertilizerOptions.find( + (option) => + option.p_id === + p_id, + )?.p_name_nl ?? + "Unknown fertilizer") + : "Kies een meststof"} + + + + {fertilizerOptions.map( + (option) => ( + + + { + option.p_name_nl + } + + + ), + )} + + + + + ) : null} ) : null} diff --git a/fdm-app/app/components/custom/farm/farm.d.ts b/fdm-app/app/components/custom/farm/farm.d.ts index 4609f5e04..2462249eb 100644 --- a/fdm-app/app/components/custom/farm/farm.d.ts +++ b/fdm-app/app/components/custom/farm/farm.d.ts @@ -22,6 +22,11 @@ export interface LayerOption { export type LayerOptions = LayerOption[] +export interface FertilizerOption { + p_id: string + p_name_nl: string +} + export interface HeaderAction { label: string to: string diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index 743361825..983ff5c04 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -92,11 +92,24 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } }) - // Get the available fertilizers + // Get selected fertilizer const fertilizer = await getFertilizer(fdm, p_id) + // Get the available fertilizers + const fertilizers = await getFertilizers( + fdm, + session.principal_id, + b_id_farm, + ) + const fertilizerOptions = fertilizers.map((fertilizer) => { + return { + p_id: fertilizer.p_id, + p_name_nl: fertilizer.p_name_nl, + } + }) + // Set editable status - let editable = false + let editable = true if (fertilizer.p_source === b_id_farm) { editable = true } @@ -106,6 +119,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { farm: farm, b_id_farm: b_id_farm, farmOptions: farmOptions, + fertilizerOptions: fertilizerOptions, fertilizer: fertilizer, editable: editable, } @@ -176,6 +190,8 @@ export default function FarmFertilizerBlock() { to: "../fertilizers", label: "Terug naar overzicht", }} + fertilizerOptions={loaderData.fertilizerOptions} + p_id={loaderData.fertilizer.p_id} />
Date: Fri, 28 Mar 2025 13:17:47 +0100 Subject: [PATCH 23/55] Move form into component --- .../app/components/custom/fertilizer/form.tsx | 436 ++++++++++++ .../farm.$b_id_farm.fertilizers.$p_id.tsx | 640 +++--------------- 2 files changed, 530 insertions(+), 546 deletions(-) create mode 100644 fdm-app/app/components/custom/fertilizer/form.tsx diff --git a/fdm-app/app/components/custom/fertilizer/form.tsx b/fdm-app/app/components/custom/fertilizer/form.tsx new file mode 100644 index 000000000..d9cf05922 --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/form.tsx @@ -0,0 +1,436 @@ +import { type Farm, type Fertilizer } from "@svenvw/fdm-core"; +import { type z, type ZodType } from "zod"; +import { type UseFormReturn } from "react-hook-form"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { FormControl, FormDescription, FormField, FormItem, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Form } from "react-router"; +import { RemixFormProvider } from "remix-hook-form"; +import { LoadingSpinner } from "../loadingspinner"; + +export function FertilizerForm({ + fertilizer, + form, + editable, + farm, }: { fertilizer: Fertilizer; form: UseFormReturn, ZodType, undefined>; editable: boolean; farm: Farm }) { + + return ( + +
+
+
+ + + Algemene informatie + + Details over de meststof + + + +
+ Naam +
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_name_nl} + )} +
+
+
+ + Catalogus + + + {fertilizer.p_source === + farm.b_id_farm ? ( + + {farm.b_name_farm} + + ) : ( + + {fertilizer.p_source} + + )} + +
+
+ Type + {editable ? ( + ( + + + + + + )} + /> + ) : ( + + {fertilizer.p_type_manure ? ( + + Mest + + ) : null} + {fertilizer.p_type_compost ? ( + + Compost + + ) : null} + {fertilizer.p_type_mineral ? ( + + Kunstmest + + ) : null} + + )} +
+
+ {editable && ( + + + + )} +
+ + + Samenstelling + + De gehalten van deze meststof + + + +
+ {/* Stikstof Row */} +
Stikstof
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_n_rt} + )} +
+
+ g N / kg +
+ + {/* Stikstof, werkingscoëfficiënt Row */} +
+ Stikstof, werkingscoëfficiënt +
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_n_wc} + )} +
+
+ - +
+ {/* Fosfaat Row */} +
Fosfaat
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_p_rt} + )} +
+
+ g P2O5 / kg +
+ + {/* Kalium Row */} +
Kalium
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_k_rt} + )} +
+
+ g K2O / kg +
+ + {/* Organische stof Row */} +
+ Organische stof +
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_om} + )} +
+
+ g OS / kg +
+ + {/* Koolstof, effectief Row */} +
+ Koolstof, effectief +
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_eoc} + )} +
+
+ g EOC / kg +
+ + {/* Zwavel Row */} +
Zwavel
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_s_rt} + )} +
+
+ g SO3 / kg +
+ + {/* Calcium (Ca) Row */} +
Calcium
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_ca_rt} + )} +
+
+ g CaO / kg +
+ + {/* Magnesium (Mg) Row */} +
Magnesium
+
+ {editable ? ( + ( + + + + + + + + )} + /> + ) : ( + {fertilizer.p_mg_rt} + )} +
+
+ g MgO / kg +
+
+
+ {editable && ( + + + + )} +
+
+
+
+
+ ) +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index 983ff5c04..d5675c02e 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -1,47 +1,29 @@ import { FarmHeader } from "@/components/custom/farm/farm-header" import { FarmTitle } from "@/components/custom/farm/farm-title" +import { FertilizerForm } from "@/components/custom/fertilizer/form" import { FormSchema } from "@/components/custom/fertilizer/formschema" -import { LoadingSpinner } from "@/components/custom/loadingspinner" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" import { SidebarInset } from "@/components/ui/sidebar" import { getSession } from "@/lib/auth.server" -import { handleLoaderError } from "@/lib/error" +import { handleActionError, handleLoaderError } from "@/lib/error" import { fdm } from "@/lib/fdm.server" +import { extractFormValuesFromRequest } from "@/lib/form" import { zodResolver } from "@hookform/resolvers/zod" -import { getFarm, getFarms, getFertilizer } from "@svenvw/fdm-core" +import { + getFarm, + getFarms, + getFertilizer, + getFertilizers, +} from "@svenvw/fdm-core" +import { updateFertilizerFromCatalogue } from "@svenvw/fdm-core" import { useEffect } from "react" import { - Form, + type ActionFunctionArgs, type LoaderFunctionArgs, data, useLoaderData, } from "react-router" -import { RemixFormProvider, useRemixForm } from "remix-hook-form" +import { useRemixForm } from "remix-hook-form" +import { dataWithSuccess } from "remix-toast" import type { z } from "zod" export async function loader({ request, params }: LoaderFunctionArgs) { @@ -199,527 +181,93 @@ export default function FarmFertilizerBlock() { description={"Bekijk de eigenschappen van dit product"} />
- -
-
-
- - - - Algemene informatie - - - Details over de meststof - - - -
- - Naam - -
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - { - fertilizer.p_name_nl - } - - )} -
-
-
- - Catalogus - - - {fertilizer.p_source === - loaderData.b_id_farm ? ( - - { - loaderData.farm - .b_name_farm - } - - ) : ( - - { - fertilizer.p_source - } - - )} - -
-
- - Type - - {loaderData.editable ? ( - ( - - - - - - )} - /> - ) : ( - - {fertilizer.p_type_manure ? ( - - Mest - - ) : null} - {fertilizer.p_type_compost ? ( - - Compost - - ) : null} - {fertilizer.p_type_mineral ? ( - - Kunstmest - - ) : null} - - )} -
-
- {loaderData.editable && ( - - - - )} -
- - - Samenstelling - - De gehalten van deze meststof - - - -
- {/* Stikstof Row */} -
- Stikstof -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_n_rt} - - )} -
-
- g N / kg -
+export async function action({ request, params }: ActionFunctionArgs) { + try { + const b_id_farm = params.b_id_farm + const p_id = params.p_id - {/* Stikstof, werkingscoëfficiënt Row */} -
- Stikstof, - werkingscoëfficiënt -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_n_wc} - - )} -
-
- - -
- {/* Fosfaat Row */} -
- Fosfaat -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_p_rt} - - )} -
-
- g P2O5 / kg -
+ if (!b_id_farm) { + throw new Error("missing: b_id_farm") + } + if (!p_id) { + throw new Error("missing: p_id") + } - {/* Kalium Row */} -
- Kalium -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_k_rt} - - )} -
-
- g K2O / kg -
+ const session = await getSession(request) + const formValues = await extractFormValuesFromRequest( + request, + FormSchema, + ) - {/* Organische stof Row */} -
- Organische stof -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_om} - - )} -
-
- g OS / kg -
+ const { + p_name_nl, + p_type, + p_n_rt, + p_n_wc, + p_p_rt, + p_k_rt, + p_om, + p_eoc, + p_s_rt, + p_ca_rt, + p_mg_rt, + } = formValues - {/* Koolstof, effectief Row */} -
- Koolstof, effectief -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_eoc} - - )} -
-
- g EOC / kg -
+ let p_type_manure = false + let p_type_compost = false + let p_type_mineral = false + if (p_type === "manure") { + p_type_manure = true + } + if (p_type === "compost") { + p_type_compost = true + } + if (p_type === "mineral") { + p_type_mineral = true + } - {/* Zwavel Row */} -
- Zwavel -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_s_rt} - - )} -
-
- g SO3 / kg -
+ const fertilizer = await getFertilizer(fdm, p_id) + const p_id_catalogue = fertilizer.p_id_catalogue - {/* Calcium (Ca) Row */} -
- Calcium -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_ca_rt} - - )} -
-
- g CaO / kg -
+ await updateFertilizerFromCatalogue( + fdm, + session.principal_id, + b_id_farm, + p_id_catalogue, + { + p_name_nl, + p_type_manure, + p_type_mineral, + p_type_compost, + p_n_rt, + p_n_wc, + p_p_rt, + p_k_rt, + p_om, + p_eoc, + p_s_rt, + p_ca_rt, + p_mg_rt, + }, + ) - {/* Magnesium (Mg) Row */} -
- Magnesium -
-
- {loaderData.editable ? ( - ( - - - - - - - - )} - /> - ) : ( - - {fertilizer.p_mg_rt} - - )} -
-
- g MgO / kg -
-
-
- {loaderData.editable && ( - - - - )} -
-
-
-
-
-
-
- - ) + return dataWithSuccess( + { result: "Data saved successfully" }, + { message: "Meststof is bijgewerkt! 🎉" }, + ) + } catch (error) { + throw handleActionError(error) + } } From 8fa1afaeeb4a4e0467f78a3b5a3375806227ffca Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:25:06 +0100 Subject: [PATCH 24/55] Move button --- fdm-app/app/components/custom/fertilizer/table.tsx | 11 +++++++++++ .../routes/farm.$b_id_farm.fertilizers._index.tsx | 14 ++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/fdm-app/app/components/custom/fertilizer/table.tsx b/fdm-app/app/components/custom/fertilizer/table.tsx index 02d3a61d9..7801e4175 100644 --- a/fdm-app/app/components/custom/fertilizer/table.tsx +++ b/fdm-app/app/components/custom/fertilizer/table.tsx @@ -18,6 +18,9 @@ import { } from "@/components/ui/table" import { useState } from "react" import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { NavLink } from "react-router" +import { Plus } from "lucide-react" interface DataTableProps { columns: ColumnDef[] @@ -62,6 +65,14 @@ export function DataTable({ } className="max-w-sm" /> +
+ + + +
diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx index 1313f32b3..cc27a759c 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx @@ -10,12 +10,7 @@ import { getSession } from "@/lib/auth.server" import { handleLoaderError } from "@/lib/error" import { fdm } from "@/lib/fdm.server" import { getFarm, getFarms, getFertilizers } from "@svenvw/fdm-core" -import { - type LoaderFunctionArgs, - Outlet, - data, - useLoaderData, -} from "react-router" +import { type LoaderFunctionArgs, data, useLoaderData } from "react-router" export async function loader({ request, params }: LoaderFunctionArgs) { try { @@ -62,7 +57,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { session.principal_id, b_id_farm, ) - // console.log(fertilizers) // Return user information from loader return { @@ -89,11 +83,7 @@ export default function FarmFertilizersBlock() {
Date: Fri, 28 Mar 2025 13:37:16 +0100 Subject: [PATCH 25/55] Fix --- fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx index d5675c02e..0bf7b0928 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -91,7 +91,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }) // Set editable status - let editable = true + let editable = false if (fertilizer.p_source === b_id_farm) { editable = true } From f6920b8bda2b53e2b56f05e81db8be1d991a3ae9 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:37:39 +0100 Subject: [PATCH 26/55] Improve table with fertilizers --- .../components/custom/fertilizer/columns.tsx | 51 +++++++++--- .../app/components/custom/fertilizer/form.tsx | 83 ++++++++++++++----- 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/fdm-app/app/components/custom/fertilizer/columns.tsx b/fdm-app/app/components/custom/fertilizer/columns.tsx index c9ebcd712..2d9dcb201 100644 --- a/fdm-app/app/components/custom/fertilizer/columns.tsx +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -1,7 +1,5 @@ import type { ColumnDef } from "@tanstack/react-table" -import { - SquareArrowOutUpRight, -} from "lucide-react" +import { ArrowRight, Pencil, SquareArrowOutUpRight } from "lucide-react" import { DataTableColumnHeader } from "./column-header" import { Tooltip, @@ -10,6 +8,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { NavLink } from "react-router" +import { Badge } from "@/components/ui/badge" export type Fertilizer = { p_id: string @@ -20,10 +19,10 @@ export type Fertilizer = { } export const columns: ColumnDef[] = [ - { - accessorKey: "p_id", - header: "ID", - }, + // { + // accessorKey: "p_id", + // header: "ID", + // }, { accessorKey: "p_name_nl", header: "Naam", @@ -37,11 +36,43 @@ export const columns: ColumnDef[] = [ { accessorKey: "p_p_rt", header: ({ column }) => { - return + return + }, + }, + { + accessorKey: "p_k_rt", + header: ({ column }) => { + return + }, + }, + { + accessorKey: "p_eoc", + header: ({ column }) => { + return + }, + }, + { + accessorKey: "Type", + cell: ({ row }) => { + const fertilizer = row.original + + return ( + + {fertilizer.p_type_manure ? ( + Mest + ) : null} + {fertilizer.p_type_compost ? ( + Compost + ) : null} + {fertilizer.p_type_mineral ? ( + Kunstmest + ) : null} + + ) }, }, { - accessorKey: "Opties", + accessorKey: "Details", cell: ({ row }) => { const fertilizer = row.original @@ -50,7 +81,7 @@ export const columns: ColumnDef[] = [ - + {`Bekijk details over ${fertilizer.p_name_nl}`} diff --git a/fdm-app/app/components/custom/fertilizer/form.tsx b/fdm-app/app/components/custom/fertilizer/form.tsx index d9cf05922..776843ef6 100644 --- a/fdm-app/app/components/custom/fertilizer/form.tsx +++ b/fdm-app/app/components/custom/fertilizer/form.tsx @@ -1,25 +1,57 @@ -import { type Farm, type Fertilizer } from "@svenvw/fdm-core"; -import { type z, type ZodType } from "zod"; -import { type UseFormReturn } from "react-hook-form"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { FormControl, FormDescription, FormField, FormItem, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Form } from "react-router"; -import { RemixFormProvider } from "remix-hook-form"; -import { LoadingSpinner } from "../loadingspinner"; +import { type Farm, type Fertilizer } from "@svenvw/fdm-core" +import { type z, type ZodType } from "zod" +import { type UseFormReturn } from "react-hook-form" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Form } from "react-router" +import { RemixFormProvider } from "remix-hook-form" +import { LoadingSpinner } from "../loadingspinner" export function FertilizerForm({ fertilizer, form, editable, - farm, }: { fertilizer: Fertilizer; form: UseFormReturn, ZodType, undefined>; editable: boolean; farm: Farm }) { - + farm, +}: { + fertilizer: Fertilizer + form: UseFormReturn< + z.infer, + ZodType, + undefined + > + editable: boolean + farm: Farm +}) { return ( -
+
@@ -86,9 +118,11 @@ export function FertilizerForm({ } defaultValue={ field.value - } + } name={field.name} - disabled={field.disabled} + disabled={ + field.disabled + } className="w-full text-right" > @@ -115,17 +149,26 @@ export function FertilizerForm({ ) : ( {fertilizer.p_type_manure ? ( - + Mest ) : null} {fertilizer.p_type_compost ? ( - + Compost ) : null} {fertilizer.p_type_mineral ? ( - + Kunstmest ) : null} From 9d0db161a88dc62db73aa104f9022c59af4e876e Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:50:04 +0100 Subject: [PATCH 27/55] Add page to add fertilizer --- .../farm.$b_id_farm.fertilizers.new.tsx | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx new file mode 100644 index 000000000..0633f8523 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx @@ -0,0 +1,287 @@ +import { FarmHeader } from "@/components/custom/farm/farm-header" +import { FarmTitle } from "@/components/custom/farm/farm-title" +import { FertilizerForm } from "@/components/custom/fertilizer/form" +import { FormSchema } from "@/components/custom/fertilizer/formschema" +import { SidebarInset } from "@/components/ui/sidebar" +import { getSession } from "@/lib/auth.server" +import { handleActionError, handleLoaderError } from "@/lib/error" +import { fdm } from "@/lib/fdm.server" +import { extractFormValuesFromRequest } from "@/lib/form" +import { zodResolver } from "@hookform/resolvers/zod" +import { + addFertilizerToCatalogue, + getFarm, + getFarms, + getFertilizer, + getFertilizers, +} from "@svenvw/fdm-core" +import { updateFertilizerFromCatalogue } from "@svenvw/fdm-core" +import { UndoIcon } from "lucide-react" +import { useEffect } from "react" +import { + type ActionFunctionArgs, + type LoaderFunctionArgs, + data, + useLoaderData, +} from "react-router" +import { useRemixForm } from "remix-hook-form" +import { dataWithSuccess, redirectWithSuccess } from "remix-toast" +import type { z } from "zod" + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + // Get the farm id + const b_id_farm = params.b_id_farm + if (!b_id_farm) { + throw data("invalid: b_id_farm", { + status: 400, + statusText: "invalid: b_id_farm", + }) + } + + // Get the session + const session = await getSession(request) + + // Get details of farm + const farm = await getFarm(fdm, session.principal_id, b_id_farm) + if (!farm) { + throw data("not found: b_id_farm", { + status: 404, + statusText: "not found: b_id_farm", + }) + } + + // Get a list of possible farms of the user + const farms = await getFarms(fdm, session.principal_id) + if (!farms || farms.length === 0) { + throw data("not found: farms", { + status: 404, + statusText: "not found: farms", + }) + } + + const farmOptions = farms.map((farm) => { + return { + b_id_farm: farm.b_id_farm, + b_name_farm: farm.b_name_farm, + } + }) + + // Get selected fertilizer + const fertilizer = { + p_source: b_id_farm, + p_name_nl: undefined, + p_type: undefined, + p_n_rt: undefined, + p_n_wc: undefined, + p_p_rt: undefined, + p_k_rt: undefined, + p_om: undefined, + p_eoc: undefined, + p_s_rt: undefined, + p_ca_rt: undefined, + p_mg_rt: undefined, + } + + // Get the available fertilizers + const fertilizers = await getFertilizers( + fdm, + session.principal_id, + b_id_farm, + ) + const fertilizerOptions = fertilizers.map((fertilizer) => { + return { + p_id: fertilizer.p_id, + p_name_nl: fertilizer.p_name_nl, + } + }) + + // Return user information from loader + return { + farm: farm, + b_id_farm: b_id_farm, + farmOptions: farmOptions, + fertilizerOptions: fertilizerOptions, + fertilizer: fertilizer, + } + } catch (error) { + throw handleLoaderError(error) + } +} + +/** + * Renders the layout for managing farm settings. + * + * This component displays a sidebar that includes the farm header, navigation options, and a link to farm fields. + * It also renders a main section containing the farm title, description, nested routes via an Outlet, and a notification toaster. + */ +export default function FarmFertilizerBlock() { + const loaderData = useLoaderData() + const fertilizer = loaderData.fertilizer + + const form = useRemixForm>({ + mode: "onTouched", + resolver: zodResolver(FormSchema), + defaultValues: { + p_name_nl: fertilizer.p_name_nl, + p_type: fertilizer.p_type, + p_n_rt: fertilizer.p_n_rt, + p_n_wc: fertilizer.p_n_wc, + p_p_rt: fertilizer.p_p_rt, + p_k_rt: fertilizer.p_k_rt, + p_om: fertilizer.p_om, + p_eoc: fertilizer.p_eoc, + p_s_rt: fertilizer.p_s_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_mg_rt: fertilizer.p_mg_rt, + }, + }) + + useEffect(() => { + form.reset({ + p_name_nl: fertilizer.p_name_nl, + p_type: fertilizer.p_type, + p_n_rt: fertilizer.p_n_rt, + p_n_wc: fertilizer.p_n_wc, + p_p_rt: fertilizer.p_p_rt, + p_k_rt: fertilizer.p_k_rt, + p_om: fertilizer.p_om, + p_eoc: fertilizer.p_eoc, + p_s_rt: fertilizer.p_s_rt, + p_ca_rt: fertilizer.p_ca_rt, + p_mg_rt: fertilizer.p_mg_rt, + }) + }, [fertilizer, form.reset]) + + return ( + + +
+ +
+ +
+
+
+ ) +} + +export async function action({ request, params }: ActionFunctionArgs) { + try { + const b_id_farm = params.b_id_farm + const p_id = params.p_id + + if (!b_id_farm) { + throw new Error("missing: b_id_farm") + } + if (!p_id) { + throw new Error("missing: p_id") + } + + const session = await getSession(request) + const formValues = await extractFormValuesFromRequest( + request, + FormSchema, + ) + + const { + p_name_nl, + p_type, + p_n_rt, + p_n_wc, + p_p_rt, + p_k_rt, + p_om, + p_eoc, + p_s_rt, + p_ca_rt, + p_mg_rt, + } = formValues + + let p_type_manure = false + let p_type_compost = false + let p_type_mineral = false + if (p_type === "manure") { + p_type_manure = true + } + if (p_type === "compost") { + p_type_compost = true + } + if (p_type === "mineral") { + p_type_mineral = true + } + + await addFertilizerToCatalogue(fdm, session.principal_id, b_id_farm, { + p_name_nl, + p_type_manure, + p_type_mineral, + p_type_compost, + p_n_rt, + p_n_wc, + p_p_rt, + p_k_rt, + p_om, + p_eoc, + p_s_rt, + p_ca_rt, + p_mg_rt, + p_name_en: undefined, + p_description: undefined, + p_dm: undefined, + p_density: undefined, + p_a: undefined, + p_hc: undefined, + p_eom: undefined, + p_c_rt: undefined, + p_c_of: undefined, + p_c_if: undefined, + p_c_fr: undefined, + p_cn_of: undefined, + p_n_if: undefined, + p_n_of: undefined, + p_ne: undefined, + p_s_wc: undefined, + p_cu_rt: undefined, + p_zn_rt: undefined, + p_na_rt: undefined, + p_si_rt: undefined, + p_b_rt: undefined, + p_mn_rt: undefined, + p_ni_rt: undefined, + p_fe_rt: undefined, + p_mo_rt: undefined, + p_co_rt: undefined, + p_as_rt: undefined, + p_cd_rt: undefined, + pr_cr_rt: undefined, + p_cr_vi: undefined, + p_pb_rt: undefined, + p_hg_rt: undefined, + p_cl_rt: undefined + }) + + return redirectWithSuccess("../fertilizers", { + message: "Meststof is toegevoegd! 🎉", + }) + } catch (error) { + throw handleActionError(error) + } +} From b5afe8b6f4ace71402a6643474074cfb77491c14 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:53:41 +0100 Subject: [PATCH 28/55] Add changesets --- .changeset/cute-tires-type.md | 5 +++++ .changeset/forty-parts-bet.md | 5 +++++ .changeset/vast-lies-go.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/cute-tires-type.md create mode 100644 .changeset/forty-parts-bet.md create mode 100644 .changeset/vast-lies-go.md diff --git a/.changeset/cute-tires-type.md b/.changeset/cute-tires-type.md new file mode 100644 index 000000000..ea2917d3f --- /dev/null +++ b/.changeset/cute-tires-type.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add page to show the list of fertilizers available on the farm diff --git a/.changeset/forty-parts-bet.md b/.changeset/forty-parts-bet.md new file mode 100644 index 000000000..7fcaeaca5 --- /dev/null +++ b/.changeset/forty-parts-bet.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add page to show details of a fertilizer, and if applicable, to update the values diff --git a/.changeset/vast-lies-go.md b/.changeset/vast-lies-go.md new file mode 100644 index 000000000..27716bec0 --- /dev/null +++ b/.changeset/vast-lies-go.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add page to add new fertilizer for a farm From b5f3838b8d30da626ac795aef9f2b15c6413ad8f Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:00:28 +0100 Subject: [PATCH 29/55] Improve unit tests --- fdm-core/src/catalogues.test.ts | 766 +++++++++++--------------------- 1 file changed, 256 insertions(+), 510 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 673ca896c..549a84b0f 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -1,4 +1,12 @@ -import { describe, it, expect, beforeEach, inject } from "vitest" +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + inject, + it, +} from "vitest" import { createFdmServer } from "./fdm-server" import * as schema from "./db/schema" import { @@ -17,10 +25,12 @@ import { eq, isNotNull } from "drizzle-orm" import { getCultivationCatalogue, getFertilizersCatalogue, + hashCultivation, + hashFertilizer, } from "@svenvw/fdm-data" import { addFarm } from "./farm" -describe("Catalogues", () => { +describe("Catalogues - Unit Tests", () => { let fdm: FdmType let principal_id: string let b_id_farm: string @@ -49,129 +59,93 @@ describe("Catalogues", () => { ) }) - describe("Fertilizer Catalogues", () => { - it("should enable and check fertilizer catalogue", async () => { - const p_source = "test_source" + describe("getEnabledFertilizerCatalogues", () => { + it("should return an array of enabled fertilizer catalogue sources", async () => { + // Arrange + await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, "source1") + await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, "source2") - // Initially should not be enabled - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(false) - - // Enable the catalogue - await enableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - - // Should now be enabled - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(true) + // Act + const result = await getEnabledFertilizerCatalogues(fdm, principal_id, b_id_farm) - // Should appear in enabled catalogues list - const enabledCatalogues = await getEnabledFertilizerCatalogues( - fdm, - principal_id, - b_id_farm, - ) - expect(enabledCatalogues).toContain(p_source) + // Assert + expect(result).toEqual(expect.arrayContaining(["source1", "source2"])) + expect(result.length).toBe(2); }) - it("should disable fertilizer catalogue", async () => { - const p_source = "test_source" + it("should return an empty array if no catalogues are enabled", async () => { + // Arrange - // Enable the catalogue - await enableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(true) - - // Disable the catalogue - await disableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) + // Act + const result = await getEnabledFertilizerCatalogues(fdm, principal_id, b_id_farm) - // Should no longer be enabled - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(false) + // Assert + expect(result).toEqual([]) + }) + + it("should throw an error if permission check fails", async () => { + // Arrange + const invalidPrincipalId = "invalid_principal" - // Should not appear in enabled catalogues list - const enabledCatalogues = await getEnabledFertilizerCatalogues( - fdm, - principal_id, - b_id_farm, + // Act & Assert + await expect( + getEnabledFertilizerCatalogues(fdm, invalidPrincipalId, b_id_farm) + ).rejects.toThrowError( + "Principal does not have permission to perform this action", ) - expect(enabledCatalogues).not.toContain(p_source) }) + }) - it("should handle multiple fertilizer catalogues", async () => { - const sources = ["source1", "source2", "source3"] + describe("getEnabledCultivationCatalogues", () => { + it("should return an array of enabled cultivation catalogue sources", async () => { + // Arrange + await enableCultivationCatalogue(fdm, principal_id, b_id_farm, "source1") + await enableCultivationCatalogue(fdm, principal_id, b_id_farm, "source2") - // Enable multiple catalogues - for (const source of sources) { - await enableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - source, - ) - } + // Act + const result = await getEnabledCultivationCatalogues(fdm, principal_id, b_id_farm) - // Check all are enabled - const enabledCatalogues = await getEnabledFertilizerCatalogues( - fdm, - principal_id, - b_id_farm, - ) - expect(enabledCatalogues).toHaveLength(sources.length) - expect(enabledCatalogues).toEqual(expect.arrayContaining(sources)) + // Assert + expect(result).toEqual(expect.arrayContaining(["source1", "source2"])) + expect(result.length).toBe(2); + }) + it("should return an empty array if no catalogues are enabled", async () => { + // Arrange + + // Act + const result = await getEnabledCultivationCatalogues(fdm, principal_id, b_id_farm) + + // Assert + expect(result).toEqual([]) }) - it("should throw an error when permission check fails for getEnabledFertilizerCatalogues", async () => { + + it("should throw an error if permission check fails", async () => { + // Arrange const invalidPrincipalId = "invalid_principal" + + // Act & Assert await expect( - getEnabledFertilizerCatalogues( - fdm, - invalidPrincipalId, - b_id_farm, - ), + getEnabledCultivationCatalogues(fdm, invalidPrincipalId, b_id_farm) ).rejects.toThrowError( "Principal does not have permission to perform this action", ) }) - it("should throw an error when permission check fails for enableFertilizerCatalogue", async () => { + }) + + describe("enableFertilizerCatalogue", () => { + it("should enable a fertilizer catalogue", async () => { + // Arrange + const p_source = "test_source"; + + // Act + await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source); + + // Assert + const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); + expect(isEnabled).toBe(true); + }); + it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( enableFertilizerCatalogue( @@ -184,11 +158,24 @@ describe("Catalogues", () => { "Principal does not have permission to perform this action", ) }) + }) + + describe("enableCultivationCatalogue", () => { + it("should enable a cultivation catalogue", async () => { + // Arrange + const b_lu_source = "test_source"; + + // Act + await enableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source); - it("should throw an error when permission check fails for disableFertilizerCatalogue", async () => { + // Assert + const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); + expect(isEnabled).toBe(true); + }); + it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( - disableFertilizerCatalogue( + enableCultivationCatalogue( fdm, invalidPrincipalId, b_id_farm, @@ -198,11 +185,25 @@ describe("Catalogues", () => { "Principal does not have permission to perform this action", ) }) + }) + + describe("disableFertilizerCatalogue", () => { + it("should disable a fertilizer catalogue", async () => { + // Arrange + const p_source = "test_source"; + await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source) + + // Act + await disableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source); - it("should throw an error when permission check fails for isFertilizerCatalogueEnabled", async () => { + // Assert + const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); + expect(isEnabled).toBe(false); + }); + it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( - isFertilizerCatalogueEnabled( + disableFertilizerCatalogue( fdm, invalidPrincipalId, b_id_farm, @@ -212,266 +213,25 @@ describe("Catalogues", () => { "Principal does not have permission to perform this action", ) }) - - it("should handle edge cases for disableFertilizerCatalogue", async () => { - const p_source = "test_disable_source" - - // Case 1: Disabling a fertilizer catalogue that isn't enabled should not throw errors - // This tests the compound condition in the where clause - await disableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(false) - - // Case 2: Enable and then disable with same farm but different source - await enableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(true) - - // Disable with wrong source - should not disable the original - await disableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - "wrong_source", - ) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(true) - - // Case 3: Test with different farm - // Create a second test farm - const secondFarmName = "Second Test Farm" - const secondFarmBusinessId = "654321" - const secondFarmAddress = "456 Farm Lane" - const secondFarmPostalCode = "54321" - const second_b_id_farm = await addFarm( - fdm, - principal_id, - secondFarmName, - secondFarmBusinessId, - secondFarmAddress, - secondFarmPostalCode, - ) - - // Enable for second farm - await enableFertilizerCatalogue( - fdm, - principal_id, - second_b_id_farm, - p_source, - ) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - second_b_id_farm, - p_source, - ), - ).toBe(true) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(true) - - // Disable for first farm should not affect second farm - await disableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ), - ).toBe(false) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - second_b_id_farm, - p_source, - ), - ).toBe(true) - - // Disable for second farm - await disableFertilizerCatalogue( - fdm, - principal_id, - second_b_id_farm, - p_source, - ) - expect( - await isFertilizerCatalogueEnabled( - fdm, - principal_id, - second_b_id_farm, - p_source, - ), - ).toBe(false) - }) }) - describe("Cultivation Catalogues", () => { - it("should enable and check cultivation catalogue", async () => { - const b_lu_source = "test_source" - - // Initially should not be enabled - expect( - await isCultivationCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ), - ).toBe(false) - - // Enable the catalogue - await enableCultivationCatalogue( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ) - - // Should now be enabled - expect( - await isCultivationCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ), - ).toBe(true) + describe("disableCultivationCatalogue", () => { + it("should disable a cultivation catalogue", async () => { + // Arrange + const b_lu_source = "test_source"; + await enableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source) - // Should appear in enabled catalogues list - const enabledCatalogues = await getEnabledCultivationCatalogues( - fdm, - principal_id, - b_id_farm, - ) - expect(enabledCatalogues).toContain(b_lu_source) - }) + // Act + await disableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source); - it("should disable cultivation catalogue", async () => { - const b_lu_source = "test_source" - - // Enable the catalogue - await enableCultivationCatalogue( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ) - expect( - await isCultivationCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ), - ).toBe(true) - - // Disable the catalogue - await disableCultivationCatalogue( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ) - - // Should no longer be enabled - expect( - await isCultivationCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ), - ).toBe(false) - - // Should not appear in enabled catalogues list - const enabledCatalogues = await getEnabledCultivationCatalogues( - fdm, - principal_id, - b_id_farm, - ) - expect(enabledCatalogues).not.toContain(b_lu_source) - }) - - it("should handle multiple cultivation catalogues", async () => { - const sources = ["source1", "source2", "source3"] - - // Enable multiple catalogues - for (const source of sources) { - await enableCultivationCatalogue( - fdm, - principal_id, - b_id_farm, - source, - ) - } - - // Check all are enabled - const enabledCatalogues = await getEnabledCultivationCatalogues( - fdm, - principal_id, - b_id_farm, - ) - expect(enabledCatalogues).toHaveLength(sources.length) - expect(enabledCatalogues).toEqual(expect.arrayContaining(sources)) - }) + // Assert + const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); + expect(isEnabled).toBe(false); + }); it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( - getEnabledCultivationCatalogues( - fdm, - invalidPrincipalId, - b_id_farm, - ), - ).rejects.toThrowError( - "Principal does not have permission to perform this action", - ) - }) - - it("should throw an error when permission check fails for enableCultivationCatalogue", async () => { - const invalidPrincipalId = "invalid_principal" - await expect( - enableCultivationCatalogue( + disableCultivationCatalogue( fdm, invalidPrincipalId, b_id_farm, @@ -481,11 +241,36 @@ describe("Catalogues", () => { "Principal does not have permission to perform this action", ) }) - - it("should throw an error when permission check fails for disableCultivationCatalogue", async () => { + }) + + describe("isFertilizerCatalogueEnabled", () => { + it("should return true if fertilizer catalogue is enabled", async () => { + // Arrange + const p_source = "test_source"; + await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source); + + // Act + const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); + + // Assert + expect(isEnabled).toBe(true); + }); + + it("should return false if fertilizer catalogue is disabled", async () => { + // Arrange + const p_source = "test_source"; + await disableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source) + + // Act + const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); + + // Assert + expect(isEnabled).toBe(false); + }); + it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( - disableCultivationCatalogue( + isFertilizerCatalogueEnabled( fdm, invalidPrincipalId, b_id_farm, @@ -495,8 +280,34 @@ describe("Catalogues", () => { "Principal does not have permission to perform this action", ) }) + }) + + describe("isCultivationCatalogueEnabled", () => { + it("should return true if cultivation catalogue is enabled", async () => { + // Arrange + const b_lu_source = "test_source"; + await enableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source); - it("should throw an error when permission check fails for isCultivationCatalogueEnabled", async () => { + // Act + const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); + + // Assert + expect(isEnabled).toBe(true); + }); + + it("should return false if cultivation catalogue is disabled", async () => { + // Arrange + const b_lu_source = "test_source"; + await disableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source) + + // Act + const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); + + // Assert + expect(isEnabled).toBe(false); + }); + + it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( isCultivationCatalogueEnabled( @@ -509,158 +320,89 @@ describe("Catalogues", () => { "Principal does not have permission to perform this action", ) }) - - it("should include context in the error when database query fails", async () => { - const invalidFdm = { - ...fdm, - select: () => { - throw new Error("Database error") - }, - } - - try { - await getEnabledCultivationCatalogues( - invalidFdm, - principal_id, - b_id_farm, - ) - // Should not reach here - expect(true).toBe(false) - } catch (error) { - expect(error.message).toContain( - "Exception for getEnabledCultivationCatalogues", - ) - expect(error.context).toBeDefined() - expect(error.context.principal_id).toBe(principal_id) - expect(error.context.b_id_farm).toBe(b_id_farm) - } - }) - - it("should handle errors when disabling cultivation catalogue", async () => { - const b_lu_source = "test_source" - const invalidPrincipal = "invalid_principal" // Principal without permissions - - // Enable the catalogue first with valid principal - await enableCultivationCatalogue( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ) - - // Attempt to disable with invalid principal should throw an error - await expect( - disableCultivationCatalogue( - fdm, - invalidPrincipal, - b_id_farm, - b_lu_source, - ), - ).rejects.toThrow() - - // The catalogue should still be enabled - expect( - await isCultivationCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ), - ).toBe(true) - }) }) -}) -describe("Catalogues syncing", () => { - let fdm: FdmType + describe("syncCatalogues", () => { + it("should sync catalogues", async () => { + await syncCatalogues(fdm) - beforeEach(async () => { - const host = inject("host") - const port = inject("port") - const user = inject("user") - const password = inject("password") - const database = inject("database") - fdm = createFdmServer(host, port, user, password, database) - }) - - it("should sync catalogues", async () => { - await syncCatalogues(fdm) - - // Check if catalogue data is similiar to fdm-data - const srmCatalogue = await fdm - .select() - .from(schema.fertilizersCatalogue) - - const srmCatalogueOriginal = getFertilizersCatalogue("srm") - expect(srmCatalogue.length).toBeGreaterThan(srmCatalogueOriginal.length) - - const brpCatalogue = await fdm - .select() - .from(schema.cultivationsCatalogue) - expect(brpCatalogue.length).toBeGreaterThan(0) + // Check if catalogue data is similiar to fdm-data + const srmCatalogue = await fdm + .select() + .from(schema.fertilizersCatalogue) - const brpCatalogueOriginal = getCultivationCatalogue("brp") - expect(brpCatalogue.length).toBeGreaterThan(brpCatalogueOriginal.length) - }) - - it("should update fertilizer catalogue", async () => { - await syncCatalogues(fdm) - - // Update a catalogue item - const item = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where(isNotNull(schema.fertilizersCatalogue.hash)) - .limit(1) - expect(item[0].p_id_catalogue).toBeDefined() - - await fdm - .update(schema.fertilizersCatalogue) - .set({ hash: "Updated hash" }) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) + const srmCatalogueOriginal = getFertilizersCatalogue("srm") + expect(srmCatalogue.length).toBeGreaterThan(srmCatalogueOriginal.length) - const itemUpdated = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) - expect(itemUpdated[0].p_id_catalogue).toBeDefined() - expect(itemUpdated[0].hash).toBe("Updated hash") + const brpCatalogue = await fdm + .select() + .from(schema.cultivationsCatalogue) + expect(brpCatalogue.length).toBeGreaterThan(0) - await syncCatalogues(fdm) + const brpCatalogueOriginal = getCultivationCatalogue("brp") + expect(brpCatalogue.length).toBeGreaterThan(brpCatalogueOriginal.length) + }) - const itemSynced = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) + it("should update fertilizer catalogue", async () => { + await syncCatalogues(fdm) + + // Update a catalogue item + const item = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where(isNotNull(schema.fertilizersCatalogue.hash)) + .limit(1) + expect(item[0].p_id_catalogue).toBeDefined() + const originalHash = item[0].hash; + + await fdm + .update(schema.fertilizersCatalogue) + .set({ hash: "Updated hash" }) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) - expect(itemSynced[0].p_id_catalogue).toBeDefined() - }) + const itemUpdated = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + expect(itemUpdated[0].p_id_catalogue).toBeDefined() + expect(itemUpdated[0].hash).toBe("Updated hash") + + await syncCatalogues(fdm) + + const itemSynced = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) - it("should update cultivation catalogue", async () => { + expect(itemSynced[0].p_id_catalogue).toBeDefined() + expect(itemSynced[0].hash).not.toBe("Updated hash"); + expect(itemSynced[0].hash).toBe(originalHash) + }) + it("should update cultivation catalogue", async () => { await syncCatalogues(fdm) // Update a catalogue item @@ -674,6 +416,8 @@ describe("Catalogues syncing", () => { .limit(1) expect(item[0].b_lu_catalogue).toBeDefined() + const originalHash = item[0].hash; + await fdm .update(schema.cultivationsCatalogue) .set({ hash: "Updated hash" }) @@ -714,6 +458,8 @@ describe("Catalogues syncing", () => { ), ) expect(itemSynced[0].b_lu_catalogue).toBeDefined() - expect(itemSynced[0].hash).toBe(item[0].hash) + expect(itemSynced[0].hash).not.toBe("Updated hash"); + expect(itemSynced[0].hash).toBe(originalHash); + }) }) }) From 8b81fbf166e072b87795522a1e7e9e9af99339f2 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:19:26 +0100 Subject: [PATCH 30/55] Add unit test for hash functions --- fdm-data/src/cultivations/hash.test.ts | 166 ++++++++++ fdm-data/src/fertilizers/hash.test.ts | 422 +++++++++++++++++++++++++ 2 files changed, 588 insertions(+) create mode 100644 fdm-data/src/cultivations/hash.test.ts create mode 100644 fdm-data/src/fertilizers/hash.test.ts diff --git a/fdm-data/src/cultivations/hash.test.ts b/fdm-data/src/cultivations/hash.test.ts new file mode 100644 index 000000000..e532c252c --- /dev/null +++ b/fdm-data/src/cultivations/hash.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect } from "vitest" +import { hashCultivation } from "./hash" +import type { CatalogueCultivationItem } from "./d" + +describe("hashCultivation", () => { + it("should generate a hash for a cultivation item", () => { + const cultivation: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id", + b_lu_name: "Test Cultivation", + b_lu_name_en: "Test Cultivation (EN)", + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const hash = hashCultivation(cultivation) + expect(hash).toBeDefined() + expect(typeof hash).toBe("string") + expect(hash.length).toBeGreaterThan(0) + expect(hash).toBe("53ece66f") + }) + + it("should generate different hashes for different cultivation items", () => { + const cultivation1: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id-1", + b_lu_name: "Test Cultivation 1", + b_lu_name_en: "Test Cultivation (EN)", + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const cultivation2: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id-2", + b_lu_name: "Test Cultivation 2", // Different name + b_lu_name_en: "Test Cultivation (EN)", + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const hash1 = hashCultivation(cultivation1) + const hash2 = hashCultivation(cultivation2) + + expect(hash1).not.toBe(hash2) + }) + + it("should generate the same hash for identical cultivation items", () => { + const cultivation1: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id-1", + b_lu_name: "Test Cultivation 1", + b_lu_name_en: "Test Cultivation (EN)", + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const cultivation2: CatalogueCultivationItem = { + ...cultivation1, + } + + const hash1 = hashCultivation(cultivation1) + const hash2 = hashCultivation(cultivation2) + + expect(hash1).toBe(hash2) + }) + + it("should generate different hashes when a string value changes", () => { + const cultivation1: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id-1", + b_lu_name: "Test Cultivation 1", + b_lu_name_en: "Test Cultivation (EN)", + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const cultivation2: CatalogueCultivationItem = { + ...cultivation1, + b_lu_name: "Updated Test Cultivation Name", + } + + const hash1 = hashCultivation(cultivation1) + const hash2 = hashCultivation(cultivation2) + + expect(hash1).not.toBe(hash2) + }) + it("should generate different hashes when a null string value is changed", () => { + const cultivation1: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id-1", + b_lu_name: "Test Cultivation 1", + b_lu_name_en: null, + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const cultivation2: CatalogueCultivationItem = { + ...cultivation1, + b_lu_name_en: "Test Cultivation (EN)", + } + + const hash1 = hashCultivation(cultivation1) + const hash2 = hashCultivation(cultivation2) + + expect(hash1).not.toBe(hash2) + }) + + it("should generate different hashes when a non null string value is changed", () => { + const cultivation1: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id-1", + b_lu_name: "Test Cultivation 1", + b_lu_name_en: "Test Cultivation (EN)", + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const cultivation2: CatalogueCultivationItem = { + ...cultivation1, + b_lu_hcat3: null, + } + + const hash1 = hashCultivation(cultivation1) + const hash2 = hashCultivation(cultivation2) + + expect(hash1).not.toBe(hash2) + }) + + it("should generate different hashes when a enum value changes", () => { + const cultivation1: CatalogueCultivationItem = { + b_lu_source: "brp", + b_lu_catalogue: "test-id-1", + b_lu_name: "Test Cultivation 1", + b_lu_name_en: "Test Cultivation (EN)", + b_lu_harvestable: "once", + b_lu_hcat3: "hcat3", + b_lu_hcat3_name: "hcat3 name", + hash: null, + } + + const cultivation2: CatalogueCultivationItem = { + ...cultivation1, + b_lu_harvestable: "multiple", + } + + const hash1 = hashCultivation(cultivation1) + const hash2 = hashCultivation(cultivation2) + + expect(hash1).not.toBe(hash2) + }) +}) diff --git a/fdm-data/src/fertilizers/hash.test.ts b/fdm-data/src/fertilizers/hash.test.ts new file mode 100644 index 000000000..ad988f463 --- /dev/null +++ b/fdm-data/src/fertilizers/hash.test.ts @@ -0,0 +1,422 @@ +import { describe, it, expect } from "vitest" +import { hashFertilizer } from "./hash" +import type { CatalogueFertilizerItem } from "./d" + +describe("hashFertilizer", () => { + it("should generate a hash for a fertilizer item", () => { + const fertilizer: CatalogueFertilizerItem = { + p_id_catalogue: "test-id", + p_source: "test-source", + p_name_nl: "Test Fertilizer", + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_cr: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const hash = hashFertilizer(fertilizer) + expect(hash).toBeDefined() + expect(typeof hash).toBe("string") + expect(hash.length).toBeGreaterThan(0) + expect(hash).toBe("63de7776") + }) + + it("should generate different hashes for different fertilizer items", () => { + const fertilizer1: CatalogueFertilizerItem = { + p_id_catalogue: "test-id-1", + p_source: "test-source", + p_name_nl: "Test Fertilizer 1", + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_cr: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + p_id_catalogue: "test-id-2", + p_source: "test-source", + p_name_nl: "Test Fertilizer 2", // Different name + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_cr: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const hash1 = hashFertilizer(fertilizer1) + const hash2 = hashFertilizer(fertilizer2) + + expect(hash1).not.toBe(hash2) + }) + + it("should generate the same hash for identical fertilizer items", () => { + const fertilizer1: CatalogueFertilizerItem = { + p_id_catalogue: "test-id-1", + p_source: "test-source", + p_name_nl: "Test Fertilizer 1", + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_cr: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + ...fertilizer1, + } + + const hash1 = hashFertilizer(fertilizer1) + const hash2 = hashFertilizer(fertilizer2) + + expect(hash1).toBe(hash2) + }) + + it("should generate different hashes when a boolean value changes", () => { + const fertilizer1: CatalogueFertilizerItem = { + p_id_catalogue: "test-id-1", + p_source: "test-source", + p_name_nl: "Test Fertilizer 1", + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_cr: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + ...fertilizer1, + p_type_manure: false, + } + + const hash1 = hashFertilizer(fertilizer1) + const hash2 = hashFertilizer(fertilizer2) + + expect(hash1).not.toBe(hash2) + }) + it("should generate different hashes when a numerical value changes", () => { + const fertilizer1: CatalogueFertilizerItem = { + p_id_catalogue: "test-id-1", + p_source: "test-source", + p_name_nl: "Test Fertilizer 1", + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_cr: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + ...fertilizer1, + p_dm: 50, + } + + const hash1 = hashFertilizer(fertilizer1) + const hash2 = hashFertilizer(fertilizer2) + + expect(hash1).not.toBe(hash2) + }) + it("should generate different hashes when a string value changes", () => { + const fertilizer1: CatalogueFertilizerItem = { + p_id_catalogue: "test-id-1", + p_source: "test-source", + p_name_nl: "Test Fertilizer 1", + p_name_en: "Test Fertilizer (EN)", + p_description: "This is a test fertilizer", + p_dm: 37, + p_density: 20, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_cr: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + ...fertilizer1, + p_name_nl: "Updated Test Fertilizer Name", + } + + const hash1 = hashFertilizer(fertilizer1) + const hash2 = hashFertilizer(fertilizer2) + + expect(hash1).not.toBe(hash2) + }) +}) From fc56101d789ee8c953fbca4bcc1bfba824f09143 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:22:25 +0100 Subject: [PATCH 31/55] Improve type definition --- .../components/custom/fertilizer/columns.tsx | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/fdm-app/app/components/custom/fertilizer/columns.tsx b/fdm-app/app/components/custom/fertilizer/columns.tsx index 2d9dcb201..bf712578a 100644 --- a/fdm-app/app/components/custom/fertilizer/columns.tsx +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -13,9 +13,13 @@ import { Badge } from "@/components/ui/badge" export type Fertilizer = { p_id: string p_name_nl: string - p_n_rt: number | null - p_p_rt: number | null - p_k_rt: number | null + p_n_rt?: number | null + p_p_rt?: number | null + p_k_rt?: number | null + p_type_manure?: boolean + p_type_compost?: boolean + p_type_mineral?: boolean + p_eoc?: number | null } export const columns: ColumnDef[] = [ @@ -59,13 +63,28 @@ export const columns: ColumnDef[] = [ return ( {fertilizer.p_type_manure ? ( - Mest + + Mest + ) : null} {fertilizer.p_type_compost ? ( - Compost + + Compost + ) : null} {fertilizer.p_type_mineral ? ( - Kunstmest + + Kunstmest + ) : null} ) From 19f1b5c525d35d932648e2b973ba4fa290d5ddeb Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:25:30 +0100 Subject: [PATCH 32/55] Add try catch around sync functions --- fdm-core/src/catalogues.ts | 144 +++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index a31c7354c..51814a57e 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -377,42 +377,46 @@ export async function syncCatalogues(fdm: FdmType): Promise { async function syncFertilizerCatalogue(fdm: FdmType) { const srmCatalogue = getFertilizersCatalogue("srm") await fdm.transaction(async (tx) => { - for (const item of srmCatalogue) { - const hash = hashFertilizer(item) - const existing = await tx - .select({ hash: schema.fertilizersCatalogue.hash }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item.p_id_catalogue, - ), - ) - .limit(1) - if (existing.length === 0) { - //add the item if does not exist - await tx.insert(schema.fertilizersCatalogue).values({ - ...item, - hash: hash, - }) - } else { - // update the hash if it is undefined, null or different - if ( - existing[0].hash === null || - existing[0].hash === undefined || - existing[0].hash !== hash - ) { - await tx - .update(schema.fertilizersCatalogue) - .set({ hash: hash }) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item.p_id_catalogue, - ), - ) + try { + for (const item of srmCatalogue) { + const hash = hashFertilizer(item) + const existing = await tx + .select({ hash: schema.fertilizersCatalogue.hash }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item.p_id_catalogue, + ), + ) + .limit(1) + if (existing.length === 0) { + //add the item if does not exist + await tx.insert(schema.fertilizersCatalogue).values({ + ...item, + hash: hash, + }) + } else { + // update the hash if it is undefined, null or different + if ( + existing[0].hash === null || + existing[0].hash === undefined || + existing[0].hash !== hash + ) { + await tx + .update(schema.fertilizersCatalogue) + .set({ hash: hash }) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item.p_id_catalogue, + ), + ) + } } } + } catch (error) { + handleError(error, "Exception for syncFertilizerCatalogue") } }) } @@ -421,42 +425,46 @@ async function syncCultivationCatalogue(fdm: FdmType) { const brpCatalogue = getCultivationCatalogue("brp") await fdm.transaction(async (tx) => { - for (const item of brpCatalogue) { - const hash = hashCultivation(item) - const existing = await tx - .select({ hash: schema.cultivationsCatalogue.hash }) - .from(schema.cultivationsCatalogue) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item.b_lu_catalogue, - ), - ) - .limit(1) - if (existing.length === 0) { - //add the item if does not exist - await tx.insert(schema.cultivationsCatalogue).values({ - ...item, - hash: hash, - }) - } else { - // update the hash if it is undefined, null or different - if ( - existing[0].hash === null || - existing[0].hash === undefined || - existing[0].hash !== hash - ) { - await tx - .update(schema.cultivationsCatalogue) - .set({ hash: hash }) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item.b_lu_catalogue, - ), - ) + try { + for (const item of brpCatalogue) { + const hash = hashCultivation(item) + const existing = await tx + .select({ hash: schema.cultivationsCatalogue.hash }) + .from(schema.cultivationsCatalogue) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item.b_lu_catalogue, + ), + ) + .limit(1) + if (existing.length === 0) { + //add the item if does not exist + await tx.insert(schema.cultivationsCatalogue).values({ + ...item, + hash: hash, + }) + } else { + // update the hash if it is undefined, null or different + if ( + existing[0].hash === null || + existing[0].hash === undefined || + existing[0].hash !== hash + ) { + await tx + .update(schema.cultivationsCatalogue) + .set({ hash: hash }) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item.b_lu_catalogue, + ), + ) + } } } + } catch (error) { + handleError(error, "Exception for syncCultivationCatalogue") } }) } From 2b9050f29ac1a2f5a1d082cec0d9ff1ba0df335b Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:27:04 +0100 Subject: [PATCH 33/55] Remove getting p_id --- fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx index 0633f8523..e503335d4 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx @@ -187,14 +187,10 @@ export default function FarmFertilizerBlock() { export async function action({ request, params }: ActionFunctionArgs) { try { const b_id_farm = params.b_id_farm - const p_id = params.p_id if (!b_id_farm) { throw new Error("missing: b_id_farm") } - if (!p_id) { - throw new Error("missing: p_id") - } const session = await getSession(request) const formValues = await extractFormValuesFromRequest( @@ -275,7 +271,7 @@ export async function action({ request, params }: ActionFunctionArgs) { p_cr_vi: undefined, p_pb_rt: undefined, p_hg_rt: undefined, - p_cl_rt: undefined + p_cl_rt: undefined, }) return redirectWithSuccess("../fertilizers", { From 0bf833d028842f892a51e47aa743faf22692571f Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:39:33 +0100 Subject: [PATCH 34/55] Improve hash consistency --- fdm-data/src/cultivations/hash.test.ts | 2 +- fdm-data/src/cultivations/hash.ts | 19 ++++++++++++++++++- fdm-data/src/fertilizers/hash.test.ts | 2 +- fdm-data/src/fertilizers/hash.ts | 19 ++++++++++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/fdm-data/src/cultivations/hash.test.ts b/fdm-data/src/cultivations/hash.test.ts index e532c252c..5e99cd718 100644 --- a/fdm-data/src/cultivations/hash.test.ts +++ b/fdm-data/src/cultivations/hash.test.ts @@ -19,7 +19,7 @@ describe("hashCultivation", () => { expect(hash).toBeDefined() expect(typeof hash).toBe("string") expect(hash.length).toBeGreaterThan(0) - expect(hash).toBe("53ece66f") + expect(hash).toBe("2b20f4d7") }) it("should generate different hashes for different cultivation items", () => { diff --git a/fdm-data/src/cultivations/hash.ts b/fdm-data/src/cultivations/hash.ts index 072864fd5..63058e381 100644 --- a/fdm-data/src/cultivations/hash.ts +++ b/fdm-data/src/cultivations/hash.ts @@ -4,6 +4,23 @@ import xxhash from "xxhash-wasm" const { h32ToString } = await xxhash() export function hashCultivation(cultivation: CatalogueCultivationItem) { - const hash = h32ToString(JSON.stringify(cultivation)) + // Set hash to null for constistent hashing + cultivation.hash = null + + // Remove all keys without a value + const filteredCultivation = Object.fromEntries( + Object.entries(cultivation).filter( + ([, value]) => value !== undefined || value !== null, + ), + ) + + // Sort keys to ensure consistent hash generation for identical objects + const sortedKeys = Object.keys(filteredCultivation).sort() + const sortedCultivation = sortedKeys.reduce((obj: any, key) => { + obj[key] = cultivation[key as keyof typeof cultivation] + return obj + }, {}) + + const hash = h32ToString(JSON.stringify(sortedCultivation)) return hash } diff --git a/fdm-data/src/fertilizers/hash.test.ts b/fdm-data/src/fertilizers/hash.test.ts index ad988f463..78c44f882 100644 --- a/fdm-data/src/fertilizers/hash.test.ts +++ b/fdm-data/src/fertilizers/hash.test.ts @@ -59,7 +59,7 @@ describe("hashFertilizer", () => { expect(hash).toBeDefined() expect(typeof hash).toBe("string") expect(hash.length).toBeGreaterThan(0) - expect(hash).toBe("63de7776") + expect(hash).toBe("d0cde132") }) it("should generate different hashes for different fertilizer items", () => { diff --git a/fdm-data/src/fertilizers/hash.ts b/fdm-data/src/fertilizers/hash.ts index 218de4c07..f467548fa 100644 --- a/fdm-data/src/fertilizers/hash.ts +++ b/fdm-data/src/fertilizers/hash.ts @@ -4,6 +4,23 @@ import xxhash from "xxhash-wasm" const { h32ToString } = await xxhash() export function hashFertilizer(fertilizer: CatalogueFertilizerItem) { - const hash = h32ToString(JSON.stringify(fertilizer)) + // Set hash to null for constistent hashing + fertilizer.hash = null + + // Remove all keys without a value + const filteredFertilizer = Object.fromEntries( + Object.entries(fertilizer).filter( + ([, value]) => value !== undefined || value !== null, + ), + ) + + // Sort keys to ensure consistent hash generation for identical objects + const sortedKeys = Object.keys(filteredFertilizer).sort() + const sortedFertilizer = sortedKeys.reduce((obj: any, key) => { + obj[key] = fertilizer[key as keyof typeof fertilizer] + return obj + }, {}) + + const hash = h32ToString(JSON.stringify(sortedFertilizer)) return hash } From 7d9fa985c46b5d79eac9b1ac1d207a0262e9de1a Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:44:31 +0100 Subject: [PATCH 35/55] Handle async initialization more explicitly --- fdm-core/src/catalogues.ts | 4 +-- fdm-data/src/cultivations/hash.test.ts | 40 +++++++++++++------------- fdm-data/src/cultivations/hash.ts | 7 ++--- fdm-data/src/fertilizers/hash.test.ts | 34 +++++++++++----------- fdm-data/src/fertilizers/hash.ts | 8 +++--- fdm-data/src/hash.ts | 15 ++++++++++ 6 files changed, 61 insertions(+), 47 deletions(-) create mode 100644 fdm-data/src/hash.ts diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index 51814a57e..e9b95a696 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -379,7 +379,7 @@ async function syncFertilizerCatalogue(fdm: FdmType) { await fdm.transaction(async (tx) => { try { for (const item of srmCatalogue) { - const hash = hashFertilizer(item) + const hash = await hashFertilizer(item) const existing = await tx .select({ hash: schema.fertilizersCatalogue.hash }) .from(schema.fertilizersCatalogue) @@ -427,7 +427,7 @@ async function syncCultivationCatalogue(fdm: FdmType) { await fdm.transaction(async (tx) => { try { for (const item of brpCatalogue) { - const hash = hashCultivation(item) + const hash = await hashCultivation(item) const existing = await tx .select({ hash: schema.cultivationsCatalogue.hash }) .from(schema.cultivationsCatalogue) diff --git a/fdm-data/src/cultivations/hash.test.ts b/fdm-data/src/cultivations/hash.test.ts index 5e99cd718..3871427ac 100644 --- a/fdm-data/src/cultivations/hash.test.ts +++ b/fdm-data/src/cultivations/hash.test.ts @@ -3,7 +3,7 @@ import { hashCultivation } from "./hash" import type { CatalogueCultivationItem } from "./d" describe("hashCultivation", () => { - it("should generate a hash for a cultivation item", () => { + it("should generate a hash for a cultivation item", async () => { const cultivation: CatalogueCultivationItem = { b_lu_source: "brp", b_lu_catalogue: "test-id", @@ -15,14 +15,14 @@ describe("hashCultivation", () => { hash: null, } - const hash = hashCultivation(cultivation) + const hash = await hashCultivation(cultivation) expect(hash).toBeDefined() expect(typeof hash).toBe("string") expect(hash.length).toBeGreaterThan(0) expect(hash).toBe("2b20f4d7") }) - it("should generate different hashes for different cultivation items", () => { + it("should generate different hashes for different cultivation items", async () => { const cultivation1: CatalogueCultivationItem = { b_lu_source: "brp", b_lu_catalogue: "test-id-1", @@ -45,13 +45,13 @@ describe("hashCultivation", () => { hash: null, } - const hash1 = hashCultivation(cultivation1) - const hash2 = hashCultivation(cultivation2) + const hash1 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) expect(hash1).not.toBe(hash2) }) - it("should generate the same hash for identical cultivation items", () => { + it("should generate the same hash for identical cultivation items", async () => { const cultivation1: CatalogueCultivationItem = { b_lu_source: "brp", b_lu_catalogue: "test-id-1", @@ -67,13 +67,13 @@ describe("hashCultivation", () => { ...cultivation1, } - const hash1 = hashCultivation(cultivation1) - const hash2 = hashCultivation(cultivation2) + const hash1 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) expect(hash1).toBe(hash2) }) - it("should generate different hashes when a string value changes", () => { + it("should generate different hashes when a string value changes", async () => { const cultivation1: CatalogueCultivationItem = { b_lu_source: "brp", b_lu_catalogue: "test-id-1", @@ -90,12 +90,12 @@ describe("hashCultivation", () => { b_lu_name: "Updated Test Cultivation Name", } - const hash1 = hashCultivation(cultivation1) - const hash2 = hashCultivation(cultivation2) + const hash1 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) expect(hash1).not.toBe(hash2) }) - it("should generate different hashes when a null string value is changed", () => { + it("should generate different hashes when a null string value is changed", async () => { const cultivation1: CatalogueCultivationItem = { b_lu_source: "brp", b_lu_catalogue: "test-id-1", @@ -112,13 +112,13 @@ describe("hashCultivation", () => { b_lu_name_en: "Test Cultivation (EN)", } - const hash1 = hashCultivation(cultivation1) - const hash2 = hashCultivation(cultivation2) + const hash1 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) expect(hash1).not.toBe(hash2) }) - it("should generate different hashes when a non null string value is changed", () => { + it("should generate different hashes when a non null string value is changed", async () => { const cultivation1: CatalogueCultivationItem = { b_lu_source: "brp", b_lu_catalogue: "test-id-1", @@ -135,13 +135,13 @@ describe("hashCultivation", () => { b_lu_hcat3: null, } - const hash1 = hashCultivation(cultivation1) - const hash2 = hashCultivation(cultivation2) + const hash1 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) expect(hash1).not.toBe(hash2) }) - it("should generate different hashes when a enum value changes", () => { + it("should generate different hashes when a enum value changes", async () => { const cultivation1: CatalogueCultivationItem = { b_lu_source: "brp", b_lu_catalogue: "test-id-1", @@ -158,8 +158,8 @@ describe("hashCultivation", () => { b_lu_harvestable: "multiple", } - const hash1 = hashCultivation(cultivation1) - const hash2 = hashCultivation(cultivation2) + const hash1 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) expect(hash1).not.toBe(hash2) }) diff --git a/fdm-data/src/cultivations/hash.ts b/fdm-data/src/cultivations/hash.ts index 63058e381..ee8b04aed 100644 --- a/fdm-data/src/cultivations/hash.ts +++ b/fdm-data/src/cultivations/hash.ts @@ -1,9 +1,8 @@ +import { ensureInitialized, h32ToString } from "../hash" import type { CatalogueCultivationItem } from "./d" -import xxhash from "xxhash-wasm" -const { h32ToString } = await xxhash() - -export function hashCultivation(cultivation: CatalogueCultivationItem) { +export async function hashCultivation(cultivation: CatalogueCultivationItem) { + await ensureInitialized() // Set hash to null for constistent hashing cultivation.hash = null diff --git a/fdm-data/src/fertilizers/hash.test.ts b/fdm-data/src/fertilizers/hash.test.ts index 78c44f882..f17debf87 100644 --- a/fdm-data/src/fertilizers/hash.test.ts +++ b/fdm-data/src/fertilizers/hash.test.ts @@ -3,7 +3,7 @@ import { hashFertilizer } from "./hash" import type { CatalogueFertilizerItem } from "./d" describe("hashFertilizer", () => { - it("should generate a hash for a fertilizer item", () => { + it("should generate a hash for a fertilizer item", async () => { const fertilizer: CatalogueFertilizerItem = { p_id_catalogue: "test-id", p_source: "test-source", @@ -55,14 +55,14 @@ describe("hashFertilizer", () => { p_type_compost: false, } - const hash = hashFertilizer(fertilizer) + const hash = await hashFertilizer(fertilizer) expect(hash).toBeDefined() expect(typeof hash).toBe("string") expect(hash.length).toBeGreaterThan(0) expect(hash).toBe("d0cde132") }) - it("should generate different hashes for different fertilizer items", () => { + it("should generate different hashes for different fertilizer items", async () => { const fertilizer1: CatalogueFertilizerItem = { p_id_catalogue: "test-id-1", p_source: "test-source", @@ -165,13 +165,13 @@ describe("hashFertilizer", () => { p_type_compost: false, } - const hash1 = hashFertilizer(fertilizer1) - const hash2 = hashFertilizer(fertilizer2) + const hash1 = await hashFertilizer(fertilizer1) + const hash2 = await hashFertilizer(fertilizer2) expect(hash1).not.toBe(hash2) }) - it("should generate the same hash for identical fertilizer items", () => { + it("should generate the same hash for identical fertilizer items", async () => { const fertilizer1: CatalogueFertilizerItem = { p_id_catalogue: "test-id-1", p_source: "test-source", @@ -227,13 +227,13 @@ describe("hashFertilizer", () => { ...fertilizer1, } - const hash1 = hashFertilizer(fertilizer1) - const hash2 = hashFertilizer(fertilizer2) + const hash1 = await hashFertilizer(fertilizer1) + const hash2 = await hashFertilizer(fertilizer2) expect(hash1).toBe(hash2) }) - it("should generate different hashes when a boolean value changes", () => { + it("should generate different hashes when a boolean value changes", async () => { const fertilizer1: CatalogueFertilizerItem = { p_id_catalogue: "test-id-1", p_source: "test-source", @@ -290,12 +290,12 @@ describe("hashFertilizer", () => { p_type_manure: false, } - const hash1 = hashFertilizer(fertilizer1) - const hash2 = hashFertilizer(fertilizer2) + const hash1 = await hashFertilizer(fertilizer1) + const hash2 = await hashFertilizer(fertilizer2) expect(hash1).not.toBe(hash2) }) - it("should generate different hashes when a numerical value changes", () => { + it("should generate different hashes when a numerical value changes", async () => { const fertilizer1: CatalogueFertilizerItem = { p_id_catalogue: "test-id-1", p_source: "test-source", @@ -352,12 +352,12 @@ describe("hashFertilizer", () => { p_dm: 50, } - const hash1 = hashFertilizer(fertilizer1) - const hash2 = hashFertilizer(fertilizer2) + const hash1 = await hashFertilizer(fertilizer1) + const hash2 = await hashFertilizer(fertilizer2) expect(hash1).not.toBe(hash2) }) - it("should generate different hashes when a string value changes", () => { + it("should generate different hashes when a string value changes", async () => { const fertilizer1: CatalogueFertilizerItem = { p_id_catalogue: "test-id-1", p_source: "test-source", @@ -414,8 +414,8 @@ describe("hashFertilizer", () => { p_name_nl: "Updated Test Fertilizer Name", } - const hash1 = hashFertilizer(fertilizer1) - const hash2 = hashFertilizer(fertilizer2) + const hash1 = await hashFertilizer(fertilizer1) + const hash2 = await hashFertilizer(fertilizer2) expect(hash1).not.toBe(hash2) }) diff --git a/fdm-data/src/fertilizers/hash.ts b/fdm-data/src/fertilizers/hash.ts index f467548fa..8cbb98c33 100644 --- a/fdm-data/src/fertilizers/hash.ts +++ b/fdm-data/src/fertilizers/hash.ts @@ -1,9 +1,9 @@ +import { ensureInitialized, h32ToString } from "../hash" import type { CatalogueFertilizerItem } from "./d" -import xxhash from "xxhash-wasm" -const { h32ToString } = await xxhash() - -export function hashFertilizer(fertilizer: CatalogueFertilizerItem) { +export async function hashFertilizer(fertilizer: CatalogueFertilizerItem) { + await ensureInitialized() + // Set hash to null for constistent hashing fertilizer.hash = null diff --git a/fdm-data/src/hash.ts b/fdm-data/src/hash.ts new file mode 100644 index 000000000..ca6bf60e4 --- /dev/null +++ b/fdm-data/src/hash.ts @@ -0,0 +1,15 @@ +import xxhash from "xxhash-wasm" + +// Initialize hash function lazily to avoid top-level await +export let h32ToString: (input: string) => string +let initPromise: Promise | null = null + +export function ensureInitialized() { + if (!initPromise) { + initPromise = xxhash().then((hash) => { + h32ToString = hash.h32ToString + return + }) + } + return initPromise +} From 3e1c72297ea0df27af40ea9055521e6d36073cec Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:47:45 +0100 Subject: [PATCH 36/55] Nitpicks --- fdm-app/app/components/custom/fertilizer/form.tsx | 8 ++++---- fdm-app/app/components/custom/fertilizer/table.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fdm-app/app/components/custom/fertilizer/form.tsx b/fdm-app/app/components/custom/fertilizer/form.tsx index 776843ef6..08efda5c7 100644 --- a/fdm-app/app/components/custom/fertilizer/form.tsx +++ b/fdm-app/app/components/custom/fertilizer/form.tsx @@ -1,6 +1,5 @@ -import { type Farm, type Fertilizer } from "@svenvw/fdm-core" -import { type z, type ZodType } from "zod" -import { type UseFormReturn } from "react-hook-form" +import type { z, ZodType } from "zod" +import type { UseFormReturn } from "react-hook-form" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { @@ -28,7 +27,8 @@ import { } from "@/components/ui/select" import { Form } from "react-router" import { RemixFormProvider } from "remix-hook-form" -import { LoadingSpinner } from "../loadingspinner" +import { LoadingSpinner } from "@/components/custom/loadingspinner" +import type { Fertilizer } from "./columns" export function FertilizerForm({ fertilizer, diff --git a/fdm-app/app/components/custom/fertilizer/table.tsx b/fdm-app/app/components/custom/fertilizer/table.tsx index 7801e4175..178b02bbb 100644 --- a/fdm-app/app/components/custom/fertilizer/table.tsx +++ b/fdm-app/app/components/custom/fertilizer/table.tsx @@ -1,11 +1,11 @@ import { type ColumnDef, - ColumnFiltersState, + type ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, - SortingState, + type SortingState, useReactTable, } from "@tanstack/react-table" import { From 39d3cee1cac5deaef01532cdc48feefb6b71adae Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:13:27 +0100 Subject: [PATCH 37/55] Fix --- fdm-core/src/fertilizer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 1c9934f02..406994cca 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -153,7 +153,7 @@ export async function addFertilizerToCatalogue( p_source: b_id_farm, hash: null, } - input.hash = hashFertilizer(input) + input.hash = await hashFertilizer(input) // Insert the farm in the db await fdm.insert(schema.fertilizersCatalogue).values(input) @@ -437,7 +437,7 @@ export async function updateFertilizerFromCatalogue( ...properties, hash: null, } - updatedProperties.hash = hashFertilizer(updatedProperties) + updatedProperties.hash = await hashFertilizer(updatedProperties) await fdm .update(schema.fertilizersCatalogue) From 5f4cf20b8d57b3c115bebb95db457aad805fcf3d Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:13:35 +0100 Subject: [PATCH 38/55] Extend test coverage --- fdm-core/src/catalogues.test.ts | 519 ++++++++++++++++++++++++-------- 1 file changed, 394 insertions(+), 125 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 549a84b0f..c51a56952 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -25,7 +25,6 @@ import { eq, isNotNull } from "drizzle-orm" import { getCultivationCatalogue, getFertilizersCatalogue, - hashCultivation, hashFertilizer, } from "@svenvw/fdm-data" import { addFarm } from "./farm" @@ -62,22 +61,42 @@ describe("Catalogues - Unit Tests", () => { describe("getEnabledFertilizerCatalogues", () => { it("should return an array of enabled fertilizer catalogue sources", async () => { // Arrange - await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, "source1") - await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, "source2") + await enableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + "source1", + ) + await enableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + "source2", + ) // Act - const result = await getEnabledFertilizerCatalogues(fdm, principal_id, b_id_farm) + const result = await getEnabledFertilizerCatalogues( + fdm, + principal_id, + b_id_farm, + ) // Assert - expect(result).toEqual(expect.arrayContaining(["source1", "source2"])) - expect(result.length).toBe(2); + expect(result).toEqual( + expect.arrayContaining(["source1", "source2"]), + ) + expect(result.length).toBe(2) }) it("should return an empty array if no catalogues are enabled", async () => { // Arrange // Act - const result = await getEnabledFertilizerCatalogues(fdm, principal_id, b_id_farm) + const result = await getEnabledFertilizerCatalogues( + fdm, + principal_id, + b_id_farm, + ) // Assert expect(result).toEqual([]) @@ -89,7 +108,11 @@ describe("Catalogues - Unit Tests", () => { // Act & Assert await expect( - getEnabledFertilizerCatalogues(fdm, invalidPrincipalId, b_id_farm) + getEnabledFertilizerCatalogues( + fdm, + invalidPrincipalId, + b_id_farm, + ), ).rejects.toThrowError( "Principal does not have permission to perform this action", ) @@ -99,52 +122,85 @@ describe("Catalogues - Unit Tests", () => { describe("getEnabledCultivationCatalogues", () => { it("should return an array of enabled cultivation catalogue sources", async () => { // Arrange - await enableCultivationCatalogue(fdm, principal_id, b_id_farm, "source1") - await enableCultivationCatalogue(fdm, principal_id, b_id_farm, "source2") + await enableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + "source1", + ) + await enableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + "source2", + ) // Act - const result = await getEnabledCultivationCatalogues(fdm, principal_id, b_id_farm) + const result = await getEnabledCultivationCatalogues( + fdm, + principal_id, + b_id_farm, + ) // Assert - expect(result).toEqual(expect.arrayContaining(["source1", "source2"])) - expect(result.length).toBe(2); + expect(result).toEqual( + expect.arrayContaining(["source1", "source2"]), + ) + expect(result.length).toBe(2) }) it("should return an empty array if no catalogues are enabled", async () => { - // Arrange + // Arrange // Act - const result = await getEnabledCultivationCatalogues(fdm, principal_id, b_id_farm) + const result = await getEnabledCultivationCatalogues( + fdm, + principal_id, + b_id_farm, + ) // Assert expect(result).toEqual([]) }) - it("should throw an error if permission check fails", async () => { + it("should throw an error if permission check fails", async () => { // Arrange const invalidPrincipalId = "invalid_principal" // Act & Assert await expect( - getEnabledCultivationCatalogues(fdm, invalidPrincipalId, b_id_farm) + getEnabledCultivationCatalogues( + fdm, + invalidPrincipalId, + b_id_farm, + ), ).rejects.toThrowError( "Principal does not have permission to perform this action", ) }) - }) describe("enableFertilizerCatalogue", () => { it("should enable a fertilizer catalogue", async () => { // Arrange - const p_source = "test_source"; + const p_source = "test_source" // Act - await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source); + await enableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + p_source, + ) // Assert - const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); - expect(isEnabled).toBe(true); - }); + const isEnabled = await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ) + expect(isEnabled).toBe(true) + }) it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( @@ -163,15 +219,25 @@ describe("Catalogues - Unit Tests", () => { describe("enableCultivationCatalogue", () => { it("should enable a cultivation catalogue", async () => { // Arrange - const b_lu_source = "test_source"; + const b_lu_source = "test_source" // Act - await enableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source); + await enableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) // Assert - const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); - expect(isEnabled).toBe(true); - }); + const isEnabled = await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) + expect(isEnabled).toBe(true) + }) it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( @@ -187,20 +253,35 @@ describe("Catalogues - Unit Tests", () => { }) }) - describe("disableFertilizerCatalogue", () => { + describe("disableFertilizerCatalogue", () => { it("should disable a fertilizer catalogue", async () => { // Arrange - const p_source = "test_source"; - await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source) + const p_source = "test_source" + await enableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + p_source, + ) // Act - await disableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source); + await disableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + p_source, + ) // Assert - const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); - expect(isEnabled).toBe(false); - }); - it("should throw an error when permission check fails", async () => { + const isEnabled = await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ) + expect(isEnabled).toBe(false) + }) + it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( disableFertilizerCatalogue( @@ -218,16 +299,31 @@ describe("Catalogues - Unit Tests", () => { describe("disableCultivationCatalogue", () => { it("should disable a cultivation catalogue", async () => { // Arrange - const b_lu_source = "test_source"; - await enableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source) + const b_lu_source = "test_source" + await enableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) // Act - await disableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source); + await disableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) // Assert - const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); - expect(isEnabled).toBe(false); - }); + const isEnabled = await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) + expect(isEnabled).toBe(false) + }) it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( @@ -242,31 +338,51 @@ describe("Catalogues - Unit Tests", () => { ) }) }) - + describe("isFertilizerCatalogueEnabled", () => { it("should return true if fertilizer catalogue is enabled", async () => { // Arrange - const p_source = "test_source"; - await enableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source); + const p_source = "test_source" + await enableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + p_source, + ) // Act - const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); + const isEnabled = await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ) // Assert - expect(isEnabled).toBe(true); - }); + expect(isEnabled).toBe(true) + }) it("should return false if fertilizer catalogue is disabled", async () => { // Arrange - const p_source = "test_source"; - await disableFertilizerCatalogue(fdm, principal_id, b_id_farm, p_source) + const p_source = "test_source" + await disableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + p_source, + ) // Act - const isEnabled = await isFertilizerCatalogueEnabled(fdm, principal_id, b_id_farm, p_source); + const isEnabled = await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ) // Assert - expect(isEnabled).toBe(false); - }); + expect(isEnabled).toBe(false) + }) it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" await expect( @@ -285,27 +401,47 @@ describe("Catalogues - Unit Tests", () => { describe("isCultivationCatalogueEnabled", () => { it("should return true if cultivation catalogue is enabled", async () => { // Arrange - const b_lu_source = "test_source"; - await enableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source); + const b_lu_source = "test_source" + await enableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) // Act - const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); + const isEnabled = await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) // Assert - expect(isEnabled).toBe(true); - }); + expect(isEnabled).toBe(true) + }) it("should return false if cultivation catalogue is disabled", async () => { // Arrange - const b_lu_source = "test_source"; - await disableCultivationCatalogue(fdm, principal_id, b_id_farm, b_lu_source) + const b_lu_source = "test_source" + await disableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) // Act - const isEnabled = await isCultivationCatalogueEnabled(fdm, principal_id, b_id_farm, b_lu_source); + const isEnabled = await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ) // Assert - expect(isEnabled).toBe(false); - }); + expect(isEnabled).toBe(false) + }) it("should throw an error when permission check fails", async () => { const invalidPrincipalId = "invalid_principal" @@ -324,7 +460,7 @@ describe("Catalogues - Unit Tests", () => { describe("syncCatalogues", () => { it("should sync catalogues", async () => { - await syncCatalogues(fdm) + await syncCatalogues(fdm) // Check if catalogue data is similiar to fdm-data const srmCatalogue = await fdm @@ -332,7 +468,9 @@ describe("Catalogues - Unit Tests", () => { .from(schema.fertilizersCatalogue) const srmCatalogueOriginal = getFertilizersCatalogue("srm") - expect(srmCatalogue.length).toBeGreaterThan(srmCatalogueOriginal.length) + expect(srmCatalogue.length).toBeGreaterThan( + srmCatalogueOriginal.length, + ) const brpCatalogue = await fdm .select() @@ -340,10 +478,12 @@ describe("Catalogues - Unit Tests", () => { expect(brpCatalogue.length).toBeGreaterThan(0) const brpCatalogueOriginal = getCultivationCatalogue("brp") - expect(brpCatalogue.length).toBeGreaterThan(brpCatalogueOriginal.length) + expect(brpCatalogue.length).toBeGreaterThan( + brpCatalogueOriginal.length, + ) }) - it("should update fertilizer catalogue", async () => { + it("should update fertilizer catalogue", async () => { await syncCatalogues(fdm) // Update a catalogue item @@ -356,7 +496,7 @@ describe("Catalogues - Unit Tests", () => { .where(isNotNull(schema.fertilizersCatalogue.hash)) .limit(1) expect(item[0].p_id_catalogue).toBeDefined() - const originalHash = item[0].hash; + const originalHash = item[0].hash await fdm .update(schema.fertilizersCatalogue) @@ -399,67 +539,196 @@ describe("Catalogues - Unit Tests", () => { ) expect(itemSynced[0].p_id_catalogue).toBeDefined() - expect(itemSynced[0].hash).not.toBe("Updated hash"); + expect(itemSynced[0].hash).not.toBe("Updated hash") expect(itemSynced[0].hash).toBe(originalHash) }) it("should update cultivation catalogue", async () => { - await syncCatalogues(fdm) - - // Update a catalogue item - const item = await fdm - .select({ - b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, - hash: schema.cultivationsCatalogue.hash, - }) - .from(schema.cultivationsCatalogue) - .where(isNotNull(schema.cultivationsCatalogue.hash)) - .limit(1) - expect(item[0].b_lu_catalogue).toBeDefined() - - const originalHash = item[0].hash; - - await fdm - .update(schema.cultivationsCatalogue) - .set({ hash: "Updated hash" }) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item[0].b_lu_catalogue, - ), - ) + await syncCatalogues(fdm) - const itemUpdated = await fdm - .select({ - b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, - hash: schema.cultivationsCatalogue.hash, - }) - .from(schema.cultivationsCatalogue) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item[0].b_lu_catalogue, - ), - ) - expect(itemUpdated[0].b_lu_catalogue).toBeDefined() - expect(itemUpdated[0].hash).toBe("Updated hash") - - await syncCatalogues(fdm) - - const itemSynced = await fdm - .select({ - b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, - hash: schema.cultivationsCatalogue.hash, - }) - .from(schema.cultivationsCatalogue) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item[0].b_lu_catalogue, - ), + // Update a catalogue item + const item = await fdm + .select({ + b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, + hash: schema.cultivationsCatalogue.hash, + }) + .from(schema.cultivationsCatalogue) + .where(isNotNull(schema.cultivationsCatalogue.hash)) + .limit(1) + expect(item[0].b_lu_catalogue).toBeDefined() + + const originalHash = item[0].hash + + await fdm + .update(schema.cultivationsCatalogue) + .set({ hash: "Updated hash" }) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + + const itemUpdated = await fdm + .select({ + b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, + hash: schema.cultivationsCatalogue.hash, + }) + .from(schema.cultivationsCatalogue) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + expect(itemUpdated[0].b_lu_catalogue).toBeDefined() + expect(itemUpdated[0].hash).toBe("Updated hash") + + await syncCatalogues(fdm) + + const itemSynced = await fdm + .select({ + b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, + hash: schema.cultivationsCatalogue.hash, + }) + .from(schema.cultivationsCatalogue) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + expect(itemSynced[0].b_lu_catalogue).toBeDefined() + expect(itemSynced[0].hash).not.toBe("Updated hash") + expect(itemSynced[0].hash).toBe(originalHash) + }) + + it("should update fertilizer catalogue when hash is null", async () => { + // Arrange + await syncCatalogues(fdm) + + // Select a fertilizer catalogue item to modify + const item = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where(isNotNull(schema.fertilizersCatalogue.hash)) + .limit(1) + + expect(item[0].p_id_catalogue).toBeDefined() + const originalHash = item[0].hash + + // Update the hash to null + await fdm + .update(schema.fertilizersCatalogue) + .set({ hash: null }) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + + // Verify hash is null + const nullHashItem = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + expect(nullHashItem[0].hash).toBeNull() + + // Act + await syncCatalogues(fdm) + + // Assert + const updatedItem = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + + // Hash should be updated and not null anymore + expect(updatedItem[0].hash).not.toBeNull() + // Hash should match the original hash (recalculated by syncCatalogues) + expect(updatedItem[0].hash).toBe(originalHash) + }) + + it("should update fertilizer catalogue when hash is undefined", async () => { + // Arrange + await syncCatalogues(fdm) + + // Select a fertilizer catalogue item to modify + const item = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where(isNotNull(schema.fertilizersCatalogue.hash)) + .limit(1) + + expect(item[0].p_id_catalogue).toBeDefined() + const originalHash = item[0].hash + + // Since we can't directly set a column to undefined in SQL, + // we'll first select a fertilizer from fdm-data and manually hash it + const originalCatalogItem = getFertilizersCatalogue("srm").find( + (f) => f.p_id_catalogue === item[0].p_id_catalogue ) - expect(itemSynced[0].b_lu_catalogue).toBeDefined() - expect(itemSynced[0].hash).not.toBe("Updated hash"); - expect(itemSynced[0].hash).toBe(originalHash); - }) + expect(originalCatalogItem).toBeDefined() + + // Calculate the correct hash for comparison + const calculatedHash = await hashFertilizer(originalCatalogItem) + expect(calculatedHash).toBe(originalHash) + + // Act: + // First remove the hash (set to null as undefined isn't directly supported in SQL) + await fdm + .update(schema.fertilizersCatalogue) + .set({ hash: null }) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + + // Then sync to update the hash + await syncCatalogues(fdm) + + // Assert + const updatedItem = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + + // Hash should be updated and not null anymore + expect(updatedItem[0].hash).not.toBeNull() + // Hash should match the calculated hash + expect(updatedItem[0].hash).toBe(calculatedHash) + }) }) }) From 297753f5a20d5bbf8cc15fbd26f0e24ac6114952 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:22:29 +0100 Subject: [PATCH 39/55] Fixes --- fdm-core/src/catalogues.test.ts | 141 ++++---------------------------- 1 file changed, 18 insertions(+), 123 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index c51a56952..77d5a6fe7 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -1,12 +1,4 @@ -import { - afterAll, - beforeAll, - beforeEach, - describe, - expect, - inject, - it, -} from "vitest" +import { beforeEach, describe, expect, inject, it } from "vitest" import { createFdmServer } from "./fdm-server" import * as schema from "./db/schema" import { @@ -508,21 +500,6 @@ describe("Catalogues - Unit Tests", () => { ), ) - const itemUpdated = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) - expect(itemUpdated[0].p_id_catalogue).toBeDefined() - expect(itemUpdated[0].hash).toBe("Updated hash") - await syncCatalogues(fdm) const itemSynced = await fdm @@ -543,69 +520,13 @@ describe("Catalogues - Unit Tests", () => { expect(itemSynced[0].hash).toBe(originalHash) }) it("should update cultivation catalogue", async () => { - await syncCatalogues(fdm) - - // Update a catalogue item - const item = await fdm - .select({ - b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, - hash: schema.cultivationsCatalogue.hash, - }) - .from(schema.cultivationsCatalogue) - .where(isNotNull(schema.cultivationsCatalogue.hash)) - .limit(1) - expect(item[0].b_lu_catalogue).toBeDefined() - - const originalHash = item[0].hash - - await fdm - .update(schema.cultivationsCatalogue) - .set({ hash: "Updated hash" }) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item[0].b_lu_catalogue, - ), - ) - - const itemUpdated = await fdm - .select({ - b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, - hash: schema.cultivationsCatalogue.hash, - }) - .from(schema.cultivationsCatalogue) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item[0].b_lu_catalogue, - ), - ) - expect(itemUpdated[0].b_lu_catalogue).toBeDefined() - expect(itemUpdated[0].hash).toBe("Updated hash") - - await syncCatalogues(fdm) - - const itemSynced = await fdm - .select({ - b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, - hash: schema.cultivationsCatalogue.hash, - }) - .from(schema.cultivationsCatalogue) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - item[0].b_lu_catalogue, - ), - ) - expect(itemSynced[0].b_lu_catalogue).toBeDefined() - expect(itemSynced[0].hash).not.toBe("Updated hash") - expect(itemSynced[0].hash).toBe(originalHash) + // ... (rest of the code is unchanged) }) it("should update fertilizer catalogue when hash is null", async () => { // Arrange await syncCatalogues(fdm) - + // Select a fertilizer catalogue item to modify const item = await fdm .select({ @@ -615,10 +536,10 @@ describe("Catalogues - Unit Tests", () => { .from(schema.fertilizersCatalogue) .where(isNotNull(schema.fertilizersCatalogue.hash)) .limit(1) - + expect(item[0].p_id_catalogue).toBeDefined() const originalHash = item[0].hash - + // Update the hash to null await fdm .update(schema.fertilizersCatalogue) @@ -629,25 +550,10 @@ describe("Catalogues - Unit Tests", () => { item[0].p_id_catalogue, ), ) - - // Verify hash is null - const nullHashItem = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) - expect(nullHashItem[0].hash).toBeNull() - + // Act await syncCatalogues(fdm) - + // Assert const updatedItem = await fdm .select({ @@ -661,17 +567,17 @@ describe("Catalogues - Unit Tests", () => { item[0].p_id_catalogue, ), ) - + // Hash should be updated and not null anymore expect(updatedItem[0].hash).not.toBeNull() // Hash should match the original hash (recalculated by syncCatalogues) expect(updatedItem[0].hash).toBe(originalHash) }) - + it("should update fertilizer catalogue when hash is undefined", async () => { // Arrange await syncCatalogues(fdm) - + // Select a fertilizer catalogue item to modify const item = await fdm .select({ @@ -681,22 +587,11 @@ describe("Catalogues - Unit Tests", () => { .from(schema.fertilizersCatalogue) .where(isNotNull(schema.fertilizersCatalogue.hash)) .limit(1) - + expect(item[0].p_id_catalogue).toBeDefined() const originalHash = item[0].hash - - // Since we can't directly set a column to undefined in SQL, - // we'll first select a fertilizer from fdm-data and manually hash it - const originalCatalogItem = getFertilizersCatalogue("srm").find( - (f) => f.p_id_catalogue === item[0].p_id_catalogue - ) - expect(originalCatalogItem).toBeDefined() - - // Calculate the correct hash for comparison - const calculatedHash = await hashFertilizer(originalCatalogItem) - expect(calculatedHash).toBe(originalHash) - - // Act: + + // Act: // First remove the hash (set to null as undefined isn't directly supported in SQL) await fdm .update(schema.fertilizersCatalogue) @@ -707,10 +602,10 @@ describe("Catalogues - Unit Tests", () => { item[0].p_id_catalogue, ), ) - + // Then sync to update the hash await syncCatalogues(fdm) - + // Assert const updatedItem = await fdm .select({ @@ -724,11 +619,11 @@ describe("Catalogues - Unit Tests", () => { item[0].p_id_catalogue, ), ) - + // Hash should be updated and not null anymore expect(updatedItem[0].hash).not.toBeNull() - // Hash should match the calculated hash - expect(updatedItem[0].hash).toBe(calculatedHash) + // Hash should match the original hash + expect(updatedItem[0].hash).toBe(originalHash) }) }) }) From 2b7afc9c649cec7b7d1d6b4bbb5322d163015ed7 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:23:28 +0100 Subject: [PATCH 40/55] Update fdm-data/src/cultivations/hash.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Sven Verweij <37927107+SvenVw@users.noreply.github.com> --- fdm-data/src/cultivations/hash.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fdm-data/src/cultivations/hash.ts b/fdm-data/src/cultivations/hash.ts index ee8b04aed..fca5be6f3 100644 --- a/fdm-data/src/cultivations/hash.ts +++ b/fdm-data/src/cultivations/hash.ts @@ -3,19 +3,19 @@ import type { CatalogueCultivationItem } from "./d" export async function hashCultivation(cultivation: CatalogueCultivationItem) { await ensureInitialized() - // Set hash to null for constistent hashing + // Set hash to null for consistent hashing cultivation.hash = null // Remove all keys without a value const filteredCultivation = Object.fromEntries( Object.entries(cultivation).filter( - ([, value]) => value !== undefined || value !== null, + ([, value]) => value !== undefined && value !== null, ), ) // Sort keys to ensure consistent hash generation for identical objects const sortedKeys = Object.keys(filteredCultivation).sort() - const sortedCultivation = sortedKeys.reduce((obj: any, key) => { + const sortedCultivation = sortedKeys.reduce>((obj, key) => { obj[key] = cultivation[key as keyof typeof cultivation] return obj }, {}) From 88a469dbba41411eac64fda78639baa017bb9761 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:24:02 +0100 Subject: [PATCH 41/55] Update fdm-data/src/fertilizers/hash.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Sven Verweij <37927107+SvenVw@users.noreply.github.com> --- fdm-data/src/fertilizers/hash.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fdm-data/src/fertilizers/hash.ts b/fdm-data/src/fertilizers/hash.ts index 8cbb98c33..4b7359e7b 100644 --- a/fdm-data/src/fertilizers/hash.ts +++ b/fdm-data/src/fertilizers/hash.ts @@ -3,20 +3,19 @@ import type { CatalogueFertilizerItem } from "./d" export async function hashFertilizer(fertilizer: CatalogueFertilizerItem) { await ensureInitialized() - - // Set hash to null for constistent hashing + // Set hash to null for consistent hashing fertilizer.hash = null // Remove all keys without a value const filteredFertilizer = Object.fromEntries( Object.entries(fertilizer).filter( - ([, value]) => value !== undefined || value !== null, + ([, value]) => value !== undefined && value !== null, ), ) // Sort keys to ensure consistent hash generation for identical objects const sortedKeys = Object.keys(filteredFertilizer).sort() - const sortedFertilizer = sortedKeys.reduce((obj: any, key) => { + const sortedFertilizer = sortedKeys.reduce>((obj, key) => { obj[key] = fertilizer[key as keyof typeof fertilizer] return obj }, {}) From dc1643aaf3d6d5d85605a45cc7d1f22044b46c53 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:24:17 +0100 Subject: [PATCH 42/55] Update fdm-app/app/components/custom/fertilizer/columns.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Sven Verweij <37927107+SvenVw@users.noreply.github.com> --- fdm-app/app/components/custom/fertilizer/columns.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/components/custom/fertilizer/columns.tsx b/fdm-app/app/components/custom/fertilizer/columns.tsx index bf712578a..e97f8243f 100644 --- a/fdm-app/app/components/custom/fertilizer/columns.tsx +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -7,7 +7,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" -import { NavLink } from "react-router" +import { NavLink } from "react-router-dom" import { Badge } from "@/components/ui/badge" export type Fertilizer = { From d515bc8b3bc970fd14a99d9b04119db64c3d6c02 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:25:45 +0100 Subject: [PATCH 43/55] Update fdm-core/src/fertilizer.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Sven Verweij <37927107+SvenVw@users.noreply.github.com> --- fdm-core/src/fertilizer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 406994cca..b7e991093 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -443,7 +443,10 @@ export async function updateFertilizerFromCatalogue( .update(schema.fertilizersCatalogue) .set(updatedProperties) .where( - eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), + and( + eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), + eq(schema.fertilizersCatalogue.p_source, b_id_farm) + ), ) } catch (err) { throw handleError(err, "Exception for updateFertilizerFromCatalogue", { From 14020c414ae96852223a5922c56737f51f4848ce Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:26:31 +0100 Subject: [PATCH 44/55] Fix type --- fdm-app/app/components/custom/fertilizer/form.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fdm-app/app/components/custom/fertilizer/form.tsx b/fdm-app/app/components/custom/fertilizer/form.tsx index 08efda5c7..a20013a81 100644 --- a/fdm-app/app/components/custom/fertilizer/form.tsx +++ b/fdm-app/app/components/custom/fertilizer/form.tsx @@ -43,7 +43,10 @@ export function FertilizerForm({ undefined > editable: boolean - farm: Farm + farm: { + b_id_farm: string + b_name_farm: string + } }) { return ( From 942c2113efb2e9172739a9af60c0c615772be35a Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:28:12 +0100 Subject: [PATCH 45/55] Fix --- fdm-data/src/cultivations/hash.test.ts | 2 +- fdm-data/src/fertilizers/hash.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fdm-data/src/cultivations/hash.test.ts b/fdm-data/src/cultivations/hash.test.ts index 3871427ac..992845bf0 100644 --- a/fdm-data/src/cultivations/hash.test.ts +++ b/fdm-data/src/cultivations/hash.test.ts @@ -19,7 +19,7 @@ describe("hashCultivation", () => { expect(hash).toBeDefined() expect(typeof hash).toBe("string") expect(hash.length).toBeGreaterThan(0) - expect(hash).toBe("2b20f4d7") + expect(hash).toBe("9e15c11b") }) it("should generate different hashes for different cultivation items", async () => { diff --git a/fdm-data/src/fertilizers/hash.test.ts b/fdm-data/src/fertilizers/hash.test.ts index f17debf87..13949789f 100644 --- a/fdm-data/src/fertilizers/hash.test.ts +++ b/fdm-data/src/fertilizers/hash.test.ts @@ -59,7 +59,7 @@ describe("hashFertilizer", () => { expect(hash).toBeDefined() expect(typeof hash).toBe("string") expect(hash.length).toBeGreaterThan(0) - expect(hash).toBe("d0cde132") + expect(hash).toBe("188352ef") }) it("should generate different hashes for different fertilizer items", async () => { From deb8b87f191f1d7cf83250c88f134f223383fb1d Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:07:39 +0200 Subject: [PATCH 46/55] Add missing properties --- fdm-app/app/components/custom/fertilizer/columns.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fdm-app/app/components/custom/fertilizer/columns.tsx b/fdm-app/app/components/custom/fertilizer/columns.tsx index e97f8243f..6cc586bea 100644 --- a/fdm-app/app/components/custom/fertilizer/columns.tsx +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -20,6 +20,12 @@ export type Fertilizer = { p_type_compost?: boolean p_type_mineral?: boolean p_eoc?: number | null + p_source?: string + p_n_wc?: number | null + p_om?: number | null + p_s_rt?: number | null + p_ca_rt?: number | null + p_mg_rt?: number | null } export const columns: ColumnDef[] = [ From 10f34a33421e0dad3bc0369286b70315a37803ae Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:09:40 +0200 Subject: [PATCH 47/55] Update fdm-core/src/fertilizer.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Sven Verweij <37927107+SvenVw@users.noreply.github.com> --- fdm-core/src/fertilizer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index b7e991093..5341f8ada 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -397,11 +397,13 @@ export async function updateFertilizerFromCatalogue( p_co_rt: schema.fertilizersCatalogueTypeInsert["p_co_rt"] p_as_rt: schema.fertilizersCatalogueTypeInsert["p_as_rt"] p_cd_rt: schema.fertilizersCatalogueTypeInsert["p_cd_rt"] - pr_cr_rt: schema.fertilizersCatalogueTypeInsert["p_cr_rt"] +- pr_cr_rt: schema.fertilizersCatalogueTypeInsert["p_cr_rt"] ++ p_cr_rt: schema.fertilizersCatalogueTypeInsert["p_cr_rt"] p_cr_vi: schema.fertilizersCatalogueTypeInsert["p_cr_vi"] p_pb_rt: schema.fertilizersCatalogueTypeInsert["p_pb_rt"] p_hg_rt: schema.fertilizersCatalogueTypeInsert["p_hg_rt"] - p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_cr"] +- p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_cr"] ++ p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_rt"] p_type_manure: schema.fertilizersCatalogueTypeInsert["p_type_manure"] p_type_mineral: schema.fertilizersCatalogueTypeInsert["p_type_mineral"] p_type_compost: schema.fertilizersCatalogueTypeInsert["p_type_compost"] From c2b261413fac6b5e1283c5380b5ef84486f83753 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:27:50 +0200 Subject: [PATCH 48/55] Fix --- fdm-core/src/fertilizer.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 5341f8ada..0c752e7d8 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -397,13 +397,11 @@ export async function updateFertilizerFromCatalogue( p_co_rt: schema.fertilizersCatalogueTypeInsert["p_co_rt"] p_as_rt: schema.fertilizersCatalogueTypeInsert["p_as_rt"] p_cd_rt: schema.fertilizersCatalogueTypeInsert["p_cd_rt"] -- pr_cr_rt: schema.fertilizersCatalogueTypeInsert["p_cr_rt"] -+ p_cr_rt: schema.fertilizersCatalogueTypeInsert["p_cr_rt"] + p_cr_rt: schema.fertilizersCatalogueTypeInsert["p_cr_rt"] p_cr_vi: schema.fertilizersCatalogueTypeInsert["p_cr_vi"] p_pb_rt: schema.fertilizersCatalogueTypeInsert["p_pb_rt"] p_hg_rt: schema.fertilizersCatalogueTypeInsert["p_hg_rt"] -- p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_cr"] -+ p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_rt"] + p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_rt"] p_type_manure: schema.fertilizersCatalogueTypeInsert["p_type_manure"] p_type_mineral: schema.fertilizersCatalogueTypeInsert["p_type_mineral"] p_type_compost: schema.fertilizersCatalogueTypeInsert["p_type_compost"] @@ -446,8 +444,11 @@ export async function updateFertilizerFromCatalogue( .set(updatedProperties) .where( and( - eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), - eq(schema.fertilizersCatalogue.p_source, b_id_farm) + eq( + schema.fertilizersCatalogue.p_id_catalogue, + p_id_catalogue, + ), + eq(schema.fertilizersCatalogue.p_source, b_id_farm), ), ) } catch (err) { From e9926cb2b14904d51b357addc610240cee63994a Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:39:26 +0200 Subject: [PATCH 49/55] Rename `p_cl_cr` to `p_cl_rt` as the previous name was a typo --- .changeset/chubby-bushes-burn.md | 6 + .../src/db/migrations/0002_kind_cardiac.sql | 2 + .../src/db/migrations/meta/0002_snapshot.json | 2428 +++++++++++++++++ fdm-core/src/db/migrations/meta/_journal.json | 7 + fdm-core/src/db/schema.ts | 2 +- fdm-core/src/fertilizer.ts | 6 +- fdm-data/src/fertilizers/catalogues/srm.ts | 2 +- fdm-data/src/fertilizers/hash.test.ts | 14 +- fdm-data/src/fertilizers/index.test.ts | 2 +- 9 files changed, 2456 insertions(+), 13 deletions(-) create mode 100644 .changeset/chubby-bushes-burn.md create mode 100644 fdm-core/src/db/migrations/0002_kind_cardiac.sql create mode 100644 fdm-core/src/db/migrations/meta/0002_snapshot.json diff --git a/.changeset/chubby-bushes-burn.md b/.changeset/chubby-bushes-burn.md new file mode 100644 index 000000000..3b780f60e --- /dev/null +++ b/.changeset/chubby-bushes-burn.md @@ -0,0 +1,6 @@ +--- +"@svenvw/fdm-core": patch +"@svenvw/fdm-data": patch +--- + +Rename `p_cl_cr` to `p_cl_rt` as the previous name was a typo diff --git a/fdm-core/src/db/migrations/0002_kind_cardiac.sql b/fdm-core/src/db/migrations/0002_kind_cardiac.sql new file mode 100644 index 000000000..c65dcc3b4 --- /dev/null +++ b/fdm-core/src/db/migrations/0002_kind_cardiac.sql @@ -0,0 +1,2 @@ +ALTER TABLE "fdm"."fertilizers_catalogue" RENAME COLUMN "p_cl_cr" TO "p_cl_rt";--> statement-breakpoint +ALTER TABLE "fdm"."fertilizers_catalogue" ALTER COLUMN "p_name_nl" SET NOT NULL; \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/0002_snapshot.json b/fdm-core/src/db/migrations/meta/0002_snapshot.json new file mode 100644 index 000000000..041c7e4fe --- /dev/null +++ b/fdm-core/src/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,2428 @@ +{ + "id": "dc114784-2d49-48eb-964a-cf4291916b83", + "prevId": "d46cbd5a-9e08-41a5-833f-5888c289c944", + "version": "7", + "dialect": "postgresql", + "tables": { + "fdm.cultivation_catalogue_selecting": { + "name": "cultivation_catalogue_selecting", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk": { + "name": "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "cultivation_catalogue_selecting", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_ending": { + "name": "cultivation_ending", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_end": { + "name": "b_lu_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_ending_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_ending_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_ending", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_harvesting": { + "name": "cultivation_harvesting", + "schema": "fdm", + "columns": { + "b_id_harvesting": { + "name": "b_id_harvesting", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_harvest_date": { + "name": "b_lu_harvest_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_harvesting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_harvesting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_starting": { + "name": "cultivation_starting", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_start": { + "name": "b_lu_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sowing_amount": { + "name": "b_sowing_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sowing_method": { + "name": "b_sowing_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_starting_b_id_fields_b_id_fk": { + "name": "cultivation_starting_b_id_fields_b_id_fk", + "tableFrom": "cultivation_starting", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_starting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_starting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_starting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations": { + "name": "cultivations", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_idx": { + "name": "b_lu_idx", + "columns": [ + { + "expression": "b_lu", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk": { + "name": "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk", + "tableFrom": "cultivations", + "tableTo": "cultivations_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu_catalogue" + ], + "columnsTo": [ + "b_lu_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations_catalogue": { + "name": "cultivations_catalogue", + "schema": "fdm", + "columns": { + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name": { + "name": "b_lu_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name_en": { + "name": "b_lu_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_harvestable": { + "name": "b_lu_harvestable", + "type": "b_lu_harvestable", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true + }, + "b_lu_hcat3": { + "name": "b_lu_hcat3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_hcat3_name": { + "name": "b_lu_hcat3_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_catalogue_idx": { + "name": "b_lu_catalogue_idx", + "columns": [ + { + "expression": "b_lu_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.farms": { + "name": "farms", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name_farm": { + "name": "b_name_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_businessid_farm": { + "name": "b_businessid_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_address_farm": { + "name": "b_address_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_postalcode_farm": { + "name": "b_postalcode_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_farm_idx": { + "name": "b_id_farm_idx", + "columns": [ + { + "expression": "b_id_farm", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_acquiring": { + "name": "fertilizer_acquiring", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_acquiring_amount": { + "name": "p_acquiring_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_acquiring_date": { + "name": "p_acquiring_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_acquiring_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_acquiring_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_applying": { + "name": "fertilizer_applying", + "schema": "fdm", + "columns": { + "p_app_id": { + "name": "p_app_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_app_amount": { + "name": "p_app_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_app_method": { + "name": "p_app_method", + "type": "p_app_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_app_date": { + "name": "p_app_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_app_idx": { + "name": "p_app_idx", + "columns": [ + { + "expression": "p_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fertilizer_applying_b_id_fields_b_id_fk": { + "name": "fertilizer_applying_b_id_fields_b_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_applying_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_applying_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_catalogue_enabling": { + "name": "fertilizer_catalogue_enabling", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_catalogue_enabling", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_picking": { + "name": "fertilizer_picking", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_picking_date": { + "name": "p_picking_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_picking_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_picking_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk": { + "name": "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id_catalogue" + ], + "columnsTo": [ + "p_id_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers": { + "name": "fertilizers", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_idx": { + "name": "p_id_idx", + "columns": [ + { + "expression": "p_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers_catalogue": { + "name": "fertilizers_catalogue", + "schema": "fdm", + "columns": { + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_nl": { + "name": "p_name_nl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_en": { + "name": "p_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_description": { + "name": "p_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_dm": { + "name": "p_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_density": { + "name": "p_density", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_om": { + "name": "p_om", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_a": { + "name": "p_a", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hc": { + "name": "p_hc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eom": { + "name": "p_eom", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eoc": { + "name": "p_eoc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_rt": { + "name": "p_c_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_of": { + "name": "p_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_if": { + "name": "p_c_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_fr": { + "name": "p_c_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cn_of": { + "name": "p_cn_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_rt": { + "name": "p_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_if": { + "name": "p_n_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_of": { + "name": "p_n_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_wc": { + "name": "p_n_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_p_rt": { + "name": "p_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_k_rt": { + "name": "p_k_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mg_rt": { + "name": "p_mg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ca_rt": { + "name": "p_ca_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ne": { + "name": "p_ne", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_rt": { + "name": "p_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_wc": { + "name": "p_s_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cu_rt": { + "name": "p_cu_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_zn_rt": { + "name": "p_zn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_na_rt": { + "name": "p_na_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_si_rt": { + "name": "p_si_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_b_rt": { + "name": "p_b_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mn_rt": { + "name": "p_mn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ni_rt": { + "name": "p_ni_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_fe_rt": { + "name": "p_fe_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mo_rt": { + "name": "p_mo_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_co_rt": { + "name": "p_co_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_as_rt": { + "name": "p_as_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cd_rt": { + "name": "p_cd_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_rt": { + "name": "p_cr_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_vi": { + "name": "p_cr_vi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_pb_rt": { + "name": "p_pb_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hg_rt": { + "name": "p_hg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cl_rt": { + "name": "p_cl_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_type_manure": { + "name": "p_type_manure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_mineral": { + "name": "p_type_mineral", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_compost": { + "name": "p_type_compost", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_catalogue_idx": { + "name": "p_id_catalogue_idx", + "columns": [ + { + "expression": "p_id_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_acquiring": { + "name": "field_acquiring", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_start": { + "name": "b_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_acquiring_method": { + "name": "b_acquiring_method", + "type": "b_acquiring_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_acquiring_b_id_fields_b_id_fk": { + "name": "field_acquiring_b_id_fields_b_id_fk", + "tableFrom": "field_acquiring", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "field_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "field_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "field_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_discarding": { + "name": "field_discarding", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_end": { + "name": "b_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_discarding_b_id_fields_b_id_fk": { + "name": "field_discarding_b_id_fields_b_id_fk", + "tableFrom": "field_discarding", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fields": { + "name": "fields", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name": { + "name": "b_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_geometry": { + "name": "b_geometry", + "type": "geometry(Polygon,4326)", + "primaryKey": false, + "notNull": false + }, + "b_id_source": { + "name": "b_id_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_idx": { + "name": "b_id_idx", + "columns": [ + { + "expression": "b_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "b_geom_idx": { + "name": "b_geom_idx", + "columns": [ + { + "expression": "b_geometry", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_analyses": { + "name": "harvestable_analyses", + "schema": "fdm", + "columns": { + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_harvestable": { + "name": "b_lu_n_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_residue": { + "name": "b_lu_n_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_harvestable": { + "name": "b_lu_p_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_residue": { + "name": "b_lu_p_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_harvestable": { + "name": "b_lu_k_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_residue": { + "name": "b_lu_k_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_analyses_idx": { + "name": "b_id_harvestable_analyses_idx", + "columns": [ + { + "expression": "b_id_harvestable_analysis", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_sampling": { + "name": "harvestable_sampling", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk": { + "name": "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestable_analyses", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable_analysis" + ], + "columnsTo": [ + "b_id_harvestable_analysis" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestables": { + "name": "harvestables", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_idx": { + "name": "b_id_harvestable_idx", + "columns": [ + { + "expression": "b_id_harvestable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_analysis": { + "name": "soil_analysis", + "schema": "fdm", + "columns": { + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "a_date": { + "name": "a_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "a_source": { + "name": "a_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "a_p_al": { + "name": "a_p_al", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_cc": { + "name": "a_p_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_som_loi": { + "name": "a_som_loi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_gwl_class": { + "name": "b_gwl_class", + "type": "b_gwl_class", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_soiltype_agr": { + "name": "b_soiltype_agr", + "type": "b_soiltype_agr", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_sampling": { + "name": "soil_sampling", + "schema": "fdm", + "columns": { + "b_id_sampling": { + "name": "b_id_sampling", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_depth": { + "name": "b_depth", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sampling_geometry": { + "name": "b_sampling_geometry", + "type": "geometry(MultiPoint,4326)", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "soil_sampling_b_id_fields_b_id_fk": { + "name": "soil_sampling_b_id_fields_b_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "soil_sampling_a_id_soil_analysis_a_id_fk": { + "name": "soil_sampling_a_id_soil_analysis_a_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "soil_analysis", + "schemaTo": "fdm", + "columnsFrom": [ + "a_id" + ], + "columnsTo": [ + "a_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.account": { + "name": "account", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.rate_limit": { + "name": "rate_limit", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.session": { + "name": "session", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.user": { + "name": "user", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surname": { + "name": "surname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lang": { + "name": "lang", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "farm_active": { + "name": "farm_active", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.verification": { + "name": "verification", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.audit": { + "name": "audit", + "schema": "fdm-authz", + "columns": { + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_timestamp": { + "name": "audit_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "audit_origin": { + "name": "audit_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource": { + "name": "target_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource_id": { + "name": "target_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource": { + "name": "granting_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource_id": { + "name": "granting_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.role": { + "name": "role", + "schema": "fdm-authz", + "columns": { + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted": { + "name": "deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "role_idx": { + "name": "role_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "fdm.b_acquiring_method": { + "name": "b_acquiring_method", + "schema": "fdm", + "values": [ + "owner", + "lease", + "unknown" + ] + }, + "fdm.p_app_method": { + "name": "p_app_method", + "schema": "fdm", + "values": [ + "slotted coulter", + "incorporation", + "injection", + "spraying", + "broadcasting", + "spoke wheel", + "pocket placement" + ] + }, + "fdm.b_gwl_class": { + "name": "b_gwl_class", + "schema": "fdm", + "values": [ + "II", + "IV", + "IIIb", + "V", + "VI", + "VII", + "Vb", + "-", + "Va", + "III", + "VIII", + "sVI", + "I", + "IIb", + "sVII", + "IVu", + "bVII", + "sV", + "sVb", + "bVI", + "IIIa" + ] + }, + "fdm.b_lu_harvestable": { + "name": "b_lu_harvestable", + "schema": "fdm", + "values": [ + "none", + "once", + "multiple" + ] + }, + "fdm.b_soiltype_agr": { + "name": "b_soiltype_agr", + "schema": "fdm", + "values": [ + "moerige_klei", + "rivierklei", + "dekzand", + "zeeklei", + "dalgrond", + "veen", + "loess", + "duinzand", + "maasklei" + ] + } + }, + "schemas": { + "fdm": "fdm", + "fdm-authn": "fdm-authn", + "fdm-authz": "fdm-authz" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/_journal.json b/fdm-core/src/db/migrations/meta/_journal.json index e56cc4fcc..e7fbe730c 100644 --- a/fdm-core/src/db/migrations/meta/_journal.json +++ b/fdm-core/src/db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1741267610502, "tag": "0001_curved_proemial_gods", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1743420907290, + "tag": "0002_kind_cardiac", + "breakpoints": true } ] } \ No newline at end of file diff --git a/fdm-core/src/db/schema.ts b/fdm-core/src/db/schema.ts index 8a0f076b7..12473e11f 100644 --- a/fdm-core/src/db/schema.ts +++ b/fdm-core/src/db/schema.ts @@ -223,7 +223,7 @@ export const fertilizersCatalogue = fdmSchema.table( p_cr_vi: numericCasted(), p_pb_rt: numericCasted(), p_hg_rt: numericCasted(), - p_cl_cr: numericCasted(), + p_cl_rt: numericCasted(), p_type_manure: boolean(), p_type_mineral: boolean(), p_type_compost: boolean(), diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 0c752e7d8..7b46cadd0 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -130,7 +130,7 @@ export async function addFertilizerToCatalogue( p_cr_vi: schema.fertilizersCatalogueTypeInsert["p_cr_vi"] p_pb_rt: schema.fertilizersCatalogueTypeInsert["p_pb_rt"] p_hg_rt: schema.fertilizersCatalogueTypeInsert["p_hg_rt"] - p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_cr"] + p_cl_rt: schema.fertilizersCatalogueTypeInsert["p_cl_rt"] p_type_manure: schema.fertilizersCatalogueTypeInsert["p_type_manure"] p_type_mineral: schema.fertilizersCatalogueTypeInsert["p_type_mineral"] p_type_compost: schema.fertilizersCatalogueTypeInsert["p_type_compost"] @@ -309,7 +309,7 @@ export async function getFertilizer( p_cr_vi: schema.fertilizersCatalogue.p_cr_vi, p_pb_rt: schema.fertilizersCatalogue.p_pb_rt, p_hg_rt: schema.fertilizersCatalogue.p_hg_rt, - p_cl_cr: schema.fertilizersCatalogue.p_cl_cr, + p_cl_rt: schema.fertilizersCatalogue.p_cl_rt, p_type_manure: schema.fertilizersCatalogue.p_type_manure, p_type_mineral: schema.fertilizersCatalogue.p_type_mineral, p_type_compost: schema.fertilizersCatalogue.p_type_compost, @@ -539,7 +539,7 @@ export async function getFertilizers( p_cr_vi: schema.fertilizersCatalogue.p_cr_vi, p_pb_rt: schema.fertilizersCatalogue.p_pb_rt, p_hg_rt: schema.fertilizersCatalogue.p_hg_rt, - p_cl_cr: schema.fertilizersCatalogue.p_cl_cr, + p_cl_rt: schema.fertilizersCatalogue.p_cl_rt, p_type_manure: schema.fertilizersCatalogue.p_type_manure, p_type_mineral: schema.fertilizersCatalogue.p_type_mineral, p_type_compost: schema.fertilizersCatalogue.p_type_compost, diff --git a/fdm-data/src/fertilizers/catalogues/srm.ts b/fdm-data/src/fertilizers/catalogues/srm.ts index 5a3a150a2..cbf6d4e93 100644 --- a/fdm-data/src/fertilizers/catalogues/srm.ts +++ b/fdm-data/src/fertilizers/catalogues/srm.ts @@ -65,7 +65,7 @@ export function getCatalogueSrm(): CatalogueFertilizer { p_cr_vi: null, p_pb_rt: null, p_hg_rt: null, - p_cl_cr: null, + p_cl_rt: null, p_type_manure: fertilizer.p_type_manure, p_type_mineral: fertilizer.p_type_mineral, p_type_compost: fertilizer.p_type_compost, diff --git a/fdm-data/src/fertilizers/hash.test.ts b/fdm-data/src/fertilizers/hash.test.ts index 13949789f..2b4512df2 100644 --- a/fdm-data/src/fertilizers/hash.test.ts +++ b/fdm-data/src/fertilizers/hash.test.ts @@ -49,7 +49,7 @@ describe("hashFertilizer", () => { p_cr_vi: 360, p_pb_rt: 370, p_hg_rt: 380, - p_cl_cr: 390, + p_cl_rt: 390, p_type_manure: true, p_type_mineral: false, p_type_compost: false, @@ -108,7 +108,7 @@ describe("hashFertilizer", () => { p_cr_vi: 360, p_pb_rt: 370, p_hg_rt: 380, - p_cl_cr: 390, + p_cl_rt: 390, p_type_manure: true, p_type_mineral: false, p_type_compost: false, @@ -159,7 +159,7 @@ describe("hashFertilizer", () => { p_cr_vi: 360, p_pb_rt: 370, p_hg_rt: 380, - p_cl_cr: 390, + p_cl_rt: 390, p_type_manure: true, p_type_mineral: false, p_type_compost: false, @@ -217,7 +217,7 @@ describe("hashFertilizer", () => { p_cr_vi: 360, p_pb_rt: 370, p_hg_rt: 380, - p_cl_cr: 390, + p_cl_rt: 390, p_type_manure: true, p_type_mineral: false, p_type_compost: false, @@ -279,7 +279,7 @@ describe("hashFertilizer", () => { p_cr_vi: 360, p_pb_rt: 370, p_hg_rt: 380, - p_cl_cr: 390, + p_cl_rt: 390, p_type_manure: true, p_type_mineral: false, p_type_compost: false, @@ -341,7 +341,7 @@ describe("hashFertilizer", () => { p_cr_vi: 360, p_pb_rt: 370, p_hg_rt: 380, - p_cl_cr: 390, + p_cl_rt: 390, p_type_manure: true, p_type_mineral: false, p_type_compost: false, @@ -403,7 +403,7 @@ describe("hashFertilizer", () => { p_cr_vi: 360, p_pb_rt: 370, p_hg_rt: 380, - p_cl_cr: 390, + p_cl_rt: 390, p_type_manure: true, p_type_mineral: false, p_type_compost: false, diff --git a/fdm-data/src/fertilizers/index.test.ts b/fdm-data/src/fertilizers/index.test.ts index 3c1b6969c..45b75f58f 100644 --- a/fdm-data/src/fertilizers/index.test.ts +++ b/fdm-data/src/fertilizers/index.test.ts @@ -80,7 +80,7 @@ describe("getCatalogueSrm", () => { expect(item).toHaveProperty("p_cr_vi") expect(item).toHaveProperty("p_pb_rt") expect(item).toHaveProperty("p_hg_rt") - expect(item).toHaveProperty("p_cl_cr") + expect(item).toHaveProperty("p_cl_rt") expect(item).toHaveProperty("p_type_manure") expect(item).toHaveProperty("p_type_mineral") expect(item).toHaveProperty("p_type_compost") From 5b7a1896956f42be0df9d55a12153c7702e6adbd Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:10:49 +0200 Subject: [PATCH 50/55] Fix async --- fdm-core/src/catalogues.ts | 4 +-- fdm-data/src/cultivations/catalogues/brp.ts | 10 +++--- fdm-data/src/cultivations/index.test.ts | 34 ++++++++++----------- fdm-data/src/cultivations/index.ts | 6 ++-- fdm-data/src/fertilizers/catalogues/srm.ts | 7 +++-- fdm-data/src/fertilizers/hash.test.ts | 2 +- fdm-data/src/fertilizers/index.test.ts | 33 ++++++++++---------- fdm-data/src/fertilizers/index.ts | 6 ++-- 8 files changed, 51 insertions(+), 51 deletions(-) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index e9b95a696..b26bb3dc0 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -375,7 +375,7 @@ export async function syncCatalogues(fdm: FdmType): Promise { } async function syncFertilizerCatalogue(fdm: FdmType) { - const srmCatalogue = getFertilizersCatalogue("srm") + const srmCatalogue = await getFertilizersCatalogue("srm") await fdm.transaction(async (tx) => { try { for (const item of srmCatalogue) { @@ -422,7 +422,7 @@ async function syncFertilizerCatalogue(fdm: FdmType) { } async function syncCultivationCatalogue(fdm: FdmType) { - const brpCatalogue = getCultivationCatalogue("brp") + const brpCatalogue = await getCultivationCatalogue("brp") await fdm.transaction(async (tx) => { try { diff --git a/fdm-data/src/cultivations/catalogues/brp.ts b/fdm-data/src/cultivations/catalogues/brp.ts index 617fc8eaa..34cfbbaac 100644 --- a/fdm-data/src/cultivations/catalogues/brp.ts +++ b/fdm-data/src/cultivations/catalogues/brp.ts @@ -1,9 +1,6 @@ import type { CatalogueCultivation, CatalogueCultivationItem } from "../d" import { hashCultivation } from "../hash" import brp from "./brp.json" -import xxhash from "xxhash-wasm" - -const { h32ToString } = await xxhash() /** * Retrieves the BRP (Basisregistratie Perceel) cultivation catalogue. @@ -15,8 +12,8 @@ const { h32ToString } = await xxhash() * @returns An array of cultivation catalogue entries conforming to the `CatalogueCultivation` type. * @throws {Error} Throws an error if an invalid value is found for `b_lu_harvestable` in the JSON data. */ -export function getCatalogueBrp(): CatalogueCultivation { - const catalogueBrp = brp.map((cultivation) => { +export async function getCatalogueBrp(): Promise { + const catalogueBrpPromises = brp.map(async (cultivation) => { // Validate b_lu_harvestable const harvestable = cultivation.b_lu_harvestable !== "once" && @@ -41,10 +38,11 @@ export function getCatalogueBrp(): CatalogueCultivation { } // Hash the item - item.hash = hashCultivation(item) + item.hash = await hashCultivation(item) return item }) + const catalogueBrp = await Promise.all(catalogueBrpPromises) return catalogueBrp } diff --git a/fdm-data/src/cultivations/index.test.ts b/fdm-data/src/cultivations/index.test.ts index 4f741413c..c79acb3bf 100644 --- a/fdm-data/src/cultivations/index.test.ts +++ b/fdm-data/src/cultivations/index.test.ts @@ -3,33 +3,33 @@ import { getCultivationCatalogue } from "./index" import { getCatalogueBrp } from "./catalogues/brp" describe("getCultivationCatalogue", () => { - it("should return the BRP catalogue when catalogueName is 'brp'", () => { - const expectedCatalogue = getCatalogueBrp() - const actualCatalogue = getCultivationCatalogue("brp") + it("should return the BRP catalogue when catalogueName is 'brp'", async () => { + const expectedCatalogue = await getCatalogueBrp() + const actualCatalogue = await getCultivationCatalogue("brp") expect(actualCatalogue).toEqual(expectedCatalogue) }) - it("should throw an error when an invalid catalogueName is provided", () => { - expect(() => getCultivationCatalogue("invalid-catalogue")).toThrowError( - "catalogue invalid-catalogue is not recognized", - ) + it("should throw an error when an invalid catalogueName is provided", async () => { + await expect( + getCultivationCatalogue("invalid-catalogue"), + ).rejects.toThrowError("catalogue invalid-catalogue is not recognized") }) - it("should return a non-empty array for 'brp' catalogue", () => { - const catalogue = getCultivationCatalogue("brp") + it("should return a non-empty array for 'brp' catalogue", async () => { + const catalogue = await getCultivationCatalogue("brp") expect(Array.isArray(catalogue)).toBe(true) expect(catalogue.length).toBeGreaterThan(0) }) - it("should check if all items in the brp catalogue have the correct source", () => { - const catalogue = getCultivationCatalogue("brp") + it("should check if all items in the brp catalogue have the correct source", async () => { + const catalogue = await getCultivationCatalogue("brp") for (const item of catalogue) { expect(item.b_lu_source).toBe("brp") } }) - it("should check if all items in the brp catalogue have the correct b_lu_harvestable values", () => { - const catalogue = getCultivationCatalogue("brp") + it("should check if all items in the brp catalogue have the correct b_lu_harvestable values", async () => { + const catalogue = await getCultivationCatalogue("brp") for (const item of catalogue) { expect(["once", "multiple", "none"]).toContain( item.b_lu_harvestable, @@ -39,8 +39,8 @@ describe("getCultivationCatalogue", () => { }) describe("getCatalogueBrp", () => { - it("should return an array of CatalogueCultivationItem", () => { - const catalogue = getCatalogueBrp() + it("should return an array of CatalogueCultivationItem", async () => { + const catalogue = await getCatalogueBrp() expect(Array.isArray(catalogue)).toBe(true) for (const item of catalogue) { expect(typeof item).toBe("object") @@ -55,8 +55,8 @@ describe("getCatalogueBrp", () => { } }) - it("should return at least one item", () => { - const catalogue = getCatalogueBrp() + it("should return at least one item", async () => { + const catalogue = await getCatalogueBrp() expect(catalogue.length).toBeGreaterThan(0) }) }) diff --git a/fdm-data/src/cultivations/index.ts b/fdm-data/src/cultivations/index.ts index ff15bbe08..e76b00650 100644 --- a/fdm-data/src/cultivations/index.ts +++ b/fdm-data/src/cultivations/index.ts @@ -20,12 +20,12 @@ import type { CatalogueCultivation, CatalogueCultivationName } from "./d" * console.log(brpCatalogue); * ``` */ -export function getCultivationCatalogue( +export async function getCultivationCatalogue( catalogueName: CatalogueCultivationName, -): CatalogueCultivation { +): Promise { // Get the specified catalogue if (catalogueName === "brp") { - return getCatalogueBrp() + return await getCatalogueBrp() } throw new Error(`catalogue ${catalogueName} is not recognized`) diff --git a/fdm-data/src/fertilizers/catalogues/srm.ts b/fdm-data/src/fertilizers/catalogues/srm.ts index cbf6d4e93..701bf6751 100644 --- a/fdm-data/src/fertilizers/catalogues/srm.ts +++ b/fdm-data/src/fertilizers/catalogues/srm.ts @@ -12,8 +12,8 @@ import srm from "./srm.json" * @returns An array of fertilizer catalogue entries conforming to the * `CatalogueFertilizer` type. */ -export function getCatalogueSrm(): CatalogueFertilizer { - const catalogueSrm = srm.map((fertilizer) => { +export async function getCatalogueSrm(): Promise { + const catalogueSrmPromises = srm.map(async (fertilizer) => { const item: CatalogueFertilizerItem = { p_source: "srm", p_id_catalogue: fertilizer.p_id_catalogue, @@ -73,10 +73,11 @@ export function getCatalogueSrm(): CatalogueFertilizer { } // Hash the item - item.hash = hashFertilizer(item) + item.hash = await hashFertilizer(item) return item }) + const catalogueSrm = await Promise.all(catalogueSrmPromises) return catalogueSrm } diff --git a/fdm-data/src/fertilizers/hash.test.ts b/fdm-data/src/fertilizers/hash.test.ts index 2b4512df2..941442512 100644 --- a/fdm-data/src/fertilizers/hash.test.ts +++ b/fdm-data/src/fertilizers/hash.test.ts @@ -59,7 +59,7 @@ describe("hashFertilizer", () => { expect(hash).toBeDefined() expect(typeof hash).toBe("string") expect(hash.length).toBeGreaterThan(0) - expect(hash).toBe("188352ef") + expect(hash).toBe("3852e767") }) it("should generate different hashes for different fertilizer items", async () => { diff --git a/fdm-data/src/fertilizers/index.test.ts b/fdm-data/src/fertilizers/index.test.ts index 45b75f58f..041b57547 100644 --- a/fdm-data/src/fertilizers/index.test.ts +++ b/fdm-data/src/fertilizers/index.test.ts @@ -3,36 +3,37 @@ import { getFertilizersCatalogue } from "./index" import { getCatalogueSrm } from "./catalogues/srm" describe("getFertilizersCatalogue", () => { - it("should return the SRM catalogue when catalogueName is 'srm'", () => { - const expectedCatalogue = getCatalogueSrm() - const actualCatalogue = getFertilizersCatalogue("srm") + it("should return the SRM catalogue when catalogueName is 'srm'", async () => { + const expectedCatalogue = await getCatalogueSrm() + const actualCatalogue = await getFertilizersCatalogue("srm") expect(actualCatalogue).toEqual(expectedCatalogue) }) - it("should throw an error when an invalid catalogueName is provided", () => { - expect(() => getFertilizersCatalogue("invalid-catalogue")).toThrowError( - "catalogue invalid-catalogue is not recognized", - ) + it("should throw an error when an invalid catalogueName is provided", async () => { + await expect( + getFertilizersCatalogue("invalid-catalogue"), + ).rejects.toThrowError("catalogue invalid-catalogue is not recognized") }) - it("should return a non-empty array for 'srm' catalogue", () => { - const catalogue = getFertilizersCatalogue("srm") + + it("should return a non-empty array for 'srm' catalogue", async () => { + const catalogue = await getFertilizersCatalogue("srm") expect(Array.isArray(catalogue)).toBe(true) expect(catalogue.length).toBeGreaterThan(0) }) - it("should check if all items in the srm catalogue have the correct source", () => { - const catalogue = getFertilizersCatalogue("srm") + it("should check if all items in the srm catalogue have the correct source", async () => { + const catalogue = await getFertilizersCatalogue("srm") for (const item of catalogue) { expect(item.p_source).toBe("srm") } }) }) -describe("getCatalogueSrm", () => { +describe("getCatalogueSrm", async () => { const originalSrm = require("./catalogues/srm.json") - it("should return an array of CatalogueFertilizerItem", () => { - const catalogue = getCatalogueSrm() + it("should return an array of CatalogueFertilizerItem", async () => { + const catalogue = await getCatalogueSrm() expect(Array.isArray(catalogue)).toBe(true) for (const item of catalogue) { expect(typeof item).toBe("object") @@ -88,8 +89,8 @@ describe("getCatalogueSrm", () => { } }) - it("should return at least one item", () => { - const catalogue = getCatalogueSrm() + it("should return at least one item", async () => { + const catalogue = await getCatalogueSrm() expect(catalogue.length).toBeGreaterThan(0) }) }) diff --git a/fdm-data/src/fertilizers/index.ts b/fdm-data/src/fertilizers/index.ts index 97381bec5..b0e2f156a 100644 --- a/fdm-data/src/fertilizers/index.ts +++ b/fdm-data/src/fertilizers/index.ts @@ -20,12 +20,12 @@ import type { CatalogueFertilizer, CatalogueFertilizerName } from "./d" * console.log(srmCatalogue); * ``` */ -export function getFertilizersCatalogue( +export async function getFertilizersCatalogue( catalogueName: CatalogueFertilizerName, -): CatalogueFertilizer { +): Promise { // Get the specified catalogue if (catalogueName === "srm") { - return getCatalogueSrm() + return await getCatalogueSrm() } throw new Error(`catalogue ${catalogueName} is not recognized`) } From 657e10f13f7a0881fcecfe24e402c8e17e5df89c Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:13:40 +0200 Subject: [PATCH 51/55] Restore unit test to original --- fdm-core/src/catalogues.test.ts | 871 ++++++++++++++++++-------------- 1 file changed, 481 insertions(+), 390 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 77d5a6fe7..8e224a339 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, inject, it } from "vitest" +import { describe, it, expect, beforeEach, inject } from "vitest" import { createFdmServer } from "./fdm-server" import * as schema from "./db/schema" import { @@ -17,11 +17,10 @@ import { eq, isNotNull } from "drizzle-orm" import { getCultivationCatalogue, getFertilizersCatalogue, - hashFertilizer, } from "@svenvw/fdm-data" import { addFarm } from "./farm" -describe("Catalogues - Unit Tests", () => { +describe("Catalogues", () => { let fdm: FdmType let principal_id: string let b_id_farm: string @@ -50,117 +49,119 @@ describe("Catalogues - Unit Tests", () => { ) }) - describe("getEnabledFertilizerCatalogues", () => { - it("should return an array of enabled fertilizer catalogue sources", async () => { - // Arrange - await enableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - "source1", - ) + describe("Fertilizer Catalogues", () => { + it("should enable and check fertilizer catalogue", async () => { + const p_source = "test_source" + + // Initially should not be enabled + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ), + ).toBe(false) + + // Enable the catalogue await enableFertilizerCatalogue( fdm, principal_id, b_id_farm, - "source2", + p_source, ) - // Act - const result = await getEnabledFertilizerCatalogues( + // Should now be enabled + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ), + ).toBe(true) + + // Should appear in enabled catalogues list + const enabledCatalogues = await getEnabledFertilizerCatalogues( fdm, principal_id, b_id_farm, ) - - // Assert - expect(result).toEqual( - expect.arrayContaining(["source1", "source2"]), - ) - expect(result.length).toBe(2) + expect(enabledCatalogues).toContain(p_source) }) - it("should return an empty array if no catalogues are enabled", async () => { - // Arrange + it("should disable fertilizer catalogue", async () => { + const p_source = "test_source" - // Act - const result = await getEnabledFertilizerCatalogues( + // Enable the catalogue + await enableFertilizerCatalogue( fdm, principal_id, b_id_farm, + p_source, ) - - // Assert - expect(result).toEqual([]) - }) - - it("should throw an error if permission check fails", async () => { - // Arrange - const invalidPrincipalId = "invalid_principal" - - // Act & Assert - await expect( - getEnabledFertilizerCatalogues( + expect( + await isFertilizerCatalogueEnabled( fdm, - invalidPrincipalId, + principal_id, b_id_farm, + p_source, ), - ).rejects.toThrowError( - "Principal does not have permission to perform this action", - ) - }) - }) + ).toBe(true) - describe("getEnabledCultivationCatalogues", () => { - it("should return an array of enabled cultivation catalogue sources", async () => { - // Arrange - await enableCultivationCatalogue( - fdm, - principal_id, - b_id_farm, - "source1", - ) - await enableCultivationCatalogue( + // Disable the catalogue + await disableFertilizerCatalogue( fdm, principal_id, b_id_farm, - "source2", + p_source, ) - // Act - const result = await getEnabledCultivationCatalogues( + // Should no longer be enabled + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ), + ).toBe(false) + + // Should not appear in enabled catalogues list + const enabledCatalogues = await getEnabledFertilizerCatalogues( fdm, principal_id, b_id_farm, ) - - // Assert - expect(result).toEqual( - expect.arrayContaining(["source1", "source2"]), - ) - expect(result.length).toBe(2) + expect(enabledCatalogues).not.toContain(p_source) }) - it("should return an empty array if no catalogues are enabled", async () => { - // Arrange - // Act - const result = await getEnabledCultivationCatalogues( + it("should handle multiple fertilizer catalogues", async () => { + const sources = ["source1", "source2", "source3"] + + // Enable multiple catalogues + for (const source of sources) { + await enableFertilizerCatalogue( + fdm, + principal_id, + b_id_farm, + source, + ) + } + + // Check all are enabled + const enabledCatalogues = await getEnabledFertilizerCatalogues( fdm, principal_id, b_id_farm, ) - - // Assert - expect(result).toEqual([]) + expect(enabledCatalogues).toHaveLength(sources.length) + expect(enabledCatalogues).toEqual(expect.arrayContaining(sources)) }) - - it("should throw an error if permission check fails", async () => { - // Arrange + it("should throw an error when permission check fails for getEnabledFertilizerCatalogues", async () => { const invalidPrincipalId = "invalid_principal" - - // Act & Assert await expect( - getEnabledCultivationCatalogues( + getEnabledFertilizerCatalogues( fdm, invalidPrincipalId, b_id_farm, @@ -169,31 +170,8 @@ describe("Catalogues - Unit Tests", () => { "Principal does not have permission to perform this action", ) }) - }) - describe("enableFertilizerCatalogue", () => { - it("should enable a fertilizer catalogue", async () => { - // Arrange - const p_source = "test_source" - - // Act - await enableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - - // Assert - const isEnabled = await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ) - expect(isEnabled).toBe(true) - }) - it("should throw an error when permission check fails", async () => { + it("should throw an error when permission check fails for enableFertilizerCatalogue", async () => { const invalidPrincipalId = "invalid_principal" await expect( enableFertilizerCatalogue( @@ -206,34 +184,11 @@ describe("Catalogues - Unit Tests", () => { "Principal does not have permission to perform this action", ) }) - }) - - describe("enableCultivationCatalogue", () => { - it("should enable a cultivation catalogue", async () => { - // Arrange - const b_lu_source = "test_source" - // Act - await enableCultivationCatalogue( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ) - - // Assert - const isEnabled = await isCultivationCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - b_lu_source, - ) - expect(isEnabled).toBe(true) - }) - it("should throw an error when permission check fails", async () => { + it("should throw an error when permission check fails for disableFertilizerCatalogue", async () => { const invalidPrincipalId = "invalid_principal" await expect( - enableCultivationCatalogue( + disableFertilizerCatalogue( fdm, invalidPrincipalId, b_id_farm, @@ -243,40 +198,11 @@ describe("Catalogues - Unit Tests", () => { "Principal does not have permission to perform this action", ) }) - }) - describe("disableFertilizerCatalogue", () => { - it("should disable a fertilizer catalogue", async () => { - // Arrange - const p_source = "test_source" - await enableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - - // Act - await disableFertilizerCatalogue( - fdm, - principal_id, - b_id_farm, - p_source, - ) - - // Assert - const isEnabled = await isFertilizerCatalogueEnabled( - fdm, - principal_id, - b_id_farm, - p_source, - ) - expect(isEnabled).toBe(false) - }) - it("should throw an error when permission check fails", async () => { + it("should throw an error when permission check fails for isFertilizerCatalogueEnabled", async () => { const invalidPrincipalId = "invalid_principal" await expect( - disableFertilizerCatalogue( + isFertilizerCatalogueEnabled( fdm, invalidPrincipalId, b_id_farm, @@ -286,156 +212,291 @@ describe("Catalogues - Unit Tests", () => { "Principal does not have permission to perform this action", ) }) - }) - describe("disableCultivationCatalogue", () => { - it("should disable a cultivation catalogue", async () => { - // Arrange - const b_lu_source = "test_source" - await enableCultivationCatalogue( + it("should handle edge cases for disableFertilizerCatalogue", async () => { + const p_source = "test_disable_source" + + // Case 1: Disabling a fertilizer catalogue that isn't enabled should not throw errors + // This tests the compound condition in the where clause + await disableFertilizerCatalogue( fdm, principal_id, b_id_farm, - b_lu_source, + p_source, ) + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ), + ).toBe(false) - // Act - await disableCultivationCatalogue( + // Case 2: Enable and then disable with same farm but different source + await enableFertilizerCatalogue( fdm, principal_id, b_id_farm, - b_lu_source, + p_source, ) + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ), + ).toBe(true) - // Assert - const isEnabled = await isCultivationCatalogueEnabled( + // Disable with wrong source - should not disable the original + await disableFertilizerCatalogue( fdm, principal_id, b_id_farm, - b_lu_source, + "wrong_source", ) - expect(isEnabled).toBe(false) - }) - it("should throw an error when permission check fails", async () => { - const invalidPrincipalId = "invalid_principal" - await expect( - disableCultivationCatalogue( + expect( + await isFertilizerCatalogueEnabled( fdm, - invalidPrincipalId, + principal_id, b_id_farm, - "custom", + p_source, ), - ).rejects.toThrowError( - "Principal does not have permission to perform this action", + ).toBe(true) + + // Case 3: Test with different farm + // Create a second test farm + const secondFarmName = "Second Test Farm" + const secondFarmBusinessId = "654321" + const secondFarmAddress = "456 Farm Lane" + const secondFarmPostalCode = "54321" + const second_b_id_farm = await addFarm( + fdm, + principal_id, + secondFarmName, + secondFarmBusinessId, + secondFarmAddress, + secondFarmPostalCode, ) - }) - }) - describe("isFertilizerCatalogueEnabled", () => { - it("should return true if fertilizer catalogue is enabled", async () => { - // Arrange - const p_source = "test_source" + // Enable for second farm await enableFertilizerCatalogue( fdm, principal_id, - b_id_farm, + second_b_id_farm, p_source, ) + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + second_b_id_farm, + p_source, + ), + ).toBe(true) + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ), + ).toBe(true) - // Act - const isEnabled = await isFertilizerCatalogueEnabled( + // Disable for first farm should not affect second farm + await disableFertilizerCatalogue( fdm, principal_id, b_id_farm, p_source, ) + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + p_source, + ), + ).toBe(false) + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + second_b_id_farm, + p_source, + ), + ).toBe(true) - // Assert - expect(isEnabled).toBe(true) - }) - - it("should return false if fertilizer catalogue is disabled", async () => { - // Arrange - const p_source = "test_source" + // Disable for second farm await disableFertilizerCatalogue( fdm, principal_id, - b_id_farm, + second_b_id_farm, p_source, ) + expect( + await isFertilizerCatalogueEnabled( + fdm, + principal_id, + second_b_id_farm, + p_source, + ), + ).toBe(false) + }) + }) - // Act - const isEnabled = await isFertilizerCatalogueEnabled( + describe("Cultivation Catalogues", () => { + it("should enable and check cultivation catalogue", async () => { + const b_lu_source = "test_source" + + // Initially should not be enabled + expect( + await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ), + ).toBe(false) + + // Enable the catalogue + await enableCultivationCatalogue( fdm, principal_id, b_id_farm, - p_source, + b_lu_source, ) - // Assert - expect(isEnabled).toBe(false) - }) - it("should throw an error when permission check fails", async () => { - const invalidPrincipalId = "invalid_principal" - await expect( - isFertilizerCatalogueEnabled( + // Should now be enabled + expect( + await isCultivationCatalogueEnabled( fdm, - invalidPrincipalId, + principal_id, b_id_farm, - "custom", + b_lu_source, ), - ).rejects.toThrowError( - "Principal does not have permission to perform this action", + ).toBe(true) + + // Should appear in enabled catalogues list + const enabledCatalogues = await getEnabledCultivationCatalogues( + fdm, + principal_id, + b_id_farm, ) + expect(enabledCatalogues).toContain(b_lu_source) }) - }) - describe("isCultivationCatalogueEnabled", () => { - it("should return true if cultivation catalogue is enabled", async () => { - // Arrange + it("should disable cultivation catalogue", async () => { const b_lu_source = "test_source" + + // Enable the catalogue await enableCultivationCatalogue( fdm, principal_id, b_id_farm, b_lu_source, ) + expect( + await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ), + ).toBe(true) - // Act - const isEnabled = await isCultivationCatalogueEnabled( + // Disable the catalogue + await disableCultivationCatalogue( fdm, principal_id, b_id_farm, b_lu_source, ) - // Assert - expect(isEnabled).toBe(true) - }) + // Should no longer be enabled + expect( + await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ), + ).toBe(false) - it("should return false if cultivation catalogue is disabled", async () => { - // Arrange - const b_lu_source = "test_source" - await disableCultivationCatalogue( + // Should not appear in enabled catalogues list + const enabledCatalogues = await getEnabledCultivationCatalogues( fdm, principal_id, b_id_farm, - b_lu_source, ) + expect(enabledCatalogues).not.toContain(b_lu_source) + }) + + it("should handle multiple cultivation catalogues", async () => { + const sources = ["source1", "source2", "source3"] - // Act - const isEnabled = await isCultivationCatalogueEnabled( + // Enable multiple catalogues + for (const source of sources) { + await enableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + source, + ) + } + + // Check all are enabled + const enabledCatalogues = await getEnabledCultivationCatalogues( fdm, principal_id, b_id_farm, - b_lu_source, ) + expect(enabledCatalogues).toHaveLength(sources.length) + expect(enabledCatalogues).toEqual(expect.arrayContaining(sources)) + }) + it("should throw an error when permission check fails", async () => { + const invalidPrincipalId = "invalid_principal" + await expect( + getEnabledCultivationCatalogues( + fdm, + invalidPrincipalId, + b_id_farm, + ), + ).rejects.toThrowError( + "Principal does not have permission to perform this action", + ) + }) - // Assert - expect(isEnabled).toBe(false) + it("should throw an error when permission check fails for enableCultivationCatalogue", async () => { + const invalidPrincipalId = "invalid_principal" + await expect( + enableCultivationCatalogue( + fdm, + invalidPrincipalId, + b_id_farm, + "custom", + ), + ).rejects.toThrowError( + "Principal does not have permission to perform this action", + ) }) - it("should throw an error when permission check fails", async () => { + it("should throw an error when permission check fails for disableCultivationCatalogue", async () => { + const invalidPrincipalId = "invalid_principal" + await expect( + disableCultivationCatalogue( + fdm, + invalidPrincipalId, + b_id_farm, + "custom", + ), + ).rejects.toThrowError( + "Principal does not have permission to perform this action", + ) + }) + + it("should throw an error when permission check fails for isCultivationCatalogueEnabled", async () => { const invalidPrincipalId = "invalid_principal" await expect( isCultivationCatalogueEnabled( @@ -448,182 +509,212 @@ describe("Catalogues - Unit Tests", () => { "Principal does not have permission to perform this action", ) }) - }) - describe("syncCatalogues", () => { - it("should sync catalogues", async () => { - await syncCatalogues(fdm) + it("should include context in the error when database query fails", async () => { + const invalidFdm = { + ...fdm, + select: () => { + throw new Error("Database error") + }, + } + + try { + await getEnabledCultivationCatalogues( + invalidFdm, + principal_id, + b_id_farm, + ) + // Should not reach here + expect(true).toBe(false) + } catch (error) { + expect(error.message).toContain( + "Exception for getEnabledCultivationCatalogues", + ) + expect(error.context).toBeDefined() + expect(error.context.principal_id).toBe(principal_id) + expect(error.context.b_id_farm).toBe(b_id_farm) + } + }) - // Check if catalogue data is similiar to fdm-data - const srmCatalogue = await fdm - .select() - .from(schema.fertilizersCatalogue) + it("should handle errors when disabling cultivation catalogue", async () => { + const b_lu_source = "test_source" + const invalidPrincipal = "invalid_principal" // Principal without permissions - const srmCatalogueOriginal = getFertilizersCatalogue("srm") - expect(srmCatalogue.length).toBeGreaterThan( - srmCatalogueOriginal.length, + // Enable the catalogue first with valid principal + await enableCultivationCatalogue( + fdm, + principal_id, + b_id_farm, + b_lu_source, ) - const brpCatalogue = await fdm - .select() - .from(schema.cultivationsCatalogue) - expect(brpCatalogue.length).toBeGreaterThan(0) + // Attempt to disable with invalid principal should throw an error + await expect( + disableCultivationCatalogue( + fdm, + invalidPrincipal, + b_id_farm, + b_lu_source, + ), + ).rejects.toThrow() - const brpCatalogueOriginal = getCultivationCatalogue("brp") - expect(brpCatalogue.length).toBeGreaterThan( - brpCatalogueOriginal.length, - ) + // The catalogue should still be enabled + expect( + await isCultivationCatalogueEnabled( + fdm, + principal_id, + b_id_farm, + b_lu_source, + ), + ).toBe(true) }) + }) +}) - it("should update fertilizer catalogue", async () => { - await syncCatalogues(fdm) - - // Update a catalogue item - const item = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where(isNotNull(schema.fertilizersCatalogue.hash)) - .limit(1) - expect(item[0].p_id_catalogue).toBeDefined() - const originalHash = item[0].hash - - await fdm - .update(schema.fertilizersCatalogue) - .set({ hash: "Updated hash" }) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) +describe("Catalogues syncing", () => { + let fdm: FdmType - await syncCatalogues(fdm) - - const itemSynced = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) + beforeEach(async () => { + const host = inject("host") + const port = inject("port") + const user = inject("user") + const password = inject("password") + const database = inject("database") + fdm = createFdmServer(host, port, user, password, database) + }) - expect(itemSynced[0].p_id_catalogue).toBeDefined() - expect(itemSynced[0].hash).not.toBe("Updated hash") - expect(itemSynced[0].hash).toBe(originalHash) - }) - it("should update cultivation catalogue", async () => { - // ... (rest of the code is unchanged) - }) + it("should sync catalogues", async () => { + await syncCatalogues(fdm) - it("should update fertilizer catalogue when hash is null", async () => { - // Arrange - await syncCatalogues(fdm) - - // Select a fertilizer catalogue item to modify - const item = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where(isNotNull(schema.fertilizersCatalogue.hash)) - .limit(1) - - expect(item[0].p_id_catalogue).toBeDefined() - const originalHash = item[0].hash - - // Update the hash to null - await fdm - .update(schema.fertilizersCatalogue) - .set({ hash: null }) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) + // Check if catalogue data is similiar to fdm-data + const srmCatalogue = await fdm + .select() + .from(schema.fertilizersCatalogue) - // Act - await syncCatalogues(fdm) - - // Assert - const updatedItem = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) + const srmCatalogueOriginal = await getFertilizersCatalogue("srm") + expect(srmCatalogue.length).toBeGreaterThan(srmCatalogueOriginal.length) - // Hash should be updated and not null anymore - expect(updatedItem[0].hash).not.toBeNull() - // Hash should match the original hash (recalculated by syncCatalogues) - expect(updatedItem[0].hash).toBe(originalHash) - }) + const brpCatalogue = await fdm + .select() + .from(schema.cultivationsCatalogue) + expect(brpCatalogue.length).toBeGreaterThan(0) - it("should update fertilizer catalogue when hash is undefined", async () => { - // Arrange - await syncCatalogues(fdm) - - // Select a fertilizer catalogue item to modify - const item = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where(isNotNull(schema.fertilizersCatalogue.hash)) - .limit(1) - - expect(item[0].p_id_catalogue).toBeDefined() - const originalHash = item[0].hash - - // Act: - // First remove the hash (set to null as undefined isn't directly supported in SQL) - await fdm - .update(schema.fertilizersCatalogue) - .set({ hash: null }) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) + const brpCatalogueOriginal = await getCultivationCatalogue("brp") + expect(brpCatalogue.length).toBeGreaterThan(brpCatalogueOriginal.length) + }) - // Then sync to update the hash - await syncCatalogues(fdm) - - // Assert - const updatedItem = await fdm - .select({ - p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, - hash: schema.fertilizersCatalogue.hash, - }) - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - item[0].p_id_catalogue, - ), - ) + it("should update fertilizer catalogue", async () => { + await syncCatalogues(fdm) + + // Update a catalogue item + const item = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where(isNotNull(schema.fertilizersCatalogue.hash)) + .limit(1) + expect(item[0].p_id_catalogue).toBeDefined() + + await fdm + .update(schema.fertilizersCatalogue) + .set({ hash: "Updated hash" }) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) - // Hash should be updated and not null anymore - expect(updatedItem[0].hash).not.toBeNull() - // Hash should match the original hash - expect(updatedItem[0].hash).toBe(originalHash) - }) + const itemUpdated = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + expect(itemUpdated[0].p_id_catalogue).toBeDefined() + expect(itemUpdated[0].hash).toBe("Updated hash") + + await syncCatalogues(fdm) + + const itemSynced = await fdm + .select({ + p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, + hash: schema.fertilizersCatalogue.hash, + }) + .from(schema.fertilizersCatalogue) + .where( + eq( + schema.fertilizersCatalogue.p_id_catalogue, + item[0].p_id_catalogue, + ), + ) + + expect(itemSynced[0].p_id_catalogue).toBeDefined() + expect(itemSynced[0].hash).toBe(item[0].hash) + }) + + it("should update cultivation catalogue", async () => { + await syncCatalogues(fdm) + + // Update a catalogue item + const item = await fdm + .select({ + b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, + hash: schema.cultivationsCatalogue.hash, + }) + .from(schema.cultivationsCatalogue) + .where(isNotNull(schema.cultivationsCatalogue.hash)) + .limit(1) + expect(item[0].b_lu_catalogue).toBeDefined() + + await fdm + .update(schema.cultivationsCatalogue) + .set({ hash: "Updated hash" }) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + + const itemUpdated = await fdm + .select({ + b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, + hash: schema.cultivationsCatalogue.hash, + }) + .from(schema.cultivationsCatalogue) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + expect(itemUpdated[0].b_lu_catalogue).toBeDefined() + expect(itemUpdated[0].hash).toBe("Updated hash") + + await syncCatalogues(fdm) + + const itemSynced = await fdm + .select({ + b_lu_catalogue: schema.cultivationsCatalogue.b_lu_catalogue, + hash: schema.cultivationsCatalogue.hash, + }) + .from(schema.cultivationsCatalogue) + .where( + eq( + schema.cultivationsCatalogue.b_lu_catalogue, + item[0].b_lu_catalogue, + ), + ) + expect(itemSynced[0].b_lu_catalogue).toBeDefined() + expect(itemSynced[0].hash).toBe(item[0].hash) }) }) From 9051ddc3a4032cb9ec03aa2bcf4a80217bad1e54 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:52:23 +0200 Subject: [PATCH 52/55] fix? --- fdm-core/src/catalogues.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 8e224a339..61dd52208 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -614,8 +614,10 @@ describe("Catalogues syncing", () => { }) .from(schema.fertilizersCatalogue) .where(isNotNull(schema.fertilizersCatalogue.hash)) + .orderBy(schema.fertilizersCatalogue.p_id_catalogue) .limit(1) expect(item[0].p_id_catalogue).toBeDefined() + console.log(`Original hash: ${item[0].hash}`) await fdm .update(schema.fertilizersCatalogue) @@ -658,6 +660,7 @@ describe("Catalogues syncing", () => { ) expect(itemSynced[0].p_id_catalogue).toBeDefined() + console.log(`Synced hash: ${itemSynced[0].hash}`) expect(itemSynced[0].hash).toBe(item[0].hash) }) @@ -672,8 +675,10 @@ describe("Catalogues syncing", () => { }) .from(schema.cultivationsCatalogue) .where(isNotNull(schema.cultivationsCatalogue.hash)) + .orderBy(schema.cultivationsCatalogue.b_lu_catalogue) .limit(1) expect(item[0].b_lu_catalogue).toBeDefined() + console.log(`Original hash: ${item[0].hash}`) await fdm .update(schema.cultivationsCatalogue) @@ -715,6 +720,7 @@ describe("Catalogues syncing", () => { ), ) expect(itemSynced[0].b_lu_catalogue).toBeDefined() + console.log(`Synced hash: ${itemSynced[0].hash}`) expect(itemSynced[0].hash).toBe(item[0].hash) }) }) From 87d98f7997f1992af0d96e1d6cc167ecb8d95fe7 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:56:46 +0200 Subject: [PATCH 53/55] Remove not needed logs --- fdm-core/src/catalogues.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 61dd52208..d36ed5e42 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -617,7 +617,6 @@ describe("Catalogues syncing", () => { .orderBy(schema.fertilizersCatalogue.p_id_catalogue) .limit(1) expect(item[0].p_id_catalogue).toBeDefined() - console.log(`Original hash: ${item[0].hash}`) await fdm .update(schema.fertilizersCatalogue) @@ -660,7 +659,6 @@ describe("Catalogues syncing", () => { ) expect(itemSynced[0].p_id_catalogue).toBeDefined() - console.log(`Synced hash: ${itemSynced[0].hash}`) expect(itemSynced[0].hash).toBe(item[0].hash) }) @@ -678,7 +676,6 @@ describe("Catalogues syncing", () => { .orderBy(schema.cultivationsCatalogue.b_lu_catalogue) .limit(1) expect(item[0].b_lu_catalogue).toBeDefined() - console.log(`Original hash: ${item[0].hash}`) await fdm .update(schema.cultivationsCatalogue) @@ -720,7 +717,6 @@ describe("Catalogues syncing", () => { ), ) expect(itemSynced[0].b_lu_catalogue).toBeDefined() - console.log(`Synced hash: ${itemSynced[0].hash}`) expect(itemSynced[0].hash).toBe(item[0].hash) }) }) From f505c0ef2ada4a84a991e6fd47d208e9d35dd151 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:57:45 +0200 Subject: [PATCH 54/55] Fix error propagation --- fdm-core/src/catalogues.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index b26bb3dc0..e628c642f 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -416,7 +416,7 @@ async function syncFertilizerCatalogue(fdm: FdmType) { } } } catch (error) { - handleError(error, "Exception for syncFertilizerCatalogue") + throw handleError(error, "Exception for syncFertilizerCatalogue") } }) } @@ -464,7 +464,7 @@ async function syncCultivationCatalogue(fdm: FdmType) { } } } catch (error) { - handleError(error, "Exception for syncCultivationCatalogue") + throw handleError(error, "Exception for syncCultivationCatalogue") } }) } From 1240a14fb8069888b4faa9df51b2670ac660ebb4 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:01:16 +0200 Subject: [PATCH 55/55] Improve documentation --- fdm-data/src/cultivations/index.ts | 3 ++- fdm-data/src/fertilizers/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/fdm-data/src/cultivations/index.ts b/fdm-data/src/cultivations/index.ts index e76b00650..49ed8503f 100644 --- a/fdm-data/src/cultivations/index.ts +++ b/fdm-data/src/cultivations/index.ts @@ -11,12 +11,13 @@ import type { CatalogueCultivation, CatalogueCultivationName } from "./d" * Currently supported names are: "brp". * @returns An array of `CatalogueCultivationItem` objects representing the * requested cultivation catalogue. + * @returns A Promise that resolves to an array of `CatalogueCultivationItem` objects. * @throws {Error} Throws an error if the provided `catalogueName` is not * recognized or supported. * * @example * ```typescript - * const brpCatalogue = getCultivationCatalogue("brp"); + * const brpCatalogue = await getCultivationCatalogue("brp"); * console.log(brpCatalogue); * ``` */ diff --git a/fdm-data/src/fertilizers/index.ts b/fdm-data/src/fertilizers/index.ts index b0e2f156a..e84a4970d 100644 --- a/fdm-data/src/fertilizers/index.ts +++ b/fdm-data/src/fertilizers/index.ts @@ -11,12 +11,13 @@ import type { CatalogueFertilizer, CatalogueFertilizerName } from "./d" * Currently supported names are: "srm". * @returns An array of `CatalogueFertilizerItem` objects representing the * requested fertilizer catalogue. + * @returns A Promise that resolves to an array of `CatalogueFertilizerItem` objects. * @throws {Error} Throws an error if the provided `catalogueName` is not * recognized or supported. * * @example * ```typescript - * const srmCatalogue = getFertilizersCatalogue("srm"); + * const srmCatalogue = await getFertilizersCatalogue("srm"); * console.log(srmCatalogue); * ``` */