simpleadmin 1.4.0 → 1.5.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.
- 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
|