typed_eav 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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +494 -0
  5. data/Rakefile +13 -0
  6. data/app/models/typed_eav/application_record.rb +7 -0
  7. data/app/models/typed_eav/field/base.rb +234 -0
  8. data/app/models/typed_eav/field/boolean.rb +22 -0
  9. data/app/models/typed_eav/field/color.rb +16 -0
  10. data/app/models/typed_eav/field/date.rb +24 -0
  11. data/app/models/typed_eav/field/date_array.rb +34 -0
  12. data/app/models/typed_eav/field/date_time.rb +29 -0
  13. data/app/models/typed_eav/field/decimal.rb +30 -0
  14. data/app/models/typed_eav/field/decimal_array.rb +31 -0
  15. data/app/models/typed_eav/field/email.rb +40 -0
  16. data/app/models/typed_eav/field/integer.rb +30 -0
  17. data/app/models/typed_eav/field/integer_array.rb +68 -0
  18. data/app/models/typed_eav/field/json.rb +26 -0
  19. data/app/models/typed_eav/field/long_text.rb +19 -0
  20. data/app/models/typed_eav/field/multi_select.rb +41 -0
  21. data/app/models/typed_eav/field/select.rb +28 -0
  22. data/app/models/typed_eav/field/text.rb +41 -0
  23. data/app/models/typed_eav/field/text_array.rb +36 -0
  24. data/app/models/typed_eav/field/url.rb +40 -0
  25. data/app/models/typed_eav/option.rb +24 -0
  26. data/app/models/typed_eav/section.rb +25 -0
  27. data/app/models/typed_eav/value.rb +149 -0
  28. data/db/migrate/20260330000000_create_typed_eav_tables.rb +132 -0
  29. data/lib/generators/typed_eav/install/install_generator.rb +28 -0
  30. data/lib/generators/typed_eav/scaffold/scaffold_generator.rb +106 -0
  31. data/lib/generators/typed_eav/scaffold/templates/config/initializers/typed_eav.rb +45 -0
  32. data/lib/generators/typed_eav/scaffold/templates/controllers/concerns/typed_eav_controller_concern.rb +24 -0
  33. data/lib/generators/typed_eav/scaffold/templates/controllers/typed_eav_controller.rb +231 -0
  34. data/lib/generators/typed_eav/scaffold/templates/helpers/typed_eav_helper.rb +150 -0
  35. data/lib/generators/typed_eav/scaffold/templates/javascript/controllers/array_field_controller.js +64 -0
  36. data/lib/generators/typed_eav/scaffold/templates/javascript/controllers/typed_eav_form_controller.js +32 -0
  37. data/lib/generators/typed_eav/scaffold/templates/views/shared/_array_field.html.erb +23 -0
  38. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/edit.html.erb +47 -0
  39. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/finders/_form.html.erb +80 -0
  40. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_boolean.html.erb +12 -0
  41. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_color.html.erb +11 -0
  42. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_common_fields.html.erb +57 -0
  43. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_date.html.erb +21 -0
  44. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_date_array.html.erb +16 -0
  45. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_date_time.html.erb +21 -0
  46. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_decimal.html.erb +21 -0
  47. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_decimal_array.html.erb +16 -0
  48. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_email.html.erb +11 -0
  49. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_integer.html.erb +21 -0
  50. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_integer_array.html.erb +16 -0
  51. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_json.html.erb +11 -0
  52. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_long_text.html.erb +21 -0
  53. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_multi_select.html.erb +6 -0
  54. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_select.html.erb +14 -0
  55. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_text.html.erb +26 -0
  56. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_text_array.html.erb +16 -0
  57. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/forms/_url.html.erb +11 -0
  58. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/index.html.erb +42 -0
  59. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/new.html.erb +7 -0
  60. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/show.html.erb +44 -0
  61. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_boolean.html.erb +10 -0
  62. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_color.html.erb +4 -0
  63. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_date.html.erb +6 -0
  64. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_date_array.html.erb +9 -0
  65. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_date_time.html.erb +5 -0
  66. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_decimal.html.erb +6 -0
  67. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_decimal_array.html.erb +9 -0
  68. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_email.html.erb +5 -0
  69. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_integer.html.erb +6 -0
  70. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_integer_array.html.erb +9 -0
  71. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_json.html.erb +7 -0
  72. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_long_text.html.erb +7 -0
  73. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_multi_select.html.erb +7 -0
  74. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_select.html.erb +7 -0
  75. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_text.html.erb +6 -0
  76. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_text_array.html.erb +9 -0
  77. data/lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_url.html.erb +5 -0
  78. data/lib/typed_eav/column_mapping.rb +64 -0
  79. data/lib/typed_eav/config.rb +91 -0
  80. data/lib/typed_eav/engine.rb +20 -0
  81. data/lib/typed_eav/has_typed_eav.rb +484 -0
  82. data/lib/typed_eav/query_builder.rb +133 -0
  83. data/lib/typed_eav/registry.rb +52 -0
  84. data/lib/typed_eav/version.rb +5 -0
  85. data/lib/typed_eav.rb +86 -0
  86. metadata +146 -0
@@ -0,0 +1,57 @@
1
+ <%# Shared common fields for all field definition forms.
2
+ Rendered inside a form_with block via:
3
+ render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <% if field.errors.any? %>
6
+ <div style="color: red; margin-bottom: 1rem;">
7
+ <h3><%= pluralize(field.errors.count, "error") %>:</h3>
8
+ <ul>
9
+ <% field.errors.each do |error| %>
10
+ <li><%= error.full_message %></li>
11
+ <% end %>
12
+ </ul>
13
+ </div>
14
+ <% end %>
15
+
16
+ <% if field.new_record? %>
17
+ <input type="hidden" name="type" value="<%= field.field_type_name %>" />
18
+ <% end %>
19
+
20
+ <div>
21
+ <label>Field Type</label>
22
+ <input type="text" value="<%= field.field_type_name.humanize %>" disabled readonly />
23
+ </div>
24
+
25
+ <div>
26
+ <%= f.label :entity_type %>
27
+ <% if field.persisted? %>
28
+ <%= f.text_field :entity_type, disabled: true %>
29
+ <% else %>
30
+ <%= f.select :entity_type, TypedEAV.registry.entity_types, { prompt: "Select entity..." } %>
31
+ <% end %>
32
+ </div>
33
+
34
+ <div>
35
+ <%= f.label :name %>
36
+ <%= f.text_field :name, required: true %>
37
+ </div>
38
+
39
+ <div>
40
+ <label>Scope</label>
41
+ <%# The controller derives scope from the ambient value (TypedEAV.current_scope)
42
+ and never permits it from params. Display it read-only so users can see which
43
+ partition this field belongs to without implying they can change it. %>
44
+ <input type="text"
45
+ value="<%= field.scope.presence || (field.new_record? ? (TypedEAV.current_scope || "Global (all scopes)") : "Global (all scopes)") %>"
46
+ disabled readonly />
47
+ </div>
48
+
49
+ <div>
50
+ <%= f.label :required %>
51
+ <%= f.check_box :required %>
52
+ </div>
53
+
54
+ <div>
55
+ <%= f.label :sort_order %>
56
+ <%= f.number_field :sort_order, min: 0 %>
57
+ </div>
@@ -0,0 +1,21 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_date, "Earliest Date" %>
7
+ <%= f.date_field :min_date, value: field.min_date %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_date, "Latest Date" %>
12
+ <%= f.date_field :max_date, value: field.max_date %>
13
+ </div>
14
+
15
+ <div>
16
+ <%= f.label :default_value %>
17
+ <%= f.date_field :default_value, value: field.default_value %>
18
+ </div>
19
+
20
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
21
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_size, "Min Items" %>
7
+ <%= f.number_field :min_size, min: 0, value: field.min_size %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_size, "Max Items" %>
12
+ <%= f.number_field :max_size, min: 1, value: field.max_size %>
13
+ </div>
14
+
15
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
16
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_datetime, "Earliest" %>
7
+ <%= f.datetime_local_field :min_datetime, value: field.min_datetime %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_datetime, "Latest" %>
12
+ <%= f.datetime_local_field :max_datetime, value: field.max_datetime %>
13
+ </div>
14
+
15
+ <div>
16
+ <%= f.label :default_value %>
17
+ <%= f.datetime_local_field :default_value, value: field.default_value %>
18
+ </div>
19
+
20
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
21
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min %>
7
+ <%= f.number_field :min, value: field.min, step: "any" %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max %>
12
+ <%= f.number_field :max, value: field.max, step: "any" %>
13
+ </div>
14
+
15
+ <div>
16
+ <%= f.label :default_value %>
17
+ <%= f.number_field :default_value, value: field.default_value, step: "any" %>
18
+ </div>
19
+
20
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
21
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_size, "Min Items" %>
7
+ <%= f.number_field :min_size, min: 0, value: field.min_size %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_size, "Max Items" %>
12
+ <%= f.number_field :max_size, min: 1, value: field.max_size %>
13
+ </div>
14
+
15
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
16
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :default_value %>
7
+ <%= f.text_field :default_value, value: field.default_value %>
8
+ </div>
9
+
10
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
11
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min %>
7
+ <%= f.number_field :min, value: field.min %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max %>
12
+ <%= f.number_field :max, value: field.max %>
13
+ </div>
14
+
15
+ <div>
16
+ <%= f.label :default_value %>
17
+ <%= f.number_field :default_value, value: field.default_value %>
18
+ </div>
19
+
20
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
21
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_size, "Min Items" %>
7
+ <%= f.number_field :min_size, min: 0, value: field.min_size %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_size, "Max Items" %>
12
+ <%= f.number_field :max_size, min: 1, value: field.max_size %>
13
+ </div>
14
+
15
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
16
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :default_value %>
7
+ <%= f.text_field :default_value, value: field.default_value %>
8
+ </div>
9
+
10
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
11
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_length %>
7
+ <%= f.number_field :min_length, min: 0, value: field.min_length %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_length %>
12
+ <%= f.number_field :max_length, min: 1, value: field.max_length %>
13
+ </div>
14
+
15
+ <div>
16
+ <%= f.label :default_value %>
17
+ <%= f.text_area :default_value, value: field.default_value, rows: 3 %>
18
+ </div>
19
+
20
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
21
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
6
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <% if field.persisted? && field.field_options.any? %>
6
+ <div>
7
+ <%= f.label :default_value %>
8
+ <%= f.select :default_value, field.field_options.sorted.map { |o| [o.label, o.value] },
9
+ { include_blank: "None", selected: field.default_value } %>
10
+ </div>
11
+ <% end %>
12
+
13
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
14
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_length %>
7
+ <%= f.number_field :min_length, min: 0, value: field.min_length %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_length %>
12
+ <%= f.number_field :max_length, min: 1, value: field.max_length %>
13
+ </div>
14
+
15
+ <div>
16
+ <%= f.label :pattern, "Regex Pattern" %>
17
+ <%= f.text_field :pattern, value: field.pattern, placeholder: "e.g. ^[A-Z]+" %>
18
+ </div>
19
+
20
+ <div>
21
+ <%= f.label :default_value %>
22
+ <%= f.text_field :default_value, value: field.default_value %>
23
+ </div>
24
+
25
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
26
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :min_size, "Min Items" %>
7
+ <%= f.number_field :min_size, min: 0, value: field.min_size %>
8
+ </div>
9
+
10
+ <div>
11
+ <%= f.label :max_size, "Max Items" %>
12
+ <%= f.number_field :max_size, min: 1, value: field.max_size %>
13
+ </div>
14
+
15
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
16
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <%= form_with(model: field, scope: :typed_eav_field,
2
+ url: field.persisted? ? typed_eav_field_path(field) : typed_eav_fields_path) do |f| %>
3
+ <%= render "typed_eav/forms/common_fields", f: f, field: field %>
4
+
5
+ <div>
6
+ <%= f.label :default_value %>
7
+ <%= f.text_field :default_value, value: field.default_value %>
8
+ </div>
9
+
10
+ <div><%= f.submit field.persisted? ? "Update" : "Create" %></div>
11
+ <% end %>
@@ -0,0 +1,42 @@
1
+ <h1>Typed Fields</h1>
2
+
3
+ <div style="margin-bottom: 1rem;">
4
+ <%= form_with url: new_typed_eav_field_path, method: :get do |f| %>
5
+ <%= f.select :type, TypedEAV.config.type_names %>
6
+ <%= f.submit "New Field" %>
7
+ <% end %>
8
+ </div>
9
+
10
+ <table>
11
+ <thead>
12
+ <tr>
13
+ <th></th>
14
+ <th>ID</th>
15
+ <th>Type</th>
16
+ <th>Entity</th>
17
+ <th>Name</th>
18
+ <th>Required</th>
19
+ <th>Scope</th>
20
+ <th>Options</th>
21
+ <th>Default</th>
22
+ <th></th>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <% @fields.each do |field| %>
27
+ <tr>
28
+ <td><%= link_to "Edit", edit_typed_eav_field_path(field) %></td>
29
+ <td><%= link_to "##{field.id}", typed_eav_field_path(field) %></td>
30
+ <td><%= field.field_type_name %></td>
31
+ <td><%= field.entity_type %></td>
32
+ <td><%= field.name %></td>
33
+ <td><%= field.required? ? "Yes" : "" %></td>
34
+ <td><code><%= field.scope.inspect %></code></td>
35
+ <td><code><%= field.options %></code></td>
36
+ <td><code><%= field.default_value.inspect %></code></td>
37
+ <td><%= button_to "Delete", typed_eav_field_path(field), method: :delete,
38
+ data: { turbo_confirm: "Delete this field and all its values?" } %></td>
39
+ </tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
@@ -0,0 +1,7 @@
1
+ <h1>New <%= @field.field_type_name.humanize %> Field</h1>
2
+
3
+ <%= render_typed_eav_form(field: @field) %>
4
+
5
+ <div style="margin-top: 1rem;">
6
+ <%= link_to "Back", typed_eav_fields_path %>
7
+ </div>
@@ -0,0 +1,44 @@
1
+ <h1><%= @field.field_type_name.humanize %> Field: <%= @field.name %></h1>
2
+
3
+ <dl>
4
+ <dt>ID</dt>
5
+ <dd><%= @field.id %></dd>
6
+
7
+ <dt>Type</dt>
8
+ <dd><%= @field.field_type_name %></dd>
9
+
10
+ <dt>Entity Type</dt>
11
+ <dd><%= @field.entity_type %></dd>
12
+
13
+ <dt>Name</dt>
14
+ <dd><%= @field.name %></dd>
15
+
16
+ <dt>Required</dt>
17
+ <dd><%= @field.required? ? "Yes" : "No" %></dd>
18
+
19
+ <dt>Scope</dt>
20
+ <dd><code><%= @field.scope.inspect %></code></dd>
21
+
22
+ <dt>Options</dt>
23
+ <dd><code><%= @field.options %></code></dd>
24
+
25
+ <dt>Default Value</dt>
26
+ <dd><code><%= @field.default_value.inspect %></code></dd>
27
+
28
+ <dt>Values Count</dt>
29
+ <dd><%= @field.values.count %></dd>
30
+ </dl>
31
+
32
+ <% if @field.optionable? && @field.field_options.any? %>
33
+ <h2>Options</h2>
34
+ <ul>
35
+ <% @field.field_options.sorted.each do |option| %>
36
+ <li><%= option.label %> (<code><%= option.value %></code>)</li>
37
+ <% end %>
38
+ </ul>
39
+ <% end %>
40
+
41
+ <div style="margin-top: 1rem;">
42
+ <%= link_to "Edit", edit_typed_eav_field_path(@field) %> |
43
+ <%= link_to "Back", typed_eav_fields_path %>
44
+ </div>
@@ -0,0 +1,10 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <% if field.required? %>
4
+ <%= form.check_box :value, { checked: typed_value.value }, "true", "false" %>
5
+ <% else %>
6
+ <%= form.select :value,
7
+ [["", ""], ["Yes", "true"], ["No", "false"]],
8
+ { selected: typed_value.value.nil? ? "" : typed_value.value.to_s } %>
9
+ <% end %>
10
+ </div>
@@ -0,0 +1,4 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.color_field :value, value: typed_value.value || "#000000" %>
4
+ </div>
@@ -0,0 +1,6 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.date_field :value, value: typed_value.value,
4
+ min: field.try(:min_date), max: field.try(:max_date),
5
+ required: field.required? %>
6
+ </div>
@@ -0,0 +1,9 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= render_array_field(
4
+ form: form,
5
+ name: :value,
6
+ value: typed_value.value,
7
+ field_method: :date_field,
8
+ field_opts: {}) %>
9
+ </div>
@@ -0,0 +1,5 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.datetime_local_field :value, value: typed_value.value,
4
+ required: field.required? %>
5
+ </div>
@@ -0,0 +1,6 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.number_field :value, value: typed_value.value,
4
+ min: field.try(:min), max: field.try(:max),
5
+ step: "any", required: field.required? %>
6
+ </div>
@@ -0,0 +1,9 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= render_array_field(
4
+ form: form,
5
+ name: :value,
6
+ value: typed_value.value,
7
+ field_method: :number_field,
8
+ field_opts: { step: "any" }) %>
9
+ </div>
@@ -0,0 +1,5 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.email_field :value, value: typed_value.value,
4
+ required: field.required? %>
5
+ </div>
@@ -0,0 +1,6 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.number_field :value, value: typed_value.value,
4
+ min: field.try(:min), max: field.try(:max),
5
+ required: field.required? %>
6
+ </div>
@@ -0,0 +1,9 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= render_array_field(
4
+ form: form,
5
+ name: :value,
6
+ value: typed_value.value,
7
+ field_method: :number_field,
8
+ field_opts: { min: field.try(:min), max: field.try(:max) }) %>
9
+ </div>
@@ -0,0 +1,7 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.text_area :value,
4
+ value: typed_value.value.is_a?(String) ? typed_value.value : typed_value.value&.to_json,
5
+ rows: 4,
6
+ placeholder: '{"key": "value"}' %>
7
+ </div>
@@ -0,0 +1,7 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.text_area :value, value: typed_value.value,
4
+ maxlength: field.try(:max_length),
5
+ required: field.required?,
6
+ rows: 4 %>
7
+ </div>
@@ -0,0 +1,7 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.select :value,
4
+ field.field_options.sorted.map { |o| [o.label, o.value] },
5
+ { selected: Array(typed_value.value) },
6
+ { multiple: true, required: field.required? } %>
7
+ </div>
@@ -0,0 +1,7 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.select :value,
4
+ field.field_options.sorted.map { |o| [o.label, o.value] },
5
+ { include_blank: !field.required?, selected: typed_value.value },
6
+ { required: field.required? } %>
7
+ </div>
@@ -0,0 +1,6 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.text_field :value, value: typed_value.value,
4
+ maxlength: field.try(:max_length),
5
+ required: field.required? %>
6
+ </div>
@@ -0,0 +1,9 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= render_array_field(
4
+ form: form,
5
+ name: :value,
6
+ value: typed_value.value,
7
+ field_method: :text_field,
8
+ field_opts: {}) %>
9
+ </div>
@@ -0,0 +1,5 @@
1
+ <div>
2
+ <%= form.label :value, field.name %>
3
+ <%= form.url_field :value, value: typed_value.value,
4
+ required: field.required? %>
5
+ </div>
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedEAV
4
+ # Maps field types to their native database column on the values table.
5
+ #
6
+ # This is the core concept borrowed from Relaticle's hybrid EAV:
7
+ # instead of serializing everything into a jsonb blob, each value
8
+ # type gets its own column so the database can natively index,
9
+ # sort, and enforce constraints.
10
+ #
11
+ # Usage in field type classes:
12
+ #
13
+ # class TypedEAV::Field::Integer < TypedEAV::Field::Base
14
+ # value_column :integer_value
15
+ # end
16
+ #
17
+ # The value model reads this to know which column to read/write.
18
+ # The query builder reads this to know which column to filter on.
19
+ # ActiveRecord handles all type casting automatically via the
20
+ # column's registered ActiveRecord::Type.
21
+ module ColumnMapping
22
+ extend ActiveSupport::Concern
23
+
24
+ DEFAULT_OPERATORS_BY_COLUMN = {
25
+ boolean_value: %i[eq not_eq is_null is_not_null],
26
+ string_value: %i[eq not_eq contains not_contains starts_with ends_with is_null is_not_null],
27
+ text_value: %i[eq not_eq contains not_contains starts_with ends_with is_null is_not_null],
28
+ integer_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
29
+ decimal_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
30
+ date_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
31
+ datetime_value: %i[eq not_eq gt gteq lt lteq between is_null is_not_null],
32
+ json_value: %i[contains is_null is_not_null],
33
+ }.freeze
34
+ FALLBACK_OPERATORS = %i[eq not_eq is_null is_not_null].freeze
35
+
36
+ class_methods do
37
+ # Declare which typed column this field type stores its value in.
38
+ def value_column(column_name = nil)
39
+ unless column_name
40
+ return @value_column || raise(NotImplementedError,
41
+ "#{name} must declare `value_column :column_name`")
42
+ end
43
+
44
+ @value_column = column_name.to_sym
45
+ end
46
+
47
+ # All operators this field type supports for querying.
48
+ # Subclasses can override to restrict or extend.
49
+ def supported_operators
50
+ @supported_operators || default_operators_for(value_column)
51
+ end
52
+
53
+ def operators(*ops)
54
+ @supported_operators = ops.map(&:to_sym)
55
+ end
56
+
57
+ private
58
+
59
+ def default_operators_for(col)
60
+ DEFAULT_OPERATORS_BY_COLUMN.fetch(col, FALLBACK_OPERATORS)
61
+ end
62
+ end
63
+ end
64
+ end