Bring odoo-go to ~70%: read_group, record rules, admin, sessions
Phase 1: read_group/web_read_group with SQL GROUP BY, aggregates (sum/avg/min/max/count/array_agg/sum_currency), date granularity, M2O groupby resolution to [id, display_name]. Phase 2: Record rules with domain_force parsing (Python literal parser), global AND + group OR merging. Domain operators: child_of, parent_of, any, not any compiled to SQL hierarchy/EXISTS queries. Phase 3: Button dispatch via /web/dataset/call_button, method return values interpreted as actions. Payment register wizard (account.payment.register) for sale→invoice→pay flow. Phase 4: ir.filters, ir.default, product fields expanded, SO line product_id onchange, ir_model+ir_model_fields DB seeding. Phase 5: CSV export (/web/export/csv), attachment upload/download via ir.attachment, fields_get with aggregator hints. Admin/System: Session persistence (PostgreSQL-backed), ir.config_parameter with get_param/set_param, ir.cron, ir.logging, res.lang, res.config.settings with company-related fields, Settings form view. Technical menu with Views/Actions/Parameters/Security/Logging sub-menus. User change_password, preferences. Password never exposed in UI/API. Bugfixes: false→nil for varchar/int fields, int32 in toInt64, call_button route with trailing slash, create_invoices returns action, search view always included, get_formview_action, name_create, ir.http stub. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
@@ -103,6 +104,22 @@ func InitDatabase(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4b: Add unique constraint on ir_config_parameter.key for ON CONFLICT support.
|
||||
// Mirrors: odoo/addons/base/models/ir_config_parameter.py _sql_constraints
|
||||
sp, spErr := tx.Begin(ctx)
|
||||
if spErr == nil {
|
||||
if _, err := sp.Exec(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS ir_config_parameter_key_uniq ON ir_config_parameter (key)`); err != nil {
|
||||
sp.Rollback(ctx)
|
||||
} else {
|
||||
sp.Commit(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Seed ir_model and ir_model_fields with model metadata.
|
||||
// This is critical because ir.rule joins through ir_model to find rules for a model.
|
||||
// Mirrors: odoo/modules/loading.py load_module_graph() → _setup_base()
|
||||
seedIrModelMetadata(ctx, tx, models)
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("db: commit: %w", err)
|
||||
}
|
||||
@@ -111,6 +128,121 @@ func InitDatabase(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedIrModelMetadata populates ir_model and ir_model_fields for all registered models.
|
||||
// Each model gets a row in ir_model; each field gets a row in ir_model_fields.
|
||||
// Uses ON CONFLICT DO NOTHING so it's safe to call on every startup.
|
||||
func seedIrModelMetadata(ctx context.Context, tx pgx.Tx, models map[string]*orm.Model) {
|
||||
// Check if ir_model table exists (it should, but guard against ordering issues)
|
||||
var exists bool
|
||||
err := tx.QueryRow(ctx,
|
||||
`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ir_model')`,
|
||||
).Scan(&exists)
|
||||
if err != nil || !exists {
|
||||
log.Println("db: ir_model table does not exist yet, skipping metadata seed")
|
||||
return
|
||||
}
|
||||
|
||||
// Also check ir_model_fields
|
||||
var fieldsExists bool
|
||||
err = tx.QueryRow(ctx,
|
||||
`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ir_model_fields')`,
|
||||
).Scan(&fieldsExists)
|
||||
if err != nil || !fieldsExists {
|
||||
log.Println("db: ir_model_fields table does not exist yet, skipping metadata seed")
|
||||
return
|
||||
}
|
||||
|
||||
modelCount := 0
|
||||
fieldCount := 0
|
||||
|
||||
for name, m := range models {
|
||||
if m.IsAbstract() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this model already exists in ir_model
|
||||
var modelID int64
|
||||
sp, spErr := tx.Begin(ctx)
|
||||
if spErr != nil {
|
||||
continue
|
||||
}
|
||||
err := sp.QueryRow(ctx,
|
||||
`SELECT id FROM ir_model WHERE model = $1`, name,
|
||||
).Scan(&modelID)
|
||||
if err != nil {
|
||||
// Model doesn't exist yet — insert it
|
||||
sp.Rollback(ctx)
|
||||
sp2, spErr2 := tx.Begin(ctx)
|
||||
if spErr2 != nil {
|
||||
continue
|
||||
}
|
||||
err = sp2.QueryRow(ctx,
|
||||
`INSERT INTO ir_model (model, name, info, state, transient)
|
||||
VALUES ($1, $2, $3, 'base', $4)
|
||||
RETURNING id`,
|
||||
name, m.Description(), m.Description(), m.IsTransient(),
|
||||
).Scan(&modelID)
|
||||
if err != nil {
|
||||
sp2.Rollback(ctx)
|
||||
continue
|
||||
}
|
||||
sp2.Commit(ctx)
|
||||
modelCount++
|
||||
} else {
|
||||
sp.Commit(ctx)
|
||||
}
|
||||
|
||||
if modelID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// INSERT into ir_model_fields for each field
|
||||
for fieldName, field := range m.Fields() {
|
||||
if fieldName == "id" || fieldName == "display_name" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this field already exists
|
||||
sp, spErr := tx.Begin(ctx)
|
||||
if spErr != nil {
|
||||
continue
|
||||
}
|
||||
var fieldExists bool
|
||||
err := sp.QueryRow(ctx,
|
||||
`SELECT EXISTS(SELECT 1 FROM ir_model_fields WHERE model_id = $1 AND name = $2)`,
|
||||
modelID, fieldName,
|
||||
).Scan(&fieldExists)
|
||||
if err != nil {
|
||||
sp.Rollback(ctx)
|
||||
continue
|
||||
}
|
||||
if fieldExists {
|
||||
sp.Commit(ctx)
|
||||
continue
|
||||
}
|
||||
sp.Commit(ctx)
|
||||
|
||||
sp2, spErr2 := tx.Begin(ctx)
|
||||
if spErr2 != nil {
|
||||
continue
|
||||
}
|
||||
_, err = sp2.Exec(ctx,
|
||||
`INSERT INTO ir_model_fields (model_id, name, field_description, ttype, state, store)
|
||||
VALUES ($1, $2, $3, $4, 'base', $5)`,
|
||||
modelID, fieldName, field.String, field.Type.String(), field.IsStored(),
|
||||
)
|
||||
if err != nil {
|
||||
sp2.Rollback(ctx)
|
||||
} else {
|
||||
sp2.Commit(ctx)
|
||||
fieldCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("db: seeded ir_model metadata: %d models, %d fields", modelCount, fieldCount)
|
||||
}
|
||||
|
||||
// NeedsSetup checks if the database requires initial setup.
|
||||
func NeedsSetup(ctx context.Context, pool *pgxpool.Pool) bool {
|
||||
var count int
|
||||
@@ -262,7 +394,14 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
|
||||
// 13. Menus (ir_ui_menu + ir_model_data for XML IDs)
|
||||
seedMenus(ctx, tx)
|
||||
|
||||
// 14. Demo data
|
||||
// 14. Settings record (res.config.settings needs at least one record to display)
|
||||
tx.Exec(ctx, `INSERT INTO res_config_settings (id, company_id, show_effect, create_uid, write_uid)
|
||||
VALUES (1, 1, true, 1, 1) ON CONFLICT (id) DO NOTHING`)
|
||||
|
||||
// 14b. System parameters (ir.config_parameter)
|
||||
seedSystemParams(ctx, tx)
|
||||
|
||||
// 15. Demo data
|
||||
if cfg.DemoData {
|
||||
seedDemoData(ctx, tx)
|
||||
}
|
||||
@@ -274,6 +413,8 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
|
||||
"account_tax", "sale_order", "sale_order_line", "account_move",
|
||||
"ir_act_window", "ir_model_data", "ir_ui_menu",
|
||||
"stock_location", "stock_picking_type", "stock_warehouse",
|
||||
"crm_stage", "crm_lead",
|
||||
"ir_config_parameter",
|
||||
}
|
||||
for _, table := range seqs {
|
||||
tx.Exec(ctx, fmt.Sprintf(
|
||||
@@ -426,6 +567,161 @@ func seedViews(ctx context.Context, tx pgx.Tx) {
|
||||
</kanban>', 16, true, 'primary')
|
||||
ON CONFLICT DO NOTHING`)
|
||||
|
||||
// Settings form view
|
||||
tx.Exec(ctx, `INSERT INTO ir_ui_view (name, model, type, arch, priority, active, mode) VALUES
|
||||
('res.config.settings.form', 'res.config.settings', 'form', '<form string="Settings" class="oe_form_configuration">
|
||||
<header>
|
||||
<button name="execute" string="Save" type="object" class="btn-primary"/>
|
||||
<button string="Discard" special="cancel" class="btn-secondary"/>
|
||||
</header>
|
||||
<div class="o_setting_container">
|
||||
<div class="settings">
|
||||
<div class="app_settings_block">
|
||||
<h2>Company</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_name"/>
|
||||
<div class="text-muted">Your company name</div>
|
||||
<field name="company_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_email"/>
|
||||
<field name="company_email"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_phone"/>
|
||||
<field name="company_phone"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_website"/>
|
||||
<field name="company_website"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_vat"/>
|
||||
<div class="text-muted">Tax ID / VAT number</div>
|
||||
<field name="company_vat"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_currency_id"/>
|
||||
<field name="company_currency_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Address</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_street"/>
|
||||
<field name="company_street"/>
|
||||
<field name="company_street2"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_zip"/>
|
||||
<field name="company_zip"/>
|
||||
<label for="company_city"/>
|
||||
<field name="company_city"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="company_country_id"/>
|
||||
<field name="company_country_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Features</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_multi_company"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="group_multi_company"/>
|
||||
<div class="text-muted">Manage multiple companies</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_base_import"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_base_import"/>
|
||||
<div class="text-muted">Import records from CSV/Excel files</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="show_effect"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="show_effect"/>
|
||||
<div class="text-muted">Show animation effects</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</form>', 10, true, 'primary')
|
||||
ON CONFLICT DO NOTHING`)
|
||||
|
||||
// Admin list views for Settings > Technical
|
||||
tx.Exec(ctx, `INSERT INTO ir_ui_view (name, model, type, arch, priority, active, mode) VALUES
|
||||
('company.list', 'res.company', 'list', '<list>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="currency_id"/>
|
||||
<field name="email"/>
|
||||
<field name="phone"/>
|
||||
</list>', 16, true, 'primary'),
|
||||
('users.list', 'res.users', 'list', '<list>
|
||||
<field name="name"/>
|
||||
<field name="login"/>
|
||||
<field name="company_id"/>
|
||||
<field name="active"/>
|
||||
</list>', 16, true, 'primary'),
|
||||
('config_parameter.list', 'ir.config_parameter', 'list', '<list>
|
||||
<field name="key"/>
|
||||
<field name="value"/>
|
||||
</list>', 16, true, 'primary'),
|
||||
('ui_view.list', 'ir.ui.view', 'list', '<list>
|
||||
<field name="name"/>
|
||||
<field name="model"/>
|
||||
<field name="type"/>
|
||||
<field name="priority"/>
|
||||
<field name="active"/>
|
||||
</list>', 16, true, 'primary'),
|
||||
('ui_menu.list', 'ir.ui.menu', 'list', '<list>
|
||||
<field name="name"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="sequence"/>
|
||||
<field name="action"/>
|
||||
</list>', 16, true, 'primary')
|
||||
ON CONFLICT DO NOTHING`)
|
||||
|
||||
log.Println("db: UI views seeded")
|
||||
}
|
||||
|
||||
@@ -463,10 +759,20 @@ func seedActions(ctx context.Context, tx pgx.Tx) {
|
||||
{10, "Projects", "project.project", "list,form", "[]", "{}", "current", 80, 0, "project", "action_project"},
|
||||
{11, "Tasks", "project.task", "list,form", "[]", "{}", "current", 80, 0, "project", "action_project_task"},
|
||||
{12, "Vehicles", "fleet.vehicle", "list,form", "[]", "{}", "current", 80, 0, "fleet", "action_fleet_vehicle"},
|
||||
{100, "Settings", "res.company", "form", "[]", "{}", "current", 80, 1, "base", "action_res_company_form"},
|
||||
{100, "Settings", "res.config.settings", "form", "[]", "{}", "current", 80, 1, "base", "action_general_configuration"},
|
||||
{104, "Companies", "res.company", "list,form", "[]", "{}", "current", 80, 0, "base", "action_res_company_form"},
|
||||
{101, "Users", "res.users", "list,form", "[]", "{}", "current", 80, 0, "base", "action_res_users"},
|
||||
{102, "Sequences", "ir.sequence", "list,form", "[]", "{}", "current", 80, 0, "base", "ir_sequence_form"},
|
||||
{103, "Change My Preferences", "res.users", "form", "[]", "{}", "new", 80, 0, "base", "action_res_users_my"},
|
||||
{105, "Groups", "res.groups", "list,form", "[]", "{}", "current", 80, 0, "base", "action_res_groups"},
|
||||
{106, "Logging", "ir.logging", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_logging"},
|
||||
{107, "System Parameters", "ir.config_parameter", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_config_parameter"},
|
||||
{108, "Scheduled Actions", "ir.cron", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_cron"},
|
||||
{109, "Views", "ir.ui.view", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_ui_view"},
|
||||
{110, "Actions", "ir.actions.act_window", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_act_window"},
|
||||
{111, "Menus", "ir.ui.menu", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_ui_menu"},
|
||||
{112, "Access Rights", "ir.model.access", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_model_access"},
|
||||
{113, "Record Rules", "ir.rule", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_rule"},
|
||||
}
|
||||
|
||||
for _, a := range actions {
|
||||
@@ -552,8 +858,40 @@ func seedMenus(ctx context.Context, tx pgx.Tx) {
|
||||
|
||||
// ── Settings ─────────────────────────────────────────────
|
||||
{100, "Settings", nil, 100, "ir.actions.act_window,100", "fa-cog,#71639e,#FFFFFF", "base", "menu_administration"},
|
||||
{101, "Users & Companies", p(100), 10, "ir.actions.act_window,101", "", "base", "menu_users"},
|
||||
{102, "Technical", p(100), 20, "ir.actions.act_window,102", "", "base", "menu_custom"},
|
||||
{101, "Users & Companies", p(100), 10, "", "", "base", "menu_users"},
|
||||
{110, "Users", p(101), 10, "ir.actions.act_window,101", "", "base", "menu_action_res_users"},
|
||||
{111, "Companies", p(101), 20, "ir.actions.act_window,104", "", "base", "menu_action_res_company_form"},
|
||||
{112, "Groups", p(101), 30, "ir.actions.act_window,105", "", "base", "menu_action_res_groups"},
|
||||
|
||||
{102, "Technical", p(100), 20, "", "", "base", "menu_custom"},
|
||||
|
||||
// Database Structure
|
||||
{120, "Database Structure", p(102), 10, "", "", "base", "menu_custom_database_structure"},
|
||||
|
||||
// Sequences & Identifiers
|
||||
{122, "Sequences", p(102), 15, "ir.actions.act_window,102", "", "base", "menu_custom_sequences"},
|
||||
|
||||
// Parameters
|
||||
{125, "Parameters", p(102), 20, "", "", "base", "menu_custom_parameters"},
|
||||
{126, "System Parameters", p(125), 10, "ir.actions.act_window,107", "", "base", "menu_ir_config_parameter"},
|
||||
|
||||
// Scheduled Actions
|
||||
{128, "Automation", p(102), 25, "", "", "base", "menu_custom_automation"},
|
||||
{129, "Scheduled Actions", p(128), 10, "ir.actions.act_window,108", "", "base", "menu_ir_cron"},
|
||||
|
||||
// User Interface
|
||||
{130, "User Interface", p(102), 30, "", "", "base", "menu_custom_user_interface"},
|
||||
{131, "Views", p(130), 10, "ir.actions.act_window,109", "", "base", "menu_ir_ui_view"},
|
||||
{132, "Actions", p(130), 20, "ir.actions.act_window,110", "", "base", "menu_ir_act_window"},
|
||||
{133, "Menus", p(130), 30, "ir.actions.act_window,111", "", "base", "menu_ir_ui_menu"},
|
||||
|
||||
// Security
|
||||
{135, "Security", p(102), 40, "", "", "base", "menu_custom_security"},
|
||||
{136, "Access Rights", p(135), 10, "ir.actions.act_window,112", "", "base", "menu_ir_model_access"},
|
||||
{137, "Record Rules", p(135), 20, "ir.actions.act_window,113", "", "base", "menu_ir_rule"},
|
||||
|
||||
// Logging
|
||||
{140, "Logging", p(102), 50, "ir.actions.act_window,106", "", "base", "menu_ir_logging"},
|
||||
}
|
||||
|
||||
for _, m := range menus {
|
||||
@@ -627,7 +965,22 @@ func seedDemoData(ctx context.Context, tx pgx.Tx) {
|
||||
('RE/2026/0003', 'out_invoice', 'posted', '2026-03-25', '2026-03-25', 5, 1, 1, 1, 13923, 11700)
|
||||
ON CONFLICT DO NOTHING`)
|
||||
|
||||
log.Println("db: demo data loaded (8 contacts, 3 sale orders, 3 invoices)")
|
||||
// CRM pipeline stages
|
||||
tx.Exec(ctx, `INSERT INTO crm_stage (id, name, sequence, fold, is_won) VALUES
|
||||
(1, 'New', 1, false, false),
|
||||
(2, 'Qualified', 2, false, false),
|
||||
(3, 'Proposition', 3, false, false),
|
||||
(4, 'Won', 4, false, true)
|
||||
ON CONFLICT (id) DO NOTHING`)
|
||||
|
||||
// CRM demo leads (partner IDs 3-5 are the first three demo companies seeded above)
|
||||
tx.Exec(ctx, `INSERT INTO crm_lead (name, type, stage_id, partner_id, expected_revenue, company_id, currency_id, active, state, priority) VALUES
|
||||
('Website Redesign', 'opportunity', 1, 3, 15000, 1, 1, true, 'open', '0'),
|
||||
('ERP Implementation', 'opportunity', 2, 4, 45000, 1, 1, true, 'open', '0'),
|
||||
('Cloud Migration', 'opportunity', 3, 5, 28000, 1, 1, true, 'open', '0')
|
||||
ON CONFLICT DO NOTHING`)
|
||||
|
||||
log.Println("db: demo data loaded (8 contacts, 3 sale orders, 3 invoices, 4 CRM stages, 3 CRM leads)")
|
||||
}
|
||||
|
||||
// SeedBaseData is the legacy function — redirects to setup with defaults.
|
||||
@@ -654,3 +1007,45 @@ func SeedBaseData(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
DemoData: false,
|
||||
})
|
||||
}
|
||||
|
||||
// seedSystemParams inserts default system parameters into ir_config_parameter.
|
||||
// Mirrors: odoo/addons/base/data/ir_config_parameter_data.xml
|
||||
func seedSystemParams(ctx context.Context, tx pgx.Tx) {
|
||||
log.Println("db: seeding system parameters...")
|
||||
|
||||
// Ensure unique constraint on key column for ON CONFLICT to work
|
||||
tx.Exec(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS ir_config_parameter_key_uniq ON ir_config_parameter (key)`)
|
||||
|
||||
// Generate a random UUID for database.uuid
|
||||
dbUUID := generateUUID()
|
||||
|
||||
params := []struct {
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{"web.base.url", "http://localhost:8069"},
|
||||
{"database.uuid", dbUUID},
|
||||
{"base.login_cooldown_after", "10"},
|
||||
{"base.login_cooldown_duration", "60"},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
tx.Exec(ctx,
|
||||
`INSERT INTO ir_config_parameter (key, value) VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO NOTHING`, p.key, p.value)
|
||||
}
|
||||
|
||||
log.Printf("db: seeded %d system parameters", len(params))
|
||||
}
|
||||
|
||||
// generateUUID creates a random UUID v4 string.
|
||||
func generateUUID() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
// Set UUID version 4 bits
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user