whoosh 1.5.0 → 1.7.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 +4 -4
- data/README.md +101 -47
- data/lib/whoosh/app.rb +3 -2
- data/lib/whoosh/cli/client_generator.rb +237 -0
- data/lib/whoosh/cli/main.rb +10 -0
- data/lib/whoosh/client_gen/base_generator.rb +84 -0
- data/lib/whoosh/client_gen/dependency_checker.rb +49 -0
- data/lib/whoosh/client_gen/fallback_backend.rb +292 -0
- data/lib/whoosh/client_gen/generators/expo.rb +1038 -0
- data/lib/whoosh/client_gen/generators/flutter.rb +915 -0
- data/lib/whoosh/client_gen/generators/htmx.rb +498 -0
- data/lib/whoosh/client_gen/generators/ios.rb +832 -0
- data/lib/whoosh/client_gen/generators/react_spa.rb +932 -0
- data/lib/whoosh/client_gen/generators/telegram_bot.rb +624 -0
- data/lib/whoosh/client_gen/generators/telegram_mini_app.rb +844 -0
- data/lib/whoosh/client_gen/introspector.rb +178 -0
- data/lib/whoosh/client_gen/ir.rb +37 -0
- data/lib/whoosh/version.rb +1 -1
- metadata +14 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# lib/whoosh/client_gen/fallback_backend.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Whoosh
|
|
7
|
+
module ClientGen
|
|
8
|
+
class FallbackBackend
|
|
9
|
+
def self.generate(root: Dir.pwd, oauth: false)
|
|
10
|
+
new(root: root, oauth: oauth).generate
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(root:, oauth:)
|
|
14
|
+
@root = root
|
|
15
|
+
@oauth = oauth
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate
|
|
19
|
+
generate_schemas
|
|
20
|
+
generate_auth_endpoint
|
|
21
|
+
generate_tasks_endpoint
|
|
22
|
+
generate_migrations
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def write_file(relative_path, content)
|
|
28
|
+
path = File.join(@root, relative_path)
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
30
|
+
File.write(path, content)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def generate_schemas
|
|
34
|
+
write_file("schemas/auth_schemas.rb", auth_schemas_content)
|
|
35
|
+
write_file("schemas/task_schemas.rb", task_schemas_content)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_auth_endpoint
|
|
39
|
+
write_file("endpoints/auth_endpoint.rb", auth_endpoint_content)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def generate_tasks_endpoint
|
|
43
|
+
write_file("endpoints/tasks_endpoint.rb", tasks_endpoint_content)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generate_migrations
|
|
47
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
48
|
+
write_file("db/migrations/#{timestamp}_create_users.rb", create_users_migration)
|
|
49
|
+
write_file("db/migrations/#{timestamp.to_i + 1}_create_tasks.rb", create_tasks_migration)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def auth_schemas_content
|
|
53
|
+
<<~RUBY
|
|
54
|
+
# frozen_string_literal: true
|
|
55
|
+
|
|
56
|
+
class LoginRequest < Whoosh::Schema
|
|
57
|
+
field :email, String, required: true
|
|
58
|
+
field :password, String, required: true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class RegisterRequest < Whoosh::Schema
|
|
62
|
+
field :name, String, required: true
|
|
63
|
+
field :email, String, required: true
|
|
64
|
+
field :password, String, required: true, min_length: 8
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class TokenResponse < Whoosh::Schema
|
|
68
|
+
field :token, String, required: true
|
|
69
|
+
field :refresh_token, String, required: true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class UserResponse < Whoosh::Schema
|
|
73
|
+
field :id, Integer, required: true
|
|
74
|
+
field :name, String, required: true
|
|
75
|
+
field :email, String, required: true
|
|
76
|
+
end
|
|
77
|
+
RUBY
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def task_schemas_content
|
|
81
|
+
<<~RUBY
|
|
82
|
+
# frozen_string_literal: true
|
|
83
|
+
|
|
84
|
+
class CreateTaskRequest < Whoosh::Schema
|
|
85
|
+
field :title, String, required: true
|
|
86
|
+
field :description, String
|
|
87
|
+
field :status, String, enum: %w[pending in_progress completed]
|
|
88
|
+
field :due_date, String
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class UpdateTaskRequest < Whoosh::Schema
|
|
92
|
+
field :title, String
|
|
93
|
+
field :description, String
|
|
94
|
+
field :status, String, enum: %w[pending in_progress completed]
|
|
95
|
+
field :due_date, String
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class TaskResponse < Whoosh::Schema
|
|
99
|
+
field :id, Integer, required: true
|
|
100
|
+
field :user_id, Integer, required: true
|
|
101
|
+
field :title, String, required: true
|
|
102
|
+
field :description, String
|
|
103
|
+
field :status, String, required: true
|
|
104
|
+
field :due_date, String
|
|
105
|
+
field :created_at, String, required: true
|
|
106
|
+
field :updated_at, String, required: true
|
|
107
|
+
end
|
|
108
|
+
RUBY
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def auth_endpoint_content
|
|
112
|
+
oauth_routes = if @oauth
|
|
113
|
+
<<~'RUBY'
|
|
114
|
+
|
|
115
|
+
App.get "/auth/:provider", auth: false do |req, db:|
|
|
116
|
+
provider = req.params[:provider]
|
|
117
|
+
redirect_url = App.oauth.authorize_url(provider: provider)
|
|
118
|
+
{ redirect_url: redirect_url }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
App.get "/auth/:provider/callback", auth: false do |req, db:|
|
|
122
|
+
provider = req.params[:provider]
|
|
123
|
+
user_info = App.oauth.handle_callback(provider: provider, params: req.params)
|
|
124
|
+
user = db[:users].where(email: user_info[:email]).first
|
|
125
|
+
unless user
|
|
126
|
+
user = { name: user_info[:name], email: user_info[:email], password_hash: nil }
|
|
127
|
+
user[:id] = db[:users].insert(user.merge(created_at: Time.now, updated_at: Time.now))
|
|
128
|
+
end
|
|
129
|
+
token = App.jwt.generate(sub: user[:id], email: user[:email])
|
|
130
|
+
refresh = App.jwt.generate(sub: user[:id], type: :refresh)
|
|
131
|
+
{ token: token, refresh_token: refresh }
|
|
132
|
+
end
|
|
133
|
+
RUBY
|
|
134
|
+
else
|
|
135
|
+
""
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
<<~RUBY
|
|
139
|
+
# frozen_string_literal: true
|
|
140
|
+
|
|
141
|
+
App.post "/auth/register", auth: false do |req, db:|
|
|
142
|
+
data = RegisterRequest.validate!(req.body)
|
|
143
|
+
existing = db[:users].where(email: data[:email]).first
|
|
144
|
+
raise Whoosh::Errors::ValidationError, "Email already registered" if existing
|
|
145
|
+
|
|
146
|
+
password_hash = BCrypt::Password.create(data[:password])
|
|
147
|
+
user_id = db[:users].insert(
|
|
148
|
+
name: data[:name],
|
|
149
|
+
email: data[:email],
|
|
150
|
+
password_hash: password_hash,
|
|
151
|
+
created_at: Time.now,
|
|
152
|
+
updated_at: Time.now
|
|
153
|
+
)
|
|
154
|
+
token = App.jwt.generate(sub: user_id, email: data[:email])
|
|
155
|
+
refresh = App.jwt.generate(sub: user_id, type: :refresh)
|
|
156
|
+
{ token: token, refresh_token: refresh }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
App.post "/auth/login", auth: false do |req, db:|
|
|
160
|
+
data = LoginRequest.validate!(req.body)
|
|
161
|
+
user = db[:users].where(email: data[:email]).first
|
|
162
|
+
raise Whoosh::Errors::UnauthorizedError, "Invalid credentials" unless user
|
|
163
|
+
|
|
164
|
+
stored = BCrypt::Password.new(user[:password_hash])
|
|
165
|
+
raise Whoosh::Errors::UnauthorizedError, "Invalid credentials" unless stored == data[:password]
|
|
166
|
+
|
|
167
|
+
token = App.jwt.generate(sub: user[:id], email: user[:email])
|
|
168
|
+
refresh = App.jwt.generate(sub: user[:id], type: :refresh)
|
|
169
|
+
{ token: token, refresh_token: refresh }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
App.post "/auth/refresh", auth: :jwt do |req, db:|
|
|
173
|
+
user = db[:users].where(id: req.current_user[:sub]).first
|
|
174
|
+
raise Whoosh::Errors::UnauthorizedError, "User not found" unless user
|
|
175
|
+
|
|
176
|
+
token = App.jwt.generate(sub: user[:id], email: user[:email])
|
|
177
|
+
refresh = App.jwt.generate(sub: user[:id], type: :refresh)
|
|
178
|
+
{ token: token, refresh_token: refresh }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
App.delete "/auth/logout", auth: :jwt do |req, db:|
|
|
182
|
+
App.jwt.revoke(req.token) if App.jwt.respond_to?(:revoke)
|
|
183
|
+
{ message: "Logged out successfully" }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
App.get "/auth/me", auth: :jwt do |req, db:|
|
|
187
|
+
user = db[:users].where(id: req.current_user[:sub]).first
|
|
188
|
+
raise Whoosh::Errors::NotFoundError, "User not found" unless user
|
|
189
|
+
|
|
190
|
+
{ id: user[:id], name: user[:name], email: user[:email] }
|
|
191
|
+
end
|
|
192
|
+
#{oauth_routes}
|
|
193
|
+
RUBY
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def tasks_endpoint_content
|
|
197
|
+
<<~RUBY
|
|
198
|
+
# frozen_string_literal: true
|
|
199
|
+
|
|
200
|
+
App.get "/tasks", auth: :jwt do |req, db:|
|
|
201
|
+
dataset = db[:tasks].where(user_id: req.current_user[:sub])
|
|
202
|
+
paginate_cursor(dataset, cursor: req.params[:cursor], limit: req.params.fetch(:limit, 20).to_i)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
App.get "/tasks/:id", auth: :jwt do |req, db:|
|
|
206
|
+
task = db[:tasks].where(id: req.params[:id], user_id: req.current_user[:sub]).first
|
|
207
|
+
raise Whoosh::Errors::NotFoundError, "Task not found" unless task
|
|
208
|
+
|
|
209
|
+
task
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
App.post "/tasks", auth: :jwt do |req, db:|
|
|
213
|
+
data = CreateTaskRequest.validate!(req.body)
|
|
214
|
+
task_id = db[:tasks].insert(
|
|
215
|
+
user_id: req.current_user[:sub],
|
|
216
|
+
title: data[:title],
|
|
217
|
+
description: data[:description],
|
|
218
|
+
status: data.fetch(:status, "pending"),
|
|
219
|
+
due_date: data[:due_date],
|
|
220
|
+
created_at: Time.now,
|
|
221
|
+
updated_at: Time.now
|
|
222
|
+
)
|
|
223
|
+
db[:tasks].where(id: task_id).first
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
App.put "/tasks/:id", auth: :jwt do |req, db:|
|
|
227
|
+
task = db[:tasks].where(id: req.params[:id], user_id: req.current_user[:sub]).first
|
|
228
|
+
raise Whoosh::Errors::NotFoundError, "Task not found" unless task
|
|
229
|
+
|
|
230
|
+
data = UpdateTaskRequest.validate!(req.body)
|
|
231
|
+
updates = data.compact.merge(updated_at: Time.now)
|
|
232
|
+
db[:tasks].where(id: req.params[:id]).update(updates)
|
|
233
|
+
db[:tasks].where(id: req.params[:id]).first
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
App.delete "/tasks/:id", auth: :jwt do |req, db:|
|
|
237
|
+
task = db[:tasks].where(id: req.params[:id], user_id: req.current_user[:sub]).first
|
|
238
|
+
raise Whoosh::Errors::NotFoundError, "Task not found" unless task
|
|
239
|
+
|
|
240
|
+
db[:tasks].where(id: req.params[:id]).delete
|
|
241
|
+
{ message: "Task deleted" }
|
|
242
|
+
end
|
|
243
|
+
RUBY
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def create_users_migration
|
|
247
|
+
<<~RUBY
|
|
248
|
+
# frozen_string_literal: true
|
|
249
|
+
|
|
250
|
+
Sequel.migration do
|
|
251
|
+
change do
|
|
252
|
+
create_table(:users) do
|
|
253
|
+
primary_key :id
|
|
254
|
+
String :name, null: false
|
|
255
|
+
String :email, null: false
|
|
256
|
+
String :password_hash
|
|
257
|
+
DateTime :created_at, null: false
|
|
258
|
+
DateTime :updated_at, null: false
|
|
259
|
+
|
|
260
|
+
index :email, unique: true
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
RUBY
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def create_tasks_migration
|
|
268
|
+
<<~RUBY
|
|
269
|
+
# frozen_string_literal: true
|
|
270
|
+
|
|
271
|
+
Sequel.migration do
|
|
272
|
+
change do
|
|
273
|
+
create_table(:tasks) do
|
|
274
|
+
primary_key :id
|
|
275
|
+
foreign_key :user_id, :users, null: false
|
|
276
|
+
String :title, null: false
|
|
277
|
+
Text :description
|
|
278
|
+
String :status, default: "pending"
|
|
279
|
+
Date :due_date
|
|
280
|
+
DateTime :created_at, null: false
|
|
281
|
+
DateTime :updated_at, null: false
|
|
282
|
+
|
|
283
|
+
index :user_id
|
|
284
|
+
index :status
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
RUBY
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|