feat: T4b wave 7 - schema for phenology, honey movement ledger, shelf life
- Phenology reports, apiary phenology state and weather days with permissions for system roles - Honey movement ledger: BOTTLING movements and initialAmount backfilled from existing bottlings with the same gram rounding; the migration reports counts and aborts if the ledger invariant fails - Shelf life and event catalog columns; backup export/import of the new tables, old bundles keep importing, invariant check after imports - e2e: test timeout 20 s (app start in hooks exceeded 5 s under load) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
326b3d9cdd
commit
22382d53a2
17 files changed
+1213
-39
No files matched your search
@@ -0,0 +1,105 @@
|
||||
-- T4 migration 4/n: plant development (phenology) and the season segment.
|
||||
-- PhenologyReport holds the beekeeper's own reports; ApiaryPhenology and
|
||||
-- ApiaryWeatherDay are derived/cached state written only by the
|
||||
-- daily-evaluation step and the weather loop.
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PhenologyPhase" AS ENUM ('SALWEIDE', 'KIRSCHE', 'APFEL', 'HOLUNDER', 'LINDE', 'TRACHTENDE', 'HERBST', 'WINTER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SeasonSegment" AS ENUM ('WINTERRUHE', 'FRUEHJAHR', 'SCHWARMZEIT', 'SOMMER', 'SPAETSOMMER', 'HERBST');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PhaseSource" AS ENUM ('REPORT', 'GTS', 'CALENDAR');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PhenologyReport" (
|
||||
"id" TEXT NOT NULL,
|
||||
"apiaryId" TEXT,
|
||||
"phase" "PhenologyPhase" NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL,
|
||||
"notes" TEXT,
|
||||
"userId" TEXT NOT NULL,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PhenologyReport_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiaryPhenology" (
|
||||
"apiaryId" TEXT NOT NULL,
|
||||
"year" INTEGER NOT NULL,
|
||||
"gts" DOUBLE PRECISION,
|
||||
"gtsThroughDay" TIMESTAMP(3),
|
||||
"missingDays" INTEGER NOT NULL DEFAULT 0,
|
||||
"estimatedPhase" "PhenologyPhase",
|
||||
"reportedPhase" "PhenologyPhase",
|
||||
"effectivePhase" "PhenologyPhase",
|
||||
"phaseSource" "PhaseSource",
|
||||
"segment" "SeasonSegment" NOT NULL,
|
||||
"segmentSince" TIMESTAMP(3) NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ApiaryPhenology_pkey" PRIMARY KEY ("apiaryId","year")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiaryWeatherDay" (
|
||||
"apiaryId" TEXT NOT NULL,
|
||||
"day" TIMESTAMP(3) NOT NULL,
|
||||
"meanTempC" DOUBLE PRECISION,
|
||||
"source" TEXT NOT NULL,
|
||||
"fetchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ApiaryWeatherDay_pkey" PRIMARY KEY ("apiaryId","day")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PhenologyReport_date_idx" ON "PhenologyReport"("date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PhenologyReport_apiaryId_phase_date_idx" ON "PhenologyReport"("apiaryId", "phase", "date");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PhenologyReport" ADD CONSTRAINT "PhenologyReport_apiaryId_fkey" FOREIGN KEY ("apiaryId") REFERENCES "Apiary"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PhenologyReport" ADD CONSTRAINT "PhenologyReport_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiaryPhenology" ADD CONSTRAINT "ApiaryPhenology_apiaryId_fkey" FOREIGN KEY ("apiaryId") REFERENCES "Apiary"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiaryWeatherDay" ADD CONSTRAINT "ApiaryWeatherDay_apiaryId_fkey" FOREIGN KEY ("apiaryId") REFERENCES "Apiary"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Permissions: the boot-time catalog sync grants new permissions to system
|
||||
-- roles only for rows it creates itself, and these rows exist before the
|
||||
-- first boot. So system roles (ADMIN: full access) get all three here; this
|
||||
-- mirrors what PermissionCatalogSyncService does for permissions it creates.
|
||||
-- Phenology reports are everyday beekeeping records, so non-system roles get
|
||||
-- them like winter-checks (assumption 16): read with colonies:read,
|
||||
-- create/update with inspections:create.
|
||||
INSERT INTO "Permission" (id, key, resource, action, "labelDe", "groupDe", "sortOrder")
|
||||
VALUES
|
||||
(gen_random_uuid()::text, 'phenology:read', 'phenology', 'read', 'Ansehen', 'Pflanzenentwicklung', 860),
|
||||
(gen_random_uuid()::text, 'phenology:create', 'phenology', 'create', 'Melden', 'Pflanzenentwicklung', 861),
|
||||
(gen_random_uuid()::text, 'phenology:update', 'phenology', 'update', 'Bearbeiten', 'Pflanzenentwicklung', 862)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE r."isSystem" = true AND p.resource = 'phenology'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT DISTINCT rp."roleId", p.id, now()
|
||||
FROM "RolePermission" rp
|
||||
JOIN "Role" r ON r.id = rp."roleId" AND r."isSystem" = false
|
||||
JOIN "Permission" have ON have.id = rp."permissionId"
|
||||
JOIN "Permission" p ON p.resource = 'phenology'
|
||||
WHERE (have.key = 'colonies:read' AND p.action = 'read')
|
||||
OR (have.key = 'inspections:create' AND p.action IN ('create', 'update'))
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,84 @@
|
||||
-- T4 migration 6/n: ledger of the honey batch. HoneyBatch.amount (kg) stays
|
||||
-- the running rest; initialAmount (kg) is the starting quantity, and
|
||||
-- amount = initialAmount + sum(BatchMovement.quantity) / 1000
|
||||
-- (quantity in grams, signed). Every bottling is booked as a BOTTLING
|
||||
-- movement. Earlier hand corrections of amount are folded into initialAmount.
|
||||
--
|
||||
-- Invariant check afterwards (must return 0 rows):
|
||||
-- SELECT hb."id" FROM "HoneyBatch" hb
|
||||
-- LEFT JOIN (SELECT "honeyBatchId", sum("quantity") g FROM "BatchMovement"
|
||||
-- WHERE "honeyBatchId" IS NOT NULL GROUP BY 1) m ON m."honeyBatchId" = hb."id"
|
||||
-- WHERE hb."initialAmount" IS NOT NULL AND hb."amount" IS NOT NULL
|
||||
-- AND abs(hb."amount" - (hb."initialAmount" + coalesce(m.g, 0) / 1000.0)) > 0.0005;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "BatchMovement" ADD COLUMN "honeyBatchId" TEXT,
|
||||
ADD COLUMN "sourceBottlingBatchId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "HoneyBatch" ADD COLUMN "initialAmount" DOUBLE PRECISION;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "BatchMovement_honeyBatchId_idx" ON "BatchMovement"("honeyBatchId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "BatchMovement_sourceBottlingBatchId_idx" ON "BatchMovement"("sourceBottlingBatchId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BatchMovement" ADD CONSTRAINT "BatchMovement_honeyBatchId_fkey" FOREIGN KEY ("honeyBatchId") REFERENCES "HoneyBatch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BatchMovement" ADD CONSTRAINT "BatchMovement_sourceBottlingBatchId_fkey" FOREIGN KEY ("sourceBottlingBatchId") REFERENCES "BottlingBatch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- A movement now belongs to exactly one of four batch kinds (name kept from
|
||||
-- 20261005100000_raw_materials_production).
|
||||
ALTER TABLE "BatchMovement" DROP CONSTRAINT "BatchMovement_exactly_one_batch";
|
||||
ALTER TABLE "BatchMovement" ADD CONSTRAINT "BatchMovement_exactly_one_batch"
|
||||
CHECK (num_nonnulls("bottlingBatchId", "productionBatchId", "rawMaterialLotId", "honeyBatchId") = 1);
|
||||
|
||||
-- Recalculation, only for batches with a recorded amount (a batch without
|
||||
-- one is not booked, as before).
|
||||
-- 1. initialAmount = amount + the bottlings rounded to the gram exactly like
|
||||
-- the bookings below (kg), so the invariant holds without a rounding gap.
|
||||
UPDATE "HoneyBatch" hb
|
||||
SET "initialAmount" = hb."amount" + coalesce((
|
||||
SELECT sum(floor(b."totalAmount" * 1000 + 0.5)) FROM "BottlingBatch" b WHERE b."honeyBatchId" = hb."id"
|
||||
), 0) / 1000.0
|
||||
WHERE hb."amount" IS NOT NULL;
|
||||
|
||||
-- 2. One BOTTLING movement per bottling, in grams, dated like the bottling.
|
||||
-- floor(x + 0.5) rounds half up like the application (Math.round), whereas
|
||||
-- round(double precision) may round half to even.
|
||||
INSERT INTO "BatchMovement" ("id", "honeyBatchId", "sourceBottlingBatchId", "type", "quantity", "notes", "userId", "createdAt")
|
||||
SELECT gen_random_uuid()::text, b."honeyBatchId", b."id", 'BOTTLING',
|
||||
-(floor(b."totalAmount" * 1000 + 0.5))::integer,
|
||||
'Übernahme aus Abfüllung (T4)', b."userId", b."createdAt"
|
||||
FROM "BottlingBatch" b
|
||||
JOIN "HoneyBatch" hb ON hb."id" = b."honeyBatchId"
|
||||
WHERE hb."amount" IS NOT NULL;
|
||||
|
||||
-- Report and self-check: the counts go to the migration log; a broken
|
||||
-- invariant aborts the whole migration (the file runs in one transaction).
|
||||
DO $$
|
||||
DECLARE
|
||||
batches integer;
|
||||
bookings integer;
|
||||
skipped integer;
|
||||
broken integer;
|
||||
BEGIN
|
||||
SELECT count(*) INTO batches FROM "HoneyBatch" WHERE "initialAmount" IS NOT NULL;
|
||||
SELECT count(*) INTO bookings FROM "BatchMovement" WHERE "honeyBatchId" IS NOT NULL AND "type" = 'BOTTLING';
|
||||
SELECT count(*) INTO skipped FROM "BottlingBatch" b
|
||||
JOIN "HoneyBatch" hb ON hb."id" = b."honeyBatchId" WHERE hb."amount" IS NULL;
|
||||
RAISE NOTICE 'honey ledger: % batches with initialAmount, % bookings created, % bottlings skipped (batch amount is NULL)',
|
||||
batches, bookings, skipped;
|
||||
|
||||
SELECT count(*) INTO broken FROM "HoneyBatch" hb
|
||||
LEFT JOIN (SELECT "honeyBatchId", sum("quantity") AS g FROM "BatchMovement"
|
||||
WHERE "honeyBatchId" IS NOT NULL GROUP BY 1) m ON m."honeyBatchId" = hb."id"
|
||||
WHERE hb."initialAmount" IS NOT NULL AND hb."amount" IS NOT NULL
|
||||
AND abs(hb."amount" - (hb."initialAmount" + coalesce(m.g, 0) / 1000.0)) > 0.0005;
|
||||
IF broken > 0 THEN
|
||||
RAISE EXCEPTION 'honey ledger invariant broken for % batches', broken;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- T4 migration 7/n (part shelf life): Product.shelfLifeMonths, the shelf life
|
||||
-- in months, used to pre-fill the best-before date. No default value.
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "shelfLifeMonths" INTEGER;
|
||||
@@ -111,6 +111,7 @@ model User {
|
||||
feedings Feeding[]
|
||||
harvests Harvest[]
|
||||
extractions Extraction[]
|
||||
phenologyReports PhenologyReport[]
|
||||
honeyMeasurements HoneyMeasurement[]
|
||||
stockMovements StockMovement[]
|
||||
assignedTasks Task[] @relation("TaskAssignee")
|
||||
@@ -240,11 +241,14 @@ model Apiary {
|
||||
updatedAt DateTime @updatedAt
|
||||
deactivatedAt DateTime?
|
||||
|
||||
colonies Colony[]
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
harvests Harvest[]
|
||||
treatments Treatment[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
colonies Colony[]
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
harvests Harvest[]
|
||||
treatments Treatment[]
|
||||
rawMaterialLots RawMaterialLot[]
|
||||
phenologyReports PhenologyReport[]
|
||||
phenologyStates ApiaryPhenology[]
|
||||
weatherDays ApiaryWeatherDay[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
@@ -928,6 +932,9 @@ model HoneyBatch {
|
||||
extraction Extraction @relation(fields: [extractionId], references: [id])
|
||||
honeyType String?
|
||||
amount Float?
|
||||
// Ausgangsmenge in kg; amount = initialAmount + Summe der Buchungen / 1000.
|
||||
// null = Menge nicht erfasst, dann wird nicht gebucht.
|
||||
initialAmount Float?
|
||||
waterContent Float?
|
||||
storageContainerId String?
|
||||
storageContainer StorageContainer? @relation(fields: [storageContainerId], references: [id])
|
||||
@@ -939,6 +946,7 @@ model HoneyBatch {
|
||||
bottlingBatches BottlingBatch[]
|
||||
relocations HoneyBatchRelocation[]
|
||||
containerAllocations HoneyBatchContainerAllocation[]
|
||||
movements BatchMovement[]
|
||||
|
||||
@@index([status])
|
||||
@@index([extractionId])
|
||||
@@ -1105,18 +1113,20 @@ enum ShopCategory {
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
sku String? @unique
|
||||
packagingSize Float?
|
||||
weight Float?
|
||||
salesUnit String?
|
||||
status ProductStatus @default(ACTIVE)
|
||||
kind ProductKind @default(BOTTLED_HONEY)
|
||||
shopCategory ShopCategory @default(HONIG)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
sku String? @unique
|
||||
packagingSize Float?
|
||||
weight Float?
|
||||
salesUnit String?
|
||||
// Haltbarkeit in Monaten, kein Standardwert (MHD-Vorbelegung bei der Abfuellung).
|
||||
shelfLifeMonths Int?
|
||||
status ProductStatus @default(ACTIVE)
|
||||
kind ProductKind @default(BOTTLED_HONEY)
|
||||
shopCategory ShopCategory @default(HONIG)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
bottlingBatches BottlingBatch[]
|
||||
productionBatches ProductionBatch[]
|
||||
@@ -1174,7 +1184,8 @@ model BottlingBatch {
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
movements BatchMovement[]
|
||||
movements BatchMovement[]
|
||||
causedMovements BatchMovement[] @relation("BatchMovementSourceBottling")
|
||||
|
||||
@@index([honeyBatchId])
|
||||
@@index([productId])
|
||||
@@ -1288,28 +1299,36 @@ enum WithdrawalReason {
|
||||
// Genau eine der beiden Chargen-Spalten ist gesetzt (CHECK-Constraint in der
|
||||
// Migration).
|
||||
model BatchMovement {
|
||||
id String @id @default(cuid())
|
||||
bottlingBatchId String?
|
||||
bottlingBatch BottlingBatch? @relation(fields: [bottlingBatchId], references: [id], onDelete: Restrict)
|
||||
productionBatchId String?
|
||||
productionBatch ProductionBatch? @relation(fields: [productionBatchId], references: [id], onDelete: Restrict)
|
||||
rawMaterialLotId String?
|
||||
rawMaterialLot RawMaterialLot? @relation(fields: [rawMaterialLotId], references: [id], onDelete: Restrict)
|
||||
type BatchMovementType
|
||||
id String @id @default(cuid())
|
||||
bottlingBatchId String?
|
||||
bottlingBatch BottlingBatch? @relation(fields: [bottlingBatchId], references: [id], onDelete: Restrict)
|
||||
productionBatchId String?
|
||||
productionBatch ProductionBatch? @relation(fields: [productionBatchId], references: [id], onDelete: Restrict)
|
||||
rawMaterialLotId String?
|
||||
rawMaterialLot RawMaterialLot? @relation(fields: [rawMaterialLotId], references: [id], onDelete: Restrict)
|
||||
// Honig: Bewegungsbuch der Honigcharge, quantity dann in Gramm.
|
||||
honeyBatchId String?
|
||||
honeyBatch HoneyBatch? @relation(fields: [honeyBatchId], references: [id], onDelete: Restrict)
|
||||
// Rueckverweis auf die verursachende Abfuellung (BOTTLING).
|
||||
sourceBottlingBatchId String?
|
||||
sourceBottlingBatch BottlingBatch? @relation("BatchMovementSourceBottling", fields: [sourceBottlingBatchId], references: [id], onDelete: SetNull)
|
||||
type BatchMovementType
|
||||
// Mit Vorzeichen: minus = raus, plus = rein.
|
||||
quantity Int
|
||||
reason WithdrawalReason?
|
||||
notes String?
|
||||
shopRequestId String?
|
||||
shopRequest ShopStockRequest? @relation(fields: [shopRequestId], references: [id], onDelete: Restrict)
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
apiKeyId String?
|
||||
createdAt DateTime @default(now())
|
||||
quantity Int
|
||||
reason WithdrawalReason?
|
||||
notes String?
|
||||
shopRequestId String?
|
||||
shopRequest ShopStockRequest? @relation(fields: [shopRequestId], references: [id], onDelete: Restrict)
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
apiKeyId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([bottlingBatchId])
|
||||
@@index([productionBatchId])
|
||||
@@index([rawMaterialLotId])
|
||||
@@index([honeyBatchId])
|
||||
@@index([sourceBottlingBatchId])
|
||||
@@index([shopRequestId])
|
||||
// ERP-Export /bewegungen: sortiert und blättert nach (createdAt, id).
|
||||
@@index([createdAt, id])
|
||||
@@ -1789,3 +1808,85 @@ model ThrottlerHit {
|
||||
@@id([key, throttlerName])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pflanzenentwicklung und Saisonabschnitt (T4b, Spec 4.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum PhenologyPhase {
|
||||
SALWEIDE
|
||||
KIRSCHE
|
||||
APFEL
|
||||
HOLUNDER
|
||||
LINDE
|
||||
TRACHTENDE
|
||||
HERBST
|
||||
WINTER
|
||||
}
|
||||
|
||||
enum SeasonSegment {
|
||||
WINTERRUHE
|
||||
FRUEHJAHR
|
||||
SCHWARMZEIT
|
||||
SOMMER
|
||||
SPAETSOMMER
|
||||
HERBST
|
||||
}
|
||||
|
||||
enum PhaseSource {
|
||||
REPORT
|
||||
GTS
|
||||
CALENDAR
|
||||
}
|
||||
|
||||
// Eigene Meldung per Antippen; zurueckgezogen statt geloescht.
|
||||
model PhenologyReport {
|
||||
id String @id @default(cuid())
|
||||
// null = alle Staende
|
||||
apiaryId String?
|
||||
apiary Apiary? @relation(fields: [apiaryId], references: [id], onDelete: Restrict)
|
||||
phase PhenologyPhase
|
||||
date DateTime
|
||||
notes String?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([date])
|
||||
@@index([apiaryId, phase, date])
|
||||
}
|
||||
|
||||
// Abgeleiteter Zustand, schreibt nur der Job/Handler (nicht im Backup).
|
||||
model ApiaryPhenology {
|
||||
apiaryId String
|
||||
apiary Apiary @relation(fields: [apiaryId], references: [id], onDelete: Cascade)
|
||||
year Int
|
||||
gts Float?
|
||||
gtsThroughDay DateTime?
|
||||
missingDays Int @default(0)
|
||||
estimatedPhase PhenologyPhase?
|
||||
reportedPhase PhenologyPhase?
|
||||
effectivePhase PhenologyPhase?
|
||||
phaseSource PhaseSource?
|
||||
segment SeasonSegment
|
||||
segmentSince DateTime
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([apiaryId, year])
|
||||
}
|
||||
|
||||
// Tagesmittel je Stand, schreibt nur die Worker-Schleife (nicht im Backup).
|
||||
model ApiaryWeatherDay {
|
||||
apiaryId String
|
||||
apiary Apiary @relation(fields: [apiaryId], references: [id], onDelete: Cascade)
|
||||
// Kalendertag Europe/Berlin
|
||||
day DateTime
|
||||
meanTempC Float?
|
||||
// 'forecast' | 'archive'
|
||||
source String
|
||||
fetchedAt DateTime @default(now())
|
||||
|
||||
@@id([apiaryId, day])
|
||||
}
|
||||
Reference in new issue
Block a user