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/.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/.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/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/.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/.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/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/.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/.changeset/red-oranges-drop.md b/.changeset/red-oranges-drop.md new file mode 100644 index 000000000..6489b1202 --- /dev/null +++ b/.changeset/red-oranges-drop.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": minor +--- + +Add to the output of `getFertilizer` and `getFertilizers` the values for `p_type_*` and `p_source` 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 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/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 new file mode 100644 index 000000000..6cc586bea --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/columns.tsx @@ -0,0 +1,118 @@ +import type { ColumnDef } from "@tanstack/react-table" +import { ArrowRight, Pencil, SquareArrowOutUpRight } from "lucide-react" +import { DataTableColumnHeader } from "./column-header" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { NavLink } from "react-router-dom" +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_type_manure?: boolean + 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[] = [ + // { + // accessorKey: "p_id", + // header: "ID", + // }, + { + accessorKey: "p_name_nl", + header: "Naam", + }, + { + accessorKey: "p_n_rt", + header: ({ column }) => { + return + }, + }, + { + accessorKey: "p_p_rt", + header: ({ column }) => { + 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: "Details", + cell: ({ row }) => { + const fertilizer = row.original + + return ( + + + + + + + + {`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 new file mode 100644 index 000000000..a20013a81 --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/form.tsx @@ -0,0 +1,482 @@ +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 { + 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 "@/components/custom/loadingspinner" +import type { Fertilizer } from "./columns" + +export function FertilizerForm({ + fertilizer, + form, + editable, + farm, +}: { + fertilizer: Fertilizer + form: UseFormReturn< + z.infer, + ZodType, + undefined + > + editable: boolean + farm: { + b_id_farm: string + b_name_farm: string + } +}) { + 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/components/custom/fertilizer/formschema.tsx b/fdm-app/app/components/custom/fertilizer/formschema.tsx new file mode 100644 index 000000000..7e5c3bf42 --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/formschema.tsx @@ -0,0 +1,102 @@ +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", + }) + .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", + }), +}) 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..178b02bbb --- /dev/null +++ b/fdm-app/app/components/custom/fertilizer/table.tsx @@ -0,0 +1,132 @@ +import { + type ColumnDef, + type ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + type SortingState, + useReactTable, +} from "@tanstack/react-table" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} 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[] + data: TData[] +} + +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 + .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. + + + )} + +
+
+
+ ) +} diff --git a/fdm-app/app/components/custom/sidebar-app.tsx b/fdm-app/app/components/custom/sidebar-app.tsx index 61a0d5e8f..f56bd40c0 100644 --- a/fdm-app/app/components/custom/sidebar-app.tsx +++ b/fdm-app/app/components/custom/sidebar-app.tsx @@ -37,6 +37,7 @@ import { Scale, Send, Settings, + Shapes, Sparkles, Sprout, Square, @@ -98,6 +99,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 @@ -217,14 +224,21 @@ export function SidebarApp(props: SideBarAppType) { */} - {/* + - - - Meststoffen - + {fertilizersLink ? ( + + + Meststoffen + + ) : ( + + + Meststoffen + + )} - */} + {/* 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.$p_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx new file mode 100644 index 000000000..0bf7b0928 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx @@ -0,0 +1,273 @@ +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 { + getFarm, + getFarms, + getFertilizer, + getFertilizers, +} from "@svenvw/fdm-core" +import { updateFertilizerFromCatalogue } from "@svenvw/fdm-core" +import { useEffect } from "react" +import { + type ActionFunctionArgs, + type LoaderFunctionArgs, + data, + useLoaderData, +} from "react-router" +import { useRemixForm } from "remix-hook-form" +import { dataWithSuccess } 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 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 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 + 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, + fertilizerOptions: fertilizerOptions, + fertilizer: fertilizer, + editable: editable, + } + } 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 + + 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_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 + } + + const fertilizer = await getFertilizer(fdm, p_id) + const p_id_catalogue = fertilizer.p_id_catalogue + + 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, + }, + ) + + return dataWithSuccess( + { result: "Data saved successfully" }, + { message: "Meststof is bijgewerkt! 🎉" }, + ) + } catch (error) { + throw handleActionError(error) + } +} 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..cc27a759c --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers._index.tsx @@ -0,0 +1,106 @@ +import { FarmHeader } from "@/components/custom/farm/farm-header" +import { FarmTitle } from "@/components/custom/farm/farm-title" +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, getFertilizers } from "@svenvw/fdm-core" +import { type LoaderFunctionArgs, 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, + } + }) + + // Get the available fertilizers + const fertilizers: Fertilizer[] = await getFertilizers( + fdm, + session.principal_id, + b_id_farm, + ) + + // Return user information from loader + return { + farm: farm, + b_id_farm: b_id_farm, + farmOptions: farmOptions, + fertilizers: fertilizers, + } + } 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 ( + + +
+ +
+
+
+ +
+
+
+
+
+ ) +} 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..e503335d4 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.tsx @@ -0,0 +1,283 @@ +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 + + if (!b_id_farm) { + throw new Error("missing: b_id_farm") + } + + 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) + } +} 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, diff --git a/fdm-app/package.json b/fdm-app/package.json index 00f758f18..9943e8ee3 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/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: "", diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index a65e89365..d36ed5e42 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -591,7 +591,7 @@ describe("Catalogues syncing", () => { .select() .from(schema.fertilizersCatalogue) - const srmCatalogueOriginal = getFertilizersCatalogue("srm") + const srmCatalogueOriginal = await getFertilizersCatalogue("srm") expect(srmCatalogue.length).toBeGreaterThan(srmCatalogueOriginal.length) const brpCatalogue = await fdm @@ -599,7 +599,7 @@ describe("Catalogues syncing", () => { .from(schema.cultivationsCatalogue) expect(brpCatalogue.length).toBeGreaterThan(0) - const brpCatalogueOriginal = getCultivationCatalogue("brp") + const brpCatalogueOriginal = await getCultivationCatalogue("brp") expect(brpCatalogue.length).toBeGreaterThan(brpCatalogueOriginal.length) }) @@ -614,6 +614,7 @@ 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() @@ -672,6 +673,7 @@ 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() diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index eeb99e322..e628c642f 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,101 @@ 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() - .from(schema.fertilizersCatalogue) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - srmItem.p_id_catalogue, - ), - ) - .limit(1) + await syncFertilizerCatalogue(fdm) + await syncCultivationCatalogue(fdm) +} - if (existingItem.length === 0) { - await fdm.insert(schema.fertilizersCatalogue).values(srmItem) - console.log( - `Inserted fertilizer catalogue item: ${srmItem.p_id_catalogue}`, - ) - } else { - // Update item if different - if (srmItem.hash && srmItem.hash !== existingItem[0].hash) { - await fdm - .update(schema.fertilizersCatalogue) - .set(srmItem) - .where( - eq( - schema.fertilizersCatalogue.p_id_catalogue, - srmItem.p_id_catalogue, - ), - ) - console.log( - `Updated fertilizer catalogue item: ${srmItem.p_id_catalogue}`, +async function syncFertilizerCatalogue(fdm: FdmType) { + const srmCatalogue = await getFertilizersCatalogue("srm") + await fdm.transaction(async (tx) => { + try { + for (const item of srmCatalogue) { + const hash = await 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) { + throw handleError(error, "Exception for syncFertilizerCatalogue") } + }) +} - // Sync cultivation catalogue (BRP) - const brpCatalogue = getCultivationCatalogue("brp") - for (const brpItem of brpCatalogue) { - const existingItem = await fdm - .select() - .from(schema.cultivationsCatalogue) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - brpItem.b_lu_catalogue, - ), - ) - .limit(1) +async function syncCultivationCatalogue(fdm: FdmType) { + const brpCatalogue = await getCultivationCatalogue("brp") - if (existingItem.length === 0) { - await fdm.insert(schema.cultivationsCatalogue).values(brpItem) - console.log( - `Inserted cultivation catalogue item: ${brpItem.b_lu_catalogue}`, - ) - } else { - // Update item if different - if (brpItem.hash && brpItem.hash !== existingItem[0].hash) { - await fdm - .update(schema.cultivationsCatalogue) - .set(brpItem) - .where( - eq( - schema.cultivationsCatalogue.b_lu_catalogue, - brpItem.b_lu_catalogue, - ), - ) - console.log( - `Updated cultivation catalogue item: ${brpItem.b_lu_catalogue}`, + await fdm.transaction(async (tx) => { + try { + for (const item of brpCatalogue) { + const hash = await 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) { + throw handleError(error, "Exception for syncCultivationCatalogue") } - } catch (err) { - throw handleError(err, "Exception for syncCatalogues") - } + }) } 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/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 43a700274..12473e11f 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(), @@ -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.test.ts b/fdm-core/src/fertilizer.test.ts index 51a383a12..2f050bdbb 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -22,17 +22,19 @@ import { removeFertilizer, removeFertilizerApplication, updateFertilizerApplication, + updateFertilizerFromCatalogue, } from "./fertilizer" import { addField } from "./field" +import { + disableFertilizerCatalogue, + enableFertilizerCatalogue, +} from "./catalogues" import { createId } from "./id" -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 +58,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 +77,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 +141,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 +152,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 +227,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 +315,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 +396,7 @@ describe("Fertilizer Data Model", () => { fdm, principal_id, b_id_farm, - p_source, + b_id_farm, ) const fertilizersWithNoCatalogue = @@ -395,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 @@ -438,60 +736,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..7b46cadd0 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" @@ -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"] @@ -127,15 +130,35 @@ 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"] }, -): 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 = await 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, @@ -238,6 +261,8 @@ 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, p_description: schema.fertilizersCatalogue.p_description, @@ -245,6 +270,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, @@ -272,7 +309,10 @@ 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, }) .from(schema.fertilizers) .leftJoin( @@ -301,6 +341,124 @@ 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"] + 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_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"] + }>, +): 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 = await hashFertilizer(updatedProperties) + + await fdm + .update(schema.fertilizersCatalogue) + .set(updatedProperties) + .where( + 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", { + p_id_catalogue, + properties, + }) + } +} + /** * Retrieves fertilizer details for a specified farm. * @@ -333,6 +491,8 @@ 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, p_description: schema.fertilizersCatalogue.p_description, @@ -340,6 +500,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, @@ -367,7 +539,10 @@ 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, }) .from(schema.fertilizers) .leftJoin( diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index 0a641f877..6a184226e 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, diff --git a/fdm-data/src/cultivations/catalogues/brp.ts b/fdm-data/src/cultivations/catalogues/brp.ts index 0de3d2c36..34cfbbaac 100644 --- a/fdm-data/src/cultivations/catalogues/brp.ts +++ b/fdm-data/src/cultivations/catalogues/brp.ts @@ -1,8 +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. @@ -14,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" && @@ -40,10 +38,11 @@ export function getCatalogueBrp(): CatalogueCultivation { } // Hash the item - item.hash = h32ToString(JSON.stringify(item)) + item.hash = await hashCultivation(item) return item }) + const catalogueBrp = await Promise.all(catalogueBrpPromises) return catalogueBrp } diff --git a/fdm-data/src/cultivations/hash.test.ts b/fdm-data/src/cultivations/hash.test.ts new file mode 100644 index 000000000..992845bf0 --- /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", async () => { + 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 = await hashCultivation(cultivation) + expect(hash).toBeDefined() + expect(typeof hash).toBe("string") + expect(hash.length).toBeGreaterThan(0) + expect(hash).toBe("9e15c11b") + }) + + it("should generate different hashes for different cultivation items", async () => { + 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 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) + + expect(hash1).not.toBe(hash2) + }) + + it("should generate the same hash for identical cultivation items", async () => { + 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 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) + + expect(hash1).toBe(hash2) + }) + + it("should generate different hashes when a string value changes", async () => { + 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 = 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", async () => { + 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 = 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", async () => { + 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 = await hashCultivation(cultivation1) + const hash2 = await hashCultivation(cultivation2) + + expect(hash1).not.toBe(hash2) + }) + + it("should generate different hashes when a enum value changes", async () => { + 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 = 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 new file mode 100644 index 000000000..fca5be6f3 --- /dev/null +++ b/fdm-data/src/cultivations/hash.ts @@ -0,0 +1,25 @@ +import { ensureInitialized, h32ToString } from "../hash" +import type { CatalogueCultivationItem } from "./d" + +export async function hashCultivation(cultivation: CatalogueCultivationItem) { + await ensureInitialized() + // 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, + ), + ) + + // Sort keys to ensure consistent hash generation for identical objects + const sortedKeys = Object.keys(filteredCultivation).sort() + const sortedCultivation = sortedKeys.reduce>((obj, 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/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..49ed8503f 100644 --- a/fdm-data/src/cultivations/index.ts +++ b/fdm-data/src/cultivations/index.ts @@ -11,21 +11,22 @@ 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); * ``` */ -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 d5d09dcbb..701bf6751 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. @@ -14,8 +12,8 @@ const { h32ToString } = await xxhash() * @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, @@ -67,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, @@ -75,10 +73,11 @@ export function getCatalogueSrm(): CatalogueFertilizer { } // Hash the item - item.hash = h32ToString(JSON.stringify(item)) + item.hash = await hashFertilizer(item) return item }) + const catalogueSrm = await Promise.all(catalogueSrmPromises) return catalogueSrm } 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.test.ts b/fdm-data/src/fertilizers/hash.test.ts new file mode 100644 index 000000000..941442512 --- /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", async () => { + 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_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const hash = await hashFertilizer(fertilizer) + expect(hash).toBeDefined() + expect(typeof hash).toBe("string") + expect(hash.length).toBeGreaterThan(0) + expect(hash).toBe("3852e767") + }) + + it("should generate different hashes for different fertilizer items", async () => { + 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_rt: 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_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + 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", async () => { + 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_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + ...fertilizer1, + } + + const hash1 = await hashFertilizer(fertilizer1) + const hash2 = await hashFertilizer(fertilizer2) + + expect(hash1).toBe(hash2) + }) + + it("should generate different hashes when a boolean value changes", async () => { + 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_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + ...fertilizer1, + p_type_manure: false, + } + + 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", async () => { + 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_rt: 390, + p_type_manure: true, + p_type_mineral: false, + p_type_compost: false, + } + + const fertilizer2: CatalogueFertilizerItem = { + ...fertilizer1, + p_dm: 50, + } + + 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", async () => { + 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_rt: 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 = 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 new file mode 100644 index 000000000..4b7359e7b --- /dev/null +++ b/fdm-data/src/fertilizers/hash.ts @@ -0,0 +1,25 @@ +import { ensureInitialized, h32ToString } from "../hash" +import type { CatalogueFertilizerItem } from "./d" + +export async function hashFertilizer(fertilizer: CatalogueFertilizerItem) { + await ensureInitialized() + // 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, + ), + ) + + // Sort keys to ensure consistent hash generation for identical objects + const sortedKeys = Object.keys(filteredFertilizer).sort() + const sortedFertilizer = sortedKeys.reduce>((obj, key) => { + obj[key] = fertilizer[key as keyof typeof fertilizer] + return obj + }, {}) + + const hash = h32ToString(JSON.stringify(sortedFertilizer)) + return hash +} diff --git a/fdm-data/src/fertilizers/index.test.ts b/fdm-data/src/fertilizers/index.test.ts index 3c1b6969c..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") @@ -80,7 +81,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") @@ -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..e84a4970d 100644 --- a/fdm-data/src/fertilizers/index.ts +++ b/fdm-data/src/fertilizers/index.ts @@ -11,21 +11,22 @@ 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); * ``` */ -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`) } 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 +} diff --git a/fdm-data/src/index.ts b/fdm-data/src/index.ts index 1023c9216..3addf3e03 100644 --- a/fdm-data/src/index.ts +++ b/fdm-data/src/index.ts @@ -13,3 +13,5 @@ export { getFertilizersCatalogue } from "./fertilizers" export { getCultivationCatalogue } from "./cultivations" +export { hashFertilizer } from "./fertilizers/hash" +export { hashCultivation } from "./cultivations/hash" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 845411d4a..ee5753534 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 @@ -3644,6 +3647,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'} @@ -13260,6 +13274,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':