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), // ISO 8601 reduced precision: "2015", "2015-03" or "2015-03-10". A build // date is often only half known; see normalizeBuildDate in api/cars.go. fText("build_date", false), fText("first_registration_date", false), // ISO YYYY-MM-DD // Link to the manufacturer service this car came from: the plugin name plus // that plugin's own id for the vehicle (the VIN, for Toyota). See // internal/api/vehicleproviders.go. Blank for a hand-entered car. fText("provider", false), fText("provider_vehicle_id", false), // What this car's page shows: the tabs switched off (["fuel"] on an EV) // and the Information fields switched off (["differentialOil"] on a car // without one). Properties of the car, so everyone it is shared with sees // the same page. Stored as the hidden sets, so anything added in a later // release is on by default. Keys are validated in internal/api/cars.go. fJSON("hidden_tabs", 2000), fJSON("hidden_fields", 2000), // And the columns of the Service history table (["parts"] for a reader who // never records what was changed). Date is not hideable and so never // appears here. fJSON("hidden_service_columns", 2000), // And the parts its services never change (["oil"] on an EV), which come // off the service form and out of that column together. fJSON("hidden_service_parts", 2000), // The order the tabs are laid out in, as tab keys, the same for the // Information rows, the Service history columns, and the connected // service's headline readings. Empty means the page's own default order. fJSON("tab_order", 2000), fJSON("field_order", 2000), fJSON("service_column_order", 2000), fJSON("metric_order", 2000), // 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 }, // Charging sessions for an electric car — the EV counterpart of fuel_entries, // same shape so the two logs behave alike. Consumption is derived on read. "charging_sessions": { fRelation("car", "cars", true, true), fDate("date", true), fNumber("km"), // odometer when plugging in fNumber("kwh"), fNumber("cost"), fBool("full_charge"), fBool("missed_session"), fText("location", false), // "Home", "Ionity Køge" fText("notes", false), attachment(), // the charge point's 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), }, // Server-wide settings as a single record, keyed "global". Today it holds // pluginSettings: the top (L1) layer of the integration cascade — every // plugin's enable state, its global config, and the registration of any // external HTTP plugin. The org (L2) and user (L3) layers keep their own // plugin config in a field of the same name below, so the global layer is // stored the way they are instead of in a file beside the binary. "app_settings": { fText("key", true), fJSON("pluginSettings", 200000), }, // 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), }, // The chargers a user owns — their own wallbox, not the public network. A // charger belongs to a person rather than to a car: it charges whichever car // is plugged into it, and it outlives any of them. "home_chargers": { fText("name", true), fText("serial", false), fText("vendor", false), // "Anker Solix", "Greencell" — who makes it fText("model", false), // "A5191" fText("site_name", false), fNumber("power_kw"), fText("connector", false), // Where this charger came from: the charger-provider id plus that // provider's own id for it (the serial, for both providers we speak to). // Blank for one added by hand. See internal/api/chargerproviders.go. fText("provider", false), fText("provider_charger_id", false), // Owner. Non-cascading, like a car's: deleting a user must not silently // wipe the records they own. fRelation("owner", "users", false, false), // A charger carries no date of its own, so the import order is the only // order there is to list them in. PocketBase adds no created field to a // collection defined through the API, so it is declared here like the // audit trail's. fAutodate("created", true, false), }, // 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), // "auto" is the region's own convention, which is what every clock in the // app read before this field existed. fSelect("time_format", []string{"auto", "24", "12"}, 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), // Holds every arrangement on this user's pages still — the garage, a // car's tabs and Information rows, the provider's readings — so reading a // page cannot nudge its layout. Per user, like the garage order. fBool("drag_locked"), 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), // The garage order: car ids in the order this user arranged them. Per // user rather than per car, so it also covers cars shared with them and // never reorders somebody else's garage. fJSON("car_order", 20000), // The charging page's tab order. Per user like the garage order, and for // the same reason: it is this person's arrangement of their own page. fJSON("charger_tab_order", 2000), // The charging page's card order, stored the same way as its tab order. fJSON("charger_card_order", 2000), }, } // 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{ "app_settings", "organizations", "cars", "service_records", "technical_checks", "parts", "car_shares", "fuel_entries", "charging_sessions", "maintenance_entries", "car_documents", "reminders", "control_audit", "home_chargers", } // reconcileOrder additionally includes "users" so its custom fields (role, // organization, preferences) are added to the built-in collection. var reconcileOrder = []string{ "app_settings", "organizations", "users", "cars", "service_records", "technical_checks", "parts", "car_shares", "fuel_entries", "charging_sessions", "maintenance_entries", "car_documents", "reminders", "control_audit", "home_chargers", } // indexes are extra SQL indexes applied at collection-create time. var indexes = map[string][]string{ // One settings record per key, so the global singleton cannot be duplicated. "app_settings": {"CREATE UNIQUE INDEX `idx_app_settings_key` ON `app_settings` (`key`)"}, "organizations": {"CREATE UNIQUE INDEX `idx_organizations_name` ON `organizations` (`name`)"}, "fuel_entries": {"CREATE INDEX `idx_fuel_entries_car_km` ON `fuel_entries` (`car`, `km`)"}, "charging_sessions": {"CREATE INDEX `idx_charging_sessions_car_km` ON `charging_sessions` (`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`)"}, // A charger is looked up by its owner, and by serial when checking whether // the account it came from has already been imported. "home_chargers": {"CREATE INDEX `idx_home_chargers_owner_serial` ON `home_chargers` (`owner`, `serial`)"}, "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`)", }, }