diff --git a/.changeset/few-boxes-work.md b/.changeset/few-boxes-work.md new file mode 100644 index 000000000..e4e3f9a3b --- /dev/null +++ b/.changeset/few-boxes-work.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": patch +--- + +Enable `bln` catalogue for measures when creating a new farm diff --git a/.changeset/hip-oranges-nail.md b/.changeset/hip-oranges-nail.md new file mode 100644 index 000000000..6517fb27d --- /dev/null +++ b/.changeset/hip-oranges-nail.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-core": minor +--- + +Add BLN3 measures tables and CRUD layer. New schema tables `measures_catalogue`, `measures`, and `measure_adopting` follow the action-asset model. Exports `addMeasure`, `getMeasure`, `getMeasures`, `getMeasuresForFarm`, `getMeasuresFromCatalogue`, `updateMeasure`, `removeMeasure`, `syncMeasuresCatalogueArray`, `enableMeasureCatalogue`, `disableMeasureCatalogue`, `isMeasureCatalogueEnabled`, `getEnabledMeasureCatalogues`, and the `Measure` / `MeasureCatalogue` types. `syncCatalogues` now accepts an optional `nmiApiKey` to populate the measures catalogue. All existing farms have the `bln` catalogue enabled by default via migration. diff --git a/.changeset/social-news-like.md b/.changeset/social-news-like.md new file mode 100644 index 000000000..e94c20115 --- /dev/null +++ b/.changeset/social-news-like.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-data": minor +--- + +Add measures catalogue module for BLN3 integration. Exports `getMeasuresCatalogue(catalogueName, nmiApiKey)` as a dispatcher (mirroring `getFertilizersCatalogue`), with BLN3 implemented in `measures/catalogues/bln.ts`. Adding future catalogues (e.g. ANLb) only requires a new file and extending the `CatalogueMeasureName` union. Also exports `hashMeasure` and the `CatalogueMeasure`, `CatalogueMeasureItem`, `CatalogueMeasureName` types using pandex naming conventions (`m_id`, `m_source`, `m_name`, etc.). diff --git a/docker-compose.yml b/docker-compose.yml index db9a97c41..cbd2479f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - POSTGRES_PASSWORD=YOUR_POSTGRES_PASSWORD # Replace with your actual PostgreSQL password - PUBLIC_MAP_PROVIDER=osm # Options: "maptiler" | "osm" - PUBLIC_MAPTILER_API_KEY=YOUR_MAPTILER_API_KEY # Replace with your actual MapTiler API key if using "maptiler" + - NMI_API_KEY= # Optional, but required for soil estimates, nutrient advice, soil analysis extraction, and BLN3 measures catalogue sync - BETTER_AUTH_SECRET=YOUR_BETTER_AUTH_SECRET # Replace with your Better Auth secret - BETTER_AUTH_URL=YOUR_DOMAIN # Replace with your domain - GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID # Replace with your Google Client ID diff --git a/fdm-app/.env.example b/fdm-app/.env.example index 6127791a6..711726640 100644 --- a/fdm-app/.env.example +++ b/fdm-app/.env.example @@ -94,6 +94,13 @@ PUBLIC_MAPTILER_API_KEY= # leave this blank. GEMINI_API_KEY= +# NMI API key for accessing NMI services. +# Used for: soil parameter estimates (atlas), soil analysis PDF extraction, +# nutrient advice, mineralization, calculator, AI-driven planning (Gerrit), +# and syncing the BLN3 measures catalogue on startup. +# Required: No (but many features are disabled without it) +NMI_API_KEY= + # ------------------------------------- # Analytics & Error Tracking (Optional) # ------------------------------------- diff --git a/fdm-app/app/lib/fdm-migrate.server.js b/fdm-app/app/lib/fdm-migrate.server.js index 6051ff80e..bbe11456e 100644 --- a/fdm-app/app/lib/fdm-migrate.server.js +++ b/fdm-app/app/lib/fdm-migrate.server.js @@ -53,7 +53,8 @@ const fdm = drizzle(client, { logger: false, schema: schema, }) -await syncCatalogues(fdm).catch((error) => +const nmiApiKey = process.env.NMI_API_KEY +await syncCatalogues(fdm, { nmiApiKey }).catch((error) => console.error("Error in syncing catalogues 🚨:", error), ) diff --git a/fdm-app/app/routes/farm.create._index.tsx b/fdm-app/app/routes/farm.create._index.tsx index 84e4829e4..7a9df87f4 100644 --- a/fdm-app/app/routes/farm.create._index.tsx +++ b/fdm-app/app/routes/farm.create._index.tsx @@ -6,6 +6,7 @@ import { addOrganicCertification, enableCultivationCatalogue, enableFertilizerCatalogue, + enableMeasureCatalogue, getFertilizersFromCatalogue, setGrazingIntention, } from "@nmi-agro/fdm-core" @@ -886,6 +887,12 @@ export async function action({ request }: ActionFunctionArgs) { b_id_farm, "brp", ), + enableMeasureCatalogue( + fdm, + session.principal_id, + b_id_farm, + "bln", + ), ) await Promise.all(setupPromises) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 2160fd87f..1bc110c2e 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -1,25 +1,40 @@ import { getCultivationCatalogue, getFertilizersCatalogue, + getMeasuresCatalogue, } from "@nmi-agro/fdm-data" import { eq, isNotNull } from "drizzle-orm" -import { beforeEach, describe, expect, inject, it } from "vitest" +import { beforeEach, describe, expect, inject, it, vi } from "vitest" import { disableCultivationCatalogue, disableFertilizerCatalogue, + disableMeasureCatalogue, enableCultivationCatalogue, enableFertilizerCatalogue, + enableMeasureCatalogue, getEnabledCultivationCatalogues, getEnabledFertilizerCatalogues, + getEnabledMeasureCatalogues, isCultivationCatalogueEnabled, isFertilizerCatalogueEnabled, + isMeasureCatalogueEnabled, syncCatalogues, + syncMeasuresCatalogueArray, } from "./catalogues" import * as schema from "./db/schema" import { addFarm } from "./farm" import type { FdmType } from "./fdm.types" import { createFdmServer } from "./fdm-server" +vi.mock("@nmi-agro/fdm-data", async (importOriginal) => { + const original = + await importOriginal() + return { + ...original, + getMeasuresCatalogue: vi.fn().mockResolvedValue([]), + } +}) + describe("Catalogues", () => { let fdm: FdmType let principal_id: string @@ -829,3 +844,227 @@ describe("Catalogues syncing", () => { ).toBe(true) }) }) + +describe("Measures Catalogue Sync", () => { + let fdm: FdmType + + const measureA = { + m_id: "bln_BM10", + m_source: "bln", + m_name: "Maatregel A", + m_description: "Beschrijving A", + m_summary: "Samenvatting A", + m_source_url: null, + m_conflicts: null, + } + + const measureB = { + m_id: "bln_BM11", + m_source: "bln", + m_name: "Maatregel B", + m_description: null, + m_summary: null, + m_source_url: "https://example.com/BM11", + m_conflicts: ["bln_BM10"], + } + + beforeEach(async () => { + const host = inject("host") + const port = inject("port") + const user = inject("user") + const password = inject("password") + const database = inject("database") + fdm = createFdmServer(host, port, user, password, database) + }) + + it("should update entry with null hash (treats as stale)", async () => { + await syncMeasuresCatalogueArray(fdm, [measureA]) + + // Simulate a row with null hash (e.g., from an old migration) + await fdm + .update(schema.measuresCatalogue) + .set({ hash: null }) + .where(eq(schema.measuresCatalogue.m_id, measureA.m_id)) + + await syncMeasuresCatalogueArray(fdm, [measureA]) + + const rows = await fdm + .select({ hash: schema.measuresCatalogue.hash }) + .from(schema.measuresCatalogue) + .where(eq(schema.measuresCatalogue.m_id, measureA.m_id)) + expect(rows[0].hash).not.toBeNull() + }) + + it("should insert new catalogue entries", async () => { + await syncMeasuresCatalogueArray(fdm, [measureA, measureB]) + + const rows = await fdm + .select() + .from(schema.measuresCatalogue) + .where( + eq(schema.measuresCatalogue.m_source, "bln"), + ) + + const ids = rows.map((r) => r.m_id) + expect(ids).toContain("bln_BM10") + expect(ids).toContain("bln_BM11") + }) + + it("should update changed entries (hash mismatch)", async () => { + await syncMeasuresCatalogueArray(fdm, [measureA]) + + const updated = { ...measureA, m_name: "Maatregel A — updated" } + await syncMeasuresCatalogueArray(fdm, [updated]) + + const rows = await fdm + .select() + .from(schema.measuresCatalogue) + .where(eq(schema.measuresCatalogue.m_id, "bln_BM10")) + expect(rows[0].m_name).toBe("Maatregel A — updated") + expect(rows[0].updated).not.toBeNull() + }) + + it("should skip unchanged entries (hash match)", async () => { + await syncMeasuresCatalogueArray(fdm, [measureA]) + + const rowsBefore = await fdm + .select({ updated: schema.measuresCatalogue.updated }) + .from(schema.measuresCatalogue) + .where(eq(schema.measuresCatalogue.m_id, "bln_BM10")) + + await syncMeasuresCatalogueArray(fdm, [measureA]) + + const rowsAfter = await fdm + .select({ updated: schema.measuresCatalogue.updated }) + .from(schema.measuresCatalogue) + .where(eq(schema.measuresCatalogue.m_id, "bln_BM10")) + + // updated timestamp should not have changed + expect(rowsAfter[0].updated).toEqual(rowsBefore[0].updated) + }) + + it("syncCatalogues without nmiApiKey should not call getMeasuresCatalogue", async () => { + vi.mocked(getMeasuresCatalogue).mockClear() + + await syncCatalogues(fdm) // no nmiApiKey + + expect(vi.mocked(getMeasuresCatalogue)).not.toHaveBeenCalled() + }) + + it("syncCatalogues with nmiApiKey should sync measures catalogue", async () => { + vi.mocked(getMeasuresCatalogue).mockResolvedValue([ + { + m_id: "bln_SYNC1", + m_source: "bln", + m_name: "Test Sync Measure", + m_description: null, + m_summary: null, + m_source_url: null, + m_conflicts: null, + }, + ]) + + await syncCatalogues(fdm, { nmiApiKey: "test-key" }) + + expect(vi.mocked(getMeasuresCatalogue)).toHaveBeenCalledWith( + "bln", + "test-key", + ) + const rows = await fdm + .select({ m_id: schema.measuresCatalogue.m_id }) + .from(schema.measuresCatalogue) + .where(eq(schema.measuresCatalogue.m_id, "bln_SYNC1")) + expect(rows).toHaveLength(1) + }) +}) + +describe("Measure Catalogues", () => { + let fdm: FdmType + let principal_id: string + let b_id_farm: string + + beforeEach(async () => { + const host = inject("host") + const port = inject("port") + const user = inject("user") + const password = inject("password") + const database = inject("database") + fdm = createFdmServer(host, port, user, password, database) + principal_id = "test_principal" + b_id_farm = await addFarm( + fdm, + principal_id, + "Test Farm", + "123456", + "123 Farm Lane", + "12345", + ) + }) + + it("should enable and check measure catalogue", async () => { + await enableMeasureCatalogue(fdm, principal_id, b_id_farm, "bln") + expect( + await isMeasureCatalogueEnabled(fdm, principal_id, b_id_farm, "bln"), + ).toBe(true) + }) + + it("should disable measure catalogue", async () => { + await enableMeasureCatalogue(fdm, principal_id, b_id_farm, "bln") + await disableMeasureCatalogue(fdm, principal_id, b_id_farm, "bln") + expect( + await isMeasureCatalogueEnabled(fdm, principal_id, b_id_farm, "bln"), + ).toBe(false) + }) + + it("should return enabled measure catalogues", async () => { + await enableMeasureCatalogue(fdm, principal_id, b_id_farm, "bln") + const enabled = await getEnabledMeasureCatalogues( + fdm, + principal_id, + b_id_farm, + ) + expect(enabled).toContain("bln") + }) + + it("should return empty array when no measure catalogues are enabled", async () => { + const enabled = await getEnabledMeasureCatalogues( + fdm, + principal_id, + b_id_farm, + ) + expect(enabled).toEqual([]) + }) + + it("should not disable a different source", async () => { + await enableMeasureCatalogue(fdm, principal_id, b_id_farm, "bln") + await disableMeasureCatalogue(fdm, principal_id, b_id_farm, "other") + expect( + await isMeasureCatalogueEnabled(fdm, principal_id, b_id_farm, "bln"), + ).toBe(true) + }) + + it("should throw when permission check fails", async () => { + await expect( + enableMeasureCatalogue(fdm, "wrong_principal", b_id_farm, "bln"), + ).rejects.toThrow() + }) + + it("should throw when permission check fails for getEnabledMeasureCatalogues", async () => { + await expect( + getEnabledMeasureCatalogues(fdm, "wrong_principal", b_id_farm), + ).rejects.toThrow() + }) + + it("should throw when permission check fails for disableMeasureCatalogue", async () => { + await expect( + disableMeasureCatalogue(fdm, "wrong_principal", b_id_farm, "bln"), + ).rejects.toThrow() + }) + + it("should throw when permission check fails for isMeasureCatalogueEnabled", async () => { + await expect( + isMeasureCatalogueEnabled(fdm, "wrong_principal", b_id_farm, "bln"), + ).rejects.toThrow() + }) +}) + diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index b5b7b4d83..2e831814d 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -5,9 +5,12 @@ import type { import { getCultivationCatalogue, getFertilizersCatalogue, + getMeasuresCatalogue, hashCultivation, hashFertilizer, + hashMeasure, } from "@nmi-agro/fdm-data" +import type { CatalogueMeasure } from "@nmi-agro/fdm-data" import { and, eq, inArray } from "drizzle-orm" import { checkPermission } from "./authorization" import type { PrincipalId } from "./authorization.types" @@ -478,14 +481,188 @@ export async function isCultivationCatalogueEnabled( } /** - * Synchronizes the fertilizer and cultivation catalogues in the FDM database with the data from fdm-data. + * Gets all enabled measure catalogues for 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. + * @returns A Promise that resolves to an array of enabled measure catalogue sources. + * @throws If retrieving the catalogues fails. + */ +export async function getEnabledMeasureCatalogues( + fdm: FdmType, + principal_id: PrincipalId, + b_id_farm: schema.farmsTypeSelect["b_id_farm"], +): Promise { + try { + await checkPermission( + fdm, + "farm", + "read", + b_id_farm, + principal_id, + "getEnabledMeasureCatalogues", + ) + const result = await fdm + .select({ + m_source: schema.measureCatalogueEnabling.m_source, + }) + .from(schema.measureCatalogueEnabling) + .where(eq(schema.measureCatalogueEnabling.b_id_farm, b_id_farm)) + + return result.map((row) => row.m_source) + } catch (err) { + throw handleError(err, "Exception for getEnabledMeasureCatalogues", { + principal_id, + b_id_farm, + }) + } +} + +/** + * Enables a measure catalogue for 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 m_source The source/name of the measure catalogue to enable. + * @returns A Promise that resolves when the catalogue has been enabled. + * @throws If enabling the catalogue fails. + */ +export async function enableMeasureCatalogue( + fdm: FdmType, + principal_id: PrincipalId, + b_id_farm: schema.farmsTypeSelect["b_id_farm"], + m_source: string, +): Promise { + try { + await checkPermission( + fdm, + "farm", + "write", + b_id_farm, + principal_id, + "enableMeasureCatalogue", + ) + await fdm.insert(schema.measureCatalogueEnabling).values({ + b_id_farm, + m_source, + }) + } catch (err) { + throw handleError(err, "Exception for enableMeasureCatalogue", { + principal_id, + b_id_farm, + m_source, + }) + } +} + +/** + * Disables a measure catalogue for 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 m_source The source/name of the measure catalogue to disable. + * @returns A Promise that resolves when the catalogue has been disabled. + * @throws If disabling the catalogue fails. + */ +export async function disableMeasureCatalogue( + fdm: FdmType, + principal_id: PrincipalId, + b_id_farm: schema.farmsTypeSelect["b_id_farm"], + m_source: string, +): Promise { + try { + await checkPermission( + fdm, + "farm", + "write", + b_id_farm, + principal_id, + "disableMeasureCatalogue", + ) + await fdm + .delete(schema.measureCatalogueEnabling) + .where( + and( + eq(schema.measureCatalogueEnabling.b_id_farm, b_id_farm), + eq(schema.measureCatalogueEnabling.m_source, m_source), + ), + ) + } catch (err) { + throw handleError(err, "Exception for disableMeasureCatalogue", { + principal_id, + b_id_farm, + m_source, + }) + } +} + +/** + * Checks if a measure catalogue is enabled for 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 m_source The source/name of the measure catalogue to check. + * @returns A Promise that resolves to true if the catalogue is enabled, false otherwise. + * @throws If checking the catalogue status fails. + */ +export async function isMeasureCatalogueEnabled( + fdm: FdmType, + principal_id: PrincipalId, + b_id_farm: schema.farmsTypeSelect["b_id_farm"], + m_source: string, +): Promise { + try { + await checkPermission( + fdm, + "farm", + "read", + b_id_farm, + principal_id, + "isMeasureCatalogueEnabled", + ) + const result = await fdm + .select({ + b_id_farm: schema.measureCatalogueEnabling.b_id_farm, + m_source: schema.measureCatalogueEnabling.m_source, + }) + .from(schema.measureCatalogueEnabling) + .where( + and( + eq(schema.measureCatalogueEnabling.b_id_farm, b_id_farm), + eq(schema.measureCatalogueEnabling.m_source, m_source), + ), + ) + + return result.length > 0 + } catch (err) { + throw handleError(err, "Exception for isMeasureCatalogueEnabled", { + principal_id, + b_id_farm, + m_source, + }) + } +} + +/** + * Synchronizes the fertilizer, cultivation, and optionally measures catalogues in the FDM database. + * + * @param fdm The FDM instance providing the connection to the database. The instance can be created with {@link createFdmServer}. + * @param options Optional configuration. Provide `nmiApiKey` to also sync the measures catalogue from the NMI API. * @returns A promise that resolves when the synchronization is complete. */ -export async function syncCatalogues(fdm: FdmType): Promise { +export async function syncCatalogues( + fdm: FdmType, + options?: { nmiApiKey?: string }, +): Promise { await syncFertilizerCatalogue(fdm) await syncCultivationCatalogue(fdm) + if (options?.nmiApiKey) { + await syncMeasuresCatalogue(fdm, options.nmiApiKey) + } } async function syncFertilizerCatalogue(fdm: FdmType) { @@ -609,3 +786,62 @@ async function syncCultivationCatalogue(fdm: FdmType) { } }) } + +async function syncMeasuresCatalogue( + fdm: FdmType, + nmiApiKey: string, +): Promise { + const measures = await getMeasuresCatalogue("bln", nmiApiKey) + return syncMeasuresCatalogueArray(fdm, measures) +} + +/** + * Synchronizes the measures catalogue with the provided array of catalogue items. + * + * Public so that tests and custom data injection can call it directly without a live API key. + * Mirrors {@link syncFertilizerCatalogueArray}. + * + * @param fdm The FDM instance providing the connection to the database. + * @param measures Array of catalogue items (in pandex naming convention from fdm-data). + */ +export async function syncMeasuresCatalogueArray( + fdm: FdmType, + measures: CatalogueMeasure, +): Promise { + await fdm.transaction(async (tx) => { + try { + for (const catalogueItem of measures) { + const hash = await hashMeasure(catalogueItem) + const item = { ...catalogueItem, hash } + const existing = await tx + .select({ hash: schema.measuresCatalogue.hash }) + .from(schema.measuresCatalogue) + .where( + eq(schema.measuresCatalogue.m_id, item.m_id), + ) + .limit(1) + if (existing.length === 0) { + await tx.insert(schema.measuresCatalogue).values(item) + } else { + if ( + existing[0].hash === null || + existing[0].hash === undefined || + existing[0].hash !== item.hash + ) { + await tx + .update(schema.measuresCatalogue) + .set({ ...item, updated: new Date() }) + .where( + eq( + schema.measuresCatalogue.m_id, + item.m_id, + ), + ) + } + } + } + } catch (error) { + throw handleError(error, "Exception for syncMeasuresCatalogue") + } + }) +} diff --git a/fdm-core/src/db/migrations/0028_spotty_greymalkin.sql b/fdm-core/src/db/migrations/0028_spotty_greymalkin.sql new file mode 100644 index 000000000..5fb8d7279 --- /dev/null +++ b/fdm-core/src/db/migrations/0028_spotty_greymalkin.sql @@ -0,0 +1,47 @@ +CREATE TABLE "fdm"."measure_adopting" ( + "b_id" text NOT NULL, + "b_id_measure" text NOT NULL, + "m_start" timestamp with time zone, + "m_end" timestamp with time zone, + "created" timestamp with time zone DEFAULT now() NOT NULL, + "updated" timestamp with time zone, + CONSTRAINT "measure_adopting_b_id_b_id_measure_pk" PRIMARY KEY("b_id","b_id_measure") +); +--> statement-breakpoint +CREATE TABLE "fdm"."measure_catalogue_enabling" ( + "b_id_farm" text NOT NULL, + "m_source" text NOT NULL, + "created" timestamp with time zone DEFAULT now() NOT NULL, + "updated" timestamp with time zone, + CONSTRAINT "measure_catalogue_enabling_b_id_farm_m_source_pk" PRIMARY KEY("b_id_farm","m_source") +); +--> statement-breakpoint +CREATE TABLE "fdm"."measures" ( + "b_id_measure" text PRIMARY KEY NOT NULL, + "m_id" text NOT NULL, + "created" timestamp with time zone DEFAULT now() NOT NULL, + "updated" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "fdm"."measures_catalogue" ( + "m_id" text PRIMARY KEY NOT NULL, + "m_source" text NOT NULL, + "m_name" text NOT NULL, + "m_description" text, + "m_summary" text, + "m_source_url" text, + "m_conflicts" text[], + "hash" text, + "created" timestamp with time zone DEFAULT now() NOT NULL, + "updated" timestamp with time zone +); +--> statement-breakpoint +ALTER TABLE "fdm"."measure_adopting" ADD CONSTRAINT "measure_adopting_b_id_fields_b_id_fk" FOREIGN KEY ("b_id") REFERENCES "fdm"."fields"("b_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fdm"."measure_adopting" ADD CONSTRAINT "measure_adopting_b_id_measure_measures_b_id_measure_fk" FOREIGN KEY ("b_id_measure") REFERENCES "fdm"."measures"("b_id_measure") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fdm"."measure_catalogue_enabling" ADD CONSTRAINT "measure_catalogue_enabling_b_id_farm_farms_b_id_farm_fk" FOREIGN KEY ("b_id_farm") REFERENCES "fdm"."farms"("b_id_farm") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "fdm"."measures" ADD CONSTRAINT "measures_m_id_measures_catalogue_m_id_fk" FOREIGN KEY ("m_id") REFERENCES "fdm"."measures_catalogue"("m_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "b_id_measure_idx" ON "fdm"."measures" USING btree ("b_id_measure");--> statement-breakpoint +CREATE UNIQUE INDEX "m_id_idx" ON "fdm"."measures_catalogue" USING btree ("m_id");--> statement-breakpoint +CREATE INDEX "m_source_idx" ON "fdm"."measures_catalogue" USING btree ("m_source");--> statement-breakpoint +INSERT INTO "fdm"."measure_catalogue_enabling" ("b_id_farm", "m_source") +SELECT "b_id_farm", 'bln' FROM "fdm"."farms"; \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/0028_snapshot.json b/fdm-core/src/db/migrations/meta/0028_snapshot.json new file mode 100644 index 000000000..4687c15e2 --- /dev/null +++ b/fdm-core/src/db/migrations/meta/0028_snapshot.json @@ -0,0 +1,4329 @@ +{ + "id": "e20ebae4-1145-4cbe-9bac-1c6f18377ef4", + "prevId": "8d40d8f9-6950-44ba-a4ba-dc9085f7117f", + "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": { + "cultivation_catalogue_selecting_b_id_farm_b_lu_source_pk": { + "name": "cultivation_catalogue_selecting_b_id_farm_b_lu_source_pk", + "columns": [ + "b_id_farm", + "b_lu_source" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_ending": { + "name": "cultivation_ending", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_end": { + "name": "b_lu_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "m_cropresidue": { + "name": "m_cropresidue", + "type": "boolean", + "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": { + "cultivation_starting_b_id_b_lu_pk": { + "name": "cultivation_starting_b_id_b_lu_pk", + "columns": [ + "b_id", + "b_lu" + ] + } + }, + "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 + }, + "b_lu_variety": { + "name": "b_lu_variety", + "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_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_harvestcat": { + "name": "b_lu_harvestcat", + "type": "b_lu_harvestcat", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "b_lu_croprotation": { + "name": "b_lu_croprotation", + "type": "b_lu_croprotation", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_dm": { + "name": "b_lu_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_hi": { + "name": "b_lu_hi", + "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_n_fixation": { + "name": "b_n_fixation", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_eom": { + "name": "b_lu_eom", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_eom_residue": { + "name": "b_lu_eom_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_rest_oravib": { + "name": "b_lu_rest_oravib", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "b_lu_variety_options": { + "name": "b_lu_variety_options", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "b_lu_start_default": { + "name": "b_lu_start_default", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_date_harvest_default": { + "name": "b_date_harvest_default", + "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": { + "b_lu_start_default_format": { + "name": "b_lu_start_default_format", + "value": "b_lu_start_default IS NULL OR b_lu_start_default ~ '^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'" + }, + "b_date_harvest_default_format": { + "name": "b_date_harvest_default_format", + "value": "b_date_harvest_default IS NULL OR b_date_harvest_default ~ '^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'" + } + }, + "isRLSEnabled": false + }, + "fdm.derogation_applying": { + "name": "derogation_applying", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_derogation": { + "name": "b_id_derogation", + "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": { + "derogation_one_per_farm_per": { + "name": "derogation_one_per_farm_per", + "columns": [ + { + "expression": "b_id_derogation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "derogation_applying_b_id_farm_farms_b_id_farm_fk": { + "name": "derogation_applying_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "derogation_applying", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "derogation_applying_b_id_derogation_derogations_b_id_derogation_fk": { + "name": "derogation_applying_b_id_derogation_derogations_b_id_derogation_fk", + "tableFrom": "derogation_applying", + "tableTo": "derogations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_derogation" + ], + "columnsTo": [ + "b_id_derogation" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "derogation_applying_pk": { + "name": "derogation_applying_pk", + "columns": [ + "b_id_farm", + "b_id_derogation" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.derogations": { + "name": "derogations", + "schema": "fdm", + "columns": { + "b_id_derogation": { + "name": "b_id_derogation", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_derogation_year": { + "name": "b_derogation_year", + "type": "integer", + "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": {}, + "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": { + "fertilizer_catalogue_enabling_b_id_farm_p_source_pk": { + "name": "fertilizer_catalogue_enabling_b_id_farm_p_source_pk", + "columns": [ + "b_id_farm", + "p_source" + ] + } + }, + "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_app_method_options": { + "name": "p_app_method_options", + "type": "p_app_method[]", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_app_amount_unit": { + "name": "p_app_amount_unit", + "type": "p_app_amount_unit", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true, + "default": "'kg/ha'" + }, + "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_no3_rt": { + "name": "p_no3_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_nh4_rt": { + "name": "p_nh4_rt", + "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_ef_nh3": { + "name": "p_ef_nh3", + "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 + }, + "p_type_rvo": { + "name": "p_type_rvo", + "type": "p_type_rvo", + "typeSchema": "fdm", + "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": { + "field_acquiring_b_id_b_id_farm_pk": { + "name": "field_acquiring_b_id_b_id_farm_pk", + "columns": [ + "b_id", + "b_id_farm" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_discarding": { + "name": "field_discarding", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": true, + "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 + }, + "b_bufferstrip": { + "name": "b_bufferstrip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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_yield_fresh": { + "name": "b_lu_yield_fresh", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield_bruto": { + "name": "b_lu_yield_bruto", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_tarra": { + "name": "b_lu_tarra", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_dm": { + "name": "b_lu_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_moist": { + "name": "b_lu_moist", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_uww": { + "name": "b_lu_uww", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_cp": { + "name": "b_lu_cp", + "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": { + "harvestable_sampling_pk": { + "name": "harvestable_sampling_pk", + "columns": [ + "b_id_harvestable", + "b_id_harvestable_analysis" + ] + } + }, + "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.intending_grazing": { + "name": "intending_grazing", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_grazing_intention": { + "name": "b_grazing_intention", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "b_grazing_intention_year": { + "name": "b_grazing_intention_year", + "type": "integer", + "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": { + "intending_grazing_b_id_farm_farms_b_id_farm_fk": { + "name": "intending_grazing_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "intending_grazing", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "intending_grazing_b_id_farm_b_grazing_intention_year_pk": { + "name": "intending_grazing_b_id_farm_b_grazing_intention_year_pk", + "columns": [ + "b_id_farm", + "b_grazing_intention_year" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.measure_adopting": { + "name": "measure_adopting", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_measure": { + "name": "b_id_measure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "m_start": { + "name": "m_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "m_end": { + "name": "m_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": { + "measure_adopting_b_id_fields_b_id_fk": { + "name": "measure_adopting_b_id_fields_b_id_fk", + "tableFrom": "measure_adopting", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "measure_adopting_b_id_measure_measures_b_id_measure_fk": { + "name": "measure_adopting_b_id_measure_measures_b_id_measure_fk", + "tableFrom": "measure_adopting", + "tableTo": "measures", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_measure" + ], + "columnsTo": [ + "b_id_measure" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "measure_adopting_b_id_b_id_measure_pk": { + "name": "measure_adopting_b_id_b_id_measure_pk", + "columns": [ + "b_id", + "b_id_measure" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.measure_catalogue_enabling": { + "name": "measure_catalogue_enabling", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "m_source": { + "name": "m_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": { + "measure_catalogue_enabling_b_id_farm_farms_b_id_farm_fk": { + "name": "measure_catalogue_enabling_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "measure_catalogue_enabling", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "measure_catalogue_enabling_b_id_farm_m_source_pk": { + "name": "measure_catalogue_enabling_b_id_farm_m_source_pk", + "columns": [ + "b_id_farm", + "m_source" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.measures": { + "name": "measures", + "schema": "fdm", + "columns": { + "b_id_measure": { + "name": "b_id_measure", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "m_id": { + "name": "m_id", + "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_id_measure_idx": { + "name": "b_id_measure_idx", + "columns": [ + { + "expression": "b_id_measure", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "measures_m_id_measures_catalogue_m_id_fk": { + "name": "measures_m_id_measures_catalogue_m_id_fk", + "tableFrom": "measures", + "tableTo": "measures_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "m_id" + ], + "columnsTo": [ + "m_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.measures_catalogue": { + "name": "measures_catalogue", + "schema": "fdm", + "columns": { + "m_id": { + "name": "m_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "m_source": { + "name": "m_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "m_name": { + "name": "m_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "m_description": { + "name": "m_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "m_summary": { + "name": "m_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "m_source_url": { + "name": "m_source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "m_conflicts": { + "name": "m_conflicts", + "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": { + "m_id_idx": { + "name": "m_id_idx", + "columns": [ + { + "expression": "m_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "m_source_idx": { + "name": "m_source_idx", + "columns": [ + { + "expression": "m_source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.organic_certifications": { + "name": "organic_certifications", + "schema": "fdm", + "columns": { + "b_id_organic": { + "name": "b_id_organic", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_organic_traces": { + "name": "b_organic_traces", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_organic_skal": { + "name": "b_organic_skal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_organic_issued": { + "name": "b_organic_issued", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_organic_expires": { + "name": "b_organic_expires", + "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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.organic_certifications_holding": { + "name": "organic_certifications_holding", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_organic": { + "name": "b_id_organic", + "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": { + "organic_one_farm_per_cert": { + "name": "organic_one_farm_per_cert", + "columns": [ + { + "expression": "b_id_organic", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organic_certifications_holding_b_id_farm_farms_b_id_farm_fk": { + "name": "organic_certifications_holding_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "organic_certifications_holding", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organic_certifications_holding_b_id_organic_organic_certifications_b_id_organic_fk": { + "name": "organic_certifications_holding_b_id_organic_organic_certifications_b_id_organic_fk", + "tableFrom": "organic_certifications_holding", + "tableTo": "organic_certifications", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_organic" + ], + "columnsTo": [ + "b_id_organic" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organic_certifications_holding_b_id_farm_b_id_organic_pk": { + "name": "organic_certifications_holding_b_id_farm_b_id_organic_pk", + "columns": [ + "b_id_farm", + "b_id_organic" + ] + } + }, + "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": "a_source", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false, + "default": "'other'" + }, + "a_al_ox": { + "name": "a_al_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_c_of": { + "name": "a_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co": { + "name": "a_ca_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co_po": { + "name": "a_ca_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_caco3_if": { + "name": "a_caco3_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cec_co": { + "name": "a_cec_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_clay_mi": { + "name": "a_clay_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cn_fr": { + "name": "a_cn_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_com_fr": { + "name": "a_com_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cu_cc": { + "name": "a_cu_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_density_sa": { + "name": "a_density_sa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_fe_ox": { + "name": "a_fe_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_cc": { + "name": "a_k_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co": { + "name": "a_k_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co_po": { + "name": "a_k_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_cc": { + "name": "a_mg_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co": { + "name": "a_mg_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co_po": { + "name": "a_mg_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_pmn": { + "name": "a_n_pmn", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_rt": { + "name": "a_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nh4_cc": { + "name": "a_nh4_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nmin_cc": { + "name": "a_nmin_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_no3_cc": { + "name": "a_no3_cc", + "type": "numeric", + "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_p_ox": { + "name": "a_p_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_rt": { + "name": "a_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_sg": { + "name": "a_p_sg", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_wa": { + "name": "a_p_wa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ph_cc": { + "name": "a_ph_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_s_rt": { + "name": "a_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_sand_mi": { + "name": "a_sand_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_silt_mi": { + "name": "a_silt_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_som_loi": { + "name": "a_som_loi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_zn_cc": { + "name": "a_zn_cc", + "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 + }, + "a_depth_upper": { + "name": "a_depth_upper", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "a_depth_lower": { + "name": "a_depth_lower", + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.invitation": { + "name": "invitation", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.member": { + "name": "member", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.organization": { + "name": "organization", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "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": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_key_unique": { + "name": "rate_limit_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "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, + "default": "now()" + }, + "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 + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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, + "default": "'nl-NL'" + }, + "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" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "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": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.invitation": { + "name": "invitation", + "schema": "fdm-authz", + "columns": { + "invitation_id": { + "name": "invitation_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 + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_principal_id": { + "name": "target_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invitation_unique_email_idx": { + "name": "invitation_unique_email_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_unique_principal_idx": { + "name": "invitation_unique_principal_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_target_email_idx": { + "name": "invitation_pending_target_email_idx", + "columns": [ + { + "expression": "target_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "invitation_target_check": { + "name": "invitation_target_check", + "value": "\"fdm-authz\".\"invitation\".\"target_email\" IS NOT NULL OR \"fdm-authz\".\"invitation\".\"target_principal_id\" IS NOT NULL" + } + }, + "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 + }, + "fdm-calculator.calculation_cache": { + "name": "calculation_cache", + "schema": "fdm-calculator", + "columns": { + "calculation_hash": { + "name": "calculation_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "calculation_function": { + "name": "calculation_function", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calculator_version": { + "name": "calculator_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-calculator.calculation_errors": { + "name": "calculation_errors", + "schema": "fdm-calculator", + "columns": { + "calculation_error_id": { + "name": "calculation_error_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "calculation_function": { + "name": "calculation_function", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "calculator_version": { + "name": "calculator_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stack_trace": { + "name": "stack_trace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "fdm.b_acquiring_method": { + "name": "b_acquiring_method", + "schema": "fdm", + "values": [ + "nl_01", + "nl_02", + "nl_03", + "nl_04", + "nl_07", + "nl_09", + "nl_10", + "nl_11", + "nl_12", + "nl_13", + "nl_61", + "nl_63", + "unknown" + ] + }, + "fdm.p_app_method": { + "name": "p_app_method", + "schema": "fdm", + "values": [ + "slotted coulter", + "incorporation", + "incorporation 2 tracks", + "injection", + "shallow injection", + "spraying", + "broadcasting", + "spoke wheel", + "pocket placement", + "narrowband" + ] + }, + "fdm.b_gwl_class": { + "name": "b_gwl_class", + "schema": "fdm", + "values": [ + "I", + "Ia", + "Ic", + "II", + "IIa", + "IIb", + "IIc", + "III", + "IIIa", + "IIIb", + "IV", + "IVu", + "IVc", + "V", + "Va", + "Vao", + "Vad", + "Vb", + "Vbo", + "Vbd", + "sV", + "sVb", + "VI", + "VIo", + "VId", + "VII", + "VIIo", + "VIId", + "VIII", + "VIIIo", + "VIIId" + ] + }, + "fdm.b_lu_harvestcat": { + "name": "b_lu_harvestcat", + "schema": "fdm", + "values": [ + "HC010", + "HC020", + "HC031", + "HC040", + "HC041", + "HC042", + "HC050" + ] + }, + "fdm.b_lu_harvestable": { + "name": "b_lu_harvestable", + "schema": "fdm", + "values": [ + "none", + "once", + "multiple" + ] + }, + "fdm.b_lu_croprotation": { + "name": "b_lu_croprotation", + "schema": "fdm", + "values": [ + "other", + "clover", + "nature", + "potato", + "grass", + "rapeseed", + "starch", + "maize", + "cereal", + "sugarbeet", + "alfalfa", + "catchcrop" + ] + }, + "fdm.a_source": { + "name": "a_source", + "schema": "fdm", + "values": [ + "nl-rva-l122", + "nl-rva-l136", + "nl-rva-l264", + "nl-rva-l320", + "nl-rva-l335", + "nl-rva-l610", + "nl-rva-l648", + "nl-rva-l697", + "nl-other-nmi", + "other" + ] + }, + "fdm.b_soiltype_agr": { + "name": "b_soiltype_agr", + "schema": "fdm", + "values": [ + "moerige_klei", + "rivierklei", + "dekzand", + "zeeklei", + "dalgrond", + "veen", + "loess", + "duinzand", + "maasklei" + ] + }, + "fdm.p_app_amount_unit": { + "name": "p_app_amount_unit", + "schema": "fdm", + "values": [ + "kg/ha", + "l/ha", + "m3/ha", + "ton/ha" + ] + }, + "fdm.p_type_rvo": { + "name": "p_type_rvo", + "schema": "fdm", + "values": [ + "10", + "11", + "12", + "13", + "14", + "17", + "18", + "19", + "23", + "30", + "31", + "32", + "33", + "35", + "39", + "40", + "41", + "42", + "43", + "46", + "50", + "56", + "60", + "61", + "75", + "76", + "80", + "81", + "90", + "91", + "92", + "25", + "26", + "27", + "95", + "96", + "97", + "98", + "99", + "100", + "101", + "102", + "103", + "104", + "105", + "106", + "107", + "108", + "109", + "110", + "111", + "112", + "113", + "114", + "115", + "116", + "117", + "120" + ] + } + }, + "schemas": { + "fdm": "fdm", + "fdm-authn": "fdm-authn", + "fdm-authz": "fdm-authz", + "fdm-calculator": "fdm-calculator" + }, + "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 1478c71a7..a76b62777 100644 --- a/fdm-core/src/db/migrations/meta/_journal.json +++ b/fdm-core/src/db/migrations/meta/_journal.json @@ -1,202 +1,209 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1731414293847, - "tag": "0000_v0", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1741267610502, - "tag": "0001_v0-15-0", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1743420907290, - "tag": "0002_v0-18-0", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1744205441260, - "tag": "0003_v0-20-0-1", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1745410821339, - "tag": "0004_v0-20-0-2", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1748353081475, - "tag": "0005_v0-20-0-3", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1748353926519, - "tag": "0006_v0-20-0-4", - "breakpoints": true - }, - { - "idx": 7, - "version": "7", - "when": 1750146397071, - "tag": "0007_v0-21-0-1", - "breakpoints": true - }, - { - "idx": 8, - "version": "7", - "when": 1750751079210, - "tag": "0008_v0-21-0-2", - "breakpoints": true - }, - { - "idx": 9, - "version": "7", - "when": 1752056714510, - "tag": "0009_v0-22-0-1", - "breakpoints": true - }, - { - "idx": 10, - "version": "7", - "when": 1753084974762, - "tag": "0010_v0-22-0-2", - "breakpoints": true - }, - { - "idx": 11, - "version": "7", - "when": 1754396961710, - "tag": "0011_v0-22-1-1", - "breakpoints": true - }, - { - "idx": 12, - "version": "7", - "when": 1754661913554, - "tag": "0012_v0-24-0-1", - "breakpoints": true - }, - { - "idx": 13, - "version": "7", - "when": 1755074095394, - "tag": "0013_v0-24-0-2", - "breakpoints": true - }, - { - "idx": 14, - "version": "7", - "when": 1760450273146, - "tag": "0014_v0-26-0-1", - "breakpoints": true - }, - { - "idx": 15, - "version": "7", - "when": 1760621691069, - "tag": "0015_v0-26-0-2", - "breakpoints": true - }, - { - "idx": 16, - "version": "7", - "when": 1761121611245, - "tag": "0016_v0-26-0-3", - "breakpoints": true - }, - { - "idx": 17, - "version": "7", - "when": 1761909360538, - "tag": "0017_v0-26-0-4", - "breakpoints": true - }, - { - "idx": 18, - "version": "7", - "when": 1763025310900, - "tag": "0018_v0-27-0-1", - "breakpoints": true - }, - { - "idx": 19, - "version": "7", - "when": 1763647193660, - "tag": "0019_v0-27-0-2", - "breakpoints": true - }, - { - "idx": 20, - "version": "7", - "when": 1767697779747, - "tag": "0020_v0-28-0-1", - "breakpoints": true - }, - { - "idx": 21, - "version": "7", - "when": 1768485087752, - "tag": "0021_v0-29-0-1", - "breakpoints": true - }, - { - "idx": 22, - "version": "7", - "when": 1768485087753, - "tag": "0022_v0-29-0-2", - "breakpoints": true - }, - { - "idx": 23, - "version": "7", - "when": 1771594531138, - "tag": "0023_v0-30-0-1", - "breakpoints": true - }, - { - "idx": 24, - "version": "7", - "when": 1773142528110, - "tag": "0024_better-auth-1-5-4", - "breakpoints": true - }, - { - "idx": 25, - "version": "7", - "when": 1773751940467, - "tag": "0025_rename-b-lu-eom-residue", - "breakpoints": true - }, - { - "idx": 26, - "version": "7", - "when": 1775635544356, - "tag": "0026_p_app_amount_unit", - "breakpoints": true - }, - { - "idx": 27, - "version": "7", - "when": 1776864619940, - "tag": "0027_add_missing_indices", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1731414293847, + "tag": "0000_v0", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1741267610502, + "tag": "0001_v0-15-0", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1743420907290, + "tag": "0002_v0-18-0", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1744205441260, + "tag": "0003_v0-20-0-1", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1745410821339, + "tag": "0004_v0-20-0-2", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1748353081475, + "tag": "0005_v0-20-0-3", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1748353926519, + "tag": "0006_v0-20-0-4", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1750146397071, + "tag": "0007_v0-21-0-1", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1750751079210, + "tag": "0008_v0-21-0-2", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1752056714510, + "tag": "0009_v0-22-0-1", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1753084974762, + "tag": "0010_v0-22-0-2", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1754396961710, + "tag": "0011_v0-22-1-1", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1754661913554, + "tag": "0012_v0-24-0-1", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1755074095394, + "tag": "0013_v0-24-0-2", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1760450273146, + "tag": "0014_v0-26-0-1", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1760621691069, + "tag": "0015_v0-26-0-2", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1761121611245, + "tag": "0016_v0-26-0-3", + "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1761909360538, + "tag": "0017_v0-26-0-4", + "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1763025310900, + "tag": "0018_v0-27-0-1", + "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1763647193660, + "tag": "0019_v0-27-0-2", + "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1767697779747, + "tag": "0020_v0-28-0-1", + "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1768485087752, + "tag": "0021_v0-29-0-1", + "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1768485087753, + "tag": "0022_v0-29-0-2", + "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1771594531138, + "tag": "0023_v0-30-0-1", + "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1773142528110, + "tag": "0024_better-auth-1-5-4", + "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1773751940467, + "tag": "0025_rename-b-lu-eom-residue", + "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1775635544356, + "tag": "0026_p_app_amount_unit", + "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1776864619940, + "tag": "0027_add_missing_indices", + "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1778141446526, + "tag": "0028_spotty_greymalkin", + "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 6f82745eb..e518f7b35 100644 --- a/fdm-core/src/db/schema.ts +++ b/fdm-core/src/db/schema.ts @@ -924,3 +924,86 @@ export type cultivationCatalogueSelectingTypeSelect = typeof cultivationCatalogueSelecting.$inferSelect export type cultivationCatalogueSelectingTypeInsert = typeof cultivationCatalogueSelecting.$inferInsert + +// Define measures_catalogue table +export const measuresCatalogue = fdmSchema.table( + "measures_catalogue", + { + m_id: text().primaryKey(), // "bln_BM1", "bln_BM2", etc. + m_source: text().notNull(), // "bln"; future: "ANLb", etc. + m_name: text().notNull(), + m_description: text(), + m_summary: text(), + m_source_url: text(), + m_conflicts: text().array(), // Conflicting m_id values + hash: text(), + created: timestamp({ withTimezone: true }).notNull().defaultNow(), + updated: timestamp({ withTimezone: true }), + }, + (table) => [ + uniqueIndex("m_id_idx").on(table.m_id), + index("m_source_idx").on(table.m_source), + ], +) + +export type measuresCatalogueTypeSelect = typeof measuresCatalogue.$inferSelect +export type measuresCatalogueTypeInsert = typeof measuresCatalogue.$inferInsert + +// Define measures table +export const measures = fdmSchema.table( + "measures", + { + b_id_measure: text().primaryKey(), + m_id: text() + .notNull() + .references(() => measuresCatalogue.m_id), + created: timestamp({ withTimezone: true }).notNull().defaultNow(), + updated: timestamp({ withTimezone: true }), + }, + (table) => [uniqueIndex("b_id_measure_idx").on(table.b_id_measure)], +) + +export type measuresTypeSelect = typeof measures.$inferSelect +export type measuresTypeInsert = typeof measures.$inferInsert + +// Define measure_adopting table +export const measureAdopting = fdmSchema.table( + "measure_adopting", + { + b_id: text() + .notNull() + .references(() => fields.b_id, { onDelete: "cascade" }), + b_id_measure: text() + .notNull() + .references(() => measures.b_id_measure), + m_start: timestamp({ withTimezone: true }), + m_end: timestamp({ withTimezone: true }), // NULL = ongoing / doorlopend + created: timestamp({ withTimezone: true }).notNull().defaultNow(), + updated: timestamp({ withTimezone: true }), + }, + (table) => [primaryKey({ columns: [table.b_id, table.b_id_measure] })], +) + +export type measureAdoptingTypeSelect = typeof measureAdopting.$inferSelect +export type measureAdoptingTypeInsert = typeof measureAdopting.$inferInsert + +// Define measure_catalogue_enabling table +export const measureCatalogueEnabling = fdmSchema.table( + "measure_catalogue_enabling", + { + b_id_farm: text() + .notNull() + .references(() => farms.b_id_farm, { onDelete: "cascade" }), + m_source: text().notNull(), + created: timestamp({ withTimezone: true }).notNull().defaultNow(), + updated: timestamp({ withTimezone: true }), + }, + (table) => { + return [primaryKey({ columns: [table.b_id_farm, table.m_source] })] + }, +) + +export type measureCatalogueEnablingTypeSelect = + typeof measureCatalogueEnabling.$inferSelect +export type measureCatalogueEnablingTypeInsert = + typeof measureCatalogueEnabling.$inferInsert diff --git a/fdm-core/src/farm.ts b/fdm-core/src/farm.ts index d1bd9c2cb..d8932559a 100644 --- a/fdm-core/src/farm.ts +++ b/fdm-core/src/farm.ts @@ -1229,6 +1229,9 @@ export async function removeFarm( b_id_farm, ), ) + await tx + .delete(schema.measureCatalogueEnabling) + .where(eq(schema.measureCatalogueEnabling.b_id_farm, b_id_farm)) // Delete custom fertilizers from the catalogue that belong to this farm await tx diff --git a/fdm-core/src/field.ts b/fdm-core/src/field.ts index 5663297f9..f97e370ea 100644 --- a/fdm-core/src/field.ts +++ b/fdm-core/src/field.ts @@ -655,6 +655,26 @@ export async function removeField( await tx .delete(schema.fertilizerApplication) .where(eq(schema.fertilizerApplication.b_id, b_id)) + + // Delete measures linked to this field + const measuresToDelete = await tx + .select({ b_id_measure: schema.measureAdopting.b_id_measure }) + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id, b_id)) + await tx + .delete(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id, b_id)) + if (measuresToDelete.length > 0) { + const measureIds = measuresToDelete.map( + (m: { + b_id_measure: schema.measuresTypeSelect["b_id_measure"] + }) => m.b_id_measure, + ) + await tx + .delete(schema.measures) + .where(inArray(schema.measures.b_id_measure, measureIds)) + } + await tx .delete(schema.fieldDiscarding) .where(eq(schema.fieldDiscarding.b_id, b_id)) diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index 65db9b7d7..11b4f0d40 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -33,15 +33,20 @@ export { export { disableCultivationCatalogue, disableFertilizerCatalogue, + disableMeasureCatalogue, enableCultivationCatalogue, enableFertilizerCatalogue, + enableMeasureCatalogue, getEnabledCultivationCatalogues, getEnabledCultivationCataloguesForFarms, getEnabledFertilizerCatalogues, getEnabledFertilizerCataloguesForFarms, + getEnabledMeasureCatalogues, isCultivationCatalogueEnabled, isFertilizerCatalogueEnabled, + isMeasureCatalogueEnabled, syncCatalogues, + syncMeasuresCatalogueArray, } from "./catalogues" export { addCultivation, @@ -76,6 +81,16 @@ export { listDerogations, removeDerogation, } from "./derogation" +export { + addMeasure, + getMeasure, + getMeasures, + getMeasuresForFarm, + getMeasuresFromCatalogue, + removeMeasure, + updateMeasure, +} from "./measure" +export type { Measure, MeasureCatalogue } from "./measure.types" export { addFarm, cancelInvitationForFarm, diff --git a/fdm-core/src/measure.test.ts b/fdm-core/src/measure.test.ts new file mode 100644 index 000000000..62277b240 --- /dev/null +++ b/fdm-core/src/measure.test.ts @@ -0,0 +1,772 @@ +import { eq, inArray } from "drizzle-orm" +import { afterAll, beforeEach, describe, expect, inject, it } from "vitest" +import { + syncMeasuresCatalogueArray, + enableMeasureCatalogue, +} from "./catalogues" +import * as schema from "./db/schema" +import { addFarm, removeFarm } from "./farm" +import type { FdmType } from "./fdm.types" +import { createFdmServer } from "./fdm-server" +import type { FdmServerType } from "./fdm-server.types" +import { addField, removeField } from "./field" +import { createId } from "./id" +import { + addMeasure, + getMeasure, + getMeasures, + getMeasuresForFarm, + getMeasuresFromCatalogue, + removeMeasure, + updateMeasure, +} from "./measure" + +const TEST_CATALOGUE_ITEM = { + m_id: "bln_BM1", + m_source: "bln", + m_name: "Toedienen compost", + m_description: "Toevoeging van compost verbetert de bodemstructuur.", + m_summary: "Compost toedienen", + m_source_url: "https://example.com/BM1", + m_conflicts: ["bln_BM2"], +} + +const TEST_CATALOGUE_ITEM_2 = { + m_id: "bln_BM2", + m_source: "bln", + m_name: "Aanleg groenbemester", + m_description: null, + m_summary: "Groenbemester inzaaien", + m_source_url: null, + m_conflicts: ["bln_BM1"], +} + +describe("Measure Data Model", () => { + let fdm: FdmServerType + let b_id_farm: string + let b_id: string + let principal_id: string + + beforeEach(async () => { + const host = inject("host") + const port = inject("port") + const user = inject("user") + const password = inject("password") + const database = inject("database") + fdm = createFdmServer(host, port, user, password, database) + principal_id = createId() + + b_id_farm = await addFarm( + fdm, + principal_id, + "Test Farm", + "123456", + "123 Farm Lane", + "12345", + ) + + b_id = await addField( + fdm, + principal_id, + b_id_farm, + "test field", + "test source", + { + type: "Polygon", + coordinates: [ + [ + [30, 10], + [40, 40], + [20, 40], + [10, 20], + [30, 10], + ], + ], + }, + new Date("2023-01-01"), + "nl_01", + new Date("2023-12-31"), + ) + + // Seed catalogue entries required by most tests + await syncMeasuresCatalogueArray(fdm, [ + TEST_CATALOGUE_ITEM, + TEST_CATALOGUE_ITEM_2, + ]) + // Enable the 'bln' catalogue for the test farm + await enableMeasureCatalogue(fdm, principal_id, b_id_farm, "bln") + }) + + afterAll(async () => { + // No cleanup needed — each test runs against a fresh isolated database + }) + + describe("getMeasuresFromCatalogue", () => { + it("should return seeded catalogue entries", async () => { + const catalogue = await getMeasuresFromCatalogue(fdm) + expect(catalogue.length).toBeGreaterThanOrEqual(2) + const ids = catalogue.map((c) => c.m_id) + expect(ids).toContain("bln_BM1") + expect(ids).toContain("bln_BM2") + }) + + it("should return entries ordered by source then name", async () => { + const catalogue = await getMeasuresFromCatalogue(fdm) + // Aanleg groenbemester < Toedienen compost alphabetically + const idx1 = catalogue.findIndex((c) => c.m_id === "bln_BM2") + const idx2 = catalogue.findIndex((c) => c.m_id === "bln_BM1") + expect(idx1).toBeLessThan(idx2) + }) + }) + + describe("addMeasure", () => { + it("should create a measure and return b_id_measure", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + expect(typeof b_id_measure).toBe("string") + expect(b_id_measure.length).toBeGreaterThan(0) + }) + + it("should insert rows into measures and measure_adopting", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + new Date("2023-12-31"), + ) + + const measureRow = await fdm + .select() + .from(schema.measures) + .where(eq(schema.measures.b_id_measure, b_id_measure)) + .limit(1) + expect(measureRow.length).toBe(1) + expect(measureRow[0].m_id).toBe("bln_BM1") + + const applyingRow = await fdm + .select() + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + .limit(1) + expect(applyingRow.length).toBe(1) + expect(applyingRow[0].b_id).toBe(b_id) + expect(applyingRow[0].m_start).toEqual(new Date("2023-03-01")) + expect(applyingRow[0].m_end).toEqual(new Date("2023-12-31")) + }) + + it("should set m_end to null when not provided (doorlopend)", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + const rows = await fdm + .select({ m_end: schema.measureAdopting.m_end }) + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + .limit(1) + expect(rows[0].m_end).toBeNull() + }) + + it("should throw for unknown m_id (FK violation)", async () => { + await expect( + addMeasure( + fdm, + principal_id, + b_id, + "bln_UNKNOWN", + new Date("2023-03-01"), + ), + ).rejects.toThrow() + }) + + it("should throw when m_end is earlier than m_start", async () => { + await expect( + addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-12-31"), + new Date("2023-01-01"), + ), + ).rejects.toThrow("m_end cannot be earlier than m_start") + }) + + it("should allow the same m_id twice on the same field when windows do NOT overlap", async () => { + // First half of the year + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-01-01"), + new Date("2023-06-30"), + ) + // Second half — adjacent but not overlapping + await expect( + addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-07-01"), + new Date("2023-12-31"), + ), + ).resolves.toBeTruthy() + }) + + it("should throw when the same m_id is added with an overlapping window", async () => { + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-01-01"), + new Date("2023-09-30"), + ) + await expect( + addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-06-01"), + new Date("2023-12-31"), + ), + ).rejects.toMatchObject({ + cause: { + message: expect.stringContaining("overlapping time window"), + }, + }) + }) + + it("should throw when an existing doorlopend measure of the same m_id is present", async () => { + // doorlopend (no end date) — overlaps everything after its start + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-01-01"), + ) + await expect( + addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-06-01"), + new Date("2023-12-31"), + ), + ).rejects.toMatchObject({ + cause: { + message: expect.stringContaining("overlapping time window"), + }, + }) + }) + + it("should throw when the catalogue is not enabled for the farm", async () => { + const other_farm = await addFarm( + fdm, + principal_id, + "Other Farm", + "999999", + "999 Other Lane", + "99999", + ) + const other_field = await addField( + fdm, + principal_id, + other_farm, + "other field", + "test source", + { + type: "Polygon", + coordinates: [ + [ + [30, 10], + [40, 40], + [20, 40], + [10, 20], + [30, 10], + ], + ], + }, + new Date("2023-01-01"), + "nl_01", + new Date("2023-12-31"), + ) + await expect( + addMeasure( + fdm, + principal_id, + other_field, + "bln_BM1", + new Date("2023-03-01"), + ), + ).rejects.toMatchObject({ + cause: { message: expect.stringContaining("not enabled") }, + }) + }) + }) + + describe("getMeasure", () => { + it("should return joined measure data", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + new Date("2023-12-31"), + ) + + const measure = await getMeasure(fdm, principal_id, b_id_measure) + + expect(measure.b_id_measure).toBe(b_id_measure) + expect(measure.m_id).toBe("bln_BM1") + expect(measure.b_id).toBe(b_id) + expect(measure.m_name).toBe("Toedienen compost") + expect(measure.m_summary).toBe("Compost toedienen") + expect(measure.m_conflicts).toEqual(["bln_BM2"]) + expect(measure.m_start).toEqual(new Date("2023-03-01")) + expect(measure.m_end).toEqual(new Date("2023-12-31")) + }) + + it("should throw when b_id_measure does not exist", async () => { + await expect( + getMeasure(fdm, principal_id, "nonexistent"), + ).rejects.toThrow() + }) + }) + + describe("getMeasures", () => { + it("should return all measures for a field", async () => { + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM2", + new Date("2023-05-01"), + ) + + const measures = await getMeasures(fdm, principal_id, b_id) + expect(measures.length).toBe(2) + }) + + it("should filter by timeframe — exclude measures outside range", async () => { + // Measure with end date within timeframe + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + new Date("2023-06-30"), + ) + // Doorlopend measure that starts before timeframe end + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM2", + new Date("2022-01-01"), + ) + + const measures = await getMeasures(fdm, principal_id, b_id, { + start: new Date("2023-01-01"), + end: new Date("2023-12-31"), + }) + expect(measures.length).toBe(2) + }) + + it("should return empty array when no measures match timeframe", async () => { + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2020-01-01"), + new Date("2020-12-31"), + ) + + const measures = await getMeasures(fdm, principal_id, b_id, { + start: new Date("2023-01-01"), + end: new Date("2023-12-31"), + }) + expect(measures.length).toBe(0) + }) + }) + + describe("getMeasuresForFarm", () => { + it("should return a Map keyed by b_id", async () => { + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + + const map = await getMeasuresForFarm(fdm, principal_id, b_id_farm) + expect(map).toBeInstanceOf(Map) + expect(map.has(b_id)).toBe(true) + expect(map.get(b_id)?.length).toBe(1) + }) + + it("should accumulate multiple measures per field in the Map", async () => { + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM2", + new Date("2023-05-01"), + ) + + const map = await getMeasuresForFarm(fdm, principal_id, b_id_farm) + expect(map.get(b_id)?.length).toBe(2) + }) + + it("should return empty Map when farm has no measures", async () => { + const map = await getMeasuresForFarm(fdm, principal_id, b_id_farm) + expect(map).toBeInstanceOf(Map) + expect(map.size).toBe(0) + }) + + it("should filter by timeframe", async () => { + // Measure within timeframe + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + new Date("2023-06-30"), + ) + // Measure outside timeframe + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM2", + new Date("2020-01-01"), + new Date("2020-12-31"), + ) + + const map = await getMeasuresForFarm(fdm, principal_id, b_id_farm, { + start: new Date("2023-01-01"), + end: new Date("2023-12-31"), + }) + expect(map.get(b_id)?.length).toBe(1) + expect(map.get(b_id)?.[0].m_id).toBe("bln_BM1") + }) + }) + + describe("updateMeasure", () => { + it("should update start date", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + + await updateMeasure( + fdm, + principal_id, + b_id_measure, + new Date("2023-04-01"), + ) + + const measure = await getMeasure(fdm, principal_id, b_id_measure) + expect(measure.m_start).toEqual(new Date("2023-04-01")) + }) + + it("should clear m_end when passed null (doorlopend)", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + new Date("2023-12-31"), + ) + + await updateMeasure( + fdm, + principal_id, + b_id_measure, + undefined, + null, + ) + + const measure = await getMeasure(fdm, principal_id, b_id_measure) + expect(measure.m_end).toBeNull() + }) + + it("should throw for unknown b_id_measure", async () => { + await expect( + updateMeasure(fdm, principal_id, "nonexistent"), + ).rejects.toThrow() + }) + + it("should throw when m_end is earlier than m_start", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + await expect( + updateMeasure( + fdm, + principal_id, + b_id_measure, + undefined, + new Date("2023-01-01"), + ), + ).rejects.toMatchObject({ + cause: { message: "m_end cannot be earlier than m_start" }, + }) + }) + + it("should throw when update would create an overlap with a sibling measure", async () => { + // Add first instance: Jan–Jun + await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-01-01"), + new Date("2023-06-30"), + ) + // Add second instance: Aug–Dec (non-overlapping so far) + const b_id_measure2 = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-08-01"), + new Date("2023-12-31"), + ) + // Now try to extend the second instance back into the first window + await expect( + updateMeasure( + fdm, + principal_id, + b_id_measure2, + new Date("2023-04-01"), + ), + ).rejects.toMatchObject({ + cause: { + message: expect.stringContaining("overlapping time window"), + }, + }) + }) + + it("should allow updating own window without a sibling", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-01-01"), + new Date("2023-06-30"), + ) + // Shift the window — no sibling, must succeed + await expect( + updateMeasure( + fdm, + principal_id, + b_id_measure, + new Date("2023-02-01"), + new Date("2023-09-30"), + ), + ).resolves.toBeUndefined() + }) + }) + + describe("removeMeasure", () => { + it("should delete both measures and measure_adopting rows", async () => { + const b_id_measure = await addMeasure( + fdm, + principal_id, + b_id, + "bln_BM1", + new Date("2023-03-01"), + ) + + await removeMeasure(fdm, principal_id, b_id_measure) + + const measureRows = await fdm + .select() + .from(schema.measures) + .where(eq(schema.measures.b_id_measure, b_id_measure)) + expect(measureRows.length).toBe(0) + + const applyingRows = await fdm + .select() + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + expect(applyingRows.length).toBe(0) + }) + + it("should throw for unknown b_id_measure", async () => { + await expect( + removeMeasure(fdm, principal_id, "nonexistent"), + ).rejects.toThrow() + }) + }) +}) + +describe("FdmType compatibility", () => { + it("should accept FdmType (not just FdmServerType) for getMeasure", async () => { + const host = inject("host") + const port = inject("port") + const user = inject("user") + const password = inject("password") + const database = inject("database") + const fdm: FdmType = createFdmServer( + host, + port, + user, + password, + database, + ) + // Just check that the function signature accepts FdmType + await expect(getMeasure(fdm, "any", "nonexistent")).rejects.toThrow() + }) +}) + +describe("Measure cascade deletion", () => { + let fdm: FdmServerType + let principal_id: string + let b_id_farm: string + let b_id: string + + beforeEach(async () => { + const host = inject("host") + const port = inject("port") + const user = inject("user") + const password = inject("password") + const database = inject("database") + fdm = createFdmServer(host, port, user, password, database) + principal_id = "test_principal" + b_id_farm = await addFarm( + fdm, + principal_id, + "Test Farm", + "123456", + "Addr", + "1234AB", + ) + b_id = await addField( + fdm, + principal_id, + b_id_farm, + "Test Field", + "acquiring", + { + type: "Polygon", + coordinates: [ + [ + [30, 10], + [40, 40], + [20, 40], + [10, 20], + [30, 10], + ], + ], + }, + new Date("2023-01-01"), + "nl_01", + ) + await syncMeasuresCatalogueArray(fdm, [ + { + m_id: "bln_DEL1", + m_source: "bln", + m_name: "Del Measure", + m_description: null, + m_summary: null, + m_source_url: null, + m_conflicts: null, + }, + ]) + await enableMeasureCatalogue(fdm, principal_id, b_id_farm, "bln") + await addMeasure( + fdm, + principal_id, + b_id, + "bln_DEL1", + new Date("2023-01-01"), + undefined, + ) + }) + + it("should delete measures when the field is removed", async () => { + await removeField(fdm, principal_id, b_id) + + const applyingAfter = await fdm + .select() + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id, b_id)) + expect(applyingAfter).toHaveLength(0) + + const measuresAfter = await fdm + .select() + .from(schema.measures) + .innerJoin( + schema.measureAdopting, + eq( + schema.measures.b_id_measure, + schema.measureAdopting.b_id_measure, + ), + ) + .where(eq(schema.measureAdopting.b_id, b_id)) + expect(measuresAfter).toHaveLength(0) + }) + + it("should delete measures when the farm is removed", async () => { + // Capture the measure_adopting rows for our field before deletion + const applyingBefore = await fdm + .select({ b_id_measure: schema.measureAdopting.b_id_measure }) + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id, b_id)) + expect(applyingBefore.length).toBeGreaterThan(0) + const measureIds = applyingBefore.map((r) => r.b_id_measure) + + await removeFarm(fdm, principal_id, b_id_farm) + + const measuresAfter = await fdm + .select() + .from(schema.measures) + .where(inArray(schema.measures.b_id_measure, measureIds)) + expect(measuresAfter).toHaveLength(0) + }) +}) diff --git a/fdm-core/src/measure.ts b/fdm-core/src/measure.ts new file mode 100644 index 000000000..ff484a95c --- /dev/null +++ b/fdm-core/src/measure.ts @@ -0,0 +1,613 @@ +import { + and, + asc, + desc, + eq, + gte, + isNotNull, + isNull, + lte, + ne, + or, + type SQL, +} from "drizzle-orm" +import { checkPermission } from "./authorization" +import type { PrincipalId } from "./authorization.types" +import type { Measure, MeasureCatalogue } from "./measure.types" +import * as schema from "./db/schema" +import { handleError } from "./error" +import type { FdmType } from "./fdm.types" +import { createId } from "./id" +import type { Timeframe } from "./timeframe" + +/** + * Throws if adding/updating a measure for `(b_id, m_id)` would create an + * overlapping time window with an existing instance of the same catalogue item + * on the same field. + * + * Two windows [A_start, A_end] and [B_start, B_end] overlap when: + * A_start ≤ B_end AND A_end ≥ B_start + * A NULL end date means the measure is ongoing (doorlopend / end = ∞). + * + * @param excludeId Optional `b_id_measure` to skip (used during updates so a + * measure is not compared against itself). + */ +async function assertNoMeasureOverlap( + fdm: FdmType, + b_id: schema.fieldsTypeSelect["b_id"], + m_id: schema.measuresCatalogueTypeSelect["m_id"], + m_start: Date, + m_end: Date | null | undefined, + excludeId?: schema.measuresTypeSelect["b_id_measure"], +): Promise { + const existing = await fdm + .select({ + b_id_measure: schema.measures.b_id_measure, + m_start: schema.measureAdopting.m_start, + m_end: schema.measureAdopting.m_end, + }) + .from(schema.measures) + .innerJoin( + schema.measureAdopting, + eq( + schema.measureAdopting.b_id_measure, + schema.measures.b_id_measure, + ), + ) + .where( + and( + eq(schema.measureAdopting.b_id, b_id), + eq(schema.measures.m_id, m_id), + excludeId + ? ne(schema.measures.b_id_measure, excludeId) + : undefined, + ), + ) + + for (const row of existing) { + const existStart = row.m_start + const existEnd = row.m_end // null = doorlopend (∞) + + if (!existStart) continue + + // A_start ≤ B_end (null B_end = ∞, so condition is always true) + const newStartBeforeExistEnd = + existEnd === null || m_start.getTime() <= existEnd.getTime() + // A_end ≥ B_start (null A_end = ∞, so condition is always true) + const newEndAfterExistStart = + m_end == null || m_end.getTime() >= existStart.getTime() + + if (newStartBeforeExistEnd && newEndAfterExistStart) { + throw new Error( + `Measure "${m_id}" already exists for this field with an overlapping time window`, + ) + } + } +} + +/** + * Creates a measure instance and applies it to a field in a single transaction. + * + * @param fdm The FDM instance providing the connection to the database. + * @param principal_id The ID of the principal making the request. + * @param b_id The ID of the field to apply the measure to. + * @param m_id The catalogue ID of the measure (e.g. "bln_BM1"). + * @param m_start The start date of the measure. + * @param m_end The optional end date. Omit or pass undefined for doorlopend (ongoing). + * @returns A Promise resolving to the new `b_id_measure`. + */ +export async function addMeasure( + fdm: FdmType, + principal_id: PrincipalId, + b_id: schema.fieldsTypeSelect["b_id"], + m_id: schema.measuresCatalogueTypeSelect["m_id"], + m_start: Date, + m_end?: Date, +): Promise { + if (m_end !== undefined && m_end.getTime() < m_start.getTime()) { + throw new Error("m_end cannot be earlier than m_start") + } + try { + await checkPermission( + fdm, + "field", + "write", + b_id, + principal_id, + "addMeasure", + ) + + // Verify the catalogue is enabled for the farm this field belongs to + const fieldFarm = await fdm + .select({ b_id_farm: schema.fieldAcquiring.b_id_farm }) + .from(schema.fieldAcquiring) + .where(eq(schema.fieldAcquiring.b_id, b_id)) + .limit(1) + const catalogueSource = await fdm + .select({ m_source: schema.measuresCatalogue.m_source }) + .from(schema.measuresCatalogue) + .where(eq(schema.measuresCatalogue.m_id, m_id)) + .limit(1) + if (catalogueSource.length === 0) { + throw new Error(`Measure catalogue item not found: ${m_id}`) + } + if (fieldFarm.length > 0) { + const enabled = await fdm + .select() + .from(schema.measureCatalogueEnabling) + .where( + and( + eq( + schema.measureCatalogueEnabling.b_id_farm, + fieldFarm[0].b_id_farm, + ), + eq( + schema.measureCatalogueEnabling.m_source, + catalogueSource[0].m_source, + ), + ), + ) + .limit(1) + if (enabled.length === 0) { + throw new Error( + `Measure catalogue "${catalogueSource[0].m_source}" is not enabled for this farm`, + ) + } + } + + const b_id_measure = createId() + await assertNoMeasureOverlap(fdm, b_id, m_id, m_start, m_end ?? null) + await fdm.transaction(async (tx) => { + await tx.insert(schema.measures).values({ b_id_measure, m_id }) + await tx.insert(schema.measureAdopting).values({ + b_id, + b_id_measure, + m_start, + m_end: m_end ?? null, + }) + }) + return b_id_measure + } catch (err) { + throw handleError(err, "Exception for addMeasure", { + b_id, + m_id, + m_start, + m_end, + }) + } +} + +/** + * Fetches a single measure (joined with catalogue data) by its instance ID. + * + * @param fdm The FDM instance providing the connection to the database. + * @param principal_id The ID of the principal making the request. + * @param b_id_measure The instance ID of the measure. + * @returns A Promise resolving to the {@link Measure}. + */ +export async function getMeasure( + fdm: FdmType, + principal_id: PrincipalId, + b_id_measure: schema.measuresTypeSelect["b_id_measure"], +): Promise { + try { + // Resolve b_id for permission check + const applying = await fdm + .select({ b_id: schema.measureAdopting.b_id }) + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + .limit(1) + + if (applying.length === 0) { + throw new Error("Measure does not exist") + } + + await checkPermission( + fdm, + "field", + "read", + applying[0].b_id, + principal_id, + "getMeasure", + ) + + const rows = await fdm + .select({ + b_id_measure: schema.measures.b_id_measure, + m_id: schema.measuresCatalogue.m_id, + b_id: schema.measureAdopting.b_id, + m_start: schema.measureAdopting.m_start, + m_end: schema.measureAdopting.m_end, + m_name: schema.measuresCatalogue.m_name, + m_summary: schema.measuresCatalogue.m_summary, + m_conflicts: schema.measuresCatalogue.m_conflicts, + }) + .from(schema.measures) + .innerJoin( + schema.measureAdopting, + eq( + schema.measureAdopting.b_id_measure, + schema.measures.b_id_measure, + ), + ) + .innerJoin( + schema.measuresCatalogue, + eq(schema.measures.m_id, schema.measuresCatalogue.m_id), + ) + .where(eq(schema.measures.b_id_measure, b_id_measure)) + .limit(1) + + if (rows.length === 0) { + throw new Error("Measure does not exist") + } + + return rows[0] as Measure + } catch (err) { + throw handleError(err, "Exception for getMeasure", { b_id_measure }) + } +} + +/** + * Fetches all measures applied to a specific field, optionally filtered by timeframe. + * + * @param fdm The FDM instance providing the connection to the database. + * @param principal_id The ID of the principal making the request. + * @param b_id The field ID. + * @param timeframe Optional timeframe to filter measures (overlap logic). + * @returns A Promise resolving to an array of {@link Measure}. + */ +export async function getMeasures( + fdm: FdmType, + principal_id: PrincipalId, + b_id: schema.fieldsTypeSelect["b_id"], + timeframe?: Timeframe, +): Promise { + try { + await checkPermission( + fdm, + "field", + "read", + b_id, + principal_id, + "getMeasures", + ) + + const timeframeCondition = buildMeasureTimeframeCondition(timeframe) + + const rows = await fdm + .select({ + b_id_measure: schema.measures.b_id_measure, + m_id: schema.measuresCatalogue.m_id, + b_id: schema.measureAdopting.b_id, + m_start: schema.measureAdopting.m_start, + m_end: schema.measureAdopting.m_end, + m_name: schema.measuresCatalogue.m_name, + m_summary: schema.measuresCatalogue.m_summary, + m_conflicts: schema.measuresCatalogue.m_conflicts, + }) + .from(schema.measures) + .innerJoin( + schema.measureAdopting, + eq( + schema.measureAdopting.b_id_measure, + schema.measures.b_id_measure, + ), + ) + .innerJoin( + schema.measuresCatalogue, + eq(schema.measures.m_id, schema.measuresCatalogue.m_id), + ) + .where( + timeframeCondition + ? and( + eq(schema.measureAdopting.b_id, b_id), + timeframeCondition, + ) + : eq(schema.measureAdopting.b_id, b_id), + ) + .orderBy( + desc(schema.measureAdopting.m_start), + asc(schema.measuresCatalogue.m_name), + ) + + return rows as Measure[] + } catch (err) { + throw handleError(err, "Exception for getMeasures", { b_id }) + } +} + +/** + * Fetches measures for all fields in a farm. Returns a Map keyed by `b_id`. + * + * @param fdm The FDM instance providing the connection to the database. + * @param principal_id The ID of the principal making the request. + * @param b_id_farm The farm ID. + * @param timeframe Optional timeframe to filter measures. + * @returns A Promise resolving to a `Map`. + */ +export async function getMeasuresForFarm( + fdm: FdmType, + principal_id: PrincipalId, + b_id_farm: schema.farmsTypeSelect["b_id_farm"], + timeframe?: Timeframe, +): Promise> { + try { + await checkPermission( + fdm, + "farm", + "read", + b_id_farm, + principal_id, + "getMeasuresForFarm", + ) + + const timeframeCondition = buildMeasureTimeframeCondition(timeframe) + + const rows = await fdm + .select({ + b_id_measure: schema.measures.b_id_measure, + m_id: schema.measuresCatalogue.m_id, + b_id: schema.measureAdopting.b_id, + m_start: schema.measureAdopting.m_start, + m_end: schema.measureAdopting.m_end, + m_name: schema.measuresCatalogue.m_name, + m_summary: schema.measuresCatalogue.m_summary, + m_conflicts: schema.measuresCatalogue.m_conflicts, + }) + .from(schema.measures) + .innerJoin( + schema.measureAdopting, + eq( + schema.measureAdopting.b_id_measure, + schema.measures.b_id_measure, + ), + ) + .innerJoin( + schema.measuresCatalogue, + eq(schema.measures.m_id, schema.measuresCatalogue.m_id), + ) + .innerJoin( + schema.fieldAcquiring, + eq(schema.fieldAcquiring.b_id, schema.measureAdopting.b_id), + ) + .where( + timeframeCondition + ? and( + eq(schema.fieldAcquiring.b_id_farm, b_id_farm), + timeframeCondition, + ) + : eq(schema.fieldAcquiring.b_id_farm, b_id_farm), + ) + .orderBy( + desc(schema.measureAdopting.m_start), + asc(schema.measuresCatalogue.m_name), + ) + + const result = new Map() + for (const row of rows) { + if (!row.b_id) continue + const existing = result.get(row.b_id) + if (existing) { + existing.push(row as Measure) + } else { + result.set(row.b_id, [row as Measure]) + } + } + return result + } catch (err) { + throw handleError(err, "Exception for getMeasuresForFarm", { + b_id_farm, + }) + } +} + +/** + * Returns all available entries from the `measures_catalogue` table. + * + * No permission check is performed — the catalogue is not per-user data. + * + * @param fdm The FDM instance providing the connection to the database. + * @returns A Promise resolving to an array of {@link MeasureCatalogue}. + */ +export async function getMeasuresFromCatalogue( + fdm: FdmType, +): Promise { + try { + return fdm + .select({ + m_id: schema.measuresCatalogue.m_id, + m_source: schema.measuresCatalogue.m_source, + m_name: schema.measuresCatalogue.m_name, + m_description: schema.measuresCatalogue.m_description, + m_summary: schema.measuresCatalogue.m_summary, + m_source_url: schema.measuresCatalogue.m_source_url, + m_conflicts: schema.measuresCatalogue.m_conflicts, + }) + .from(schema.measuresCatalogue) + .orderBy( + asc(schema.measuresCatalogue.m_source), + asc(schema.measuresCatalogue.m_name), + ) + } catch (err) { + throw handleError(err, "Exception for getMeasuresFromCatalogue", {}) + } +} + +/** + * Updates the start and/or end date of an existing measure. + * Pass `m_end = null` to clear the end date (doorlopend). + * + * @param fdm The FDM instance providing the connection to the database. + * @param principal_id The ID of the principal making the request. + * @param b_id_measure The instance ID of the measure. + * @param m_start Optional new start date. + * @param m_end Optional new end date. Pass `null` to clear it (doorlopend). + */ +export async function updateMeasure( + fdm: FdmType, + principal_id: PrincipalId, + b_id_measure: schema.measuresTypeSelect["b_id_measure"], + m_start?: Date, + m_end?: Date | null, +): Promise { + try { + const applying = await fdm + .select({ + b_id: schema.measureAdopting.b_id, + m_start: schema.measureAdopting.m_start, + m_end: schema.measureAdopting.m_end, + }) + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + .limit(1) + + if (applying.length === 0) { + throw new Error("Measure does not exist") + } + + await checkPermission( + fdm, + "field", + "write", + applying[0].b_id, + principal_id, + "updateMeasure", + ) + + const effectiveStart = m_start ?? applying[0].m_start ?? undefined + const effectiveEnd = m_end !== undefined ? m_end : applying[0].m_end + if ( + effectiveStart && + effectiveEnd instanceof Date && + effectiveEnd.getTime() < effectiveStart.getTime() + ) { + throw new Error("m_end cannot be earlier than m_start") + } + + // Fetch m_id for the overlap check + const measureRow = await fdm + .select({ m_id: schema.measures.m_id }) + .from(schema.measures) + .where(eq(schema.measures.b_id_measure, b_id_measure)) + .limit(1) + if (measureRow.length > 0) { + await assertNoMeasureOverlap( + fdm, + applying[0].b_id, + measureRow[0].m_id, + effectiveStart ?? new Date(0), + effectiveEnd instanceof Date ? effectiveEnd : null, + b_id_measure, + ) + } + + const updates: Partial = { + updated: new Date(), + } + if (m_start !== undefined) updates.m_start = m_start + if (m_end !== undefined) updates.m_end = m_end + + await fdm + .update(schema.measureAdopting) + .set(updates) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + } catch (err) { + throw handleError(err, "Exception for updateMeasure", { + b_id_measure, + m_start, + m_end, + }) + } +} + +/** + * Deletes a measure instance and its `measure_adopting` row. + * + * @param fdm The FDM instance providing the connection to the database. + * @param principal_id The ID of the principal making the request. + * @param b_id_measure The instance ID of the measure to remove. + */ +export async function removeMeasure( + fdm: FdmType, + principal_id: PrincipalId, + b_id_measure: schema.measuresTypeSelect["b_id_measure"], +): Promise { + try { + const applying = await fdm + .select({ b_id: schema.measureAdopting.b_id }) + .from(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + .limit(1) + + if (applying.length === 0) { + throw new Error("Measure does not exist") + } + + await checkPermission( + fdm, + "field", + "write", + applying[0].b_id, + principal_id, + "removeMeasure", + ) + + await fdm.transaction(async (tx) => { + await tx + .delete(schema.measureAdopting) + .where(eq(schema.measureAdopting.b_id_measure, b_id_measure)) + await tx + .delete(schema.measures) + .where(eq(schema.measures.b_id_measure, b_id_measure)) + }) + } catch (err) { + throw handleError(err, "Exception for removeMeasure", { b_id_measure }) + } +} + +/** + * Builds a SQL condition for filtering measures based on a timeframe overlap. + * + * A measure overlaps a timeframe if: + * 1. It has an end date AND (starts within, ends within, or spans the timeframe) + * 2. It has no end date (doorlopend) AND its start is on or before the timeframe's end + * + * @param timeframe An object with optional `start` and `end` Date properties. + * @returns A Drizzle-ORM SQL condition, or `undefined` if the timeframe is not provided. + */ +export const buildMeasureTimeframeCondition = ( + timeframe: Timeframe | undefined, +): SQL | undefined => { + if (!timeframe?.start || !timeframe?.end) { + return undefined + } + + return or( + // Case 1: Measure has an end date and overlaps with the timeframe + and( + isNotNull(schema.measureAdopting.m_end), + or( + // Measure starts within the timeframe + and( + gte(schema.measureAdopting.m_start, timeframe.start), + lte(schema.measureAdopting.m_start, timeframe.end), + ), + // Measure ends within the timeframe + and( + gte(schema.measureAdopting.m_end, timeframe.start), + lte(schema.measureAdopting.m_end, timeframe.end), + ), + // Measure spans the entire timeframe + and( + lte(schema.measureAdopting.m_start, timeframe.start), + gte(schema.measureAdopting.m_end, timeframe.end), + ), + ), + ), + // Case 2: Measure has no end date and its start is on or before the timeframe's end + and( + isNull(schema.measureAdopting.m_end), + lte(schema.measureAdopting.m_start, timeframe.end), + ), + ) +} diff --git a/fdm-core/src/measure.types.ts b/fdm-core/src/measure.types.ts new file mode 100644 index 000000000..bbdad3497 --- /dev/null +++ b/fdm-core/src/measure.types.ts @@ -0,0 +1,21 @@ +export type MeasureCatalogue = { + m_id: string // "bln_BM1", etc. + m_source: string // "bln", etc. + m_name: string + m_description: string | null + m_summary: string | null + m_source_url: string | null + m_conflicts: string[] | null // m_id values +} + +export type Measure = { + b_id_measure: string + m_id: string + b_id: string + m_start: Date | null + m_end: Date | null // null = doorlopend / ongoing + // Denormalized from measures_catalogue: + m_name: string + m_summary: string | null + m_conflicts: string[] | null +} diff --git a/fdm-data/src/index.ts b/fdm-data/src/index.ts index 3ed58cc56..1827da0c2 100644 --- a/fdm-data/src/index.ts +++ b/fdm-data/src/index.ts @@ -26,3 +26,6 @@ export type { CatalogueFertilizerName, } from "./fertilizers/d" export { hashFertilizer } from "./fertilizers/hash" +export { getMeasuresCatalogue } from "./measures" +export type { CatalogueMeasure, CatalogueMeasureItem, CatalogueMeasureName } from "./measures/d" +export { hashMeasure } from "./measures/hash" diff --git a/fdm-data/src/measures/catalogues/bln.ts b/fdm-data/src/measures/catalogues/bln.ts new file mode 100644 index 000000000..b682269c8 --- /dev/null +++ b/fdm-data/src/measures/catalogues/bln.ts @@ -0,0 +1,62 @@ +import type { CatalogueMeasure } from "../d" + +interface BLN3ApiMeasure { + bln_id: string + name: string + summary: string | null + description: string | null + source_url: string | null + conflicts_with_measure: string[] | null +} + +const FETCH_TIMEOUT_MS = 30_000 + +/** + * Fetches the BLN3 measures catalogue from the NMI API. + * + * Transforms the API response from BLN3-specific naming to the pandex naming + * convention used throughout FDM. The `bln_id` is namespaced as `m_id = "bln_{bln_id}"` + * so measures from different frameworks can coexist in the same table. + * + * @param nmiApiKey - Bearer token for the NMI API + * @returns Array of catalogue items in pandex naming convention + */ +export async function getCatalogueBln( + nmiApiKey: string, +): Promise { + let res: Response + try { + res = await fetch("https://api.nmi-agro.nl/maatwerk/bln3/measures", { + headers: { Authorization: `Bearer ${nmiApiKey}` }, + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }) + } catch (err) { + if (err instanceof DOMException && err.name === "TimeoutError") { + throw new Error( + `Fetching BLN measures catalogue timed out after ${FETCH_TIMEOUT_MS}ms`, + ) + } + throw err + } + if (!res.ok) { + throw new Error( + `Failed to fetch BLN measures catalogue: ${res.status} ${res.statusText}`, + ) + } + const json = await res.json() + if (!json?.data || !Array.isArray(json.data.measures)) { + throw new Error( + `Unexpected response shape from BLN measures catalogue API: expected json.data.measures to be an array, got ${JSON.stringify(json)}`, + ) + } + return json.data.measures.map((item: BLN3ApiMeasure) => ({ + m_id: `bln_${item.bln_id}`, + m_source: "bln", + m_name: item.name, + m_description: item.description ?? null, + m_summary: item.summary ?? null, + m_source_url: item.source_url ?? null, + m_conflicts: + item.conflicts_with_measure?.map((id) => `bln_${id}`) ?? null, + })) +} diff --git a/fdm-data/src/measures/d.ts b/fdm-data/src/measures/d.ts new file mode 100644 index 000000000..e50876d23 --- /dev/null +++ b/fdm-data/src/measures/d.ts @@ -0,0 +1,14 @@ +export type CatalogueMeasureName = "bln" + +export interface CatalogueMeasureItem { + m_id: string // "bln_BM1", "bln_BM2", etc. + m_source: CatalogueMeasureName | string // "bln"; future: "anlb", etc. + m_name: string + m_description: string | null + m_summary: string | null + m_source_url: string | null + m_conflicts: string[] | null // m_id values, e.g. ["bln_BM2"] + hash?: string | null +} + +export type CatalogueMeasure = CatalogueMeasureItem[] diff --git a/fdm-data/src/measures/hash.test.ts b/fdm-data/src/measures/hash.test.ts new file mode 100644 index 000000000..4dbfd2c9b --- /dev/null +++ b/fdm-data/src/measures/hash.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest" +import type { CatalogueMeasureItem } from "./d" +import { hashMeasure } from "./hash" + +const baseMeasure: CatalogueMeasureItem = { + m_id: "bln_BM1", + m_source: "bln", + m_name: "Toedienen compost", + m_description: "Toevoeging van compost verbetert de bodemstructuur.", + m_summary: "Compost toedienen", + m_source_url: "https://example.com/BM1", + m_conflicts: ["bln_BM2"], +} + +describe("hashMeasure", () => { + it("should generate a hash string", async () => { + const hash = await hashMeasure({ ...baseMeasure }) + expect(hash).toBeDefined() + expect(typeof hash).toBe("string") + expect(hash.length).toBeGreaterThan(0) + }) + + it("should generate the same hash for identical items", async () => { + const hash1 = await hashMeasure({ ...baseMeasure }) + const hash2 = await hashMeasure({ ...baseMeasure }) + expect(hash1).toBe(hash2) + }) + + it("should generate different hashes for different items", async () => { + const hash1 = await hashMeasure({ ...baseMeasure }) + const hash2 = await hashMeasure({ ...baseMeasure, m_name: "Updated name" }) + expect(hash1).not.toBe(hash2) + }) + + it("should be stable even when hash field is pre-set to null", async () => { + const item1 = { ...baseMeasure, hash: null } + const item2 = { ...baseMeasure } + const hash1 = await hashMeasure(item1) + const hash2 = await hashMeasure(item2) + expect(hash1).toBe(hash2) + }) + + it("should not mutate the input object", async () => { + const item = { ...baseMeasure, hash: "existing-hash" } + await hashMeasure(item) + expect(item.hash).toBe("existing-hash") + }) + + it("should detect changes in m_conflicts array", async () => { + const hash1 = await hashMeasure({ ...baseMeasure, m_conflicts: ["bln_BM2"] }) + const hash2 = await hashMeasure({ ...baseMeasure, m_conflicts: ["bln_BM3"] }) + expect(hash1).not.toBe(hash2) + }) +}) diff --git a/fdm-data/src/measures/hash.ts b/fdm-data/src/measures/hash.ts new file mode 100644 index 000000000..ac29c0973 --- /dev/null +++ b/fdm-data/src/measures/hash.ts @@ -0,0 +1,29 @@ +import { ensureInitialized, h32ToString } from "../hash" +import type { CatalogueMeasureItem } from "./d" + +export async function hashMeasure(measure: CatalogueMeasureItem) { + await ensureInitialized() + // Work on a shallow copy so the caller's object is not mutated + const copy = { ...measure } + copy.hash = null + + // Remove all keys without a value + const filteredMeasure = Object.fromEntries( + Object.entries(copy).filter( + ([, value]) => value !== undefined && value !== null, + ), + ) + + // Sort keys to ensure consistent hash generation for identical objects + const sortedKeys = Object.keys(filteredMeasure).sort() + const sortedMeasure = sortedKeys.reduce>( + (obj, key) => { + obj[key] = copy[key as keyof typeof copy] + return obj + }, + {}, + ) + + const hash = h32ToString(JSON.stringify(sortedMeasure)) + return hash +} diff --git a/fdm-data/src/measures/index.ts b/fdm-data/src/measures/index.ts new file mode 100644 index 000000000..8f4089e4d --- /dev/null +++ b/fdm-data/src/measures/index.ts @@ -0,0 +1,31 @@ +import { getCatalogueBln } from "./catalogues/bln" +import type { CatalogueMeasure, CatalogueMeasureName } from "./d" + +/** + * Retrieves a measures catalogue based on the specified name. + * + * This function acts as a dispatcher, selecting and returning the appropriate + * measures catalogue based on the provided `catalogueName`. + * + * @param catalogueName - The name of the desired measures catalogue. Currently + * supported: `"bln"`. + * @param nmiApiKey - Bearer token for the NMI API (required for all current catalogues). + * @returns A Promise that resolves to an array of `CatalogueMeasureItem` objects. + * @throws {Error} Throws an error if the provided `catalogueName` is not recognized. + * + * @example + * ```typescript + * const blnCatalogue = await getMeasuresCatalogue("bln", apiKey) + * console.log(blnCatalogue) + * ``` + */ +export async function getMeasuresCatalogue( + catalogueName: CatalogueMeasureName, + nmiApiKey: string, +): Promise { + if (catalogueName === "bln") { + return await getCatalogueBln(nmiApiKey) + } + throw new Error(`catalogue ${catalogueName} is not recognized`) +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b89b1b98..b68d8ff10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,7 +61,7 @@ importers: dependencies: '@google/adk': specifier: ^1.1.0 - version: 1.1.0(@grpc/grpc-js@1.14.3)(@mikro-orm/mariadb@6.6.11(@mikro-orm/core@6.6.14)(pg@8.20.0))(@mikro-orm/mssql@6.6.11(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/mysql@6.6.11(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/postgresql@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5))(@mikro-orm/sqlite@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + version: 1.1.0(@grpc/grpc-js@1.14.3)(@mikro-orm/mariadb@6.6.14(@mikro-orm/core@6.6.14)(pg@8.20.0))(@mikro-orm/mssql@6.6.14(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/mysql@6.6.14(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/postgresql@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5))(@mikro-orm/sqlite@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) '@nmi-agro/fdm-calculator': specifier: workspace:^ version: link:../fdm-calculator @@ -92,7 +92,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.2)(@vitest/coverage-v8@4.1.5)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)) + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.6.2)(@vitest/coverage-v8@4.1.5)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)) fdm-app: dependencies: @@ -182,7 +182,7 @@ importers: version: 7.3.5 better-auth: specifier: 'catalog:' - version: 1.6.10(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))(mysql2@3.20.0(@types/node@25.6.2))(next@16.2.3(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.5) + version: 1.6.10(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))(mysql2@3.20.0(@types/node@25.6.2))(next@16.2.3(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.5) chrono-node: specifier: ^2.9.1 version: 2.9.1 @@ -203,7 +203,7 @@ importers: version: 3.4.2 drizzle-orm: specifier: 'catalog:' - version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) + version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) file-type: specifier: ^22.0.1 version: 22.0.1 @@ -284,7 +284,7 @@ importers: version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) recharts: specifier: ^3.8.1 - version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1) + version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@16.13.1)(react@19.2.6)(redux@5.0.1) remark-gfm: specifier: ^4.0.1 version: 4.0.1 @@ -302,7 +302,7 @@ importers: version: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) tailwind-merge: specifier: ^3.5.0 - version: 3.5.0 + version: 3.6.0 tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@4.3.0) @@ -421,13 +421,13 @@ importers: version: 7946.0.16 better-auth: specifier: 'catalog:' - version: 1.6.10(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))(mysql2@3.20.0(@types/node@25.6.2))(next@16.2.3(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.5) + version: 1.6.10(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))(mysql2@3.20.0(@types/node@25.6.2))(next@16.2.3(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.5) decimal.js: specifier: ^10.6.0 version: 10.6.0 drizzle-orm: specifier: 'catalog:' - version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) + version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) nanoid: specifier: ^5.1.11 version: 5.1.11 @@ -3318,8 +3318,8 @@ packages: resolution: {integrity: sha512-jKdtf1A2wI2D48phOPJzTc3h7Bev64Ype0FHwbUgHEdZ5VxrCNLKOziFnYqMfPmBe0piVExLaPN2qXgbzCiApw==} engines: {node: '>= 18.12.0'} - '@mikro-orm/knex@6.6.11': - resolution: {integrity: sha512-MUxqw+3COpcM06DC3ufW4Aov5RZWpW1Rv/kMfJkHQX+bO81jPdinXkRtx1l8EVWFRiLJEB+3MNhptFQRlmJNXA==} + '@mikro-orm/knex@6.6.14': + resolution: {integrity: sha512-xQWq9+7TwE8LLul1RkhjB7/0/iCHMlkSmEToVpz+NNFoPj6M32DfY9mhNnM6qPZ/HF50WjpcVgCgi9ADrEBSFA==} engines: {node: '>= 18.12.0'} peerDependencies: '@mikro-orm/core': ^6.0.0 @@ -3334,26 +3334,26 @@ packages: mariadb: optional: true - '@mikro-orm/mariadb@6.6.11': - resolution: {integrity: sha512-TPUFGJHGPGiQC2LE263iyyBbaG1nwSsa6UVQ8ma2QFxLRt62XqGFEw7XJ1uUXXoqZn/4RW8jrIAWFgbrmfnx3g==} + '@mikro-orm/mariadb@6.6.14': + resolution: {integrity: sha512-utm833ym7ScKN9szU+BZoOQqmuXPm2WIIruC66OZIGLze9kw4eGUdoT+QD8kvq2bzGux2RZZ/9AdzjcxDWVvWg==} engines: {node: '>= 18.12.0'} peerDependencies: '@mikro-orm/core': ^6.0.0 - '@mikro-orm/mssql@6.6.11': - resolution: {integrity: sha512-LjMiObzrwKJw9Pt/VUgAgsNU3j/FDZjW8wxvD8522rPdXnNyHtNBDAQQhdWt/e+yJr4bZh7HhoQYekH8Z9G6Yg==} + '@mikro-orm/mssql@6.6.14': + resolution: {integrity: sha512-juofAWhCkN+Pa/g/ppI8hMvqoWzvAX2GG2THc2+7UU33iLAcepFunRudertHgzb+XkpxwVn9I9wSRQcvwRBmvw==} engines: {node: '>= 18.12.0'} peerDependencies: '@mikro-orm/core': ^6.0.0 - '@mikro-orm/mysql@6.6.11': - resolution: {integrity: sha512-SPtBLl82Qq+pKx/d5rF276LosKz6JO7D8vTaeudadk6/zlXjpE3SciGmyvt5/+htzts4k348F8zQMCf297NdzQ==} + '@mikro-orm/mysql@6.6.14': + resolution: {integrity: sha512-H52L3LnHuTbB6PTYK583MzijMywyuRrJnEoKGzVjUkH4VCXOo9wp4Cppk+CBXn9JP0Ngd59CCoGUIGKRg4p/NA==} engines: {node: '>= 18.12.0'} peerDependencies: '@mikro-orm/core': ^6.0.0 - '@mikro-orm/postgresql@6.6.11': - resolution: {integrity: sha512-YIQroXsAPXRJc3ruk8M5ynbQEQtGUO0Swjb/MMtjn5o9qypqmPBoq4ANCwUY9P2jVlmheQM1O5VK/1OBm7/EVg==} + '@mikro-orm/postgresql@6.6.14': + resolution: {integrity: sha512-hgyxpuTaXK0nYhhkmPkz8lx1nzhsqtOQuqQ+oabtyEKuqzPeANRJaV2TczIFYMIczyxKWOylV7g//13qrwqmNQ==} engines: {node: '>= 18.12.0'} peerDependencies: '@mikro-orm/core': ^6.0.0 @@ -3364,8 +3364,8 @@ packages: peerDependencies: '@mikro-orm/core': ^6.0.0 - '@mikro-orm/sqlite@6.6.11': - resolution: {integrity: sha512-WCO9w6JERp7qMRJKXoNF1ELrQ6PrMBU24EwDdhkY8LH76uqDM4jtfSbIcBDafORiZG/D+Rs8JshS1qEQEX9x7w==} + '@mikro-orm/sqlite@6.6.14': + resolution: {integrity: sha512-SJCGMB8gJgfsGK3MROpHphyCpCBat/Cc2TE5Py4A7SZ82eGzYEpT/dMBpJ+OyRGk/Irpvf6PJiKfgSZog5CaFQ==} engines: {node: '>= 18.12.0'} peerDependencies: '@mikro-orm/core': ^6.0.0 @@ -7222,8 +7222,8 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} - cssdb@8.8.0: - resolution: {integrity: sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==} + cssdb@8.8.1: + resolution: {integrity: sha512-PdLTDamqN1muXEmfQggrogLmD+ZjfOhlZsFFs28tYSTqnlk6gEwg5wQCt6wLl2HstegUYgof6GrYyXXODFDC5g==} cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -9154,8 +9154,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - knex@3.2.8: - resolution: {integrity: sha512-ElXXxu9Nq+5hWYdBUddYIWIT5yKKs5KNCsmKGbJSHPyaMpAABp3xs4L55GgdQoAs6QQ7dv72ai3M4pxYQ8utEg==} + knex@3.2.10: + resolution: {integrity: sha512-oypTHfrc9i72iyxaUQBKHOxhcr0xM65MPf6FpN02nimsftXwzXprIkLjfXdubvhbu4PMWLp023q8o8CYvHSuZw==} engines: {node: '>=16'} hasBin: true peerDependencies: @@ -11028,9 +11028,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-json-view-lite@2.5.0: resolution: {integrity: sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==} engines: {node: '>=18'} @@ -11986,8 +11983,8 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} @@ -14074,12 +14071,12 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))': + '@better-auth/drizzle-adapter@1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))': dependencies: '@better-auth/core': 1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 optionalDependencies: - drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) '@better-auth/kysely-adapter@1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.17)': dependencies: @@ -15986,12 +15983,12 @@ snapshots: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': dependencies: - '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) '@google-cloud/precise-date': 4.0.0 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) google-auth-library: 9.15.1(encoding@0.1.13) @@ -16000,13 +15997,13 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': dependencies: - '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) '@grpc/grpc-js': 1.14.3 '@grpc/proto-loader': 0.8.1 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) google-auth-library: 9.15.1(encoding@0.1.13) @@ -16014,9 +16011,9 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-resource-util@3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': + '@google-cloud/opentelemetry-resource-util@3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': dependencies: - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 gcp-metadata: 6.1.1(encoding@0.1.13) @@ -16056,20 +16053,20 @@ snapshots: - encoding - supports-color - '@google/adk@1.1.0(@grpc/grpc-js@1.14.3)(@mikro-orm/mariadb@6.6.11(@mikro-orm/core@6.6.14)(pg@8.20.0))(@mikro-orm/mssql@6.6.11(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/mysql@6.6.11(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/postgresql@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5))(@mikro-orm/sqlite@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google/adk@1.1.0(@grpc/grpc-js@1.14.3)(@mikro-orm/mariadb@6.6.14(@mikro-orm/core@6.6.14)(pg@8.20.0))(@mikro-orm/mssql@6.6.14(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/mysql@6.6.14(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/postgresql@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5))(@mikro-orm/sqlite@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0))(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': dependencies: '@a2a-js/sdk': 0.3.13(@grpc/grpc-js@1.14.3)(express@4.22.1) - '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) - '@google-cloud/opentelemetry-cloud-trace-exporter': 3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@google-cloud/opentelemetry-cloud-trace-exporter': 3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) '@google-cloud/storage': 7.19.0(encoding@0.1.13) '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) '@mikro-orm/core': 6.6.14 - '@mikro-orm/mariadb': 6.6.11(@mikro-orm/core@6.6.14)(pg@8.20.0) - '@mikro-orm/mssql': 6.6.11(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0) - '@mikro-orm/mysql': 6.6.11(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0) - '@mikro-orm/postgresql': 6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5) + '@mikro-orm/mariadb': 6.6.14(@mikro-orm/core@6.6.14)(pg@8.20.0) + '@mikro-orm/mssql': 6.6.14(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0) + '@mikro-orm/mysql': 6.6.14(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0) + '@mikro-orm/postgresql': 6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5) '@mikro-orm/reflection': 6.6.14(@mikro-orm/core@6.6.14) - '@mikro-orm/sqlite': 6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0) + '@mikro-orm/sqlite': 6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.205.0 @@ -16571,11 +16568,11 @@ snapshots: mikro-orm: 6.6.14 reflect-metadata: 0.2.2 - '@mikro-orm/knex@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)': + '@mikro-orm/knex@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)': dependencies: '@mikro-orm/core': 6.6.14 fs-extra: 11.3.3 - knex: 3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7) + knex: 3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7) sqlstring: 2.3.3 optionalDependencies: mariadb: 3.4.5 @@ -16589,11 +16586,11 @@ snapshots: - supports-color - tedious - '@mikro-orm/knex@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7)': + '@mikro-orm/knex@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7)': dependencies: '@mikro-orm/core': 6.6.14 fs-extra: 11.3.3 - knex: 3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7) + knex: 3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7) sqlstring: 2.3.3 optionalDependencies: mariadb: 3.4.5 @@ -16607,11 +16604,11 @@ snapshots: - supports-color - tedious - '@mikro-orm/knex@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1))': + '@mikro-orm/knex@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1))': dependencies: '@mikro-orm/core': 6.6.14 fs-extra: 11.3.3 - knex: 3.2.8(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)) + knex: 3.2.10(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)) sqlstring: 2.3.3 optionalDependencies: mariadb: 3.4.5 @@ -16625,10 +16622,10 @@ snapshots: - supports-color - tedious - '@mikro-orm/mariadb@6.6.11(@mikro-orm/core@6.6.14)(pg@8.20.0)': + '@mikro-orm/mariadb@6.6.14(@mikro-orm/core@6.6.14)(pg@8.20.0)': dependencies: '@mikro-orm/core': 6.6.14 - '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0) + '@mikro-orm/knex': 6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0) mariadb: 3.4.5 transitivePeerDependencies: - better-sqlite3 @@ -16642,10 +16639,10 @@ snapshots: - supports-color - tedious - '@mikro-orm/mssql@6.6.11(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)': + '@mikro-orm/mssql@6.6.14(@azure/core-client@1.10.1)(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)': dependencies: '@mikro-orm/core': 6.6.14 - '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)) + '@mikro-orm/knex': 6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)) tedious: 19.2.1(@azure/core-client@1.10.1) tsqlstring: 1.0.1 transitivePeerDependencies: @@ -16661,10 +16658,10 @@ snapshots: - sqlite3 - supports-color - '@mikro-orm/mysql@6.6.11(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0)': + '@mikro-orm/mysql@6.6.14(@mikro-orm/core@6.6.14)(@types/node@25.6.2)(mariadb@3.4.5)(pg@8.20.0)': dependencies: '@mikro-orm/core': 6.6.14 - '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0) + '@mikro-orm/knex': 6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0) mysql2: 3.20.0(@types/node@25.6.2) transitivePeerDependencies: - '@types/node' @@ -16679,10 +16676,10 @@ snapshots: - supports-color - tedious - '@mikro-orm/postgresql@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)': + '@mikro-orm/postgresql@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)': dependencies: '@mikro-orm/core': 6.6.14 - '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0) + '@mikro-orm/knex': 6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0) pg: 8.20.0 postgres-array: 3.0.4 postgres-date: 2.1.0 @@ -16705,10 +16702,10 @@ snapshots: globby: 11.1.0 ts-morph: 27.0.2 - '@mikro-orm/sqlite@6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)': + '@mikro-orm/sqlite@6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)': dependencies: '@mikro-orm/core': 6.6.14 - '@mikro-orm/knex': 6.6.11(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7) + '@mikro-orm/knex': 6.6.14(@mikro-orm/core@6.6.14)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7) fs-extra: 11.3.3 sqlite3: 5.1.7 sqlstring-sqlite: 0.1.1 @@ -19800,7 +19797,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.2)(@vitest/coverage-v8@4.1.5)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)) + vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.6.2)(@vitest/coverage-v8@4.1.5)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)) '@vitest/expect@4.1.5': dependencies: @@ -20244,10 +20241,10 @@ snapshots: batch@0.6.1: {} - better-auth@1.6.10(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))(mysql2@3.20.0(@types/node@25.6.2))(next@16.2.3(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.5): + better-auth@1.6.10(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7))(mysql2@3.20.0(@types/node@25.6.2))(next@16.2.3(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.5): dependencies: '@better-auth/core': 1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) - '@better-auth/drizzle-adapter': 1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7)) + '@better-auth/drizzle-adapter': 1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7)) '@better-auth/kysely-adapter': 1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.17) '@better-auth/memory-adapter': 1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0) '@better-auth/mongo-adapter': 1.6.10(@better-auth/core@1.6.10(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0) @@ -20265,7 +20262,7 @@ snapshots: zod: 4.4.3 optionalDependencies: drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7) mysql2: 3.20.0(@types/node@25.6.2) next: 16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) pg: 8.20.0 @@ -20896,7 +20893,7 @@ snapshots: css-what@6.2.2: {} - cssdb@8.8.0: {} + cssdb@8.8.1: {} cssesc@3.0.0: {} @@ -21211,11 +21208,11 @@ snapshots: esbuild: 0.25.12 tsx: 4.21.0 - drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7): + drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7))(kysely@0.28.17)(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(postgres@3.4.9)(sqlite3@5.1.7): optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/pg': 8.15.6 - knex: 3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7) + knex: 3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7) kysely: 0.28.17 mysql2: 3.20.0(@types/node@25.6.2) pg: 8.20.0 @@ -23067,7 +23064,7 @@ snapshots: kleur@3.0.3: {} - knex@3.2.8(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7): + knex@3.2.10(mysql2@3.20.0(@types/node@25.6.2))(pg@8.20.0)(sqlite3@5.1.7): dependencies: colorette: 2.0.19 commander: 10.0.1 @@ -23090,7 +23087,7 @@ snapshots: transitivePeerDependencies: - supports-color - knex@3.2.8(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)): + knex@3.2.10(pg@8.20.0)(tedious@19.2.1(@azure/core-client@1.10.1)): dependencies: colorette: 2.0.19 commander: 10.0.1 @@ -24939,7 +24936,7 @@ snapshots: css-blank-pseudo: 7.0.1(postcss@8.5.14) css-has-pseudo: 7.0.3(postcss@8.5.14) css-prefers-color-scheme: 10.0.0(postcss@8.5.14) - cssdb: 8.8.0 + cssdb: 8.8.1 postcss: 8.5.14 postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.14) postcss-clamp: 4.1.0(postcss@8.5.14) @@ -25380,8 +25377,6 @@ snapshots: react-is@16.13.1: {} - react-is@18.3.1: {} - react-json-view-lite@2.5.0(react@19.2.6): dependencies: react: 19.2.6 @@ -25540,7 +25535,7 @@ snapshots: readdirp@4.1.2: {} - recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1): + recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@16.13.1)(react@19.2.6)(redux@5.0.1): dependencies: '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6) clsx: 2.1.1 @@ -25550,7 +25545,7 @@ snapshots: immer: 10.2.0 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - react-is: 18.3.1 + react-is: 16.13.1 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 @@ -26595,7 +26590,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwind-merge@3.5.0: {} + tailwind-merge@3.6.0: {} tailwindcss-animate@1.0.7(tailwindcss@4.3.0): dependencies: @@ -27230,6 +27225,35 @@ snapshots: tsx: 4.21.0 yaml: 2.8.4 + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.6.2)(@vitest/coverage-v8@4.1.5)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 25.6.2 + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) + transitivePeerDependencies: + - msw + vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.2)(@vitest/coverage-v8@4.1.5)(vite@8.0.11(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)): dependencies: '@vitest/expect': 4.1.5