lcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/skills/lcp-custom-field/SKILL.md +205 -0
- data/.claude/skills/lcp-getting-started/SKILL.md +332 -0
- data/.claude/skills/lcp-host-binding/SKILL.md +287 -0
- data/.claude/skills/lcp-model/SKILL.md +185 -0
- data/.claude/skills/lcp-permissions/SKILL.md +176 -0
- data/.claude/skills/lcp-presenter/SKILL.md +194 -0
- data/.claude/skills/lcp-workflow/SKILL.md +281 -0
- data/CHANGELOG.md +69 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +17 -0
- data/app/assets/javascripts/lcp_ruby/application.js +58 -0
- data/app/assets/javascripts/lcp_ruby/controllers/advanced_filter_controller.js +1521 -0
- data/app/assets/javascripts/lcp_ruby/controllers/approval_actions_controller.js +24 -0
- data/app/assets/javascripts/lcp_ruby/controllers/array_input_controller.js +156 -0
- data/app/assets/javascripts/lcp_ruby/controllers/batch_select_controller.js +405 -0
- data/app/assets/javascripts/lcp_ruby/controllers/cascading_selects_controller.js +436 -0
- data/app/assets/javascripts/lcp_ruby/controllers/char_counter_controller.js +28 -0
- data/app/assets/javascripts/lcp_ruby/controllers/clipboard_controller.js +62 -0
- data/app/assets/javascripts/lcp_ruby/controllers/conditional_rendering_controller.js +178 -0
- data/app/assets/javascripts/lcp_ruby/controllers/confirm_dialog_controller.js +131 -0
- data/app/assets/javascripts/lcp_ruby/controllers/custom_fields_manage_controller.js +80 -0
- data/app/assets/javascripts/lcp_ruby/controllers/dialog_controller.js +421 -0
- data/app/assets/javascripts/lcp_ruby/controllers/dialog_nested_controller.js +182 -0
- data/app/assets/javascripts/lcp_ruby/controllers/direct_upload_controller.js +100 -0
- data/app/assets/javascripts/lcp_ruby/controllers/drawer_controller.js +205 -0
- data/app/assets/javascripts/lcp_ruby/controllers/dropdown_controller.js +160 -0
- data/app/assets/javascripts/lcp_ruby/controllers/export_dialog_controller.js +15 -0
- data/app/assets/javascripts/lcp_ruby/controllers/field_picker_controller.js +290 -0
- data/app/assets/javascripts/lcp_ruby/controllers/file_upload_controller.js +135 -0
- data/app/assets/javascripts/lcp_ruby/controllers/filter_auto_submit_controller.js +46 -0
- data/app/assets/javascripts/lcp_ruby/controllers/filter_dirty_controller.js +94 -0
- data/app/assets/javascripts/lcp_ruby/controllers/form_actions_controller.js +68 -0
- data/app/assets/javascripts/lcp_ruby/controllers/form_handling_controller.js +67 -0
- data/app/assets/javascripts/lcp_ruby/controllers/import_dialog_controller.js +142 -0
- data/app/assets/javascripts/lcp_ruby/controllers/import_mapper_controller.js +322 -0
- data/app/assets/javascripts/lcp_ruby/controllers/index_sortable_controller.js +388 -0
- data/app/assets/javascripts/lcp_ruby/controllers/inline_create_controller.js +137 -0
- data/app/assets/javascripts/lcp_ruby/controllers/kanban_board_controller.js +290 -0
- data/app/assets/javascripts/lcp_ruby/controllers/menu_controller.js +19 -0
- data/app/assets/javascripts/lcp_ruby/controllers/nested_forms_controller.js +261 -0
- data/app/assets/javascripts/lcp_ruby/controllers/responsive_sidebar_controller.js +161 -0
- data/app/assets/javascripts/lcp_ruby/controllers/responsive_top_nav_controller.js +394 -0
- data/app/assets/javascripts/lcp_ruby/controllers/saved_filters_controller.js +129 -0
- data/app/assets/javascripts/lcp_ruby/controllers/search_controller.js +82 -0
- data/app/assets/javascripts/lcp_ruby/controllers/selection_controller.js +33 -0
- data/app/assets/javascripts/lcp_ruby/controllers/sidebar_controller.js +83 -0
- data/app/assets/javascripts/lcp_ruby/controllers/sidebar_toggle_controller.js +66 -0
- data/app/assets/javascripts/lcp_ruby/controllers/slider_controller.js +26 -0
- data/app/assets/javascripts/lcp_ruby/controllers/submenu_controller.js +55 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tom_select_controller.js +168 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tree_index_controller.js +106 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tree_reparent_controller.js +182 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tree_select_controller.js +119 -0
- data/app/assets/javascripts/lcp_ruby/controllers/ui_components_controller.js +135 -0
- data/app/assets/javascripts/lcp_ruby/controllers/zone_tabs_controller.js +66 -0
- data/app/assets/javascripts/lcp_ruby/dev_toolbar.js +494 -0
- data/app/assets/javascripts/lcp_ruby/i18n.js.erb +29 -0
- data/app/assets/javascripts/lcp_ruby/lucide_init.js +24 -0
- data/app/assets/javascripts/lcp_ruby/stimulus_bootstrap.js +7 -0
- data/app/assets/javascripts/lcp_ruby/utils.js +119 -0
- data/app/assets/javascripts/lcp_ruby/xhr_fetch.js +411 -0
- data/app/assets/stylesheets/lcp_ruby/application.css +1940 -0
- data/app/assets/stylesheets/lcp_ruby/dev_toolbar.css +355 -0
- data/app/controllers/concerns/lcp_ruby/dialog_rendering.rb +167 -0
- data/app/controllers/concerns/lcp_ruby/form_action_execution.rb +249 -0
- data/app/controllers/concerns/lcp_ruby/page_authorization.rb +105 -0
- data/app/controllers/concerns/lcp_ruby/zone_resolution.rb +199 -0
- data/app/controllers/lcp_ruby/actions_controller.rb +481 -0
- data/app/controllers/lcp_ruby/application_controller.rb +59 -0
- data/app/controllers/lcp_ruby/approval_tasks_controller.rb +98 -0
- data/app/controllers/lcp_ruby/auth/base_controller.rb +58 -0
- data/app/controllers/lcp_ruby/auth/callbacks_controller.rb +103 -0
- data/app/controllers/lcp_ruby/auth/passwords_controller.rb +15 -0
- data/app/controllers/lcp_ruby/auth/registrations_controller.rb +37 -0
- data/app/controllers/lcp_ruby/auth/sessions_controller.rb +112 -0
- data/app/controllers/lcp_ruby/custom_fields_controller.rb +370 -0
- data/app/controllers/lcp_ruby/dev_toolbar_controller.rb +197 -0
- data/app/controllers/lcp_ruby/dialogs_controller.rb +199 -0
- data/app/controllers/lcp_ruby/health_controller.rb +23 -0
- data/app/controllers/lcp_ruby/impersonation_controller.rb +32 -0
- data/app/controllers/lcp_ruby/landing_controller.rb +49 -0
- data/app/controllers/lcp_ruby/metrics_controller.rb +17 -0
- data/app/controllers/lcp_ruby/resources_controller.rb +2218 -0
- data/app/controllers/lcp_ruby/saved_filters_controller.rb +221 -0
- data/app/helpers/lcp_ruby/condition_helper.rb +87 -0
- data/app/helpers/lcp_ruby/dashboard_helper.rb +45 -0
- data/app/helpers/lcp_ruby/dev_toolbar_helper.rb +17 -0
- data/app/helpers/lcp_ruby/dialog_helper.rb +27 -0
- data/app/helpers/lcp_ruby/display/card_helper.rb +317 -0
- data/app/helpers/lcp_ruby/display_helper.rb +63 -0
- data/app/helpers/lcp_ruby/display_template_helper.rb +100 -0
- data/app/helpers/lcp_ruby/form_helper.rb +1020 -0
- data/app/helpers/lcp_ruby/grouped_query_helper.rb +107 -0
- data/app/helpers/lcp_ruby/i18n_payload_helper.rb +197 -0
- data/app/helpers/lcp_ruby/layout_helper.rb +832 -0
- data/app/helpers/lcp_ruby/link_through_helper.rb +29 -0
- data/app/helpers/lcp_ruby/oidc_button_helper.rb +59 -0
- data/app/helpers/lcp_ruby/search_helper.rb +31 -0
- data/app/helpers/lcp_ruby/tree_helper.rb +140 -0
- data/app/helpers/lcp_ruby/view_slot_helper.rb +33 -0
- data/app/models/lcp_ruby/user.rb +61 -0
- data/app/views/layouts/lcp_ruby/application.html.erb +168 -0
- data/app/views/layouts/lcp_ruby/auth.html.erb +170 -0
- data/app/views/lcp_ruby/auth/callbacks/failure.html.erb +18 -0
- data/app/views/lcp_ruby/auth/mailer/reset_password_instructions.html.erb +9 -0
- data/app/views/lcp_ruby/auth/passwords/edit.html.erb +34 -0
- data/app/views/lcp_ruby/auth/passwords/new.html.erb +24 -0
- data/app/views/lcp_ruby/auth/registrations/edit.html.erb +48 -0
- data/app/views/lcp_ruby/auth/registrations/new.html.erb +46 -0
- data/app/views/lcp_ruby/auth/sessions/new.html.erb +55 -0
- data/app/views/lcp_ruby/auth/shared/_links.html.erb +13 -0
- data/app/views/lcp_ruby/custom_fields/_manage_row.html.erb +82 -0
- data/app/views/lcp_ruby/custom_fields/manage.html.erb +47 -0
- data/app/views/lcp_ruby/dialogs/_dialog_composite_frame.html.erb +87 -0
- data/app/views/lcp_ruby/dialogs/_dialog_frame.html.erb +60 -0
- data/app/views/lcp_ruby/dialogs/_dialog_show_frame.html.erb +14 -0
- data/app/views/lcp_ruby/dialogs/_show_secret.html.erb +10 -0
- data/app/views/lcp_ruby/dialogs/_success.html.erb +1 -0
- data/app/views/lcp_ruby/errors/not_found.html.erb +5 -0
- data/app/views/lcp_ruby/menu_renderers/_user_menu.html.erb +51 -0
- data/app/views/lcp_ruby/menu_renderers/_user_menu_panel.html.erb +32 -0
- data/app/views/lcp_ruby/navigation/_auto_top.html.erb +16 -0
- data/app/views/lcp_ruby/navigation/_both_sidebar.html.erb +1 -0
- data/app/views/lcp_ruby/navigation/_both_top.html.erb +1 -0
- data/app/views/lcp_ruby/navigation/_impersonation_banner.html.erb +14 -0
- data/app/views/lcp_ruby/navigation/_item_content.html.erb +23 -0
- data/app/views/lcp_ruby/navigation/_link_or_button.html.erb +51 -0
- data/app/views/lcp_ruby/navigation/_mobile_header.html.erb +23 -0
- data/app/views/lcp_ruby/navigation/_panel_item.html.erb +32 -0
- data/app/views/lcp_ruby/navigation/_sidebar.html.erb +38 -0
- data/app/views/lcp_ruby/navigation/_sidebar_item.html.erb +38 -0
- data/app/views/lcp_ruby/navigation/_sidebar_toggle.html.erb +32 -0
- data/app/views/lcp_ruby/navigation/_top.html.erb +26 -0
- data/app/views/lcp_ruby/navigation/_top_item.html.erb +112 -0
- data/app/views/lcp_ruby/navigation/_widget_item.html.erb +14 -0
- data/app/views/lcp_ruby/resources/_action_button.html.erb +138 -0
- data/app/views/lcp_ruby/resources/_advanced_filter.html.erb +55 -0
- data/app/views/lcp_ruby/resources/_api_status_banner.html.erb +11 -0
- data/app/views/lcp_ruby/resources/_association_list.html.erb +132 -0
- data/app/views/lcp_ruby/resources/_audit_history.html.erb +66 -0
- data/app/views/lcp_ruby/resources/_batch_toolbar.html.erb +54 -0
- data/app/views/lcp_ruby/resources/_filter_form.html.erb +57 -0
- data/app/views/lcp_ruby/resources/_form.html.erb +73 -0
- data/app/views/lcp_ruby/resources/_form_action_button.html.erb +20 -0
- data/app/views/lcp_ruby/resources/_form_action_dropdown_item.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_form_actions.html.erb +38 -0
- data/app/views/lcp_ruby/resources/_form_section.html.erb +235 -0
- data/app/views/lcp_ruby/resources/_grid_page.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_grouped_index.html.erb +61 -0
- data/app/views/lcp_ruby/resources/_inline_create_form.html.erb +50 -0
- data/app/views/lcp_ruby/resources/_json_items_list.html.erb +77 -0
- data/app/views/lcp_ruby/resources/_json_nested_fields.html.erb +30 -0
- data/app/views/lcp_ruby/resources/_kanban_card.html.erb +31 -0
- data/app/views/lcp_ruby/resources/_kanban_card_body.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_kanban_column.html.erb +33 -0
- data/app/views/lcp_ruby/resources/_kanban_column_header.html.erb +10 -0
- data/app/views/lcp_ruby/resources/_kanban_index.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_nested_field_cell.html.erb +33 -0
- data/app/views/lcp_ruby/resources/_nested_fields.html.erb +26 -0
- data/app/views/lcp_ruby/resources/_nested_row_content.html.erb +65 -0
- data/app/views/lcp_ruby/resources/_scope_filters.html.erb +71 -0
- data/app/views/lcp_ruby/resources/_semantic_index_page.html.erb +162 -0
- data/app/views/lcp_ruby/resources/_semantic_page.html.erb +121 -0
- data/app/views/lcp_ruby/resources/_show_sections.html.erb +128 -0
- data/app/views/lcp_ruby/resources/_table_index.html.erb +169 -0
- data/app/views/lcp_ruby/resources/_tile_card.html.erb +26 -0
- data/app/views/lcp_ruby/resources/_tile_card_body.html.erb +19 -0
- data/app/views/lcp_ruby/resources/_tiles_index.html.erb +15 -0
- data/app/views/lcp_ruby/resources/_transition_button.html.erb +33 -0
- data/app/views/lcp_ruby/resources/_tree_index.html.erb +61 -0
- data/app/views/lcp_ruby/resources/_view_switcher.html.erb +24 -0
- data/app/views/lcp_ruby/resources/edit.html.erb +9 -0
- data/app/views/lcp_ruby/resources/index.html.erb +80 -0
- data/app/views/lcp_ruby/resources/new.html.erb +9 -0
- data/app/views/lcp_ruby/resources/show.html.erb +37 -0
- data/app/views/lcp_ruby/shared/_breadcrumbs.html.erb +15 -0
- data/app/views/lcp_ruby/shared/_custom_partial_error.html.erb +3 -0
- data/app/views/lcp_ruby/shared/_flash_messages.html.erb +19 -0
- data/app/views/lcp_ruby/slots/index/_advanced_filter.html.erb +3 -0
- data/app/views/lcp_ruby/slots/index/_collection_actions.html.erb +50 -0
- data/app/views/lcp_ruby/slots/index/_manage_all.html.erb +4 -0
- data/app/views/lcp_ruby/slots/index/_pagination_footer.html.erb +71 -0
- data/app/views/lcp_ruby/slots/index/_predefined_filters.html.erb +8 -0
- data/app/views/lcp_ruby/slots/index/_saved_filters.html.erb +87 -0
- data/app/views/lcp_ruby/slots/index/_search.html.erb +52 -0
- data/app/views/lcp_ruby/slots/index/_search_parameter.html.erb +72 -0
- data/app/views/lcp_ruby/slots/index/_sort_dropdown.html.erb +21 -0
- data/app/views/lcp_ruby/slots/index/_summary_bar.html.erb +22 -0
- data/app/views/lcp_ruby/slots/index/_view_switcher.html.erb +1 -0
- data/app/views/lcp_ruby/slots/show/_approval_status.html.erb +8 -0
- data/app/views/lcp_ruby/slots/show/_back_to_list.html.erb +1 -0
- data/app/views/lcp_ruby/slots/show/_copy_url.html.erb +5 -0
- data/app/views/lcp_ruby/slots/show/_single_actions.html.erb +4 -0
- data/app/views/lcp_ruby/slots/show/_view_switcher.html.erb +1 -0
- data/app/views/lcp_ruby/widgets/_approval_status.html.erb +150 -0
- data/app/views/lcp_ruby/widgets/_chart.html.erb +36 -0
- data/app/views/lcp_ruby/widgets/_embed.html.erb +13 -0
- data/app/views/lcp_ruby/widgets/_kpi_card.html.erb +29 -0
- data/app/views/lcp_ruby/widgets/_list.html.erb +17 -0
- data/app/views/lcp_ruby/widgets/_presenter_zone.html.erb +84 -0
- data/app/views/lcp_ruby/widgets/_record_show_zone.html.erb +11 -0
- data/app/views/lcp_ruby/widgets/_text.html.erb +3 -0
- data/app/views/lcp_ruby/widgets/_workflow_graph.html.erb +32 -0
- data/app/views/lcp_ruby/zones/_custom_zone.html.erb +16 -0
- data/app/views/lcp_ruby/zones/_error.html.erb +5 -0
- data/app/views/lcp_ruby/zones/_zone_frame.html.erb +25 -0
- data/app/views/lcp_ruby/zones/_zone_search.html.erb +20 -0
- data/config/locales/cs.yml +859 -0
- data/config/locales/en.yml +731 -0
- data/config/routes.rb +119 -0
- data/docs/README.md +225 -0
- data/docs/architecture.md +212 -0
- data/docs/feature-catalog.md +763 -0
- data/docs/feature-catalog.yml +20911 -0
- data/docs/getting-started.md +1187 -0
- data/docs/guides/action-buttons.md +537 -0
- data/docs/guides/adding-locale.md +353 -0
- data/docs/guides/api-backed-models.md +478 -0
- data/docs/guides/attachments.md +399 -0
- data/docs/guides/auditing.md +333 -0
- data/docs/guides/batch-actions.md +342 -0
- data/docs/guides/composite-pages.md +1290 -0
- data/docs/guides/computed-fields.md +350 -0
- data/docs/guides/conditional-rendering.md +832 -0
- data/docs/guides/custom-actions.md +238 -0
- data/docs/guides/custom-fields.md +439 -0
- data/docs/guides/custom-renderers.md +234 -0
- data/docs/guides/custom-types.md +274 -0
- data/docs/guides/dashboards.md +504 -0
- data/docs/guides/debugging/README.md +38 -0
- data/docs/guides/debugging/controllers.md +147 -0
- data/docs/guides/debugging/data.md +165 -0
- data/docs/guides/debugging/metadata.md +143 -0
- data/docs/guides/debugging/models.md +126 -0
- data/docs/guides/debugging/permissions.md +147 -0
- data/docs/guides/debugging/presenters.md +151 -0
- data/docs/guides/developer-tools.md +418 -0
- data/docs/guides/dialogs.md +392 -0
- data/docs/guides/display-types.md +1430 -0
- data/docs/guides/eager-loading.md +230 -0
- data/docs/guides/event-handlers.md +135 -0
- data/docs/guides/export.md +305 -0
- data/docs/guides/extensibility.md +761 -0
- data/docs/guides/groups.md +220 -0
- data/docs/guides/hierarchical-authorization.md +427 -0
- data/docs/guides/host-application.md +556 -0
- data/docs/guides/host-controller-integration.md +473 -0
- data/docs/guides/impersonation.md +83 -0
- data/docs/guides/import.md +165 -0
- data/docs/guides/inherited-permissions.md +459 -0
- data/docs/guides/menu.md +373 -0
- data/docs/guides/monitoring.md +254 -0
- data/docs/guides/oidc-setup.md +399 -0
- data/docs/guides/permission-source.md +205 -0
- data/docs/guides/permissions.md +364 -0
- data/docs/guides/presenters.md +2324 -0
- data/docs/guides/record-aliases.md +303 -0
- data/docs/guides/rendering-extension-points.md +280 -0
- data/docs/guides/role-source.md +288 -0
- data/docs/guides/selectbox.md +516 -0
- data/docs/guides/sequences.md +291 -0
- data/docs/guides/soft-delete.md +460 -0
- data/docs/guides/theming.md +129 -0
- data/docs/guides/tiles.md +383 -0
- data/docs/guides/tree-structures.md +297 -0
- data/docs/guides/userstamps.md +288 -0
- data/docs/guides/view-groups.md +259 -0
- data/docs/guides/view-slots.md +352 -0
- data/docs/guides/virtual-columns.md +810 -0
- data/docs/guides/workflow.md +692 -0
- data/docs/reference/api-backed-models.md +404 -0
- data/docs/reference/api-tokens.md +128 -0
- data/docs/reference/auditing.md +277 -0
- data/docs/reference/boot_lifecycle.md +188 -0
- data/docs/reference/cascading_selects.md +189 -0
- data/docs/reference/condition-operators.md +445 -0
- data/docs/reference/custom-fields.md +483 -0
- data/docs/reference/dialogs.md +286 -0
- data/docs/reference/doctor.md +168 -0
- data/docs/reference/dynamic-references.md +95 -0
- data/docs/reference/eager-loading.md +192 -0
- data/docs/reference/engine-configuration.md +989 -0
- data/docs/reference/export.md +309 -0
- data/docs/reference/forms.md +68 -0
- data/docs/reference/groups.md +176 -0
- data/docs/reference/host-controller-integration.md +342 -0
- data/docs/reference/i18n.md +497 -0
- data/docs/reference/i18n_check.md +351 -0
- data/docs/reference/import.md +260 -0
- data/docs/reference/invariant_check.md +216 -0
- data/docs/reference/menu.md +985 -0
- data/docs/reference/model-dsl.md +1157 -0
- data/docs/reference/models.md +2972 -0
- data/docs/reference/monitoring.md +222 -0
- data/docs/reference/oidc-bearer.md +269 -0
- data/docs/reference/oidc.md +407 -0
- data/docs/reference/page_filters.md +328 -0
- data/docs/reference/pages.md +1375 -0
- data/docs/reference/permission-source.md +185 -0
- data/docs/reference/permissions.md +715 -0
- data/docs/reference/presenter-dsl.md +1719 -0
- data/docs/reference/presenters.md +3627 -0
- data/docs/reference/role-source.md +227 -0
- data/docs/reference/theme-variables.md +139 -0
- data/docs/reference/tree-structures.md +374 -0
- data/docs/reference/types.md +470 -0
- data/docs/reference/view-groups.md +347 -0
- data/docs/reference/view-slots.md +228 -0
- data/docs/reference/virtual_forms.md +196 -0
- data/docs/reference/workflow-approvals.md +387 -0
- data/docs/reference/workflow.md +651 -0
- data/examples/crm/Gemfile +9 -0
- data/examples/crm/Gemfile.lock +417 -0
- data/examples/crm/Rakefile +2 -0
- data/examples/crm/app/actions/activity/complete.rb +20 -0
- data/examples/crm/app/actions/deal/close_won.rb +20 -0
- data/examples/crm/app/assets/config/manifest.js +3 -0
- data/examples/crm/app/controllers/application_controller.rb +9 -0
- data/examples/crm/app/event_handlers/deal/on_stage_change.rb +17 -0
- data/examples/crm/app/lcp_services/computed/weighted_deal_value.rb +13 -0
- data/examples/crm/app/lcp_services/data_providers/active_contacts_count.rb +14 -0
- data/examples/crm/app/lcp_services/data_providers/open_deals_count.rb +14 -0
- data/examples/crm/app/lcp_services/data_providers/pending_activities_count.rb +15 -0
- data/examples/crm/app/lcp_services/data_providers/pipeline_value.rb +27 -0
- data/examples/crm/app/lcp_services/data_providers/won_deals_count.rb +14 -0
- data/examples/crm/app/lcp_services/defaults/thirty_days_out.rb +11 -0
- data/examples/crm/app/lcp_services/transforms/titlecase.rb +11 -0
- data/examples/crm/app/lcp_services/validators/deal_credit_limit.rb +19 -0
- data/examples/crm/app/lcp_services/validators/deal_documents_required.rb +17 -0
- data/examples/crm/app/renderers/conditional_badge.rb +39 -0
- data/examples/crm/bin/rails +4 -0
- data/examples/crm/bin/rake +4 -0
- data/examples/crm/config/application.rb +31 -0
- data/examples/crm/config/boot.rb +2 -0
- data/examples/crm/config/database.yml +12 -0
- data/examples/crm/config/environment.rb +2 -0
- data/examples/crm/config/initializers/lcp_ruby.rb +45 -0
- data/examples/crm/config/lcp_ruby/menu.yml +53 -0
- data/examples/crm/config/lcp_ruby/models/activity.rb +43 -0
- data/examples/crm/config/lcp_ruby/models/city.rb +19 -0
- data/examples/crm/config/lcp_ruby/models/company.rb +65 -0
- data/examples/crm/config/lcp_ruby/models/contact.rb +62 -0
- data/examples/crm/config/lcp_ruby/models/country.rb +22 -0
- data/examples/crm/config/lcp_ruby/models/custom_field_definition.rb +60 -0
- data/examples/crm/config/lcp_ruby/models/deal.rb +85 -0
- data/examples/crm/config/lcp_ruby/models/deal_category.rb +15 -0
- data/examples/crm/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
- data/examples/crm/config/lcp_ruby/models/region.rb +15 -0
- data/examples/crm/config/lcp_ruby/models/saved_filter.rb +50 -0
- data/examples/crm/config/lcp_ruby/pages/activity_quick_log.yml +19 -0
- data/examples/crm/config/lcp_ruby/pages/company_detail.yml +127 -0
- data/examples/crm/config/lcp_ruby/pages/deals_overview.yml +65 -0
- data/examples/crm/config/lcp_ruby/permissions/activity.yml +40 -0
- data/examples/crm/config/lcp_ruby/permissions/custom_field_definition.yml +16 -0
- data/examples/crm/config/lcp_ruby/permissions/deal.yml +54 -0
- data/examples/crm/config/lcp_ruby/permissions/default.yml +16 -0
- data/examples/crm/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
- data/examples/crm/config/lcp_ruby/permissions/saved_filter.yml +27 -0
- data/examples/crm/config/lcp_ruby/presenters/activity.rb +140 -0
- data/examples/crm/config/lcp_ruby/presenters/activity_quick_form.rb +36 -0
- data/examples/crm/config/lcp_ruby/presenters/activity_short.rb +16 -0
- data/examples/crm/config/lcp_ruby/presenters/activity_tiles.rb +35 -0
- data/examples/crm/config/lcp_ruby/presenters/city.rb +53 -0
- data/examples/crm/config/lcp_ruby/presenters/company.rb +147 -0
- data/examples/crm/config/lcp_ruby/presenters/company_activities_zone.rb +26 -0
- data/examples/crm/config/lcp_ruby/presenters/company_archive.rb +37 -0
- data/examples/crm/config/lcp_ruby/presenters/company_contacts_zone.rb +22 -0
- data/examples/crm/config/lcp_ruby/presenters/company_deals_zone.rb +36 -0
- data/examples/crm/config/lcp_ruby/presenters/company_short.rb +21 -0
- data/examples/crm/config/lcp_ruby/presenters/company_show_zone.rb +38 -0
- data/examples/crm/config/lcp_ruby/presenters/company_sidebar.rb +15 -0
- data/examples/crm/config/lcp_ruby/presenters/company_tiles.rb +35 -0
- data/examples/crm/config/lcp_ruby/presenters/contact.rb +120 -0
- data/examples/crm/config/lcp_ruby/presenters/contact_quick_form.rb +17 -0
- data/examples/crm/config/lcp_ruby/presenters/contact_short.rb +20 -0
- data/examples/crm/config/lcp_ruby/presenters/contact_tiles.rb +34 -0
- data/examples/crm/config/lcp_ruby/presenters/country.rb +44 -0
- data/examples/crm/config/lcp_ruby/presenters/custom_fields.rb +124 -0
- data/examples/crm/config/lcp_ruby/presenters/deal.rb +181 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_category.rb +46 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_overview.rb +6 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_pipeline.rb +18 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_short.rb +25 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_tiles.rb +48 -0
- data/examples/crm/config/lcp_ruby/presenters/region.rb +42 -0
- data/examples/crm/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
- data/examples/crm/config/lcp_ruby/presenters/saved_filters.rb +93 -0
- data/examples/crm/config/lcp_ruby/views/activities.yml +16 -0
- data/examples/crm/config/lcp_ruby/views/cities.yml +10 -0
- data/examples/crm/config/lcp_ruby/views/companies.yml +14 -0
- data/examples/crm/config/lcp_ruby/views/contacts.yml +16 -0
- data/examples/crm/config/lcp_ruby/views/countries.yml +8 -0
- data/examples/crm/config/lcp_ruby/views/custom_fields.rb +9 -0
- data/examples/crm/config/lcp_ruby/views/deal_categories.yml +8 -0
- data/examples/crm/config/lcp_ruby/views/deals.yml +19 -0
- data/examples/crm/config/lcp_ruby/views/pipeline.yml +10 -0
- data/examples/crm/config/lcp_ruby/views/regions.yml +10 -0
- data/examples/crm/config/lcp_ruby/views/saved_filters.yml +7 -0
- data/examples/crm/config/locales/cs.yml +338 -0
- data/examples/crm/config/locales/en.yml +353 -0
- data/examples/crm/config/routes.rb +4 -0
- data/examples/crm/config/storage.yml +3 -0
- data/examples/crm/config.ru +2 -0
- data/examples/crm/db/migrate/20260219104942_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/crm/db/schema.rb +245 -0
- data/examples/crm/db/seeds.rb +1111 -0
- data/examples/crm/erd.md +163 -0
- data/examples/hr/Gemfile +9 -0
- data/examples/hr/Gemfile.lock +419 -0
- data/examples/hr/Rakefile +6 -0
- data/examples/hr/app/actions/asset/assign_asset.rb +20 -0
- data/examples/hr/app/actions/asset/return_asset.rb +20 -0
- data/examples/hr/app/actions/candidate/advance.rb +32 -0
- data/examples/hr/app/actions/candidate/hire.rb +20 -0
- data/examples/hr/app/actions/candidate/reject_candidate.rb +20 -0
- data/examples/hr/app/actions/expense_claim/approve.rb +21 -0
- data/examples/hr/app/actions/expense_claim/reject.rb +21 -0
- data/examples/hr/app/actions/expense_claim/submit.rb +20 -0
- data/examples/hr/app/actions/interview/complete_interview.rb +20 -0
- data/examples/hr/app/actions/leave_request/approve.rb +21 -0
- data/examples/hr/app/actions/leave_request/cancel.rb +20 -0
- data/examples/hr/app/actions/leave_request/reject.rb +21 -0
- data/examples/hr/app/assets/config/manifest.js +1 -0
- data/examples/hr/app/assets/stylesheets/application.css +10 -0
- data/examples/hr/app/condition_services/is_own_org_unit.rb +14 -0
- data/examples/hr/app/condition_services/is_own_record.rb +20 -0
- data/examples/hr/app/controllers/application_controller.rb +15 -0
- data/examples/hr/app/event_handlers/asset_assignment/on_create.rb +24 -0
- data/examples/hr/app/event_handlers/candidate/on_status_change.rb +18 -0
- data/examples/hr/app/event_handlers/leave_request/on_status_change.rb +45 -0
- data/examples/hr/app/helpers/application_helper.rb +2 -0
- data/examples/hr/app/javascript/application.js +1 -0
- data/examples/hr/app/jobs/application_job.rb +7 -0
- data/examples/hr/app/lcp_services/computed/employee_tenure.rb +28 -0
- data/examples/hr/app/lcp_services/computed/leave_remaining.rb +13 -0
- data/examples/hr/app/lcp_services/data_providers/headcount_text.rb +14 -0
- data/examples/hr/app/lcp_services/data_providers/open_positions_count.rb +14 -0
- data/examples/hr/app/lcp_services/data_providers/pending_expenses_count.rb +14 -0
- data/examples/hr/app/lcp_services/data_providers/pending_leaves_count.rb +14 -0
- data/examples/hr/app/lcp_services/defaults/current_year.rb +11 -0
- data/examples/hr/app/lcp_services/transforms/titlecase.rb +11 -0
- data/examples/hr/app/lcp_services/validators/expense_receipt_required.rb +17 -0
- data/examples/hr/app/lcp_services/validators/leave_balance_check.rb +37 -0
- data/examples/hr/app/models/application_record.rb +3 -0
- data/examples/hr/app/views/layouts/application.html.erb +29 -0
- data/examples/hr/app/views/pwa/manifest.json.erb +22 -0
- data/examples/hr/app/views/pwa/service-worker.js +26 -0
- data/examples/hr/bin/brakeman +7 -0
- data/examples/hr/bin/bundler-audit +6 -0
- data/examples/hr/bin/ci +6 -0
- data/examples/hr/bin/dev +2 -0
- data/examples/hr/bin/docker-entrypoint +8 -0
- data/examples/hr/bin/importmap +4 -0
- data/examples/hr/bin/jobs +6 -0
- data/examples/hr/bin/kamal +27 -0
- data/examples/hr/bin/rails +4 -0
- data/examples/hr/bin/rake +4 -0
- data/examples/hr/bin/rubocop +8 -0
- data/examples/hr/bin/setup +35 -0
- data/examples/hr/bin/thrust +5 -0
- data/examples/hr/config/application.rb +31 -0
- data/examples/hr/config/boot.rb +2 -0
- data/examples/hr/config/bundler-audit.yml +5 -0
- data/examples/hr/config/cache.yml +16 -0
- data/examples/hr/config/ci.rb +20 -0
- data/examples/hr/config/credentials.yml.enc +1 -0
- data/examples/hr/config/database.yml +36 -0
- data/examples/hr/config/deploy.yml +119 -0
- data/examples/hr/config/environment.rb +5 -0
- data/examples/hr/config/environments/development.rb +66 -0
- data/examples/hr/config/environments/production.rb +74 -0
- data/examples/hr/config/environments/test.rb +45 -0
- data/examples/hr/config/importmap.rb +3 -0
- data/examples/hr/config/initializers/assets.rb +7 -0
- data/examples/hr/config/initializers/content_security_policy.rb +29 -0
- data/examples/hr/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/hr/config/initializers/inflections.rb +16 -0
- data/examples/hr/config/initializers/lcp_ruby.rb +18 -0
- data/examples/hr/config/lcp_ruby/menu.yml +86 -0
- data/examples/hr/config/lcp_ruby/models/announcement.rb +33 -0
- data/examples/hr/config/lcp_ruby/models/asset.rb +73 -0
- data/examples/hr/config/lcp_ruby/models/asset_assignment.rb +39 -0
- data/examples/hr/config/lcp_ruby/models/audit_log.yml +57 -0
- data/examples/hr/config/lcp_ruby/models/candidate.rb +68 -0
- data/examples/hr/config/lcp_ruby/models/custom_field_definition.rb +60 -0
- data/examples/hr/config/lcp_ruby/models/dashboard.rb +17 -0
- data/examples/hr/config/lcp_ruby/models/document.rb +38 -0
- data/examples/hr/config/lcp_ruby/models/employee.rb +128 -0
- data/examples/hr/config/lcp_ruby/models/employee_skill.rb +29 -0
- data/examples/hr/config/lcp_ruby/models/expense_claim.rb +70 -0
- data/examples/hr/config/lcp_ruby/models/goal.rb +48 -0
- data/examples/hr/config/lcp_ruby/models/group.rb +37 -0
- data/examples/hr/config/lcp_ruby/models/group_membership.rb +26 -0
- data/examples/hr/config/lcp_ruby/models/interview.rb +54 -0
- data/examples/hr/config/lcp_ruby/models/job_posting.rb +67 -0
- data/examples/hr/config/lcp_ruby/models/leave_balance.rb +31 -0
- data/examples/hr/config/lcp_ruby/models/leave_request.rb +52 -0
- data/examples/hr/config/lcp_ruby/models/leave_type.rb +35 -0
- data/examples/hr/config/lcp_ruby/models/organization_unit.rb +42 -0
- data/examples/hr/config/lcp_ruby/models/performance_review.rb +59 -0
- data/examples/hr/config/lcp_ruby/models/position.rb +43 -0
- data/examples/hr/config/lcp_ruby/models/skill.rb +27 -0
- data/examples/hr/config/lcp_ruby/models/training_course.rb +53 -0
- data/examples/hr/config/lcp_ruby/models/training_enrollment.rb +35 -0
- data/examples/hr/config/lcp_ruby/pages/dashboard.yml +101 -0
- data/examples/hr/config/lcp_ruby/permissions/announcement.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/asset.yml +35 -0
- data/examples/hr/config/lcp_ruby/permissions/audit_log.yml +28 -0
- data/examples/hr/config/lcp_ruby/permissions/candidate.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/custom_field_definition.yml +28 -0
- data/examples/hr/config/lcp_ruby/permissions/dashboard.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/default.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/document.yml +37 -0
- data/examples/hr/config/lcp_ruby/permissions/employee.yml +55 -0
- data/examples/hr/config/lcp_ruby/permissions/expense_claim.yml +45 -0
- data/examples/hr/config/lcp_ruby/permissions/group.yml +27 -0
- data/examples/hr/config/lcp_ruby/permissions/job_posting.yml +34 -0
- data/examples/hr/config/lcp_ruby/permissions/leave_request.yml +45 -0
- data/examples/hr/config/lcp_ruby/permissions/performance_review.yml +42 -0
- data/examples/hr/config/lcp_ruby/presenters/announcement.rb +47 -0
- data/examples/hr/config/lcp_ruby/presenters/asset.rb +69 -0
- data/examples/hr/config/lcp_ruby/presenters/asset_assignment.rb +47 -0
- data/examples/hr/config/lcp_ruby/presenters/audit_logs.yml +43 -0
- data/examples/hr/config/lcp_ruby/presenters/candidate.rb +71 -0
- data/examples/hr/config/lcp_ruby/presenters/custom_fields.rb +124 -0
- data/examples/hr/config/lcp_ruby/presenters/dashboard.rb +37 -0
- data/examples/hr/config/lcp_ruby/presenters/document.rb +46 -0
- data/examples/hr/config/lcp_ruby/presenters/employee.rb +167 -0
- data/examples/hr/config/lcp_ruby/presenters/employee_archive.rb +38 -0
- data/examples/hr/config/lcp_ruby/presenters/employee_directory.rb +27 -0
- data/examples/hr/config/lcp_ruby/presenters/employee_skill.rb +57 -0
- data/examples/hr/config/lcp_ruby/presenters/expense_claim.rb +76 -0
- data/examples/hr/config/lcp_ruby/presenters/goal.rb +60 -0
- data/examples/hr/config/lcp_ruby/presenters/group.rb +48 -0
- data/examples/hr/config/lcp_ruby/presenters/interview.rb +59 -0
- data/examples/hr/config/lcp_ruby/presenters/job_posting.rb +73 -0
- data/examples/hr/config/lcp_ruby/presenters/leave_balance.rb +50 -0
- data/examples/hr/config/lcp_ruby/presenters/leave_request.rb +89 -0
- data/examples/hr/config/lcp_ruby/presenters/leave_type.rb +52 -0
- data/examples/hr/config/lcp_ruby/presenters/organization_unit.rb +56 -0
- data/examples/hr/config/lcp_ruby/presenters/performance_review.rb +87 -0
- data/examples/hr/config/lcp_ruby/presenters/position.rb +54 -0
- data/examples/hr/config/lcp_ruby/presenters/skill.rb +45 -0
- data/examples/hr/config/lcp_ruby/presenters/training_course.rb +61 -0
- data/examples/hr/config/lcp_ruby/presenters/training_enrollment.rb +49 -0
- data/examples/hr/config/lcp_ruby/views/announcements.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/asset_assignments.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/assets.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/audit_logs.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/candidates.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/custom_fields.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/dashboard.yml +6 -0
- data/examples/hr/config/lcp_ruby/views/documents.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/employee_skills.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/employees.yml +14 -0
- data/examples/hr/config/lcp_ruby/views/expense_claims.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/goals.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/groups.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/interviews.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/job_postings.yml +6 -0
- data/examples/hr/config/lcp_ruby/views/leave_balances.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/leave_requests.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/leave_types.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/organization_units.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/performance_reviews.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/positions.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/skills.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/training_courses.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/training_enrollments.yml +10 -0
- data/examples/hr/config/locales/cs.yml +496 -0
- data/examples/hr/config/locales/en.yml +740 -0
- data/examples/hr/config/locales/sk.yml +496 -0
- data/examples/hr/config/puma.rb +42 -0
- data/examples/hr/config/queue.yml +18 -0
- data/examples/hr/config/recurring.yml +15 -0
- data/examples/hr/config/routes.rb +4 -0
- data/examples/hr/config/storage.yml +27 -0
- data/examples/hr/config.ru +6 -0
- data/examples/hr/db/cache_schema.rb +12 -0
- data/examples/hr/db/migrate/20260303202825_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/hr/db/queue_schema.rb +129 -0
- data/examples/hr/db/schema.rb +588 -0
- data/examples/hr/db/seeds.rb +932 -0
- data/examples/hr/erd.md +396 -0
- data/examples/hr/public/400.html +135 -0
- data/examples/hr/public/404.html +135 -0
- data/examples/hr/public/406-unsupported-browser.html +135 -0
- data/examples/hr/public/422.html +135 -0
- data/examples/hr/public/500.html +135 -0
- data/examples/hr/public/icon.svg +3 -0
- data/examples/hr/public/robots.txt +1 -0
- data/examples/showcase/Gemfile +15 -0
- data/examples/showcase/Gemfile.lock +425 -0
- data/examples/showcase/Rakefile +2 -0
- data/examples/showcase/app/actions/showcase_batch_task/assign_batch.rb +20 -0
- data/examples/showcase/app/actions/showcase_batch_task/close_task.rb +21 -0
- data/examples/showcase/app/actions/showcase_condition/approve.rb +20 -0
- data/examples/showcase/app/actions/showcase_permission/lock.rb +20 -0
- data/examples/showcase/app/assets/config/manifest.js +3 -0
- data/examples/showcase/app/assets/stylesheets/application.css +27 -0
- data/examples/showcase/app/condition_services/budget_threshold.rb +16 -0
- data/examples/showcase/app/condition_services/overdue_check.rb +11 -0
- data/examples/showcase/app/controllers/application_controller.rb +2 -0
- data/examples/showcase/app/controllers/docs_controller.rb +48 -0
- data/examples/showcase/app/controllers/host_inventory_items_managed_controller.rb +90 -0
- data/examples/showcase/app/controllers/host_inventory_items_report_controller.rb +39 -0
- data/examples/showcase/app/controllers/host_inventory_items_wizard_controller.rb +50 -0
- data/examples/showcase/app/data_providers/showcase_amount_provider.rb +52 -0
- data/examples/showcase/app/data_providers/weather_station_provider.rb +140 -0
- data/examples/showcase/app/event_handlers/showcase_model/on_status_change.rb +17 -0
- data/examples/showcase/app/event_handlers/showcase_permission/on_status_change.rb +17 -0
- data/examples/showcase/app/event_handlers/showcase_workflow/log_review_exit.rb +16 -0
- data/examples/showcase/app/event_handlers/showcase_workflow/notify_reviewers.rb +15 -0
- data/examples/showcase/app/event_handlers/showcase_workflow/request_submitted.rb +15 -0
- data/examples/showcase/app/lcp_metrics/showcase_metrics.rb +31 -0
- data/examples/showcase/app/lcp_services/computed/showcase_score.rb +18 -0
- data/examples/showcase/app/lcp_services/computed/showcase_total.rb +13 -0
- data/examples/showcase/app/lcp_services/defaults/one_week_from_now.rb +11 -0
- data/examples/showcase/app/lcp_services/menu_items/recent_announcements.rb +36 -0
- data/examples/showcase/app/lcp_services/virtual_columns/project_health.rb +26 -0
- data/examples/showcase/app/model_extensions/lcp_error_log_extension.rb +12 -0
- data/examples/showcase/app/models/host_inventory_item.rb +20 -0
- data/examples/showcase/app/models/platform_profile.rb +27 -0
- data/examples/showcase/app/models/platform_user.rb +5 -0
- data/examples/showcase/app/views/docs/show.html.erb +19 -0
- data/examples/showcase/app/views/host_inventory_items_report/index.html.erb +61 -0
- data/examples/showcase/app/views/host_inventory_items_report/show.html.erb +39 -0
- data/examples/showcase/app/views/layouts/docs.html.erb +46 -0
- data/examples/showcase/app/views/showcase/menu_renderers/_status_pill.html.erb +17 -0
- data/examples/showcase/app/views/showcase_custom/_activity_timeline.html.erb +37 -0
- data/examples/showcase/app/views/showcase_custom/_card_index.html.erb +74 -0
- data/examples/showcase/app/views/showcase_custom/_detail_show.html.erb +115 -0
- data/examples/showcase/app/views/showcase_custom/_location_map.html.erb +29 -0
- data/examples/showcase/app/views/showcase_custom/_location_view.html.erb +34 -0
- data/examples/showcase/app/views/showcase_custom/_quick_stats.html.erb +50 -0
- data/examples/showcase/app/views/showcase_custom/_stats_sidebar.html.erb +42 -0
- data/examples/showcase/app/views/showcase_custom/_tags_editor.html.erb +48 -0
- data/examples/showcase/bin/rails +4 -0
- data/examples/showcase/bin/rake +4 -0
- data/examples/showcase/config/application.rb +37 -0
- data/examples/showcase/config/boot.rb +2 -0
- data/examples/showcase/config/database.yml +12 -0
- data/examples/showcase/config/environment.rb +2 -0
- data/examples/showcase/config/initializers/devise.rb +5 -0
- data/examples/showcase/config/initializers/lcp_ruby.rb +175 -0
- data/examples/showcase/config/lcp_ruby/auth.yml +60 -0
- data/examples/showcase/config/lcp_ruby/jobs/data_import.yml +7 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_cleanup.yml +11 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_event_triggered.yml +11 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_multi_step.yml +9 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_webhook.yml +13 -0
- data/examples/showcase/config/lcp_ruby/menu.yml +249 -0
- data/examples/showcase/config/lcp_ruby/models/_base_document.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/_categorized_document.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/_contactable.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/api_token.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/article.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/article_tag.rb +10 -0
- data/examples/showcase/config/lcp_ruby/models/author.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/batch_operation.yml +79 -0
- data/examples/showcase/config/lcp_ruby/models/batch_operation_item.yml +45 -0
- data/examples/showcase/config/lcp_ruby/models/category.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/comment.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/custom_field_definition.rb +59 -0
- data/examples/showcase/config/lcp_ruby/models/department.rb +32 -0
- data/examples/showcase/config/lcp_ruby/models/employee.rb +52 -0
- data/examples/showcase/config/lcp_ruby/models/employee_emergency_contact.rb +24 -0
- data/examples/showcase/config/lcp_ruby/models/employee_profile.rb +18 -0
- data/examples/showcase/config/lcp_ruby/models/employee_skill.rb +10 -0
- data/examples/showcase/config/lcp_ruby/models/export_log.yml +27 -0
- data/examples/showcase/config/lcp_ruby/models/export_profile.yml +37 -0
- data/examples/showcase/config/lcp_ruby/models/feature.rb +76 -0
- data/examples/showcase/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/group.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/group_membership.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/group_role_mapping.rb +14 -0
- data/examples/showcase/config/lcp_ruby/models/host_inventory_item.yml +111 -0
- data/examples/showcase/config/lcp_ruby/models/import_profile.rb +30 -0
- data/examples/showcase/config/lcp_ruby/models/import_row.rb +32 -0
- data/examples/showcase/config/lcp_ruby/models/ingredient_def.rb +11 -0
- data/examples/showcase/config/lcp_ruby/models/lcp_error_log.yml +61 -0
- data/examples/showcase/config/lcp_ruby/models/page_config.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/permission_config.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/pipeline.rb +15 -0
- data/examples/showcase/config/lcp_ruby/models/pipeline_stage.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/platform_profile.rb +104 -0
- data/examples/showcase/config/lcp_ruby/models/profile_setting.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/profile_tag.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/project.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/role.rb +24 -0
- data/examples/showcase/config/lcp_ruby/models/saved_filter.rb +50 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_aggregate.rb +93 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_company.rb +14 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_item.rb +25 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_announcement.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_array.rb +45 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_attachment.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_audit_log.yml +44 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_audited_record.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_batch_task.rb +27 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_business_unit.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition.rb +24 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition_category.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition_task.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition_threshold.rb +13 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_contact.rb +22 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_custom_render.rb +38 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_delete_reason.rb +11 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_division.rb +31 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_extensibility.rb +25 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_field.rb +53 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_form.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_form_action.rb +38 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_grade.rb +18 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_hr_employee.rb +90 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_item_class.rb +31 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_job_execution.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_memo.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_model.rb +74 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_organization.rb +31 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_permission.rb +30 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_person.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_positioning.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_quick_note.rb +13 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_recipe.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_report.rb +32 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_school_class.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_search.rb +84 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_sequence.rb +46 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete.rb +30 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete_item.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_student.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_type_default.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_userstamps.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_virtual_field.rb +68 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_workflow.rb +54 -0
- data/examples/showcase/config/lcp_ruby/models/skill.rb +22 -0
- data/examples/showcase/config/lcp_ruby/models/tag.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/user.rb +54 -0
- data/examples/showcase/config/lcp_ruby/models/weather_station.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_approval_request.yml +77 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_approval_step.yml +56 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_approval_task.yml +51 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_audit_log.yml +74 -0
- data/examples/showcase/config/lcp_ruby/pages/article_detail.yml +32 -0
- data/examples/showcase/config/lcp_ruby/pages/author_detail.yml +27 -0
- data/examples/showcase/config/lcp_ruby/pages/category_detail.yml +24 -0
- data/examples/showcase/config/lcp_ruby/pages/chart_showcase.yml +151 -0
- data/examples/showcase/config/lcp_ruby/pages/department_detail.yml +55 -0
- data/examples/showcase/config/lcp_ruby/pages/department_explorer.yml +60 -0
- data/examples/showcase/config/lcp_ruby/pages/employee_overview.yml +106 -0
- data/examples/showcase/config/lcp_ruby/pages/employee_transfer_dialog.yml +24 -0
- data/examples/showcase/config/lcp_ruby/pages/hr_turnover_dashboard.yml +288 -0
- data/examples/showcase/config/lcp_ruby/pages/main_dashboard.yml +142 -0
- data/examples/showcase/config/lcp_ruby/pages/monitoring_dashboard.yml +58 -0
- data/examples/showcase/config/lcp_ruby/pages/pipeline_detail.yml +16 -0
- data/examples/showcase/config/lcp_ruby/pages/showcase_custom_zones.yml +39 -0
- data/examples/showcase/config/lcp_ruby/pages/showcase_form_action_dialog.yml +11 -0
- data/examples/showcase/config/lcp_ruby/pages/workflow_request_detail.yml +34 -0
- data/examples/showcase/config/lcp_ruby/permissions/api_token.yml +26 -0
- data/examples/showcase/config/lcp_ruby/permissions/batch_operation.yml +36 -0
- data/examples/showcase/config/lcp_ruby/permissions/custom_field_definition.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/default.yml +28 -0
- data/examples/showcase/config/lcp_ruby/permissions/export_log.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/export_profile.yml +25 -0
- data/examples/showcase/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
- data/examples/showcase/config/lcp_ruby/permissions/group.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/group_membership.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/group_role_mapping.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/host_inventory_item.yml +34 -0
- data/examples/showcase/config/lcp_ruby/permissions/import_profile.yml +29 -0
- data/examples/showcase/config/lcp_ruby/permissions/import_row.yml +17 -0
- data/examples/showcase/config/lcp_ruby/permissions/lcp_error_log.yml +8 -0
- data/examples/showcase/config/lcp_ruby/permissions/page_config.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/permission_config.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/platform_profile.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/profile_setting.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/profile_tag.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/role.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/saved_filter.yml +39 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_announcement.yml +22 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_audit_log.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_audited_record.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_batch_task.yml +31 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_condition.yml +54 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_contact.yml +21 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_custom_render.yml +21 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_delete_reason.yml +12 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_form_action.yml +26 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_grade.yml +33 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_job_execution.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_permission.yml +47 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_quick_note.yml +12 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_school_class.yml +37 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_student.yml +43 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_workflow.yml +34 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_request.yml +18 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_step.yml +18 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_task.yml +18 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_audit_log.yml +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/announcements.rb +59 -0
- data/examples/showcase/config/lcp_ruby/presenters/approval_requests.rb +69 -0
- data/examples/showcase/config/lcp_ruby/presenters/approval_steps.rb +59 -0
- data/examples/showcase/config/lcp_ruby/presenters/approval_tasks.rb +65 -0
- data/examples/showcase/config/lcp_ruby/presenters/article_comments_zone.rb +12 -0
- data/examples/showcase/config/lcp_ruby/presenters/article_related_zone.rb +15 -0
- data/examples/showcase/config/lcp_ruby/presenters/articles.rb +149 -0
- data/examples/showcase/config/lcp_ruby/presenters/articles_tiles.rb +30 -0
- data/examples/showcase/config/lcp_ruby/presenters/author_articles_zone.rb +12 -0
- data/examples/showcase/config/lcp_ruby/presenters/author_show_zone.rb +12 -0
- data/examples/showcase/config/lcp_ruby/presenters/authors.rb +39 -0
- data/examples/showcase/config/lcp_ruby/presenters/batch_operation_items.yml +45 -0
- data/examples/showcase/config/lcp_ruby/presenters/batch_operations.yml +66 -0
- data/examples/showcase/config/lcp_ruby/presenters/categories.rb +50 -0
- data/examples/showcase/config/lcp_ruby/presenters/category_articles_zone.rb +20 -0
- data/examples/showcase/config/lcp_ruby/presenters/category_children_zone.rb +10 -0
- data/examples/showcase/config/lcp_ruby/presenters/comment_quick_add_dialog.rb +15 -0
- data/examples/showcase/config/lcp_ruby/presenters/custom_fields.rb +124 -0
- data/examples/showcase/config/lcp_ruby/presenters/dashboard_employees.rb +16 -0
- data/examples/showcase/config/lcp_ruby/presenters/delete_reason_dialog.rb +13 -0
- data/examples/showcase/config/lcp_ruby/presenters/departments.rb +55 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_add_employee_dialog.rb +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_children_zone.rb +10 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_detail_zone.rb +14 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_employees_zone.rb +16 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_list_selection_zone.rb +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_overview_index_zone.rb +36 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_quick_add_dialog.rb +14 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_show_zone.rb +20 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_transfer_form_zone.rb +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/employees.rb +165 -0
- data/examples/showcase/config/lcp_ruby/presenters/employees_tiles.rb +33 -0
- data/examples/showcase/config/lcp_ruby/presenters/export_logs.yml +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/export_profiles.yml +70 -0
- data/examples/showcase/config/lcp_ruby/presenters/extensibility_quick_edit_dialog.rb +13 -0
- data/examples/showcase/config/lcp_ruby/presenters/feature_kanban.rb +30 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_card.rb +179 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_hub.rb +44 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_table.rb +37 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_tiles.rb +48 -0
- data/examples/showcase/config/lcp_ruby/presenters/group_memberships.rb +54 -0
- data/examples/showcase/config/lcp_ruby/presenters/group_role_mappings.rb +48 -0
- data/examples/showcase/config/lcp_ruby/presenters/groups.rb +74 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items.rb +99 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_managed.rb +84 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_report.rb +40 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_wizard.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/import_profiles.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/import_rows.rb +39 -0
- data/examples/showcase/config/lcp_ruby/presenters/lcp_error_logs.yml +46 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens.rb +35 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens_create_dialog.rb +27 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_employee_profile.rb +24 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_settings.rb +50 -0
- data/examples/showcase/config/lcp_ruby/presenters/page_configs.rb +56 -0
- data/examples/showcase/config/lcp_ruby/presenters/permission_configs.rb +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipeline_edit_zone.rb +11 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages.rb +51 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages_zone.rb +11 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipelines.rb +40 -0
- data/examples/showcase/config/lcp_ruby/presenters/platform_profiles.rb +90 -0
- data/examples/showcase/config/lcp_ruby/presenters/projects.rb +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/quick_note_dialog.rb +14 -0
- data/examples/showcase/config/lcp_ruby/presenters/roles.rb +60 -0
- data/examples/showcase/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
- data/examples/showcase/config/lcp_ruby/presenters/saved_filters.rb +94 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregate_items.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates.rb +115 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates_tiles.rb +44 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_amount_kanban.rb +31 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_arrays.rb +140 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_attachments.rb +66 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_audit_logs.rb +46 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_audited_records.rb +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks.rb +97 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks_archive.rb +52 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_business_units.rb +43 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_conditions.rb +195 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_contacts.rb +82 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_render_with.rb +50 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_sections.rb +88 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_zones_main.rb +75 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_divisions.rb +53 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_extensibility.rb +48 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_card.rb +34 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_table.rb +128 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_tiles.rb +42 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_form_action_dialog.rb +27 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions.rb +132 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions_overflow.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_forms.rb +110 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_grades.rb +38 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_hr_employees.rb +84 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_item_classes.rb +120 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_job_executions.rb +150 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_memos.rb +96 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_models.rb +112 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_organizations.rb +106 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_people.rb +97 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_permissions.rb +84 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_positioning.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes.rb +105 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes_raw.rb +32 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_reports.rb +109 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_school_classes.rb +34 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_searches.rb +226 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_sequences.rb +67 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete.rb +88 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_archive.rb +52 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_items.rb +47 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_students.rb +33 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_type_defaults.rb +92 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_userstamps.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_virtual_fields.rb +104 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_admin.rb +24 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_kanban.rb +36 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_workflows.rb +103 -0
- data/examples/showcase/config/lcp_ruby/presenters/tags.rb +35 -0
- data/examples/showcase/config/lcp_ruby/presenters/users.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/weather_stations.rb +60 -0
- data/examples/showcase/config/lcp_ruby/presenters/workflow_audit_logs.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/workflow_request_audit_zone.rb +28 -0
- data/examples/showcase/config/lcp_ruby/theme.yml +2 -0
- data/examples/showcase/config/lcp_ruby/types/currency.yml +15 -0
- data/examples/showcase/config/lcp_ruby/types/percentage.yml +16 -0
- data/examples/showcase/config/lcp_ruby/types/rating.yml +20 -0
- data/examples/showcase/config/lcp_ruby/views/announcements.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/approval_requests.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/approval_steps.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/approval_tasks.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/articles.yml +12 -0
- data/examples/showcase/config/lcp_ruby/views/authors.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/batch_operation_items.yml +6 -0
- data/examples/showcase/config/lcp_ruby/views/batch_operations.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/categories.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/custom_fields.rb +8 -0
- data/examples/showcase/config/lcp_ruby/views/dashboard.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/department_explorer.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/departments.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/employee_overview.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/employees.yml +12 -0
- data/examples/showcase/config/lcp_ruby/views/export_logs.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/export_profiles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/features.yml +19 -0
- data/examples/showcase/config/lcp_ruby/views/group_memberships.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/group_role_mappings.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/groups.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/host_inventory_items.yml +16 -0
- data/examples/showcase/config/lcp_ruby/views/hr_turnover_dashboard.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/import_profiles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/import_rows.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/lcp_error_logs.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/monitoring.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/my_api_tokens.rb +9 -0
- data/examples/showcase/config/lcp_ruby/views/page_configs.yml +11 -0
- data/examples/showcase/config/lcp_ruby/views/permission_configs.yml +11 -0
- data/examples/showcase/config/lcp_ruby/views/pipeline_stages.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/pipelines.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/platform_profiles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/projects.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/roles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/saved_filters.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_aggregate_items.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_aggregates.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_arrays.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_attachments.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_audit_logs.yml +11 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_audited_records.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_batch_tasks.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_business_units.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_conditions.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_contacts.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_custom_render_with.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_custom_sections.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_custom_zones.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_divisions.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_extensibility.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_fields.yml +13 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_form_actions.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_forms.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_grades.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_hr_employees.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_item_classes.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_job_executions.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_memos.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_models.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_organizations.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_people.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_permissions.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_positioning.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_recipes.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_reports.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_school_classes.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_searches.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_sequences.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete_items.yml +8 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_students.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_userstamps.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_virtual_fields.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_workflows.yml +16 -0
- data/examples/showcase/config/lcp_ruby/views/tags.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/users.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/weather_stations.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/workflow_audit_logs.yml +9 -0
- data/examples/showcase/config/lcp_ruby/workflows/showcase_form_action.rb +48 -0
- data/examples/showcase/config/lcp_ruby/workflows/showcase_workflow.rb +199 -0
- data/examples/showcase/config/locales/cs.yml +2818 -0
- data/examples/showcase/config/locales/en.yml +67 -0
- data/examples/showcase/config/locales/lcp_ruby/api_tokens.cs.yml +46 -0
- data/examples/showcase/config/locales/lcp_ruby/api_tokens.en.yml +49 -0
- data/examples/showcase/config/routes.rb +21 -0
- data/examples/showcase/config/storage.yml +3 -0
- data/examples/showcase/config.ru +2 -0
- data/examples/showcase/db/migrate/20260219124321_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/showcase/db/migrate/20260220072723_create_lcp_ruby_users.rb +42 -0
- data/examples/showcase/db/migrate/20260406120000_create_host_inventory_items.rb +32 -0
- data/examples/showcase/db/migrate/20260428134542_add_oidc_columns_to_lcp_ruby_users.rb +13 -0
- data/examples/showcase/db/migrate/20260502120000_create_platform_profiles.rb +19 -0
- data/examples/showcase/db/schema.rb +1222 -0
- data/examples/showcase/db/seeds.rb +3085 -0
- data/examples/showcase/erd.md +567 -0
- data/examples/showcase/test/fixtures/import_articles.csv +4 -0
- data/examples/showcase/test/fixtures/import_employees.csv +4 -0
- data/examples/todo/Gemfile +8 -0
- data/examples/todo/Gemfile.lock +415 -0
- data/examples/todo/Rakefile +2 -0
- data/examples/todo/app/assets/config/manifest.js +1 -0
- data/examples/todo/app/controllers/application_controller.rb +6 -0
- data/examples/todo/app/lcp_services/defaults/one_week_from_now.rb +11 -0
- data/examples/todo/bin/rails +4 -0
- data/examples/todo/bin/rake +4 -0
- data/examples/todo/config/application.rb +31 -0
- data/examples/todo/config/boot.rb +2 -0
- data/examples/todo/config/database.yml +12 -0
- data/examples/todo/config/environment.rb +2 -0
- data/examples/todo/config/lcp_ruby/models/todo_item.yml +67 -0
- data/examples/todo/config/lcp_ruby/models/todo_list.yml +49 -0
- data/examples/todo/config/lcp_ruby/permissions/default.yml +10 -0
- data/examples/todo/config/lcp_ruby/permissions/todo_item.yml +21 -0
- data/examples/todo/config/lcp_ruby/presenters/todo_item.yml +75 -0
- data/examples/todo/config/lcp_ruby/presenters/todo_list.yml +68 -0
- data/examples/todo/config/lcp_ruby/views/todo_items.yml +11 -0
- data/examples/todo/config/lcp_ruby/views/todo_lists.yml +9 -0
- data/examples/todo/config/routes.rb +4 -0
- data/examples/todo/config/storage.yml +3 -0
- data/examples/todo/config.ru +2 -0
- data/examples/todo/db/migrate/20260219103416_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/todo/db/schema.rb +63 -0
- data/examples/todo/db/seeds.rb +24 -0
- data/examples/todo/erd.md +21 -0
- data/exe/lcp +33 -0
- data/lib/generators/lcp_ruby/agent_setup_generator.rb +102 -0
- data/lib/generators/lcp_ruby/api_tokens_generator.rb +102 -0
- data/lib/generators/lcp_ruby/auditing_generator.rb +54 -0
- data/lib/generators/lcp_ruby/background_jobs_generator.rb +61 -0
- data/lib/generators/lcp_ruby/batch_operations_generator.rb +62 -0
- data/lib/generators/lcp_ruby/claude_skills_generator.rb +47 -0
- data/lib/generators/lcp_ruby/custom_fields_generator.rb +54 -0
- data/lib/generators/lcp_ruby/dsl_to_yaml.rb +72 -0
- data/lib/generators/lcp_ruby/entity/color_palette.rb +22 -0
- data/lib/generators/lcp_ruby/entity/field_descriptor.rb +24 -0
- data/lib/generators/lcp_ruby/entity/field_token_parser.rb +101 -0
- data/lib/generators/lcp_ruby/entity/role_discovery.rb +45 -0
- data/lib/generators/lcp_ruby/entity_generator.rb +1104 -0
- data/lib/generators/lcp_ruby/export_generator.rb +71 -0
- data/lib/generators/lcp_ruby/format_support.rb +56 -0
- data/lib/generators/lcp_ruby/gapfree_sequences_generator.rb +64 -0
- data/lib/generators/lcp_ruby/groups_generator.rb +94 -0
- data/lib/generators/lcp_ruby/host_controller_generator.rb +202 -0
- data/lib/generators/lcp_ruby/import_generator.rb +96 -0
- data/lib/generators/lcp_ruby/install_auth_generator.rb +432 -0
- data/lib/generators/lcp_ruby/install_generator.rb +319 -0
- data/lib/generators/lcp_ruby/monitoring_generator.rb +58 -0
- data/lib/generators/lcp_ruby/oidc_role_mappings_generator.rb +60 -0
- data/lib/generators/lcp_ruby/pages_generator.rb +66 -0
- data/lib/generators/lcp_ruby/permission_source_generator.rb +66 -0
- data/lib/generators/lcp_ruby/role_model_generator.rb +73 -0
- data/lib/generators/lcp_ruby/saved_filters_generator.rb +62 -0
- data/lib/generators/lcp_ruby/templates/add_oidc_columns_to_lcp_ruby_users.rb.erb +13 -0
- data/lib/generators/lcp_ruby/templates/agent_setup/agents_md.md +3 -0
- data/lib/generators/lcp_ruby/templates/agent_setup/claude_md.md +12 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/create_dialog.rb +31 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/locales.en.yml +43 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/model.rb +23 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/permissions.yml +30 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/presenter.rb +38 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/view_group.rb +13 -0
- data/lib/generators/lcp_ruby/templates/auditing/model.rb +34 -0
- data/lib/generators/lcp_ruby/templates/auditing/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/auditing/presenter.rb +41 -0
- data/lib/generators/lcp_ruby/templates/auditing/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/model.rb +59 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/permissions.yml +22 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/presenter.rb +82 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/item_model.rb +28 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/item_presenter.rb +43 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/model.rb +42 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/permissions.yml +44 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/presenter.rb +61 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/create_lcp_ruby_users.rb.erb +50 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/model.rb +60 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/presenter.rb +128 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/entity/model.rb +42 -0
- data/lib/generators/lcp_ruby/templates/entity/permissions.yml +20 -0
- data/lib/generators/lcp_ruby/templates/entity/presenter.rb +55 -0
- data/lib/generators/lcp_ruby/templates/entity/view_group.rb +8 -0
- data/lib/generators/lcp_ruby/templates/export/export_log_model.rb +30 -0
- data/lib/generators/lcp_ruby/templates/export/export_log_permissions.yml +24 -0
- data/lib/generators/lcp_ruby/templates/export/export_logs_presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/export/export_profile_model.rb +28 -0
- data/lib/generators/lcp_ruby/templates/export/export_profile_permissions.yml +26 -0
- data/lib/generators/lcp_ruby/templates/export/export_profiles_presenter.rb +59 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/model.rb +18 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/groups/group_membership_model.rb +17 -0
- data/lib/generators/lcp_ruby/templates/groups/group_model.rb +28 -0
- data/lib/generators/lcp_ruby/templates/groups/group_permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/groups/group_presenter.rb +53 -0
- data/lib/generators/lcp_ruby/templates/groups/group_role_mapping_model.rb +15 -0
- data/lib/generators/lcp_ruby/templates/groups/group_view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/host_controller/controller.rb.erb +131 -0
- data/lib/generators/lcp_ruby/templates/import/data_import_job.yml +7 -0
- data/lib/generators/lcp_ruby/templates/import/import_profile_model.rb +30 -0
- data/lib/generators/lcp_ruby/templates/import/import_profile_permissions.yml +29 -0
- data/lib/generators/lcp_ruby/templates/import/import_profiles_presenter.rb +57 -0
- data/lib/generators/lcp_ruby/templates/import/import_row_model.rb +32 -0
- data/lib/generators/lcp_ruby/templates/import/import_row_permissions.yml +11 -0
- data/lib/generators/lcp_ruby/templates/import/import_rows_presenter.rb +35 -0
- data/lib/generators/lcp_ruby/templates/install/default_permissions.yml +26 -0
- data/lib/generators/lcp_ruby/templates/install/menu.yml.tt +71 -0
- data/lib/generators/lcp_ruby/templates/install/model.rb +20 -0
- data/lib/generators/lcp_ruby/templates/install/permissions.yml +25 -0
- data/lib/generators/lcp_ruby/templates/install/presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/install/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/entra.yml.erb +46 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/generic.yml.erb +55 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/google.yml.erb +42 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/keycloak.yml.erb +45 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/okta.yml.erb +45 -0
- data/lib/generators/lcp_ruby/templates/install_auth/user.rb +36 -0
- data/lib/generators/lcp_ruby/templates/monitoring/model.rb +34 -0
- data/lib/generators/lcp_ruby/templates/monitoring/model_extension.rb +12 -0
- data/lib/generators/lcp_ruby/templates/monitoring/page.yml +49 -0
- data/lib/generators/lcp_ruby/templates/monitoring/permissions.yml +8 -0
- data/lib/generators/lcp_ruby/templates/monitoring/presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/locales.en.yml +15 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/model.rb +32 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/permissions.yml +21 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/presenter.rb +41 -0
- data/lib/generators/lcp_ruby/templates/pages/model.rb +19 -0
- data/lib/generators/lcp_ruby/templates/pages/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/pages/presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/pages/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/permission_source/model.rb +21 -0
- data/lib/generators/lcp_ruby/templates/permission_source/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/permission_source/presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/permission_source/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/role_model/model.rb +24 -0
- data/lib/generators/lcp_ruby/templates/role_model/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/role_model/presenter.rb +62 -0
- data/lib/generators/lcp_ruby/templates/role_model/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/model.rb +51 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/permissions.yml +44 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/presenter.rb +96 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/save_dialog_presenter.rb +19 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_model.rb +50 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/step_model.rb +36 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/step_permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_model.rb +34 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_presenter.rb +39 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/model.rb +48 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/model.rb +43 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/presenter.rb +70 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/workflow_approvals_generator.rb +93 -0
- data/lib/generators/lcp_ruby/workflow_audit_log_generator.rb +54 -0
- data/lib/generators/lcp_ruby/workflow_definition_generator.rb +77 -0
- data/lib/lcp.rb +6 -0
- data/lib/lcp_ruby/actions/action_executor.rb +66 -0
- data/lib/lcp_ruby/actions/action_registry.rb +48 -0
- data/lib/lcp_ruby/actions/api_tokens/revoke.rb +13 -0
- data/lib/lcp_ruby/actions/base_action.rb +79 -0
- data/lib/lcp_ruby/actions/form_action_pipeline.rb +138 -0
- data/lib/lcp_ruby/aggregates/query_builder.rb +6 -0
- data/lib/lcp_ruby/api_tokens/model_extension.rb +41 -0
- data/lib/lcp_ruby/api_tokens/resolver_registry.rb +53 -0
- data/lib/lcp_ruby/api_tokens/token_generator.rb +27 -0
- data/lib/lcp_ruby/api_tokens/verifier.rb +38 -0
- data/lib/lcp_ruby/app_template.rb +181 -0
- data/lib/lcp_ruby/array_query.rb +120 -0
- data/lib/lcp_ruby/asset_copier.rb +62 -0
- data/lib/lcp_ruby/association_fk_type.rb +191 -0
- data/lib/lcp_ruby/association_join_column.rb +28 -0
- data/lib/lcp_ruby/association_options_builder.rb +231 -0
- data/lib/lcp_ruby/auditing/audit_writer.rb +258 -0
- data/lib/lcp_ruby/auditing/contract_validator.rb +95 -0
- data/lib/lcp_ruby/auditing/registry.rb +29 -0
- data/lib/lcp_ruby/auditing/setup.rb +49 -0
- data/lib/lcp_ruby/authentication/audit_subscriber.rb +51 -0
- data/lib/lcp_ruby/authentication/bearer_jwt_verifier.rb +139 -0
- data/lib/lcp_ruby/authentication/devise_setup.rb +47 -0
- data/lib/lcp_ruby/authentication/errors.rb +27 -0
- data/lib/lcp_ruby/authentication/http_fetcher.rb +36 -0
- data/lib/lcp_ruby/authentication/jwks_cache.rb +91 -0
- data/lib/lcp_ruby/authentication/oidc_bearer_resolver.rb +84 -0
- data/lib/lcp_ruby/authentication/omniauth_builder.rb +147 -0
- data/lib/lcp_ruby/authentication/provider.rb +108 -0
- data/lib/lcp_ruby/authentication/provider_registry.rb +227 -0
- data/lib/lcp_ruby/authentication/role_mapper.rb +94 -0
- data/lib/lcp_ruby/authentication/test_support.rb +257 -0
- data/lib/lcp_ruby/authentication/user_resolver.rb +169 -0
- data/lib/lcp_ruby/authentication.rb +40 -0
- data/lib/lcp_ruby/authorization/association_lookup.rb +56 -0
- data/lib/lcp_ruby/authorization/authorization_error.rb +12 -0
- data/lib/lcp_ruby/authorization/cache.rb +89 -0
- data/lib/lcp_ruby/authorization/codes.rb +17 -0
- data/lib/lcp_ruby/authorization/impersonated_user.rb +29 -0
- data/lib/lcp_ruby/authorization/includes_hint.rb +110 -0
- data/lib/lcp_ruby/authorization/inherited_parent_validator.rb +142 -0
- data/lib/lcp_ruby/authorization/invariant_check/configuration.rb +132 -0
- data/lib/lcp_ruby/authorization/invariant_error.rb +15 -0
- data/lib/lcp_ruby/authorization/misconfigured_page_error.rb +30 -0
- data/lib/lcp_ruby/authorization/page_gate.rb +57 -0
- data/lib/lcp_ruby/authorization/permission_evaluator.rb +343 -0
- data/lib/lcp_ruby/authorization/policy_factory.rb +91 -0
- data/lib/lcp_ruby/authorization/runtime_invariant_validator.rb +421 -0
- data/lib/lcp_ruby/authorization/scope_builder.rb +227 -0
- data/lib/lcp_ruby/authorization/scope_resolver.rb +28 -0
- data/lib/lcp_ruby/authorized_controller.rb +44 -0
- data/lib/lcp_ruby/background_jobs/base_handler.rb +113 -0
- data/lib/lcp_ruby/background_jobs/change_handler.rb +17 -0
- data/lib/lcp_ruby/background_jobs/contract.rb +16 -0
- data/lib/lcp_ruby/background_jobs/contract_validator.rb +112 -0
- data/lib/lcp_ruby/background_jobs/declarative/base_action.rb +11 -0
- data/lib/lcp_ruby/background_jobs/declarative/call_webhook_action.rb +174 -0
- data/lib/lcp_ruby/background_jobs/declarative/fire_event_action.rb +24 -0
- data/lib/lcp_ruby/background_jobs/declarative/registry.rb +34 -0
- data/lib/lcp_ruby/background_jobs/declarative/run_scope_action.rb +98 -0
- data/lib/lcp_ruby/background_jobs/declarative/send_notification_action.rb +13 -0
- data/lib/lcp_ruby/background_jobs/definition.rb +134 -0
- data/lib/lcp_ruby/background_jobs/enqueue.rb +83 -0
- data/lib/lcp_ruby/background_jobs/errors.rb +9 -0
- data/lib/lcp_ruby/background_jobs/executor_job.rb +111 -0
- data/lib/lcp_ruby/background_jobs/handler_factory.rb +46 -0
- data/lib/lcp_ruby/background_jobs/host_source.rb +33 -0
- data/lib/lcp_ruby/background_jobs/model_source.rb +93 -0
- data/lib/lcp_ruby/background_jobs/registry.rb +81 -0
- data/lib/lcp_ruby/background_jobs/resolver.rb +29 -0
- data/lib/lcp_ruby/background_jobs/schedule_adapter.rb +14 -0
- data/lib/lcp_ruby/background_jobs/setup.rb +145 -0
- data/lib/lcp_ruby/background_jobs/static_source.rb +19 -0
- data/lib/lcp_ruby/background_jobs/steps_executor.rb +52 -0
- data/lib/lcp_ruby/background_jobs/triggers/event_trigger.rb +97 -0
- data/lib/lcp_ruby/background_jobs/triggers/trigger_installer.rb +20 -0
- data/lib/lcp_ruby/background_jobs/unique_key_builder.rb +31 -0
- data/lib/lcp_ruby/batch_actions/base_service.rb +70 -0
- data/lib/lcp_ruby/batch_actions/batch_action_handler.rb +200 -0
- data/lib/lcp_ruby/batch_actions/custom_action_dispatcher.rb +133 -0
- data/lib/lcp_ruby/batch_actions/destroy_service.rb +37 -0
- data/lib/lcp_ruby/batch_actions/permanently_destroy_service.rb +33 -0
- data/lib/lcp_ruby/batch_actions/restore_service.rb +33 -0
- data/lib/lcp_ruby/batch_actions.rb +5 -0
- data/lib/lcp_ruby/bulk_updater.rb +25 -0
- data/lib/lcp_ruby/cli/docs_command.rb +29 -0
- data/lib/lcp_ruby/cli/examples_command.rb +29 -0
- data/lib/lcp_ruby/cli/new_command.rb +509 -0
- data/lib/lcp_ruby/cli/run_command.rb +155 -0
- data/lib/lcp_ruby/cli/skills_command.rb +54 -0
- data/lib/lcp_ruby/cli.rb +74 -0
- data/lib/lcp_ruby/condition_evaluator.rb +366 -0
- data/lib/lcp_ruby/condition_service_registry.rb +58 -0
- data/lib/lcp_ruby/condition_services/current_user_role.rb +28 -0
- data/lib/lcp_ruby/condition_services/feature_flag.rb +63 -0
- data/lib/lcp_ruby/condition_services/impersonating.rb +24 -0
- data/lib/lcp_ruby/conditions/validator.rb +35 -0
- data/lib/lcp_ruby/configuration.rb +431 -0
- data/lib/lcp_ruby/controller/authentication.rb +118 -0
- data/lib/lcp_ruby/controller/authorization.rb +198 -0
- data/lib/lcp_ruby/controller/bearer_authentication.rb +76 -0
- data/lib/lcp_ruby/controller/crud_helpers.rb +233 -0
- data/lib/lcp_ruby/controller/error_handling.rb +94 -0
- data/lib/lcp_ruby/controller/impersonation.rb +70 -0
- data/lib/lcp_ruby/controller/locale_binding.rb +62 -0
- data/lib/lcp_ruby/controller/path_helpers.rb +125 -0
- data/lib/lcp_ruby/controller/presenter_setup.rb +89 -0
- data/lib/lcp_ruby/controller/search.rb +321 -0
- data/lib/lcp_ruby/controller/view_helpers.rb +105 -0
- data/lib/lcp_ruby/current.rb +13 -0
- data/lib/lcp_ruby/custom_fields/applicator.rb +194 -0
- data/lib/lcp_ruby/custom_fields/contract_validator.rb +77 -0
- data/lib/lcp_ruby/custom_fields/definition_change_handler.rb +21 -0
- data/lib/lcp_ruby/custom_fields/query.rb +112 -0
- data/lib/lcp_ruby/custom_fields/registry.rb +70 -0
- data/lib/lcp_ruby/custom_fields/setup.rb +58 -0
- data/lib/lcp_ruby/custom_fields/utils.rb +40 -0
- data/lib/lcp_ruby/custom_fields.rb +9 -0
- data/lib/lcp_ruby/data_source/api_error_placeholder.rb +47 -0
- data/lib/lcp_ruby/data_source/api_filter_translator.rb +72 -0
- data/lib/lcp_ruby/data_source/api_model_concern.rb +131 -0
- data/lib/lcp_ruby/data_source/api_preloader.rb +44 -0
- data/lib/lcp_ruby/data_source/base.rb +85 -0
- data/lib/lcp_ruby/data_source/cached_wrapper.rb +107 -0
- data/lib/lcp_ruby/data_source/host.rb +71 -0
- data/lib/lcp_ruby/data_source/registry.rb +39 -0
- data/lib/lcp_ruby/data_source/resilient_wrapper.rb +67 -0
- data/lib/lcp_ruby/data_source/rest_json.rb +247 -0
- data/lib/lcp_ruby/data_source/setup.rb +57 -0
- data/lib/lcp_ruby/dev_toolbar.rb +8 -0
- data/lib/lcp_ruby/display/base_renderer.rb +21 -0
- data/lib/lcp_ruby/display/count_badge.rb +11 -0
- data/lib/lcp_ruby/display/icon_badge.rb +17 -0
- data/lib/lcp_ruby/display/renderer_registry.rb +138 -0
- data/lib/lcp_ruby/display/renderers/attachment_link.rb +26 -0
- data/lib/lcp_ruby/display/renderers/attachment_list.rb +32 -0
- data/lib/lcp_ruby/display/renderers/attachment_preview.rb +26 -0
- data/lib/lcp_ruby/display/renderers/avatar.rb +14 -0
- data/lib/lcp_ruby/display/renderers/badge.rb +43 -0
- data/lib/lcp_ruby/display/renderers/boolean_icon.rb +54 -0
- data/lib/lcp_ruby/display/renderers/code.rb +39 -0
- data/lib/lcp_ruby/display/renderers/collection.rb +34 -0
- data/lib/lcp_ruby/display/renderers/color_swatch.rb +25 -0
- data/lib/lcp_ruby/display/renderers/concerns/attachment_helpers.rb +73 -0
- data/lib/lcp_ruby/display/renderers/concerns/workflow_helpers.rb +35 -0
- data/lib/lcp_ruby/display/renderers/copy_code.rb +33 -0
- data/lib/lcp_ruby/display/renderers/currency.rb +14 -0
- data/lib/lcp_ruby/display/renderers/date.rb +17 -0
- data/lib/lcp_ruby/display/renderers/datetime.rb +17 -0
- data/lib/lcp_ruby/display/renderers/email_link.rb +15 -0
- data/lib/lcp_ruby/display/renderers/file_size.rb +11 -0
- data/lib/lcp_ruby/display/renderers/heading.rb +11 -0
- data/lib/lcp_ruby/display/renderers/image.rb +17 -0
- data/lib/lcp_ruby/display/renderers/internal_link.rb +23 -0
- data/lib/lcp_ruby/display/renderers/link.rb +15 -0
- data/lib/lcp_ruby/display/renderers/link_list.rb +90 -0
- data/lib/lcp_ruby/display/renderers/markdown.rb +33 -0
- data/lib/lcp_ruby/display/renderers/number.rb +14 -0
- data/lib/lcp_ruby/display/renderers/percentage.rb +12 -0
- data/lib/lcp_ruby/display/renderers/phone_link.rb +15 -0
- data/lib/lcp_ruby/display/renderers/progress_bar.rb +15 -0
- data/lib/lcp_ruby/display/renderers/rating.rb +15 -0
- data/lib/lcp_ruby/display/renderers/record_link.rb +101 -0
- data/lib/lcp_ruby/display/renderers/relative_date.rb +15 -0
- data/lib/lcp_ruby/display/renderers/rich_text.rb +15 -0
- data/lib/lcp_ruby/display/renderers/text.rb +12 -0
- data/lib/lcp_ruby/display/renderers/truncate.rb +17 -0
- data/lib/lcp_ruby/display/renderers/url_link.rb +22 -0
- data/lib/lcp_ruby/display/renderers/workflow_badge.rb +37 -0
- data/lib/lcp_ruby/display/renderers/workflow_timeline.rb +173 -0
- data/lib/lcp_ruby/display/renderers.rb +3 -0
- data/lib/lcp_ruby/display/text_badge.rb +15 -0
- data/lib/lcp_ruby/dsl/condition_builder.rb +190 -0
- data/lib/lcp_ruby/dsl/dsl_loader.rb +365 -0
- data/lib/lcp_ruby/dsl/field_builder.rb +35 -0
- data/lib/lcp_ruby/dsl/job_builder.rb +92 -0
- data/lib/lcp_ruby/dsl/model_builder.rb +544 -0
- data/lib/lcp_ruby/dsl/presenter_builder.rb +1272 -0
- data/lib/lcp_ruby/dsl/source_location_capture.rb +52 -0
- data/lib/lcp_ruby/dsl/type_builder.rb +88 -0
- data/lib/lcp_ruby/dsl/view_group_builder.rb +92 -0
- data/lib/lcp_ruby/dsl/workflow_builder.rb +319 -0
- data/lib/lcp_ruby/dynamic.rb +7 -0
- data/lib/lcp_ruby/dynamic_references/resolver.rb +154 -0
- data/lib/lcp_ruby/dynamic_references/validator.rb +92 -0
- data/lib/lcp_ruby/embed_providers/base.rb +18 -0
- data/lib/lcp_ruby/embed_providers/grafana.rb +38 -0
- data/lib/lcp_ruby/embed_providers/metabase.rb +37 -0
- data/lib/lcp_ruby/engine.rb +680 -0
- data/lib/lcp_ruby/events/async_handler_job.rb +21 -0
- data/lib/lcp_ruby/events/dispatcher.rb +52 -0
- data/lib/lcp_ruby/events/handler_base.rb +51 -0
- data/lib/lcp_ruby/events/handler_registry.rb +49 -0
- data/lib/lcp_ruby/export/data_generator.rb +158 -0
- data/lib/lcp_ruby/export/export_handler.rb +315 -0
- data/lib/lcp_ruby/export/field_tree_builder.rb +219 -0
- data/lib/lcp_ruby/export/setup.rb +94 -0
- data/lib/lcp_ruby/export/value_formatter.rb +223 -0
- data/lib/lcp_ruby/export.rb +9 -0
- data/lib/lcp_ruby/gem_paths.rb +51 -0
- data/lib/lcp_ruby/generators/entity_menu_writer.rb +258 -0
- data/lib/lcp_ruby/generators/feature_registry.rb +208 -0
- data/lib/lcp_ruby/generators/prerequisites.rb +90 -0
- data/lib/lcp_ruby/grouped_query/builder.rb +206 -0
- data/lib/lcp_ruby/grouped_query/result_wrapper.rb +72 -0
- data/lib/lcp_ruby/grouped_query/row.rb +31 -0
- data/lib/lcp_ruby/groups/change_handler.rb +18 -0
- data/lib/lcp_ruby/groups/contract.rb +42 -0
- data/lib/lcp_ruby/groups/contract_validator.rb +110 -0
- data/lib/lcp_ruby/groups/host_loader.rb +54 -0
- data/lib/lcp_ruby/groups/model_loader.rb +186 -0
- data/lib/lcp_ruby/groups/registry.rb +113 -0
- data/lib/lcp_ruby/groups/setup.rb +129 -0
- data/lib/lcp_ruby/groups/yaml_loader.rb +97 -0
- data/lib/lcp_ruby/hash_utils.rb +42 -0
- data/lib/lcp_ruby/i18n_check/configuration.rb +104 -0
- data/lib/lcp_ruby/i18n_check/heuristics.rb +26 -0
- data/lib/lcp_ruby/i18n_check/key_deriver.rb +136 -0
- data/lib/lcp_ruby/i18n_check/offense.rb +29 -0
- data/lib/lcp_ruby/i18n_check/registry_walker.rb +621 -0
- data/lib/lcp_ruby/i18n_check/reporter.rb +96 -0
- data/lib/lcp_ruby/i18n_check/runner.rb +46 -0
- data/lib/lcp_ruby/i18n_check.rb +15 -0
- data/lib/lcp_ruby/i18n_lint.rb +145 -0
- data/lib/lcp_ruby/import/auto_mapper.rb +98 -0
- data/lib/lcp_ruby/import/field_tree_builder.rb +178 -0
- data/lib/lcp_ruby/import/file_parser.rb +223 -0
- data/lib/lcp_ruby/import/import_dialog_handler.rb +410 -0
- data/lib/lcp_ruby/import/import_job_handler.rb +224 -0
- data/lib/lcp_ruby/import/row_processor.rb +281 -0
- data/lib/lcp_ruby/import/setup.rb +277 -0
- data/lib/lcp_ruby/import/value_coercer.rb +143 -0
- data/lib/lcp_ruby/import.rb +14 -0
- data/lib/lcp_ruby/json_item_wrapper.rb +152 -0
- data/lib/lcp_ruby/kanban/board.rb +28 -0
- data/lib/lcp_ruby/kanban/column.rb +28 -0
- data/lib/lcp_ruby/kanban/default_provider.rb +376 -0
- data/lib/lcp_ruby/kanban/host_provider.rb +94 -0
- data/lib/lcp_ruby/kanban/move_result.rb +52 -0
- data/lib/lcp_ruby/kanban/provider_test_harness.rb +54 -0
- data/lib/lcp_ruby/kanban/swimlane.rb +21 -0
- data/lib/lcp_ruby/menu.rb +46 -0
- data/lib/lcp_ruby/metadata/aggregate_definition.rb +6 -0
- data/lib/lcp_ruby/metadata/association_definition.rb +196 -0
- data/lib/lcp_ruby/metadata/auth_validator.rb +222 -0
- data/lib/lcp_ruby/metadata/configuration_validator.rb +7958 -0
- data/lib/lcp_ruby/metadata/contract_result.rb +9 -0
- data/lib/lcp_ruby/metadata/display_template_definition.rb +77 -0
- data/lib/lcp_ruby/metadata/enum_label_resolver.rb +27 -0
- data/lib/lcp_ruby/metadata/erd_generator.rb +274 -0
- data/lib/lcp_ruby/metadata/event_definition.rb +55 -0
- data/lib/lcp_ruby/metadata/field_definition.rb +267 -0
- data/lib/lcp_ruby/metadata/group_definition.rb +31 -0
- data/lib/lcp_ruby/metadata/i18n_label.rb +29 -0
- data/lib/lcp_ruby/metadata/loader.rb +916 -0
- data/lib/lcp_ruby/metadata/menu_definition.rb +116 -0
- data/lib/lcp_ruby/metadata/menu_item.rb +792 -0
- data/lib/lcp_ruby/metadata/menu_item_resolver.rb +105 -0
- data/lib/lcp_ruby/metadata/model_definition.rb +612 -0
- data/lib/lcp_ruby/metadata/model_hash_merger.rb +88 -0
- data/lib/lcp_ruby/metadata/model_inheritance_resolver.rb +165 -0
- data/lib/lcp_ruby/metadata/page_definition.rb +245 -0
- data/lib/lcp_ruby/metadata/path_template.rb +231 -0
- data/lib/lcp_ruby/metadata/permission_definition.rb +237 -0
- data/lib/lcp_ruby/metadata/permission_merger.rb +81 -0
- data/lib/lcp_ruby/metadata/presenter_definition.rb +689 -0
- data/lib/lcp_ruby/metadata/reserved_names.rb +79 -0
- data/lib/lcp_ruby/metadata/responsive_policy.rb +95 -0
- data/lib/lcp_ruby/metadata/schema_validator.rb +172 -0
- data/lib/lcp_ruby/metadata/validation_definition.rb +69 -0
- data/lib/lcp_ruby/metadata/view_group_definition.rb +208 -0
- data/lib/lcp_ruby/metadata/virtual_column_definition.rb +154 -0
- data/lib/lcp_ruby/metadata/zone_definition.rb +423 -0
- data/lib/lcp_ruby/metrics/collector.rb +123 -0
- data/lib/lcp_ruby/metrics/collector_registry.rb +57 -0
- data/lib/lcp_ruby/metrics/error_recorder.rb +70 -0
- data/lib/lcp_ruby/metrics/fingerprint.rb +33 -0
- data/lib/lcp_ruby/metrics/json_query.rb +47 -0
- data/lib/lcp_ruby/metrics/metric_definitions.rb +105 -0
- data/lib/lcp_ruby/metrics/prometheus_check.rb +9 -0
- data/lib/lcp_ruby/metrics/rate_limiter.rb +99 -0
- data/lib/lcp_ruby/metrics/setup.rb +71 -0
- data/lib/lcp_ruby/metrics/subscriber.rb +126 -0
- data/lib/lcp_ruby/model_factory/aggregate_applicator.rb +14 -0
- data/lib/lcp_ruby/model_factory/api_association_applicator.rb +119 -0
- data/lib/lcp_ruby/model_factory/api_builder.rb +85 -0
- data/lib/lcp_ruby/model_factory/array_type.rb +78 -0
- data/lib/lcp_ruby/model_factory/array_type_applicator.rb +33 -0
- data/lib/lcp_ruby/model_factory/association_applicator.rb +201 -0
- data/lib/lcp_ruby/model_factory/attachment_applicator.rb +160 -0
- data/lib/lcp_ruby/model_factory/auditing_applicator.rb +72 -0
- data/lib/lcp_ruby/model_factory/builder.rb +235 -0
- data/lib/lcp_ruby/model_factory/callback_applicator.rb +63 -0
- data/lib/lcp_ruby/model_factory/computed_applicator.rb +55 -0
- data/lib/lcp_ruby/model_factory/default_applicator.rb +85 -0
- data/lib/lcp_ruby/model_factory/enum_applicator.rb +24 -0
- data/lib/lcp_ruby/model_factory/inherited_parent_validator_applicator.rb +39 -0
- data/lib/lcp_ruby/model_factory/label_method_builder.rb +82 -0
- data/lib/lcp_ruby/model_factory/managed_tracking.rb +71 -0
- data/lib/lcp_ruby/model_factory/positioning_applicator.rb +23 -0
- data/lib/lcp_ruby/model_factory/ransack_applicator.rb +66 -0
- data/lib/lcp_ruby/model_factory/registry.rb +33 -0
- data/lib/lcp_ruby/model_factory/schema_manager.rb +655 -0
- data/lib/lcp_ruby/model_factory/scope_applicator.rb +87 -0
- data/lib/lcp_ruby/model_factory/sequence_applicator.rb +173 -0
- data/lib/lcp_ruby/model_factory/service_accessor_applicator.rb +40 -0
- data/lib/lcp_ruby/model_factory/soft_delete_applicator.rb +141 -0
- data/lib/lcp_ruby/model_factory/transform_applicator.rb +51 -0
- data/lib/lcp_ruby/model_factory/tree_applicator.rb +239 -0
- data/lib/lcp_ruby/model_factory/userstamps_applicator.rb +73 -0
- data/lib/lcp_ruby/model_factory/validation_applicator.rb +319 -0
- data/lib/lcp_ruby/model_factory/virtual_column_applicator.rb +79 -0
- data/lib/lcp_ruby/model_factory/workflow_applicator.rb +141 -0
- data/lib/lcp_ruby/pages/change_handler.rb +15 -0
- data/lib/lcp_ruby/pages/contract_validator.rb +74 -0
- data/lib/lcp_ruby/pages/definition_validator.rb +42 -0
- data/lib/lcp_ruby/pages/filter_form.rb +200 -0
- data/lib/lcp_ruby/pages/filter_form_validator.rb +636 -0
- data/lib/lcp_ruby/pages/registry.rb +133 -0
- data/lib/lcp_ruby/pages/resolver.rb +32 -0
- data/lib/lcp_ruby/pages/scope_context_resolver.rb +37 -0
- data/lib/lcp_ruby/pages/scope_filter_set.rb +57 -0
- data/lib/lcp_ruby/pages/setup.rb +46 -0
- data/lib/lcp_ruby/path_utils.rb +12 -0
- data/lib/lcp_ruby/permissions/change_handler.rb +22 -0
- data/lib/lcp_ruby/permissions/contract_validator.rb +74 -0
- data/lib/lcp_ruby/permissions/definition_validator.rb +119 -0
- data/lib/lcp_ruby/permissions/registry.rb +135 -0
- data/lib/lcp_ruby/permissions/setup.rb +51 -0
- data/lib/lcp_ruby/permissions/source_resolver.rb +56 -0
- data/lib/lcp_ruby/presenter/action_set.rb +236 -0
- data/lib/lcp_ruby/presenter/breadcrumb_builder.rb +183 -0
- data/lib/lcp_ruby/presenter/breadcrumb_path_helper.rb +17 -0
- data/lib/lcp_ruby/presenter/column_set.rb +268 -0
- data/lib/lcp_ruby/presenter/enrichment.rb +136 -0
- data/lib/lcp_ruby/presenter/field_value_resolver.rb +237 -0
- data/lib/lcp_ruby/presenter/includes_resolver/association_dependency.rb +59 -0
- data/lib/lcp_ruby/presenter/includes_resolver/dependency_collector.rb +394 -0
- data/lib/lcp_ruby/presenter/includes_resolver/loading_strategy.rb +70 -0
- data/lib/lcp_ruby/presenter/includes_resolver/strategy_resolver.rb +123 -0
- data/lib/lcp_ruby/presenter/includes_resolver.rb +42 -0
- data/lib/lcp_ruby/presenter/layout_builder.rb +467 -0
- data/lib/lcp_ruby/presenter/link_resolver.rb +65 -0
- data/lib/lcp_ruby/presenter/metadata_lookup.rb +28 -0
- data/lib/lcp_ruby/presenter/resolver.rb +25 -0
- data/lib/lcp_ruby/record_aliases/metadata_checker.rb +213 -0
- data/lib/lcp_ruby/record_aliases/setup.rb +212 -0
- data/lib/lcp_ruby/reserved_route_segments.rb +37 -0
- data/lib/lcp_ruby/roles/change_handler.rb +11 -0
- data/lib/lcp_ruby/roles/contract_validator.rb +67 -0
- data/lib/lcp_ruby/roles/registry.rb +89 -0
- data/lib/lcp_ruby/roles/setup.rb +50 -0
- data/lib/lcp_ruby/routing/presenter_routes.rb +104 -0
- data/lib/lcp_ruby/saved_filters/change_handler.rb +13 -0
- data/lib/lcp_ruby/saved_filters/contract_validator.rb +85 -0
- data/lib/lcp_ruby/saved_filters/registry.rb +36 -0
- data/lib/lcp_ruby/saved_filters/resolver.rb +108 -0
- data/lib/lcp_ruby/saved_filters/setup.rb +42 -0
- data/lib/lcp_ruby/saved_filters/stale_field_validator.rb +84 -0
- data/lib/lcp_ruby/schemas/auth.json +208 -0
- data/lib/lcp_ruby/schemas/menu.json +338 -0
- data/lib/lcp_ruby/schemas/model.json +1161 -0
- data/lib/lcp_ruby/schemas/page.json +877 -0
- data/lib/lcp_ruby/schemas/permission.json +454 -0
- data/lib/lcp_ruby/schemas/presenter.json +2274 -0
- data/lib/lcp_ruby/schemas/theme.json +62 -0
- data/lib/lcp_ruby/schemas/type.json +146 -0
- data/lib/lcp_ruby/schemas/view_group.json +163 -0
- data/lib/lcp_ruby/search/custom_field_filter.rb +171 -0
- data/lib/lcp_ruby/search/custom_filter_interceptor.rb +40 -0
- data/lib/lcp_ruby/search/filter_metadata_builder.rb +409 -0
- data/lib/lcp_ruby/search/filter_param_builder.rb +177 -0
- data/lib/lcp_ruby/search/operator_registry.rb +79 -0
- data/lib/lcp_ruby/search/param_sanitizer.rb +25 -0
- data/lib/lcp_ruby/search/parameter_definition.rb +187 -0
- data/lib/lcp_ruby/search/parameterized_scope_applicator.rb +129 -0
- data/lib/lcp_ruby/search/query_builder.rb +143 -0
- data/lib/lcp_ruby/search/query_language_parser.rb +549 -0
- data/lib/lcp_ruby/search/query_language_serializer.rb +193 -0
- data/lib/lcp_ruby/search/quick_search.rb +162 -0
- data/lib/lcp_ruby/search/relative_date_expander.rb +57 -0
- data/lib/lcp_ruby/search_result.rb +70 -0
- data/lib/lcp_ruby/sequences/sequence_manager.rb +51 -0
- data/lib/lcp_ruby/services/accessors/json_field.rb +23 -0
- data/lib/lcp_ruby/services/built_in_accessors.rb +17 -0
- data/lib/lcp_ruby/services/built_in_defaults.rb +22 -0
- data/lib/lcp_ruby/services/built_in_transforms.rb +20 -0
- data/lib/lcp_ruby/services/checker.rb +133 -0
- data/lib/lcp_ruby/services/registry.rb +83 -0
- data/lib/lcp_ruby/skills_installer.rb +73 -0
- data/lib/lcp_ruby/sort/enum_sort_order.rb +38 -0
- data/lib/lcp_ruby/tasks/destroy_order_resolver.rb +57 -0
- data/lib/lcp_ruby/tasks/doctor.rb +294 -0
- data/lib/lcp_ruby/tasks/permission_resolve_formatter.rb +245 -0
- data/lib/lcp_ruby/types/built_in_types.rb +157 -0
- data/lib/lcp_ruby/types/transforms/base_transform.rb +11 -0
- data/lib/lcp_ruby/types/transforms/downcase.rb +11 -0
- data/lib/lcp_ruby/types/transforms/normalize_phone.rb +19 -0
- data/lib/lcp_ruby/types/transforms/normalize_url.rb +16 -0
- data/lib/lcp_ruby/types/transforms/strip.rb +11 -0
- data/lib/lcp_ruby/types/type_definition.rb +112 -0
- data/lib/lcp_ruby/types/type_registry.rb +75 -0
- data/lib/lcp_ruby/url_safety.rb +97 -0
- data/lib/lcp_ruby/user_snapshot.rb +15 -0
- data/lib/lcp_ruby/version.rb +3 -0
- data/lib/lcp_ruby/view_slots/registry.rb +71 -0
- data/lib/lcp_ruby/view_slots/slot_component.rb +22 -0
- data/lib/lcp_ruby/view_slots/slot_context.rb +20 -0
- data/lib/lcp_ruby/virtual_columns/builder.rb +234 -0
- data/lib/lcp_ruby/virtual_columns/collector.rb +186 -0
- data/lib/lcp_ruby/virtual_columns.rb +4 -0
- data/lib/lcp_ruby/virtual_fields/synthetic_marker.rb +17 -0
- data/lib/lcp_ruby/virtual_fields/types/array_of.rb +49 -0
- data/lib/lcp_ruby/virtual_fields/virtual_field.rb +107 -0
- data/lib/lcp_ruby/virtual_fields/virtual_form.rb +144 -0
- data/lib/lcp_ruby/widgets/chart_palette.rb +25 -0
- data/lib/lcp_ruby/widgets/chartkick_check.rb +9 -0
- data/lib/lcp_ruby/widgets/data_resolver.rb +676 -0
- data/lib/lcp_ruby/widgets/date_grouper.rb +54 -0
- data/lib/lcp_ruby/widgets/presenter_zone_resolver.rb +170 -0
- data/lib/lcp_ruby/widgets/record_source_resolver.rb +56 -0
- data/lib/lcp_ruby/widgets/scope_applicator.rb +187 -0
- data/lib/lcp_ruby/workflow/approval/activation_handler.rb +39 -0
- data/lib/lcp_ruby/workflow/approval/approval_definition.rb +117 -0
- data/lib/lcp_ruby/workflow/approval/approver_resolver.rb +98 -0
- data/lib/lcp_ruby/workflow/approval/cleanup_handler.rb +37 -0
- data/lib/lcp_ruby/workflow/approval/contract_validator.rb +96 -0
- data/lib/lcp_ruby/workflow/approval/data_builder.rb +53 -0
- data/lib/lcp_ruby/workflow/approval/discard_handler.rb +51 -0
- data/lib/lcp_ruby/workflow/approval/engine.rb +314 -0
- data/lib/lcp_ruby/workflow/approval/registry.rb +40 -0
- data/lib/lcp_ruby/workflow/approval/resolution_handler.rb +103 -0
- data/lib/lcp_ruby/workflow/approval/setup.rb +138 -0
- data/lib/lcp_ruby/workflow/approval/step_definition.rb +52 -0
- data/lib/lcp_ruby/workflow/approval/step_evaluator.rb +163 -0
- data/lib/lcp_ruby/workflow/approval/system_evaluator.rb +29 -0
- data/lib/lcp_ruby/workflow/approval/task_manager.rb +202 -0
- data/lib/lcp_ruby/workflow/audit_contract_validator.rb +64 -0
- data/lib/lcp_ruby/workflow/audit_registry.rb +24 -0
- data/lib/lcp_ruby/workflow/audit_writer.rb +51 -0
- data/lib/lcp_ruby/workflow/change_handler.rb +14 -0
- data/lib/lcp_ruby/workflow/contract.rb +21 -0
- data/lib/lcp_ruby/workflow/contract_validator.rb +44 -0
- data/lib/lcp_ruby/workflow/errors.rb +12 -0
- data/lib/lcp_ruby/workflow/host_source.rb +19 -0
- data/lib/lcp_ruby/workflow/mermaid_builder.rb +217 -0
- data/lib/lcp_ruby/workflow/model_source.rb +79 -0
- data/lib/lcp_ruby/workflow/registry.rb +113 -0
- data/lib/lcp_ruby/workflow/resolver.rb +32 -0
- data/lib/lcp_ruby/workflow/setup.rb +135 -0
- data/lib/lcp_ruby/workflow/state_definition.rb +59 -0
- data/lib/lcp_ruby/workflow/state_machine.rb +78 -0
- data/lib/lcp_ruby/workflow/static_source.rb +20 -0
- data/lib/lcp_ruby/workflow/transition_action_builder.rb +46 -0
- data/lib/lcp_ruby/workflow/transition_definition.rb +70 -0
- data/lib/lcp_ruby/workflow/transition_executor.rb +140 -0
- data/lib/lcp_ruby/workflow/transition_label_resolver.rb +21 -0
- data/lib/lcp_ruby/workflow/transition_result.rb +20 -0
- data/lib/lcp_ruby/workflow/value_resolver.rb +58 -0
- data/lib/lcp_ruby/workflow/workflow_definition.rb +195 -0
- data/lib/lcp_ruby.rb +764 -0
- data/lib/rubocop/cop/lcp_ruby/no_hardcoded_i18n_string.rb +249 -0
- data/lib/tasks/lcp_ruby.rake +432 -0
- data/lib/tasks/lcp_ruby_assets.rake +37 -0
- data/lib/tasks/lcp_ruby_auth.rake +49 -0
- data/lib/tasks/lcp_ruby_db.rake +76 -0
- data/lib/tasks/lcp_ruby_doctor.rake +20 -0
- data/lib/tasks/lcp_ruby_feature_catalog.rake +61 -0
- data/lib/tasks/lcp_ruby_gapfree_sequences.rake +39 -0
- data/lib/tasks/lcp_ruby_i18n_check.rake +23 -0
- data/lib/tasks/lcp_ruby_i18n_lint.rake +20 -0
- data/lib/tasks/lcp_ruby_invariant_check.rake +72 -0
- data/vendor/assets/javascripts/lcp_ruby/activestorage.min.js +866 -0
- data/vendor/assets/javascripts/lcp_ruby/highlight.min.js +1244 -0
- data/vendor/assets/javascripts/lcp_ruby/lucide.min.js +12 -0
- data/vendor/assets/javascripts/lcp_ruby/stimulus.umd.js +2588 -0
- data/vendor/assets/javascripts/lcp_ruby/tom-select.complete.min.js +444 -0
- data/vendor/assets/stylesheets/lcp_ruby/highlight-github.min.css +12 -0
- data/vendor/assets/stylesheets/lcp_ruby/tom-select.css +412 -0
- metadata +1950 -0
|
@@ -0,0 +1,2972 @@
|
|
|
1
|
+
# Models Reference
|
|
2
|
+
|
|
3
|
+
File: `config/lcp_ruby/models/<name>.yml`
|
|
4
|
+
|
|
5
|
+
Model YAML defines the data layer: database columns, validations, associations, scopes, and events. The engine reads these files at boot and dynamically creates ActiveRecord models under the `LcpRuby::Dynamic::` namespace.
|
|
6
|
+
|
|
7
|
+
## Top-Level Attributes
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
model:
|
|
11
|
+
name: <model_name>
|
|
12
|
+
label: "Display Name"
|
|
13
|
+
label_plural: "Display Names"
|
|
14
|
+
table_name: <custom_table_name>
|
|
15
|
+
slug_field: <field_name>
|
|
16
|
+
fields: []
|
|
17
|
+
validations: []
|
|
18
|
+
associations: []
|
|
19
|
+
scopes: []
|
|
20
|
+
events: []
|
|
21
|
+
display_templates: {}
|
|
22
|
+
virtual_columns: {} # computed query-time columns (expressions, JOINs, aggregates)
|
|
23
|
+
aggregates: {} # legacy alias for virtual_columns, still supported
|
|
24
|
+
positioning: true | { field: position, scope: parent_id }
|
|
25
|
+
options: {}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### `name`
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
|---|---|
|
|
32
|
+
| **Required** | yes |
|
|
33
|
+
| **Type** | string (snake_case) |
|
|
34
|
+
|
|
35
|
+
Internal identifier for the model. Used to generate the AR class name (`LcpRuby::Dynamic::TodoItem` for `todo_item`), default table name, and cross-references from presenters and permissions.
|
|
36
|
+
|
|
37
|
+
### `label`
|
|
38
|
+
|
|
39
|
+
| | |
|
|
40
|
+
|---|---|
|
|
41
|
+
| **Required** | no |
|
|
42
|
+
| **Default** | `name.humanize` |
|
|
43
|
+
| **Type** | string |
|
|
44
|
+
|
|
45
|
+
Human-readable singular name displayed in the UI (page titles, flash messages).
|
|
46
|
+
|
|
47
|
+
### `label_plural`
|
|
48
|
+
|
|
49
|
+
| | |
|
|
50
|
+
|---|---|
|
|
51
|
+
| **Required** | no |
|
|
52
|
+
| **Default** | `label.pluralize` |
|
|
53
|
+
| **Type** | string |
|
|
54
|
+
|
|
55
|
+
Plural form of the label. Used in index page headings and navigation.
|
|
56
|
+
|
|
57
|
+
### `abstract`
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| **Required** | no |
|
|
62
|
+
| **Default** | `false` |
|
|
63
|
+
| **Type** | boolean |
|
|
64
|
+
|
|
65
|
+
When `true`, this model serves as a reusable template. Abstract models produce no AR class or DB table — their fields, associations, scopes, events, validations, options, virtual_columns, display_templates, and indexes are merged into child models that reference them via `inherits`. Abstract models must not define `table_name` or `options.sti`.
|
|
66
|
+
|
|
67
|
+
**Convention:** Abstract model names start with `_` (underscore) to signal "not instantiable." This is a convention, not enforced — the `abstract: true` key is the authoritative flag.
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
model:
|
|
71
|
+
name: _auditable
|
|
72
|
+
abstract: true
|
|
73
|
+
fields:
|
|
74
|
+
- name: created_by
|
|
75
|
+
type: string
|
|
76
|
+
- name: updated_by
|
|
77
|
+
type: string
|
|
78
|
+
options:
|
|
79
|
+
userstamps: true
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `inherits`
|
|
83
|
+
|
|
84
|
+
| | |
|
|
85
|
+
|---|---|
|
|
86
|
+
| **Required** | no |
|
|
87
|
+
| **Type** | string |
|
|
88
|
+
|
|
89
|
+
Name of a parent model whose fields, associations, scopes, events, validations, options, virtual_columns, display_templates, and indexes are merged into this model. The parent model must be either `abstract: true` or have `options.sti: true`.
|
|
90
|
+
|
|
91
|
+
**Two inheritance modes:**
|
|
92
|
+
|
|
93
|
+
1. **Abstract inheritance** — parent has `abstract: true`. Merged at metadata level; parent produces no AR class or DB table.
|
|
94
|
+
2. **STI inheritance** — parent has `options.sti: true`. Child AR class inherits from parent AR class and shares the parent's table via [Single Table Inheritance](#single-table-inheritance-sti).
|
|
95
|
+
|
|
96
|
+
**Merge rules:**
|
|
97
|
+
|
|
98
|
+
| Section | Behavior |
|
|
99
|
+
|---------|----------|
|
|
100
|
+
| `fields` | Parent fields first, then child fields appended. Child field with same name **overrides** parent field entirely. |
|
|
101
|
+
| `associations` | Parent first, child appended. Same-name override. |
|
|
102
|
+
| `scopes` | Parent first, child appended. Same-name override. |
|
|
103
|
+
| `events` | Parent first, child appended. Same-name override. |
|
|
104
|
+
| `validations` | Additive — parent validations concatenated with child. No override. |
|
|
105
|
+
| `options` | Deep merge — child options override parent where keys collide. |
|
|
106
|
+
| `virtual_columns` | Merge by key — child overrides parent on collision. |
|
|
107
|
+
| `display_templates` | Merge by key — child overrides parent on collision. |
|
|
108
|
+
| `indexes` | Concatenated (all inherited + all child). |
|
|
109
|
+
| `positioning` | Child wins if present, otherwise inherited. |
|
|
110
|
+
| `data_source` | Child wins if present, otherwise inherited. |
|
|
111
|
+
| `name`, `table_name`, `label`, `label_plural` | Never inherited — always the child's own identity. |
|
|
112
|
+
|
|
113
|
+
**Chaining:** A model can inherit from a parent that itself inherits from another abstract model. An abstract → STI chain (abstract parent → STI parent → STI child) is supported. Circular inheritance is detected and raises `MetadataError` at load time.
|
|
114
|
+
|
|
115
|
+
**Cross-format:** YAML models can inherit from DSL-defined abstract models and vice versa.
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
model:
|
|
119
|
+
name: ticket
|
|
120
|
+
inherits: _auditable
|
|
121
|
+
fields:
|
|
122
|
+
- name: title
|
|
123
|
+
type: string
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `table_name`
|
|
127
|
+
|
|
128
|
+
| | |
|
|
129
|
+
|---|---|
|
|
130
|
+
| **Required** | no |
|
|
131
|
+
| **Default** | `name.pluralize` |
|
|
132
|
+
| **Type** | string |
|
|
133
|
+
|
|
134
|
+
Override the database table name. Use this when integrating with an existing database that has a non-standard table naming convention.
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
model:
|
|
138
|
+
name: todo_item
|
|
139
|
+
table_name: legacy_todo_items
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Virtual Models
|
|
143
|
+
|
|
144
|
+
Set `table_name: _virtual` to create a metadata-only model — no database table and no ActiveRecord class are created. Virtual models exist purely for field metadata (types, labels, validations, transforms, defaults) and are used as item definitions for JSON field nested editing.
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
model:
|
|
148
|
+
name: address
|
|
149
|
+
table_name: _virtual
|
|
150
|
+
fields:
|
|
151
|
+
- name: street
|
|
152
|
+
type: string
|
|
153
|
+
validations: [{ type: presence }]
|
|
154
|
+
- name: city
|
|
155
|
+
type: string
|
|
156
|
+
validations: [{ type: presence }]
|
|
157
|
+
- name: zip
|
|
158
|
+
type: string
|
|
159
|
+
- name: country
|
|
160
|
+
type: string
|
|
161
|
+
default: "CZ"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Virtual models:
|
|
165
|
+
- Are loaded by `Metadata::Loader` and available via `LcpRuby.loader.model_definitions`
|
|
166
|
+
- Are **not** registered in `LcpRuby.registry` (no AR class to register)
|
|
167
|
+
- Are **not** built by `ModelFactory::Builder` — `SchemaManager.ensure_table!` skips them
|
|
168
|
+
- Should not have associations or scopes (these produce warnings in `ConfigurationValidator`)
|
|
169
|
+
- Are referenced from presenter `nested_fields` sections via `target_model:` — see [JSON Field with Target Model](presenters.md#json-field-source-model-backed)
|
|
170
|
+
|
|
171
|
+
When a `json_field:` + `target_model:` nested section is rendered, each hash item from the JSON array is wrapped in a `JsonItemWrapper` that uses the virtual model's field definitions for type coercion, getter/setter access, and per-item validation.
|
|
172
|
+
|
|
173
|
+
#### API-Backed Models
|
|
174
|
+
|
|
175
|
+
Add a `data_source` key to create a model backed by an external REST API or host-provided adapter instead of the database:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
model:
|
|
179
|
+
name: external_building
|
|
180
|
+
data_source:
|
|
181
|
+
type: rest_json # rest_json | host
|
|
182
|
+
base_url: "https://api.example.com"
|
|
183
|
+
resource: "/buildings"
|
|
184
|
+
auth:
|
|
185
|
+
type: bearer
|
|
186
|
+
token_env: "API_TOKEN"
|
|
187
|
+
cache:
|
|
188
|
+
enabled: true
|
|
189
|
+
ttl: 300
|
|
190
|
+
fields:
|
|
191
|
+
- name: name
|
|
192
|
+
type: string
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
API-backed models use `ActiveModel` instead of `ActiveRecord`. They support index and show views, cross-source associations (DB model → API model), permissions, and display renderers. Features requiring database access (soft_delete, auditing, userstamps, tree, positioning, custom_fields) are not available.
|
|
196
|
+
|
|
197
|
+
See [API-Backed Models Reference](api-backed-models.md) and [API-Backed Models Guide](../guides/api-backed-models.md) for full documentation.
|
|
198
|
+
|
|
199
|
+
### `positioning`
|
|
200
|
+
|
|
201
|
+
| | |
|
|
202
|
+
|---|---|
|
|
203
|
+
| **Required** | no |
|
|
204
|
+
| **Type** | boolean or hash |
|
|
205
|
+
|
|
206
|
+
Enables automatic record positioning via the [`positioning`](https://github.com/brendon/positioning) gem. When present, records get automatic sequential position assignment on create, gap closing on destroy, and atomic reorder support.
|
|
207
|
+
|
|
208
|
+
**Minimal form** — uses `position` field with no scope:
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
positioning: true
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Hash form** — custom field and/or scoped positioning:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
positioning:
|
|
218
|
+
field: position # optional, default: "position"
|
|
219
|
+
scope: pipeline_id # optional, string or array of strings
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Multi-column scope:**
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
positioning:
|
|
226
|
+
scope: [pipeline_id, category]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
When `positioning` is set:
|
|
230
|
+
- The `positioned` gem macro is applied to the dynamic model
|
|
231
|
+
- The position column gets a `NOT NULL` constraint
|
|
232
|
+
- A unique index on `[scope_columns..., position_column]` is added (except on SQLite)
|
|
233
|
+
- The `positioning.field` must reference a declared field of type `integer`
|
|
234
|
+
- Each `positioning.scope` entry must be a declared field or a belongs_to FK
|
|
235
|
+
|
|
236
|
+
See [Record Positioning](../design/record_positioning.md) for the full design.
|
|
237
|
+
|
|
238
|
+
### `indexes`
|
|
239
|
+
|
|
240
|
+
| | |
|
|
241
|
+
|---|---|
|
|
242
|
+
| **Required** | no |
|
|
243
|
+
| **Default** | `[]` |
|
|
244
|
+
| **Type** | array of index objects |
|
|
245
|
+
|
|
246
|
+
Declares database indexes on the model's table. Each index specifies columns, optional uniqueness, and optional custom name.
|
|
247
|
+
|
|
248
|
+
```yaml
|
|
249
|
+
indexes:
|
|
250
|
+
- columns: [seq_model, seq_field, scope_key]
|
|
251
|
+
unique: true
|
|
252
|
+
- columns: [department_id, created_at]
|
|
253
|
+
- columns: [email]
|
|
254
|
+
unique: true
|
|
255
|
+
name: idx_users_email_uniq
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Key | Type | Default | Description |
|
|
259
|
+
|-----|------|---------|-------------|
|
|
260
|
+
| `columns` | array of strings | (required) | Column names to include in the index |
|
|
261
|
+
| `unique` | boolean | `false` | Create a unique constraint index |
|
|
262
|
+
| `name` | string | auto-generated | Custom index name |
|
|
263
|
+
|
|
264
|
+
Indexes are created at boot by `SchemaManager` (idempotent — existing indexes are skipped). All referenced columns must be defined fields on the model.
|
|
265
|
+
|
|
266
|
+
**DSL:**
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
define_model :gapfree_sequence do
|
|
270
|
+
field :seq_model, :string
|
|
271
|
+
field :seq_field, :string
|
|
272
|
+
field :scope_key, :string
|
|
273
|
+
index %i[seq_model seq_field scope_key], unique: true
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `slug_field`
|
|
278
|
+
|
|
279
|
+
| | |
|
|
280
|
+
|---|---|
|
|
281
|
+
| **Required** | no |
|
|
282
|
+
| **Default** | none |
|
|
283
|
+
| **Type** | string |
|
|
284
|
+
|
|
285
|
+
Name of a string field to use as URL slug. When set, the controller resolves non-numeric `params[:id]` via `find_by!(slug_field => param)` before falling back to numeric ID lookup. The dynamic model class also overrides `to_param` to return the slug value (with fallback to `id` when slug is nil/blank), so Rails URL helpers automatically generate slug-based URLs.
|
|
286
|
+
|
|
287
|
+
The field must be:
|
|
288
|
+
- A declared field on the model (not a virtual column)
|
|
289
|
+
- A string-compatible type (`string`, `email`, `url`, or a custom type resolving to a string column)
|
|
290
|
+
- Not an attachment field
|
|
291
|
+
|
|
292
|
+
The platform auto-adds an `exclusion` validation that prevents records from being saved with slug values that collide with reserved route segments (`new`, `edit`, `saved-filters`, etc.).
|
|
293
|
+
|
|
294
|
+
```yaml
|
|
295
|
+
model:
|
|
296
|
+
name: article
|
|
297
|
+
slug_field: slug
|
|
298
|
+
fields:
|
|
299
|
+
- { name: title, type: string }
|
|
300
|
+
- { name: slug, type: string }
|
|
301
|
+
validations:
|
|
302
|
+
- { field: slug, type: uniqueness }
|
|
303
|
+
- { field: slug, type: format, options: { with: "\\A[a-z0-9-]+\\z" } }
|
|
304
|
+
indexes:
|
|
305
|
+
- { columns: [slug], unique: true }
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Recommendations (validated as warnings at boot):**
|
|
309
|
+
|
|
310
|
+
- Add a `uniqueness` validation on the slug field — without it, `find_by!` returns non-deterministic results when duplicates exist.
|
|
311
|
+
- Use a format pattern that requires at least one non-numeric character — purely numeric slugs (e.g., `"123"`) are unreachable via URL because numeric params always resolve as record IDs.
|
|
312
|
+
- Use a lowercase-only format pattern — `find_by!` is case-sensitive on PostgreSQL, so `/articles/My-Slug` and `/articles/my-slug` would be different URLs.
|
|
313
|
+
- Avoid dots in slugs — Rails routing splits `params[:id]` on `.` into id and format (e.g., `/articles/v2.0` parses as `id: "v2", format: "0"`).
|
|
314
|
+
- Add a database index on the slug field — `find_by!` runs on every non-numeric request.
|
|
315
|
+
|
|
316
|
+
The recommended format pattern `\A[a-z0-9-]+\z` avoids all of the above pitfalls.
|
|
317
|
+
|
|
318
|
+
**STI note:** `slug_field` is not inherited by STI child models. Each child that needs slug resolution must declare its own `slug_field`. The `to_param` override is inherited via Ruby class hierarchy (so outbound links work), but the inbound resolve chain checks the child's own `ModelDefinition`.
|
|
319
|
+
|
|
320
|
+
**DSL:**
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
define_model :article do
|
|
324
|
+
slug_field :slug
|
|
325
|
+
|
|
326
|
+
field :slug, :string
|
|
327
|
+
field :title, :string
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
See [Record Aliases Guide](../guides/record-aliases.md) for full examples.
|
|
332
|
+
|
|
333
|
+
## Fields
|
|
334
|
+
|
|
335
|
+
Each field defines a database column and its metadata.
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
fields:
|
|
339
|
+
- name: title
|
|
340
|
+
type: string
|
|
341
|
+
label: "Title"
|
|
342
|
+
default: "Untitled"
|
|
343
|
+
column_options:
|
|
344
|
+
limit: 255
|
|
345
|
+
"null": false
|
|
346
|
+
enum_values: []
|
|
347
|
+
validations: []
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Field Attributes
|
|
351
|
+
|
|
352
|
+
#### `name`
|
|
353
|
+
|
|
354
|
+
| | |
|
|
355
|
+
|---|---|
|
|
356
|
+
| **Required** | yes |
|
|
357
|
+
| **Type** | string |
|
|
358
|
+
|
|
359
|
+
Column name. Must be unique within the model. Used as the ActiveRecord attribute name.
|
|
360
|
+
|
|
361
|
+
**Reserved names:** Some field names conflict with ActiveRecord/ActiveModel internals and must be avoided. Using a reserved name causes `ActiveModel::DangerousAttributeError` at runtime or silently overrides framework methods.
|
|
362
|
+
|
|
363
|
+
Names that **must not** be used as field names:
|
|
364
|
+
|
|
365
|
+
| Category | Names |
|
|
366
|
+
|----------|-------|
|
|
367
|
+
| Identity / lifecycle | `id`, `type`, `class`, `hash`, `object_id` |
|
|
368
|
+
| Persistence | `save`, `save!`, `destroy`, `destroy!`, `delete`, `update`, `update!`, `create` |
|
|
369
|
+
| State | `new_record?`, `persisted?`, `frozen?`, `valid?`, `invalid?`, `errors`, `changed?`, `changes` |
|
|
370
|
+
| Introspection | `model_name`, `respond_to?`, `send`, `inspect`, `to_s`, `to_param`, `to_model` |
|
|
371
|
+
| Attributes | `reload`, `assign_attributes`, `becomes`, `attributes`, `read_attribute`, `write_attribute`, `attribute_names`, `serializable_hash` |
|
|
372
|
+
| Object | `dup`, `clone`, `freeze` |
|
|
373
|
+
|
|
374
|
+
Additionally, these names are reserved by the platform: `custom_data` (used by custom fields), `discarded_at` (used by soft delete), `position` (used by positioning).
|
|
375
|
+
|
|
376
|
+
> **Tip:** If you need a field conceptually named after a reserved word, add a prefix or suffix — e.g., use `ai_model` instead of `model_name`, or `record_type` instead of `type` (unless you intend STI).
|
|
377
|
+
|
|
378
|
+
#### `type`
|
|
379
|
+
|
|
380
|
+
| | |
|
|
381
|
+
|---|---|
|
|
382
|
+
| **Required** | yes |
|
|
383
|
+
| **Type** | string (base type or registered business type) |
|
|
384
|
+
|
|
385
|
+
Determines the database column type and default form input behavior. Accepts one of the 14 base types below, or any registered [business type](types.md) name (e.g., `email`, `phone`, `url`, `color`).
|
|
386
|
+
|
|
387
|
+
**Base types:**
|
|
388
|
+
|
|
389
|
+
| Type | DB Column | Description |
|
|
390
|
+
|------|-----------|-------------|
|
|
391
|
+
| `string` | `:string` | Short text (varchar). Default form input: text field. |
|
|
392
|
+
| `text` | `:text` | Long text. Default form input: textarea. |
|
|
393
|
+
| `integer` | `:integer` | Whole number. |
|
|
394
|
+
| `float` | `:float` | Floating-point number. |
|
|
395
|
+
| `decimal` | `:decimal` | Precise decimal. Use `column_options: { precision:, scale: }`. |
|
|
396
|
+
| `boolean` | `:boolean` | True/false. Default form input: checkbox. |
|
|
397
|
+
| `date` | `:date` | Date without time. |
|
|
398
|
+
| `datetime` | `:datetime` | Date and time. |
|
|
399
|
+
| `enum` | `:string` | Stored as string. Requires `enum_values`. Default form input: select. |
|
|
400
|
+
| `file` | `:string` | File path/reference stored as string. |
|
|
401
|
+
| `rich_text` | `:text` | Rich text content. Default form input: WYSIWYG editor (Trix). Requires Action Text — `lcp new` installs it by default; if you used `--skip-action-text` or added LCP to an existing app without Action Text, the field silently renders as a plain textarea. The boot logger prints a warning naming each affected field with recovery steps (`bin/rails action_text:install` → `db:migrate` → restart). |
|
|
402
|
+
| `json` | `:jsonb` / `:json` | JSON data. Uses jsonb on PostgreSQL, json on other adapters. |
|
|
403
|
+
| `uuid` | `:string` | UUID stored as string. |
|
|
404
|
+
| `array` | PG: native array / SQLite: `:json` | Typed array of scalars. Requires `item_type`. Default form input: tag chips. |
|
|
405
|
+
| `attachment` | none (uses Active Storage) | File attachment (single or multiple). Requires Active Storage. |
|
|
406
|
+
|
|
407
|
+
**Built-in business types:**
|
|
408
|
+
|
|
409
|
+
| Type | Base | Transforms | Input | Description |
|
|
410
|
+
|------|------|------------|-------|-------------|
|
|
411
|
+
| `email` | string | strip, downcase | `<input type="email">` | Email with format validation |
|
|
412
|
+
| `phone` | string | strip, normalize_phone | `<input type="tel">` | Phone with format validation |
|
|
413
|
+
| `url` | string | strip, normalize_url | `<input type="url">` | URL with format validation, auto-prepends `https://` |
|
|
414
|
+
| `color` | string | strip, downcase | `<input type="color">` | Hex color (`#rrggbb`) with format validation |
|
|
415
|
+
| `bigint` | integer | — | `<input type="number">` | 8-byte integer — use for FK columns that reference Rails' default `bigint` PK to avoid silent `INT`/`BIGINT` join mismatches. |
|
|
416
|
+
|
|
417
|
+
Business types bundle transforms (normalization), validations, HTML input hints, and column options into a reusable definition. See [Types Reference](types.md) for defining custom types.
|
|
418
|
+
|
|
419
|
+
##### Attachment Fields
|
|
420
|
+
|
|
421
|
+
The `attachment` type uses Active Storage instead of a database column. It supports both single-file (`has_one_attached`) and multi-file (`has_many_attached`) attachments.
|
|
422
|
+
|
|
423
|
+
**Options:**
|
|
424
|
+
|
|
425
|
+
| Option | Type | Default | Description |
|
|
426
|
+
|--------|------|---------|-------------|
|
|
427
|
+
| `multiple` | boolean | `false` | Use `has_many_attached` instead of `has_one_attached` |
|
|
428
|
+
| `accept` | string | none | HTML `accept` attribute hint for file input (e.g., `"image/*"`). Not validated server-side — use `content_types` for validation. |
|
|
429
|
+
| `max_size` | string | global default | Maximum file size per file (e.g., `"10MB"`, `"512KB"`) |
|
|
430
|
+
| `min_size` | string | none | Minimum file size per file |
|
|
431
|
+
| `content_types` | array | global default | Allowed MIME types. Supports wildcards (e.g., `"image/*"`) |
|
|
432
|
+
| `max_files` | integer | none | Maximum number of files (only for `multiple: true`) |
|
|
433
|
+
| `variants` | hash | none | Named image variant configurations |
|
|
434
|
+
|
|
435
|
+
```yaml
|
|
436
|
+
fields:
|
|
437
|
+
# Single image attachment with variants
|
|
438
|
+
- name: photo
|
|
439
|
+
type: attachment
|
|
440
|
+
label: "Photo"
|
|
441
|
+
options:
|
|
442
|
+
accept: "image/*"
|
|
443
|
+
max_size: 5MB
|
|
444
|
+
content_types: ["image/jpeg", "image/png", "image/webp"]
|
|
445
|
+
variants:
|
|
446
|
+
thumbnail: { resize_to_limit: [100, 100] }
|
|
447
|
+
medium: { resize_to_limit: [300, 300] }
|
|
448
|
+
|
|
449
|
+
# Multiple file attachment
|
|
450
|
+
- name: files
|
|
451
|
+
type: attachment
|
|
452
|
+
label: "Documents"
|
|
453
|
+
options:
|
|
454
|
+
multiple: true
|
|
455
|
+
max_files: 10
|
|
456
|
+
max_size: 50MB
|
|
457
|
+
content_types: ["application/pdf", "image/*"]
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Attachment fields require Active Storage to be set up in the host application. See the [Attachments Guide](../guides/attachments.md) for prerequisites and complete examples.
|
|
461
|
+
|
|
462
|
+
##### Array Fields
|
|
463
|
+
|
|
464
|
+
The `array` type stores a list of scalar values. It requires an `item_type` to specify the element type.
|
|
465
|
+
|
|
466
|
+
**Supported item types:** `string`, `integer`, `float`
|
|
467
|
+
|
|
468
|
+
**Storage:** On PostgreSQL, uses native array columns (`text[]`, `integer[]`, `float[]`). On SQLite and other adapters, uses a JSON column with transparent serialization.
|
|
469
|
+
|
|
470
|
+
```yaml
|
|
471
|
+
fields:
|
|
472
|
+
# String array (e.g., tags)
|
|
473
|
+
- name: tags
|
|
474
|
+
type: array
|
|
475
|
+
item_type: string
|
|
476
|
+
default: []
|
|
477
|
+
|
|
478
|
+
# Integer array (e.g., scores)
|
|
479
|
+
- name: scores
|
|
480
|
+
type: array
|
|
481
|
+
item_type: integer
|
|
482
|
+
default: []
|
|
483
|
+
|
|
484
|
+
# Float array (e.g., measurements)
|
|
485
|
+
- name: measurements
|
|
486
|
+
type: array
|
|
487
|
+
item_type: float
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Auto-generated scopes:** Each array field automatically creates two scopes:
|
|
491
|
+
|
|
492
|
+
| Scope | Description | SQL (PostgreSQL) |
|
|
493
|
+
|-------|-------------|------------------|
|
|
494
|
+
| `with_<field>(values)` | Records whose array contains ALL given values | `@>` (array containment) |
|
|
495
|
+
| `with_any_<field>(values)` | Records whose array contains ANY of the given values | `&&` (array overlap) |
|
|
496
|
+
|
|
497
|
+
```ruby
|
|
498
|
+
# Find records tagged with both "ruby" AND "rails"
|
|
499
|
+
Article.with_tags(["ruby", "rails"])
|
|
500
|
+
|
|
501
|
+
# Find records tagged with "ruby" OR "python"
|
|
502
|
+
Article.with_any_tags(["ruby", "python"])
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Quick search:** Array fields with `item_type: string` are automatically included in quick search (`?qs=`). The search matches any element that contains the query text (case-insensitive on PostgreSQL, case-sensitive on SQLite).
|
|
506
|
+
|
|
507
|
+
**Presenter defaults:** Array fields automatically use the `array_input` form input (tag-style chips) and the `collection` renderer. These can be overridden in the presenter.
|
|
508
|
+
|
|
509
|
+
See also: [Array validations](#array_length), [Condition operators for arrays](condition-operators.md#array-operators).
|
|
510
|
+
|
|
511
|
+
#### `label`
|
|
512
|
+
|
|
513
|
+
| | |
|
|
514
|
+
|---|---|
|
|
515
|
+
| **Required** | no |
|
|
516
|
+
| **Default** | `name.humanize` |
|
|
517
|
+
| **Type** | string |
|
|
518
|
+
|
|
519
|
+
Display label for form inputs, table headers, and show fields.
|
|
520
|
+
|
|
521
|
+
#### `default`
|
|
522
|
+
|
|
523
|
+
| | |
|
|
524
|
+
|---|---|
|
|
525
|
+
| **Required** | no |
|
|
526
|
+
| **Default** | `nil` |
|
|
527
|
+
| **Type** | scalar, string (built-in key), or hash (service) |
|
|
528
|
+
|
|
529
|
+
Default value for new records. Supports exactly three forms — pick by the type of value you write:
|
|
530
|
+
|
|
531
|
+
| Form | YAML shape | When applied | Use for |
|
|
532
|
+
|------|-----------|--------------|---------|
|
|
533
|
+
| Scalar | `default: false` / `default: "medium"` / `default: 0` | DB column default at table creation | Static literal values |
|
|
534
|
+
| Built-in dynamic | `default: current_date` (string matching a built-in key) | Runtime `after_initialize` on new records | `current_date`, `current_datetime`, `current_year`, `current_user.id` |
|
|
535
|
+
| Service dynamic | `default: { service: "my_service" }` | Runtime `after_initialize` on new records | Custom logic registered in `app/lcp_services/defaults/` |
|
|
536
|
+
|
|
537
|
+
A bare string that does **not** match a built-in key is treated as a literal scalar (e.g. `default: "draft"`). The built-in dynamic keys follow the [Dynamic References](dynamic-references.md) grammar — boot-time validation catches typos against the configured `user_class`. For other dynamic computations expressible as field references see [`computed`](#computed) — distinct from `default` because it re-evaluates on every read.
|
|
538
|
+
|
|
539
|
+
**Scalar default** — applied as a database column default:
|
|
540
|
+
|
|
541
|
+
```yaml
|
|
542
|
+
- name: completed
|
|
543
|
+
type: boolean
|
|
544
|
+
default: false
|
|
545
|
+
|
|
546
|
+
- name: priority
|
|
547
|
+
type: string
|
|
548
|
+
default: "medium"
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Built-in dynamic default** — a string matching a built-in default key. Applied at runtime via `after_initialize` (only on new records, only when the field is blank):
|
|
552
|
+
|
|
553
|
+
| Key | Value | Description |
|
|
554
|
+
|-----|-------|-------------|
|
|
555
|
+
| `current_date` | `Date.today` | Today's date |
|
|
556
|
+
| `current_datetime` | `Time.current` | Current date and time |
|
|
557
|
+
| `current_user.id` | `LcpRuby::Current.user&.id` | Current user's ID |
|
|
558
|
+
|
|
559
|
+
```yaml
|
|
560
|
+
- name: start_date
|
|
561
|
+
type: date
|
|
562
|
+
default: current_date
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Service dynamic default** — delegates to a registered default service. Applied at runtime via `after_initialize`:
|
|
566
|
+
|
|
567
|
+
```yaml
|
|
568
|
+
- name: expected_close_date
|
|
569
|
+
type: date
|
|
570
|
+
default:
|
|
571
|
+
service: thirty_days_out
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
```ruby
|
|
575
|
+
# DSL
|
|
576
|
+
field :start_date, :date, default: "current_date"
|
|
577
|
+
field :expected_close_date, :date, default: { service: "thirty_days_out" }
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Defaults on service-accessor fields** — fields whose value lives behind an accessor service (most commonly `source: { service: json_field, ... }`) follow the same Rails-like default semantics as regular columns: the default is applied **on new-record creation** in the `after_initialize` callback, which calls the accessor's setter (e.g. `JsonField.set`) to write the value into the underlying store (JSON column) before `save`. Read-time fallback is NOT used — the accessor's getter returns the raw stored value.
|
|
581
|
+
|
|
582
|
+
```yaml
|
|
583
|
+
- name: theme
|
|
584
|
+
type: enum
|
|
585
|
+
default: "auto"
|
|
586
|
+
values: { light: "Light", dark: "Dark", auto: "System" }
|
|
587
|
+
source:
|
|
588
|
+
service: json_field
|
|
589
|
+
options: { column: profile_data, key: theme }
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
For `record = Preference.new`, `record.theme == "auto"` and after `record.save!` the column holds `profile_data == {"theme" => "auto"}`. All three default forms work identically to regular columns:
|
|
593
|
+
|
|
594
|
+
- literal (`default: "auto"`)
|
|
595
|
+
- built-in service key (`default: "current_date"`)
|
|
596
|
+
- service hash (`default: { service: "thirty_days_out" }`)
|
|
597
|
+
|
|
598
|
+
**Bind_to models** get the same behavior automatically — `DefaultApplicator` is in the `bind_to_auto_features` list when any field has a `default:`, so it registers the `after_initialize` callback on the host class. No `bind_to_apply:` declaration is needed.
|
|
599
|
+
|
|
600
|
+
**Existing rows:** If you add a `default:` to a field that already has data in the database, **existing rows with a missing JSON key return `nil`** — the default is not retroactively applied, mirroring how DB column defaults behave in Rails. To backfill old rows, write a one-shot Rails migration:
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
# db/migrate/20260521_backfill_theme_default.rb
|
|
604
|
+
class BackfillThemeDefault < ActiveRecord::Migration[7.1]
|
|
605
|
+
def up
|
|
606
|
+
LcpRuby::Dynamic::Preference.where("profile_data->>'theme' IS NULL").find_each do |p|
|
|
607
|
+
p.update!(theme: "auto")
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The accessor's setter writes the value into the JSON column like any normal update.
|
|
614
|
+
|
|
615
|
+
Service contract: `def self.call(record, field_name) -> value`. Register in `app/lcp_services/defaults/`. See [Extensibility Guide](../guides/extensibility.md).
|
|
616
|
+
|
|
617
|
+
#### `column_options`
|
|
618
|
+
|
|
619
|
+
| | |
|
|
620
|
+
|---|---|
|
|
621
|
+
| **Required** | no |
|
|
622
|
+
| **Default** | `{}` |
|
|
623
|
+
| **Type** | hash |
|
|
624
|
+
|
|
625
|
+
Options passed directly to the ActiveRecord migration column definition.
|
|
626
|
+
|
|
627
|
+
| Option | Applicable Types | Description |
|
|
628
|
+
|--------|-----------------|-------------|
|
|
629
|
+
| `limit` | string | Maximum character length |
|
|
630
|
+
| `precision` | decimal | Total number of digits |
|
|
631
|
+
| `scale` | decimal | Digits after decimal point |
|
|
632
|
+
| `null` | all | Allow NULL values (`true`/`false`) |
|
|
633
|
+
|
|
634
|
+
```yaml
|
|
635
|
+
column_options:
|
|
636
|
+
limit: 255
|
|
637
|
+
"null": false
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
> Note: Quote `null` as `"null"` in YAML to avoid it being parsed as a null value.
|
|
641
|
+
|
|
642
|
+
#### `enum_values`
|
|
643
|
+
|
|
644
|
+
| | |
|
|
645
|
+
|---|---|
|
|
646
|
+
| **Required** | yes (when type is `enum`) |
|
|
647
|
+
| **Type** | array of strings or hashes |
|
|
648
|
+
|
|
649
|
+
Defines the allowed values for an enum field. Two formats are supported:
|
|
650
|
+
|
|
651
|
+
**Simple format** — value and label are the same:
|
|
652
|
+
|
|
653
|
+
```yaml
|
|
654
|
+
enum_values: [draft, published, archived]
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Hash format** — separate internal value and display label:
|
|
658
|
+
|
|
659
|
+
```yaml
|
|
660
|
+
enum_values:
|
|
661
|
+
- { value: draft, label: "Draft" }
|
|
662
|
+
- { value: published, label: "Published" }
|
|
663
|
+
- { value: archived, label: "Archived" }
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Enum values are always stored and compared as strings internally. Even if you write `value: 0` (integer) in YAML, it is converted to `"0"` via `.to_s`.
|
|
667
|
+
|
|
668
|
+
**Display label resolution** (in badges, table cells, show pages, and form selects):
|
|
669
|
+
|
|
670
|
+
1. **i18n** — `I18n.t("lcp_ruby.enums.<model>.<field>.<value>")` (highest priority)
|
|
671
|
+
2. **YAML label** — the `label` key from the hash format above
|
|
672
|
+
3. **Humanize** — `value.humanize` as fallback
|
|
673
|
+
|
|
674
|
+
**Form select ordering**: by default, form `<select>` and radio inputs render enum values in **declaration order** (the order in `enum_values`). To sort alphabetically by label or reverse declaration order, set `input_options: { sort: alphabetical }` (or `reverse`) on the form field — see [Presenters reference — Select (enum)](presenters.md#input-options).
|
|
675
|
+
|
|
676
|
+
#### `item_type`
|
|
677
|
+
|
|
678
|
+
| | |
|
|
679
|
+
|---|---|
|
|
680
|
+
| **Required** | yes (when type is `array`) |
|
|
681
|
+
| **Type** | string |
|
|
682
|
+
|
|
683
|
+
Specifies the scalar type of array elements. Must be one of: `string`, `integer`, `float`.
|
|
684
|
+
|
|
685
|
+
```yaml
|
|
686
|
+
- name: tags
|
|
687
|
+
type: array
|
|
688
|
+
item_type: string
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
```ruby
|
|
692
|
+
# DSL
|
|
693
|
+
field :tags, :array, item_type: :string
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
#### `transforms`
|
|
697
|
+
|
|
698
|
+
| | |
|
|
699
|
+
|---|---|
|
|
700
|
+
| **Required** | no |
|
|
701
|
+
| **Default** | `[]` |
|
|
702
|
+
| **Type** | array of strings |
|
|
703
|
+
|
|
704
|
+
Field-level transforms applied via `ActiveRecord.normalizes`. Transforms run before validation on every assignment. Field-level transforms extend type-level transforms (defined via [custom types](types.md)) — the union is applied, with type-level transforms running first.
|
|
705
|
+
|
|
706
|
+
```yaml
|
|
707
|
+
# YAML
|
|
708
|
+
- name: title
|
|
709
|
+
type: string
|
|
710
|
+
transforms: [strip]
|
|
711
|
+
|
|
712
|
+
# With custom type that already has transforms
|
|
713
|
+
- name: email
|
|
714
|
+
type: email # email type has strip+downcase
|
|
715
|
+
transforms: [strip] # deduplicated, won't strip twice
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
# DSL
|
|
720
|
+
field :first_name, :string, transforms: [:strip, :titlecase]
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Built-in transforms: `strip`, `downcase`, `normalize_url`, `normalize_phone`. Custom transforms can be registered in `app/lcp_services/transforms/`. See [Extensibility Guide](../guides/extensibility.md).
|
|
724
|
+
|
|
725
|
+
#### `computed`
|
|
726
|
+
|
|
727
|
+
| | |
|
|
728
|
+
|---|---|
|
|
729
|
+
| **Required** | no |
|
|
730
|
+
| **Default** | `nil` |
|
|
731
|
+
| **Type** | string (template) or hash (service) |
|
|
732
|
+
|
|
733
|
+
Computed fields are automatically calculated before save. They are rendered as readonly in forms.
|
|
734
|
+
|
|
735
|
+
**Template syntax** — interpolates other field values:
|
|
736
|
+
|
|
737
|
+
```yaml
|
|
738
|
+
- name: full_name
|
|
739
|
+
type: string
|
|
740
|
+
computed: "{first_name} {last_name}"
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
The `{field}` micro-syntax (curly braces, dot-paths like `{company.name}`) is identical to the one used by [Display Templates](#display-templates) — the same parser handles both. Use `computed:` when the result is persisted on the record (read-write field, available to scopes and exports); use a display template when you only need it for rendering.
|
|
744
|
+
|
|
745
|
+
**Service syntax** — delegates to a registered computed service:
|
|
746
|
+
|
|
747
|
+
```yaml
|
|
748
|
+
- name: weighted_value
|
|
749
|
+
type: decimal
|
|
750
|
+
computed:
|
|
751
|
+
service: weighted_deal_value
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
```ruby
|
|
755
|
+
# DSL
|
|
756
|
+
field :full_name, :string, computed: "{first_name} {last_name}"
|
|
757
|
+
field :weighted_value, :decimal, computed: { service: "weighted_deal_value" }
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Service contract: `def self.call(record) -> value`. Register in `app/lcp_services/computed/`. See [Extensibility Guide](../guides/extensibility.md).
|
|
761
|
+
|
|
762
|
+
**Execution order:** Computed fields are recalculated in the order they appear in the `fields:` list. A computed field can read another computed field's value as long as the dependency is defined earlier. Circular dependencies are not detected — avoiding them is the implementor's responsibility.
|
|
763
|
+
|
|
764
|
+
#### `sequence`
|
|
765
|
+
|
|
766
|
+
| | |
|
|
767
|
+
|---|---|
|
|
768
|
+
| **Required** | no |
|
|
769
|
+
| **Default** | `nil` |
|
|
770
|
+
| **Type** | boolean (`true`) or hash |
|
|
771
|
+
|
|
772
|
+
Auto-numbering sequence field. Assigns a unique sequential value from a gap-free counter on record creation. Requires the `gapfree_sequence` model (run `rails generate lcp_ruby:gapfree_sequences`).
|
|
773
|
+
|
|
774
|
+
**Shorthand** — all defaults (global scope, raw counter, start 1, step 1, readonly):
|
|
775
|
+
|
|
776
|
+
```yaml
|
|
777
|
+
- name: seq
|
|
778
|
+
type: integer
|
|
779
|
+
sequence: true
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
**Full form:**
|
|
783
|
+
|
|
784
|
+
```yaml
|
|
785
|
+
- name: invoice_number
|
|
786
|
+
type: string
|
|
787
|
+
sequence:
|
|
788
|
+
scope: [_year]
|
|
789
|
+
format: "INV-%{_year}-%{sequence:04d}"
|
|
790
|
+
start: 1
|
|
791
|
+
step: 1
|
|
792
|
+
readonly: true
|
|
793
|
+
assign_on: create
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
| Option | Type | Default | Description |
|
|
797
|
+
|--------|------|---------|-------------|
|
|
798
|
+
| `scope` | array of strings | `[]` (global) | Scope columns or virtual keys (`_year`, `_month`, `_day`). Counter resets per unique scope combination. |
|
|
799
|
+
| `format` | string | none (raw integer) | Template with `%{sequence}`, `%{sequence:Nd}`, `%{_year}`, `%{_month}`, `%{_day}`, `%{field_name}`. |
|
|
800
|
+
| `start` | integer | `1` | Initial counter value for a new scope. |
|
|
801
|
+
| `step` | integer | `1` | Increment per record. |
|
|
802
|
+
| `readonly` | boolean | `true` | Render as disabled input in forms. |
|
|
803
|
+
| `assign_on` | string | `"create"` | `"create"` (before_create only) or `"always"` (also fills blank on update). |
|
|
804
|
+
|
|
805
|
+
```ruby
|
|
806
|
+
# DSL
|
|
807
|
+
field :invoice_number, :string,
|
|
808
|
+
sequence: { scope: [:_year], format: "INV-%{_year}-%{sequence:04d}" }
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
A field cannot have both `sequence` and `computed` (or `source`) — these are mutually exclusive. See [Sequence Fields Guide](../guides/sequences.md) for scoping, format templates, and counter management.
|
|
812
|
+
|
|
813
|
+
#### `validations`
|
|
814
|
+
|
|
815
|
+
| | |
|
|
816
|
+
|---|---|
|
|
817
|
+
| **Required** | no |
|
|
818
|
+
| **Default** | `[]` |
|
|
819
|
+
| **Type** | array of validation objects |
|
|
820
|
+
|
|
821
|
+
Field-level validations. See [Validations](#validations) below.
|
|
822
|
+
|
|
823
|
+
## Validations
|
|
824
|
+
|
|
825
|
+
Validations can be defined at the field level (inside a field's `validations` array) or at the model level (in the top-level `validations` array). Model-level validations support the `custom` and `service` types.
|
|
826
|
+
|
|
827
|
+
### Validation Types
|
|
828
|
+
|
|
829
|
+
Each validation has a `type` and optional `options` hash.
|
|
830
|
+
|
|
831
|
+
#### `presence`
|
|
832
|
+
|
|
833
|
+
Validates that the field is not blank.
|
|
834
|
+
|
|
835
|
+
```yaml
|
|
836
|
+
validations:
|
|
837
|
+
- type: presence
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
> **Not for boolean fields.** `false.present? == false`, so `validates :presence` on a `:boolean` field rejects valid `false` values (a Rails footgun). `bundle exec rake lcp_ruby:validate` reports this as an error and offers three resolutions:
|
|
841
|
+
>
|
|
842
|
+
> - Drop the validator and declare `null: false` on the column. Phase 4b auto-applies `inclusion: { in: [true, false] }` at runtime.
|
|
843
|
+
> - Replace with explicit `validates :inclusion, options: { in: [true, false] }`.
|
|
844
|
+
> - If the field has three meaningful states (yes / no / pending), switch from nullable boolean to `enum` — `null:` boolean tri-state is a data-modeling antipattern (standard checkboxes don't preserve `nil`, `WHERE flag = false` doesn't catch `NULL` rows).
|
|
845
|
+
|
|
846
|
+
With conditional options:
|
|
847
|
+
|
|
848
|
+
```yaml
|
|
849
|
+
validations:
|
|
850
|
+
- type: presence
|
|
851
|
+
options: { if: "requires_title?" }
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
#### `length`
|
|
855
|
+
|
|
856
|
+
Validates string length.
|
|
857
|
+
|
|
858
|
+
| Option | Type | Description |
|
|
859
|
+
|--------|------|-------------|
|
|
860
|
+
| `minimum` | integer | Minimum length |
|
|
861
|
+
| `maximum` | integer | Maximum length |
|
|
862
|
+
| `is` | integer | Exact length |
|
|
863
|
+
| `in` | range | Length range (e.g., `3..100`) |
|
|
864
|
+
|
|
865
|
+
```yaml
|
|
866
|
+
validations:
|
|
867
|
+
- type: length
|
|
868
|
+
options: { minimum: 3, maximum: 100 }
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
#### `numericality`
|
|
872
|
+
|
|
873
|
+
Validates numeric constraints.
|
|
874
|
+
|
|
875
|
+
| Option | Type | Description |
|
|
876
|
+
|--------|------|-------------|
|
|
877
|
+
| `greater_than` | number | Must be > value |
|
|
878
|
+
| `greater_than_or_equal_to` | number | Must be >= value |
|
|
879
|
+
| `less_than` | number | Must be < value |
|
|
880
|
+
| `less_than_or_equal_to` | number | Must be <= value |
|
|
881
|
+
| `equal_to` | number | Must be exactly value |
|
|
882
|
+
| `allow_nil` | boolean | Skip validation when nil |
|
|
883
|
+
|
|
884
|
+
```yaml
|
|
885
|
+
validations:
|
|
886
|
+
- type: numericality
|
|
887
|
+
options: { greater_than_or_equal_to: 0, allow_nil: true }
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
#### `format`
|
|
891
|
+
|
|
892
|
+
Validates against a regular expression.
|
|
893
|
+
|
|
894
|
+
| Option | Type | Description |
|
|
895
|
+
|--------|------|-------------|
|
|
896
|
+
| `with` | string | Regex pattern (converted to `Regexp` at runtime) |
|
|
897
|
+
|
|
898
|
+
```yaml
|
|
899
|
+
validations:
|
|
900
|
+
- type: format
|
|
901
|
+
options: { with: "\\A[a-zA-Z0-9]+\\z" }
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
#### `inclusion`
|
|
905
|
+
|
|
906
|
+
Validates that the value is in a given set.
|
|
907
|
+
|
|
908
|
+
| Option | Type | Description |
|
|
909
|
+
|--------|------|-------------|
|
|
910
|
+
| `in` | array | Allowed values |
|
|
911
|
+
|
|
912
|
+
```yaml
|
|
913
|
+
validations:
|
|
914
|
+
- type: inclusion
|
|
915
|
+
options: { in: [low, medium, high] }
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
#### `exclusion`
|
|
919
|
+
|
|
920
|
+
Validates that the value is not in a given set.
|
|
921
|
+
|
|
922
|
+
| Option | Type | Description |
|
|
923
|
+
|--------|------|-------------|
|
|
924
|
+
| `in` | array | Excluded values |
|
|
925
|
+
|
|
926
|
+
```yaml
|
|
927
|
+
validations:
|
|
928
|
+
- type: exclusion
|
|
929
|
+
options: { in: [banned, restricted] }
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
#### `uniqueness`
|
|
933
|
+
|
|
934
|
+
Validates uniqueness of the value.
|
|
935
|
+
|
|
936
|
+
| Option | Type | Description |
|
|
937
|
+
|--------|------|-------------|
|
|
938
|
+
| `scope` | string or array | Columns to scope uniqueness to |
|
|
939
|
+
| `case_sensitive` | boolean | Case-sensitive comparison |
|
|
940
|
+
|
|
941
|
+
```yaml
|
|
942
|
+
validations:
|
|
943
|
+
- type: uniqueness
|
|
944
|
+
options: { scope: company_id, case_sensitive: false }
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
#### `confirmation`
|
|
948
|
+
|
|
949
|
+
Validates that a `<field>_confirmation` attribute matches the field.
|
|
950
|
+
|
|
951
|
+
```yaml
|
|
952
|
+
validations:
|
|
953
|
+
- type: confirmation
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
#### `custom`
|
|
957
|
+
|
|
958
|
+
Delegates to a custom validator class. The class must inherit from `ActiveModel::Validator`.
|
|
959
|
+
|
|
960
|
+
| Attribute | Type | Description |
|
|
961
|
+
|-----------|------|-------------|
|
|
962
|
+
| `validator_class` | string | Fully qualified class name |
|
|
963
|
+
|
|
964
|
+
```yaml
|
|
965
|
+
# Field-level custom validation
|
|
966
|
+
fields:
|
|
967
|
+
- name: email
|
|
968
|
+
type: string
|
|
969
|
+
validations:
|
|
970
|
+
- type: custom
|
|
971
|
+
validator_class: "EmailFormatValidator"
|
|
972
|
+
|
|
973
|
+
# Model-level custom validation
|
|
974
|
+
validations:
|
|
975
|
+
- type: custom
|
|
976
|
+
validator_class: "BusinessRuleValidator"
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
#### `comparison`
|
|
980
|
+
|
|
981
|
+
Compares the field value against another field on the same record. Skipped when either value is nil.
|
|
982
|
+
|
|
983
|
+
| Attribute | Type | Description |
|
|
984
|
+
|-----------|------|-------------|
|
|
985
|
+
| `operator` | string | Comparison operator: `gt`, `gte`, `lt`, `lte`, `eq`, `not_eq` |
|
|
986
|
+
| `field_ref` | string | Name of the field to compare against |
|
|
987
|
+
| `message` | string | Custom error message |
|
|
988
|
+
|
|
989
|
+
```yaml
|
|
990
|
+
fields:
|
|
991
|
+
- name: due_date
|
|
992
|
+
type: date
|
|
993
|
+
validations:
|
|
994
|
+
- type: comparison
|
|
995
|
+
operator: gte
|
|
996
|
+
field_ref: start_date
|
|
997
|
+
message: "must be on or after start date"
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
```ruby
|
|
1001
|
+
# DSL
|
|
1002
|
+
field :due_date, :date do
|
|
1003
|
+
validates :comparison, operator: :gte, field_ref: :start_date,
|
|
1004
|
+
message: "must be on or after start date"
|
|
1005
|
+
end
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
#### `service`
|
|
1009
|
+
|
|
1010
|
+
Delegates validation to a registered service class. The service receives the record and can add errors.
|
|
1011
|
+
|
|
1012
|
+
| Attribute | Type | Description |
|
|
1013
|
+
|-----------|------|-------------|
|
|
1014
|
+
| `service` | string | Service key registered in `app/lcp_services/validators/` |
|
|
1015
|
+
|
|
1016
|
+
```yaml
|
|
1017
|
+
# Model-level service validation
|
|
1018
|
+
validations:
|
|
1019
|
+
- type: service
|
|
1020
|
+
service: deal_credit_limit
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
```ruby
|
|
1024
|
+
# DSL
|
|
1025
|
+
validates_model :service, service: "deal_credit_limit"
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
Service contract: `def self.call(record, **opts) -> void` (adds errors directly to `record.errors`). Register in `app/lcp_services/validators/`. See [Extensibility Guide](../guides/extensibility.md).
|
|
1029
|
+
|
|
1030
|
+
#### `array_length`
|
|
1031
|
+
|
|
1032
|
+
Validates the number of items in an array field.
|
|
1033
|
+
|
|
1034
|
+
| Option | Type | Description |
|
|
1035
|
+
|--------|------|-------------|
|
|
1036
|
+
| `minimum` | integer | Minimum number of items |
|
|
1037
|
+
| `maximum` | integer | Maximum number of items |
|
|
1038
|
+
|
|
1039
|
+
```yaml
|
|
1040
|
+
fields:
|
|
1041
|
+
- name: tags
|
|
1042
|
+
type: array
|
|
1043
|
+
item_type: string
|
|
1044
|
+
validations:
|
|
1045
|
+
- type: array_length
|
|
1046
|
+
options: { minimum: 1, maximum: 10 }
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
```ruby
|
|
1050
|
+
# DSL
|
|
1051
|
+
field :tags, :array, item_type: :string do
|
|
1052
|
+
validates :array_length, minimum: 1, maximum: 10
|
|
1053
|
+
end
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
#### `array_inclusion`
|
|
1057
|
+
|
|
1058
|
+
Validates that all items in the array are within an allowed set.
|
|
1059
|
+
|
|
1060
|
+
| Option | Type | Description |
|
|
1061
|
+
|--------|------|-------------|
|
|
1062
|
+
| `in` | array | Allowed values |
|
|
1063
|
+
|
|
1064
|
+
```yaml
|
|
1065
|
+
fields:
|
|
1066
|
+
- name: tags
|
|
1067
|
+
type: array
|
|
1068
|
+
item_type: string
|
|
1069
|
+
validations:
|
|
1070
|
+
- type: array_inclusion
|
|
1071
|
+
options: { in: [ruby, python, java, go, rust] }
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
```ruby
|
|
1075
|
+
# DSL
|
|
1076
|
+
field :tags, :array, item_type: :string do
|
|
1077
|
+
validates :array_inclusion, in: %w[ruby python java go rust]
|
|
1078
|
+
end
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
#### `array_uniqueness`
|
|
1082
|
+
|
|
1083
|
+
Validates that all items in the array are unique (no duplicates).
|
|
1084
|
+
|
|
1085
|
+
```yaml
|
|
1086
|
+
fields:
|
|
1087
|
+
- name: tags
|
|
1088
|
+
type: array
|
|
1089
|
+
item_type: string
|
|
1090
|
+
validations:
|
|
1091
|
+
- type: array_uniqueness
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
```ruby
|
|
1095
|
+
# DSL
|
|
1096
|
+
field :tags, :array, item_type: :string do
|
|
1097
|
+
validates :array_uniqueness
|
|
1098
|
+
end
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### Conditional Validations (`when:`)
|
|
1102
|
+
|
|
1103
|
+
Any validation can be made conditional using the `when:` key. When present, the validation only runs if the condition evaluates to true. The condition uses the same `{ field:, operator:, value: }` syntax as [`visible_when`](presenters.md#field-visibility) and [`condition`](condition-operators.md).
|
|
1104
|
+
|
|
1105
|
+
```yaml
|
|
1106
|
+
fields:
|
|
1107
|
+
- name: value
|
|
1108
|
+
type: decimal
|
|
1109
|
+
validations:
|
|
1110
|
+
- type: presence
|
|
1111
|
+
when:
|
|
1112
|
+
field: stage
|
|
1113
|
+
operator: not_in
|
|
1114
|
+
value: [lead]
|
|
1115
|
+
- type: numericality
|
|
1116
|
+
options: { greater_than_or_equal_to: 0, allow_nil: true }
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
```ruby
|
|
1120
|
+
# DSL
|
|
1121
|
+
field :value, :decimal do
|
|
1122
|
+
validates :presence, when: { field: :stage, operator: :not_in, value: %w[lead] }
|
|
1123
|
+
validates :numericality, greater_than_or_equal_to: 0, allow_nil: true
|
|
1124
|
+
end
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
Service-based conditions are also supported:
|
|
1128
|
+
|
|
1129
|
+
```yaml
|
|
1130
|
+
validations:
|
|
1131
|
+
- type: presence
|
|
1132
|
+
when:
|
|
1133
|
+
service: requires_approval
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### Common Validation Options
|
|
1137
|
+
|
|
1138
|
+
These options can be added to any validation type:
|
|
1139
|
+
|
|
1140
|
+
| Option | Type | Description |
|
|
1141
|
+
|--------|------|-------------|
|
|
1142
|
+
| `message` | string | Custom error message |
|
|
1143
|
+
| `allow_blank` | boolean | Skip validation when value is blank |
|
|
1144
|
+
| `if` | string | Method name; only validate when method returns true |
|
|
1145
|
+
| `unless` | string | Method name; skip validation when method returns true |
|
|
1146
|
+
|
|
1147
|
+
```yaml
|
|
1148
|
+
validations:
|
|
1149
|
+
- type: presence
|
|
1150
|
+
options: { message: "must be provided", if: "active?" }
|
|
1151
|
+
- type: numericality
|
|
1152
|
+
options: { greater_than: 0, allow_blank: true, unless: "draft?" }
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
The `if` and `unless` options reference method names on the model instance. These methods must be defined in a model extension or custom validator.
|
|
1156
|
+
|
|
1157
|
+
## Associations
|
|
1158
|
+
|
|
1159
|
+
Associations define relationships between models.
|
|
1160
|
+
|
|
1161
|
+
```yaml
|
|
1162
|
+
associations:
|
|
1163
|
+
# Standard belongs_to
|
|
1164
|
+
- type: belongs_to
|
|
1165
|
+
name: company
|
|
1166
|
+
target_model: company
|
|
1167
|
+
foreign_key: company_id
|
|
1168
|
+
required: true
|
|
1169
|
+
inverse_of: employees
|
|
1170
|
+
|
|
1171
|
+
# Polymorphic belongs_to (creates _id + _type columns automatically)
|
|
1172
|
+
- type: belongs_to
|
|
1173
|
+
name: commentable
|
|
1174
|
+
polymorphic: true
|
|
1175
|
+
required: false
|
|
1176
|
+
|
|
1177
|
+
# has_many with polymorphic inverse
|
|
1178
|
+
- type: has_many
|
|
1179
|
+
name: comments
|
|
1180
|
+
target_model: comment
|
|
1181
|
+
as: commentable
|
|
1182
|
+
|
|
1183
|
+
# has_many through (join model)
|
|
1184
|
+
- type: has_many
|
|
1185
|
+
name: taggings
|
|
1186
|
+
target_model: tagging
|
|
1187
|
+
- type: has_many
|
|
1188
|
+
name: tags
|
|
1189
|
+
through: taggings
|
|
1190
|
+
source: tag
|
|
1191
|
+
|
|
1192
|
+
# Join on a business key instead of the target's primary key.
|
|
1193
|
+
# The owner's `division_code` matches `divisions.vema_code`.
|
|
1194
|
+
- type: belongs_to
|
|
1195
|
+
name: division
|
|
1196
|
+
target_model: division
|
|
1197
|
+
foreign_key: division_code
|
|
1198
|
+
primary_key: vema_code
|
|
1199
|
+
required: false
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
### Association Attributes
|
|
1203
|
+
|
|
1204
|
+
#### `type`
|
|
1205
|
+
|
|
1206
|
+
| | |
|
|
1207
|
+
|---|---|
|
|
1208
|
+
| **Required** | yes |
|
|
1209
|
+
| **Allowed** | `belongs_to`, `has_many`, `has_one` |
|
|
1210
|
+
|
|
1211
|
+
The ActiveRecord association type.
|
|
1212
|
+
|
|
1213
|
+
#### `name`
|
|
1214
|
+
|
|
1215
|
+
| | |
|
|
1216
|
+
|---|---|
|
|
1217
|
+
| **Required** | yes |
|
|
1218
|
+
| **Type** | string |
|
|
1219
|
+
|
|
1220
|
+
The association name. Used as the method name on the model (e.g., `record.company`).
|
|
1221
|
+
|
|
1222
|
+
#### `target_model`
|
|
1223
|
+
|
|
1224
|
+
| | |
|
|
1225
|
+
|---|---|
|
|
1226
|
+
| **Required** | conditionally (see below) |
|
|
1227
|
+
| **Type** | string |
|
|
1228
|
+
|
|
1229
|
+
Name of another LCP model. The engine resolves this to `LcpRuby::Dynamic::<TargetModel>` at runtime. Use this for associations between LCP-managed models.
|
|
1230
|
+
|
|
1231
|
+
At least one of `target_model`, `class_name`, `polymorphic`, `as`, or `through` must be present.
|
|
1232
|
+
|
|
1233
|
+
```yaml
|
|
1234
|
+
- type: belongs_to
|
|
1235
|
+
name: todo_list
|
|
1236
|
+
target_model: todo_list
|
|
1237
|
+
foreign_key: todo_list_id
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
#### `class_name`
|
|
1241
|
+
|
|
1242
|
+
| | |
|
|
1243
|
+
|---|---|
|
|
1244
|
+
| **Required** | conditionally (see `target_model`) |
|
|
1245
|
+
| **Type** | string |
|
|
1246
|
+
|
|
1247
|
+
Fully qualified class name for associations pointing to non-LCP models (e.g., your host app's `User` model). When `target_model` is set, `class_name` is ignored — the engine generates it automatically.
|
|
1248
|
+
|
|
1249
|
+
```yaml
|
|
1250
|
+
- type: belongs_to
|
|
1251
|
+
name: author
|
|
1252
|
+
class_name: "User"
|
|
1253
|
+
foreign_key: author_id
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
#### `foreign_key`
|
|
1257
|
+
|
|
1258
|
+
| | |
|
|
1259
|
+
|---|---|
|
|
1260
|
+
| **Required** | no |
|
|
1261
|
+
| **Default** | Rails convention (`<name>_id` for `belongs_to`) |
|
|
1262
|
+
| **Type** | string |
|
|
1263
|
+
|
|
1264
|
+
The foreign key column name. Specify this when the column name does not follow Rails naming conventions.
|
|
1265
|
+
|
|
1266
|
+
#### `primary_key`
|
|
1267
|
+
|
|
1268
|
+
| | |
|
|
1269
|
+
|---|---|
|
|
1270
|
+
| **Required** | no |
|
|
1271
|
+
| **Default** | target model's primary key column (usually `id`) |
|
|
1272
|
+
| **Type** | string |
|
|
1273
|
+
| **Applies to** | `belongs_to`, `has_many`, `has_one` (not `through:`, not `polymorphic:`) |
|
|
1274
|
+
|
|
1275
|
+
Column on the PK side of the join that the `foreign_key` matches against. Mirrors Rails's native `primary_key:` option and lets associations join on a natural business key (`vema_code`, `iso_code`, `sku`, ...) instead of a surrogate `id`.
|
|
1276
|
+
|
|
1277
|
+
**Semantics — the join pair depends on association type:**
|
|
1278
|
+
|
|
1279
|
+
| Association type (declared on *owner*) | Effective join pair |
|
|
1280
|
+
|---|---|
|
|
1281
|
+
| `belongs_to` | `owner.foreign_key = target.primary_key` |
|
|
1282
|
+
| `has_many` (non-`through`) | `target.foreign_key = owner.primary_key` |
|
|
1283
|
+
| `has_one` (non-`through`) | `target.foreign_key = owner.primary_key` |
|
|
1284
|
+
|
|
1285
|
+
```yaml
|
|
1286
|
+
# On employee — belongs_to joins to a non-PK column on the target
|
|
1287
|
+
- type: belongs_to
|
|
1288
|
+
name: division
|
|
1289
|
+
target_model: division
|
|
1290
|
+
foreign_key: division_code
|
|
1291
|
+
primary_key: vema_code # employees.division_code = divisions.vema_code
|
|
1292
|
+
required: false
|
|
1293
|
+
|
|
1294
|
+
# On division — reverse side, same join pair
|
|
1295
|
+
- type: has_many
|
|
1296
|
+
name: employees
|
|
1297
|
+
target_model: employee
|
|
1298
|
+
foreign_key: division_code
|
|
1299
|
+
primary_key: vema_code # employees.division_code = divisions.vema_code
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
**FK column type.** When `primary_key:` points at an LCP model's field, the auto-created FK column on the `belongs_to` side matches that field's type instead of the default `bigint`. For a `belongs_to :division, primary_key: :vema_code` where `divisions.vema_code` is `integer`, the engine creates `employees.division_code` as `integer`. For string business keys (`"EUR"`, `"0042"`), the FK is created as `string`.
|
|
1303
|
+
|
|
1304
|
+
When `primary_key:` points at a non-LCP class (`class_name:` without `target_model:`), LCP cannot introspect the target column's type — the configurator **must** declare the FK column explicitly via `fields:`:
|
|
1305
|
+
|
|
1306
|
+
```yaml
|
|
1307
|
+
fields:
|
|
1308
|
+
- name: division_code
|
|
1309
|
+
type: string # must match the external column type
|
|
1310
|
+
|
|
1311
|
+
associations:
|
|
1312
|
+
- type: belongs_to
|
|
1313
|
+
name: division
|
|
1314
|
+
class_name: "::HostDivision"
|
|
1315
|
+
foreign_key: division_code
|
|
1316
|
+
primary_key: vema_code
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
Without the explicit field, `ConfigurationValidator` appends an error at boot.
|
|
1320
|
+
|
|
1321
|
+
**Boot-time validation** (runs via `bundle exec rake lcp_ruby:validate`):
|
|
1322
|
+
|
|
1323
|
+
- The referenced column must exist on the PK side (target for `belongs_to`, owner for `has_many`/`has_one`).
|
|
1324
|
+
- A warning fires when the column has no uniqueness constraint (unscoped `validates … uniqueness: true` or a single-column unique `indexes:` entry). Scoped uniqueness emits a softer warning pointing at `query_constraints:` as the proper fix for partition-scoped joins.
|
|
1325
|
+
- A warning fires when the FK and PK column types differ.
|
|
1326
|
+
- A warning fires when the FK is declared as an explicit `field:` but has no covering `indexes:` entry — auto-indexing does not cover explicit FK fields, so joins would silently degrade to table scans.
|
|
1327
|
+
- A warning fires when the forward and inverse associations declare different join pairs (reciprocity alignment).
|
|
1328
|
+
|
|
1329
|
+
**Rejected configurations** (hard errors):
|
|
1330
|
+
|
|
1331
|
+
- `primary_key:` together with `through:` — put it on the individual link associations instead.
|
|
1332
|
+
- `primary_key:` together with `polymorphic: true` — each polymorphic target can have a different PK column; there is no single column that makes sense.
|
|
1333
|
+
- `primary_key:` on tree-managed self-referencing associations (the recursive CTE hardcodes `id`-based joins — out of scope for v1).
|
|
1334
|
+
- `primary_key:` on cross-source (DB ↔ API) associations — the API applicator has its own `id`-based lookup paths that do not yet read the join column (out of scope for v1).
|
|
1335
|
+
|
|
1336
|
+
**`bind_to:` models.** `primary_key:` is allowed on associations whose target is a `bind_to` model (LCP reads the YAML to render form selects, cascading selects, tree selects, [page filter form fields](page_filters.md), import/export metadata, and virtual-column SQL). The host AR class is responsible for declaring its own Rails macro-level `primary_key:`. Uniqueness and type-compat checks are skipped for `bind_to` targets because LCP cannot introspect the host schema — to opt back into the column-exists check, list the join column in the `bind_to` model's YAML `fields:`.
|
|
1337
|
+
|
|
1338
|
+
**Migration note — redundant denormalized columns.** The common motivation for this feature is dropping denormalized columns (`division_code` + `division_name` + `bu_code` + `bu_name`) in favor of a single FK that joins on the business code. The migration steps are:
|
|
1339
|
+
|
|
1340
|
+
1. Add `primary_key:` to the association in YAML.
|
|
1341
|
+
2. Remove the redundant field entries (`division_name`, `bu_code`, `bu_name`, ...) from the owner model.
|
|
1342
|
+
3. Update any host code that referenced the redundant fields to traverse via the association (`record.division.vema_name`, `record.division.business_unit.vema_name`, ...).
|
|
1343
|
+
|
|
1344
|
+
LCP's auto-migration is additive — removing a YAML field leaves the column in the DB. A manual SQL migration to drop the column is optional and host-app-specific.
|
|
1345
|
+
|
|
1346
|
+
See [design/association_primary_key.md](../design/association_primary_key.md) for the full feature specification, including every downstream consumer that was updated (form helpers, cascading selects, tree selects, page parameters [retired in PR 5; replaced by [filter forms](page_filters.md)], import/export field trees, virtual columns).
|
|
1347
|
+
|
|
1348
|
+
#### `dependent`
|
|
1349
|
+
|
|
1350
|
+
| | |
|
|
1351
|
+
|---|---|
|
|
1352
|
+
| **Required** | no |
|
|
1353
|
+
| **Default** | none |
|
|
1354
|
+
| **Type** | string |
|
|
1355
|
+
| **Allowed** | `destroy`, `delete`, `delete_all`, `nullify`, `restrict_with_error`, `restrict_with_exception`, `discard` |
|
|
1356
|
+
|
|
1357
|
+
What happens to associated records when the parent is destroyed (or discarded). Applicable to all association types.
|
|
1358
|
+
|
|
1359
|
+
The `discard` value is special — it is **not** passed to ActiveRecord. Instead, when the parent record is discarded (soft-deleted), the `SoftDeleteApplicator` cascades the discard to child records with `dependent: :discard`. This requires:
|
|
1360
|
+
- The parent model must have `soft_delete` enabled
|
|
1361
|
+
- The target (child) model must also have `soft_delete` enabled
|
|
1362
|
+
- Only valid on `has_many` / `has_one` associations (not `belongs_to`)
|
|
1363
|
+
|
|
1364
|
+
#### `required`
|
|
1365
|
+
|
|
1366
|
+
| | |
|
|
1367
|
+
|---|---|
|
|
1368
|
+
| **Required** | no (but **strongly recommended on `belongs_to`** — see note) |
|
|
1369
|
+
| **Default** | `true` for `belongs_to` (AR Rails 5+), `false` for `has_many`/`has_one` |
|
|
1370
|
+
| **Type** | boolean |
|
|
1371
|
+
|
|
1372
|
+
Whether the association is mandatory. For `belongs_to`, setting `required: false` allows the foreign key to be NULL.
|
|
1373
|
+
|
|
1374
|
+
> **Always set `required:` explicitly on `belongs_to`.** Omitting it inherits the AR Rails 5+ default (`required: true`), which silently rejects nullable parents at save time. `bundle exec rake lcp_ruby:validate` warns whenever a `belongs_to` declaration lacks an explicit `required:` kwarg. The `lcp_ruby:entity` generator emits it for every generated association.
|
|
1375
|
+
|
|
1376
|
+
#### `inverse_of`
|
|
1377
|
+
|
|
1378
|
+
| | |
|
|
1379
|
+
|---|---|
|
|
1380
|
+
| **Required** | no |
|
|
1381
|
+
| **Default** | none (Rails auto-detection) |
|
|
1382
|
+
| **Type** | string |
|
|
1383
|
+
| **Applies to** | all types |
|
|
1384
|
+
|
|
1385
|
+
Specifies the inverse association on the target model. Rails usually infers this, but explicit setting avoids ambiguity and improves performance by preventing extra queries.
|
|
1386
|
+
|
|
1387
|
+
```yaml
|
|
1388
|
+
associations:
|
|
1389
|
+
- type: has_many
|
|
1390
|
+
name: tasks
|
|
1391
|
+
target_model: task
|
|
1392
|
+
inverse_of: project
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
#### `counter_cache`
|
|
1396
|
+
|
|
1397
|
+
| | |
|
|
1398
|
+
|---|---|
|
|
1399
|
+
| **Required** | no |
|
|
1400
|
+
| **Default** | none |
|
|
1401
|
+
| **Type** | boolean or string |
|
|
1402
|
+
| **Applies to** | `belongs_to` |
|
|
1403
|
+
|
|
1404
|
+
Maintains a count column on the parent model. Set to `true` to use the default column name (`<children>_count`), or a string for a custom column name.
|
|
1405
|
+
|
|
1406
|
+
The counter column must be added as a field on the parent model.
|
|
1407
|
+
|
|
1408
|
+
```yaml
|
|
1409
|
+
# On child model
|
|
1410
|
+
associations:
|
|
1411
|
+
- type: belongs_to
|
|
1412
|
+
name: project
|
|
1413
|
+
target_model: project
|
|
1414
|
+
counter_cache: true
|
|
1415
|
+
|
|
1416
|
+
# Or with custom column name
|
|
1417
|
+
counter_cache: tasks_count
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
#### `touch`
|
|
1421
|
+
|
|
1422
|
+
| | |
|
|
1423
|
+
|---|---|
|
|
1424
|
+
| **Required** | no |
|
|
1425
|
+
| **Default** | none |
|
|
1426
|
+
| **Type** | boolean or string |
|
|
1427
|
+
| **Applies to** | `belongs_to` |
|
|
1428
|
+
|
|
1429
|
+
Updates the parent's `updated_at` timestamp when the child is saved. Set to `true` for `updated_at`, or a string for a custom timestamp column.
|
|
1430
|
+
|
|
1431
|
+
```yaml
|
|
1432
|
+
associations:
|
|
1433
|
+
- type: belongs_to
|
|
1434
|
+
name: project
|
|
1435
|
+
target_model: project
|
|
1436
|
+
touch: true
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
#### `polymorphic`
|
|
1440
|
+
|
|
1441
|
+
| | |
|
|
1442
|
+
|---|---|
|
|
1443
|
+
| **Required** | no |
|
|
1444
|
+
| **Default** | `false` |
|
|
1445
|
+
| **Type** | boolean |
|
|
1446
|
+
| **Applies to** | `belongs_to` |
|
|
1447
|
+
|
|
1448
|
+
Creates a polymorphic association. When `true`, the engine automatically creates both `<name>_id` and `<name>_type` columns. No `target_model` or `class_name` is needed — the type is determined at runtime from the `_type` column.
|
|
1449
|
+
|
|
1450
|
+
```yaml
|
|
1451
|
+
# On child model (e.g., comment)
|
|
1452
|
+
associations:
|
|
1453
|
+
- type: belongs_to
|
|
1454
|
+
name: commentable
|
|
1455
|
+
polymorphic: true
|
|
1456
|
+
required: false
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
#### `as`
|
|
1460
|
+
|
|
1461
|
+
| | |
|
|
1462
|
+
|---|---|
|
|
1463
|
+
| **Required** | no |
|
|
1464
|
+
| **Default** | none |
|
|
1465
|
+
| **Type** | string |
|
|
1466
|
+
| **Applies to** | `has_many`, `has_one` |
|
|
1467
|
+
|
|
1468
|
+
The polymorphic interface name on the target model. Used with `polymorphic` belongs_to on the other side.
|
|
1469
|
+
|
|
1470
|
+
```yaml
|
|
1471
|
+
# On parent model (e.g., post)
|
|
1472
|
+
associations:
|
|
1473
|
+
- type: has_many
|
|
1474
|
+
name: comments
|
|
1475
|
+
target_model: comment
|
|
1476
|
+
as: commentable
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1479
|
+
#### `through`
|
|
1480
|
+
|
|
1481
|
+
| | |
|
|
1482
|
+
|---|---|
|
|
1483
|
+
| **Required** | no |
|
|
1484
|
+
| **Default** | none |
|
|
1485
|
+
| **Type** | string |
|
|
1486
|
+
| **Applies to** | `has_many`, `has_one` |
|
|
1487
|
+
|
|
1488
|
+
Creates a through association via a join model. The value is the name of another association on this model that serves as the join. No FK columns are created for through associations.
|
|
1489
|
+
|
|
1490
|
+
```yaml
|
|
1491
|
+
associations:
|
|
1492
|
+
- type: has_many
|
|
1493
|
+
name: taggings
|
|
1494
|
+
target_model: tagging
|
|
1495
|
+
- type: has_many
|
|
1496
|
+
name: tags
|
|
1497
|
+
through: taggings
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
#### `source`
|
|
1501
|
+
|
|
1502
|
+
| | |
|
|
1503
|
+
|---|---|
|
|
1504
|
+
| **Required** | no |
|
|
1505
|
+
| **Default** | none (Rails infers from association name) |
|
|
1506
|
+
| **Type** | string |
|
|
1507
|
+
| **Applies to** | `has_many`, `has_one` (with `through`) |
|
|
1508
|
+
|
|
1509
|
+
Specifies the source association on the join model. Only needed when the name cannot be inferred automatically.
|
|
1510
|
+
|
|
1511
|
+
```yaml
|
|
1512
|
+
associations:
|
|
1513
|
+
- type: has_many
|
|
1514
|
+
name: authors
|
|
1515
|
+
through: authorships
|
|
1516
|
+
source: person
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
#### `autosave`
|
|
1520
|
+
|
|
1521
|
+
| | |
|
|
1522
|
+
|---|---|
|
|
1523
|
+
| **Required** | no |
|
|
1524
|
+
| **Default** | none (Rails default) |
|
|
1525
|
+
| **Type** | boolean |
|
|
1526
|
+
| **Applies to** | all types |
|
|
1527
|
+
|
|
1528
|
+
When `true`, saves associated records whenever the parent is saved.
|
|
1529
|
+
|
|
1530
|
+
```yaml
|
|
1531
|
+
associations:
|
|
1532
|
+
- type: has_many
|
|
1533
|
+
name: items
|
|
1534
|
+
target_model: item
|
|
1535
|
+
autosave: true
|
|
1536
|
+
```
|
|
1537
|
+
|
|
1538
|
+
#### `validate`
|
|
1539
|
+
|
|
1540
|
+
| | |
|
|
1541
|
+
|---|---|
|
|
1542
|
+
| **Required** | no |
|
|
1543
|
+
| **Default** | none (Rails default) |
|
|
1544
|
+
| **Type** | boolean |
|
|
1545
|
+
| **Applies to** | all types |
|
|
1546
|
+
|
|
1547
|
+
When `true`, validates associated records on save. Set to `false` to skip validation of associated records.
|
|
1548
|
+
|
|
1549
|
+
```yaml
|
|
1550
|
+
associations:
|
|
1551
|
+
- type: has_many
|
|
1552
|
+
name: items
|
|
1553
|
+
target_model: item
|
|
1554
|
+
validate: false
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
#### `order`
|
|
1558
|
+
|
|
1559
|
+
| | |
|
|
1560
|
+
|---|---|
|
|
1561
|
+
| **Required** | no |
|
|
1562
|
+
| **Default** | none |
|
|
1563
|
+
| **Type** | hash |
|
|
1564
|
+
| **Applies to** | `has_many`, `has_one` |
|
|
1565
|
+
|
|
1566
|
+
Default ordering for associated records. The engine passes this as a scope lambda to the ActiveRecord association. Keys are column names, values are `asc` or `desc`.
|
|
1567
|
+
|
|
1568
|
+
```yaml
|
|
1569
|
+
associations:
|
|
1570
|
+
- type: has_many
|
|
1571
|
+
name: todo_items
|
|
1572
|
+
target_model: todo_item
|
|
1573
|
+
order:
|
|
1574
|
+
position: asc
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
This generates: `has_many :todo_items, -> { order(position: :asc) }, ...`
|
|
1578
|
+
|
|
1579
|
+
Useful for sortable nested forms where child records have a position field.
|
|
1580
|
+
|
|
1581
|
+
#### `skip_inverse_check`
|
|
1582
|
+
|
|
1583
|
+
| | |
|
|
1584
|
+
|---|---|
|
|
1585
|
+
| **Required** | no |
|
|
1586
|
+
| **Default** | `false` |
|
|
1587
|
+
| **Type** | boolean |
|
|
1588
|
+
| **Applies to** | `belongs_to` only |
|
|
1589
|
+
|
|
1590
|
+
Suppresses the missing-inverse warning from `lcp_ruby:validate`. Use when a `belongs_to` is intentionally one-directional and you don't want a `has_many` on the parent side.
|
|
1591
|
+
|
|
1592
|
+
**When to use:** audit/owner/author references where reverse traversal would be noise.
|
|
1593
|
+
|
|
1594
|
+
```yaml
|
|
1595
|
+
associations:
|
|
1596
|
+
- type: belongs_to
|
|
1597
|
+
name: author
|
|
1598
|
+
target_model: teacher
|
|
1599
|
+
skip_inverse_check: true # `teacher.has_many :authored_announcements` would be unused noise
|
|
1600
|
+
- type: belongs_to
|
|
1601
|
+
name: created_by
|
|
1602
|
+
target_model: user
|
|
1603
|
+
skip_inverse_check: true # `user.has_many :created_things` × N tables: clutter
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
**When NOT to use:** the inverse would actually be queried (`teacher.school_classes`, `school_class.students`). Adding the `has_many` is the right move; this flag exists for the cases where it isn't.
|
|
1607
|
+
|
|
1608
|
+
**Validation.** `skip_inverse_check: true` on `has_many` or `has_one` raises `MetadataError` at boot — that case (a parent declaring children with no `belongs_to` back) is a genuine bug because the FK has nowhere to go.
|
|
1609
|
+
|
|
1610
|
+
## Nested Attributes
|
|
1611
|
+
|
|
1612
|
+
Associations can declare `nested_attributes` to enable creating and updating associated records through the parent model's form and the import system. This uses Rails' `accepts_nested_attributes_for` under the hood.
|
|
1613
|
+
|
|
1614
|
+
### Boolean Shorthand
|
|
1615
|
+
|
|
1616
|
+
For `has_one` associations, the recommended form is the boolean shorthand:
|
|
1617
|
+
|
|
1618
|
+
```yaml
|
|
1619
|
+
associations:
|
|
1620
|
+
- type: has_one
|
|
1621
|
+
name: employee_profile
|
|
1622
|
+
target_model: employee_profile
|
|
1623
|
+
dependent: destroy
|
|
1624
|
+
inverse_of: employee
|
|
1625
|
+
nested_attributes: true # safe defaults: reject_if: all_blank, update_only: true
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
`nested_attributes: true` is equivalent to `{ reject_if: all_blank, update_only: true }`. These defaults are safe for both forms and import:
|
|
1629
|
+
- `reject_if: all_blank` prevents creating empty nested records when all fields are blank
|
|
1630
|
+
- `update_only: true` updates existing nested records in-place instead of destroying and recreating them (which would break foreign key references)
|
|
1631
|
+
|
|
1632
|
+
### Explicit Hash Form
|
|
1633
|
+
|
|
1634
|
+
For fine-grained control, use the explicit hash:
|
|
1635
|
+
|
|
1636
|
+
```yaml
|
|
1637
|
+
associations:
|
|
1638
|
+
- type: has_many
|
|
1639
|
+
name: todo_items
|
|
1640
|
+
target_model: todo_item
|
|
1641
|
+
dependent: destroy
|
|
1642
|
+
inverse_of: todo_list
|
|
1643
|
+
nested_attributes:
|
|
1644
|
+
allow_destroy: true
|
|
1645
|
+
reject_if: all_blank
|
|
1646
|
+
limit: 50
|
|
1647
|
+
update_only: false
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
### Nested Attributes Keys
|
|
1651
|
+
|
|
1652
|
+
#### `allow_destroy`
|
|
1653
|
+
|
|
1654
|
+
| | |
|
|
1655
|
+
|---|---|
|
|
1656
|
+
| **Required** | no |
|
|
1657
|
+
| **Default** | `false` |
|
|
1658
|
+
| **Type** | boolean |
|
|
1659
|
+
|
|
1660
|
+
When `true`, nested records can be marked for deletion by passing `_destroy: true` in the nested attributes hash. Without this option, nested records can only be created and updated, not removed through the parent form.
|
|
1661
|
+
|
|
1662
|
+
```yaml
|
|
1663
|
+
nested_attributes:
|
|
1664
|
+
allow_destroy: true
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
#### `reject_if`
|
|
1668
|
+
|
|
1669
|
+
| | |
|
|
1670
|
+
|---|---|
|
|
1671
|
+
| **Required** | no |
|
|
1672
|
+
| **Default** | none |
|
|
1673
|
+
| **Type** | string |
|
|
1674
|
+
|
|
1675
|
+
Controls when nested records are silently rejected (not saved). The special value `"all_blank"` rejects any nested record where every attribute value is blank. This is useful for forms that render empty rows for new records — blank rows are ignored instead of causing validation errors.
|
|
1676
|
+
|
|
1677
|
+
Can also be set to a symbol name referencing a custom method on the model that returns `true` to reject the record.
|
|
1678
|
+
|
|
1679
|
+
```yaml
|
|
1680
|
+
nested_attributes:
|
|
1681
|
+
reject_if: all_blank
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
#### `limit`
|
|
1685
|
+
|
|
1686
|
+
| | |
|
|
1687
|
+
|---|---|
|
|
1688
|
+
| **Required** | no |
|
|
1689
|
+
| **Default** | none (unlimited) |
|
|
1690
|
+
| **Type** | integer |
|
|
1691
|
+
|
|
1692
|
+
Maximum number of nested records that can be processed at once. If the incoming attributes hash contains more records than the limit, a `TooManyRecords` exception is raised. Use this to prevent abuse or accidental mass-creation of associated records.
|
|
1693
|
+
|
|
1694
|
+
```yaml
|
|
1695
|
+
nested_attributes:
|
|
1696
|
+
limit: 50
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
#### `update_only`
|
|
1700
|
+
|
|
1701
|
+
| | |
|
|
1702
|
+
|---|---|
|
|
1703
|
+
| **Required** | no |
|
|
1704
|
+
| **Default** | `false` |
|
|
1705
|
+
| **Type** | boolean |
|
|
1706
|
+
|
|
1707
|
+
When `true`, existing nested records are updated in-place instead of being destroyed and recreated. This preserves the nested record's ID, which is important when other records reference it via foreign keys.
|
|
1708
|
+
|
|
1709
|
+
**Note:** Despite the name, `update_only: true` still allows **creating** new nested records when none exists. Rails' internal logic falls through to `build_<association>` when no existing record is found. The name only means "don't destroy+recreate existing records".
|
|
1710
|
+
|
|
1711
|
+
```yaml
|
|
1712
|
+
nested_attributes:
|
|
1713
|
+
update_only: true
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
### Requirements
|
|
1717
|
+
|
|
1718
|
+
- The parent association **must** specify `inverse_of` for nested attributes to work correctly. Without `inverse_of`, Rails cannot properly link the parent and child records during validation, which can cause unexpected behavior or validation failures.
|
|
1719
|
+
- Nested attributes are used with `has_many` and `has_one` associations.
|
|
1720
|
+
- `has_one :through` associations are **not compatible** with `nested_attributes` (no direct FK). The `ConfigurationValidator` warns at boot if this is attempted.
|
|
1721
|
+
|
|
1722
|
+
### Import Integration
|
|
1723
|
+
|
|
1724
|
+
`has_one` associations with `nested_attributes` enabled are automatically available for import. See [Import Reference](import.md#nested-has_one-import) for details on:
|
|
1725
|
+
- Dot-notation column mapping (`hire_detail.starting_salary`)
|
|
1726
|
+
- `nested_associations` whitelist on the presenter
|
|
1727
|
+
- `nested_blank_strategy` for controlling blank value handling
|
|
1728
|
+
|
|
1729
|
+
### Full Example
|
|
1730
|
+
|
|
1731
|
+
```yaml
|
|
1732
|
+
model:
|
|
1733
|
+
name: todo_list
|
|
1734
|
+
label: "Todo List"
|
|
1735
|
+
|
|
1736
|
+
fields:
|
|
1737
|
+
- name: name
|
|
1738
|
+
type: string
|
|
1739
|
+
validations:
|
|
1740
|
+
- type: presence
|
|
1741
|
+
|
|
1742
|
+
associations:
|
|
1743
|
+
- type: has_many
|
|
1744
|
+
name: todo_items
|
|
1745
|
+
target_model: todo_item
|
|
1746
|
+
dependent: destroy
|
|
1747
|
+
inverse_of: todo_list
|
|
1748
|
+
nested_attributes:
|
|
1749
|
+
allow_destroy: true
|
|
1750
|
+
reject_if: all_blank
|
|
1751
|
+
limit: 50
|
|
1752
|
+
```
|
|
1753
|
+
|
|
1754
|
+
## Scopes
|
|
1755
|
+
|
|
1756
|
+
Scopes define named query methods on the model.
|
|
1757
|
+
|
|
1758
|
+
```yaml
|
|
1759
|
+
scopes:
|
|
1760
|
+
- name: open_deals
|
|
1761
|
+
where_not: { stage: [closed_won, closed_lost] }
|
|
1762
|
+
- name: won
|
|
1763
|
+
where: { stage: closed_won }
|
|
1764
|
+
- name: recent # order + limit only — no where
|
|
1765
|
+
order: { created_at: desc }
|
|
1766
|
+
limit: 10
|
|
1767
|
+
- name: upcoming # order-only sort helper
|
|
1768
|
+
order: { due_at: asc }
|
|
1769
|
+
- name: first_five # limit-only cap
|
|
1770
|
+
limit: 5
|
|
1771
|
+
```
|
|
1772
|
+
|
|
1773
|
+
Only `name` is required — every other attribute (`where`, `where_not`, `order`, `limit`) is independently optional. Combine any subset, or use just one. Do not write `where: {}` to satisfy a perceived requirement; just omit it.
|
|
1774
|
+
|
|
1775
|
+
### Scope Attributes
|
|
1776
|
+
|
|
1777
|
+
#### `name`
|
|
1778
|
+
|
|
1779
|
+
| | |
|
|
1780
|
+
|---|---|
|
|
1781
|
+
| **Required** | yes |
|
|
1782
|
+
| **Type** | string |
|
|
1783
|
+
|
|
1784
|
+
The scope method name. Called as `ModelClass.scope_name` in queries and referenced from [predefined filters](presenters.md#search-configuration).
|
|
1785
|
+
|
|
1786
|
+
#### `where`
|
|
1787
|
+
|
|
1788
|
+
| | |
|
|
1789
|
+
|---|---|
|
|
1790
|
+
| **Required** | no |
|
|
1791
|
+
| **Type** | hash |
|
|
1792
|
+
|
|
1793
|
+
Generates `scope :name, -> { where(...) }`. Keys are column names, values are the expected values (scalar or array).
|
|
1794
|
+
|
|
1795
|
+
#### `where_not`
|
|
1796
|
+
|
|
1797
|
+
| | |
|
|
1798
|
+
|---|---|
|
|
1799
|
+
| **Required** | no |
|
|
1800
|
+
| **Type** | hash |
|
|
1801
|
+
|
|
1802
|
+
Generates `scope :name, -> { where.not(...) }`. Same syntax as `where` but negated.
|
|
1803
|
+
|
|
1804
|
+
#### `order`
|
|
1805
|
+
|
|
1806
|
+
| | |
|
|
1807
|
+
|---|---|
|
|
1808
|
+
| **Required** | no |
|
|
1809
|
+
| **Type** | hash |
|
|
1810
|
+
|
|
1811
|
+
Generates ordering. Keys are column names, values are `asc` or `desc`.
|
|
1812
|
+
|
|
1813
|
+
Valid column names are everything `SchemaManager` puts on the table: declared `field:` rows, `belongs_to` FK columns (incl. polymorphic `_type`), userstamps, timestamps when enabled, soft-delete column, positioning column, and `id`. The same set is accepted in `where:` / `where_not:`.
|
|
1814
|
+
|
|
1815
|
+
#### `limit`
|
|
1816
|
+
|
|
1817
|
+
| | |
|
|
1818
|
+
|---|---|
|
|
1819
|
+
| **Required** | no |
|
|
1820
|
+
| **Type** | integer |
|
|
1821
|
+
|
|
1822
|
+
Limits the number of returned records. Combine with `order` for "top N" queries.
|
|
1823
|
+
|
|
1824
|
+
#### `type`
|
|
1825
|
+
|
|
1826
|
+
| | |
|
|
1827
|
+
|---|---|
|
|
1828
|
+
| **Required** | no |
|
|
1829
|
+
| **Allowed** | `custom`, `parameterized` |
|
|
1830
|
+
|
|
1831
|
+
- `custom` — the scope is not generated from YAML. Instead, it must be defined in Ruby code via a model extension. The scope entry in YAML serves as documentation and allows it to be referenced from predefined filters.
|
|
1832
|
+
- `parameterized` — the scope accepts typed parameters at runtime. The `parameters` attribute defines the parameter schema (see below). The actual scope must be defined in Ruby — either as a standard AR scope with keyword arguments, or as a `filter_*` interceptor method.
|
|
1833
|
+
|
|
1834
|
+
#### `parameters`
|
|
1835
|
+
|
|
1836
|
+
| | |
|
|
1837
|
+
|---|---|
|
|
1838
|
+
| **Required** | only when `type: parameterized` |
|
|
1839
|
+
| **Type** | array of parameter definitions |
|
|
1840
|
+
|
|
1841
|
+
Each parameter has:
|
|
1842
|
+
|
|
1843
|
+
| Key | Type | Required | Description |
|
|
1844
|
+
|-----|------|----------|-------------|
|
|
1845
|
+
| `name` | string | yes | Parameter name (used as keyword argument) |
|
|
1846
|
+
| `type` | string | yes | One of: `boolean`, `string`, `integer`, `float`, `enum`, `date`, `datetime`, `model_select` |
|
|
1847
|
+
| `default` | any | no | Default value when the parameter is not provided |
|
|
1848
|
+
| `required` | boolean | no | Whether the parameter must be provided (default: `false`). If a required parameter is missing, the scope is skipped |
|
|
1849
|
+
| `min` | number | no | Minimum value (integer/float only). Values below this are clamped |
|
|
1850
|
+
| `max` | number | no | Maximum value (integer/float only). Values above this are clamped |
|
|
1851
|
+
| `values` | array | no | Allowed values (enum type only). Values not in the list are rejected |
|
|
1852
|
+
| `model` | string | no | Target model name (model_select only) |
|
|
1853
|
+
| `display_field` | string | no | Field to display in the select dropdown (model_select only) |
|
|
1854
|
+
|
|
1855
|
+
### Parameterized Scopes
|
|
1856
|
+
|
|
1857
|
+
Parameterized scopes let users configure scope arguments at runtime. Parameters are cast, validated, and clamped before being passed to the scope.
|
|
1858
|
+
|
|
1859
|
+
```yaml
|
|
1860
|
+
scopes:
|
|
1861
|
+
- name: created_recently
|
|
1862
|
+
type: parameterized
|
|
1863
|
+
parameters:
|
|
1864
|
+
- name: days
|
|
1865
|
+
type: integer
|
|
1866
|
+
default: 30
|
|
1867
|
+
min: 1
|
|
1868
|
+
max: 365
|
|
1869
|
+
|
|
1870
|
+
- name: by_min_price
|
|
1871
|
+
type: parameterized
|
|
1872
|
+
parameters:
|
|
1873
|
+
- name: min_price
|
|
1874
|
+
type: float
|
|
1875
|
+
default: 0.0
|
|
1876
|
+
min: 0
|
|
1877
|
+
|
|
1878
|
+
- name: by_status_filter
|
|
1879
|
+
type: parameterized
|
|
1880
|
+
parameters:
|
|
1881
|
+
- name: status
|
|
1882
|
+
type: enum
|
|
1883
|
+
values: [draft, published, archived]
|
|
1884
|
+
required: true
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
The Ruby implementation can be either a standard scope or a `filter_*` interceptor:
|
|
1888
|
+
|
|
1889
|
+
```ruby
|
|
1890
|
+
# Option A: AR scope with keyword arguments
|
|
1891
|
+
scope :created_recently, ->(days: 30) {
|
|
1892
|
+
where("created_at >= ?", days.to_i.days.ago)
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
# Option B: filter_* interceptor (more flexible)
|
|
1896
|
+
def self.filter_by_min_price(scope, params, evaluator)
|
|
1897
|
+
min = params[:min_price] || 0
|
|
1898
|
+
scope.where("price >= ?", min)
|
|
1899
|
+
end
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
In the [query language](../design/advanced_search.md), parameterized scopes use `@` prefix syntax:
|
|
1903
|
+
|
|
1904
|
+
```
|
|
1905
|
+
@created_recently(days: 7)
|
|
1906
|
+
@by_status_filter(status: 'published')
|
|
1907
|
+
```
|
|
1908
|
+
|
|
1909
|
+
### Combined Scopes
|
|
1910
|
+
|
|
1911
|
+
Scope attributes are additive — a single scope can combine `where`, `where_not`, `order`, and `limit`. They are also independent — any subset is valid (just `order:`, just `limit:`, `where:` + `order:` without `limit:`, etc.). Each attribute is applied only when present, so an order-only scope is `WHERE`-free SQL.
|
|
1912
|
+
|
|
1913
|
+
```yaml
|
|
1914
|
+
scopes:
|
|
1915
|
+
- name: top_open_deals
|
|
1916
|
+
where_not: { stage: [closed_won, closed_lost] }
|
|
1917
|
+
order: { value: desc }
|
|
1918
|
+
limit: 5
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1921
|
+
This generates: `scope :top_open_deals, -> { where.not(stage: [...]).order(value: :desc).limit(5) }`
|
|
1922
|
+
|
|
1923
|
+
### NULL / Empty Value Scopes
|
|
1924
|
+
|
|
1925
|
+
YAML `null` maps to Ruby `nil`, which ActiveRecord translates to `WHERE column IS NULL`. This works in both `where` and `where_not` conditions.
|
|
1926
|
+
|
|
1927
|
+
```yaml
|
|
1928
|
+
scopes:
|
|
1929
|
+
# Records where phone is NULL
|
|
1930
|
+
- name: without_phone
|
|
1931
|
+
where:
|
|
1932
|
+
phone: null
|
|
1933
|
+
|
|
1934
|
+
# Records where phone is NOT NULL
|
|
1935
|
+
- name: with_phone
|
|
1936
|
+
where_not:
|
|
1937
|
+
phone: null
|
|
1938
|
+
|
|
1939
|
+
# Records where phone is NULL or empty string
|
|
1940
|
+
- name: blank_phone
|
|
1941
|
+
where:
|
|
1942
|
+
phone: [null, ""]
|
|
1943
|
+
```
|
|
1944
|
+
|
|
1945
|
+
Generated SQL:
|
|
1946
|
+
|
|
1947
|
+
| Scope | SQL |
|
|
1948
|
+
|-------|-----|
|
|
1949
|
+
| `without_phone` | `WHERE phone IS NULL` |
|
|
1950
|
+
| `with_phone` | `WHERE phone IS NOT NULL` |
|
|
1951
|
+
| `blank_phone` | `WHERE phone IS NULL OR phone = ''` |
|
|
1952
|
+
|
|
1953
|
+
## Events
|
|
1954
|
+
|
|
1955
|
+
Events trigger [event handlers](../guides/event-handlers.md) in response to record changes.
|
|
1956
|
+
|
|
1957
|
+
```yaml
|
|
1958
|
+
events:
|
|
1959
|
+
- name: after_create
|
|
1960
|
+
type: lifecycle
|
|
1961
|
+
- name: on_stage_change
|
|
1962
|
+
type: field_change
|
|
1963
|
+
field: stage
|
|
1964
|
+
condition:
|
|
1965
|
+
field: stage
|
|
1966
|
+
operator: not_in
|
|
1967
|
+
value: [lead]
|
|
1968
|
+
```
|
|
1969
|
+
|
|
1970
|
+
### Event Attributes
|
|
1971
|
+
|
|
1972
|
+
#### `name`
|
|
1973
|
+
|
|
1974
|
+
| | |
|
|
1975
|
+
|---|---|
|
|
1976
|
+
| **Required** | yes |
|
|
1977
|
+
| **Type** | string |
|
|
1978
|
+
|
|
1979
|
+
Event identifier. Matched against `HandlerBase.handles_event` in registered handlers. For lifecycle events, use one of the predefined names.
|
|
1980
|
+
|
|
1981
|
+
#### `type`
|
|
1982
|
+
|
|
1983
|
+
| | |
|
|
1984
|
+
|---|---|
|
|
1985
|
+
| **Required** | no |
|
|
1986
|
+
| **Default** | inferred from `name` |
|
|
1987
|
+
| **Allowed** | `lifecycle`, `field_change` |
|
|
1988
|
+
|
|
1989
|
+
- `lifecycle` — triggered by ActiveRecord callbacks
|
|
1990
|
+
- `field_change` — triggered when a specific field's value changes during an update
|
|
1991
|
+
|
|
1992
|
+
If omitted, the type is inferred: names matching `after_create`, `after_update`, `before_destroy`, or `after_destroy` are treated as lifecycle; all others as field_change.
|
|
1993
|
+
|
|
1994
|
+
#### `field`
|
|
1995
|
+
|
|
1996
|
+
| | |
|
|
1997
|
+
|---|---|
|
|
1998
|
+
| **Required** | yes (for `field_change` type) |
|
|
1999
|
+
| **Type** | string |
|
|
2000
|
+
|
|
2001
|
+
The field to monitor for changes. The event fires only when this field's value changes.
|
|
2002
|
+
|
|
2003
|
+
#### `condition`
|
|
2004
|
+
|
|
2005
|
+
| | |
|
|
2006
|
+
|---|---|
|
|
2007
|
+
| **Required** | no |
|
|
2008
|
+
| **Type** | hash (recommended) or string (deprecated) |
|
|
2009
|
+
|
|
2010
|
+
Optional condition that must be met for the event to fire. Uses the same `{ field:, operator:, value: }` syntax as [condition operators](condition-operators.md). When a `field_change` event fires, the condition is evaluated against the record's current state — the event is only dispatched if the condition returns true.
|
|
2011
|
+
|
|
2012
|
+
```yaml
|
|
2013
|
+
# Hash condition (recommended)
|
|
2014
|
+
events:
|
|
2015
|
+
- name: on_stage_change
|
|
2016
|
+
type: field_change
|
|
2017
|
+
field: stage
|
|
2018
|
+
condition:
|
|
2019
|
+
field: stage
|
|
2020
|
+
operator: not_eq
|
|
2021
|
+
value: draft
|
|
2022
|
+
```
|
|
2023
|
+
|
|
2024
|
+
```ruby
|
|
2025
|
+
# DSL
|
|
2026
|
+
on_field_change :on_stage_change, field: :stage,
|
|
2027
|
+
condition: { field: :stage, operator: :not_in, value: %w[lead] }
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
> **Deprecated:** String conditions (evaluated via `instance_eval`) are deprecated and will be removed in a future version. Migrate to Hash conditions.
|
|
2031
|
+
|
|
2032
|
+
### Lifecycle Events
|
|
2033
|
+
|
|
2034
|
+
| Name | Trigger |
|
|
2035
|
+
|------|---------|
|
|
2036
|
+
| `after_create` | After a new record is created |
|
|
2037
|
+
| `after_update` | After an existing record is updated |
|
|
2038
|
+
| `before_destroy` | Before a record is destroyed |
|
|
2039
|
+
| `after_destroy` | After a record is destroyed |
|
|
2040
|
+
|
|
2041
|
+
## Display Templates
|
|
2042
|
+
|
|
2043
|
+
Display templates define rich HTML representations for records when displayed in contexts like `association_list`. Unlike `to_label` (which remains plain text), display templates support structured layouts with titles, subtitles, icons, and badges — or delegate to custom renderers/partials.
|
|
2044
|
+
|
|
2045
|
+
Templates live on the **model** (not the presenter), so the same record can be rendered consistently across different presenters. The presenter selects which template to use by name.
|
|
2046
|
+
|
|
2047
|
+
### YAML Syntax
|
|
2048
|
+
|
|
2049
|
+
```yaml
|
|
2050
|
+
display_templates:
|
|
2051
|
+
default:
|
|
2052
|
+
template: "{first_name} {last_name}"
|
|
2053
|
+
subtitle: "{position} at {company.name}"
|
|
2054
|
+
icon: user
|
|
2055
|
+
badge: "{status}"
|
|
2056
|
+
compact:
|
|
2057
|
+
template: "{last_name}, {first_name}"
|
|
2058
|
+
card:
|
|
2059
|
+
renderer: ContactCardRenderer
|
|
2060
|
+
mini:
|
|
2061
|
+
partial: "contacts/mini_label"
|
|
2062
|
+
```
|
|
2063
|
+
|
|
2064
|
+
### Three Forms
|
|
2065
|
+
|
|
2066
|
+
| Form | Detected by | Description |
|
|
2067
|
+
|------|-------------|-------------|
|
|
2068
|
+
| **Structured** | `template` key | Title, optional subtitle/icon/badge with `{field}` interpolation |
|
|
2069
|
+
| **Renderer** | `renderer` key | Delegates to a registered `Display::BaseRenderer` subclass |
|
|
2070
|
+
| **Partial** | `partial` key | Renders a Rails partial with `record` local |
|
|
2071
|
+
|
|
2072
|
+
### Structured Template Keys
|
|
2073
|
+
|
|
2074
|
+
| Key | Type | Description |
|
|
2075
|
+
|-----|------|-------------|
|
|
2076
|
+
| `template` | string | **Required.** Main text with `{field}` placeholders |
|
|
2077
|
+
| `subtitle` | string | Secondary text below the title |
|
|
2078
|
+
| `icon` | string | Icon identifier (rendered as text; style via CSS) |
|
|
2079
|
+
| `badge` | string | Small label, supports `{field}` placeholders |
|
|
2080
|
+
|
|
2081
|
+
Field placeholders use the same dot-path syntax as presenter fields: `{field_name}` for direct fields, `{association.field}` for related records. The exact same `{field}` micro-syntax is also accepted by the [`computed`](#computed) field attribute when used in template form — both go through the same parser.
|
|
2082
|
+
|
|
2083
|
+
### Renderer Form
|
|
2084
|
+
|
|
2085
|
+
| Key | Type | Description |
|
|
2086
|
+
|-----|------|-------------|
|
|
2087
|
+
| `renderer` | string | Class name of a registered `Display::BaseRenderer` |
|
|
2088
|
+
| `options` | hash | Passed to the renderer's `render` method |
|
|
2089
|
+
|
|
2090
|
+
### Partial Form
|
|
2091
|
+
|
|
2092
|
+
| Key | Type | Description |
|
|
2093
|
+
|-----|------|-------------|
|
|
2094
|
+
| `partial` | string | Rails partial path (e.g., `"contacts/mini_label"`) |
|
|
2095
|
+
|
|
2096
|
+
### DSL Syntax
|
|
2097
|
+
|
|
2098
|
+
```ruby
|
|
2099
|
+
define_model :contact do
|
|
2100
|
+
display_template :default,
|
|
2101
|
+
template: "{first_name} {last_name}",
|
|
2102
|
+
subtitle: "{position} at {company.name}",
|
|
2103
|
+
icon: "user"
|
|
2104
|
+
|
|
2105
|
+
display_template :card, renderer: "ContactCardRenderer"
|
|
2106
|
+
display_template :mini, partial: "contacts/mini_label"
|
|
2107
|
+
end
|
|
2108
|
+
```
|
|
2109
|
+
|
|
2110
|
+
### Permission Filtering
|
|
2111
|
+
|
|
2112
|
+
Fields referenced in templates are resolved through `FieldValueResolver`, which respects the current user's `PermissionEvaluator`. If a field is not readable, it renders as blank rather than exposing unauthorized data.
|
|
2113
|
+
|
|
2114
|
+
### Eager Loading
|
|
2115
|
+
|
|
2116
|
+
The `IncludesResolver` automatically detects dot-path fields in display templates (e.g., `{company.name}`) and generates nested eager loading (e.g., `{ contacts: :company }`) to prevent N+1 queries.
|
|
2117
|
+
|
|
2118
|
+
## Virtual Columns
|
|
2119
|
+
|
|
2120
|
+
Virtual computed columns that are not stored in the database — they are calculated at query time and injected into SELECT statements. Virtual columns can be referenced in presenter table columns and show sections just like regular fields. They support sorting, conditional rendering (`visible_when`, `item_classes`), and type coercion.
|
|
2121
|
+
|
|
2122
|
+
Virtual column names must not collide with field names on the same model.
|
|
2123
|
+
|
|
2124
|
+
The `virtual_columns` key is the unified replacement for the older `aggregates` key. Both keys are accepted and merged — see [Backward Compatibility](#backward-compatibility) below.
|
|
2125
|
+
|
|
2126
|
+
### YAML Syntax
|
|
2127
|
+
|
|
2128
|
+
```yaml
|
|
2129
|
+
virtual_columns:
|
|
2130
|
+
# Declarative aggregate (same as before)
|
|
2131
|
+
issues_count:
|
|
2132
|
+
function: count
|
|
2133
|
+
association: issues
|
|
2134
|
+
|
|
2135
|
+
open_issues_count:
|
|
2136
|
+
function: count
|
|
2137
|
+
association: issues
|
|
2138
|
+
where: { status: open }
|
|
2139
|
+
|
|
2140
|
+
total_revenue:
|
|
2141
|
+
function: sum
|
|
2142
|
+
association: orders
|
|
2143
|
+
source_field: amount
|
|
2144
|
+
default: 0
|
|
2145
|
+
|
|
2146
|
+
# Expression — inline SQL expression (replaces "sql" key)
|
|
2147
|
+
is_overdue:
|
|
2148
|
+
expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
|
|
2149
|
+
type: boolean
|
|
2150
|
+
|
|
2151
|
+
# Expression with JOIN — pull data from another table
|
|
2152
|
+
company_name:
|
|
2153
|
+
expression: "companies.name"
|
|
2154
|
+
join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
|
|
2155
|
+
type: string
|
|
2156
|
+
|
|
2157
|
+
# Expression with JOIN + GROUP BY — aggregate across a joined table
|
|
2158
|
+
total_value:
|
|
2159
|
+
expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)"
|
|
2160
|
+
join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id"
|
|
2161
|
+
group: true
|
|
2162
|
+
type: decimal
|
|
2163
|
+
|
|
2164
|
+
# Auto-include — always loaded, even when not explicitly referenced
|
|
2165
|
+
priority_score:
|
|
2166
|
+
expression: "(%{table}.urgency * 10 + %{table}.impact * 5)"
|
|
2167
|
+
type: integer
|
|
2168
|
+
auto_include: true
|
|
2169
|
+
|
|
2170
|
+
# Service — Ruby service class
|
|
2171
|
+
health_score:
|
|
2172
|
+
service: project_health
|
|
2173
|
+
type: integer
|
|
2174
|
+
```
|
|
2175
|
+
|
|
2176
|
+
### DSL Syntax
|
|
2177
|
+
|
|
2178
|
+
```ruby
|
|
2179
|
+
virtual_column :issues_count, function: :count, association: :issues
|
|
2180
|
+
virtual_column :open_issues_count, function: :count, association: :issues,
|
|
2181
|
+
where: { status: "open" }
|
|
2182
|
+
virtual_column :total_revenue, function: :sum, association: :orders,
|
|
2183
|
+
source_field: :amount, default: 0
|
|
2184
|
+
virtual_column :is_overdue,
|
|
2185
|
+
expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END",
|
|
2186
|
+
type: :boolean
|
|
2187
|
+
virtual_column :company_name,
|
|
2188
|
+
expression: "companies.name",
|
|
2189
|
+
join: "LEFT JOIN companies ON companies.id = %{table}.company_id",
|
|
2190
|
+
type: :string
|
|
2191
|
+
virtual_column :total_value,
|
|
2192
|
+
expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)",
|
|
2193
|
+
join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id",
|
|
2194
|
+
group: true,
|
|
2195
|
+
type: :decimal
|
|
2196
|
+
|
|
2197
|
+
# Legacy alias still works
|
|
2198
|
+
aggregate :contacts_count, function: :count, association: :contacts
|
|
2199
|
+
```
|
|
2200
|
+
|
|
2201
|
+
### Four Virtual Column Types
|
|
2202
|
+
|
|
2203
|
+
| Type | Detected by | Description |
|
|
2204
|
+
|------|-------------|-------------|
|
|
2205
|
+
| **Declarative** | `function` + `association` | SQL aggregate function over a has_many association (correlated subquery) |
|
|
2206
|
+
| **Expression** | `expression` key | Inline SQL expression injected into SELECT |
|
|
2207
|
+
| **Expression + JOIN** | `expression` + `join` | SQL expression that requires a JOIN clause |
|
|
2208
|
+
| **Service** | `service` key | Ruby service class from `app/lcp_services/virtual_columns/` (or `aggregates/`) |
|
|
2209
|
+
|
|
2210
|
+
### Declarative Attributes
|
|
2211
|
+
|
|
2212
|
+
| Attribute | Type | Required | Description |
|
|
2213
|
+
|-----------|------|----------|-------------|
|
|
2214
|
+
| `function` | string | yes | SQL aggregate function: `count`, `sum`, `min`, `max`, `avg` |
|
|
2215
|
+
| `association` | string | yes | Name of a `has_many` association on the model |
|
|
2216
|
+
| `source_field` | string | for sum/min/max/avg | Field on the target model to aggregate. Optional for `count` (defaults to `*`) |
|
|
2217
|
+
| `where` | hash | no | Equality conditions on the target model (see [Where Conditions](#where-conditions)) |
|
|
2218
|
+
| `distinct` | boolean | no | Use `DISTINCT` in the aggregate function (default: `false`) |
|
|
2219
|
+
| `default` | any | no | Default value via `COALESCE`. `count` always defaults to `0` even without this |
|
|
2220
|
+
| `include_discarded` | boolean | no | Include soft-deleted records (default: `false`). Only relevant when the target model uses `soft_delete` |
|
|
2221
|
+
|
|
2222
|
+
### Expression Attributes
|
|
2223
|
+
|
|
2224
|
+
| Attribute | Type | Required | Description |
|
|
2225
|
+
|-----------|------|----------|-------------|
|
|
2226
|
+
| `expression` | string | yes | SQL expression. Use `%{table}` for the parent model's quoted table name |
|
|
2227
|
+
| `type` | string | yes | Result type: `string`, `integer`, `float`, `decimal`, `boolean`, `date`, `datetime`, `json` |
|
|
2228
|
+
| `default` | any | no | Default value via `COALESCE` |
|
|
2229
|
+
| `join` | string | no | SQL JOIN clause to add. Use `%{table}` for the parent table name |
|
|
2230
|
+
| `group` | boolean | no | Whether this column requires `GROUP BY parent_table.id` (default: `false`) |
|
|
2231
|
+
| `auto_include` | boolean | no | Always include this column in queries, even when not referenced by the presenter (default: `false`) |
|
|
2232
|
+
|
|
2233
|
+
**Notes:**
|
|
2234
|
+
- `auto_include: true` and `group: true` cannot be combined on the same column.
|
|
2235
|
+
- `auto_include: true` only affects controller queries. Direct model queries (`Model.find`, `Model.where`) from custom actions, event handlers, or other Ruby code do not include virtual columns — use `VirtualColumns::Builder.apply` explicitly.
|
|
2236
|
+
- The `join` attribute is a single string, not an array. Concatenate multiple JOINs in one string if needed.
|
|
2237
|
+
|
|
2238
|
+
The legacy `sql` key is accepted as an alias for `expression`. You cannot specify both `sql` and `expression` on the same column.
|
|
2239
|
+
|
|
2240
|
+
### Service Attributes
|
|
2241
|
+
|
|
2242
|
+
| Attribute | Type | Required | Description |
|
|
2243
|
+
|-----------|------|----------|-------------|
|
|
2244
|
+
| `service` | string | yes | Service key looked up in `app/lcp_services/virtual_columns/` (falls back to `app/lcp_services/aggregates/`) |
|
|
2245
|
+
| `type` | string | yes | Result type |
|
|
2246
|
+
| `options` | hash | no | Options hash passed to the service's `call` method |
|
|
2247
|
+
|
|
2248
|
+
The service class must implement `self.call(record, options:)`. Optionally implement `self.sql_expression(model_class, options:)` to return a SQL string — this enables sorting and avoids per-record evaluation.
|
|
2249
|
+
|
|
2250
|
+
**Note:** Service virtual columns without `sql_expression` are only resolved on show/edit pages (per-record via `call`). On index pages, they return `nil` — if referenced in `table_columns` or `item_classes`, the display will be empty and conditions will evaluate incorrectly.
|
|
2251
|
+
|
|
2252
|
+
### Expression Details
|
|
2253
|
+
|
|
2254
|
+
#### `%{table}` Placeholder
|
|
2255
|
+
|
|
2256
|
+
The `%{table}` placeholder is replaced with the parent model's quoted table name at query time. Always use it to reference the parent table:
|
|
2257
|
+
|
|
2258
|
+
```yaml
|
|
2259
|
+
# Good — portable across table names
|
|
2260
|
+
expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
|
|
2261
|
+
|
|
2262
|
+
# Bad — hardcoded table name
|
|
2263
|
+
expression: "CASE WHEN orders.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
|
|
2264
|
+
```
|
|
2265
|
+
|
|
2266
|
+
#### JOIN Clause
|
|
2267
|
+
|
|
2268
|
+
The `join` attribute adds a SQL JOIN to the query. Multiple virtual columns can specify JOINs — duplicates are automatically deduplicated (compared case-insensitively after whitespace normalization):
|
|
2269
|
+
|
|
2270
|
+
```yaml
|
|
2271
|
+
virtual_columns:
|
|
2272
|
+
company_name:
|
|
2273
|
+
expression: "companies.name"
|
|
2274
|
+
join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
|
|
2275
|
+
type: string
|
|
2276
|
+
company_country:
|
|
2277
|
+
expression: "companies.country"
|
|
2278
|
+
join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
|
|
2279
|
+
type: string
|
|
2280
|
+
```
|
|
2281
|
+
|
|
2282
|
+
Both columns reference the same JOIN — it is applied only once.
|
|
2283
|
+
|
|
2284
|
+
#### GROUP BY
|
|
2285
|
+
|
|
2286
|
+
When `group: true` is set, the query adds `GROUP BY parent_table.id`. This is needed when the expression uses aggregate functions over joined rows:
|
|
2287
|
+
|
|
2288
|
+
```yaml
|
|
2289
|
+
total_value:
|
|
2290
|
+
expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)"
|
|
2291
|
+
join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id"
|
|
2292
|
+
group: true
|
|
2293
|
+
type: decimal
|
|
2294
|
+
```
|
|
2295
|
+
|
|
2296
|
+
**Caution:** Multiple JOINs combined with `group: true` can produce cartesian products (e.g., 3 line_items × 2 payments = 6 rows per order, inflating SUM results). When aggregating over multiple joined tables, prefer correlated subqueries in `expression:` instead of `join:` + `group:`:
|
|
2297
|
+
|
|
2298
|
+
```yaml
|
|
2299
|
+
# Good — independent correlated subqueries, no cartesian product
|
|
2300
|
+
total_line_value:
|
|
2301
|
+
expression: "(SELECT COALESCE(SUM(li.quantity * li.unit_price), 0) FROM line_items li WHERE li.order_id = %{table}.id)"
|
|
2302
|
+
type: decimal
|
|
2303
|
+
total_payments:
|
|
2304
|
+
expression: "(SELECT COALESCE(SUM(p.amount), 0) FROM payments p WHERE p.order_id = %{table}.id)"
|
|
2305
|
+
type: decimal
|
|
2306
|
+
|
|
2307
|
+
# Avoid — two joins with group = cartesian risk
|
|
2308
|
+
```
|
|
2309
|
+
|
|
2310
|
+
Reserve `join:` + `group: true` for cases where a single JOIN + GROUP is genuinely needed.
|
|
2311
|
+
|
|
2312
|
+
#### Auto-Include
|
|
2313
|
+
|
|
2314
|
+
Columns with `auto_include: true` are always loaded in every query context (index, show, edit) regardless of whether the presenter references them. This is useful for columns needed by conditional rendering or business logic:
|
|
2315
|
+
|
|
2316
|
+
```yaml
|
|
2317
|
+
priority_score:
|
|
2318
|
+
expression: "(%{table}.urgency * 10 + %{table}.impact * 5)"
|
|
2319
|
+
type: integer
|
|
2320
|
+
auto_include: true
|
|
2321
|
+
```
|
|
2322
|
+
|
|
2323
|
+
### Where Conditions
|
|
2324
|
+
|
|
2325
|
+
The `where` hash applies equality conditions to **declarative** aggregate subqueries only. Expression columns embed their own WHERE conditions directly in the SQL string.
|
|
2326
|
+
|
|
2327
|
+
```yaml
|
|
2328
|
+
where: { status: open } # WHERE status = 'open'
|
|
2329
|
+
where: { status: [open, in_progress] } # WHERE status IN ('open', 'in_progress')
|
|
2330
|
+
where: { deleted_at: null } # WHERE deleted_at IS NULL
|
|
2331
|
+
where: { status: active, priority: high } # WHERE status = 'active' AND priority = 'high'
|
|
2332
|
+
where: { assignee_id: :current_user } # WHERE assignee_id = <current_user.id>
|
|
2333
|
+
```
|
|
2334
|
+
|
|
2335
|
+
The `:current_user` placeholder resolves to `current_user.id` at query time. When no user is signed in, it resolves to `nil` (producing `IS NULL`). This enables per-user aggregates like "my open issues count".
|
|
2336
|
+
|
|
2337
|
+
### Type Inference
|
|
2338
|
+
|
|
2339
|
+
Declarative aggregates infer their result type automatically:
|
|
2340
|
+
|
|
2341
|
+
| Function | Inferred type |
|
|
2342
|
+
|----------|---------------|
|
|
2343
|
+
| `count` | `integer` (always) |
|
|
2344
|
+
| `sum`, `min`, `max` | Same as `source_field` type |
|
|
2345
|
+
| `avg` | `float` (or `decimal` if source is `decimal`) |
|
|
2346
|
+
|
|
2347
|
+
Expression and service virtual columns require an explicit `type` attribute.
|
|
2348
|
+
|
|
2349
|
+
### Type Coercion
|
|
2350
|
+
|
|
2351
|
+
Virtual columns register an ActiveRecord `attribute` declaration at boot time, ensuring consistent Ruby types regardless of database adapter. For example, SQLite returns `0`/`1` for booleans while PostgreSQL returns `true`/`false` — the `attribute :is_overdue, :boolean` declaration normalizes both to Ruby `true`/`false`.
|
|
2352
|
+
|
|
2353
|
+
| Virtual column `type` | Ruby class |
|
|
2354
|
+
|----------------------|------------|
|
|
2355
|
+
| `string` | `String` |
|
|
2356
|
+
| `integer` | `Integer` |
|
|
2357
|
+
| `float` | `Float` |
|
|
2358
|
+
| `decimal` | `BigDecimal` |
|
|
2359
|
+
| `boolean` | `TrueClass`/`FalseClass` |
|
|
2360
|
+
| `date` | `Date` |
|
|
2361
|
+
| `datetime` | `Time` |
|
|
2362
|
+
| `text` | `String` |
|
|
2363
|
+
|
|
2364
|
+
**NULL safety for booleans:** SQL boolean expressions like `(due_date < CURRENT_DATE AND status != 'done')` return NULL (not FALSE) when any operand is NULL (e.g., `due_date` is NULL). After type coercion, `nil` ≠ `false` — a condition `{ operator: eq, value: false }` would not match `nil`. Use `default: false` on boolean virtual columns that use arithmetic/comparison operators. Exception: `EXISTS(...)` always returns TRUE/FALSE (never NULL), so `default:` is not needed for EXISTS expressions.
|
|
2365
|
+
|
|
2366
|
+
### Soft Delete Awareness
|
|
2367
|
+
|
|
2368
|
+
When the target model uses `soft_delete`, **declarative** aggregates automatically exclude soft-deleted records (`WHERE discarded_at IS NULL`). Set `include_discarded: true` to include them.
|
|
2369
|
+
|
|
2370
|
+
For **expression** columns with correlated subqueries or JOINs referencing a soft-deletable model, you must filter discarded records manually in the SQL (e.g., `AND discarded_at IS NULL`). The platform does not inject soft-delete conditions into raw SQL expressions.
|
|
2371
|
+
|
|
2372
|
+
### Presenter Usage
|
|
2373
|
+
|
|
2374
|
+
Virtual columns are referenced in presenters as regular fields:
|
|
2375
|
+
|
|
2376
|
+
```yaml
|
|
2377
|
+
# Index — sortable virtual column
|
|
2378
|
+
table_columns:
|
|
2379
|
+
- { field: issues_count, sortable: true }
|
|
2380
|
+
- { field: total_revenue, renderer: currency, sortable: true }
|
|
2381
|
+
- { field: is_overdue }
|
|
2382
|
+
- { field: company_name }
|
|
2383
|
+
|
|
2384
|
+
# Show — in a section
|
|
2385
|
+
layout:
|
|
2386
|
+
- section: "Statistics"
|
|
2387
|
+
fields:
|
|
2388
|
+
- { field: issues_count }
|
|
2389
|
+
- { field: total_revenue, renderer: currency }
|
|
2390
|
+
- { field: company_name }
|
|
2391
|
+
```
|
|
2392
|
+
|
|
2393
|
+
Virtual columns are visible to all roles regardless of field permissions — they are computed values, not stored data.
|
|
2394
|
+
|
|
2395
|
+
#### Explicit Virtual Column Lists
|
|
2396
|
+
|
|
2397
|
+
Presenters can explicitly list virtual columns to include, beyond what is auto-detected from field references:
|
|
2398
|
+
|
|
2399
|
+
```yaml
|
|
2400
|
+
# In index config
|
|
2401
|
+
index:
|
|
2402
|
+
table_columns:
|
|
2403
|
+
- { field: title }
|
|
2404
|
+
virtual_columns: [total_value, is_overdue]
|
|
2405
|
+
|
|
2406
|
+
# In show config
|
|
2407
|
+
show:
|
|
2408
|
+
virtual_columns: [total_value]
|
|
2409
|
+
layout:
|
|
2410
|
+
- section: "Details"
|
|
2411
|
+
fields:
|
|
2412
|
+
- { field: title }
|
|
2413
|
+
```
|
|
2414
|
+
|
|
2415
|
+
### Auto-Detection (Collector)
|
|
2416
|
+
|
|
2417
|
+
The system automatically detects which virtual columns are needed for each page context by scanning:
|
|
2418
|
+
|
|
2419
|
+
| Source | Context |
|
|
2420
|
+
|--------|---------|
|
|
2421
|
+
| `table_columns` field names | `:index` |
|
|
2422
|
+
| `tile.fields` field names | `:index` |
|
|
2423
|
+
| `item_classes[].when` conditions (recursive through `all`/`any`/`not`) | `:index` |
|
|
2424
|
+
| Permission `record_rules[].when` conditions (all roles, recursive) | `:index` |
|
|
2425
|
+
| Action `visible_when` / `disable_when` conditions (single, collection, batch) | `:index`, `:show`, `:edit` |
|
|
2426
|
+
| Show `layout` field names | `:show` |
|
|
2427
|
+
| Form field/section `visible_when` / `disable_when` conditions | `:edit` |
|
|
2428
|
+
| Explicit `virtual_columns` lists | per context |
|
|
2429
|
+
| `auto_include: true` columns | all contexts |
|
|
2430
|
+
| Sort params at runtime (`?sort=field_name`) | `:index` (dynamic) |
|
|
2431
|
+
|
|
2432
|
+
This means you don't need to manually list virtual columns in most cases — referencing a virtual column in a table column or condition is sufficient.
|
|
2433
|
+
|
|
2434
|
+
**Note:** `record_rules` auto-detection scans all roles (not just the current user's role) to collect virtual column names. Including an unused virtual column in SELECT is harmless, while missing one would cause a runtime error.
|
|
2435
|
+
|
|
2436
|
+
### Backward Compatibility
|
|
2437
|
+
|
|
2438
|
+
The `aggregates` YAML key continues to work and is merged with `virtual_columns`:
|
|
2439
|
+
|
|
2440
|
+
```yaml
|
|
2441
|
+
# Both keys accepted — merged into a single set
|
|
2442
|
+
aggregates:
|
|
2443
|
+
issues_count:
|
|
2444
|
+
function: count
|
|
2445
|
+
association: issues
|
|
2446
|
+
|
|
2447
|
+
virtual_columns:
|
|
2448
|
+
is_overdue:
|
|
2449
|
+
expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
|
|
2450
|
+
type: boolean
|
|
2451
|
+
```
|
|
2452
|
+
|
|
2453
|
+
A name collision between the two keys raises a `MetadataError`.
|
|
2454
|
+
|
|
2455
|
+
The Ruby API also preserves backward compatibility:
|
|
2456
|
+
|
|
2457
|
+
| Legacy method | Aliased to |
|
|
2458
|
+
|---------------|------------|
|
|
2459
|
+
| `model_def.aggregates` | `model_def.virtual_columns` |
|
|
2460
|
+
| `model_def.aggregate(name)` | `model_def.virtual_column(name)` |
|
|
2461
|
+
| `model_def.aggregate_names` | `model_def.virtual_column_names` |
|
|
2462
|
+
| DSL `aggregate(...)` | DSL `virtual_column(...)` |
|
|
2463
|
+
|
|
2464
|
+
The legacy `sql` key in YAML is accepted as an alias for `expression`.
|
|
2465
|
+
|
|
2466
|
+
### Limitations
|
|
2467
|
+
|
|
2468
|
+
- **Not filterable via Ransack** — Virtual columns are SQL aliases, not real database columns. They are not added to `ransackable_attributes` and cannot be used in the advanced filter builder or saved filters. To filter by a virtual column value, use a custom scope.
|
|
2469
|
+
- **Not in summary bar / column summaries** — Both `summary_bar` and column-level `summary` operate on real database columns via `scope.sum(field)` etc. Virtual column names are silently skipped.
|
|
2470
|
+
- **Permission scopes cannot reference virtual columns** — `ScopeBuilder`'s `where` and `field_match` types use `.where()` which requires real columns. Use `type: custom` with a named scope that embeds the SQL expression.
|
|
2471
|
+
- **Read-only in forms** — Virtual columns are computed values and never appear in `permitted_params`. If referenced in a form layout, they render as read-only display fields.
|
|
2472
|
+
- **DB portability is the configurator's responsibility** — The platform does not abstract SQL differences between PostgreSQL and SQLite in `expression:` and `join:` strings. For DB-portable logic, use service virtual columns with Arel.
|
|
2473
|
+
- **Security** — `expression:` and `join:` values are defined in YAML/DSL by the platform configurator and are not user input. The `%{table}` placeholder is resolved to a properly quoted table name. No user-supplied values are interpolated into these strings.
|
|
2474
|
+
|
|
2475
|
+
### Complete Example
|
|
2476
|
+
|
|
2477
|
+
```yaml
|
|
2478
|
+
model:
|
|
2479
|
+
name: order
|
|
2480
|
+
fields:
|
|
2481
|
+
- { name: title, type: string }
|
|
2482
|
+
- { name: status, type: string }
|
|
2483
|
+
- { name: due_date, type: date }
|
|
2484
|
+
associations:
|
|
2485
|
+
- { type: has_many, name: line_items, target_model: line_item, foreign_key: order_id }
|
|
2486
|
+
- { type: belongs_to, name: company, target_model: company, foreign_key: company_id, required: false }
|
|
2487
|
+
virtual_columns:
|
|
2488
|
+
items_count:
|
|
2489
|
+
function: count
|
|
2490
|
+
association: line_items
|
|
2491
|
+
total_value:
|
|
2492
|
+
expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)"
|
|
2493
|
+
join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id"
|
|
2494
|
+
group: true
|
|
2495
|
+
type: decimal
|
|
2496
|
+
is_overdue:
|
|
2497
|
+
expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
|
|
2498
|
+
type: boolean
|
|
2499
|
+
company_name:
|
|
2500
|
+
expression: "companies.name"
|
|
2501
|
+
join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
|
|
2502
|
+
type: string
|
|
2503
|
+
priority_score:
|
|
2504
|
+
expression: "(%{table}.urgency * 10 + %{table}.impact * 5)"
|
|
2505
|
+
type: integer
|
|
2506
|
+
auto_include: true
|
|
2507
|
+
```
|
|
2508
|
+
|
|
2509
|
+
## Options
|
|
2510
|
+
|
|
2511
|
+
```yaml
|
|
2512
|
+
options:
|
|
2513
|
+
timestamps: true
|
|
2514
|
+
label_method: title
|
|
2515
|
+
sti: true # enable Single Table Inheritance on this model
|
|
2516
|
+
soft_delete: true # or { column: deleted_at }
|
|
2517
|
+
auditing: true # or { only: [title, status], ... }
|
|
2518
|
+
userstamps: true # or { created_by: author_id, updated_by: editor_id, store_name: true }
|
|
2519
|
+
tree: true # or { parent_field: parent_category_id, ... }
|
|
2520
|
+
```
|
|
2521
|
+
|
|
2522
|
+
### `timestamps`
|
|
2523
|
+
|
|
2524
|
+
| | |
|
|
2525
|
+
|---|---|
|
|
2526
|
+
| **Default** | `true` |
|
|
2527
|
+
| **Type** | boolean |
|
|
2528
|
+
|
|
2529
|
+
Adds `created_at` and `updated_at` columns to the table.
|
|
2530
|
+
|
|
2531
|
+
### `label_method`
|
|
2532
|
+
|
|
2533
|
+
| | |
|
|
2534
|
+
|---|---|
|
|
2535
|
+
| **Default** | `"to_s"` |
|
|
2536
|
+
| **Type** | string |
|
|
2537
|
+
|
|
2538
|
+
Method called on records to generate display text (e.g., in association select dropdowns). Should return a human-readable string.
|
|
2539
|
+
|
|
2540
|
+
### `sti`
|
|
2541
|
+
|
|
2542
|
+
| | |
|
|
2543
|
+
|---|---|
|
|
2544
|
+
| **Default** | `false` |
|
|
2545
|
+
| **Type** | boolean |
|
|
2546
|
+
|
|
2547
|
+
Enables Single Table Inheritance (STI) on this model. See [Single Table Inheritance](#single-table-inheritance-sti) for details.
|
|
2548
|
+
|
|
2549
|
+
### `custom_fields`
|
|
2550
|
+
|
|
2551
|
+
| | |
|
|
2552
|
+
|---|---|
|
|
2553
|
+
| **Default** | `false` |
|
|
2554
|
+
| **Type** | boolean |
|
|
2555
|
+
|
|
2556
|
+
Enables user-defined custom fields on this model. When `true`, the engine:
|
|
2557
|
+
- Adds a `custom_data` column (JSONB on PostgreSQL, JSON on SQLite) to the model's table
|
|
2558
|
+
- Creates a GIN index on the column (PostgreSQL only)
|
|
2559
|
+
- Installs dynamic accessor methods for each custom field definition
|
|
2560
|
+
- Validates custom field values based on their definition constraints
|
|
2561
|
+
- Auto-generates a management presenter at `/custom-fields-<model_name>`
|
|
2562
|
+
|
|
2563
|
+
Custom field definitions are stored in the built-in `custom_field_definition` model. Access is controlled through the `custom_data` virtual field in permissions.
|
|
2564
|
+
|
|
2565
|
+
```yaml
|
|
2566
|
+
options:
|
|
2567
|
+
custom_fields: true
|
|
2568
|
+
```
|
|
2569
|
+
|
|
2570
|
+
See [Custom Fields Reference](custom-fields.md) for the complete attribute reference and [Custom Fields Guide](../guides/custom-fields.md) for a step-by-step tutorial.
|
|
2571
|
+
|
|
2572
|
+
### `soft_delete`
|
|
2573
|
+
|
|
2574
|
+
| | |
|
|
2575
|
+
|---|---|
|
|
2576
|
+
| **Default** | `false` (disabled) |
|
|
2577
|
+
| **Type** | `true` or Hash |
|
|
2578
|
+
|
|
2579
|
+
Enables soft delete (logical deletion) for this model. Instead of permanently deleting records, the engine sets a timestamp column to mark them as discarded.
|
|
2580
|
+
|
|
2581
|
+
**Simple form** — uses default column `discarded_at`:
|
|
2582
|
+
|
|
2583
|
+
```yaml
|
|
2584
|
+
options:
|
|
2585
|
+
soft_delete: true
|
|
2586
|
+
```
|
|
2587
|
+
|
|
2588
|
+
**Hash form** — custom column name:
|
|
2589
|
+
|
|
2590
|
+
```yaml
|
|
2591
|
+
options:
|
|
2592
|
+
soft_delete:
|
|
2593
|
+
column: deleted_at
|
|
2594
|
+
```
|
|
2595
|
+
|
|
2596
|
+
| Key | Type | Default | Description |
|
|
2597
|
+
|-----|------|---------|-------------|
|
|
2598
|
+
| `column` | string | `"discarded_at"` | Name of the datetime column that stores the discard timestamp |
|
|
2599
|
+
|
|
2600
|
+
When `soft_delete` is enabled, the engine automatically:
|
|
2601
|
+
|
|
2602
|
+
- Creates the timestamp column (`discarded_at` by default) plus tracking columns (`discarded_by_type`, `discarded_by_id`)
|
|
2603
|
+
- Adds scopes: `kept` (non-discarded), `discarded` (discarded only), `with_discarded` (all records)
|
|
2604
|
+
- Adds instance methods: `discard!`, `undiscard!`, `discarded?`, `kept?`, `cascade_discarded?`
|
|
2605
|
+
- Changes the controller `destroy` action to soft-delete (sets `discarded_at`) instead of hard-delete
|
|
2606
|
+
- Filters discarded records from the default index view (applies `kept` scope)
|
|
2607
|
+
- Supports `dependent: :discard` on `has_many` associations for automatic cascade discard/undiscard
|
|
2608
|
+
|
|
2609
|
+
The `discard!(by:)` method accepts an optional `by:` parameter to track which record triggered the discard (used for cascade tracking). The `undiscard!` method only restores cascade-discarded children — manually discarded records are left as-is.
|
|
2610
|
+
|
|
2611
|
+
#### Cascade behavior with `has_many dependent:`
|
|
2612
|
+
|
|
2613
|
+
`discard!` is **not** the same call path as `destroy!`. ActiveRecord's `dependent:` rules only fire for actual destroys, so the parent's children are handled like this:
|
|
2614
|
+
|
|
2615
|
+
| `dependent:` on `has_many` | Effect when parent is `discard!`-ed |
|
|
2616
|
+
|---|---|
|
|
2617
|
+
| `:discard` | Children are also soft-deleted (cascade tracked — `undiscard!` will restore them). **Both parent and child models must have `soft_delete` enabled.** |
|
|
2618
|
+
| `:destroy`, `:delete`, `:delete_all` | **Not cascaded.** Children remain untouched and become orphans (visible via the parent's `with_discarded` scope only). |
|
|
2619
|
+
| `:nullify` | Not cascaded — children keep their FK. |
|
|
2620
|
+
| `:restrict_with_exception`, `:restrict_with_error` | Not cascaded — children block the **destroy** path, but `discard!` bypasses the check. |
|
|
2621
|
+
| (omitted) | Not cascaded. |
|
|
2622
|
+
|
|
2623
|
+
Mix `:destroy` and `soft_delete` only when you actually want to drop children permanently while keeping the parent recoverable; otherwise prefer `:discard` everywhere in a soft-delete graph for a coherent restore story.
|
|
2624
|
+
|
|
2625
|
+
See [Soft Delete Guide](../guides/soft-delete.md) for setup and usage examples.
|
|
2626
|
+
|
|
2627
|
+
### `auditing`
|
|
2628
|
+
|
|
2629
|
+
| | |
|
|
2630
|
+
|---|---|
|
|
2631
|
+
| **Default** | `false` (disabled) |
|
|
2632
|
+
| **Type** | `true`, `false`, or Hash |
|
|
2633
|
+
|
|
2634
|
+
Enables change auditing for this model. When enabled, all field changes are tracked and stored via the configured `audit_writer`. Set to `false` to explicitly disable (same as omitting the key).
|
|
2635
|
+
|
|
2636
|
+
**Simple form** — tracks all fields:
|
|
2637
|
+
|
|
2638
|
+
```yaml
|
|
2639
|
+
options:
|
|
2640
|
+
auditing: true
|
|
2641
|
+
```
|
|
2642
|
+
|
|
2643
|
+
**Hash form** — fine-grained control:
|
|
2644
|
+
|
|
2645
|
+
```yaml
|
|
2646
|
+
options:
|
|
2647
|
+
auditing:
|
|
2648
|
+
only:
|
|
2649
|
+
- title
|
|
2650
|
+
- status
|
|
2651
|
+
track_associations: true
|
|
2652
|
+
track_attachments: true
|
|
2653
|
+
expand_custom_fields: true
|
|
2654
|
+
expand_json_fields:
|
|
2655
|
+
- addresses
|
|
2656
|
+
```
|
|
2657
|
+
|
|
2658
|
+
| Key | Type | Default | Description |
|
|
2659
|
+
|-----|------|---------|-------------|
|
|
2660
|
+
| `only` | array of strings | all fields | Track only these fields. Mutually exclusive with `ignore`. |
|
|
2661
|
+
| `ignore` | array of strings | none | Track all fields except these. Mutually exclusive with `only`. |
|
|
2662
|
+
| `track_associations` | boolean | `true` | Include nested_attributes child changes in parent audit entry |
|
|
2663
|
+
| `track_attachments` | boolean | `false` | Include attachment changes in audit trail (reserved, not yet implemented) |
|
|
2664
|
+
| `expand_custom_fields` | boolean | `true` | Expand `custom_data` JSON into individual `cf:` prefixed field changes |
|
|
2665
|
+
| `expand_json_fields` | array of strings | `[]` | JSON columns to expand into dot-path key changes |
|
|
2666
|
+
|
|
2667
|
+
> **Note:** `only` and `ignore` are mutually exclusive — specifying both causes a validation error.
|
|
2668
|
+
|
|
2669
|
+
See [Auditing Reference](auditing.md) for the audit log model contract, changes data format, and configuration options. See [Auditing Guide](../guides/auditing.md) for setup and usage examples.
|
|
2670
|
+
|
|
2671
|
+
### `userstamps`
|
|
2672
|
+
|
|
2673
|
+
| | |
|
|
2674
|
+
|---|---|
|
|
2675
|
+
| **Default** | `false` (disabled) |
|
|
2676
|
+
| **Type** | `true` or Hash |
|
|
2677
|
+
|
|
2678
|
+
Enables automatic user tracking — stores the ID of the user who created and last updated each record via a `before_save` callback that reads from `LcpRuby::Current.user`.
|
|
2679
|
+
|
|
2680
|
+
**Simple form** — uses default columns `created_by_id` and `updated_by_id`:
|
|
2681
|
+
|
|
2682
|
+
```yaml
|
|
2683
|
+
options:
|
|
2684
|
+
userstamps: true
|
|
2685
|
+
```
|
|
2686
|
+
|
|
2687
|
+
**Hash form** — custom column names and name snapshots:
|
|
2688
|
+
|
|
2689
|
+
```yaml
|
|
2690
|
+
options:
|
|
2691
|
+
userstamps:
|
|
2692
|
+
created_by: author_id
|
|
2693
|
+
updated_by: editor_id
|
|
2694
|
+
store_name: true
|
|
2695
|
+
```
|
|
2696
|
+
|
|
2697
|
+
| Key | Type | Default | Description |
|
|
2698
|
+
|-----|------|---------|-------------|
|
|
2699
|
+
| `created_by` | string | `"created_by_id"` | Column name for the creating user's FK |
|
|
2700
|
+
| `updated_by` | string | `"updated_by_id"` | Column name for the last updating user's FK |
|
|
2701
|
+
| `store_name` | boolean | `false` | Add denormalized `_name` snapshot columns (e.g., `created_by_name`, `updated_by_name`) |
|
|
2702
|
+
|
|
2703
|
+
The applicator automatically:
|
|
2704
|
+
- Adds `bigint` columns (nullable, indexed) for creator and updater FK
|
|
2705
|
+
- Adds `string` columns for name snapshots when `store_name: true`
|
|
2706
|
+
- Adds `belongs_to` associations pointing to `LcpRuby.configuration.user_class`
|
|
2707
|
+
- Sets `created_by_id` only on `new_record?`, `updated_by_id` on every save
|
|
2708
|
+
- Writes `nil` when `LcpRuby::Current.user` is not set (seeds, jobs, console)
|
|
2709
|
+
- `update_columns` bypasses the callback by design (same as Rails timestamps)
|
|
2710
|
+
|
|
2711
|
+
### `tree`
|
|
2712
|
+
|
|
2713
|
+
| | |
|
|
2714
|
+
|---|---|
|
|
2715
|
+
| **Default** | `false` (disabled) |
|
|
2716
|
+
| **Type** | `true` or Hash |
|
|
2717
|
+
|
|
2718
|
+
Enables tree/hierarchy structure for this model. The model becomes self-referencing with parent-child relationships, enabling nested structures like categories, organizational units, or threaded comments.
|
|
2719
|
+
|
|
2720
|
+
**Simple form** — uses defaults:
|
|
2721
|
+
|
|
2722
|
+
```yaml
|
|
2723
|
+
options:
|
|
2724
|
+
tree: true
|
|
2725
|
+
```
|
|
2726
|
+
|
|
2727
|
+
**Hash form** — custom configuration:
|
|
2728
|
+
|
|
2729
|
+
```yaml
|
|
2730
|
+
options:
|
|
2731
|
+
tree:
|
|
2732
|
+
parent_field: parent_category_id
|
|
2733
|
+
parent_name: parent_category
|
|
2734
|
+
children_name: subcategories
|
|
2735
|
+
dependent: nullify
|
|
2736
|
+
max_depth: 5
|
|
2737
|
+
ordered: true
|
|
2738
|
+
position_field: position
|
|
2739
|
+
```
|
|
2740
|
+
|
|
2741
|
+
| Key | Type | Default | Description |
|
|
2742
|
+
|-----|------|---------|-------------|
|
|
2743
|
+
| `parent_field` | string | `"parent_id"` | Foreign key column for the parent record |
|
|
2744
|
+
| `parent_name` | string | `"parent"` | Name of the `belongs_to` parent association |
|
|
2745
|
+
| `children_name` | string | `"children"` | Name of the `has_many` children association |
|
|
2746
|
+
| `dependent` | string | `"destroy"` | What happens to children when parent is deleted. Values: `destroy`, `nullify`, `restrict_with_exception`, `restrict_with_error`, `discard` (requires soft_delete) |
|
|
2747
|
+
| `max_depth` | integer | `10` | Maximum allowed tree depth. Enforced by cycle detection validation |
|
|
2748
|
+
| `ordered` | boolean | `false` | Enable position-based ordering of siblings. Automatically configures `positioning` scoped to parent |
|
|
2749
|
+
| `position_field` | string | `"position"` | Column name for sibling ordering (only used when `ordered: true`) |
|
|
2750
|
+
|
|
2751
|
+
The `TreeApplicator` automatically:
|
|
2752
|
+
- Creates `belongs_to` / `has_many` self-referential associations
|
|
2753
|
+
- Adds `roots` and `leaves` scopes
|
|
2754
|
+
- Adds traversal instance methods: `root?`, `leaf?`, `ancestors`, `descendants`, `subtree`, `subtree_ids`, `siblings`, `depth`, `path`, `root`
|
|
2755
|
+
- Adds cycle detection validation (prevents self-reference, circular chains, and max_depth violations)
|
|
2756
|
+
- Adds a database index on the parent field
|
|
2757
|
+
- When `ordered: true`, configures `positioning` scoped to the parent field (unless the model already has explicit positioning)
|
|
2758
|
+
|
|
2759
|
+
See [Tree Structures Reference](tree-structures.md) for full details on scopes, methods, and presenter integration.
|
|
2760
|
+
|
|
2761
|
+
## Complete Example
|
|
2762
|
+
|
|
2763
|
+
### YAML (TODO App)
|
|
2764
|
+
|
|
2765
|
+
```yaml
|
|
2766
|
+
model:
|
|
2767
|
+
name: todo_item
|
|
2768
|
+
label: "Todo Item"
|
|
2769
|
+
label_plural: "Todo Items"
|
|
2770
|
+
|
|
2771
|
+
fields:
|
|
2772
|
+
- name: title
|
|
2773
|
+
type: string
|
|
2774
|
+
label: "Title"
|
|
2775
|
+
transforms: [strip]
|
|
2776
|
+
column_options:
|
|
2777
|
+
limit: 255
|
|
2778
|
+
"null": false
|
|
2779
|
+
validations:
|
|
2780
|
+
- type: presence
|
|
2781
|
+
|
|
2782
|
+
- name: completed
|
|
2783
|
+
type: boolean
|
|
2784
|
+
label: "Completed"
|
|
2785
|
+
default: false
|
|
2786
|
+
|
|
2787
|
+
- name: start_date
|
|
2788
|
+
type: date
|
|
2789
|
+
label: "Start Date"
|
|
2790
|
+
default: current_date
|
|
2791
|
+
|
|
2792
|
+
- name: due_date
|
|
2793
|
+
type: date
|
|
2794
|
+
label: "Due Date"
|
|
2795
|
+
default:
|
|
2796
|
+
service: one_week_from_now
|
|
2797
|
+
validations:
|
|
2798
|
+
- type: comparison
|
|
2799
|
+
operator: gte
|
|
2800
|
+
field_ref: start_date
|
|
2801
|
+
message: "must be on or after start date"
|
|
2802
|
+
- type: presence
|
|
2803
|
+
when:
|
|
2804
|
+
field: completed
|
|
2805
|
+
operator: eq
|
|
2806
|
+
value: false
|
|
2807
|
+
|
|
2808
|
+
associations:
|
|
2809
|
+
- type: belongs_to
|
|
2810
|
+
name: todo_list
|
|
2811
|
+
target_model: todo_list
|
|
2812
|
+
foreign_key: todo_list_id
|
|
2813
|
+
required: true
|
|
2814
|
+
|
|
2815
|
+
options:
|
|
2816
|
+
timestamps: true
|
|
2817
|
+
label_method: title
|
|
2818
|
+
```
|
|
2819
|
+
|
|
2820
|
+
### Ruby DSL (CRM App)
|
|
2821
|
+
|
|
2822
|
+
```ruby
|
|
2823
|
+
define_model :deal do
|
|
2824
|
+
label "Deal"
|
|
2825
|
+
label_plural "Deals"
|
|
2826
|
+
|
|
2827
|
+
field :title, :string, label: "Title", limit: 255, null: false do
|
|
2828
|
+
validates :presence
|
|
2829
|
+
end
|
|
2830
|
+
|
|
2831
|
+
field :stage, :enum, label: "Stage", default: "lead",
|
|
2832
|
+
values: {
|
|
2833
|
+
lead: "Lead", qualified: "Qualified", proposal: "Proposal",
|
|
2834
|
+
negotiation: "Negotiation", closed_won: "Closed Won",
|
|
2835
|
+
closed_lost: "Closed Lost"
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
field :value, :decimal, label: "Value", precision: 12, scale: 2 do
|
|
2839
|
+
validates :numericality, greater_than_or_equal_to: 0, allow_nil: true
|
|
2840
|
+
validates :presence, when: { field: :stage, operator: :not_in, value: %w[lead] }
|
|
2841
|
+
end
|
|
2842
|
+
|
|
2843
|
+
field :weighted_value, :decimal, label: "Weighted Value", precision: 12, scale: 2,
|
|
2844
|
+
computed: { service: "weighted_deal_value" }
|
|
2845
|
+
|
|
2846
|
+
field :expected_close_date, :date, label: "Expected Close",
|
|
2847
|
+
default: { service: "thirty_days_out" } do
|
|
2848
|
+
validates :comparison, operator: :gte, field_ref: :created_at,
|
|
2849
|
+
message: "cannot be before deal creation"
|
|
2850
|
+
end
|
|
2851
|
+
|
|
2852
|
+
belongs_to :company, model: :company, required: true
|
|
2853
|
+
belongs_to :contact, model: :contact, required: false
|
|
2854
|
+
|
|
2855
|
+
validates :contact_id, :presence,
|
|
2856
|
+
when: { field: :stage, operator: :in, value: %w[negotiation closed_won closed_lost] }
|
|
2857
|
+
|
|
2858
|
+
validates_model :service, service: "deal_credit_limit"
|
|
2859
|
+
|
|
2860
|
+
scope :open_deals, where_not: { stage: ["closed_won", "closed_lost"] }
|
|
2861
|
+
|
|
2862
|
+
on_field_change :on_stage_change, field: :stage,
|
|
2863
|
+
condition: { field: :stage, operator: :not_in, value: %w[lead] }
|
|
2864
|
+
|
|
2865
|
+
timestamps true
|
|
2866
|
+
label_method :title
|
|
2867
|
+
end
|
|
2868
|
+
```
|
|
2869
|
+
|
|
2870
|
+
## Single Table Inheritance (STI)
|
|
2871
|
+
|
|
2872
|
+
STI allows multiple models to share a single database table, with a `type` column distinguishing the AR class. Each child model gets its own presenter, permissions, workflow, and pages.
|
|
2873
|
+
|
|
2874
|
+
### Enabling STI
|
|
2875
|
+
|
|
2876
|
+
Set `options.sti: true` on the parent model:
|
|
2877
|
+
|
|
2878
|
+
```yaml
|
|
2879
|
+
# models/document.yml
|
|
2880
|
+
model:
|
|
2881
|
+
name: document
|
|
2882
|
+
options:
|
|
2883
|
+
sti: true
|
|
2884
|
+
fields:
|
|
2885
|
+
- name: title
|
|
2886
|
+
type: string
|
|
2887
|
+
- name: status
|
|
2888
|
+
type: enum
|
|
2889
|
+
enum_values: [draft, published]
|
|
2890
|
+
```
|
|
2891
|
+
|
|
2892
|
+
Define child models with `inherits:` pointing to the STI parent:
|
|
2893
|
+
|
|
2894
|
+
```yaml
|
|
2895
|
+
# models/invoice.yml
|
|
2896
|
+
model:
|
|
2897
|
+
name: invoice
|
|
2898
|
+
inherits: document
|
|
2899
|
+
fields:
|
|
2900
|
+
- name: amount
|
|
2901
|
+
type: decimal
|
|
2902
|
+
- name: due_date
|
|
2903
|
+
type: date
|
|
2904
|
+
```
|
|
2905
|
+
|
|
2906
|
+
```yaml
|
|
2907
|
+
# models/credit_note.yml
|
|
2908
|
+
model:
|
|
2909
|
+
name: credit_note
|
|
2910
|
+
inherits: document
|
|
2911
|
+
fields:
|
|
2912
|
+
- name: reason
|
|
2913
|
+
type: text
|
|
2914
|
+
```
|
|
2915
|
+
|
|
2916
|
+
### How it works
|
|
2917
|
+
|
|
2918
|
+
- A single `documents` table is created with a `type` column, plus columns from all children (`amount`, `due_date`, `reason`).
|
|
2919
|
+
- `LcpRuby::Dynamic::Invoice` inherits from `LcpRuby::Dynamic::Document` at the Ruby level.
|
|
2920
|
+
- `Invoice.all` returns only invoices; `Document.all` returns all types.
|
|
2921
|
+
- Parent validations, enums, callbacks, and scopes are inherited via AR class hierarchy (not re-applied).
|
|
2922
|
+
- Each child can have its own presenter (with its own slug, columns, form) and its own permissions.
|
|
2923
|
+
|
|
2924
|
+
### Restrictions
|
|
2925
|
+
|
|
2926
|
+
- STI children must not define `table_name` (they inherit the parent's table).
|
|
2927
|
+
- STI children must not set `options.sti: true` (only the root parent can be STI).
|
|
2928
|
+
- Abstract models cannot be STI parents (use `abstract: true` or `options.sti: true`, not both).
|
|
2929
|
+
|
|
2930
|
+
### Permission fallback
|
|
2931
|
+
|
|
2932
|
+
When no permissions are defined for an STI child, the platform falls back to the parent model's permissions, then to `_default`. This applies to both YAML and DB permission sources.
|
|
2933
|
+
|
|
2934
|
+
### Workflow fallback
|
|
2935
|
+
|
|
2936
|
+
When no workflow is defined for an STI child, the platform falls back to the parent model's workflow.
|
|
2937
|
+
|
|
2938
|
+
### Chaining with abstract inheritance
|
|
2939
|
+
|
|
2940
|
+
An abstract → STI chain is supported: define common fields in an abstract base, then an STI parent that inherits from it, then STI children:
|
|
2941
|
+
|
|
2942
|
+
```yaml
|
|
2943
|
+
# models/_auditable.yml
|
|
2944
|
+
model:
|
|
2945
|
+
name: _auditable
|
|
2946
|
+
abstract: true
|
|
2947
|
+
fields:
|
|
2948
|
+
- name: created_by
|
|
2949
|
+
type: string
|
|
2950
|
+
|
|
2951
|
+
# models/document.yml
|
|
2952
|
+
model:
|
|
2953
|
+
name: document
|
|
2954
|
+
inherits: _auditable
|
|
2955
|
+
options:
|
|
2956
|
+
sti: true
|
|
2957
|
+
fields:
|
|
2958
|
+
- name: title
|
|
2959
|
+
type: string
|
|
2960
|
+
|
|
2961
|
+
# models/invoice.yml
|
|
2962
|
+
model:
|
|
2963
|
+
name: invoice
|
|
2964
|
+
inherits: document
|
|
2965
|
+
fields:
|
|
2966
|
+
- name: amount
|
|
2967
|
+
type: decimal
|
|
2968
|
+
```
|
|
2969
|
+
|
|
2970
|
+
In this chain, `invoice` gets `created_by` (from `_auditable`), `title` (from `document`), and `amount` (its own).
|
|
2971
|
+
|
|
2972
|
+
Source: `lib/lcp_ruby/metadata/model_definition.rb`, `lib/lcp_ruby/metadata/field_definition.rb`, `lib/lcp_ruby/metadata/validation_definition.rb`, `lib/lcp_ruby/metadata/association_definition.rb`, `lib/lcp_ruby/metadata/event_definition.rb`
|