wowsql-sdk 1.3.0 → 3.0.1

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.
@@ -0,0 +1,250 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require_relative 'exceptions'
4
+
5
+ module WOWSQL
6
+ # Schema management client for PostgreSQL.
7
+ #
8
+ # Requires a SERVICE ROLE key (wowsql_service_...), not an anonymous key.
9
+ #
10
+ # @example
11
+ # schema = WOWSQL::WOWSQLSchema.new(
12
+ # "myproject",
13
+ # "wowsql_service_..."
14
+ # )
15
+ # schema.create_table("users", [
16
+ # { "name" => "id", "type" => "SERIAL", "auto_increment" => true },
17
+ # { "name" => "email", "type" => "VARCHAR(255)", "unique" => true, "nullable" => false },
18
+ # { "name" => "name", "type" => "VARCHAR(255)" },
19
+ # { "name" => "metadata", "type" => "JSONB", "default" => "'{}'" },
20
+ # { "name" => "created_at", "type" => "TIMESTAMPTZ", "default" => "CURRENT_TIMESTAMP" },
21
+ # ], primary_key: "id", indexes: ["email"])
22
+ class WOWSQLSchema
23
+ attr_reader :base_url
24
+
25
+ # @param project_url [String] Project subdomain or full URL
26
+ # @param service_key [String] Service role key (wowsql_service_...)
27
+ # @param base_domain [String] Base domain (default: "wowsql.com")
28
+ # @param secure [Boolean] Use HTTPS (default: true)
29
+ # @param timeout [Integer] Request timeout in seconds (default: 30)
30
+ # @param verify_ssl [Boolean] Verify SSL certificates (default: true)
31
+ def initialize(project_url, service_key, base_domain: 'wowsql.com', secure: true,
32
+ timeout: 30, verify_ssl: true)
33
+ if project_url.start_with?('http://') || project_url.start_with?('https://')
34
+ base = project_url.chomp('/')
35
+ base = base.split('/api').first if base.include?('/api')
36
+ @base_url = base
37
+ else
38
+ protocol = secure ? 'https' : 'http'
39
+ if project_url.include?(".#{base_domain}") || project_url.end_with?(base_domain)
40
+ @base_url = "#{protocol}://#{project_url}"
41
+ else
42
+ @base_url = "#{protocol}://#{project_url}.#{base_domain}"
43
+ end
44
+ end
45
+
46
+ @timeout = timeout
47
+
48
+ ssl_options = verify_ssl ? {} : { verify: false }
49
+
50
+ @conn = Faraday.new(url: @base_url, ssl: ssl_options) do |f|
51
+ f.request :json
52
+ f.response :json
53
+ f.adapter Faraday.default_adapter
54
+ f.options.timeout = timeout
55
+ end
56
+
57
+ @conn.headers['Authorization'] = "Bearer #{service_key}"
58
+ @conn.headers['Content-Type'] = 'application/json'
59
+ end
60
+
61
+ # ── Table operations ─────────────────────────────────────────
62
+
63
+ # Create a new table.
64
+ #
65
+ # @param table_name [String] Name of the table
66
+ # @param columns [Array<Hash>] Column definitions (name, type, auto_increment, unique, nullable, default)
67
+ # @param primary_key [String, nil] Primary key column name
68
+ # @param indexes [Array<String>, nil] Columns to create indexes on
69
+ # @return [Hash]
70
+ def create_table(table_name, columns, primary_key: nil, indexes: nil)
71
+ request('POST', '/api/v2/schema/tables', nil, {
72
+ table_name: table_name,
73
+ columns: columns,
74
+ primary_key: primary_key,
75
+ indexes: indexes
76
+ })
77
+ end
78
+
79
+ # Alter an existing table.
80
+ #
81
+ # Operations: add_column, drop_column, modify_column, rename_column
82
+ #
83
+ # @param table_name [String] Table name
84
+ # @param operation [String] Operation type
85
+ # @param column_name [String, nil] Column name
86
+ # @param column_type [String, nil] Column type
87
+ # @param new_column_name [String, nil] New column name (for rename)
88
+ # @param nullable [Boolean] Whether column is nullable
89
+ # @param default [String, nil] Default value
90
+ # @return [Hash]
91
+ def alter_table(table_name, operation, column_name: nil, column_type: nil,
92
+ new_column_name: nil, nullable: true, default: nil)
93
+ request('PATCH', "/api/v2/schema/tables/#{table_name}", nil, {
94
+ table_name: table_name,
95
+ operation: operation,
96
+ column_name: column_name,
97
+ column_type: column_type,
98
+ new_column_name: new_column_name,
99
+ nullable: nullable,
100
+ default: default
101
+ })
102
+ end
103
+
104
+ # Drop a table. WARNING: This cannot be undone!
105
+ #
106
+ # @param table_name [String] Table to drop
107
+ # @param cascade [Boolean] Also drop dependent objects
108
+ # @return [Hash]
109
+ def drop_table(table_name, cascade: false)
110
+ request('DELETE', "/api/v2/schema/tables/#{table_name}", { 'cascade' => cascade }, nil)
111
+ end
112
+
113
+ # Execute raw DDL SQL.
114
+ #
115
+ # Only schema statements are allowed: CREATE TABLE, ALTER TABLE,
116
+ # DROP TABLE, CREATE INDEX, DROP INDEX, etc.
117
+ #
118
+ # @param sql [String] DDL SQL statement
119
+ # @return [Hash]
120
+ def execute_sql(sql)
121
+ request('POST', '/api/v2/schema/execute', nil, { sql: sql })
122
+ end
123
+
124
+ # ── Convenience methods ──────────────────────────────────────
125
+
126
+ # Add a column to an existing table.
127
+ #
128
+ # @param table_name [String] Table name
129
+ # @param column_name [String] Column name
130
+ # @param column_type [String] Column type
131
+ # @param nullable [Boolean] Whether column is nullable
132
+ # @param default [String, nil] Default value
133
+ # @return [Hash]
134
+ def add_column(table_name, column_name, column_type, nullable: true, default: nil)
135
+ alter_table(
136
+ table_name, 'add_column',
137
+ column_name: column_name,
138
+ column_type: column_type,
139
+ nullable: nullable,
140
+ default: default
141
+ )
142
+ end
143
+
144
+ # Drop a column from a table.
145
+ #
146
+ # @param table_name [String] Table name
147
+ # @param column_name [String] Column name
148
+ # @return [Hash]
149
+ def drop_column(table_name, column_name)
150
+ alter_table(table_name, 'drop_column', column_name: column_name)
151
+ end
152
+
153
+ # Rename a column.
154
+ #
155
+ # @param table_name [String] Table name
156
+ # @param old_name [String] Current column name
157
+ # @param new_name [String] New column name
158
+ # @return [Hash]
159
+ def rename_column(table_name, old_name, new_name)
160
+ alter_table(
161
+ table_name, 'rename_column',
162
+ column_name: old_name,
163
+ new_column_name: new_name
164
+ )
165
+ end
166
+
167
+ # Change column type, nullability, or default value.
168
+ #
169
+ # @param table_name [String] Table name
170
+ # @param column_name [String] Column name
171
+ # @param column_type [String, nil] New column type
172
+ # @param nullable [Boolean, nil] New nullability
173
+ # @param default [String, nil] New default value
174
+ # @return [Hash]
175
+ def modify_column(table_name, column_name, column_type: nil, nullable: nil, default: nil)
176
+ kwargs = { column_name: column_name }
177
+ kwargs[:column_type] = column_type unless column_type.nil?
178
+ kwargs[:nullable] = nullable unless nullable.nil?
179
+ kwargs[:default] = default unless default.nil?
180
+ alter_table(table_name, 'modify_column', **kwargs)
181
+ end
182
+
183
+ # Create an index.
184
+ #
185
+ # @param table_name [String] Table to index
186
+ # @param columns [String, Array<String>] Column(s)
187
+ # @param unique [Boolean] Create a UNIQUE index
188
+ # @param name [String, nil] Custom index name
189
+ # @param using [String, nil] Index method (btree, hash, gin, gist)
190
+ # @return [Hash]
191
+ def create_index(table_name, columns, unique: false, name: nil, using: nil)
192
+ cols = columns.is_a?(Array) ? columns : [columns]
193
+ idx_name = name || "idx_#{table_name}_#{cols.join('_')}"
194
+ unique_kw = unique ? 'UNIQUE ' : ''
195
+ using_kw = using ? " USING #{using}" : ''
196
+ col_list = cols.map { |c| "\"#{c}\"" }.join(', ')
197
+ sql = "CREATE #{unique_kw}INDEX IF NOT EXISTS \"#{idx_name}\" ON \"#{table_name}\"#{using_kw} (#{col_list})"
198
+ execute_sql(sql)
199
+ end
200
+
201
+ # List all tables via the v2 REST API.
202
+ #
203
+ # @return [Array<String>]
204
+ def list_tables
205
+ resp = request('GET', '/api/v2/tables', nil, nil)
206
+ resp['tables'] || []
207
+ end
208
+
209
+ # Get column-level schema information for a table.
210
+ #
211
+ # @param table_name [String] Table name
212
+ # @return [Hash]
213
+ def get_table_schema(table_name)
214
+ request('GET', "/api/v2/tables/#{table_name}/schema", nil, nil)
215
+ end
216
+
217
+ # Close the HTTP connection.
218
+ def close
219
+ @conn.close if @conn.respond_to?(:close)
220
+ end
221
+
222
+ private
223
+
224
+ def request(method, path, params = nil, json = nil)
225
+ response = @conn.public_send(method.downcase, path) do |req|
226
+ req.params = params if params
227
+ req.body = json if json
228
+ end
229
+
230
+ if response.status == 403
231
+ raise SchemaPermissionError.new(
232
+ 'Schema operations require a SERVICE ROLE key. ' \
233
+ 'You are using an anonymous key which cannot modify database schema.',
234
+ 403
235
+ )
236
+ end
237
+
238
+ if response.status >= 400
239
+ error_data = response.body.is_a?(Hash) ? response.body : {}
240
+ error_msg = error_data['detail'] || error_data['message'] ||
241
+ "Request failed with status #{response.status}"
242
+ raise WOWSQLError.new(error_msg, response.status, error_data)
243
+ end
244
+
245
+ response.body
246
+ rescue Faraday::Error => e
247
+ raise WOWSQLError.new("Request failed: #{e.message}")
248
+ end
249
+ end
250
+ end