Introduce registry-pull production stacks (docker-compose.prod.yml) for both the multi-container Docker setup and the all-in-one Docker AIO image, with everything an operator needs (superuser, super-admin, ports, volumes) driven from .env. The API Server now bootstraps PocketBase on startup: a new internal/bootstrap package (Go port of setup-pocketbase.mjs) creates missing collections, reconciles existing ones, and creates the DriverVault super-admin from DRIVERVAULT_SUPERADMIN_* when absent. Idempotent and gated by PB_BOOTSTRAP. The PocketBase superuser is still upserted by the PocketBase container, since the REST API cannot bootstrap the first superuser. Move PocketBase to port 8070 (internal + published) and the web app to 8090 across both stacks, with matching CORS defaults. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
219 lines
7.9 KiB
Go
219 lines
7.9 KiB
Go
package bootstrap
|
|
|
|
// This file is the Go mirror of the DESIRED schema, create order, reconcile
|
|
// order and INDEXES in scripts/setup-pocketbase.mjs. Keep the two in sync: edit
|
|
// here and re-run the server (bootstrap applies on startup), or run the script.
|
|
//
|
|
// Access rules are left null on every collection on purpose — every client goes
|
|
// through the API Server, which authenticates as a superuser, so the database is
|
|
// never exposed directly (including attachments, proxied by the API).
|
|
|
|
// collectionsSchema is the desired field set per collection.
|
|
var collectionsSchema = map[string][]fieldDef{
|
|
"cars": {
|
|
fText("name", true),
|
|
fText("make", false),
|
|
fText("model", false),
|
|
fNumber("year"),
|
|
fText("registration", false),
|
|
fText("registration_country", false),
|
|
fText("vin", false),
|
|
fNumber("service_interval_days"),
|
|
fNumber("service_interval_km"),
|
|
// Roadworthiness inspection cycle. Only prefills a check's next-due date.
|
|
fNumber("technical_check_interval_days"),
|
|
fText("oil_spec", false),
|
|
fText("transmission_oil_spec", false),
|
|
fText("differential_oil_spec", false),
|
|
fText("brake_fluid_spec", false),
|
|
fText("coolant_spec", false),
|
|
fNumber("current_km"),
|
|
// Bi-fuel LPG conversions are their own choice: the car runs on either tank.
|
|
fSelect("fuel_type", []string{
|
|
"petrol", "petrol_lpg", "diesel", "diesel_lpg", "hybrid", "electric", "hydrogen",
|
|
}, false),
|
|
fText("build_date", false), // ISO YYYY-MM-DD (date-only)
|
|
fText("first_registration_date", false), // ISO YYYY-MM-DD
|
|
// Owner of this car. Non-cascading: deleting a user must not wipe their cars.
|
|
fRelation("owner", "users", false, false),
|
|
},
|
|
"service_records": {
|
|
fRelation("car", "cars", true, true),
|
|
fDate("date", true),
|
|
fNumber("km"),
|
|
fBool("changed_oil"),
|
|
fBool("changed_engine_air_filter"),
|
|
fBool("changed_cabin_air_filter"),
|
|
fText("notes", false),
|
|
attachment(), // the workshop receipt / stamped service-book page
|
|
},
|
|
// Mandatory roadworthiness inspections (przegląd techniczny / MOT / TÜV).
|
|
"technical_checks": {
|
|
fRelation("car", "cars", true, true),
|
|
fDate("date", true),
|
|
fSelect("result", []string{"passed", "failed"}, false),
|
|
fNumber("cost"),
|
|
fText("station", false),
|
|
fDate("valid_until", false),
|
|
fText("notes", false),
|
|
attachment(), // the certificate
|
|
},
|
|
"parts": {
|
|
fRelation("car", "cars", true, true),
|
|
fText("name", true),
|
|
fText("part_number", false),
|
|
fText("category", false),
|
|
fText("notes", false),
|
|
attachment(), // a photo of the box, or the part's spec sheet
|
|
},
|
|
// Fuel refills. Consumption is derived on read, not stored.
|
|
"fuel_entries": {
|
|
fRelation("car", "cars", true, true),
|
|
fDate("date", true),
|
|
fNumber("km"), // odometer at the pump
|
|
fNumber("liters"),
|
|
fNumber("cost"),
|
|
fBool("full_tank"),
|
|
fBool("missed_fill"),
|
|
fText("station", false),
|
|
fText("notes", false),
|
|
attachment(), // the pump receipt
|
|
},
|
|
// Workshop visits and repairs (unplanned/one-off garage work with a labour bill).
|
|
"maintenance_entries": {
|
|
fRelation("car", "cars", true, true),
|
|
fDate("date", true),
|
|
fNumber("km"),
|
|
fSelect("type", []string{"repair", "inspection", "bodywork", "tyres", "diagnostics", "recall", "warranty", "other"}, false),
|
|
fSelect("status", []string{"scheduled", "in_progress", "completed"}, false),
|
|
fText("workshop", false),
|
|
fText("location", false),
|
|
fText("description", false),
|
|
fText("parts_used", false),
|
|
fNumber("labor_cost"),
|
|
fNumber("parts_cost"),
|
|
fText("invoice_number", false),
|
|
fDate("warranty_until", false),
|
|
fText("notes", false),
|
|
attachment(), // the workshop's invoice
|
|
},
|
|
// Insurance, pollution certificates, registration papers … expiry drives reminders.
|
|
"car_documents": {
|
|
fRelation("car", "cars", true, true),
|
|
fSelect("type", []string{"insurance", "pollution", "registration", "inspection", "roadTax", "warranty", "other"}, false),
|
|
fText("title", true),
|
|
fText("provider", false),
|
|
fText("reference", false),
|
|
fDate("issue_date", false),
|
|
fDate("expiry_date", false),
|
|
fNumber("cost"),
|
|
fText("notes", false),
|
|
attachment(), // the scan/PDF of the paperwork
|
|
},
|
|
// User-set reminders (derived document/service ones are computed on read).
|
|
"reminders": {
|
|
fRelation("car", "cars", true, true),
|
|
fText("title", true),
|
|
fSelect("type", []string{"maintenance", "document", "service", "inspection", "other"}, false),
|
|
fDate("due_date", false),
|
|
fNumber("due_km"),
|
|
fNumber("repeat_days"),
|
|
fNumber("repeat_km"),
|
|
fBool("done"),
|
|
fDate("done_at", false),
|
|
fText("notes", false),
|
|
},
|
|
// Per-car sharing grants. Cascades on both relations.
|
|
"car_shares": {
|
|
fRelation("car", "cars", true, true),
|
|
fRelation("user", "users", true, true),
|
|
fSelect("permission", []string{"read", "write"}, true),
|
|
fAutodate("created", true, false),
|
|
},
|
|
// Append-only audit trail for OCPP charger control. Actor/org stored as plain
|
|
// text ids (not relations) so the trail survives user or org deletion.
|
|
"control_audit": {
|
|
fText("user_id", false),
|
|
fText("org_id", false),
|
|
fText("serial", false),
|
|
fText("action", true),
|
|
fText("result", false),
|
|
fJSON("params", 10000),
|
|
fAutodate("created", true, false),
|
|
},
|
|
// Tenants that users belong to.
|
|
"organizations": {
|
|
fText("name", true),
|
|
fAutodate("created", true, false),
|
|
// Per-organization plugin/integration config (middle layer of the cascade).
|
|
fJSON("pluginSettings", 100000),
|
|
},
|
|
// Custom fields layered onto the built-in "users" auth collection.
|
|
"users": {
|
|
fText("bio", false),
|
|
fSelect("theme", []string{"light", "dark", "system"}, false),
|
|
fText("locale", false),
|
|
fSelect("date_format", []string{"YMD", "DMY_NUM", "DMY", "MDY"}, false),
|
|
fSelect("currency", []string{
|
|
"EUR", "GBP", "CHF", "PLN", "CZK", "HUF", "RON", "BGN", "DKK", "SEK", "NOK",
|
|
"ISK", "ALL", "AMD", "AZN", "BAM", "BYN", "GEL", "MDL", "MKD", "RSD", "RUB",
|
|
"TRY", "UAH", "USD", "CAD", "AUD", "JPY",
|
|
}, false),
|
|
fSelect("font_size", []string{"small", "medium", "large"}, false),
|
|
fDate("deletion_requested_at", false),
|
|
// Access role. Empty value is treated as "user" by the API.
|
|
fSelect("role", []string{"user", "admin", "superadmin"}, false),
|
|
// Organization membership. Non-cascading: deleting an org keeps its people.
|
|
fRelation("organization", "organizations", false, false),
|
|
// Per-user plugin/integration config (bottom layer of the cascade).
|
|
fJSON("pluginSettings", 100000),
|
|
},
|
|
}
|
|
|
|
// createOrder is the dependency order for creating missing collections.
|
|
// "users" is PocketBase's built-in auth collection and is never created here.
|
|
var createOrder = []string{
|
|
"organizations",
|
|
"cars",
|
|
"service_records",
|
|
"technical_checks",
|
|
"parts",
|
|
"car_shares",
|
|
"fuel_entries",
|
|
"maintenance_entries",
|
|
"car_documents",
|
|
"reminders",
|
|
"control_audit",
|
|
}
|
|
|
|
// reconcileOrder additionally includes "users" so its custom fields (role,
|
|
// organization, preferences) are added to the built-in collection.
|
|
var reconcileOrder = []string{
|
|
"organizations",
|
|
"users",
|
|
"cars",
|
|
"service_records",
|
|
"technical_checks",
|
|
"parts",
|
|
"car_shares",
|
|
"fuel_entries",
|
|
"maintenance_entries",
|
|
"car_documents",
|
|
"reminders",
|
|
"control_audit",
|
|
}
|
|
|
|
// indexes are extra SQL indexes applied at collection-create time.
|
|
var indexes = map[string][]string{
|
|
"organizations": {"CREATE UNIQUE INDEX `idx_organizations_name` ON `organizations` (`name`)"},
|
|
"fuel_entries": {"CREATE INDEX `idx_fuel_entries_car_km` ON `fuel_entries` (`car`, `km`)"},
|
|
"maintenance_entries": {"CREATE INDEX `idx_maintenance_entries_car_date` ON `maintenance_entries` (`car`, `date`)"},
|
|
"car_documents": {"CREATE INDEX `idx_car_documents_car_expiry` ON `car_documents` (`car`, `expiry_date`)"},
|
|
"reminders": {"CREATE INDEX `idx_reminders_car_due` ON `reminders` (`car`, `due_date`)"},
|
|
"technical_checks": {"CREATE INDEX `idx_technical_checks_car_date` ON `technical_checks` (`car`, `date`)"},
|
|
"control_audit": {
|
|
"CREATE INDEX `idx_control_audit_serial_created` ON `control_audit` (`serial`, `created`)",
|
|
"CREATE INDEX `idx_control_audit_user_created` ON `control_audit` (`user_id`, `created`)",
|
|
},
|
|
}
|