data_porter 0.1.0 → 0.4.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 (168) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -1
  3. data/README.md +63 -386
  4. data/ROADMAP.md +89 -0
  5. data/app/assets/javascripts/data_porter/stimulus.min.js +2 -0
  6. data/app/assets/javascripts/data_porter/turbo.min.js +29 -0
  7. data/app/assets/stylesheets/data_porter/alerts.css +25 -0
  8. data/app/assets/stylesheets/data_porter/application.css +12 -646
  9. data/app/assets/stylesheets/data_porter/badges.css +73 -0
  10. data/app/assets/stylesheets/data_porter/base.css +56 -0
  11. data/app/assets/stylesheets/data_porter/cards.css +60 -0
  12. data/app/assets/stylesheets/data_porter/layout.css +128 -0
  13. data/app/assets/stylesheets/data_porter/mapping.css +79 -0
  14. data/app/assets/stylesheets/data_porter/modal.css +49 -0
  15. data/app/assets/stylesheets/data_porter/preview.css +24 -0
  16. data/app/assets/stylesheets/data_porter/progress.css +37 -0
  17. data/app/assets/stylesheets/data_porter/table.css +45 -0
  18. data/app/controllers/data_porter/imports_controller.rb +74 -10
  19. data/app/controllers/data_porter/mapping_templates_controller.rb +85 -0
  20. data/app/javascript/data_porter/mapping_controller.js +86 -0
  21. data/app/javascript/data_porter/progress_controller.js +1 -1
  22. data/app/javascript/data_porter/template_form_controller.js +46 -0
  23. data/app/jobs/data_porter/extract_headers_job.rb +12 -0
  24. data/app/models/data_porter/data_import.rb +8 -2
  25. data/app/models/data_porter/mapping_template.rb +15 -0
  26. data/app/views/data_porter/imports/index.html.erb +9 -8
  27. data/app/views/data_porter/imports/new.html.erb +10 -4
  28. data/app/views/data_porter/imports/show.html.erb +41 -13
  29. data/app/views/data_porter/mapping_templates/_form.html.erb +40 -0
  30. data/app/views/data_porter/mapping_templates/edit.html.erb +11 -0
  31. data/app/views/data_porter/mapping_templates/index.html.erb +42 -0
  32. data/app/views/data_porter/mapping_templates/new.html.erb +11 -0
  33. data/app/views/layouts/data_porter/application.html.erb +162 -0
  34. data/config/routes.rb +3 -0
  35. data/docs/CONFIGURATION.md +81 -0
  36. data/docs/MAPPING.md +44 -0
  37. data/docs/SOURCES.md +94 -0
  38. data/docs/TARGETS.md +176 -0
  39. data/docs/screenshots/mapping.jpg +0 -0
  40. data/lib/data_porter/components/mapping/column_row.rb +52 -0
  41. data/lib/data_porter/components/mapping/form.rb +127 -0
  42. data/lib/data_porter/components/mapping/template_select.rb +35 -0
  43. data/lib/data_porter/components/preview/results_summary.rb +21 -0
  44. data/lib/data_porter/components/preview/summary_cards.rb +32 -0
  45. data/lib/data_porter/components/preview/table.rb +56 -0
  46. data/lib/data_porter/components/progress/bar.rb +35 -0
  47. data/lib/data_porter/components/shared/failure_alert.rb +22 -0
  48. data/lib/data_porter/components/shared/status_badge.rb +18 -0
  49. data/lib/data_porter/components.rb +9 -6
  50. data/lib/data_porter/configuration.rb +1 -1
  51. data/lib/data_porter/engine.rb +7 -1
  52. data/lib/data_porter/orchestrator.rb +21 -1
  53. data/lib/data_porter/sources/base.rb +18 -3
  54. data/lib/data_porter/sources/csv.rb +5 -0
  55. data/lib/data_porter/sources/xlsx.rb +76 -0
  56. data/lib/data_porter/sources.rb +3 -1
  57. data/lib/data_porter/version.rb +1 -1
  58. data/lib/generators/data_porter/install/install_generator.rb +4 -0
  59. data/lib/generators/data_porter/install/templates/create_data_porter_mapping_templates.rb.erb +16 -0
  60. data/lib/generators/data_porter/install/templates/initializer.rb +1 -1
  61. metadata +72 -135
  62. data/.claude/commands/blog-status.md +0 -10
  63. data/.claude/commands/blog.md +0 -109
  64. data/.claude/commands/task-done.md +0 -27
  65. data/.claude/commands/tm/add-dependency.md +0 -58
  66. data/.claude/commands/tm/add-subtask.md +0 -79
  67. data/.claude/commands/tm/add-task.md +0 -81
  68. data/.claude/commands/tm/analyze-complexity.md +0 -124
  69. data/.claude/commands/tm/analyze-project.md +0 -100
  70. data/.claude/commands/tm/auto-implement-tasks.md +0 -100
  71. data/.claude/commands/tm/command-pipeline.md +0 -80
  72. data/.claude/commands/tm/complexity-report.md +0 -120
  73. data/.claude/commands/tm/convert-task-to-subtask.md +0 -74
  74. data/.claude/commands/tm/expand-all-tasks.md +0 -52
  75. data/.claude/commands/tm/expand-task.md +0 -52
  76. data/.claude/commands/tm/fix-dependencies.md +0 -82
  77. data/.claude/commands/tm/help.md +0 -101
  78. data/.claude/commands/tm/init-project-quick.md +0 -49
  79. data/.claude/commands/tm/init-project.md +0 -53
  80. data/.claude/commands/tm/install-taskmaster.md +0 -118
  81. data/.claude/commands/tm/learn.md +0 -106
  82. data/.claude/commands/tm/list-tasks-by-status.md +0 -42
  83. data/.claude/commands/tm/list-tasks-with-subtasks.md +0 -30
  84. data/.claude/commands/tm/list-tasks.md +0 -46
  85. data/.claude/commands/tm/next-task.md +0 -69
  86. data/.claude/commands/tm/parse-prd-with-research.md +0 -51
  87. data/.claude/commands/tm/parse-prd.md +0 -52
  88. data/.claude/commands/tm/project-status.md +0 -67
  89. data/.claude/commands/tm/quick-install-taskmaster.md +0 -23
  90. data/.claude/commands/tm/remove-all-subtasks.md +0 -94
  91. data/.claude/commands/tm/remove-dependency.md +0 -65
  92. data/.claude/commands/tm/remove-subtask.md +0 -87
  93. data/.claude/commands/tm/remove-subtasks.md +0 -89
  94. data/.claude/commands/tm/remove-task.md +0 -110
  95. data/.claude/commands/tm/setup-models.md +0 -52
  96. data/.claude/commands/tm/show-task.md +0 -85
  97. data/.claude/commands/tm/smart-workflow.md +0 -58
  98. data/.claude/commands/tm/sync-readme.md +0 -120
  99. data/.claude/commands/tm/tm-main.md +0 -147
  100. data/.claude/commands/tm/to-cancelled.md +0 -58
  101. data/.claude/commands/tm/to-deferred.md +0 -50
  102. data/.claude/commands/tm/to-done.md +0 -47
  103. data/.claude/commands/tm/to-in-progress.md +0 -39
  104. data/.claude/commands/tm/to-pending.md +0 -35
  105. data/.claude/commands/tm/to-review.md +0 -43
  106. data/.claude/commands/tm/update-single-task.md +0 -122
  107. data/.claude/commands/tm/update-task.md +0 -75
  108. data/.claude/commands/tm/update-tasks-from-id.md +0 -111
  109. data/.claude/commands/tm/validate-dependencies.md +0 -72
  110. data/.claude/commands/tm/view-models.md +0 -52
  111. data/.env.example +0 -12
  112. data/.mcp.json +0 -24
  113. data/.taskmaster/CLAUDE.md +0 -435
  114. data/.taskmaster/config.json +0 -44
  115. data/.taskmaster/docs/prd.txt +0 -2044
  116. data/.taskmaster/state.json +0 -6
  117. data/.taskmaster/tasks/task_001.md +0 -19
  118. data/.taskmaster/tasks/task_002.md +0 -19
  119. data/.taskmaster/tasks/task_003.md +0 -19
  120. data/.taskmaster/tasks/task_004.md +0 -19
  121. data/.taskmaster/tasks/task_005.md +0 -19
  122. data/.taskmaster/tasks/task_006.md +0 -19
  123. data/.taskmaster/tasks/task_007.md +0 -19
  124. data/.taskmaster/tasks/task_008.md +0 -19
  125. data/.taskmaster/tasks/task_009.md +0 -19
  126. data/.taskmaster/tasks/task_010.md +0 -19
  127. data/.taskmaster/tasks/task_011.md +0 -19
  128. data/.taskmaster/tasks/task_012.md +0 -19
  129. data/.taskmaster/tasks/task_013.md +0 -19
  130. data/.taskmaster/tasks/task_014.md +0 -19
  131. data/.taskmaster/tasks/task_015.md +0 -19
  132. data/.taskmaster/tasks/task_016.md +0 -19
  133. data/.taskmaster/tasks/task_017.md +0 -19
  134. data/.taskmaster/tasks/task_018.md +0 -19
  135. data/.taskmaster/tasks/task_019.md +0 -19
  136. data/.taskmaster/tasks/task_020.md +0 -19
  137. data/.taskmaster/tasks/tasks.json +0 -299
  138. data/.taskmaster/templates/example_prd.txt +0 -47
  139. data/.taskmaster/templates/example_prd_rpg.txt +0 -511
  140. data/CLAUDE.md +0 -65
  141. data/config/database.yml +0 -3
  142. data/docs/SPEC.md +0 -2012
  143. data/docs/UI.md +0 -32
  144. data/docs/blog/001-why-build-a-data-import-engine.md +0 -166
  145. data/docs/blog/002-scaffolding-a-rails-engine.md +0 -188
  146. data/docs/blog/003-configuration-dsl.md +0 -222
  147. data/docs/blog/004-store-model-jsonb.md +0 -237
  148. data/docs/blog/005-target-dsl.md +0 -284
  149. data/docs/blog/006-parsing-csv-sources.md +0 -300
  150. data/docs/blog/007-orchestrator.md +0 -247
  151. data/docs/blog/008-actioncable-stimulus.md +0 -376
  152. data/docs/blog/009-phlex-ui-components.md +0 -446
  153. data/docs/blog/010-controllers-routing.md +0 -374
  154. data/docs/blog/011-generators.md +0 -364
  155. data/docs/blog/012-json-api-sources.md +0 -323
  156. data/docs/blog/013-testing-rails-engine.md +0 -618
  157. data/docs/blog/014-dry-run.md +0 -307
  158. data/docs/blog/015-publishing-retro.md +0 -264
  159. data/docs/blog/016-erb-view-templates.md +0 -431
  160. data/docs/blog/017-showcase-final-retro.md +0 -220
  161. data/docs/blog/BACKLOG.md +0 -8
  162. data/docs/blog/SERIES.md +0 -154
  163. data/lib/data_porter/components/failure_alert.rb +0 -20
  164. data/lib/data_porter/components/preview_table.rb +0 -54
  165. data/lib/data_porter/components/progress_bar.rb +0 -33
  166. data/lib/data_porter/components/results_summary.rb +0 -19
  167. data/lib/data_porter/components/status_badge.rb +0 -16
  168. data/lib/data_porter/components/summary_cards.rb +0 -30
data/docs/blog/SERIES.md DELETED
@@ -1,154 +0,0 @@
1
- ---
2
- series: "Building DataPorter - A Data Import Engine for Rails"
3
- author: ""
4
- repo: ""
5
- status: complete
6
- ---
7
-
8
- # Series Plan
9
-
10
- ## Target audience
11
- Ruby/Rails developers (intermediate+) interested in gem architecture, Rails engines, and clean DSL design.
12
-
13
- ## Series structure
14
- Each article: ~5-8 min read, one clear concept, real code, decisions explained.
15
-
16
- ---
17
-
18
- ## Articles
19
-
20
- ### Part 1 — Why build a data import engine?
21
- - **Tasks:** None (intro)
22
- - **Hook:** The repetitive import pattern in Rails apps
23
- - **Covers:** Problem statement, existing solutions (maintenance_tasks, activeadmin_import), why a new gem, what we'll build (3-step workflow diagram)
24
- - **Key decisions:** Mountable engine vs. lib, isolate_namespace, target audience
25
- - **File:** `docs/blog/001-why-build-a-data-import-engine.md`
26
- - **Status:** draft
27
-
28
- ### Part 2 — Scaffolding a Rails Engine gem
29
- - **Tasks:** #1
30
- - **Hook:** `bundle gem` is just the start
31
- - **Covers:** Gem structure, Engine setup, isolate_namespace, directory tree, gemspec dependencies
32
- - **Key decisions:** Why isolate_namespace, directory conventions, dependency choices (store_model, phlex, turbo)
33
- - **File:** `docs/blog/002-scaffolding-a-rails-engine.md`
34
- - **Status:** draft
35
-
36
- ### Part 3 — Configuration DSL: making the gem flexible
37
- - **Tasks:** #2
38
- - **Hook:** A good gem adapts to any host app
39
- - **Covers:** Configuration singleton pattern, DSL with `configure` block, sensible defaults, context_builder lambda
40
- - **Key decisions:** Why not Rails config, lambda vs block for context, what to make configurable vs. convention
41
- - **File:** `docs/blog/003-configuration-dsl.md`
42
- - **Status:** draft
43
-
44
- ### Part 4 — Modeling import data with StoreModel & JSONB
45
- - **Tasks:** #3, #4
46
- - **Hook:** Storing structured data without extra tables
47
- - **Covers:** StoreModel gem, ImportRecord/Error/Report models, JSONB attributes, TypeValidator
48
- - **Key decisions:** JSONB vs. separate tables, store_model vs. hand-rolled, validation strategy (column-level vs DB-level)
49
- - **File:** `docs/blog/004-store-model-jsonb.md`
50
- - **Status:** draft
51
-
52
- ### Part 5 — Designing a Target DSL
53
- - **Tasks:** #5, #6
54
- - **Hook:** One file = one import type, zero boilerplate
55
- - **Covers:** Target base class, class-level DSL (label, model, columns, csv_mapping), Registry pattern, auto-discovery
56
- - **Key decisions:** DSL design (class methods vs. instance), class_attribute vs. class instance vars, hook pattern for extensibility
57
- - **File:** `docs/blog/005-target-dsl.md`
58
- - **Status:** draft
59
-
60
- ### Part 6 — Parsing CSV data with Sources
61
- - **Tasks:** #7, #8
62
- - **Hook:** The first end-to-end flow
63
- - **Covers:** DataImport model, migration, Source base class, CSV source, ActiveStorage integration, column mapping
64
- - **Key decisions:** Polymorphic user association, enum state machine, auto-mapping vs explicit mapping
65
- - **File:** `docs/blog/006-parsing-csv-sources.md`
66
- - **Status:** draft
67
-
68
- ### Part 7 — The Orchestrator: coordinating the import workflow
69
- - **Tasks:** #9, #10
70
- - **Hook:** The brain of the engine
71
- - **Covers:** Orchestrator class (parse! and import!), state transitions, error handling per-record, ActiveJob integration
72
- - **Key decisions:** Why an orchestrator (not controller logic), transaction boundaries, error recovery strategy
73
- - **File:** `docs/blog/007-orchestrator.md`
74
- - **Status:** draft
75
-
76
- ### Part 8 — Real-time progress with ActionCable & Stimulus
77
- - **Tasks:** #11, #14
78
- - **Hook:** Users hate staring at a spinner with no feedback
79
- - **Covers:** Broadcaster service, ImportChannel, Stimulus controller, progress bar updates, auto-reload on completion
80
- - **Key decisions:** ActionCable vs. polling vs. SSE, channel naming strategy, Stimulus vs. Turbo Streams
81
- - **File:** `docs/blog/008-actioncable-stimulus.md`
82
- - **Status:** draft
83
-
84
- ### Part 9 — Building the UI with Phlex & Tailwind
85
- - **Tasks:** #13
86
- - **Hook:** Auto-generated preview tables from a DSL
87
- - **Covers:** Phlex components, scoped Tailwind (dp- prefix), preview table with dynamic columns, status badges, CSS custom properties for theming
88
- - **Key decisions:** Phlex vs. ERB/ViewComponent, Tailwind prefix strategy, how to not pollute host app styles
89
- - **File:** `docs/blog/009-phlex-ui-components.md`
90
- - **Status:** draft
91
-
92
- ### Part 10 — Controllers & routing in a Rails Engine
93
- - **Tasks:** #12
94
- - **Hook:** Engine controllers are tricky — here's the clean way
95
- - **Covers:** ImportsController, inheriting from host's parent controller, engine routes, strong params, Turbo integration
96
- - **Key decisions:** Dynamic parent controller inheritance, route namespacing, how auth flows from host to engine
97
- - **File:** `docs/blog/010-controllers-routing.md`
98
- - **Status:** draft
99
-
100
- ### Part 11 — Generators: install & target scaffolding
101
- - **Tasks:** #15, #16
102
- - **Hook:** Great gems install in one command
103
- - **Covers:** Install generator (migration, initializer, routes), Target generator (column parsing, template), Rails::Generators API
104
- - **Key decisions:** What to generate vs. what to configure, template format (ERB .tt), route injection strategy
105
- - **File:** `docs/blog/011-generators.md`
106
- - **Status:** draft
107
-
108
- ### Part 12 — Adding JSON & API sources
109
- - **Tasks:** #18, #19
110
- - **Hook:** CSV is just the beginning
111
- - **Covers:** JSON source (file + raw text), API source (HTTP client, params injection), source plugin architecture
112
- - **Key decisions:** Source abstraction design, how to handle auth headers, response_root extraction
113
- - **File:** `docs/blog/012-json-api-sources.md`
114
- - **Status:** draft
115
-
116
- ### Part 13 — Testing a Rails Engine with RSpec
117
- - **Tasks:** #17
118
- - **Hook:** Testing an engine is different from testing an app
119
- - **Covers:** Dummy app setup, spec organization, testing StoreModels, mocking ActiveStorage, testing jobs, controller specs
120
- - **Key decisions:** Dummy app vs. shared examples, factory vs. fixture, what to unit test vs. integration test
121
- - **File:** `docs/blog/013-testing-rails-engine.md`
122
- - **Status:** draft
123
-
124
- ### Part 14 — Dry Run: validate before you persist
125
- - **Tasks:** #20
126
- - **Hook:** Preview catches column errors, dry-run catches DB errors
127
- - **Covers:** Transaction + rollback pattern, enriching records with DB errors, DryRunJob, UI integration
128
- - **Key decisions:** Why two validation layers, transaction rollback approach, when to offer dry-run
129
- - **File:** `docs/blog/014-dry-run.md`
130
- - **Status:** draft
131
-
132
- ### Part 15 — Publishing the gem & retrospective
133
- - **Tasks:** None (wrap-up)
134
- - **Hook:** From idea to rubygems.org
135
- - **Covers:** Gemspec final polish, CHANGELOG, versioning, publishing, series recap, what worked, what we'd do differently
136
- - **Key decisions:** Semantic versioning strategy, open source considerations
137
- - **File:** `docs/blog/015-publishing-retro.md`
138
- - **Status:** draft
139
-
140
- ### Part 16 — ERB View Templates: Composing Phlex Components
141
- - **Tasks:** None (view layer completion)
142
- - **Hook:** Phlex components are pure Ruby. ERB is what the browser sees. The glue is `.call`.
143
- - **Covers:** ERB templates composing Phlex, has_one_attached bugfix, ActiveStorage test setup, view testing in Rails 8, plain CSS stylesheet
144
- - **Key decisions:** raw component.call vs phlex-rails, plain CSS vs build step, view test infrastructure
145
- - **File:** `docs/blog/016-erb-view-templates.md`
146
- - **Status:** draft
147
-
148
- ### Part 17 — Showcase & Final Retrospective
149
- - **Tasks:** None (series finale)
150
- - **Hook:** 17 articles, 22 components, one complete gem. DataPorter in action.
151
- - **Covers:** Full workflow walkthrough with screenshots, architecture overview, series recap, lessons learned, what's next
152
- - **Key decisions:** N/A (retrospective)
153
- - **File:** `docs/blog/017-showcase-final-retro.md`
154
- - **Status:** draft
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DataPorter
4
- module Components
5
- class FailureAlert < Base
6
- def initialize(report:)
7
- super()
8
- @report = report
9
- end
10
-
11
- def view_template
12
- div(class: "dp-alert dp-alert--danger") do
13
- @report.error_reports.each do |err|
14
- p { err.message }
15
- end
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DataPorter
4
- module Components
5
- class PreviewTable < Base
6
- def initialize(columns:, records:)
7
- super()
8
- @columns = columns
9
- @records = records
10
- end
11
-
12
- def view_template
13
- div(class: "dp-preview-table") do
14
- table(class: "dp-table") do
15
- render_header
16
- render_body
17
- end
18
- end
19
- end
20
-
21
- private
22
-
23
- def render_header
24
- thead do
25
- tr do
26
- th { "#" }
27
- th { "Status" }
28
- @columns.each { |col| th { col.label } }
29
- th { "Errors" }
30
- end
31
- end
32
- end
33
-
34
- def render_body
35
- tbody do
36
- @records.each { |record| render_row(record) }
37
- end
38
- end
39
-
40
- def render_row(record)
41
- tr(class: "dp-row--#{record.status}") do
42
- td { record.line_number.to_s }
43
- td { record.status }
44
- @columns.each { |col| td { record.data[col.name.to_s].to_s } }
45
- td(class: "dp-errors") { error_messages(record) }
46
- end
47
- end
48
-
49
- def error_messages(record)
50
- record.errors_list.map(&:message).join(", ")
51
- end
52
- end
53
- end
54
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DataPorter
4
- module Components
5
- class ProgressBar < Base
6
- def initialize(import_id:)
7
- super()
8
- @import_id = import_id
9
- end
10
-
11
- def view_template
12
- div(class: "dp-progress", **stimulus_controller_attrs) do
13
- render_bar
14
- end
15
- end
16
-
17
- private
18
-
19
- def render_bar
20
- div(class: "dp-progress-bar", data_data_porter__progress_target: "bar", style: "width: 0%") do
21
- span(data_data_porter__progress_target: "text") { "0%" }
22
- end
23
- end
24
-
25
- def stimulus_controller_attrs
26
- {
27
- data_controller: "data-porter--progress",
28
- data_data_porter__progress_id_value: @import_id.to_s
29
- }
30
- end
31
- end
32
- end
33
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DataPorter
4
- module Components
5
- class ResultsSummary < Base
6
- def initialize(report:)
7
- super()
8
- @report = report
9
- end
10
-
11
- def view_template
12
- div(class: "dp-results") do
13
- p { "Created: #{@report.imported_count}" }
14
- p { "Errors: #{@report.errored_count}" }
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DataPorter
4
- module Components
5
- class StatusBadge < Base
6
- def initialize(status:)
7
- super()
8
- @status = status.to_s
9
- end
10
-
11
- def view_template
12
- span(class: "dp-badge dp-badge--#{@status}") { @status.capitalize }
13
- end
14
- end
15
- end
16
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DataPorter
4
- module Components
5
- class SummaryCards < Base
6
- def initialize(report:)
7
- super()
8
- @report = report
9
- end
10
-
11
- def view_template
12
- div(class: "dp-summary-cards") do
13
- card("dp-card--complete", @report.complete_count, "Ready")
14
- card("dp-card--partial", @report.partial_count, "Incomplete")
15
- card("dp-card--missing", @report.missing_count, "Missing")
16
- card("dp-card--duplicate", @report.duplicate_count, "Duplicates")
17
- end
18
- end
19
-
20
- private
21
-
22
- def card(css_class, count, label)
23
- div(class: "dp-card #{css_class}") do
24
- strong { count.to_s }
25
- plain " #{label}"
26
- end
27
- end
28
- end
29
- end
30
- end