daidan 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 +7 -0
- data/.gitignore +9 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +44 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +4 -0
- data/bin/console +8 -0
- data/bin/daidan +35 -0
- data/bin/setup +6 -0
- data/daidan.gemspec +37 -0
- data/lib/daidan/commands.rb +36 -0
- data/lib/daidan/config/application.rb +75 -0
- data/lib/daidan/db/connection.rb +14 -0
- data/lib/daidan/db/create_base_user_migration.rb +10 -0
- data/lib/daidan/generators/base_generator.rb +158 -0
- data/lib/daidan/generators/resource_generator.rb +258 -0
- data/lib/daidan/graphql/mutations/base_mutation.rb +4 -0
- data/lib/daidan/graphql/mutations/login_user.rb +20 -0
- data/lib/daidan/graphql/types/base_object_type.rb +5 -0
- data/lib/daidan/graphql/types/base_user_type.rb +5 -0
- data/lib/daidan/middleware/jwt_authentication.rb +27 -0
- data/lib/daidan/models/user.rb +34 -0
- data/lib/daidan/version.rb +5 -0
- data/lib/daidan.rb +30 -0
- data/sig/daidan.rbs +3 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: da756040fd2fcaebe748b6533b4216bcb25aae4a554c018e81e05380b4e721e4
|
4
|
+
data.tar.gz: 634513d9b7ce3588e35155ddfaee1dfafa83ccac14b6b866d6373417d42ba4ee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a13f2d9507e15a8a8810e802766f25a614d9a005d424efe7413d9415a23b57a856a1a408099af54132103bcda77dc581a7fb8b2818c1b73c54237e7acf621aef
|
7
|
+
data.tar.gz: e63537fab9ec1ad7d94b0f3d5191e211d66a46bd99a6eab7de6c72557146c0efee4551f7cc319b736a1f6f852b5043e7292881fa64dad31b39cb52ba09dac005
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
daidan (0.1.0)
|
5
|
+
bcrypt (~> 3.1)
|
6
|
+
dotenv (~> 3.1)
|
7
|
+
graphql (~> 2.4)
|
8
|
+
jwt (~> 2.9)
|
9
|
+
sequel (~> 5.0)
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
base64 (0.2.0)
|
15
|
+
bcrypt (3.1.20)
|
16
|
+
bigdecimal (3.1.8)
|
17
|
+
dotenv (3.1.7)
|
18
|
+
fiber-storage (1.0.0)
|
19
|
+
graphql (2.4.8)
|
20
|
+
base64
|
21
|
+
fiber-storage
|
22
|
+
jwt (2.9.3)
|
23
|
+
base64
|
24
|
+
rake (13.2.1)
|
25
|
+
sequel (5.87.0)
|
26
|
+
bigdecimal
|
27
|
+
zeitwerk (2.7.1)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
x86_64-linux
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
bcrypt
|
35
|
+
daidan!
|
36
|
+
dotenv
|
37
|
+
graphql
|
38
|
+
jwt
|
39
|
+
rake
|
40
|
+
sequel
|
41
|
+
zeitwerk
|
42
|
+
|
43
|
+
BUNDLED WITH
|
44
|
+
2.5.23
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Bernard Cesarz
|
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,41 @@
|
|
1
|
+
# Daidan
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/daidan`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
15
|
+
```
|
16
|
+
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/daidan.
|
36
|
+
|
37
|
+
|
38
|
+
## NOTES
|
39
|
+
|
40
|
+
### MIGRATIONS:
|
41
|
+
`sequel sqlite://db.db -m db/migrations`
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/daidan
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
parser = OptionParser.new do |opts|
|
5
|
+
opts.banner = 'Usage: daidan [command] [arguments]'
|
6
|
+
end
|
7
|
+
|
8
|
+
parser.order!
|
9
|
+
command = ARGV.shift
|
10
|
+
|
11
|
+
if command == 'new'
|
12
|
+
require_relative '../lib/daidan/generators/base_generator'
|
13
|
+
|
14
|
+
app_name = ARGV.shift
|
15
|
+
if app_name.nil? || app_name.strip.empty?
|
16
|
+
puts 'Usage: daidan new app_name'
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
Daidan::Generators::BaseGenerator.new(app_name).generate
|
20
|
+
exit 0
|
21
|
+
else
|
22
|
+
require_relative '../lib/daidan'
|
23
|
+
|
24
|
+
if command.nil?
|
25
|
+
puts parser
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
if Daidan::Commands.respond_to?(command)
|
30
|
+
Daidan::Commands.public_send(command, *ARGV)
|
31
|
+
else
|
32
|
+
puts "Unknown command: #{command}"
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
end
|
data/bin/setup
ADDED
data/daidan.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'daidan'
|
5
|
+
spec.version = '0.1.0'
|
6
|
+
spec.authors = ['Bernard Cesarz']
|
7
|
+
spec.email = ['cesarzb@protonmail.com']
|
8
|
+
|
9
|
+
spec.summary = 'Lightweight GraphQL web framework.'
|
10
|
+
spec.description = 'A lightweight Ruby framework for building GraphQL-based web applications.'
|
11
|
+
spec.homepage = 'https://github.com/cesarzb/daidan'
|
12
|
+
spec.licenses = ['MIT']
|
13
|
+
|
14
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
15
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
16
|
+
spec.metadata['changelog_uri'] = 'https://github.com/cesarzb/daidan/blob/main/CHANGELOG.md'
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
18
|
+
spec.metadata['license_uri'] = 'https://opensource.org/licenses/MIT'
|
19
|
+
|
20
|
+
spec.files = Dir.chdir(__dir__) do
|
21
|
+
`git ls-files -z`.split("\x0").reject do |file|
|
22
|
+
file.start_with?('test/', 'spec/', 'features/', '.git/') || file.end_with?('.gem')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.bindir = 'bin'
|
27
|
+
spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.add_dependency 'bcrypt', '~> 3.1'
|
31
|
+
spec.add_dependency 'dotenv', '~> 3.1'
|
32
|
+
spec.add_dependency 'graphql', '~> 2.4'
|
33
|
+
spec.add_dependency 'jwt', '~> 2.9'
|
34
|
+
spec.add_dependency 'sequel', '~> 5.0'
|
35
|
+
|
36
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
37
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Daidan
|
2
|
+
module Commands
|
3
|
+
def self.create_base_user
|
4
|
+
require_relative 'db/connection'
|
5
|
+
require_relative 'config/application'
|
6
|
+
Daidan::Db::Connection.setup
|
7
|
+
|
8
|
+
Sequel::Model.db.transaction do
|
9
|
+
Sequel.migration do
|
10
|
+
change do
|
11
|
+
create_table(:users) do
|
12
|
+
primary_key :id
|
13
|
+
String :name, null: false
|
14
|
+
String :email, null: false, unique: true
|
15
|
+
String :password_digest, null: false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end.apply(Sequel::Model.db, :up)
|
19
|
+
end
|
20
|
+
puts "Table 'users' created successfully!"
|
21
|
+
rescue Sequel::DatabaseError => e
|
22
|
+
raise unless e.message =~ /table `users` already exists/i
|
23
|
+
|
24
|
+
puts "The 'users' table is already created."
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.generate(resource_name, *fields)
|
28
|
+
require_relative 'db/connection'
|
29
|
+
require_relative 'config/application'
|
30
|
+
Daidan::Db::Connection.setup
|
31
|
+
|
32
|
+
require_relative 'generators/resource_generator'
|
33
|
+
Daidan::Generators::ResourceGenerator.new(resource_name, fields).generate
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Daidan
|
2
|
+
class Application
|
3
|
+
def initialize
|
4
|
+
setup_zeitwerk
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
process_request(env)
|
10
|
+
rescue JSON::ParserError
|
11
|
+
handle_json_parse_error
|
12
|
+
rescue StandardError => e
|
13
|
+
handle_internal_server_error(e)
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def process_request(env)
|
19
|
+
req = Rack::Request.new(env)
|
20
|
+
|
21
|
+
if req.post? && req.path == '/graphql'
|
22
|
+
handle_graphql_request(req, env)
|
23
|
+
else
|
24
|
+
not_found_response
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def graphql_schema
|
29
|
+
raise NotImplementedError, 'Subclasses must define `graphql_schema`'
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_json_parse_error
|
33
|
+
[400, { 'content-type' => 'application/json' }, [{ error: 'Invalid JSON' }.to_json]]
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle_internal_server_error(error)
|
37
|
+
puts "Error processing request: #{error.message}"
|
38
|
+
[500, { 'content-type' => 'application/json' }, [{ error: 'Internal Server Error' }.to_json]]
|
39
|
+
end
|
40
|
+
|
41
|
+
def not_found_response
|
42
|
+
[404, { 'content-type' => 'text/plain' }, ['Not Found']]
|
43
|
+
end
|
44
|
+
|
45
|
+
def setup_zeitwerk
|
46
|
+
app_root = Dir.pwd
|
47
|
+
|
48
|
+
loader = Zeitwerk::Loader.new
|
49
|
+
loader.push_dir(File.join(app_root, ''))
|
50
|
+
loader.collapse(File.join(app_root, 'graphql'))
|
51
|
+
loader.collapse(File.join(app_root, 'graphql', 'types'))
|
52
|
+
loader.collapse(File.join(app_root, 'graphql', 'mutations'))
|
53
|
+
loader.collapse(File.join(app_root, 'models'))
|
54
|
+
loader.setup
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def handle_graphql_request(req, env)
|
60
|
+
body = req.body.read
|
61
|
+
params = body.empty? ? {} : JSON.parse(body)
|
62
|
+
|
63
|
+
current_user = env['current_user_id'] ? User.find(id: env['current_user_id']) : nil
|
64
|
+
|
65
|
+
result = graphql_schema.execute(
|
66
|
+
params['query'],
|
67
|
+
variables: params['variables'],
|
68
|
+
context: { current_user: current_user },
|
69
|
+
operation_name: params['operationName']
|
70
|
+
)
|
71
|
+
|
72
|
+
[200, { 'content-type' => 'application/json' }, [result.to_json]]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Daidan
|
2
|
+
module Db
|
3
|
+
class Connection
|
4
|
+
def self.setup(config_path = File.join(Dir.pwd, 'config', 'database.yml'))
|
5
|
+
@db ||= begin
|
6
|
+
db_config = YAML.load_file(config_path)['db']
|
7
|
+
Sequel.connect(db_config).tap do |db|
|
8
|
+
Sequel::Model.db = db
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Daidan
|
4
|
+
module Generators
|
5
|
+
class BaseGenerator
|
6
|
+
def initialize(app_name)
|
7
|
+
@app_name = app_name.strip
|
8
|
+
validate_app_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate
|
12
|
+
create_directories
|
13
|
+
create_files
|
14
|
+
puts "✅ Created new app: #{@app_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_app_name
|
20
|
+
return if @app_name.match?(/\A[a-z0-9_]+\z/)
|
21
|
+
|
22
|
+
puts "Invalid app name '#{@app_name}'. Use lowercase letters, numbers and underscores only."
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_directories
|
27
|
+
dirs = [
|
28
|
+
"./#{@app_name}/config",
|
29
|
+
"./#{@app_name}/db/migrations",
|
30
|
+
"./#{@app_name}/graphql/mutations",
|
31
|
+
"./#{@app_name}/graphql/types",
|
32
|
+
"./#{@app_name}/models"
|
33
|
+
]
|
34
|
+
dirs.each { |d| FileUtils.mkdir_p(d) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_files
|
38
|
+
create_application_rb
|
39
|
+
create_database_yml
|
40
|
+
create_config_ru
|
41
|
+
create_gemfile
|
42
|
+
create_schema_rb
|
43
|
+
create_mutation_type_rb
|
44
|
+
create_query_type_rb
|
45
|
+
create_readme
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_application_rb
|
49
|
+
content = <<~RUBY
|
50
|
+
class Application < Daidan::Application
|
51
|
+
def graphql_schema
|
52
|
+
Schema
|
53
|
+
end
|
54
|
+
end
|
55
|
+
RUBY
|
56
|
+
write_file('config/application.rb', content)
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_database_yml
|
60
|
+
content = <<~YAML
|
61
|
+
db:
|
62
|
+
adapter: sqlite
|
63
|
+
database: db.db
|
64
|
+
YAML
|
65
|
+
write_file('config/database.yml', content)
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_config_ru
|
69
|
+
content = <<~RUBY
|
70
|
+
require 'rack'
|
71
|
+
require 'rack/cors'
|
72
|
+
require 'daidan'
|
73
|
+
require_relative 'config/application'
|
74
|
+
|
75
|
+
use Rack::Reloader, 0
|
76
|
+
|
77
|
+
use Rack::Cors do
|
78
|
+
allow do
|
79
|
+
origins '*'
|
80
|
+
|
81
|
+
resource '/graphql',
|
82
|
+
headers: :any,
|
83
|
+
methods: %i[get post options],
|
84
|
+
credentials: false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
run Application.new
|
89
|
+
RUBY
|
90
|
+
write_file('config.ru', content)
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_gemfile
|
94
|
+
content = <<~RUBY
|
95
|
+
source 'https://rubygems.org'
|
96
|
+
|
97
|
+
gem 'bcrypt'
|
98
|
+
gem 'dotenv'
|
99
|
+
gem 'graphql'
|
100
|
+
gem 'jwt'
|
101
|
+
gem 'puma'
|
102
|
+
gem 'rack'
|
103
|
+
gem 'rubocop'
|
104
|
+
gem 'sequel'
|
105
|
+
gem 'sqlite3'
|
106
|
+
gem 'zeitwerk'
|
107
|
+
gem 'rack-cors'
|
108
|
+
|
109
|
+
gem 'daidan', path: '../daidan'
|
110
|
+
RUBY
|
111
|
+
write_file('Gemfile', content)
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_schema_rb
|
115
|
+
content = <<~RUBY
|
116
|
+
class Schema < GraphQL::Schema
|
117
|
+
query(QueryType)
|
118
|
+
mutation(MutationType)
|
119
|
+
end
|
120
|
+
RUBY
|
121
|
+
write_file('graphql/schema.rb', content)
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_mutation_type_rb
|
125
|
+
content = <<~RUBY
|
126
|
+
class MutationType < Daidan::BaseObjectType
|
127
|
+
description 'The mutation root of this schema'
|
128
|
+
end
|
129
|
+
RUBY
|
130
|
+
write_file('graphql/types/mutation_type.rb', content)
|
131
|
+
end
|
132
|
+
|
133
|
+
def create_query_type_rb
|
134
|
+
content = <<~RUBY
|
135
|
+
class QueryType < Daidan::BaseObjectType
|
136
|
+
description 'The query root of this schema'
|
137
|
+
end
|
138
|
+
RUBY
|
139
|
+
write_file('graphql/types/query_type.rb', content)
|
140
|
+
end
|
141
|
+
|
142
|
+
def create_readme
|
143
|
+
content = <<~MD
|
144
|
+
# Readme
|
145
|
+
|
146
|
+
Write something about your project here.
|
147
|
+
MD
|
148
|
+
write_file('readme.md', content)
|
149
|
+
end
|
150
|
+
|
151
|
+
def write_file(relative_path, content)
|
152
|
+
full_path = File.join("./#{@app_name}", relative_path)
|
153
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
154
|
+
File.write(full_path, content)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Daidan
|
4
|
+
module Generators
|
5
|
+
class ResourceGenerator
|
6
|
+
VALID_TYPES = %w[string decimal float integer].freeze
|
7
|
+
|
8
|
+
def initialize(resource, fields)
|
9
|
+
@resource = resource.downcase
|
10
|
+
@class_name = @resource.capitalize
|
11
|
+
@fields = fields
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
parse_fields(@fields)
|
16
|
+
validate_inputs
|
17
|
+
generate_migration
|
18
|
+
generate_model
|
19
|
+
generate_mutations
|
20
|
+
generate_type
|
21
|
+
inject_mutation_type
|
22
|
+
inject_query_type
|
23
|
+
puts "✅ Generated resource: #{@resource}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def parse_fields(fields)
|
29
|
+
@parsed_fields = fields.map do |f|
|
30
|
+
name, type = f.split(':', 2)
|
31
|
+
unless name && type && !name.empty? && !type.empty?
|
32
|
+
puts "Invalid field specification #{f}. Use name:type."
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
unless name.match?(/\A[a-z_]+\z/)
|
36
|
+
puts "Invalid field name '#{name}'. Use lowercase letters and underscores only."
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
unless VALID_TYPES.include?(type)
|
40
|
+
puts "Invalid field type '#{type}'. Allowed types: #{VALID_TYPES.join(', ')}."
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
[name, type]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_inputs
|
48
|
+
return if @resource.match?(/\A[a-z_]+\z/)
|
49
|
+
|
50
|
+
puts 'Invalid resource name. Use lowercase letters and underscores only.'
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def db_type_map
|
55
|
+
map = {
|
56
|
+
'string' => 'String',
|
57
|
+
'decimal' => 'Decimal',
|
58
|
+
'float' => 'Float',
|
59
|
+
'integer' => 'Integer'
|
60
|
+
}
|
61
|
+
map.default = 'String'
|
62
|
+
map
|
63
|
+
end
|
64
|
+
|
65
|
+
def graphql_type_map
|
66
|
+
map = {
|
67
|
+
'string' => 'String',
|
68
|
+
'decimal' => 'Float',
|
69
|
+
'float' => 'Float',
|
70
|
+
'integer' => 'Int'
|
71
|
+
}
|
72
|
+
map.default = 'String'
|
73
|
+
map
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_migration
|
77
|
+
migrations_dir = File.join('.', 'db', 'migrations')
|
78
|
+
FileUtils.mkdir_p(migrations_dir)
|
79
|
+
existing = Dir[File.join(migrations_dir, '*.rb')]
|
80
|
+
numbers = existing.map { |f| File.basename(f).split('_', 2).first.to_i }
|
81
|
+
next_number = numbers.empty? ? 1 : numbers.max + 1
|
82
|
+
migration_name = "create_#{@resource}s"
|
83
|
+
table_name = "#{@resource}s"
|
84
|
+
migration_file = File.join(migrations_dir, format("%03d_#{migration_name}.rb", next_number))
|
85
|
+
|
86
|
+
fields = @parsed_fields.map do |(name, type)|
|
87
|
+
db_t = db_type_map[type]
|
88
|
+
null_part = ', null: false'
|
89
|
+
size_part = type == 'decimal' ? ', size: [10, 2]' : ''
|
90
|
+
" #{db_t} :#{name}#{size_part}#{null_part}"
|
91
|
+
end.join("\n")
|
92
|
+
|
93
|
+
migration_content = <<~RUBY
|
94
|
+
Sequel.migration do
|
95
|
+
change do
|
96
|
+
create_table(:#{table_name}) do
|
97
|
+
primary_key :id
|
98
|
+
#{fields}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
RUBY
|
103
|
+
|
104
|
+
File.write(migration_file, migration_content)
|
105
|
+
end
|
106
|
+
|
107
|
+
def generate_model
|
108
|
+
models_dir = File.join('.', 'models')
|
109
|
+
FileUtils.mkdir_p(models_dir)
|
110
|
+
model_file = File.join(models_dir, "#{@resource}.rb")
|
111
|
+
|
112
|
+
model_content = <<~RUBY
|
113
|
+
class #{@class_name} < Sequel::Model
|
114
|
+
set_dataset :#{@resource}s
|
115
|
+
end
|
116
|
+
RUBY
|
117
|
+
|
118
|
+
File.write(model_file, model_content)
|
119
|
+
end
|
120
|
+
|
121
|
+
def generate_mutations
|
122
|
+
mutations_dir = File.join('.', 'graphql', 'mutations')
|
123
|
+
FileUtils.mkdir_p(mutations_dir)
|
124
|
+
|
125
|
+
create_args = @parsed_fields.map do |(name, type)|
|
126
|
+
gtype = graphql_type_map[type]
|
127
|
+
" argument :#{name}, #{gtype}, required: true"
|
128
|
+
end.join("\n")
|
129
|
+
|
130
|
+
update_args = @parsed_fields.map do |(name, type)|
|
131
|
+
gtype = graphql_type_map[type]
|
132
|
+
" argument :#{name}, #{gtype}, required: false"
|
133
|
+
end.join("\n")
|
134
|
+
|
135
|
+
create_mutation_file = File.join(mutations_dir, "create_#{@resource}.rb")
|
136
|
+
create_mutation_content = <<~RUBY
|
137
|
+
class Create#{@class_name} < Daidan::BaseMutation
|
138
|
+
#{create_args}
|
139
|
+
|
140
|
+
type #{@class_name}Type
|
141
|
+
|
142
|
+
def resolve(#{@parsed_fields.map { |f| "#{f[0]}:" }.join(', ')})
|
143
|
+
#{@class_name}.create(#{@parsed_fields.map { |(n, t)| "#{n}: #{n}" }.join(', ')})
|
144
|
+
rescue Sequel::Error => e
|
145
|
+
GraphQL::ExecutionError.new("Unable to create #{@resource}: \#{e.message}")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
RUBY
|
149
|
+
File.write(create_mutation_file, create_mutation_content)
|
150
|
+
|
151
|
+
update_mutation_file = File.join(mutations_dir, "update_#{@resource}.rb")
|
152
|
+
update_mutation_content = <<~RUBY
|
153
|
+
class Update#{@class_name} < Daidan::BaseMutation
|
154
|
+
argument :id, ID, required: true
|
155
|
+
#{update_args}
|
156
|
+
|
157
|
+
type #{@class_name}Type
|
158
|
+
|
159
|
+
def resolve(id:, **attributes)
|
160
|
+
record = #{@class_name}[id]
|
161
|
+
raise GraphQL::ExecutionError, '#{@class_name} not found' unless record
|
162
|
+
|
163
|
+
attributes.each do |k,v|
|
164
|
+
record.update(k => v) if v
|
165
|
+
end
|
166
|
+
|
167
|
+
record
|
168
|
+
rescue Sequel::Error => e
|
169
|
+
GraphQL::ExecutionError.new("Unable to update #{@resource}: \#{e.message}")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
RUBY
|
173
|
+
File.write(update_mutation_file, update_mutation_content)
|
174
|
+
|
175
|
+
delete_mutation_file = File.join(mutations_dir, "delete_#{@resource}.rb")
|
176
|
+
delete_mutation_content = <<~RUBY
|
177
|
+
class Delete#{@class_name} < Daidan::BaseMutation
|
178
|
+
argument :id, ID, required: true
|
179
|
+
|
180
|
+
type #{@class_name}Type
|
181
|
+
|
182
|
+
def resolve(id:)
|
183
|
+
record = #{@class_name}[id]
|
184
|
+
raise GraphQL::ExecutionError, '#{@class_name} not found' unless record
|
185
|
+
|
186
|
+
record.destroy
|
187
|
+
record
|
188
|
+
rescue Sequel::Error => e
|
189
|
+
GraphQL::ExecutionError.new("Unable to delete #{@resource}: \#{e.message}")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
RUBY
|
193
|
+
File.write(delete_mutation_file, delete_mutation_content)
|
194
|
+
end
|
195
|
+
|
196
|
+
def generate_type
|
197
|
+
types_dir = File.join('.', 'graphql', 'types')
|
198
|
+
FileUtils.mkdir_p(types_dir)
|
199
|
+
type_file = File.join(types_dir, "#{@resource}_type.rb")
|
200
|
+
|
201
|
+
type_fields = @parsed_fields.map do |(name, type)|
|
202
|
+
gtype = graphql_type_map[type]
|
203
|
+
" field :#{name}, #{gtype}, null: false"
|
204
|
+
end.join("\n")
|
205
|
+
|
206
|
+
type_content = <<~RUBY
|
207
|
+
class #{@class_name}Type < Daidan::BaseObjectType
|
208
|
+
#{type_fields}
|
209
|
+
end
|
210
|
+
RUBY
|
211
|
+
|
212
|
+
File.write(type_file, type_content)
|
213
|
+
end
|
214
|
+
|
215
|
+
def inject_mutation_type
|
216
|
+
mutation_type_file = File.join('.', 'graphql', 'types', 'mutation_type.rb')
|
217
|
+
return unless File.exist?(mutation_type_file)
|
218
|
+
|
219
|
+
lines = File.read(mutation_type_file).lines
|
220
|
+
i = lines.rindex { |l| l.strip == 'end' }
|
221
|
+
return unless i
|
222
|
+
|
223
|
+
insert_lines = [
|
224
|
+
" field :create_#{@resource}, mutation: Create#{@class_name}",
|
225
|
+
" field :update_#{@resource}, mutation: Update#{@class_name}",
|
226
|
+
" field :delete_#{@resource}, mutation: Delete#{@class_name}"
|
227
|
+
]
|
228
|
+
lines.insert(i, *insert_lines.map { |l| l + "\n" })
|
229
|
+
File.write(mutation_type_file, lines.join)
|
230
|
+
end
|
231
|
+
|
232
|
+
def inject_query_type
|
233
|
+
query_type_file = File.join('.', 'graphql', 'types', 'query_type.rb')
|
234
|
+
return unless File.exist?(query_type_file)
|
235
|
+
|
236
|
+
lines = File.read(query_type_file).lines
|
237
|
+
i = lines.rindex { |l| l.strip == 'end' }
|
238
|
+
return unless i
|
239
|
+
|
240
|
+
field_lines = [
|
241
|
+
" field :#{@resource}s, [#{@class_name}Type], null: false do",
|
242
|
+
" description 'Retrieve a list of #{@resource}s'",
|
243
|
+
' end'
|
244
|
+
]
|
245
|
+
lines.insert(i, *field_lines.map { |l| l + "\n" })
|
246
|
+
|
247
|
+
def_lines = [
|
248
|
+
" def #{@resource}s",
|
249
|
+
" #{@class_name}.all",
|
250
|
+
' end'
|
251
|
+
]
|
252
|
+
lines.insert(i + field_lines.size, *def_lines.map { |l| l + "\n" })
|
253
|
+
|
254
|
+
File.write(query_type_file, lines.join)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Daidan
|
2
|
+
class LoginUser < BaseMutation
|
3
|
+
argument :email, String, required: true
|
4
|
+
argument :password, String, required: true
|
5
|
+
|
6
|
+
field :token, String, null: false
|
7
|
+
field :user, BaseUserType, null: false
|
8
|
+
|
9
|
+
def resolve(email:, password:)
|
10
|
+
user = User.where(email: email).first
|
11
|
+
|
12
|
+
raise GraphQL::ExecutionError, 'Invalid email or password' unless user && user.authenticate(password)
|
13
|
+
|
14
|
+
payload = { user_id: user.id }
|
15
|
+
|
16
|
+
token = JWT.encode(payload, ENV['JWT_SECRET'], 'HS256')
|
17
|
+
{ token: token, user: user }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Daidan
|
2
|
+
module Middleware
|
3
|
+
class JwtAuthentication
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
auth_header = env['HTTP_AUTHORIZATION']
|
10
|
+
if auth_header && auth_header.start_with?('Bearer ')
|
11
|
+
token = auth_header.split(' ').last
|
12
|
+
|
13
|
+
begin
|
14
|
+
payload, = JWT.decode(token, ENV['JWT_SECRET'], true, { algorithm: 'HS256' })
|
15
|
+
env['current_user_id'] = payload['user_id']
|
16
|
+
rescue JWT::DecodeError
|
17
|
+
env['current_user_id'] = nil
|
18
|
+
end
|
19
|
+
else
|
20
|
+
env['current_user_id'] = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
@app.call(env)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Daidan
|
2
|
+
class User < Sequel::Model
|
3
|
+
include BCrypt
|
4
|
+
attr_accessor :password
|
5
|
+
|
6
|
+
set_dataset :users
|
7
|
+
|
8
|
+
plugin :validation_helpers
|
9
|
+
|
10
|
+
def before_save
|
11
|
+
super
|
12
|
+
encrypt_password if password
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate
|
16
|
+
super
|
17
|
+
validates_presence %i[email], message: 'is required'
|
18
|
+
validates_unique :email, message: 'is already taken'
|
19
|
+
validates_format(/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, :email, message: 'is not a valid email address')
|
20
|
+
errors.add(:password, 'cannot be empty') if new? && !password
|
21
|
+
errors.add(:password, 'must be at least 6 characters') if password && password.length < 6
|
22
|
+
end
|
23
|
+
|
24
|
+
def authenticate(submitted_password)
|
25
|
+
Password.new(password_digest) == submitted_password
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def encrypt_password
|
31
|
+
self.password_digest = Password.create(password)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/daidan.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
require 'dotenv/load'
|
5
|
+
require 'graphql'
|
6
|
+
require 'json'
|
7
|
+
require 'jwt'
|
8
|
+
require 'rack'
|
9
|
+
require 'sequel'
|
10
|
+
require 'yaml'
|
11
|
+
require 'zeitwerk'
|
12
|
+
|
13
|
+
require_relative 'daidan/version'
|
14
|
+
require_relative 'daidan/db/connection'
|
15
|
+
require_relative 'daidan/config/application'
|
16
|
+
|
17
|
+
Daidan::Db::Connection.setup
|
18
|
+
require_relative 'daidan/commands'
|
19
|
+
|
20
|
+
require_relative 'daidan/middleware/jwt_authentication'
|
21
|
+
require_relative 'daidan/graphql/mutations/base_mutation'
|
22
|
+
require_relative 'daidan/graphql/types/base_object_type'
|
23
|
+
require_relative 'daidan/graphql/types/base_user_type'
|
24
|
+
require_relative 'daidan/graphql/mutations/login_user'
|
25
|
+
|
26
|
+
module Daidan
|
27
|
+
autoload :User, 'daidan/models/user'
|
28
|
+
|
29
|
+
class Error < StandardError; end
|
30
|
+
end
|
data/sig/daidan.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: daidan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
original_platform: ''
|
7
|
+
authors:
|
8
|
+
- Bernard Cesarz
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bcrypt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dotenv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: graphql
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.4'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: jwt
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.9'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sequel
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '13.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '13.0'
|
97
|
+
description: A lightweight Ruby framework for building GraphQL-based web applications.
|
98
|
+
email:
|
99
|
+
- cesarzb@protonmail.com
|
100
|
+
executables:
|
101
|
+
- console
|
102
|
+
- daidan
|
103
|
+
- setup
|
104
|
+
extensions: []
|
105
|
+
extra_rdoc_files: []
|
106
|
+
files:
|
107
|
+
- ".gitignore"
|
108
|
+
- CHANGELOG.md
|
109
|
+
- Gemfile
|
110
|
+
- Gemfile.lock
|
111
|
+
- LICENSE.txt
|
112
|
+
- README.md
|
113
|
+
- Rakefile
|
114
|
+
- bin/console
|
115
|
+
- bin/daidan
|
116
|
+
- bin/setup
|
117
|
+
- daidan.gemspec
|
118
|
+
- lib/daidan.rb
|
119
|
+
- lib/daidan/commands.rb
|
120
|
+
- lib/daidan/config/application.rb
|
121
|
+
- lib/daidan/db/connection.rb
|
122
|
+
- lib/daidan/db/create_base_user_migration.rb
|
123
|
+
- lib/daidan/generators/base_generator.rb
|
124
|
+
- lib/daidan/generators/resource_generator.rb
|
125
|
+
- lib/daidan/graphql/mutations/base_mutation.rb
|
126
|
+
- lib/daidan/graphql/mutations/login_user.rb
|
127
|
+
- lib/daidan/graphql/types/base_object_type.rb
|
128
|
+
- lib/daidan/graphql/types/base_user_type.rb
|
129
|
+
- lib/daidan/middleware/jwt_authentication.rb
|
130
|
+
- lib/daidan/models/user.rb
|
131
|
+
- lib/daidan/version.rb
|
132
|
+
- sig/daidan.rbs
|
133
|
+
homepage: https://github.com/cesarzb/daidan
|
134
|
+
licenses:
|
135
|
+
- MIT
|
136
|
+
metadata:
|
137
|
+
homepage_uri: https://github.com/cesarzb/daidan
|
138
|
+
source_code_uri: https://github.com/cesarzb/daidan
|
139
|
+
changelog_uri: https://github.com/cesarzb/daidan/blob/main/CHANGELOG.md
|
140
|
+
allowed_push_host: https://rubygems.org
|
141
|
+
license_uri: https://opensource.org/licenses/MIT
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubygems_version: 3.6.1
|
157
|
+
specification_version: 4
|
158
|
+
summary: Lightweight GraphQL web framework.
|
159
|
+
test_files: []
|