simpleadmin 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +0 -3
- data/.rspec +0 -2
- data/.rubocop.yml +10 -58
- data/.travis.yml +1 -11
- data/Gemfile +3 -17
- data/Gemfile.lock +14 -150
- data/LICENSE.txt +21 -0
- data/README.md +35 -19
- data/Rakefile +2 -4
- data/bin/console +4 -3
- data/lib/simpleadmin.rb +104 -2
- data/lib/simpleadmin/adapters/base.rb +50 -0
- data/lib/simpleadmin/adapters/postgres.rb +132 -0
- data/lib/simpleadmin/config.rb +83 -0
- data/lib/simpleadmin/database_connector.rb +35 -0
- data/lib/simpleadmin/decorators/fields/base.rb +34 -0
- data/lib/simpleadmin/decorators/fields/image.rb +19 -0
- data/lib/simpleadmin/decorators/fields_decorator.rb +44 -0
- data/lib/simpleadmin/version.rb +3 -1
- data/simpleadmin.gemspec +18 -14
- metadata +29 -35
- data/CODE_OF_CONDUCT.md +0 -74
- data/app/controllers/simple_admin/base_controller.rb +0 -15
- data/app/controllers/simple_admin/entities_controller.rb +0 -26
- data/app/controllers/simple_admin/entity_field_type_actions_controller.rb +0 -13
- data/app/controllers/simple_admin/resources_controller.rb +0 -80
- data/app/controllers/simple_admin/versions_controller.rb +0 -9
- data/app/services/entity_field_types/status_field.rb +0 -8
- data/app/services/entity_service.rb +0 -33
- data/app/services/resource_search_service.rb +0 -22
- data/app/services/resource_service.rb +0 -43
- data/lib/rails/application.rb +0 -13
- data/lib/simpleadmin/application.rb +0 -11
- data/lib/simpleadmin/engine.rb +0 -6
- data/lib/simpleadmin/routes.rb +0 -17
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'simpleadmin'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "simpleadmin"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start(__FILE__)
|
data/lib/simpleadmin.rb
CHANGED
@@ -1,7 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/app'
|
4
|
+
require 'sequel'
|
5
|
+
require 'bcrypt'
|
6
|
+
|
1
7
|
require 'simpleadmin/version'
|
2
|
-
require 'simpleadmin/
|
8
|
+
require 'simpleadmin/config'
|
9
|
+
require 'simpleadmin/database_connector'
|
3
10
|
|
4
|
-
|
11
|
+
# Administrative dashboard without wasting time
|
12
|
+
#
|
13
|
+
# @since 1.0.0
|
14
|
+
#
|
15
|
+
# @see https://getsimpleadmin.com
|
5
16
|
|
6
17
|
module Simpleadmin
|
18
|
+
# API endpoints for integration with cloud version of getsimpleadmin.com
|
19
|
+
#
|
20
|
+
# This is used, Rack conception to connect it in any popular web frameworks
|
21
|
+
# like Ruby on Rails, Hanami, Sinatra and etc.
|
22
|
+
#
|
23
|
+
# @since 1.0.0
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
#
|
27
|
+
# # config/routes.rb
|
28
|
+
# Rails.application.routes.draw do
|
29
|
+
# mount Simpleadmin::Application, at: 'simpleadmin'
|
30
|
+
# end
|
31
|
+
class Application < Rack::App
|
32
|
+
WIDGETS = %w[quantity week_statistic].freeze
|
33
|
+
|
34
|
+
before { respond_forbidden! if secret_key_invalid? }
|
35
|
+
before { respond_forbidden! unless current_path?('/v1/tables') || allowed_table? }
|
36
|
+
|
37
|
+
namespace '/v1' do
|
38
|
+
get '/widgets/:widget_name' do
|
39
|
+
respond_forbidden!(message: 'Nonexistent widget') unless WIDGETS.include?(params['widget_name'])
|
40
|
+
|
41
|
+
client.public_send(params['widget_name'], params['table_name'])
|
42
|
+
end
|
43
|
+
|
44
|
+
get '/tables' do
|
45
|
+
client.tables
|
46
|
+
end
|
47
|
+
|
48
|
+
get '/tables/:table_name' do
|
49
|
+
client.table_columns(params['table_name'])
|
50
|
+
end
|
51
|
+
|
52
|
+
get '/resources' do
|
53
|
+
client.resources(params.values_at('table_name', 'table_fields', 'per_page',
|
54
|
+
'page', 'query', 'model_attributes', 'sort'))
|
55
|
+
end
|
56
|
+
|
57
|
+
get '/resources/:id' do
|
58
|
+
client.resource(params['id'], params['table_name'], params['table_fields'])
|
59
|
+
end
|
60
|
+
|
61
|
+
post '/resources' do
|
62
|
+
client.create_resource(request_body_params['table_name'], request_body_params['resource'])
|
63
|
+
end
|
64
|
+
|
65
|
+
patch '/resources/:id' do
|
66
|
+
client.update_resource(request_body_params['table_name'], params['id'], request_body_params['resource'])
|
67
|
+
end
|
68
|
+
|
69
|
+
delete '/resources/:id' do
|
70
|
+
client.destroy_resouce(request_body_params['table_name'], params['id'])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def client
|
77
|
+
@client ||= connector.client
|
78
|
+
end
|
79
|
+
|
80
|
+
def respond_forbidden!(message: 'Forbidden')
|
81
|
+
response.status = 403
|
82
|
+
response.write(message)
|
83
|
+
|
84
|
+
finish!
|
85
|
+
end
|
86
|
+
|
87
|
+
def request_body_params
|
88
|
+
@request_body_params ||= Rack::Utils.parse_nested_query(request.body.read)
|
89
|
+
end
|
90
|
+
|
91
|
+
def connector
|
92
|
+
@connector ||= DatabaseConnector.new(
|
93
|
+
database_credentials: Config.database_credentials
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def allowed_table?
|
98
|
+
Config.allowed_table?(params['table_name'] || request_body_params['table_name'])
|
99
|
+
end
|
100
|
+
|
101
|
+
def secret_key_invalid?
|
102
|
+
BCrypt::Password.new(request.get_header('HTTP_SIMPLEADMIN_SECRET_KEY')) != ENV['SIMPLE_ADMIN_SECRET_KEY']
|
103
|
+
end
|
104
|
+
|
105
|
+
def current_path?(path)
|
106
|
+
request.path_info == path
|
107
|
+
end
|
108
|
+
end
|
7
109
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simpleadmin/decorators/fields_decorator'
|
4
|
+
|
5
|
+
module Simpleadmin
|
6
|
+
module Adapters
|
7
|
+
# Base class to provide a unified interface for each adapter
|
8
|
+
#
|
9
|
+
# @since 1.0.0
|
10
|
+
class Base
|
11
|
+
def initialize(database_credentials:)
|
12
|
+
@database_credentials = database_credentials
|
13
|
+
end
|
14
|
+
|
15
|
+
def tables
|
16
|
+
raise NotImplementedError, 'Please follow the unified interface, add method #tables'
|
17
|
+
end
|
18
|
+
|
19
|
+
def table_columns(*_args)
|
20
|
+
raise NotImplementedError, 'Please follow the unified interface, add method #table_columns'
|
21
|
+
end
|
22
|
+
|
23
|
+
def resources(*_args)
|
24
|
+
raise NotImplementedError, 'Please follow the unified interface, add method #resources'
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :database_credentials
|
30
|
+
|
31
|
+
def order_asc?(order)
|
32
|
+
order == 'asc'
|
33
|
+
end
|
34
|
+
|
35
|
+
def order_desc?(order)
|
36
|
+
order == 'desc'
|
37
|
+
end
|
38
|
+
|
39
|
+
def model_class_by_table_name(name)
|
40
|
+
name.classify.safe_constantize
|
41
|
+
end
|
42
|
+
|
43
|
+
def table_names
|
44
|
+
return client.tables if Config.allowed_tables.include?(:all)
|
45
|
+
|
46
|
+
Config.allowed_tables
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module Simpleadmin
|
6
|
+
module Adapters
|
7
|
+
# Postgres adapter for API endpoints
|
8
|
+
#
|
9
|
+
# @since 1.0.0
|
10
|
+
class Postgres < Base
|
11
|
+
def tables
|
12
|
+
table_names.map do |table_name|
|
13
|
+
{
|
14
|
+
table_name: table_name,
|
15
|
+
table_columns: columns_by_table_name(table_name)
|
16
|
+
}
|
17
|
+
end.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
def table_columns(table_name)
|
21
|
+
columns_by_table_name(table_name).to_json
|
22
|
+
end
|
23
|
+
|
24
|
+
def resources(permitted_params_values)
|
25
|
+
table_name, table_fields, per_page, page, query, model_attributes, sort = *permitted_params_values
|
26
|
+
|
27
|
+
per_page = per_page.to_i
|
28
|
+
page = page.to_i
|
29
|
+
|
30
|
+
table_fields_names = table_fields.map { |field| field['field_name'].to_sym }
|
31
|
+
|
32
|
+
total = client.from(table_name).count
|
33
|
+
resources = client.from(table_name).limit(per_page)
|
34
|
+
|
35
|
+
resources, total = *search(query, table_name, model_attributes) unless query.nil? || query.empty?
|
36
|
+
|
37
|
+
resources = resources.offset((per_page * page) - per_page).select(*table_fields_names)
|
38
|
+
|
39
|
+
if order_asc?(sort['order'])
|
40
|
+
resources = resources.order(Sequel.asc(sort['column_name'].to_sym))
|
41
|
+
elsif order_desc?(sort['order'])
|
42
|
+
resources = resources.order(Sequel.desc(sort['column_name'].to_sym))
|
43
|
+
end
|
44
|
+
|
45
|
+
{
|
46
|
+
resources: Decorators::FieldDecorator.call(resources, table_name, table_fields),
|
47
|
+
total: total
|
48
|
+
}.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
def resource(resource_id, table_name, table_fields)
|
52
|
+
client.from(table_name).first(id: resource_id).slice(*table_fields.map(&:to_sym)).to_json
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_resource(table_name, resource_params)
|
56
|
+
Config.on_create.call(model_class_by_table_name(table_name), resource_params)
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_resource(table_name, resource_id, resource_params, _primary_key=:id)
|
60
|
+
Config.on_update.call(model_class_by_table_name(table_name), resource_id, resource_params)
|
61
|
+
end
|
62
|
+
|
63
|
+
def destroy_resouce(table_name, resource_id, _primary_key=:id)
|
64
|
+
Config.on_destroy.call(model_class_by_table_name(table_name), resource_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
def quantity(table_name)
|
68
|
+
{
|
69
|
+
widget_name: :quantity,
|
70
|
+
result: client.from(table_name).count
|
71
|
+
}.to_json
|
72
|
+
end
|
73
|
+
|
74
|
+
def week_statistic(table_name)
|
75
|
+
result = 6.downto(0).map do |day|
|
76
|
+
current_date_time = DateTime.now - day
|
77
|
+
|
78
|
+
client.from(table_name).where(
|
79
|
+
created_at: beggining_of_day(current_date_time)..at_end_of_day(current_date_time)
|
80
|
+
).count
|
81
|
+
end
|
82
|
+
|
83
|
+
{
|
84
|
+
widget_name: :week_statistic,
|
85
|
+
result: result
|
86
|
+
}.to_json
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def client
|
92
|
+
@client ||= Sequel.connect(database_credentials)
|
93
|
+
end
|
94
|
+
|
95
|
+
def beggining_of_day(date)
|
96
|
+
DateTime.new(date.year, date.month, date.day, 0, 0, 0, 0)
|
97
|
+
end
|
98
|
+
|
99
|
+
def at_end_of_day(date)
|
100
|
+
DateTime.new(date.year, date.month, date.day, 23, 59, 59, 59)
|
101
|
+
end
|
102
|
+
|
103
|
+
def columns_by_table_name(name)
|
104
|
+
client.schema(name).map do |column_attributes|
|
105
|
+
column_name, column_information = *column_attributes
|
106
|
+
|
107
|
+
{
|
108
|
+
column_name: column_name,
|
109
|
+
data_type: column_information[:db_type]
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def search(search_query, table_name, model_attributes)
|
115
|
+
query_expressions = model_attributes.map do |model_attribute|
|
116
|
+
Sequel.like(model_attribute.to_sym, "%#{search_query}%")
|
117
|
+
end
|
118
|
+
|
119
|
+
query = query_expressions.first if query_expressions.one?
|
120
|
+
|
121
|
+
query_expressions.each_cons(2) do |current_value, next_value|
|
122
|
+
query = query.nil? ? current_value | next_value : query | (current_value | next_value)
|
123
|
+
end
|
124
|
+
|
125
|
+
[
|
126
|
+
client.from(table_name).where(query),
|
127
|
+
client.from(table_name).where(query).count
|
128
|
+
]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module Simpleadmin
|
6
|
+
# Configuration storage to customize allowed tables, to choose a database adapter and name
|
7
|
+
#
|
8
|
+
# @since 1.0.0
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# Simpleadmin::Config.setup do |config|
|
13
|
+
# config.database_credentials = {
|
14
|
+
# adapter: :postgres,
|
15
|
+
# database: 'squiz_development'
|
16
|
+
# }
|
17
|
+
#
|
18
|
+
# config.allowed_tables = ['users'] # Allowed tables
|
19
|
+
# # config.allowed_tables = [:all] # Allow all tables
|
20
|
+
#
|
21
|
+
# config.on_create = lambda do |model_class, resource_params|
|
22
|
+
# model_class.create(resource_params)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# config.on_update = lambda do |model_class, resource_id, resource_params|
|
26
|
+
# model_class.find(resource_id).update(resource_params)
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# config.on_destroy = lambda do |model_class, resource_id|
|
30
|
+
# model_class.find(resource_id).destroy
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
class Config
|
34
|
+
DEFAULT_TABLE_SCHEMAS = ['public'].freeze
|
35
|
+
|
36
|
+
include Singleton
|
37
|
+
|
38
|
+
attr_accessor :table_schemas, :database_credentials, :allowed_tables, :client
|
39
|
+
attr_accessor :on_create, :on_update, :on_destroy
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def setup
|
43
|
+
yield(instance)
|
44
|
+
end
|
45
|
+
|
46
|
+
def allowed_table?(table_name)
|
47
|
+
return true if instance.allowed_tables.include?(:all)
|
48
|
+
|
49
|
+
instance.allowed_tables.include?(table_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def allowed_tables
|
53
|
+
instance.allowed_tables || []
|
54
|
+
end
|
55
|
+
|
56
|
+
def database_credentials
|
57
|
+
instance.database_credentials
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_create
|
61
|
+
raise NotImplementedError, 'Please define #on_create in an initializer to use gem' if instance.on_create.nil?
|
62
|
+
|
63
|
+
instance.on_create
|
64
|
+
end
|
65
|
+
|
66
|
+
def on_update
|
67
|
+
raise NotImplementedError, 'Please define #on_update in an initializer to use gem' if instance.on_update.nil?
|
68
|
+
|
69
|
+
instance.on_update
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_destroy
|
73
|
+
raise NotImplementedError, 'Please define #on_destroy in an initializer to use gem' if instance.on_destroy.nil?
|
74
|
+
|
75
|
+
instance.on_destroy
|
76
|
+
end
|
77
|
+
|
78
|
+
def table_schemas
|
79
|
+
instance.table_schemas || DEFAULT_TABLE_SCHEMAS
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simpleadmin/adapters/base'
|
4
|
+
require 'simpleadmin/adapters/postgres'
|
5
|
+
|
6
|
+
module Simpleadmin
|
7
|
+
# Connector service that handles different databases to provide unified API endpoints
|
8
|
+
#
|
9
|
+
# @since 1.0.0
|
10
|
+
#
|
11
|
+
class DatabaseConnector
|
12
|
+
ADAPTERS_MAPPER = {
|
13
|
+
postgres: Adapters::Postgres
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(database_credentials:)
|
17
|
+
@database_credentials = database_credentials
|
18
|
+
end
|
19
|
+
|
20
|
+
def client
|
21
|
+
adapter.new(database_credentials: database_credentials)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :database_credentials
|
27
|
+
|
28
|
+
def adapter
|
29
|
+
adapter_class = ADAPTERS_MAPPER[database_credentials[:adapter]]
|
30
|
+
raise ArgumentError, 'Invalid adapter name or adapter not exist' if adapter_class.nil?
|
31
|
+
|
32
|
+
adapter_class
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simpleadmin
|
4
|
+
module Decorators
|
5
|
+
module Fields
|
6
|
+
class Base
|
7
|
+
def initialize(table_name, table_field_name, resource)
|
8
|
+
@table_name = table_name
|
9
|
+
@table_field_name = table_field_name
|
10
|
+
|
11
|
+
@resource = resource
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
raise NotImplementedError, 'Please follow the unified interface, add method #call'
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :table_name, :table_field_name, :resource
|
21
|
+
|
22
|
+
def model
|
23
|
+
model_class = table_name.classify.safe_constantize
|
24
|
+
|
25
|
+
if model_class.nil?
|
26
|
+
raise ArgumentError, "The model (#{table_name.classify}) does not exist"
|
27
|
+
else
|
28
|
+
model_class
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|