localtower 0.5.0 → 1.0.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 (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