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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.gem
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - YYYY-MM-DD
4
+
5
+ - Initial release with core features:
6
+ - GraphQL schema support
7
+ - Resource generator for models, migrations, and types
8
+ - Middleware for JWT authentication
9
+ - Database connection handling
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'bcrypt'
8
+ gem 'dotenv'
9
+ gem 'graphql'
10
+ gem 'jwt'
11
+ gem 'rake'
12
+ gem 'sequel'
13
+ gem 'zeitwerk'
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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'daidan'
6
+
7
+ require 'irb'
8
+ IRB.start(__FILE__)
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
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,10 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:users) do
4
+ primary_key :id
5
+ String :name, null: false
6
+ String :email, null: false, unique: true
7
+ String :password_digest, null: false
8
+ end
9
+ end
10
+ 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,4 @@
1
+ module Daidan
2
+ class BaseMutation < GraphQL::Schema::Mutation
3
+ end
4
+ 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,5 @@
1
+ module Daidan
2
+ class BaseObjectType < GraphQL::Schema::Object
3
+ field :id, ID, null: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Daidan
2
+ class BaseUserType < Daidan::BaseObjectType
3
+ field :email, String, null: false
4
+ end
5
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daidan
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,3 @@
1
+ module Daidan
2
+ VERSION: String
3
+ end
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: []