dbdoc_engine 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.
Files changed (198) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +331 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/builds/dbdoc_engine/application.css +5 -0
  5. data/app/assets/images/dbdoc_engine/arrowdown.svg +3 -0
  6. data/app/assets/images/dbdoc_engine/arrowhorizontal.svg +3 -0
  7. data/app/assets/images/dbdoc_engine/arrowleft.svg +3 -0
  8. data/app/assets/images/dbdoc_engine/changelog.svg +3 -0
  9. data/app/assets/images/dbdoc_engine/column_stats_dbdocs.svg +23 -0
  10. data/app/assets/images/dbdoc_engine/diagram.svg +3 -0
  11. data/app/assets/images/dbdoc_engine/double_arrow.svg +4 -0
  12. data/app/assets/images/dbdoc_engine/group_bu.svg +3 -0
  13. data/app/assets/images/dbdoc_engine/japan_circle.png +0 -0
  14. data/app/assets/images/dbdoc_engine/log_in_image.png +0 -0
  15. data/app/assets/images/dbdoc_engine/logo.svg +12 -0
  16. data/app/assets/images/dbdoc_engine/orange_changelog.svg +3 -0
  17. data/app/assets/images/dbdoc_engine/orange_fields.svg +23 -0
  18. data/app/assets/images/dbdoc_engine/orange_logo.svg +12 -0
  19. data/app/assets/images/dbdoc_engine/orange_table.svg +21 -0
  20. data/app/assets/images/dbdoc_engine/orange_updates.svg +43 -0
  21. data/app/assets/images/dbdoc_engine/orange_wiki.svg +3 -0
  22. data/app/assets/images/dbdoc_engine/search.svg +3 -0
  23. data/app/assets/images/dbdoc_engine/setting.svg +3 -0
  24. data/app/assets/images/dbdoc_engine/table_dbdocs.svg +21 -0
  25. data/app/assets/images/dbdoc_engine/uk_circle_transparent.png +0 -0
  26. data/app/assets/images/dbdoc_engine/update_stats_dbdocs.svg +43 -0
  27. data/app/assets/images/dbdoc_engine/wiki.svg +3 -0
  28. data/app/assets/stylesheets/dbdoc_engine/admin.css +176 -0
  29. data/app/assets/stylesheets/dbdoc_engine/admin_header.css +179 -0
  30. data/app/assets/stylesheets/dbdoc_engine/application.scss +1 -0
  31. data/app/assets/stylesheets/dbdoc_engine/changelog.css +173 -0
  32. data/app/assets/stylesheets/dbdoc_engine/dashboard.css +513 -0
  33. data/app/assets/stylesheets/dbdoc_engine/dbdoc_application.css +117 -0
  34. data/app/assets/stylesheets/dbdoc_engine/ecommerce.css +253 -0
  35. data/app/assets/stylesheets/dbdoc_engine/group_details.css +178 -0
  36. data/app/assets/stylesheets/dbdoc_engine/header.css +212 -0
  37. data/app/assets/stylesheets/dbdoc_engine/loading_spinner.css +127 -0
  38. data/app/assets/stylesheets/dbdoc_engine/login.css +213 -0
  39. data/app/assets/stylesheets/dbdoc_engine/schema_diagram.css +149 -0
  40. data/app/assets/stylesheets/dbdoc_engine/sidebar.css +296 -0
  41. data/app/assets/stylesheets/dbdoc_engine/table_details.css +417 -0
  42. data/app/controllers/dbdoc_engine/admin/base_controller.rb +23 -0
  43. data/app/controllers/dbdoc_engine/admin/dashboard_controller.rb +16 -0
  44. data/app/controllers/dbdoc_engine/admin/data_transfer_controller.rb +63 -0
  45. data/app/controllers/dbdoc_engine/admin/db_design_dynamic_tables_controller.rb +198 -0
  46. data/app/controllers/dbdoc_engine/admin/db_design_table_groups_controller.rb +107 -0
  47. data/app/controllers/dbdoc_engine/application_controller.rb +65 -0
  48. data/app/controllers/dbdoc_engine/concerns/internationalization.rb +57 -0
  49. data/app/controllers/dbdoc_engine/db_doc_sessions_controller.rb +33 -0
  50. data/app/controllers/dbdoc_engine/home_controller.rb +79 -0
  51. data/app/controllers/dbdoc_engine/schema_diagram_controller.rb +293 -0
  52. data/app/helper/dbdoc_engine/application_helper.rb +35 -0
  53. data/app/helpers/dbdoc_engine/application_helper.rb +4 -0
  54. data/app/helpers/dbdoc_engine/changelogs_helper.rb +27 -0
  55. data/app/helpers/dbdoc_engine/column_helper.rb +30 -0
  56. data/app/helpers/dbdoc_engine/db_design_dynamic_tables_helper.rb +15 -0
  57. data/app/helpers/dbdoc_engine/home_helper.rb +75 -0
  58. data/app/javascript/dbdoc_engine/application.js +12 -0
  59. data/app/javascript/dbdoc_engine/controllers/application.js +29 -0
  60. data/app/javascript/dbdoc_engine/controllers/auto_submit_controller.js +17 -0
  61. data/app/javascript/dbdoc_engine/controllers/chart_controller.js +58 -0
  62. data/app/javascript/dbdoc_engine/controllers/column-type_controller.js +149 -0
  63. data/app/javascript/dbdoc_engine/controllers/column_controller.js +362 -0
  64. data/app/javascript/dbdoc_engine/controllers/column_search_controller.js +42 -0
  65. data/app/javascript/dbdoc_engine/controllers/dbdoc_accordion_controller.js +42 -0
  66. data/app/javascript/dbdoc_engine/controllers/ecommerce_controller.js +73 -0
  67. data/app/javascript/dbdoc_engine/controllers/group_details_controller.js +88 -0
  68. data/app/javascript/dbdoc_engine/controllers/import_export_controller.js +200 -0
  69. data/app/javascript/dbdoc_engine/controllers/index.js +9 -0
  70. data/app/javascript/dbdoc_engine/controllers/language_controller.js +100 -0
  71. data/app/javascript/dbdoc_engine/controllers/loading_spinner_controller.js +48 -0
  72. data/app/javascript/dbdoc_engine/controllers/login_controller.js +75 -0
  73. data/app/javascript/dbdoc_engine/controllers/notification_controller.js +15 -0
  74. data/app/javascript/dbdoc_engine/controllers/schema_diagram_controller.js +1129 -0
  75. data/app/javascript/dbdoc_engine/controllers/select2_controller.js +67 -0
  76. data/app/javascript/dbdoc_engine/controllers/sidebar_controller.js +943 -0
  77. data/app/javascript/dbdoc_engine/controllers/table_details_controller.js +245 -0
  78. data/app/javascript/dbdoc_engine/controllers/table_group_validation_controller.js +148 -0
  79. data/app/javascript/dbdoc_engine/controllers/table_validation_controller.js +423 -0
  80. data/app/jobs/dbdoc_engine/application_job.rb +4 -0
  81. data/app/mailers/dbdoc_engine/application_mailer.rb +6 -0
  82. data/app/models/dbdoc_engine/application_record.rb +6 -0
  83. data/app/models/dbdoc_engine/concerns/soft_deletable.rb +30 -0
  84. data/app/models/dbdoc_engine/db_design_changelog.rb +44 -0
  85. data/app/models/dbdoc_engine/db_design_dynamic_column.rb +211 -0
  86. data/app/models/dbdoc_engine/db_design_dynamic_table.rb +124 -0
  87. data/app/models/dbdoc_engine/db_design_table_group.rb +88 -0
  88. data/app/models/dbdoc_engine/user.rb +21 -0
  89. data/app/queries/dbdoc_engine/admin_dashboard_queries.rb +71 -0
  90. data/app/queries/dbdoc_engine/db_design_changelog_queries.rb +68 -0
  91. data/app/queries/dbdoc_engine/db_design_dynamic_column_queries.rb +37 -0
  92. data/app/queries/dbdoc_engine/db_design_dynamic_table_commands.rb +106 -0
  93. data/app/queries/dbdoc_engine/db_design_dynamic_table_queries.rb +194 -0
  94. data/app/queries/dbdoc_engine/db_design_table_group_queries.rb +154 -0
  95. data/app/services/dbdoc_engine/db_design_dynamic_table_export_service.rb +38 -0
  96. data/app/services/dbdoc_engine/db_design_dynamic_table_handler_service.rb +49 -0
  97. data/app/services/dbdoc_engine/db_design_dynamic_tables_service.rb +21 -0
  98. data/app/services/dbdoc_engine/error_handler_service.rb +43 -0
  99. data/app/services/dbdoc_engine/schema_rb_import_service.rb +194 -0
  100. data/app/services/dbdoc_engine/schema_rb_parser_service.rb +339 -0
  101. data/app/services/dbdoc_engine/table_filter_service.rb +35 -0
  102. data/app/services/dbdoc_engine/table_groups_service.rb +199 -0
  103. data/app/services/dbdoc_engine/table_management_service.rb +192 -0
  104. data/app/views/dbdoc_engine/admin/dashboard/_action_badge.html.erb +11 -0
  105. data/app/views/dbdoc_engine/admin/dashboard/_changelog_rows.html.erb +22 -0
  106. data/app/views/dbdoc_engine/admin/dashboard/_changelog_table_headers.html.erb +8 -0
  107. data/app/views/dbdoc_engine/admin/dashboard/_filter_fields.html.erb +43 -0
  108. data/app/views/dbdoc_engine/admin/dashboard/index.html.erb +159 -0
  109. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_column_fields.html.erb +225 -0
  110. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_deleted_table_index.html.erb +110 -0
  111. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_foreign_key_fields.html.erb +51 -0
  112. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_form.html.erb +75 -0
  113. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_recent_activity.html.erb +39 -0
  114. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_columns.html.erb +127 -0
  115. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_index.html.erb +109 -0
  116. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_information.html.erb +99 -0
  117. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/deleted_tables.html.erb +95 -0
  118. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/edit.html.erb +23 -0
  119. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_all_to_excel.xlsx.axlsx +240 -0
  120. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_to_excel.xlsx.axlsx +135 -0
  121. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/index.html.erb +109 -0
  122. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/new.html.erb +25 -0
  123. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/show_table_info.html.erb +125 -0
  124. data/app/views/dbdoc_engine/admin/db_design_table_groups/_deleted_table_groups_list.html.erb +75 -0
  125. data/app/views/dbdoc_engine/admin/db_design_table_groups/_form.html.erb +88 -0
  126. data/app/views/dbdoc_engine/admin/db_design_table_groups/_table_groups_list.html.erb +82 -0
  127. data/app/views/dbdoc_engine/admin/db_design_table_groups/deleted_groups.html.erb +60 -0
  128. data/app/views/dbdoc_engine/admin/db_design_table_groups/edit.html.erb +25 -0
  129. data/app/views/dbdoc_engine/admin/db_design_table_groups/index.html.erb +85 -0
  130. data/app/views/dbdoc_engine/admin/db_design_table_groups/new.html.erb +26 -0
  131. data/app/views/dbdoc_engine/db_doc_sessions/new.html.erb +59 -0
  132. data/app/views/dbdoc_engine/home/changelog_details.html.erb +80 -0
  133. data/app/views/dbdoc_engine/home/changelogs.html.erb +20 -0
  134. data/app/views/dbdoc_engine/home/group_details.html.erb +94 -0
  135. data/app/views/dbdoc_engine/home/index.html.erb +11 -0
  136. data/app/views/dbdoc_engine/home/partials/_action_badge.html.erb +11 -0
  137. data/app/views/dbdoc_engine/home/partials/_breadcrumb_navigation.html.erb +30 -0
  138. data/app/views/dbdoc_engine/home/partials/_changelog_rows.html.erb +35 -0
  139. data/app/views/dbdoc_engine/home/partials/_changelog_table_headers.html.erb +16 -0
  140. data/app/views/dbdoc_engine/home/partials/_column_headers.html.erb +23 -0
  141. data/app/views/dbdoc_engine/home/partials/_column_row.html.erb +157 -0
  142. data/app/views/dbdoc_engine/home/partials/_filter_form.html.erb +47 -0
  143. data/app/views/dbdoc_engine/home/partials/_group_section.html.erb +84 -0
  144. data/app/views/dbdoc_engine/home/partials/_pagination.html.erb +5 -0
  145. data/app/views/dbdoc_engine/home/partials/_stats_container.html.erb +46 -0
  146. data/app/views/dbdoc_engine/home/partials/_table_groups.html.erb +7 -0
  147. data/app/views/dbdoc_engine/home/partials/_table_information_section.html.erb +50 -0
  148. data/app/views/dbdoc_engine/home/partials/_table_section.html.erb +48 -0
  149. data/app/views/dbdoc_engine/home/table_details.html.erb +9 -0
  150. data/app/views/dbdoc_engine/schema_diagram/index.html.erb +102 -0
  151. data/app/views/dbdoc_engine/shared/_admin_header.html.erb +78 -0
  152. data/app/views/dbdoc_engine/shared/_header.html.erb +94 -0
  153. data/app/views/dbdoc_engine/shared/_js_translations.html.erb +3 -0
  154. data/app/views/dbdoc_engine/shared/_language_button.html.erb +14 -0
  155. data/app/views/dbdoc_engine/shared/_sidebar.html.erb +128 -0
  156. data/app/views/kaminari/dbdoc_engine/_first_page.html.erb +3 -0
  157. data/app/views/kaminari/dbdoc_engine/_gap.html.erb +3 -0
  158. data/app/views/kaminari/dbdoc_engine/_last_page.html.erb +3 -0
  159. data/app/views/kaminari/dbdoc_engine/_next_page.html.erb +3 -0
  160. data/app/views/kaminari/dbdoc_engine/_page.html.erb +9 -0
  161. data/app/views/kaminari/dbdoc_engine/_paginator.html.erb +17 -0
  162. data/app/views/kaminari/dbdoc_engine/_prev_page.html.erb +3 -0
  163. data/app/views/layouts/dbdoc_engine/application.html.erb +107 -0
  164. data/app/views/layouts/dbdoc_engine/header.html.erb +108 -0
  165. data/config/importmap.rb +11 -0
  166. data/config/locales/en.yml +307 -0
  167. data/config/locales/ja.yml +306 -0
  168. data/config/routes.rb +73 -0
  169. data/db/migrate/rails7/20250227060610_create_db_design_table_groups.rb +15 -0
  170. data/db/migrate/rails7/20250227094626_create_db_design_dynamic_tables.rb +19 -0
  171. data/db/migrate/rails7/20250228022732_create_db_design_dynamic_columns.rb +34 -0
  172. data/db/migrate/rails7/20250401051453_create_db_design_changelogs.rb +26 -0
  173. data/db/migrate/rails7/20250411040822_create_users.rb +14 -0
  174. data/db/migrate/rails7/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
  175. data/db/migrate/rails8/20250227060610_create_db_design_table_groups.rb +15 -0
  176. data/db/migrate/rails8/20250227094626_create_db_design_dynamic_tables.rb +19 -0
  177. data/db/migrate/rails8/20250228022732_create_db_design_dynamic_columns.rb +34 -0
  178. data/db/migrate/rails8/20250401051453_create_db_design_changelogs.rb +26 -0
  179. data/db/migrate/rails8/20250411040822_create_users.rb +14 -0
  180. data/db/migrate/rails8/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
  181. data/db/seeds.rb +28 -0
  182. data/lib/dbdoc_engine/engine.rb +57 -0
  183. data/lib/dbdoc_engine/version.rb +3 -0
  184. data/lib/dbdoc_engine.rb +9 -0
  185. data/lib/generators/dbdoc_engine/install/install_generator.rb +245 -0
  186. data/lib/generators/dbdoc_engine/uninstall/uninstall_generator.rb +196 -0
  187. data/lib/tasks/dbdoc_engine_tasks.rake +44 -0
  188. data/public/dbdoc_engine_assets/images/camel_chess_head.png +0 -0
  189. data/public/dbdoc_engine_assets/images/dblogo.svg +4 -0
  190. data/public/dbdoc_engine_assets/images/japan_circle.png +0 -0
  191. data/public/dbdoc_engine_assets/images/king_chess_head.png +0 -0
  192. data/public/dbdoc_engine_assets/images/login-bg.svg +44 -0
  193. data/public/dbdoc_engine_assets/images/logo.png +0 -0
  194. data/public/dbdoc_engine_assets/images/logo.svg +12 -0
  195. data/public/dbdoc_engine_assets/images/queen_chess_head.png +0 -0
  196. data/public/dbdoc_engine_assets/images/soldier_chess_headd.png +0 -0
  197. data/public/dbdoc_engine_assets/images/uk_circle_transparent.png +0 -0
  198. metadata +415 -0
@@ -0,0 +1,423 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Stimulus controller that handles form validation for database table creation/editing
5
+ * Validates table information, columns, and checks for duplicate column names
6
+ */
7
+ export default class extends Controller {
8
+ // Define target elements that can be referenced in the controller
9
+ static targets = ["tableName", "physicalTableName", "tableGroup", "createdBy", "updatedBy"]
10
+
11
+ /**
12
+ * Connect lifecycle method - called when controller is connected to the DOM
13
+ * Sets up event listeners for real-time data syncing
14
+ */
15
+ connect() {
16
+ // Check if we're in create mode (hidden updated_by field exists)
17
+ const hiddenUpdatedBy = this.element.querySelector('input[name$="[updated_by]"][type="hidden"]');
18
+ const createdBy = this.element.querySelector('input[name$="[created_by]"]');
19
+
20
+ // If both fields exist, set up the sync between created_by and updated_by
21
+ if (hiddenUpdatedBy && createdBy) {
22
+ // Sync on input changes
23
+ createdBy.addEventListener('input', () => {
24
+ hiddenUpdatedBy.value = createdBy.value;
25
+ });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Main validation method triggered on form submission
31
+ * Prevents default form submission, validates all required fields,
32
+ * and only submits if all validations pass
33
+ *
34
+ * @param {Event} event - The form submission event
35
+ */
36
+ validate(event) {
37
+ event.preventDefault()
38
+ let isValid = true
39
+
40
+ // Reset previous error states
41
+ this.clearErrors()
42
+
43
+ // For new record: Make sure hidden updated_by is synced with created_by before validation
44
+ const hiddenUpdatedBy = this.element.querySelector('input[name$="[updated_by]"][type="hidden"]');
45
+ const createdBy = this.element.querySelector('input[name$="[created_by]"]');
46
+ if (hiddenUpdatedBy && createdBy) {
47
+ hiddenUpdatedBy.value = createdBy.value;
48
+ }
49
+
50
+ // Store all invalid elements for scrolling later
51
+ const invalidElements = []
52
+
53
+ // Run all validation checks and aggregate results
54
+ isValid = this.validateTableInformation(invalidElements) && isValid
55
+ isValid = this.validateColumns(invalidElements) && isValid
56
+ isValid = this.checkDuplicateColumnNames(invalidElements) && isValid
57
+
58
+ if (isValid) {
59
+ event.target.submit() // If everything is valid, submit the form
60
+ } else {
61
+ // If not valid, scroll to the first invalid element
62
+ if (invalidElements.length > 0) {
63
+ // Expand accordion section if the error is in a column
64
+ const accordionParent = this.findAccordionParent(invalidElements[0])
65
+ if (accordionParent) {
66
+ this.expandAccordion(accordionParent)
67
+ }
68
+
69
+ // Scroll to the element with error
70
+ setTimeout(() => {
71
+ invalidElements[0].scrollIntoView({ behavior: 'smooth', block: 'center' })
72
+ invalidElements[0].focus()
73
+ }, accordionParent ? 400 : 0) // Small delay when expanding accordion
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Validates the main table information fields
80
+ * Checks for required values in table name, physical name, group, created by fields,
81
+ * and updated_by when editing an existing table
82
+ *
83
+ * @param {Array} invalidElements - Array to collect invalid elements
84
+ * @returns {boolean} - True if all table information is valid, false otherwise
85
+ */
86
+ validateTableInformation(invalidElements = []) {
87
+ let isValid = true;
88
+
89
+ // Select input fields safely using attribute selectors
90
+ const tableName = this.element.querySelector('[name$="[table_name]"]');
91
+ const physicalTableName = this.element.querySelector('[name$="[physical_table_name]"]');
92
+ const tableGroup = this.element.querySelector('[name$="[db_design_table_group_id]"]');
93
+ const createdBy = this.element.querySelector('[name$="[created_by]"]');
94
+
95
+ // Updated by field - first get the visible one (not hidden)
96
+ const visibleUpdatedBy = this.element.querySelector('input[name$="[updated_by]"]:not([type="hidden"])');
97
+ // Determines if we're in edit mode (has visible updated_by field)
98
+ const isEditing = !!visibleUpdatedBy;
99
+
100
+ // Validate each field only if it exists, showing appropriate errors
101
+ if (!tableName || !tableName.value.trim()) {
102
+ this.showError(tableName, "table_name_required");
103
+ invalidElements.push(tableName);
104
+ isValid = false;
105
+ }
106
+
107
+ if (!physicalTableName || !physicalTableName.value.trim()) {
108
+ this.showError(physicalTableName, "physical_table_name_required");
109
+ if (!invalidElements.length) invalidElements.push(physicalTableName);
110
+ isValid = false;
111
+ }
112
+
113
+ if (!tableGroup || !tableGroup.value.trim()) {
114
+ this.showError(tableGroup, "table_group_must_be_selected");
115
+ if (!invalidElements.length) invalidElements.push(tableGroup);
116
+ isValid = false;
117
+ }
118
+
119
+ if (!createdBy || !createdBy.value.trim()) {
120
+ this.showError(createdBy, "created_by_field_required");
121
+ if (!invalidElements.length) invalidElements.push(createdBy);
122
+ isValid = false;
123
+ }
124
+
125
+ // Only validate updated_by when editing (only if visible updated_by exists)
126
+ if (isEditing && (!visibleUpdatedBy.value.trim())) {
127
+ this.showError(visibleUpdatedBy, "updated_by_required");
128
+ if (!invalidElements.length) invalidElements.push(visibleUpdatedBy);
129
+ isValid = false;
130
+ }
131
+
132
+ return isValid;
133
+ }
134
+
135
+ /**
136
+ * Validates all column fields for each table column
137
+ * Skips columns marked for deletion and validates required fields
138
+ * Performs special validation for foreign keys and data types with length requirements
139
+ *
140
+ * @param {Array} invalidElements - Array to collect invalid elements
141
+ * @returns {boolean} - True if all columns are valid, false otherwise
142
+ */
143
+ validateColumns(invalidElements = []) {
144
+ const columnContainers = document.querySelectorAll('.column-fields')
145
+ let allColumnsValid = true
146
+
147
+ columnContainers.forEach((container) => {
148
+ // Skip columns marked for deletion
149
+ const destroyInput = container.querySelector('[name$="[_destroy]"]')
150
+ if (destroyInput && destroyInput.value === '1') {
151
+ return
152
+ }
153
+
154
+ // Select column fields
155
+ const columnName = container.querySelector('[name$="[column_name]"]')
156
+ const physicalColumnName = container.querySelector('[name$="[physical_column_name]"]')
157
+ const dataType = container.querySelector('[name$="[data_type]"]')
158
+ const length = container.querySelector('[name$="[length]"]')
159
+ const createdBy = container.querySelector('[name$="[created_by]"]')
160
+
161
+ // Find foreign key related elements with more robust selection
162
+ const foreignKeyCheckbox = container.querySelector('[name$="[is_foreign_key]"]')
163
+ const foreignKeyFieldsContainer = container.querySelector('.foreign-key-fields')
164
+ const referencedTable = container.querySelector('[name$="[foreign_table_name]"]')
165
+ const referencedColumn = container.querySelector('[name$="[foreign_column_name]"]')
166
+ const relationshipType = container.querySelector('[name$="[relationship_type]"]')
167
+
168
+ // Multiple ways to determine if foreign key is active
169
+ const isForeignKeyChecked = foreignKeyCheckbox?.checked ||
170
+ foreignKeyCheckbox?.value === '1' ||
171
+ !foreignKeyFieldsContainer?.classList.contains('hidden')
172
+
173
+ let columnHasError = false;
174
+
175
+ // Validate required column fields
176
+ if (!columnName.value.trim()) {
177
+ this.showError(columnName, "column_name_required")
178
+ if (!columnHasError) {
179
+ invalidElements.push(columnName)
180
+ columnHasError = true
181
+ }
182
+ allColumnsValid = false
183
+ }
184
+
185
+ if (!physicalColumnName.value.trim()) {
186
+ this.showError(physicalColumnName, "physical_column_name_required")
187
+ if (!columnHasError) {
188
+ invalidElements.push(physicalColumnName)
189
+ columnHasError = true
190
+ }
191
+ allColumnsValid = false
192
+ }
193
+
194
+ if (!dataType.value) {
195
+ this.showError(dataType, "data_type_must_be_selected")
196
+ if (!columnHasError) {
197
+ invalidElements.push(dataType)
198
+ columnHasError = true
199
+ }
200
+ allColumnsValid = false
201
+ }
202
+
203
+ // Special validation for data types that require length specification
204
+ const lengthRequiredTypes = ['varchar', 'character', 'char']
205
+ if (lengthRequiredTypes.includes(dataType.value) && (!length.value || parseInt(length.value) <= 0)) {
206
+ this.showError(length, "length_is_required_for_this_data_type")
207
+ if (!columnHasError) {
208
+ invalidElements.push(length)
209
+ columnHasError = true
210
+ }
211
+ allColumnsValid = false
212
+ }
213
+
214
+ if (!createdBy.value.trim()) {
215
+ this.showError(createdBy, "created_by_field_required")
216
+ if (!columnHasError) {
217
+ invalidElements.push(createdBy)
218
+ columnHasError = true
219
+ }
220
+ allColumnsValid = false
221
+ }
222
+
223
+ // Enhanced foreign key validation - only validate if it's marked as a foreign key
224
+ if (isForeignKeyChecked) {
225
+ // Check if referenced table is selected
226
+ if (!referencedTable || !referencedTable.value.trim()) {
227
+ this.showError(referencedTable, "referenced_table_is_required_for_foreign_key")
228
+ if (!columnHasError) {
229
+ invalidElements.push(referencedTable)
230
+ columnHasError = true
231
+ }
232
+ allColumnsValid = false
233
+ }
234
+
235
+ // Check if referenced column is selected
236
+ if (!referencedColumn || !referencedColumn.value.trim()) {
237
+ this.showError(referencedColumn, "referenced_column_is_required_for_foreign_key")
238
+ if (!columnHasError) {
239
+ invalidElements.push(referencedColumn)
240
+ columnHasError = true
241
+ }
242
+ allColumnsValid = false
243
+ }
244
+
245
+ if (!relationshipType || !relationshipType.value.trim()) {
246
+ this.showError(relationshipType, "relationship_type_is_required_for_foreign_key")
247
+ if (!columnHasError) {
248
+ invalidElements.push(relationshipType)
249
+ columnHasError = true
250
+ }
251
+ allColumnsValid = false
252
+ }
253
+ }
254
+ })
255
+
256
+ return allColumnsValid
257
+ }
258
+
259
+ /**
260
+ * Checks for duplicate column names within the table
261
+ * Collects all active column names and marks duplicates with errors
262
+ *
263
+ * @param {Array} invalidElements - Array to collect invalid elements
264
+ * @returns {boolean} - True if no duplicates found, false otherwise
265
+ */
266
+ checkDuplicateColumnNames(invalidElements = []) {
267
+ const columnContainers = document.querySelectorAll('.column-fields')
268
+ const columnNames = new Map()
269
+ let hasDuplicates = false
270
+
271
+ // First pass: collect all active column names
272
+ columnContainers.forEach((container) => {
273
+ // Skip columns marked for deletion
274
+ const destroyInput = container.querySelector('[name$="[_destroy]"]')
275
+ if (destroyInput && destroyInput.value === '1') {
276
+ return
277
+ }
278
+
279
+ const columnNameInput = container.querySelector('[name$="[column_name]"]')
280
+ if (columnNameInput && columnNameInput.value.trim()) {
281
+ const columnName = columnNameInput.value.trim().toLowerCase()
282
+
283
+ if (columnNames.has(columnName)) {
284
+ // Store references to all inputs with this name for error marking
285
+ columnNames.set(columnName, [...columnNames.get(columnName), columnNameInput])
286
+ hasDuplicates = true
287
+ } else {
288
+ columnNames.set(columnName, [columnNameInput])
289
+ }
290
+ }
291
+ })
292
+
293
+ // Second pass: show errors for duplicates
294
+ if (hasDuplicates) {
295
+ columnNames.forEach((inputs, name) => {
296
+ if (inputs.length > 1) {
297
+ inputs.forEach((input, index) => {
298
+ this.showError(input, "duplicate_columns")
299
+ // Only add the first instance of each duplicate to the invalid elements
300
+ if (index === 0 && invalidElements.length === 0) {
301
+ invalidElements.push(input)
302
+ }
303
+ })
304
+ }
305
+ })
306
+ return false
307
+ }
308
+
309
+ return true
310
+ }
311
+
312
+ /**
313
+ * Displays an error message for an input element
314
+ * Adds is-invalid class and creates/updates an error message div
315
+ *
316
+ * @param {HTMLElement} element - The input element to mark as invalid
317
+ * @param {string} messageKey - The translation key for the error message
318
+ */
319
+ showError(element, messageKey) {
320
+ if (!element) return
321
+
322
+ // Add 'is-invalid' class to input/select field
323
+ element.classList.add('is-invalid')
324
+
325
+ // Get translated message from the hidden translations div
326
+ const translatedMessage = this.getTranslation(messageKey)
327
+
328
+ // First, remove any existing error messages for this element
329
+ const existingErrors = element.parentNode.querySelectorAll('.invalid-feedback')
330
+ existingErrors.forEach(errorEl => errorEl.remove())
331
+
332
+ // Create a new error message element
333
+ const errorElement = document.createElement('div')
334
+ errorElement.classList.add('invalid-feedback', 'd-block')
335
+ errorElement.textContent = translatedMessage
336
+
337
+ // Append it after the input element
338
+ element.parentNode.appendChild(errorElement)
339
+ }
340
+
341
+ /**
342
+ * Retrieves a translated error message using a global translations object
343
+ * Falls back to the key itself if translation not found
344
+ *
345
+ * @param {string} key - The translation key
346
+ * @returns {string} - The translated message or the key if not found
347
+ */
348
+ getTranslation(key) {
349
+ // Use the global Translations object if available
350
+ if (window.Translations && window.Translations[key]) {
351
+ return window.Translations[key];
352
+ }
353
+ return key;
354
+ }
355
+
356
+ /**
357
+ * Clears all validation errors from the form
358
+ * Removes is-invalid classes and error message elements
359
+ */
360
+ clearErrors() {
361
+ // Remove 'is-invalid' class and error messages
362
+ this.element.querySelectorAll('.is-invalid').forEach(el => {
363
+ el.classList.remove('is-invalid')
364
+ const errorEl = el.nextElementSibling
365
+ if (errorEl && errorEl.classList.contains('invalid-feedback')) {
366
+ errorEl.remove()
367
+ }
368
+ })
369
+ }
370
+
371
+ /**
372
+ * Finds the closest accordion parent element for an input
373
+ * Updated to work with custom dbdoc-accordion controller
374
+ *
375
+ * @param {HTMLElement} element - The input element
376
+ * @returns {HTMLElement|null} - The accordion item element or null
377
+ */
378
+ findAccordionParent(element) {
379
+ if (!element) return null
380
+
381
+ // Look for the accordion item that contains the dbdoc-accordion controller
382
+ let parent = element.closest('[data-controller*="dbdoc-accordion"]')
383
+
384
+ return parent
385
+ }
386
+
387
+ /**
388
+ * Expands an accordion section using the custom dbdoc-accordion controller
389
+ *
390
+ * @param {HTMLElement} accordionElement - The accordion item element with dbdoc-accordion controller
391
+ */
392
+ expandAccordion(accordionElement) {
393
+ if (!accordionElement) return
394
+
395
+ // Get the Stimulus controller instance for the accordion
396
+ const accordionController = window.Stimulus.getControllerForElementAndIdentifier(accordionElement, 'dbdoc-accordion')
397
+
398
+ if (accordionController) {
399
+ // Check if it's already open by looking at the content target
400
+ const contentTarget = accordionController.contentTarget
401
+ const isOpen = contentTarget && contentTarget.classList.contains('show')
402
+
403
+ if (!isOpen) {
404
+ // Use the controller's open method to expand the accordion
405
+ accordionController.open()
406
+ }
407
+ } else {
408
+ // Fallback method if controller instance is not accessible
409
+ const contentElement = accordionElement.querySelector('[data-dbdoc-accordion-target="content"]')
410
+ const buttonElement = accordionElement.querySelector('[data-dbdoc-accordion-target="button"]')
411
+
412
+ if (contentElement && buttonElement) {
413
+ const isOpen = contentElement.classList.contains('show')
414
+
415
+ if (!isOpen) {
416
+ contentElement.classList.add('show')
417
+ buttonElement.setAttribute('aria-expanded', 'true')
418
+ buttonElement.classList.remove('collapsed')
419
+ }
420
+ }
421
+ }
422
+ }
423
+ }
@@ -0,0 +1,4 @@
1
+ module DbdocEngine
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module DbdocEngine
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # app/models/dbdoc_engine/application_record.rb
2
+ module DbdocEngine
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ # app/models/concerns/soft_deletable.rb
2
+ module DbdocEngine::Concerns::SoftDeletable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ scope :without_deleted, -> { where(deleted_at: nil) }
7
+ scope :with_deleted, -> { unscope(where: :deleted_at) }
8
+ scope :only_deleted, -> { unscope(where: :deleted_at).where.not(deleted_at: nil) }
9
+ end
10
+
11
+ def deleted?
12
+ deleted_at.present?
13
+ end
14
+
15
+ def soft_delete
16
+ update(deleted_at: Time.current)
17
+ end
18
+
19
+ def restore
20
+ update(deleted_at: nil)
21
+ end
22
+
23
+ def self.soft_delete_all
24
+ update_all(deleted_at: Time.current)
25
+ end
26
+
27
+ def self.restore_all
28
+ update_all(deleted_at: nil)
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # Model for logging changes made to tables, table groups, and columns.
2
+ # It tracks actions such as create, update, and delete, and stores related metadata like entity names,
3
+ # descriptions, and the user who made the changes.
4
+ module DbdocEngine
5
+ class DbDesignChangelog < ApplicationRecord
6
+ self.table_name = 'db_design_changelogs'
7
+ # Constants for action types
8
+ ACTION_CREATE = 'create'.freeze
9
+ ACTION_UPDATE = 'update'.freeze
10
+ ACTION_DELETE = 'delete'.freeze
11
+
12
+ # Constants for entity types
13
+ ENTITY_TABLE_GROUP = 'table_group'.freeze
14
+ ENTITY_TABLE = 'table'.freeze
15
+ ENTITY_COLUMN = 'column'.freeze
16
+
17
+ # Validations
18
+ validates :change_timestamp, presence: true
19
+
20
+ # Ensures action type is present and valid
21
+ validates :action_type, presence: true, inclusion: { in: [ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE] }
22
+
23
+ # Ensures entity type is present and valid
24
+ validates :entity_type, presence: true, inclusion: { in: [ENTITY_TABLE_GROUP, ENTITY_TABLE, ENTITY_COLUMN] }
25
+
26
+ # Ensures entity name is provided
27
+ validates :entity_name, presence: true
28
+
29
+ # Ensures a description of the change is provided
30
+ validates :description, presence: true
31
+
32
+ # Ensures the name of the user who made the change is provided
33
+ validates :changed_by, presence: true
34
+
35
+ # Helper method to create changelog entry
36
+ def self.log_change(*args)
37
+ DbdocEngine::DbDesignChangelogQueries.log_change(*args)
38
+ end
39
+
40
+ def self.filtered_changelogs(params)
41
+ DbdocEngine::DbDesignChangelogQueries.filtered_changelogs(params)
42
+ end
43
+ end
44
+ end