autorest 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c854866f2912dfd5420837fc1aadbb19e8c61de643682c72e2ccff56256d8685
4
+ data.tar.gz: c5be271a7e68f590840621f86ef96216406200fa1e3eafd2b3d4ed49efe7fc5c
5
+ SHA512:
6
+ metadata.gz: eb6f7672555ac4bbd854a7a9635805a593a9a2562e3abd5e9d7974a93eaa02c866704a644268a9d65f97fd92094ad470d0905899e155c99b0276bccf895187ec
7
+ data.tar.gz: e6499256011494ed17c5b697c88777699599caeb4068903f5d11abd4ffa077f99ccaea32c0ab85b39d1ac28211705fb90f79c3622b004ef36bca3d0608ff0454
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Harish Kumar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # 🌐 AutoREST
2
+ [![made-with-ruby](https://img.shields.io/badge/Made%20with-Ruby-red)](https://www.ruby-lang.org)
3
+
4
+ Generate full-featured API servers for your database tables in seconds.
5
+
6
+ # ℹ About
7
+ AutoREST is a database-agnostic RESTful API generator for Ruby. With just your database credentials, it scaffolds a live API server supporting CRUD operations — no Rails, no boilerplate.
8
+
9
+ **Supported Databases**:
10
+
11
+ * SQLite
12
+ * MySQL
13
+ * PostgreSQL
14
+ * Oracle
15
+
16
+ # ✨ Features
17
+
18
+ * 🛠 Generates RESTful APIs from your database schema
19
+ * 🔌 Pluggable DB adapter system
20
+ * 🎛 CLI interface powered by [Thor](https://github.com/rails/thor)
21
+ * 🗃 Supports major relational DBs via corresponding gems
22
+ * 🔥 Runs on Puma + Rack
23
+
24
+ # 🚀 Installation
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'autorest'
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ ```bash
34
+ $ bundle install
35
+ ```
36
+
37
+ Or install it as a gem:
38
+
39
+ ```bash
40
+ $ gem install autorest
41
+ ```
42
+ > Note: Depending on the DB you use, you may need to install additional gems manually:
43
+ > * `sqlite3`
44
+ > * `mysql2`
45
+ > * `pg`
46
+ > * `ruby-oci8`
47
+
48
+ # 🏃🏻‍♀️ Quickstart
49
+ To get your hand on AutoREST, run:
50
+
51
+ ```bash
52
+ autorest boot sqlite://[path/to/sqlite.db]/[table_name]
53
+ ```
54
+
55
+ If you want to try with MySQL/PostgreSQL/Oracle, run:
56
+
57
+ ```bash
58
+ autorest boot mysql://[username]:[password]@[host]:[port]/[database]/[table_name]
59
+ ```
60
+
61
+ for PostgreSQL (or) Oracle, use `pg://` (or) `orcl://` respectively instead of `mysql://`
62
+
63
+ Now you can access the server at `http://localhost:7914`
64
+
65
+ # 🖥 CLI usage
66
+ 1. Via Interactive CLI
67
+
68
+ ```bash
69
+ $ autorest new
70
+ ```
71
+
72
+ 2. Via YAML config file
73
+
74
+ ```bash
75
+ $ autorest server <path/to/config>.yml
76
+ ```
77
+
78
+ 3. Via DSN
79
+
80
+ ```bash
81
+ $ autorest boot mysql://[username]:[password]@[host]:[port]/[database]/[table_name]
82
+ ```
83
+
84
+ # 📦 Configuration example
85
+ ```yaml
86
+ db:
87
+ kind: mysql # sqlite, mysql, pg, orcl
88
+ host: localhost
89
+ port: 3306
90
+ user: root
91
+ passwd: secret
92
+ name: mydb # for sqlite: path/to/sqlite.db, for oracle: SID
93
+ tables: [users, posts]
94
+
95
+ server:
96
+ host: 127.0.0.1
97
+ port: 8080
98
+ ```
99
+
100
+ # 🌐 API endpoints
101
+ Once the server is running, you can access the following RESTful API endpoints for the selected tables:
102
+
103
+ * `GET /<table>` - Returns all rows from table
104
+ * `GET /<table>/:id` - Returns a single row by ID (or any primary key)
105
+ * `POST /<table>` - Creates a new row in table
106
+ * `PUT /<table>/:id` - Updates an existing row by ID (or any primary key)
107
+ * `PATCH /<table>/:id` - Updates an existing row by ID (or any primary key)
108
+ * `DELETE /users/:id` - Deletes a user by ID
109
+
110
+ The `PATCH` method simply allows one to update a subset of the columns, whereas the `PUT` method allows one to update all columns.
111
+
112
+ # ✍🏻 Contributing
113
+ Contributions are welcome! While the basic functionality of this project works, there is a lot of room for improvement. If you have any suggestions or find any bugs, please [open an issue](https://github.com/harishtpj/AutoREST/issues/new/choose) or [create a pull request](https://github.com/harishtpj/AutoREST/pulls).
114
+
115
+ # 📝 License
116
+
117
+ #### Copyright © 2025 [M.V.Harish Kumar](https://github.com/harishtpj). <br>
118
+
119
+ #### This project is [MIT](https://github.com/harishtpj/AutoREST/blob/0341e153b1a8a1df139ff7225cb5f997818db89b/LICENSE) licensed.
data/bin/autorest ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "autorest"
4
+
5
+ AutoREST::CLI.start(ARGV)
@@ -0,0 +1,180 @@
1
+ # Database Adapter class for AutoREST.
2
+ #
3
+ # This abstract class serves as a base class for specific database adapters such as
4
+ # SQLite, MySQL, PostgreSQL, and Oracle. It defines the common interface that all
5
+ # adapters must implement. These include methods for preparing the database (e.g.,
6
+ # fetching table and column metadata), executing SQL queries, and managing database
7
+ # connections.
8
+ #
9
+ # @abstract
10
+ class AutoREST::DBAdapter
11
+
12
+ # Initializes a new DBAdapter instance.
13
+ #
14
+ # @param db_kind [Symbol] The type of database (e.g., :sqlite, :mysql, :pg, :orcl)
15
+ # @param db_name [String] The database name or SID (for Oracle)
16
+ # @param db_conn [Object] The database connection object
17
+ def initialize(db_kind, db_name, db_conn)
18
+ @db_kind = db_kind
19
+ @dbname = db_name
20
+ @db_conn = db_conn
21
+ @tables = nil
22
+ end
23
+
24
+ # Prepares the database by fetching metadata, such as tables and columns.
25
+ # This method must be implemented by subclasses.
26
+ #
27
+ # @raise [NotImplementedError] If the method is not implemented by a subclass.
28
+ # @abstract
29
+ def prepare
30
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
31
+ end
32
+
33
+ # Executes a raw SQL query.
34
+ #
35
+ # @param sql [String] The SQL query to execute
36
+ # @return [Array<Hash>] The result of the query as an array of hashes
37
+ # @abstract
38
+ def exec_sql(sql)
39
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
40
+ end
41
+
42
+ # Escapes input data to safely use in SQL queries.
43
+ #
44
+ # @param input [String] The raw input data
45
+ # @return [String] The escaped input data
46
+ # @abstract
47
+ def escape(input)
48
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
49
+ end
50
+
51
+ # Sets the access tables for the database. If no tables are specified, all tables are accessible.
52
+ #
53
+ # @param access_tab [Array<String>] A list of tables the user has access to
54
+ # @return [void]
55
+ def set_access_tables(access_tab)
56
+ @access_tables = access_tab.empty? ? @tables.keys : access_tab
57
+ end
58
+
59
+ # Returns the list of table names in the database.
60
+ #
61
+ # @return [Array<String>] A list of table names
62
+ def tables
63
+ prepare if @tables.nil?
64
+ @tables.keys
65
+ end
66
+
67
+ # Returns the list of column names for a given table.
68
+ #
69
+ # @param table_name [String] The name of the table
70
+ # @return [Array<String>] A list of column names
71
+ def columns(table_name)
72
+ prepare if @tables.nil?
73
+ @tables[table_name].keys
74
+ end
75
+
76
+ # Returns the rows of a table for the specified columns.
77
+ #
78
+ # @param table_name [String] The name of the table
79
+ # @param cols [String, Array<String>] The columns to retrieve (defaults to "*")
80
+ # @return [Array<Hash>, String] The rows of the table as an array of hashes, or an error message
81
+ def rows(table_name, cols = "*")
82
+ prepare if @tables.nil?
83
+ return "404: Table #{table_name} does not exist" unless @tables.include?(table_name)
84
+ return "403: Insufficient rights to access Table #{table_name}" unless @access_tables.include?(table_name)
85
+ result = exec_sql("select #{escape(cols)} from #{escape(table_name)}")
86
+ return "404: Table #{table_name} is empty" if result.empty?
87
+ result
88
+ end
89
+
90
+ # Returns a specific row in a table identified by its primary key value.
91
+ #
92
+ # @param table_name [String] The name of the table
93
+ # @param value [String, Integer] The value of the primary key
94
+ # @param cols [String, Array<String>] The columns to retrieve (defaults to "*")
95
+ # @return [Hash, String] The row as a hash, or an error message
96
+ def row(table_name, value, cols = "*")
97
+ prepare if @tables.nil?
98
+ return "404: Table #{table_name} does not exist" unless @tables.include?(table_name)
99
+ return "403: Insufficient rights to access Table #{table_name}" unless @access_tables.include?(table_name)
100
+ return "502: Table does not have primary key" if pkey(table_name).nil?
101
+ result = exec_sql("select #{escape(cols)} from #{table_name} where #{pkey(table_name)} = #{value.inspect}")
102
+ return "404: Row not found" if result.empty?
103
+ result
104
+ end
105
+
106
+ # Inserts a new row into a table.
107
+ #
108
+ # @param table_name [String] The name of the table
109
+ # @param data [Hash] The data to insert, where keys are column names and values are column values
110
+ # @return [Hash, String] The inserted row as a hash, or an error message
111
+ def insert(table_name, data)
112
+ prepare if @tables.nil?
113
+ return "404: Table #{table_name} does not exist" unless @tables.include?(table_name)
114
+ return "403: Insufficient rights to access Table #{table_name}" unless @access_tables.include?(table_name)
115
+ return "409: Row already exists" if has_row(table_name, data[pkey(table_name)])
116
+ cols = data.keys.join(", ")
117
+ values = data.values.map(&:inspect).join(", ")
118
+ exec_sql("insert into #{escape(table_name)} (#{cols}) values (#{values})")
119
+ row(table_name, data[pkey(table_name)])
120
+ end
121
+
122
+ # Updates an existing row in a table.
123
+ #
124
+ # @param table_name [String] The name of the table
125
+ # @param pk [String, Integer] The primary key of the row to update
126
+ # @param value [Hash] The new values for the row, where keys are column names and values are column values
127
+ # @param patch [Boolean] If true, allows the partial changes, for PATCH requests (defaults to false)
128
+ # @return [Hash, String] The updated row as a hash, or an error message
129
+ def update(table_name, pk, value, patch = false)
130
+ prepare if @tables.nil?
131
+ return "404: Table #{table_name} does not exist" unless @tables.include?(table_name)
132
+ return "403: Insufficient rights to update Table #{table_name}" unless @access_tables.include?(table_name)
133
+ return "404: Row not found" unless has_row(table_name, pk)
134
+ return "422: Primary key mismatch" if (pk != value[pkey(table_name)].to_s && !patch)
135
+ return "422: Invalid data" if (value.keys & columns(table_name) != value.keys)
136
+ kvpairs = value.map { |k, v| "#{k} = #{v.inspect}" }.join(", ")
137
+ exec_sql("update #{escape(table_name)} set #{kvpairs} where #{pkey(table_name)} = #{pk.inspect}")
138
+ row(table_name, pk)
139
+ end
140
+
141
+ # Deletes a row from a table.
142
+ #
143
+ # @param table_name [String] The name of the table
144
+ # @param value [String, Integer] The value of the primary key of the row to delete
145
+ # @return [Hash, String] The deleted row as a hash, or an error message
146
+ def del_row(table_name, value)
147
+ prepare if @tables.nil?
148
+ return "404: Table #{table_name} does not exist" unless @tables.include?(table_name)
149
+ return "403: Insufficient rights to delete Table #{table_name}" unless @access_tables.include?(table_name)
150
+ result = row(table_name, value)
151
+ return result if result.is_a?(String)
152
+ exec_sql("delete from #{escape(table_name)} where #{pkey(table_name)} = #{value.inspect}")
153
+ result
154
+ end
155
+
156
+ # Closes the database connection.
157
+ #
158
+ # @return [void]
159
+ def close
160
+ @db_conn.close
161
+ end
162
+
163
+ private
164
+ # Returns the primary key column name for a given table.
165
+ #
166
+ # @param table_name [String] The name of the table
167
+ # @return [String, nil] The primary key column name, or nil if no primary key exists
168
+ def pkey(table_name)
169
+ @tables[table_name].keys.find { |k| @tables[table_name][k][:pk] }
170
+ end
171
+
172
+ # Checks if a row exists in the table based on the primary key value.
173
+ #
174
+ # @param table_name [String] The name of the table
175
+ # @param value [String, Integer] The value of the primary key
176
+ # @return [Boolean] True if the row exists, false otherwise
177
+ def has_row(table_name, value)
178
+ !row(table_name, value).is_a?(String)
179
+ end
180
+ end
@@ -0,0 +1,57 @@
1
+ # DB Adapter for MySQL database
2
+ begin
3
+ require "mysql2"
4
+ rescue LoadError
5
+ warn "Please install the 'mysql2' gem to use MySQL database."
6
+ end
7
+
8
+ require_relative "adapter"
9
+
10
+ # MySQL adapter for AutoREST.
11
+ #
12
+ # Uses the `mysql2` gem to connect and interact with a MySQL database.
13
+ # Automatically detects tables and primary key columns.
14
+ #
15
+ # @example Initialize adapter
16
+ # db = AutoREST::MySQLDB.new("localhost", 3306, "root", "password", "mydb")
17
+ #
18
+ class AutoREST::MySQLDB < AutoREST::DBAdapter
19
+
20
+ # @param host [String] Hostname of the MySQL server
21
+ # @param port [Integer] Port number
22
+ # @param user [String] Username
23
+ # @param passwd [String] Password
24
+ # @param dbname [String] Name of the MySQL database
25
+ def initialize(host, port, user, passwd, dbname)
26
+ conn = Mysql2::Client.new(host: host, port: port, username: user, password: passwd, database: dbname)
27
+ super(:mysql, dbname, conn)
28
+ end
29
+
30
+ # Loads table metadata including columns and primary keys.
31
+ # @return [void]
32
+ def prepare
33
+ @tables = {}
34
+ @db_conn.query("show tables").each do |t|
35
+ tname = t["Tables_in_#{@dbname}"]
36
+ row_details = @db_conn.query("desc #{tname}")
37
+ @tables[tname] = {}
38
+ row_details.each do |row|
39
+ @tables[tname][row["Field"]] = {type: row["Type"], pk: row["Key"] == "PRI"}
40
+ end
41
+ end
42
+ end
43
+
44
+ # Executes a raw SQL query.
45
+ # @param sql [String] The SQL query to run
46
+ # @return [Array<Hash>] Resulting rows
47
+ def exec_sql(sql)
48
+ @db_conn.query(sql).to_a
49
+ end
50
+
51
+ # Escapes identifiers or values for safe usage in queries.
52
+ # @param input [String] Table or column name
53
+ # @return [String] Escaped string
54
+ def escape(input)
55
+ @db_conn.escape(input)
56
+ end
57
+ end
@@ -0,0 +1,90 @@
1
+ # DB Adapter for Oracle database
2
+ begin
3
+ ENV['NLS_LANG'] ||= 'AMERICAN_AMERICA.US7ASCII'
4
+ require "oci8"
5
+ rescue LoadError
6
+ warn "Please install the 'ruby-oci8' gem to use Oracle database."
7
+ end
8
+
9
+ require_relative "adapter"
10
+
11
+ # Oracle DB adapter for AutoREST.
12
+ #
13
+ # Uses the `oci8` gem to connect and interact with an Oracle database.
14
+ # Retrieves tables and primary key details from Oracle's user-owned tables and constraints.
15
+ #
16
+ # @example Initialize adapter
17
+ # db = AutoREST::OracleDB.new("localhost", 1521, "sys", "secret", "ORCL")
18
+ #
19
+ class AutoREST::OracleDB < AutoREST::DBAdapter
20
+
21
+ # @param host [String] Hostname of the Oracle server
22
+ # @param port [Integer] Port number
23
+ # @param user [String] Username
24
+ # @param passwd [String] Password
25
+ # @param sid [String] Oracle SID (System Identifier)
26
+ def initialize(host, port, user, passwd, sid)
27
+ conn = OCI8.new(user, passwd, "//#{host}:#{port}/#{sid}")
28
+ conn.autocommit = true
29
+ super(:orcl, sid, conn)
30
+ end
31
+
32
+ # Loads table metadata including columns and primary keys.
33
+ #
34
+ # Queries Oracle's `user_tab_columns` and `user_cons_columns` system views to get
35
+ # the column details and primary key information.
36
+ #
37
+ # @return [void]
38
+ def prepare
39
+ desc_query = <<~SQL
40
+ SELECT c.column_name,
41
+ c.data_type,
42
+ CASE WHEN pk.pk_column IS NOT NULL THEN 'YES' ELSE 'NO' END AS primary_key
43
+ FROM user_tab_columns c
44
+ LEFT JOIN (
45
+ SELECT ucc.column_name AS pk_column
46
+ FROM user_cons_columns ucc
47
+ JOIN user_constraints uc
48
+ ON ucc.constraint_name = uc.constraint_name
49
+ WHERE uc.constraint_type = 'P'
50
+ AND uc.table_name = :1
51
+ ) pk ON c.column_name = pk.pk_column
52
+ WHERE c.table_name = :1
53
+ SQL
54
+
55
+ @tables = {}
56
+ @db_conn.exec("select * from cat") do |t|
57
+ tname = t[0]
58
+ @tables[tname] = {}
59
+ @db_conn.exec(desc_query, tname) do |row|
60
+ @tables[tname][row[0]] = {type: row[1], pk: row[2] == "YES"}
61
+ end
62
+ end
63
+ end
64
+
65
+ # Executes a raw SQL query.
66
+ # @param sql [String] The SQL query to run
67
+ # @return [Array<Hash>] Resulting rows
68
+ def exec_sql(sql)
69
+ cursor = @db_conn.exec(sql)
70
+ cols = cursor.get_col_names
71
+ res = []
72
+ while row = cursor.fetch
73
+ res << Hash[cols.zip(row)]
74
+ end
75
+ res
76
+ end
77
+
78
+ # Closes the database connection.
79
+ # @return [void]
80
+ def close
81
+ @db_conn.logoff
82
+ end
83
+
84
+ # Escapes a string input to safely use in SQL queries.
85
+ # @param input [String] Raw user input
86
+ # @return [String] Escaped string
87
+ def escape(input)
88
+ input.to_s.gsub("'", "''")
89
+ end
90
+ end
@@ -0,0 +1,80 @@
1
+ # DB Adapter for PostgreSQL database
2
+ begin
3
+ require "pg"
4
+ rescue LoadError
5
+ warn "Please install the 'pg' gem to use PostgreSQL database."
6
+ end
7
+
8
+ require_relative "adapter"
9
+
10
+ # PostgreSQL adapter for AutoREST.
11
+ #
12
+ # Uses the `pg` gem to connect and interact with a PostgreSQL database.
13
+ # Detects tables and their primary keys by querying PostgreSQL system catalogs.
14
+ #
15
+ # @example Initialize adapter
16
+ # db = AutoREST::PostgresDB.new("localhost", 5432, "postgres", "secret", "mydb")
17
+ #
18
+ class AutoREST::PostgresDB < AutoREST::DBAdapter
19
+
20
+ # @param host [String] Hostname of the PostgreSQL server
21
+ # @param port [Integer] Port number
22
+ # @param user [String] Username
23
+ # @param passwd [String] Password
24
+ # @param dbname [String] Name of the PostgreSQL database
25
+ def initialize(host, port, user, passwd, dbname)
26
+ conn = PG.connect(host: host, port: port, user: user, password: passwd, dbname: dbname)
27
+ super(:pg, dbname, conn)
28
+ end
29
+
30
+ # Loads table metadata including columns and primary keys.
31
+ #
32
+ # It excludes system tables by filtering out `pg_catalog` and `information_schema` schemas.
33
+ #
34
+ # @return [void]
35
+ def prepare
36
+ desc_query = <<-SQL
37
+ SELECT
38
+ a.attname AS cname,
39
+ pg_catalog.format_type(a.atttypid, a.atttypmod) AS dtype,
40
+ coalesce(i.indisprimary, false) AS pk
41
+ FROM
42
+ pg_catalog.pg_attribute a
43
+ JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
44
+ JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
45
+ LEFT JOIN pg_catalog.pg_index i
46
+ ON c.oid = i.indrelid
47
+ AND a.attnum = ANY(i.indkey)
48
+ AND i.indisprimary
49
+ WHERE
50
+ c.relname = $1
51
+ AND a.attnum > 0
52
+ AND NOT a.attisdropped
53
+ ORDER BY a.attnum;
54
+ SQL
55
+ @tables = {}
56
+ @db_conn.exec("SELECT tablename FROM pg_catalog.pg_tables
57
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')").each do |t|
58
+ tname = t["tablename"]
59
+ row_details = @db_conn.exec_params(desc_query, [tname])
60
+ @tables[tname] = {}
61
+ row_details.each do |row|
62
+ @tables[tname][row["cname"]] = {type: row["type"], pk: row["pk"]}
63
+ end
64
+ end
65
+ end
66
+
67
+ # Executes a raw SQL query.
68
+ # @param sql [String] The SQL query to run
69
+ # @return [Array<Hash>] Resulting rows
70
+ def exec_sql(sql)
71
+ @db_conn.exec(sql).to_a
72
+ end
73
+
74
+ # Escapes a string input to safely use in SQL queries.
75
+ # @param input [String] Raw user input
76
+ # @return [String] Escaped string
77
+ def escape(input)
78
+ @db_conn.escape_string(input)
79
+ end
80
+ end
@@ -0,0 +1,54 @@
1
+ # DB Adapter for SQLite database
2
+ begin
3
+ require "sqlite3"
4
+ rescue LoadError
5
+ warn "Please install the 'sqlite3' gem to use SQLite database."
6
+ end
7
+
8
+ require_relative "adapter"
9
+
10
+ # SQLite adapter for AutoREST.
11
+ #
12
+ # Uses the `sqlite3` gem to connect and query the SQLite database.
13
+ # Automatically discovers tables and primary keys.
14
+ #
15
+ # @example Initialize adapter
16
+ # db = AutoREST::SQLiteDB.new("data.db")
17
+ #
18
+ class AutoREST::SQLiteDB < AutoREST::DBAdapter
19
+
20
+ # @param dbname [String] Path to the SQLite database file
21
+ def initialize(dbname)
22
+ conn = SQLite3::Database.new(dbname)
23
+ conn.results_as_hash = true
24
+ super(:sqlite, dbname, conn)
25
+ end
26
+
27
+ # Loads table metadata including columns and primary keys.
28
+ # @return [void]
29
+ def prepare
30
+ @tables = {}
31
+ @db_conn.execute("SELECT name FROM sqlite_master WHERE type='table'").each do |t|
32
+ tname = t['name']
33
+ row_details = @db_conn.execute("select name, type, pk from pragma_table_info('#{tname}')")
34
+ @tables[tname] = {}
35
+ row_details.each do |row|
36
+ @tables[tname][row['name']] = {type: row['type'], pk: row['pk'] == 1}
37
+ end
38
+ end
39
+ end
40
+
41
+ # Executes a raw SQL query.
42
+ # @param sql [String] The SQL query to run
43
+ # @return [Array<Hash>] Resulting rows
44
+ def exec_sql(sql)
45
+ @db_conn.execute(sql)
46
+ end
47
+
48
+ # Escapes identifiers or values for safe usage in queries.
49
+ # @param input [String] Table or column name
50
+ # @return [String] Escaped string
51
+ def escape(input)
52
+ SQLite3::Database.quote(input)
53
+ end
54
+ end
@@ -0,0 +1,123 @@
1
+ # The main server instance for AutoREST
2
+ require "sinatra/base"
3
+
4
+ # The main server instance for AutoREST.
5
+ #
6
+ # This class sets up a Sinatra web server that acts as the interface for
7
+ # interacting with the AutoREST API. It handles requests related to various
8
+ # database operations such as querying, inserting, updating, and deleting rows.
9
+ #
10
+ # @note This server is designed to work with different database adapters like
11
+ # SQLite, MySQL, PostgreSQL, and Oracle, as provided by the AutoREST framework.
12
+ #
13
+ # @example Starting the server
14
+ # AutoREST::Server.new(db_conn).run!
15
+ #
16
+ # @see AutoREST::DBAdapter
17
+ class AutoREST::Server < Sinatra::Base
18
+ # Initializes a new AutoREST::Server instance.
19
+ #
20
+ # @param db_conn [AutoREST::DBAdapter] The database connection object to interact with
21
+ def initialize(db_conn)
22
+ super()
23
+ @db_conn = db_conn
24
+ end
25
+
26
+ before do
27
+ content_type :json
28
+ end
29
+
30
+ helpers do
31
+ # Helper method to return a formatted error response.
32
+ #
33
+ # @param msg [String] The error message to return
34
+ # @param status [Integer] The HTTP status code to return (default is 400)
35
+ # @return [String] The JSON-formatted error response
36
+ def error(msg, status = 400)
37
+ halt status, { error: msg }.to_json
38
+ end
39
+
40
+ # Helper method to parse the body of the incoming request as JSON.
41
+ #
42
+ # @param req [Sinatra::Request] The incoming request object
43
+ # @return [Hash] The parsed JSON body
44
+ def get_body(req)
45
+ req.body.rewind
46
+ JSON.parse(req.body.read)
47
+ end
48
+ end
49
+
50
+ get '/' do
51
+ { message: "Welcome to AutoREST API Server" }.to_json
52
+ end
53
+
54
+ get '/:table/?' do |tname|
55
+ cols = params["only"] || "*"
56
+ q = @db_conn.rows(tname, cols)
57
+ if q.is_a?(String)
58
+ code, msg = q.split(": ", 2)
59
+ error(msg, code.to_i)
60
+ end
61
+ q.to_json
62
+ end
63
+
64
+ post '/:table/?' do |tname|
65
+ data = get_body(request)
66
+ error("Incomplete request body") if data.empty?
67
+ q = @db_conn.insert(tname, data)
68
+ if q.is_a?(String)
69
+ code, msg = q.split(": ", 2)
70
+ error(msg, code.to_i)
71
+ end
72
+ q.to_json
73
+ end
74
+
75
+ get '/:table/:pk/?' do |tname, pk|
76
+ cols = params["only"] || "*"
77
+ pk = pk.match?(/\A-?\d+(\.\d+)?\z/) ? pk.to_i : pk
78
+ q = @db_conn.row(tname, pk, cols)
79
+ if q.is_a?(String)
80
+ code, msg = q.split(": ", 2)
81
+ error(msg, code.to_i)
82
+ end
83
+ q.to_json
84
+ end
85
+
86
+ put '/:table/:pk/?' do |tname, pk|
87
+ data = get_body(request)
88
+ error("Incomplete request body") if (data.empty? || data.keys != @db_conn.columns(tname))
89
+ pk = pk.match?(/\A-?\d+(\.\d+)?\z/) ? pk.to_i : pk
90
+ q = @db_conn.update(tname, pk, data)
91
+ if q.is_a?(String)
92
+ code, msg = q.split(": ", 2)
93
+ error(msg, code.to_i)
94
+ end
95
+ q.to_json
96
+ end
97
+
98
+ patch '/:table/:pk/?' do |tname, pk|
99
+ data = get_body(request)
100
+ error("Incomplete request body") if data.empty?
101
+ pk = pk.match?(/\A-?\d+(\.\d+)?\z/) ? pk.to_i : pk
102
+ q = @db_conn.update(tname, pk, data, true)
103
+ if q.is_a?(String)
104
+ code, msg = q.split(": ", 2)
105
+ error(msg, code.to_i)
106
+ end
107
+ q.to_json
108
+ end
109
+
110
+ delete '/:table/:pk/?' do |tname, pk|
111
+ pk = pk.match?(/\A-?\d+(\.\d+)?\z/) ? pk.to_i : pk
112
+ q = @db_conn.del_row(tname, pk)
113
+ if q.is_a?(String)
114
+ code, msg = q.split(": ", 2)
115
+ error(msg, code.to_i)
116
+ end
117
+ q.to_json
118
+ end
119
+
120
+ error 500 do
121
+ { error: "Internal Server Error" }.to_json
122
+ end
123
+ end
@@ -0,0 +1,3 @@
1
+ module AutoREST
2
+ VERSION = "0.1.0"
3
+ end
data/lib/autorest.rb ADDED
@@ -0,0 +1,153 @@
1
+ # The main executable for AutoREST
2
+ #
3
+ # This file defines the command-line interface (CLI) for the AutoREST gem using Thor.
4
+ # The CLI allows users to generate a new AutoREST API server, start the server using
5
+ # a configuration file or DSN, and view the current version of the API.
6
+ #
7
+ # Commands:
8
+ # - `version`: Prints the version of AutoREST.
9
+ # - `new`: Creates a new AutoREST API server project.
10
+ # - `server FILE`: Starts the AutoREST API server using a configuration file.
11
+ # - `boot DSN`: Starts the AutoREST API server using a DSN.
12
+ #
13
+ # The commands involve setting up a database connection and starting a server with
14
+ # specified parameters such as the host, port, and database tables.
15
+
16
+ require "thor"
17
+ require "tty-prompt"
18
+ require "rack"
19
+ require "rack/handler/puma"
20
+ require "yaml"
21
+ require "uri"
22
+
23
+ require_relative "autorest/version"
24
+ require_relative "autorest/server"
25
+
26
+ class AutoREST::CLI < Thor
27
+
28
+ # Determines if the program should exit on failure
29
+ def self.exit_on_failure?
30
+ true
31
+ end
32
+
33
+ map "-v" => "version"
34
+ desc "version", "Prints the version of AutoREST"
35
+ # Prints the version of AutoREST
36
+ def version
37
+ puts "AutoREST v#{AutoREST::VERSION}"
38
+ end
39
+
40
+ map "-n" => "new"
41
+ desc "new", "Creates a new AutoREST API server"
42
+ # Creates a new AutoREST API server project
43
+ # Prompts the user for database details and creates a configuration file.
44
+ def new
45
+ prompt = TTY::Prompt.new
46
+ opts = {db: {}, server: { host: "localhost", port: 7914 } }
47
+ puts "Welcome to AutoREST API Server Generator"
48
+ project_name = prompt.ask("Enter the project's name:")
49
+ opts[:db][:kind] = prompt.select("Select your database:",
50
+ {"SQLite" => :sqlite, "MySQL" => :mysql, "PostgreSQL" => :pg, "Oracle" => :orcl},
51
+ default: "SQLite")
52
+
53
+ if opts[:db][:kind] == :sqlite
54
+ require_relative "autorest/db/sqlite"
55
+ opts[:db][:name] = prompt.ask("Enter location of DB file:")
56
+ db = AutoREST::SQLiteDB.new(opts[:db][:name])
57
+ else
58
+ def_port = {mysql: 3306, pg: 5432, orcl: 1521}
59
+ def_usr = {mysql: "root", pg: "postgres", orcl: "SYS"}
60
+ opts[:db][:host] = prompt.ask("Enter hostname of DB:", default: "localhost")
61
+ opts[:db][:port] = prompt.ask("Enter port of DB:", default: def_port[opts[:db][:kind]])
62
+ opts[:db][:user] = prompt.ask("Enter username:", default: def_usr[opts[:db][:kind]])
63
+ opts[:db][:passwd] = prompt.ask("Enter password:", echo: false)
64
+ opts[:db][:name] = prompt.ask("Enter database #{opts[:db][:kind] == :orcl ? "SID" : "name"}:")
65
+ case opts[:db][:kind]
66
+ when :mysql
67
+ require_relative "autorest/db/mysql"
68
+ db = AutoREST::MySQLDB.new(opts[:db][:host], opts[:db][:port], opts[:db][:user], opts[:db][:passwd], opts[:db][:name])
69
+ when :pg
70
+ require_relative "autorest/db/postgres"
71
+ db = AutoREST::PostgresDB.new(opts[:db][:host], opts[:db][:port], opts[:db][:user], opts[:db][:passwd], opts[:db][:name])
72
+ when :orcl
73
+ require_relative "autorest/db/oracle"
74
+ db = AutoREST::OracleDB.new(opts[:db][:host], opts[:db][:port], opts[:db][:user], opts[:db][:passwd], opts[:db][:name])
75
+ end
76
+ end
77
+
78
+ opts[:db][:tables] = prompt.multi_select("Select tables from database:", db.tables)
79
+ db.set_access_tables(opts[:db][:tables])
80
+ puts "Creating configuration file..."
81
+ File.open("#{project_name}.yml", "w") do |f|
82
+ f.write(opts.to_yaml)
83
+ end
84
+ puts "Successfully completed!"
85
+ start_server(db)
86
+ end
87
+
88
+ map "-S" => "server"
89
+ desc "server FILE", "Starts the AutoREST API server using a config file"
90
+ # Starts the AutoREST API server using a configuration file
91
+ # Loads the configuration file and starts the server with the specified database settings.
92
+ def server(file)
93
+ opts = YAML.load_file(file)
94
+ case opts[:db][:kind]
95
+ when :sqlite
96
+ require_relative "autorest/db/sqlite"
97
+ db = AutoREST::SQLiteDB.new(opts[:db][:name])
98
+ when :mysql
99
+ require_relative "autorest/db/mysql"
100
+ db = AutoREST::MySQLDB.new(opts[:db][:host], opts[:db][:port], opts[:db][:user], opts[:db][:passwd], opts[:db][:name])
101
+ when :pg
102
+ require_relative "autorest/db/postgres"
103
+ db = AutoREST::PostgresDB.new(opts[:db][:host], opts[:db][:port], opts[:db][:user], opts[:db][:passwd], opts[:db][:name])
104
+ when :orcl
105
+ require_relative "autorest/db/oracle"
106
+ db = AutoREST::OracleDB.new(opts[:db][:host], opts[:db][:port], opts[:db][:user], opts[:db][:passwd], opts[:db][:name])
107
+ end
108
+ db.prepare
109
+ db.set_access_tables(opts[:db][:tables])
110
+ servinfo = opts.fetch(:server, { host: "localhost", port: 7914})
111
+ start_server(db, servinfo[:host], servinfo[:port])
112
+ end
113
+
114
+ map "-s" => "boot"
115
+ desc "boot DSN", "Starts the AutoREST API server using a DSN"
116
+ # Starts the AutoREST API server using a DSN (Data Source Name)
117
+ # Parses the DSN and starts the server using the corresponding database.
118
+ def boot(dsn)
119
+ uri = URI.parse(dsn)
120
+ if uri.scheme == "sqlite"
121
+ require_relative "autorest/db/sqlite"
122
+ db = AutoREST::SQLiteDB.new(uri.host)
123
+ table, *_ = uri.path.sub(/^\//, '').split('/')
124
+ else
125
+ database, table = uri.path.sub(/^\//, '').split('/')
126
+ passwd = URI.decode_www_form_component(uri.password)
127
+ case uri.scheme
128
+ when "mysql"
129
+ require_relative "autorest/db/mysql"
130
+ db = AutoREST::MySQLDB.new(uri.host, uri.port, uri.user, passwd, database)
131
+ when "pg"
132
+ require_relative "autorest/db/postgres"
133
+ db = AutoREST::PostgresDB.new(uri.host, uri.port, uri.user, passwd, database)
134
+ when "orcl"
135
+ require_relative "autorest/db/oracle"
136
+ db = AutoREST::OracleDB.new(uri.host, uri.port, uri.user, passwd, database)
137
+ end
138
+ end
139
+ db.prepare
140
+ db.set_access_tables([table])
141
+ start_server(db)
142
+ end
143
+
144
+ no_commands do
145
+ # Starts the AutoREST server
146
+ def start_server(db, host = "localhost", port = 7914)
147
+ server = AutoREST::Server.new(db)
148
+ puts "Starting server..."
149
+ Rack::Handler::Puma.run(server, Host: host, Port: port)
150
+ db.close
151
+ end
152
+ end
153
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: autorest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - M.V. Harish Kumar
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-05-22 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sinatra
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: tty-prompt
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: puma
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: yaml
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: AutoREST is a lightweight CLI tool that turns your SQL database into
83
+ a fully working RESTful API server using Puma and Rack. Supports SQLite, MySQL,
84
+ PostgreSQL, and Oracle.
85
+ email:
86
+ - harishtpj@outlook.com
87
+ executables:
88
+ - autorest
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - LICENSE
93
+ - README.md
94
+ - bin/autorest
95
+ - lib/autorest.rb
96
+ - lib/autorest/db/adapter.rb
97
+ - lib/autorest/db/mysql.rb
98
+ - lib/autorest/db/oracle.rb
99
+ - lib/autorest/db/postgres.rb
100
+ - lib/autorest/db/sqlite.rb
101
+ - lib/autorest/server.rb
102
+ - lib/autorest/version.rb
103
+ homepage: https://github.com/harishtpj/AutoREST
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.6.2
122
+ specification_version: 4
123
+ summary: Tool to generate RESTful APIs
124
+ test_files: []