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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile +7 -5
- data/Gemfile.lock +21 -33
- data/README.md +39 -5
- data/Rakefile +5 -3
- data/app_rail-airtable.gemspec +23 -18
- data/bin/ara_generator +4 -2
- data/bin/console +4 -3
- data/examples/schemas/daily_logs.json +86 -0
- data/examples/schemas/locations.json +85 -0
- data/examples/schemas/users.json +51 -0
- data/lib/app_rail/airtable/application_record.rb +24 -21
- data/lib/app_rail/airtable/authenticatable.rb +31 -18
- data/lib/app_rail/airtable/authentication_helpers.rb +10 -7
- data/lib/app_rail/airtable/generator.rb +15 -0
- data/lib/app_rail/airtable/sinatra.rb +37 -25
- data/lib/app_rail/airtable/string_ext.rb +4 -2
- data/lib/app_rail/airtable/version.rb +3 -1
- data/lib/app_rail/airtable.rb +9 -7
- data/templates/project/Gemfile +2 -1
- data/templates/project/config.ru +2 -0
- data/templates/project/lib/server.rb.tt +6 -0
- data/templates/project/spec/server_spec.rb.tt +34 -0
- data/templates/project/spec/spec_helper.rb +56 -56
- metadata +54 -9
- data/templates/project/Gemfile.lock +0 -96
@@ -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 =
|
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[
|
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[
|
65
|
+
self['Password Hash']
|
53
66
|
end
|
54
|
-
|
67
|
+
|
55
68
|
def oauth_session
|
56
|
-
{ access_token: self[
|
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[
|
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(
|
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: [
|
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: [
|
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
|
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
|
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
|
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,
|
80
|
-
|
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
|
data/lib/app_rail/airtable.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
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
|
data/templates/project/Gemfile
CHANGED
data/templates/project/config.ru
CHANGED
@@ -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
|
23
|
-
require
|
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
|
-
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
#
|
74
|
-
#
|
75
|
-
# - http://
|
76
|
-
# - http://
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
#
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
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
|