app_rail-airtable 0.3.7 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bcrypt'
2
4
  require 'securerandom'
3
5
 
@@ -5,56 +7,67 @@ module AppRail
5
7
  module Airtable
6
8
  module Authenticatable
7
9
  include BCrypt
8
-
10
+
9
11
  def self.included(klass)
10
12
  klass.extend(ClassMethods)
13
+ klass.prepend(InstanceMethods) # prepends `initialize` instance method in order to take precedence over Airrecord::Table#initialize
11
14
  end
12
-
15
+
13
16
  module ClassMethods
14
17
  def create(email:, password:)
15
- user = User.new("Email" => email, "Password Hash" => password_hash(password), "Access Token" => next_access_token)
18
+ user = new('Email' => email, 'Password Hash' => password_hash(password), 'Access Token' => next_access_token)
16
19
  user.create
17
20
  user
18
21
  end
19
-
22
+
20
23
  def create_session_as_json(email:, password:)
21
24
  user = find_by_email_and_password(email, password)
22
25
  return nil unless user
23
-
24
- user["Access Token"] = next_access_token
26
+
27
+ user['Access Token'] = next_access_token
25
28
  user.save
26
29
  user&.oauth_session
27
30
  end
28
-
31
+
29
32
  def find_by_email_and_password(email, password)
30
33
  user = all(filter: "{Email} = \"#{email}\"").first
31
34
  user&.valid_password?(password) ? user : nil
32
35
  end
33
36
 
34
37
  def find_by_access_token(access_token)
35
- all(filter: "{Access Token} = \"#{access_token}\"").first
38
+ all(filter: "{Access Token} = \"#{access_token}\"").first
36
39
  end
37
-
40
+
38
41
  def password_hash(password)
39
42
  BCrypt::Password.create(password)
40
- end
41
-
43
+ end
44
+
42
45
  def next_access_token
43
46
  SecureRandom.hex
44
- end
47
+ end
48
+ end
49
+
50
+ module InstanceMethods
51
+ def initialize(*args)
52
+ if (kwargs = args.first) && kwargs.is_a?(Hash) && kwargs.size == 2 && kwargs.key?(:email) && kwargs.key?(:password)
53
+ super('Email' => kwargs[:email], 'Password Hash' => self.class.password_hash(kwargs[:password]), 'Access Token' => self.class.next_access_token)
54
+ else
55
+ super
56
+ end
57
+ end
45
58
  end
46
-
59
+
47
60
  def valid_password?(password)
48
61
  BCrypt::Password.new(password_hash) == password
49
62
  end
50
-
63
+
51
64
  def password_hash
52
- self["Password Hash"]
65
+ self['Password Hash']
53
66
  end
54
-
67
+
55
68
  def oauth_session
56
- { access_token: self["Access Token"], scope: :user, refresh_token: "", token_type: :bearer, expires_in: 60000 }
69
+ { access_token: self['Access Token'], scope: :user, refresh_token: '', token_type: :bearer, expires_in: 60_000 }
57
70
  end
58
71
  end
59
72
  end
60
- end
73
+ end
@@ -1,27 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppRail
2
4
  module Airtable
3
5
  module AuthenticationHelpers
4
6
  def current_user
5
7
  @current_user ||= find_current_user
6
8
  end
7
-
9
+
8
10
  def find_current_user
9
11
  authorization_header && bearer_token ? find_authenticatable_resource(access_token: bearer_token) : nil
10
12
  end
11
-
13
+
12
14
  def bearer_token
13
- authorization_values = authorization_header.split(" ")
15
+ authorization_values = authorization_header.split(' ')
14
16
  return nil unless authorization_values.count > 1
17
+
15
18
  authorization_values[1]
16
19
  end
17
-
20
+
18
21
  def authorization_header
19
- request.env["HTTP_AUTHORIZATION"]
22
+ request.env['HTTP_AUTHORIZATION']
20
23
  end
21
-
24
+
22
25
  def authenticate!
23
26
  halt 401 unless current_user
24
27
  end
25
28
  end
26
29
  end
27
- end
30
+ end
@@ -55,6 +55,21 @@ module AppRail
55
55
  raise "ERROR! The key \"#{key}\" is not present in the schema definition."
56
56
  end
57
57
  end
58
+
59
+ def fields_as_params_examples(fields)
60
+ fields.each_with_object({}) do |field, hsh|
61
+ hsh[field['name']] = case field['type']
62
+ when 'date'
63
+ '01/01/2022'
64
+ when 'string', 'text'
65
+ 'MyString'
66
+ when 'integer'
67
+ 10
68
+ else
69
+ 'MyString'
70
+ end
71
+ end
72
+ end
58
73
  end
59
74
  end
60
75
  end
@@ -1,89 +1,101 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sinatra'
2
4
  require 'json'
3
5
  require 'active_support'
4
6
 
5
7
  # TODO: MBS - move to configure block or other
6
- Airrecord.api_key = ENV.fetch("AIRTABLE_API_KEY")
8
+ Airrecord.api_key = ENV.fetch('AIRTABLE_API_KEY')
7
9
 
8
10
  class Symbol
9
-
10
11
  def pluralize
11
12
  ActiveSupport::Inflector.pluralize(self)
12
13
  end
13
-
14
+
14
15
  def classify
15
- ActiveSupport::Inflector.classify(self)
16
+ ActiveSupport::Inflector.classify(self)
16
17
  end
17
-
18
+
18
19
  def classify_constantize
19
20
  ActiveSupport::Inflector.constantize(classify)
20
21
  end
21
-
22
22
  end
23
23
 
24
24
  module AppRail
25
25
  module Airtable
26
26
  class Sinatra < Sinatra::Base
27
27
  @@authenticated_route = false
28
-
28
+
29
29
  helpers do
30
30
  def request_body_as_json
31
31
  request.body.rewind
32
32
  JSON.parse(request.body.read)
33
33
  end
34
-
34
+
35
35
  def params_and_body_as_json
36
36
  request_body_as_json.merge(params)
37
37
  end
38
38
  end
39
-
40
- def self.authenticatable_resources(name, only: [:index, :show, :create])
41
-
39
+
40
+ def self.authenticatable_resources(name, only: %i[index show create update])
42
41
  # If authentication is used then include the correct helpers
43
42
  # Allowing the routes to use `authenticate!` and `current_user`
44
43
  helpers AppRail::Airtable::AuthenticationHelpers
45
-
44
+
46
45
  resources(name, only: only)
47
-
46
+
48
47
  return unless block_given?
48
+
49
49
  @@authenticated_route = true
50
50
  yield
51
51
  @@authenticated_route = false
52
52
  end
53
53
 
54
- def self.resources(name, only: [:index, :show, :create])
54
+ def self.resources(name, only: %i[index show create update])
55
55
  only = [only] if only.is_a?(Symbol)
56
-
56
+
57
57
  index_route(name, authenticated_route?) if only.include?(:index)
58
58
  show_route(name, authenticated_route?) if only.include?(:show)
59
59
  create_route(name, authenticated_route?) if only.include?(:create)
60
+ update_route(name, authenticated_route?) if only.include?(:update)
60
61
  end
61
-
62
+
62
63
  def self.index_route(name, authenticated_route)
63
- get "/#{name.to_s}" do
64
+ get "/#{name}" do
64
65
  authenticate! if authenticated_route
65
66
  name.classify_constantize.index(user: authenticated_route ? current_user : nil).map(&:ar_list_item_as_json).to_json
66
67
  end
67
68
  end
68
-
69
+
69
70
  def self.show_route(name, authenticated_route)
70
- get "/#{name.to_s}/:id" do
71
+ get "/#{name}/:id" do
71
72
  authenticate! if authenticated_route
72
73
  name.classify_constantize.find(params['id']).ar_stack_as_json.to_json
73
74
  end
74
75
  end
75
-
76
+
76
77
  def self.create_route(name, authenticated_route)
77
- post "/#{name.to_s}" do
78
+ post "/#{name}" do
78
79
  authenticate! if authenticated_route
79
- as_json = name.classify_constantize.create_as_json(current_user: authenticated_route ? current_user : nil, params: params_and_body_as_json)
80
- [201, as_json.to_json]
80
+ as_json = name.classify_constantize.create_as_json(current_user: authenticated_route ? current_user : nil,
81
+ params: params_and_body_as_json)
82
+ [201, as_json.to_json]
81
83
  end
82
84
  end
83
-
85
+
86
+ def self.update_route(name, authenticated_route)
87
+ put "/#{name}/:id" do
88
+ authenticate! if authenticated_route
89
+ record = name.classify_constantize.find(params['id'])
90
+ as_json = record.update_as_json(current_user: authenticated_route ? current_user : nil,
91
+ params: params_and_body_as_json)
92
+ [200, as_json.to_json]
93
+ end
94
+ end
95
+
84
96
  def self.authenticated_route?
85
97
  @@authenticated_route
86
98
  end
87
99
  end
88
100
  end
89
- end
101
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class String
2
4
  def snake_case
3
- gsub(" ", "_").underscore
5
+ gsub(' ', '_').underscore
4
6
  end
5
- end
7
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppRail
2
4
  module Airtable
3
- VERSION = "0.3.7"
5
+ VERSION = '0.4.2'
4
6
  end
5
7
  end
@@ -1,10 +1,12 @@
1
- require "app_rail/airtable/version"
2
- require "app_rail/airtable/string_ext"
3
- require "app_rail/airtable/application_record"
4
- require "app_rail/airtable/authenticatable"
5
- require "app_rail/airtable/authentication_helpers"
6
- require "app_rail/airtable/sinatra"
7
- require "app_rail/airtable/generator"
1
+ # frozen_string_literal: true
2
+
3
+ require 'app_rail/airtable/version'
4
+ require 'app_rail/airtable/string_ext'
5
+ require 'app_rail/airtable/application_record'
6
+ require 'app_rail/airtable/authenticatable'
7
+ require 'app_rail/airtable/authentication_helpers'
8
+ require 'app_rail/airtable/sinatra'
9
+ require 'app_rail/airtable/generator'
8
10
 
9
11
  module AppRail
10
12
  module Airtable
@@ -5,7 +5,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
5
 
6
6
  ruby '2.7.4'
7
7
 
8
- gem 'app_rail-airtable', '~> 0.3.3'
8
+ gem 'app_rail-airtable', '~> 0.4.0'
9
+ gem 'app_rail-steps'
9
10
 
10
11
  group :development do
11
12
  gem 'dotenv'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  $stdout.sync = true
2
4
 
3
5
  ENV['RACK_ENV'] ||= 'development'
@@ -25,6 +25,12 @@ class <%= table['name'].singularize %> < AppRail::Airtable::ApplicationRecord
25
25
  record = <%= table['name'].singularize %>.create(email: params["payload"]["email"], password: params["payload"]["password"])
26
26
  { response: { id: record.id }, oauth_session: record.oauth_session}
27
27
  end
28
+
29
+ def update_as_json(params:, current_user:)
30
+ params['payload'].each { |attr, v| public_send("#{attr}=", v) }
31
+ save
32
+ { response: { id: id }, oauth_session: oauth_session }
33
+ end
28
34
  <% end -%>
29
35
  end
30
36
 
@@ -69,6 +69,24 @@ RSpec.describe Server do
69
69
  it { expect(json_response).to eq [resource_to_json] }
70
70
  end
71
71
 
72
+ describe 'PUT /<%= path_name %>/:id"' do
73
+ let(:params) { <%= fields_as_params_examples(table['fields']) %> }
74
+ let(:response) { put "/<%= path_name %>/#{id}", params.to_json, headers }
75
+ let(:mock_auth_resource) { instance_double(<%= auth_model_name %>) }
76
+ let(:mock_resource) { instance_double(<%= model_name %>, id: id) }
77
+ let(:resource_to_json) { { id: id } }
78
+ let(:headers) { auth_headers.merge('CONTENT_TYPE' => 'application/json') }
79
+ let(:id) { '1' }
80
+
81
+ before do
82
+ allow(<%= auth_model_name %>).to receive(:find_by_access_token).with(access_token) { mock_auth_resource }
83
+ allow(<%= model_name %>).to receive(:find).with(id) { mock_resource }
84
+ allow(mock_resource).to receive(:update_as_json).with(current_user: mock_auth_resource, params: { 'id' => '1' }.merge(params)) { resource_to_json }
85
+ end
86
+
87
+ it { expect(response.status).to eq 200 }
88
+ it { expect(json_response).to eq resource_to_json }
89
+ end
72
90
  <% end -%>
73
91
  <% else -%>
74
92
  <% @tables.each do |table| -%>
@@ -89,6 +107,22 @@ RSpec.describe Server do
89
107
  it { expect(json_response).to eq [resource_to_json] }
90
108
  end
91
109
 
110
+ describe 'PUT /<%= path_name %>/:id"' do
111
+ let(:params) { <%= fields_as_params_examples(table['fields']) %> }
112
+ let(:response) { put "/<%= path_name %>/#{id}", params.to_json, headers }
113
+ let(:mock_resource) { instance_double(<%= model_name %>, id: id) }
114
+ let(:resource_to_json) { { id: id } }
115
+ let(:headers) { auth_headers.merge('CONTENT_TYPE' => 'application/json') }
116
+ let(:id) { '1' }
117
+
118
+ before do
119
+ allow(<%= model_name %>).to receive(:find).with(id) { mock_resource }
120
+ allow(mock_resource).to receive(:update_as_json).with(current_user: mock_auth_resource, params: { 'id' => '1' }.merge(params)) { resource_to_json }
121
+ end
122
+
123
+ it { expect(response.status).to eq 200 }
124
+ it { expect(json_response).to eq resource_to_json }
125
+ end
92
126
  <% end -%>
93
127
  <% end -%>
94
128
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file was generated by the `rspec --init` command. Conventionally, all
2
4
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
5
  # The generated `.rspec` file contains `--require spec_helper` which will cause
@@ -19,8 +21,9 @@ ENV['AIRTABLE_API_KEY'] = 'keyABC123'
19
21
  ENV['AIRTABLE_BASE_ID'] = 'appABC123'
20
22
  ENV['RACK_ENV'] = 'test'
21
23
 
22
- require "rack/test"
23
- require "app_rail/airtable"
24
+ require 'rack/test'
25
+ require 'app_rail/airtable'
26
+ require 'byebug'
24
27
 
25
28
  require './lib/server'
26
29
 
@@ -55,60 +58,57 @@ RSpec.configure do |config|
55
58
  # triggering implicit auto-inclusion in groups with matching metadata.
56
59
  config.shared_context_metadata_behavior = :apply_to_host_groups
57
60
 
58
- # The settings below are suggested to provide a good initial experience
59
- # with RSpec, but feel free to customize to your heart's content.
60
- =begin
61
- # This allows you to limit a spec run to individual examples or groups
62
- # you care about by tagging them with `:focus` metadata. When nothing
63
- # is tagged with `:focus`, all examples get run. RSpec also provides
64
- # aliases for `it`, `describe`, and `context` that include `:focus`
65
- # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
66
- config.filter_run_when_matching :focus
67
-
68
- # Allows RSpec to persist some state between runs in order to support
69
- # the `--only-failures` and `--next-failure` CLI options. We recommend
70
- # you configure your source control system to ignore this file.
71
- config.example_status_persistence_file_path = "spec/examples.txt"
72
-
73
- # Limits the available syntax to the non-monkey patched syntax that is
74
- # recommended. For more details, see:
75
- # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
76
- # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
77
- # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
78
- config.disable_monkey_patching!
79
-
80
- # This setting enables warnings. It's recommended, but in some cases may
81
- # be too noisy due to issues in dependencies.
82
- config.warnings = true
83
-
84
- # Many RSpec users commonly either run the entire suite or an individual
85
- # file, and it's useful to allow more verbose output when running an
86
- # individual spec file.
87
- if config.files_to_run.one?
88
- # Use the documentation formatter for detailed output,
89
- # unless a formatter has already been configured
90
- # (e.g. via a command-line flag).
91
- config.default_formatter = "doc"
92
- end
93
-
94
- # Print the 10 slowest examples and example groups at the
95
- # end of the spec run, to help surface which specs are running
96
- # particularly slow.
97
- config.profile_examples = 10
98
-
99
- # Run specs in random order to surface order dependencies. If you find an
100
- # order dependency and want to debug it, you can fix the order by providing
101
- # the seed, which is printed after each run.
102
- # --seed 1234
103
- config.order = :random
104
-
105
- # Seed global randomization in this process using the `--seed` CLI option.
106
- # Setting this allows you to use `--seed` to deterministically reproduce
107
- # test failures related to randomization by passing the same `--seed` value
108
- # as the one that triggered the failure.
109
- Kernel.srand config.seed
110
- =end
61
+ # The settings below are suggested to provide a good initial experience
62
+ # with RSpec, but feel free to customize to your heart's content.
63
+ # # This allows you to limit a spec run to individual examples or groups
64
+ # # you care about by tagging them with `:focus` metadata. When nothing
65
+ # # is tagged with `:focus`, all examples get run. RSpec also provides
66
+ # # aliases for `it`, `describe`, and `context` that include `:focus`
67
+ # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
68
+ # config.filter_run_when_matching :focus
69
+ #
70
+ # # Allows RSpec to persist some state between runs in order to support
71
+ # # the `--only-failures` and `--next-failure` CLI options. We recommend
72
+ # # you configure your source control system to ignore this file.
73
+ # config.example_status_persistence_file_path = "spec/examples.txt"
74
+ #
75
+ # # Limits the available syntax to the non-monkey patched syntax that is
76
+ # # recommended. For more details, see:
77
+ # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
78
+ # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
79
+ # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
80
+ # config.disable_monkey_patching!
81
+ #
82
+ # # This setting enables warnings. It's recommended, but in some cases may
83
+ # # be too noisy due to issues in dependencies.
84
+ # config.warnings = true
85
+ #
86
+ # # Many RSpec users commonly either run the entire suite or an individual
87
+ # # file, and it's useful to allow more verbose output when running an
88
+ # # individual spec file.
89
+ # if config.files_to_run.one?
90
+ # # Use the documentation formatter for detailed output,
91
+ # # unless a formatter has already been configured
92
+ # # (e.g. via a command-line flag).
93
+ # config.default_formatter = "doc"
94
+ # end
95
+ #
96
+ # # Print the 10 slowest examples and example groups at the
97
+ # # end of the spec run, to help surface which specs are running
98
+ # # particularly slow.
99
+ # config.profile_examples = 10
100
+ #
101
+ # # Run specs in random order to surface order dependencies. If you find an
102
+ # # order dependency and want to debug it, you can fix the order by providing
103
+ # # the seed, which is printed after each run.
104
+ # # --seed 1234
105
+ # config.order = :random
106
+ #
107
+ # # Seed global randomization in this process using the `--seed` CLI option.
108
+ # # Setting this allows you to use `--seed` to deterministically reproduce
109
+ # # test failures related to randomization by passing the same `--seed` value
110
+ # # as the one that triggered the failure.
111
+ # Kernel.srand config.seed
111
112
 
112
113
  config.include Rack::Test::Methods
113
-
114
114
  end