app_rail-airtable 0.1.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +7 -3
- data/Gemfile.lock +33 -36
- data/README.md +63 -15
- data/Rakefile +5 -3
- data/app_rail-airtable.gemspec +26 -15
- data/bin/ara_generator +8 -0
- 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 +43 -25
- data/lib/app_rail/airtable/authenticatable.rb +78 -0
- data/lib/app_rail/airtable/authentication_helpers.rb +38 -0
- data/lib/app_rail/airtable/generator.rb +75 -0
- data/lib/app_rail/airtable/sinatra.rb +106 -18
- data/lib/app_rail/airtable/string_ext.rb +7 -0
- data/lib/app_rail/airtable/version.rb +3 -1
- data/lib/app_rail/airtable.rb +9 -3
- data/templates/project/.env.tt +2 -0
- data/templates/project/.gitignore +19 -0
- data/templates/project/Gemfile +24 -0
- data/templates/project/app.json +17 -0
- data/templates/project/boot.rb +8 -0
- data/templates/project/config.ru +6 -0
- data/templates/project/lib/server.rb.tt +68 -0
- data/templates/project/spec/server_spec.rb.tt +128 -0
- data/templates/project/spec/spec_helper.rb +114 -0
- metadata +96 -7
@@ -1,49 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'airrecord'
|
4
|
+
require 'app_rail/steps'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
2
6
|
|
3
7
|
module AppRail
|
4
8
|
module Airtable
|
5
9
|
class ApplicationRecord < Airrecord::Table
|
6
|
-
|
7
|
-
|
10
|
+
include ActiveSupport::Inflector
|
11
|
+
include AppRail::Steps::Displayable
|
12
|
+
|
13
|
+
def self.base_key
|
14
|
+
ENV.fetch('AIRTABLE_BASE_ID')
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.table_name
|
18
|
+
name.pluralize
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.airtable_attr(*attributes)
|
22
|
+
attributes.each do |attribute|
|
23
|
+
define_method(attribute.to_s.snake_case) { self[attribute] }
|
24
|
+
define_method("#{attribute.to_s.snake_case}=") { |value| self[attribute] = value }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Step utilities
|
29
|
+
def self.ar_list_item(text:, detail_text: nil, image: nil, sf_symbol: nil, material_icon: nil)
|
8
30
|
define_method(:ar_list_item_as_json) do
|
9
31
|
{
|
10
|
-
id:
|
11
|
-
text:
|
12
|
-
detailText:
|
13
|
-
imageURL:
|
14
|
-
sfSymbolName: sf_symbol,
|
15
|
-
materialIconName: material_icon
|
32
|
+
id: id,
|
33
|
+
text: method_value(text).to_s,
|
34
|
+
detailText: method_value(detail_text).to_s,
|
35
|
+
imageURL: method_value(image),
|
36
|
+
sfSymbolName: method_value(sf_symbol),
|
37
|
+
materialIconName: method_value(material_icon)
|
16
38
|
}.compact
|
17
39
|
end
|
18
40
|
end
|
19
|
-
|
41
|
+
|
20
42
|
def self.ar_stack(text_items:)
|
21
43
|
define_method(:ar_stack_as_json) do
|
22
|
-
text_items.map{|ti| {type: :text, label: ti, text: self[ti].to_s} }
|
44
|
+
text_items.map { |ti| { type: :text, label: ti, text: self[ti].to_s } }
|
23
45
|
end
|
24
46
|
end
|
25
|
-
|
47
|
+
|
26
48
|
# Customisable behaviour
|
27
|
-
|
49
|
+
|
28
50
|
# Override to provide custom sorting or filtering for index
|
29
|
-
def self.index
|
51
|
+
def self.index(user:)
|
30
52
|
all
|
31
53
|
end
|
32
|
-
|
54
|
+
|
33
55
|
private
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
respond_to?(name) ? send(name) : self[name]
|
40
|
-
end
|
41
|
-
|
56
|
+
|
57
|
+
def method_value(name)
|
58
|
+
send(name) if name.present?
|
59
|
+
end
|
60
|
+
|
42
61
|
# size is either :small, :large or :full
|
43
62
|
def image(name, index: 0, size: :full)
|
44
|
-
self[name][index][
|
63
|
+
self[name][index]['thumbnails'][size.to_s]['url'] if self[name] && self[name].length > index
|
45
64
|
end
|
46
|
-
|
47
65
|
end
|
48
66
|
end
|
49
|
-
end
|
67
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module AppRail
|
7
|
+
module Airtable
|
8
|
+
module Authenticatable
|
9
|
+
class AlreadyExistsError < StandardError; end
|
10
|
+
|
11
|
+
include BCrypt
|
12
|
+
|
13
|
+
def self.included(klass)
|
14
|
+
klass.extend(ClassMethods)
|
15
|
+
klass.prepend(InstanceMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def create(email:, password:)
|
20
|
+
raise AlreadyExistsError if find_by_email(email)
|
21
|
+
|
22
|
+
user = new('Email' => email, 'Password Hash' => password_hash(password), 'Access Token' => next_access_token)
|
23
|
+
user.create
|
24
|
+
user
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_by_email_and_password(email, password)
|
28
|
+
user = find_by_email(email)
|
29
|
+
user&.valid_password?(password) ? user : nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_by_email(email)
|
33
|
+
all(filter: "{Email} = \"#{email}\"").first
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_by_access_token(access_token)
|
37
|
+
all(filter: "{Access Token} = \"#{access_token}\"").first
|
38
|
+
end
|
39
|
+
|
40
|
+
def password_hash(password)
|
41
|
+
BCrypt::Password.create(password)
|
42
|
+
end
|
43
|
+
|
44
|
+
def next_access_token
|
45
|
+
SecureRandom.hex
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
def valid_password?(password)
|
51
|
+
BCrypt::Password.new(password_hash) == password
|
52
|
+
end
|
53
|
+
|
54
|
+
def password_hash
|
55
|
+
self['Password Hash']
|
56
|
+
end
|
57
|
+
|
58
|
+
def oauth_session
|
59
|
+
ensure_access_token!
|
60
|
+
|
61
|
+
{
|
62
|
+
access_token: self['Access Token'],
|
63
|
+
scope: :user,
|
64
|
+
token_type: :bearer,
|
65
|
+
expires_in: 31_536_000 # 1 year
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def ensure_access_token!
|
70
|
+
unless self['Access Token']
|
71
|
+
self['Access Token'] = User.next_access_token
|
72
|
+
save
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppRail
|
4
|
+
module Airtable
|
5
|
+
module AuthenticationHelpers
|
6
|
+
def current_user
|
7
|
+
@current_user ||= find_current_user
|
8
|
+
end
|
9
|
+
|
10
|
+
def find_current_user
|
11
|
+
authorization_header && bearer_token ? find_authenticatable_resource(access_token: bearer_token) : nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def bearer_token
|
15
|
+
authorization_values = authorization_header.split(' ')
|
16
|
+
return nil unless authorization_values.count > 1
|
17
|
+
|
18
|
+
authorization_values[1]
|
19
|
+
end
|
20
|
+
|
21
|
+
def authorization_header
|
22
|
+
request.env['HTTP_AUTHORIZATION']
|
23
|
+
end
|
24
|
+
|
25
|
+
def oauth_client_id
|
26
|
+
ENV.fetch('OAUTH_CLIENT_ID')
|
27
|
+
end
|
28
|
+
|
29
|
+
def oauth_client_secret
|
30
|
+
ENV.fetch('OAUTH_CLIENT_SECRET')
|
31
|
+
end
|
32
|
+
|
33
|
+
def authenticate!
|
34
|
+
halt 401 unless current_user
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'app_rail/airtable/string_ext'
|
5
|
+
require 'thor'
|
6
|
+
require 'airrecord'
|
7
|
+
|
8
|
+
module AppRail
|
9
|
+
module Airtable
|
10
|
+
class Generator < Thor::Group
|
11
|
+
include Thor::Actions
|
12
|
+
|
13
|
+
argument :output_directory
|
14
|
+
argument :api_key
|
15
|
+
argument :base_id
|
16
|
+
argument :schema, required: false
|
17
|
+
|
18
|
+
def self.source_root
|
19
|
+
File.join(File.dirname(__FILE__), '..', '..', '..')
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_project
|
23
|
+
validate_airtable_schema
|
24
|
+
directory('templates/project', output_directory)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.exit_on_failure?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def validate_airtable_schema
|
34
|
+
tables.each do |table|
|
35
|
+
table_name = table['name']
|
36
|
+
table_fields = table['fields'].concat(table['associations']).map { |field| field['name'] }.compact
|
37
|
+
|
38
|
+
airtable_table = find_table(table_name)
|
39
|
+
compare_fields(table_fields, airtable_table)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def tables
|
44
|
+
@tables ||= schema ? JSON.parse(schema)['tables'] : []
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_table(table_name)
|
48
|
+
Airrecord.table(api_key, base_id, table_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def compare_fields(table_fields, airtable_table)
|
52
|
+
airtable_table.records.first.fields.each_key do |key|
|
53
|
+
next if table_fields.include? key
|
54
|
+
|
55
|
+
raise "ERROR! The key \"#{key}\" is not present in the schema definition."
|
56
|
+
end
|
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
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -1,46 +1,134 @@
|
|
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
|
8
|
+
Airrecord.api_key = ENV.fetch('AIRTABLE_API_KEY')
|
6
9
|
|
7
10
|
class Symbol
|
8
|
-
|
9
11
|
def pluralize
|
10
12
|
ActiveSupport::Inflector.pluralize(self)
|
11
13
|
end
|
12
|
-
|
14
|
+
|
15
|
+
def singularize
|
16
|
+
ActiveSupport::Inflector.singularize(self)
|
17
|
+
end
|
18
|
+
|
13
19
|
def classify
|
14
|
-
ActiveSupport::Inflector.classify(self)
|
20
|
+
ActiveSupport::Inflector.classify(self)
|
15
21
|
end
|
16
|
-
|
22
|
+
|
17
23
|
def classify_constantize
|
18
24
|
ActiveSupport::Inflector.constantize(classify)
|
19
25
|
end
|
20
|
-
|
21
26
|
end
|
22
27
|
|
23
28
|
module AppRail
|
24
29
|
module Airtable
|
25
30
|
class Sinatra < Sinatra::Base
|
31
|
+
@@authenticated_route = false
|
32
|
+
|
33
|
+
helpers do
|
34
|
+
def request_body_as_json
|
35
|
+
request.body.rewind
|
36
|
+
JSON.parse(request.body.read)
|
37
|
+
end
|
38
|
+
|
39
|
+
def params_and_body_as_json
|
40
|
+
request_body_as_json.merge(params)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.authenticatable_resources(name, only: %i[index show create update])
|
45
|
+
# If authentication is used then include the correct helpers
|
46
|
+
# Allowing the routes to use `authenticate!` and `current_user`
|
47
|
+
helpers AppRail::Airtable::AuthenticationHelpers
|
48
|
+
|
49
|
+
sign_in_route(name)
|
50
|
+
sign_out_route(name)
|
51
|
+
resources(name, only: only)
|
52
|
+
|
53
|
+
return unless block_given?
|
54
|
+
|
55
|
+
@@authenticated_route = true
|
56
|
+
yield
|
57
|
+
@@authenticated_route = false
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.resources(name, only: %i[index show create update])
|
61
|
+
only = [only] if only.is_a?(Symbol)
|
62
|
+
|
63
|
+
index_route(name, authenticated_route?) if only.include?(:index)
|
64
|
+
show_route(name, authenticated_route?) if only.include?(:show)
|
65
|
+
create_route(name, authenticated_route?) if only.include?(:create)
|
66
|
+
update_route(name, authenticated_route?) if only.include?(:update)
|
67
|
+
end
|
26
68
|
|
27
|
-
def self.
|
28
|
-
|
29
|
-
|
69
|
+
def self.sign_in_route(name)
|
70
|
+
post "/#{name}/session" do
|
71
|
+
halt [401, { error: 'Invalid client_id' }.to_json] unless params['client_id'] == oauth_client_id
|
72
|
+
halt [401, { error: 'Invalid client_secret' }.to_json] unless params['client_secret'] == oauth_client_secret
|
73
|
+
|
74
|
+
resource = name.classify_constantize.authenticate_by_params(params)
|
75
|
+
halt [401, { error: 'Invalid credentials' }.to_json] unless resource
|
76
|
+
|
77
|
+
resource.oauth_session.to_json
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.sign_out_route(name)
|
82
|
+
delete "/#{name}/session" do
|
83
|
+
authenticate!
|
84
|
+
current_user.access_token = nil
|
85
|
+
current_user.save
|
86
|
+
|
87
|
+
# Assume that the client will reload and trigger a 401
|
88
|
+
[].to_json
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.index_route(name, authenticated_route)
|
93
|
+
get "/#{name}" do
|
94
|
+
authenticate! if authenticated_route
|
95
|
+
name.classify_constantize.index(user: authenticated_route ? current_user : nil).map(&:ar_list_item_as_json).to_json
|
30
96
|
end
|
31
|
-
|
32
|
-
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.show_route(name, authenticated_route)
|
100
|
+
get "/#{name}/:id" do
|
101
|
+
authenticate! if authenticated_route
|
33
102
|
name.classify_constantize.find(params['id']).ar_stack_as_json.to_json
|
34
103
|
end
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.create_route(name, authenticated_route)
|
107
|
+
post "/#{name}" do
|
108
|
+
authenticate! if authenticated_route
|
109
|
+
begin
|
110
|
+
as_json = name.classify_constantize.create_as_json(current_user: authenticated_route ? current_user : nil,
|
111
|
+
params: params_and_body_as_json)
|
112
|
+
[201, as_json.to_json]
|
113
|
+
rescue AppRail::Airtable::Authenticatable::AlreadyExistsError
|
114
|
+
[422, { error: "#{name.singularize} already exists" }.to_json]
|
115
|
+
end
|
41
116
|
end
|
42
117
|
end
|
43
118
|
|
119
|
+
def self.update_route(name, authenticated_route)
|
120
|
+
put "/#{name}/:id" do
|
121
|
+
authenticate! if authenticated_route
|
122
|
+
record = name.classify_constantize.find(params['id'])
|
123
|
+
as_json = record.update_as_json(current_user: authenticated_route ? current_user : nil,
|
124
|
+
params: params_and_body_as_json)
|
125
|
+
[200, as_json.to_json]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.authenticated_route?
|
130
|
+
@@authenticated_route
|
131
|
+
end
|
44
132
|
end
|
45
133
|
end
|
46
|
-
end
|
134
|
+
end
|
data/lib/app_rail/airtable.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
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'
|
4
10
|
|
5
11
|
module AppRail
|
6
12
|
module Airtable
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
5
|
+
|
6
|
+
ruby '3.1.2'
|
7
|
+
|
8
|
+
gem 'app_rail-airtable'
|
9
|
+
gem 'app_rail-steps'
|
10
|
+
gem 'puma'
|
11
|
+
gem 'sinatra', '~> 3.0', '>= 3.0.2'
|
12
|
+
|
13
|
+
group :development do
|
14
|
+
gem 'dotenv'
|
15
|
+
end
|
16
|
+
|
17
|
+
group :test do
|
18
|
+
gem 'rack-test'
|
19
|
+
gem 'rspec'
|
20
|
+
end
|
21
|
+
|
22
|
+
group :development, :test do
|
23
|
+
gem 'byebug'
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
{
|
2
|
+
"name": "App Rail Airtable Service",
|
3
|
+
"description": "A barebones template to convert Airtable data to App Rail format",
|
4
|
+
"buildpacks": [
|
5
|
+
{
|
6
|
+
"url": "heroku/ruby"
|
7
|
+
}
|
8
|
+
],
|
9
|
+
"env": {
|
10
|
+
"AIRTABLE_API_KEY": {
|
11
|
+
"description": "Your Airtable API key."
|
12
|
+
},
|
13
|
+
"AIRTABLE_BASE_ID": {
|
14
|
+
"description": "The ID of the Base you'd like to connect to."
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_rail/airtable'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
<% @tables.each do |table| -%>
|
7
|
+
<% keys = table['fields'].map { |field| field['name'] if field['type'] != 'association' }.compact -%>
|
8
|
+
class <%= table['name'].singularize %> < AppRail::Airtable::ApplicationRecord
|
9
|
+
<% if table['authenticatable'] == 'True' -%>
|
10
|
+
include AppRail::Airtable::Authenticatable
|
11
|
+
|
12
|
+
<% end -%>
|
13
|
+
airtable_attr <%= keys.map{|f| "\"#{f}\""}.join(", ") %>
|
14
|
+
<% table['ar_class_methods']&.each do |method| -%>
|
15
|
+
<%= method['name'] %> <%= method['properties'].map {|prop| "#{prop['type']}: :#{prop['value']}" }.join(', ') %>
|
16
|
+
<% end -%>
|
17
|
+
|
18
|
+
<% table['associations']&.each do |association| -%>
|
19
|
+
<% word_ending_method = association['type'] == 'has_many' ? :pluralize : :singularize -%>
|
20
|
+
<%= association['type'] %> <%= ":#{association['model'].snake_case.send(word_ending_method)}, class: \"#{association['model'].camelize.singularize}\", column: \"#{association['model'].camelize.send(word_ending_method)}\"" %>
|
21
|
+
<% end -%>
|
22
|
+
<% if table['authenticatable'] == 'True' -%>
|
23
|
+
|
24
|
+
def self.create_as_json(params:, current_user:)
|
25
|
+
record = <%= table['name'].singularize %>.create(email: params["payload"]["email"], password: params["payload"]["password"])
|
26
|
+
{ response: { id: record.id }, oauth_session: record.oauth_session}
|
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
|
34
|
+
<% end -%>
|
35
|
+
end
|
36
|
+
|
37
|
+
<% end -%>
|
38
|
+
|
39
|
+
<% authenticatable_table = @tables.select { |table| table['authenticatable'] == 'True' }&.first -%>
|
40
|
+
<% authenticatable_resource = authenticatable_table['name'] if authenticatable_table -%>
|
41
|
+
class Server < AppRail::Airtable::Sinatra
|
42
|
+
<% if authenticatable_resource -%>
|
43
|
+
<% model_name = authenticatable_resource.singularize -%>
|
44
|
+
helpers do
|
45
|
+
def find_authenticatable_resource(access_token:)
|
46
|
+
<%= model_name %>.find_by_access_token(access_token)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
authenticatable_resources :<%= authenticatable_resource.snake_case %> do
|
51
|
+
<% @tables.each do |table| -%>
|
52
|
+
<% next if table['name'] == authenticatable_resource -%>
|
53
|
+
resources :<%= table['name'].snake_case %>
|
54
|
+
<% end -%>
|
55
|
+
end
|
56
|
+
|
57
|
+
post "/sessions" do
|
58
|
+
oauth_session = <%= model_name %>.create_session_as_json(email: params["username"], password: params["password"])
|
59
|
+
halt 401 unless oauth_session
|
60
|
+
|
61
|
+
oauth_session.to_json
|
62
|
+
end
|
63
|
+
<% else -%>
|
64
|
+
<% @tables.each do |table| -%>
|
65
|
+
resources :<%= table['name'].snake_case %>
|
66
|
+
<% end -%>
|
67
|
+
<% end -%>
|
68
|
+
end
|