localtower 0.5.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -17
  3. data/app/controllers/localtower/pages_controller.rb +27 -12
  4. data/app/views/layouts/localtower/application.html.erb +16 -33
  5. data/app/views/localtower/pages/_alert_no_models.html.erb +3 -0
  6. data/app/views/localtower/pages/migrations.html.erb +52 -83
  7. data/app/views/localtower/pages/models.html.erb +53 -70
  8. data/app/views/localtower/pages/new_migration.html.erb +103 -0
  9. data/app/views/localtower/pages/new_model.html.erb +87 -0
  10. data/config/routes.rb +4 -4
  11. data/lib/localtower/generators/migration.rb +51 -120
  12. data/lib/localtower/generators/model.rb +52 -28
  13. data/lib/localtower/generators/service_objects/insert_array.rb +23 -0
  14. data/lib/localtower/generators/service_objects/insert_defaults.rb +15 -43
  15. data/lib/localtower/generators/service_objects/insert_indexes.rb +80 -0
  16. data/lib/localtower/generators/service_objects/insert_nullable.rb +23 -0
  17. data/lib/localtower/status.rb +1 -1
  18. data/lib/localtower/tools.rb +13 -16
  19. data/lib/localtower/version.rb +1 -1
  20. data/public/css/app.css +0 -49
  21. data/public/js/app.js +215 -70
  22. data/public/screenshots/v1.0.0/migrations.png +0 -0
  23. data/public/screenshots/v1.0.0/models.png +0 -0
  24. data/public/screenshots/v1.0.0/new_migration.png +0 -0
  25. data/public/screenshots/v1.0.0/new_model.png +0 -0
  26. data/spec/dummy/Gemfile +0 -2
  27. data/spec/dummy/Gemfile.lock +1 -10
  28. data/spec/dummy/app/models/post.rb +3 -0
  29. data/spec/dummy/app/models/user.rb +1 -0
  30. data/spec/dummy/config/environments/development.rb +1 -0
  31. data/spec/dummy/config/puma.rb +1 -1
  32. data/spec/dummy/db/migrate/20230119221452_create_users.rb +14 -0
  33. data/spec/dummy/db/migrate/20230119221751_change_users_at1674166670.rb +7 -0
  34. data/spec/dummy/db/migrate/20230119222054_create_posts.rb +11 -0
  35. data/spec/dummy/db/migrate/20230119222106_change_posts_at1674166865.rb +5 -0
  36. data/spec/dummy/db/schema.rb +20 -8
  37. data/spec/dummy/log/development.log +15281 -3345
  38. data/spec/dummy/log/localtower.log +1897 -132
  39. data/spec/dummy/log/test.log +0 -0
  40. data/spec/dummy/test/index.html +38 -0
  41. data/spec/dummy/tmp/pids/server.pid +1 -0
  42. data/spec/factories/migration.rb +25 -41
  43. data/spec/factories/model.rb +39 -25
  44. data/spec/lib/localtower/generators/migration_spec.rb +36 -63
  45. data/spec/lib/localtower/generators/model_spec.rb +43 -34
  46. data/spec/lib/localtower/generators/service_objects/insert_array_spec.rb +47 -0
  47. data/spec/lib/localtower/generators/service_objects/insert_defaults_spec.rb +30 -35
  48. data/spec/lib/localtower/generators/service_objects/insert_indexes_spec.rb +90 -0
  49. data/spec/lib/localtower/generators/service_objects/insert_nullable_spec.rb +61 -0
  50. data/spec/lib/localtower/tools_spec.rb +1 -11
  51. data/spec/spec_helper.rb +8 -3
  52. metadata +34 -18
  53. data/app/views/localtower/pages/_migrations.html.erb +0 -57
  54. data/app/views/localtower/pages/schema.html.erb +0 -67
  55. data/spec/dummy/db/migrate/20221115190039_create_users.rb +0 -13
  56. data/spec/dummy/db/migrate/20221115193020_change_users.rb +0 -8
  57. data/spec/dummy/db/migrate/20221115193532_change_users_at1668540931.rb +0 -5
  58. data/spec/dummy/db/migrate/20221115193605_change_users_at1668540964.rb +0 -5
  59. data/spec/dummy/db/migrate/20221115193637_change_users_at1668540996.rb +0 -5
  60. data/spec/dummy/db/migrate/20221115193642_change_users_at1668541001.rb +0 -5
  61. data/spec/lib/localtower/generators/relation_spec.rb +0 -65
data/public/js/app.js CHANGED
@@ -1,88 +1,187 @@
1
1
  window.MainApp = {};
2
2
 
3
+ Array.prototype.unique = function () {
4
+ return this.filter(function (value, index, self) {
5
+ return self.indexOf(value) === index;
6
+ });
7
+ };
8
+
3
9
  MainApp = {
4
- init: function() {
5
- },
10
+ init: function () {},
6
11
 
7
- ready: function() {
8
- $('.grid').masonry({
9
- itemSelector: '.grid-item',
12
+ ready: function () {
13
+ $(".grid").masonry({
14
+ itemSelector: ".grid-item",
10
15
  percentPosition: true,
11
- columnWidth: '.grid-sizer',
16
+ columnWidth: ".grid-sizer",
17
+ });
18
+
19
+ MainApp.whenActionOnElement("click", "duplicateLineNewModel", function (e) {
20
+ e.preventDefault();
21
+
22
+ if (MainApp.modelNameAndAttributesAreFilled()) {
23
+ MainApp.duplicateLine();
24
+ } else {
25
+ MainApp.notFilled();
26
+ }
27
+
28
+ return false;
12
29
  });
13
30
 
14
- MainApp.whenActionOnElement("click", "duplicate", function(e) {
31
+ MainApp.whenActionOnElement(
32
+ "click",
33
+ "duplicateLineNewMigration",
34
+ function (e) {
35
+ e.preventDefault();
36
+ MainApp.duplicateLineMigration();
37
+ return false;
38
+ }
39
+ );
40
+
41
+ MainApp.whenActionOnElement("click", "removeLineModel", function (e) {
15
42
  e.preventDefault();
16
- MainApp.duplicateLine();
43
+ MainApp.removeLineModel(e.currentTarget);
17
44
  return false;
18
45
  });
19
46
 
20
- MainApp.whenActionOnElement("click", "remove", function(e) {
47
+ MainApp.whenActionOnElement("click", "removeLineMigration", function (e) {
21
48
  e.preventDefault();
22
- MainApp.removeLine(e.currentTarget);
49
+ MainApp.removeLineMigration(e.currentTarget);
23
50
  return false;
24
51
  });
25
52
 
26
- MainApp.whenActionOnElement("click", "submit", function(e) {
53
+ // New Model
54
+ MainApp.whenActionOnElement("click", "submitNewModel", function (e) {
55
+ if (!MainApp.modelNameAndAttributesAreFilled()) {
56
+ MainApp.notFilled();
57
+ e.preventDefault();
58
+ return false;
59
+ }
60
+
27
61
  var current = $(e.currentTarget);
28
62
  $(".full-message").show();
29
63
 
30
64
  if (current.val() === "false") {
31
65
  $(".full-message-migrate").show();
32
66
  }
67
+ });
33
68
 
69
+ // New Migration
70
+ MainApp.whenActionOnElement("click", "submitNewMigration", function (e) {
71
+ var current = $(e.currentTarget);
72
+ $(".full-message").show();
73
+
74
+ if (current.val() === "false") {
75
+ $(".full-message-migrate").show();
76
+ }
34
77
  });
35
78
 
36
- MainApp.whenActionOnElement("change", "action", function(e) {
79
+ MainApp.whenActionOnElement("change", "action", function (e) {
37
80
  MainApp.adaptLines();
38
81
  });
39
82
 
40
83
  MainApp.adaptLines();
41
84
  MainApp.sanitizeInputs();
42
-
43
85
  hljs.highlightAll();
44
86
  },
45
87
 
46
88
  // INSTANCE --------------------------------------------------
47
- sanitizeInputs: function() {
48
- $('[data-sain]').keyup(function(el) {
49
- var currentInputValue = $(el.currentTarget).val();
50
- var cleanInputValue = currentInputValue.replace(/[^a-zA-Z0-9]/g, '');
51
- $(el.currentTarget).val(cleanInputValue);
52
- })
89
+ sanitizeInputs: function () {
90
+ function camelize(str) {
91
+ return str
92
+ .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
93
+ return word.toUpperCase();
94
+ })
95
+ .replace(/\s+/g, "");
96
+ }
97
+
98
+ function snakeCase(str) {
99
+ return str
100
+ .replace(/\ /g, "_")
101
+ .replace(/[^a-zA-Z0-9\_]/g, "")
102
+ .replace(/\_\_/g, "_")
103
+ .toLowerCase();
104
+ }
105
+
106
+ // Model name
107
+ MainApp.whenActionOnElement("keyup", "modelName", function (e) {
108
+ var currentInputValue = $(e.currentTarget).val();
109
+ var cleanInputValue = currentInputValue.replace(/[^a-zA-Z0-9]/g, "");
110
+ cleanInputValue = camelize(cleanInputValue);
111
+ $(e.currentTarget).val(cleanInputValue);
112
+ });
113
+
114
+ // Model attributes
115
+ MainApp.whenActionOnElement("keyup", "attributeName", function (e) {
116
+ var currentInputValue = $(e.currentTarget).val();
117
+ $(e.currentTarget).val(snakeCase(currentInputValue));
118
+ });
119
+
120
+ // Migration
121
+ MainApp.whenActionOnElement("keyup", "column_text", function (e) {
122
+ var currentInputValue = $(e.currentTarget).val();
123
+ $(e.currentTarget).val(snakeCase(currentInputValue));
124
+ });
53
125
  },
54
126
 
127
+ modelNameAndAttributesAreFilled: function () {
128
+ // attributes name:
129
+ var valuesForAttributes = [];
130
+ MainApp.bySelector("attributeName").each(function (el) {
131
+ valuesForAttributes.push($(this).val());
132
+ });
133
+ // model name:
134
+ valuesForAttributes.push(MainApp.bySelector("modelName").first().val());
135
+
136
+ return (
137
+ valuesForAttributes.filter(function (n) {
138
+ return n === "";
139
+ }).length === 0
140
+ );
141
+ },
55
142
 
143
+ notFilled: function () {
144
+ alert("Please fill all the fields");
145
+ },
56
146
 
57
147
  // This is a little bit dirty but it works well for the moment:
58
148
  // We dynamically show/hide fields
59
- adaptLines: function() {
60
- $.each(MainApp.bySelector("tr"), function() {
61
- var $tr = $(this);
149
+ adaptLines: function () {
150
+ $.each(MainApp.bySelector("table").find("table"), function (table) {
151
+ var $table = $(this);
62
152
 
63
- var action_input = $tr.find("[data-selector='action']");
153
+ var action_input = $table.find("[data-selector='action']");
64
154
  var action = action_input.val();
65
155
 
66
- var belongs_to_input = $tr.find("[data-selector='belongs_to']");
67
- var belongs_to_label = MainApp.bySelector('belongs_to_label');
156
+ var belongs_to_input = $table.find("[data-selector='belongs_to']");
157
+ var belongs_to_label = $table.find("[data-selector='belongs_to_label']");
68
158
 
69
- var column_text_input = $tr.find("[data-selector='column_text']");
70
- var column_text_label = MainApp.bySelector('column_text_label');
159
+ var column_text_input = $table.find("[data-selector='column_text']");
160
+ var column_input = $table.find("[data-selector='column_list']");
161
+ var column_label = $table.find("[data-selector='column_label']");
71
162
 
72
- var column_input = $tr.find("[data-selector='column_list']");
73
- var column_label = MainApp.bySelector('column_label');
163
+ var new_column_name_input = $table.find(
164
+ "[data-selector='new_column_name']"
165
+ );
166
+ var new_column_name_label = $table.find(
167
+ "[data-selector='new_column_name_label']"
168
+ );
74
169
 
75
- var new_column_name_input = $tr.find("[data-selector='new_column_name']");
76
- var new_column_name_label = MainApp.bySelector('new_column_name_label');
170
+ var column_type_input = $table.find("[data-selector='column_type']");
171
+ var column_type_label = $table.find(
172
+ "[data-selector='column_type_label']"
173
+ );
77
174
 
78
- var column_type_input = $tr.find("[data-selector='column_type']");
79
- var column_type_label = MainApp.bySelector('column_type_label');
175
+ var index_options_inputs = $table.find("[data-selector='index_options']");
176
+ var index_options_label = $table.find(
177
+ "[data-selector='index_options_label']"
178
+ );
80
179
 
81
- var index_input = $tr.find("[data-selector='index']");
82
- var index_label = MainApp.bySelector('index_label');
180
+ var default_input = $table.find("[data-selector='default_input']");
181
+ var default_label = $table.find("[data-selector='default_label']");
83
182
 
84
- var nullable_input = $tr.find("[data-selector='nullable']");
85
- var nullable_label = MainApp.bySelector('nullable_label');
183
+ var nullable_input = $table.find("[data-selector='nullable_input']");
184
+ var nullable_label = $table.find("[data-selector='nullable_label']");
86
185
 
87
186
  $.each(
88
187
  [
@@ -90,8 +189,6 @@ MainApp = {
90
189
  belongs_to_label,
91
190
 
92
191
  column_text_input,
93
- column_text_label,
94
-
95
192
  column_input,
96
193
  column_label,
97
194
 
@@ -101,31 +198,32 @@ MainApp = {
101
198
  column_type_input,
102
199
  column_type_label,
103
200
 
104
- index_input,
105
- index_label,
201
+ index_options_inputs,
202
+ index_options_label,
203
+
204
+ default_input,
205
+ default_label,
106
206
 
107
207
  nullable_input,
108
208
  nullable_label,
109
209
  ],
110
- function(i, el) {
210
+ function (i, el) {
111
211
  el.hide();
112
- });
212
+ }
213
+ );
113
214
 
114
215
  var mapping = {
115
216
  add_column: [
116
217
  column_text_input,
117
- column_text_label,
218
+ column_label,
118
219
  column_type_input,
119
220
  column_type_label,
120
- index_input,
121
- index_label,
221
+ default_input,
222
+ default_label,
122
223
  nullable_input,
123
224
  nullable_label,
124
225
  ],
125
- remove_column: [
126
- column_input,
127
- column_label,
128
- ],
226
+ remove_column: [column_input, column_label],
129
227
  rename_column: [
130
228
  column_input,
131
229
  column_label,
@@ -138,55 +236,102 @@ MainApp = {
138
236
  column_type_input,
139
237
  column_type_label,
140
238
  ],
141
- belongs_to: [
142
- belongs_to_input,
143
- belongs_to_label,
144
- ],
239
+ belongs_to: [belongs_to_input, belongs_to_label],
145
240
  add_index_to_column: [
146
241
  column_input,
147
242
  column_label,
243
+ index_options_inputs,
244
+ index_options_label,
148
245
  ],
149
- remove_index_to_column: [
150
- column_input,
151
- column_label,
152
- ],
246
+ remove_index_to_column: [column_input, column_label],
153
247
  drop_table: [],
154
- }
248
+ };
155
249
 
156
250
  inputs_to_show = mapping[action];
157
251
 
158
252
  if (inputs_to_show) {
159
- $.each(inputs_to_show, function(i, el) {
253
+ $.each(inputs_to_show, function (i, el) {
160
254
  el.show();
161
255
  });
162
256
  }
163
257
  });
164
258
  },
165
259
 
166
- duplicateLine: function() {
260
+ duplicateLine: function () {
167
261
  var tr = MainApp.bySelector("tbody").find("tr").last().clone();
168
262
  MainApp.bySelector("tbody").append(tr);
169
- MainApp.bySelector("tbody").find("tr").last().find('[name="models[attributes][][attribute_name]"]').val('').focus();
263
+ MainApp.bySelector("tbody")
264
+ .find("tr")
265
+ .last()
266
+ .find('[name="model[attributes][][attribute_name]"]')
267
+ .val("")
268
+ .focus();
170
269
  },
171
270
 
172
- removeLine: function(target) {
271
+ duplicateLineMigration: function () {
272
+ var table = MainApp.bySelector("table").find("table").last().clone();
273
+ MainApp.bySelector("table").append(table);
274
+ MainApp.adaptLines();
275
+
276
+ // Populate select/option for column list:
277
+ var allAttributes = [];
278
+ MainApp.bySelector("table")
279
+ .find('[data-selector="column_text"]')
280
+ .each(function (i, el) {
281
+ allAttributes.push(el.value);
282
+ });
283
+ allAttributes = allAttributes.unique();
284
+
285
+ var columnSelector = MainApp.bySelector("table")
286
+ .find("table")
287
+ .last()
288
+ .find('[data-selector="column_list"]')
289
+ .last();
290
+
291
+ var currentValues = [];
292
+ columnSelector.find("option").each(function (i, el) {
293
+ currentValues.push(el.value);
294
+ });
295
+
296
+ currentValues = currentValues.concat(allAttributes).unique().sort();
297
+
298
+ columnSelector.empty(); // remove old options
299
+ $.each(currentValues, function (i, value) {
300
+ columnSelector.append(
301
+ $("<option></option>").attr("value", value).text(value)
302
+ );
303
+ });
304
+
305
+ MainApp.bySelector("table")
306
+ .find("table")
307
+ .last()
308
+ .find('[name="migrations[][column]"]')
309
+ .val("")
310
+ .focus();
311
+ },
312
+
313
+ removeLineModel: function (target) {
173
314
  if ($(target).parents("tbody").find("tr").length > 1) {
174
315
  $(target).parents("tr").remove();
175
316
  }
176
317
  },
177
318
 
319
+ removeLineMigration: function (target) {
320
+ if ($(target).parents(".container-table").find("table").length > 1) {
321
+ $(target).parents("table").remove();
322
+ }
323
+ },
324
+
178
325
  // PRIVATE
179
- bySelector: function(selector) {
326
+ bySelector: function (selector) {
180
327
  return $("[data-selector='" + selector + "']");
181
328
  },
182
329
 
183
- whenActionOnElement: function(action, selector, callback) {
330
+ whenActionOnElement: function (action, selector, callback) {
184
331
  $(document).on(action, "[data-selector='" + selector + "']", callback);
185
- }
186
- }
187
-
188
- MainApp.init();
332
+ },
333
+ };
189
334
 
190
- $(document).ready(function() {
335
+ $(document).ready(function () {
191
336
  MainApp.ready();
192
337
  });
data/spec/dummy/Gemfile CHANGED
@@ -3,8 +3,6 @@ source 'https://rubygems.org'
3
3
  gem "rails", "~> 7.0.1"
4
4
  gem 'pg'
5
5
  gem 'puma'
6
- gem 'jquery-rails'
7
- gem 'turbolinks', '~> 5'
8
6
 
9
7
  group :development, :test do
10
8
  localtower_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /Users/damln/Work/localtower
3
3
  specs:
4
- localtower (0.4.2)
4
+ localtower (0.5.0)
5
5
  active_link_to (>= 1.0.4)
6
6
  rails (>= 5.2.0)
7
7
  thor (>= 0.18.1)
@@ -87,10 +87,6 @@ GEM
87
87
  activesupport (>= 5.0)
88
88
  i18n (1.12.0)
89
89
  concurrent-ruby (~> 1.0)
90
- jquery-rails (4.5.0)
91
- rails-dom-testing (>= 1, < 3)
92
- railties (>= 4.2.0)
93
- thor (>= 0.14, < 2.0)
94
90
  loofah (2.19.0)
95
91
  crass (~> 1.0.2)
96
92
  nokogiri (>= 1.5.9)
@@ -150,9 +146,6 @@ GEM
150
146
  rake (13.0.6)
151
147
  thor (1.2.1)
152
148
  timeout (0.3.0)
153
- turbolinks (5.2.1)
154
- turbolinks-source (~> 5.2)
155
- turbolinks-source (5.2.0)
156
149
  tzinfo (2.0.5)
157
150
  concurrent-ruby (~> 1.0)
158
151
  websocket-driver (0.7.5)
@@ -164,12 +157,10 @@ PLATFORMS
164
157
  ruby
165
158
 
166
159
  DEPENDENCIES
167
- jquery-rails
168
160
  localtower!
169
161
  pg
170
162
  puma
171
163
  rails (~> 7.0.1)
172
- turbolinks (~> 5)
173
164
  tzinfo-data
174
165
 
175
166
  BUNDLED WITH
@@ -0,0 +1,3 @@
1
+ class Post < ApplicationRecord
2
+ # belongs_to :user
3
+ end
@@ -1,2 +1,3 @@
1
1
  class User < ApplicationRecord
2
+ # has_many :posts
2
3
  end
@@ -37,6 +37,7 @@ Rails.application.configure do
37
37
  # Raise an error on page load if there are pending migrations.
38
38
  # config.active_record.migration_error = :page_load
39
39
  config.active_record.migration_error = false if defined?(Localtower)
40
+ config.reload_classes_only_on_change = false
40
41
 
41
42
  # Debug mode disables concatenation and preprocessing of assets.
42
43
  # This option may cause significant delays in view rendering with a large
@@ -4,7 +4,7 @@
4
4
  # the maximum value specified for Puma. Default is set to 5 threads for minimum
5
5
  # and maximum, this matches the default thread size of Active Record.
6
6
  #
7
- threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
7
+ threads_count = ENV.fetch("RAILS_MAX_THREADS") { 1 }.to_i
8
8
  threads threads_count, threads_count
9
9
 
10
10
  # Specifies the `port` that Puma will listen on to receive requests, default is 3000.
@@ -0,0 +1,14 @@
1
+ class CreateUsers < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :email, null: false
5
+ t.integer :login_count, default: 0, null: false
6
+ t.jsonb :metadata, default: {}, null: false
7
+ t.string :name
8
+
9
+ t.timestamps
10
+ end
11
+ add_index :users, :email, unique: true
12
+ add_index :users, :name
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ class ChangeUsersAt1674166670 < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :users, :city, :string
4
+ add_index :users, :city
5
+ rename_column :users, :login_count, :signin_count
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ class CreatePosts < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :posts do |t|
4
+ t.string :title, null: false
5
+ t.string :content, null: false
6
+
7
+ t.timestamps
8
+ end
9
+ add_index :posts, :title
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class ChangePostsAt1674166865 < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_reference :posts, :user, foreign_key: true, index: true
4
+ end
5
+ end
@@ -10,20 +10,32 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema[7.0].define(version: 2022_11_15_193642) do
13
+ ActiveRecord::Schema[7.0].define(version: 2023_01_19_222106) do
14
14
  # These are extensions that must be enabled in order to support this database
15
15
  enable_extension "plpgsql"
16
16
 
17
+ create_table "posts", force: :cascade do |t|
18
+ t.string "title", null: false
19
+ t.string "content", null: false
20
+ t.datetime "created_at", null: false
21
+ t.datetime "updated_at", null: false
22
+ t.bigint "user_id"
23
+ t.index ["title"], name: "index_posts_on_title"
24
+ t.index ["user_id"], name: "index_posts_on_user_id"
25
+ end
26
+
17
27
  create_table "users", force: :cascade do |t|
18
- t.text "name"
19
- t.string "reference"
20
- t.integer "age"
28
+ t.string "email", null: false
29
+ t.integer "signin_count", default: 0, null: false
30
+ t.jsonb "metadata", default: {}, null: false
31
+ t.string "name"
21
32
  t.datetime "created_at", null: false
22
33
  t.datetime "updated_at", null: false
23
- t.string "family"
24
- t.string "lol"
25
- t.index ["age"], name: "index_users_on_age"
26
- t.index ["reference"], name: "index_users_on_reference"
34
+ t.string "city"
35
+ t.index ["city"], name: "index_users_on_city"
36
+ t.index ["email"], name: "index_users_on_email", unique: true
37
+ t.index ["name"], name: "index_users_on_name"
27
38
  end
28
39
 
40
+ add_foreign_key "posts", "users"
29
41
  end