v0.1.0 — Moon Base initial scaffold: auth, garden plant registry + log
This commit is contained in:
92
prisma/migrations/20260615000000_init/migration.sql
Normal file
92
prisma/migrations/20260615000000_init/migration.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
-- Moon Base v0.1 — initial schema
|
||||
-- Creates auth, audit log, and the garden plant registry.
|
||||
|
||||
-- Enums
|
||||
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'MEMBER');
|
||||
CREATE TYPE "PlantCategory" AS ENUM (
|
||||
'CANOPY_TREE', 'UNDERSTORY_TREE', 'LARGE_SHRUB', 'SHRUB',
|
||||
'HERB', 'GROUNDCOVER', 'VINE', 'ANNUAL', 'PERENNIAL',
|
||||
'BULB', 'MUSHROOM', 'OTHER'
|
||||
);
|
||||
CREATE TYPE "PlantLogType" AS ENUM (
|
||||
'OBSERVATION', 'CARE', 'HARVEST', 'TREATMENT', 'TRANSPLANT', 'DIED'
|
||||
);
|
||||
|
||||
-- User
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"username" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"role" "UserRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"mustChangePassword" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- AuditEvent
|
||||
CREATE TABLE "AuditEvent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"entityId" TEXT,
|
||||
"actorId" TEXT,
|
||||
"payload" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "AuditEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "AuditEvent_entityId_idx" ON "AuditEvent"("entityId");
|
||||
CREATE INDEX "AuditEvent_createdAt_idx" ON "AuditEvent"("createdAt");
|
||||
|
||||
-- Plant
|
||||
CREATE TABLE "Plant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"commonName" TEXT NOT NULL,
|
||||
"species" TEXT,
|
||||
"variety" TEXT,
|
||||
"category" "PlantCategory" NOT NULL,
|
||||
"zone" TEXT,
|
||||
"plantedAt" TIMESTAMP(3),
|
||||
"source" TEXT,
|
||||
"notes" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "Plant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "Plant_category_idx" ON "Plant"("category");
|
||||
CREATE INDEX "Plant_zone_idx" ON "Plant"("zone");
|
||||
CREATE INDEX "Plant_active_idx" ON "Plant"("active");
|
||||
|
||||
-- PlantLog
|
||||
CREATE TABLE "PlantLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"plantId" TEXT NOT NULL,
|
||||
"type" "PlantLogType" NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"notes" TEXT,
|
||||
"quantity" TEXT,
|
||||
"actorId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "PlantLog_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "PlantLog_plantId_fkey" FOREIGN KEY ("plantId") REFERENCES "Plant"("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX "PlantLog_plantId_idx" ON "PlantLog"("plantId");
|
||||
CREATE INDEX "PlantLog_date_idx" ON "PlantLog"("date");
|
||||
|
||||
-- Provision admin accounts (bonna + tony)
|
||||
-- Passwords are bcrypt hashes of temporary values — change on first login.
|
||||
-- bonna: moonbase! | tony: moonbase!
|
||||
INSERT INTO "User" ("id", "email", "username", "name", "passwordHash", "role", "active", "mustChangePassword", "createdAt", "updatedAt")
|
||||
VALUES
|
||||
('cluser0000000bonna', 'bonna@moonbase.local', 'bonna', 'Bonna Moon',
|
||||
'$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'ADMIN', true, true,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('cluser0000000tony0', 'tony@moonbase.local', 'tony', 'Tony Moon',
|
||||
'$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'ADMIN', true, true,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT ("username") DO UPDATE SET "updatedAt" = CURRENT_TIMESTAMP;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
113
prisma/schema.prisma
Normal file
113
prisma/schema.prisma
Normal file
@@ -0,0 +1,113 @@
|
||||
// Moon Base — property management for the Moon homestead.
|
||||
// Tracks the food forest (plants, care logs, harvests), home maintenance,
|
||||
// and equipment. v0.1 ships the garden module; home + equipment come next.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum UserRole {
|
||||
ADMIN // full access — add/edit/delete anything
|
||||
MEMBER // can log observations and care, but not delete records
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
username String? @unique
|
||||
name String
|
||||
passwordHash String
|
||||
role UserRole @default(MEMBER)
|
||||
active Boolean @default(true)
|
||||
mustChangePassword Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audit log — append-only, every mutation lands here
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model AuditEvent {
|
||||
id String @id @default(cuid())
|
||||
action String // e.g. "plant.create", "plant.log.add"
|
||||
entityId String?
|
||||
actorId String?
|
||||
payload Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([entityId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Garden — food forest plant registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum PlantCategory {
|
||||
CANOPY_TREE // apple, pear, cherry, walnut — the tall ones
|
||||
UNDERSTORY_TREE // serviceberry, elderberry, pawpaw
|
||||
LARGE_SHRUB // hazelnuts, currants, gooseberries
|
||||
SHRUB // roses, raspberries, blueberries
|
||||
HERB // comfrey, yarrow, mint, chives
|
||||
GROUNDCOVER // strawberries, clover, creeping thyme
|
||||
VINE // grapes, hops, kiwi
|
||||
ANNUAL // tomatoes, squash, beans — seasonal
|
||||
PERENNIAL // asparagus, rhubarb, perennial veg
|
||||
BULB // garlic, onions, tulips
|
||||
MUSHROOM // shiitake log, oyster bed, etc.
|
||||
OTHER
|
||||
}
|
||||
|
||||
model Plant {
|
||||
id String @id @default(cuid())
|
||||
commonName String
|
||||
species String? // scientific name (optional)
|
||||
variety String? // cultivar / variety name
|
||||
category PlantCategory
|
||||
zone String? // location on the property, freeform ("Back fence guild", "Front yard")
|
||||
plantedAt DateTime? // when it went in the ground
|
||||
source String? // where it came from ("Jung's Nursery", "from seed", "division from Mom's")
|
||||
notes String? // general notes about this plant
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
logs PlantLog[]
|
||||
|
||||
@@index([category])
|
||||
@@index([zone])
|
||||
@@index([active])
|
||||
}
|
||||
|
||||
enum PlantLogType {
|
||||
OBSERVATION // general note — "looking healthy", "saw first flowers"
|
||||
CARE // watering, mulching, pruning, fertilizing, chop-and-drop
|
||||
HARVEST // picked fruit, veg, herb, flower
|
||||
TREATMENT // pest or disease treatment
|
||||
TRANSPLANT // moved to a different spot
|
||||
DIED // plant died or was removed
|
||||
}
|
||||
|
||||
model PlantLog {
|
||||
id String @id @default(cuid())
|
||||
plantId String
|
||||
plant Plant @relation(fields: [plantId], references: [id])
|
||||
type PlantLogType
|
||||
date DateTime @default(now())
|
||||
notes String?
|
||||
quantity String? // freeform for harvests: "2 lbs", "1 gallon", "a big handful"
|
||||
actorId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([plantId])
|
||||
@@index([date])
|
||||
}
|
||||
75
prisma/seed.ts
Normal file
75
prisma/seed.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { PrismaClient, PlantCategory } from "@prisma/client";
|
||||
import { hashSync } from "bcryptjs";
|
||||
|
||||
const db = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding Moon Base...");
|
||||
|
||||
// Admin accounts — only created if they don't exist yet.
|
||||
const password = hashSync("moonbase!", 10);
|
||||
|
||||
await db.user.upsert({
|
||||
where: { username: "bonna" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "cluser0000000bonna",
|
||||
email: "bonna@moonbase.local",
|
||||
username: "bonna",
|
||||
name: "Bonna Moon",
|
||||
passwordHash: password,
|
||||
role: "ADMIN",
|
||||
mustChangePassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
await db.user.upsert({
|
||||
where: { username: "tony" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "cluser0000000tony0",
|
||||
email: "tony@moonbase.local",
|
||||
username: "tony",
|
||||
name: "Tony Moon",
|
||||
passwordHash: password,
|
||||
role: "ADMIN",
|
||||
mustChangePassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.SEED_MODE === "minimal") {
|
||||
console.log("Minimal seed complete (admin accounts only).");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sample plants for dev — a small starter food forest
|
||||
const plants = [
|
||||
{ commonName: "Apple", variety: "Honeycrisp", category: PlantCategory.CANOPY_TREE, zone: "Back yard, north fence", source: "Jung's Nursery", plantedAt: new Date("2024-04-15") },
|
||||
{ commonName: "Apple", variety: "Liberty", category: PlantCategory.CANOPY_TREE, zone: "Back yard, north fence", source: "Jung's Nursery", plantedAt: new Date("2024-04-15") },
|
||||
{ commonName: "Pear", variety: "Bartlett", category: PlantCategory.CANOPY_TREE, zone: "Back yard, east side", source: "Jung's Nursery", plantedAt: new Date("2024-04-20") },
|
||||
{ commonName: "Elderberry", variety: "Bob Gordon", category: PlantCategory.UNDERSTORY_TREE, zone: "Back fence, south", source: "Raintree Nursery", plantedAt: new Date("2024-05-01") },
|
||||
{ commonName: "Serviceberry", species: "Amelanchier canadensis", category: PlantCategory.UNDERSTORY_TREE, zone: "Front yard", source: "local nursery", plantedAt: new Date("2023-04-10") },
|
||||
{ commonName: "Hazelnut", variety: "American", category: PlantCategory.LARGE_SHRUB, zone: "Back yard, west fence", source: "Oikos Tree Crops", plantedAt: new Date("2024-04-15") },
|
||||
{ commonName: "Comfrey", species: "Symphytum officinale", category: PlantCategory.HERB, zone: "Under apple trees", source: "from seed", plantedAt: new Date("2024-05-15"), notes: "Chop-and-drop around the apples. Cut before it flowers to prevent spreading." },
|
||||
{ commonName: "Raspberry", variety: "Heritage", category: PlantCategory.SHRUB, zone: "Back fence, east", source: "division", plantedAt: new Date("2023-05-01") },
|
||||
{ commonName: "Strawberry", variety: "Ozark Beauty", category: PlantCategory.GROUNDCOVER, zone: "Under pear tree", source: "Jung's Nursery", plantedAt: new Date("2024-05-10") },
|
||||
{ commonName: "Garlic", variety: "Music", category: PlantCategory.BULB, zone: "Raised bed #1", source: "Keene Organics", plantedAt: new Date("2024-10-10") },
|
||||
];
|
||||
|
||||
for (const p of plants) {
|
||||
await db.plant.upsert({
|
||||
where: { id: `seed-${p.commonName.toLowerCase().replace(/\s/g, "-")}-${p.variety?.toLowerCase().replace(/\s/g, "-") ?? "0"}` },
|
||||
update: {},
|
||||
create: {
|
||||
id: `seed-${p.commonName.toLowerCase().replace(/\s/g, "-")}-${p.variety?.toLowerCase().replace(/\s/g, "-") ?? "0"}`,
|
||||
...p,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Demo seed complete.");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => db.$disconnect());
|
||||
Reference in New Issue
Block a user