dami 1.0.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/CHANGELOG.md +306 -0
- data/LICENSE +21 -0
- data/README.md +106 -0
- data/bin/dami +10 -0
- data/docs/01.Getting_Started.md +220 -0
- data/docs/02.Models_and_Fields.md +244 -0
- data/docs/03.Querying.md +387 -0
- data/docs/04.Creating_Updating_Deleting.md +226 -0
- data/docs/05.Validation.md +259 -0
- data/docs/06.Protection.md +160 -0
- data/docs/07.Associations.md +239 -0
- data/docs/08.Scopes.md +99 -0
- data/docs/09.Migrations.md +165 -0
- data/docs/10.Flows_and_Commands.md +181 -0
- data/docs/11.Localization.md +435 -0
- data/docs/12.TheDamiWay.md +227 -0
- data/docs/Manifesto.md +305 -0
- data/docs/site.md +477 -0
- data/lib/dami/actions/command.rb +80 -0
- data/lib/dami/actions/context.rb +63 -0
- data/lib/dami/actions/draft.rb +36 -0
- data/lib/dami/actions/flow.rb +42 -0
- data/lib/dami/adapters/base.rb +40 -0
- data/lib/dami/adapters/sqlite/connection.rb +122 -0
- data/lib/dami/adapters/sqlite/core.rb +17 -0
- data/lib/dami/adapters/sqlite/query.rb +353 -0
- data/lib/dami/adapters/sqlite/schema.rb +135 -0
- data/lib/dami/cli.rb +117 -0
- data/lib/dami/configuration.rb +246 -0
- data/lib/dami/core.rb +98 -0
- data/lib/dami/dsl_guardrails.rb +45 -0
- data/lib/dami/errors.rb +89 -0
- data/lib/dami/inflector.rb +245 -0
- data/lib/dami/localization.rb +131 -0
- data/lib/dami/migration.rb +84 -0
- data/lib/dami/migrator.rb +105 -0
- data/lib/dami/plugins/associations.rb +284 -0
- data/lib/dami/plugins/nested_attributes.rb +180 -0
- data/lib/dami/plugins/protection.rb +30 -0
- data/lib/dami/plugins/validations.rb +90 -0
- data/lib/dami/query/builder.rb +132 -0
- data/lib/dami/query/enumerable.rb +62 -0
- data/lib/dami/query/persistence.rb +133 -0
- data/lib/dami/record_proxy.rb +32 -0
- data/lib/dami/result.rb +27 -0
- data/lib/dami/schema/diff.rb +84 -0
- data/lib/dami/schema/dumper.rb +60 -0
- data/lib/dami/schema/generator.rb +69 -0
- data/lib/dami/schema/introspector.rb +34 -0
- data/lib/dami/schema/loader.rb +58 -0
- data/lib/dami/schema.rb +13 -0
- data/lib/dami/validation_rules.rb +72 -0
- data/lib/dami/version.rb +3 -0
- data/lib/dami.rb +32 -0
- metadata +218 -0
data/lib/dami/cli.rb
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# File: ./lib/dami/cli.rb
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
require 'thor'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require_relative '../dami' # Load the main Dami library
|
|
7
|
+
|
|
8
|
+
module Dami
|
|
9
|
+
# 1. Define the parent namespace first to prevent loading errors.
|
|
10
|
+
class CLI < Thor; end
|
|
11
|
+
|
|
12
|
+
# 2. Define a base class for our subcommands to share common helper methods.
|
|
13
|
+
class CLI::Base < Thor
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# This helper loads the user's application environment. It looks for a
|
|
17
|
+
# standard initializer and then loads all model files from the configured path.
|
|
18
|
+
def load_user_app
|
|
19
|
+
initializer_path = File.expand_path("config/initializers/dami.rb", Dir.pwd)
|
|
20
|
+
require initializer_path if File.exist?(initializer_path)
|
|
21
|
+
|
|
22
|
+
model_paths = Array(Dami.configuration.models_path)
|
|
23
|
+
model_paths.each do |path|
|
|
24
|
+
search_path = File.expand_path(path)
|
|
25
|
+
Dir.glob(File.join(search_path, '**', '*.rb')).each { |file| load file }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 3. Define the subcommand classes.
|
|
31
|
+
|
|
32
|
+
# Handles all `dami db:*` subcommands.
|
|
33
|
+
class DB < CLI::Base
|
|
34
|
+
desc "migrate", "Run all pending database migrations"
|
|
35
|
+
def migrate
|
|
36
|
+
load_user_app
|
|
37
|
+
puts "Running migrations..."
|
|
38
|
+
|
|
39
|
+
db = Dami.database(:default) # Assumes the app has already connected.
|
|
40
|
+
migrator = Dami::Migrator.new(db)
|
|
41
|
+
migrator.migrate
|
|
42
|
+
|
|
43
|
+
puts "✅ Migrations complete. Schema has been updated in #{Dami.configuration.schema_path}."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "rollback [STEPS]", "Revert the last migration (or the last STEPS migrations)"
|
|
47
|
+
def rollback(steps = 1)
|
|
48
|
+
load_user_app
|
|
49
|
+
db = Dami.database(:default)
|
|
50
|
+
Dami::Migrator.new(db).rollback(steps.to_i)
|
|
51
|
+
puts "✅ Rolled back #{steps} migration(s). Schema has been updated in #{Dami.configuration.schema_path}."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "schema_dump", "Generate a schema file from the current database state"
|
|
55
|
+
def schema_dump
|
|
56
|
+
load_user_app
|
|
57
|
+
puts "Dumping schema..."
|
|
58
|
+
|
|
59
|
+
db = Dami.database(:default)
|
|
60
|
+
dumper = Dami::Schema::Dumper.new(db)
|
|
61
|
+
schema_content = dumper.dump
|
|
62
|
+
|
|
63
|
+
schema_path = Dami.configuration.schema_path
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(schema_path))
|
|
65
|
+
File.write(schema_path, schema_content)
|
|
66
|
+
puts "✅ Schema dumped to #{schema_path}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Handles all `dami generate *` subcommands.
|
|
71
|
+
class Generate < CLI::Base
|
|
72
|
+
desc "migration NAME", "Generate a new migration by comparing models to the schema"
|
|
73
|
+
def migration(name)
|
|
74
|
+
load_user_app
|
|
75
|
+
|
|
76
|
+
loader = Dami::Schema::Loader.new
|
|
77
|
+
loader.load_from_path(Dami.configuration.schema_path)
|
|
78
|
+
|
|
79
|
+
introspector = Dami::Schema::Introspector.new
|
|
80
|
+
|
|
81
|
+
differ = Dami::Schema::Diff.new(loader.schema, introspector.introspect)
|
|
82
|
+
diff_result = differ.diff
|
|
83
|
+
|
|
84
|
+
if diff_result[:up].empty?
|
|
85
|
+
puts "✅ Schema is up to date. No migration generated."
|
|
86
|
+
else
|
|
87
|
+
generator = Dami::Schema::Generator.new
|
|
88
|
+
path = generator.generate(diff_result, name)
|
|
89
|
+
puts "✅ New migration created: #{path}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 4. Finally, reopen the main CLI class to add the subcommands.
|
|
95
|
+
# This works because `DB` and `Generate` are now fully defined.
|
|
96
|
+
class CLI < Thor
|
|
97
|
+
desc "db", "Manage database tasks (migrate, rollback, schema_dump)"
|
|
98
|
+
subcommand "db", DB
|
|
99
|
+
|
|
100
|
+
desc "generate", "Generate new files (e.g., migrations)"
|
|
101
|
+
subcommand "generate", Generate
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# This modification to the Configuration class is still needed.
|
|
106
|
+
# It ensures the CLI has a place to get database connection details from.
|
|
107
|
+
class Dami::Configuration
|
|
108
|
+
attr_accessor :database_config
|
|
109
|
+
|
|
110
|
+
# We redefine initialize to add the new attribute with a default.
|
|
111
|
+
def initialize
|
|
112
|
+
@models_path = 'app/models'
|
|
113
|
+
@migrations_path = 'db/migrations'
|
|
114
|
+
@schema_path = 'db/schema.rb'
|
|
115
|
+
@database_config = {}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
module Dami
|
|
2
|
+
def self.model(name, &block)
|
|
3
|
+
config = ModelConfig.new(name)
|
|
4
|
+
config.instance_eval(&block)
|
|
5
|
+
(@models ||= {})[name] = config.to_h.merge(name: name).freeze
|
|
6
|
+
end
|
|
7
|
+
def self.behavior(model_name, &block)
|
|
8
|
+
model_config = find_model(model_name)
|
|
9
|
+
BehaviorProxy.new(model_config).instance_eval(&block)
|
|
10
|
+
end
|
|
11
|
+
def self.scopes(model_name, &block)
|
|
12
|
+
model_config = find_model(model_name)
|
|
13
|
+
ScopesProxy.new(model_config.fetch(:scopes, {})).instance_eval(&block)
|
|
14
|
+
end
|
|
15
|
+
class Configuration
|
|
16
|
+
attr_accessor :models_path, :migrations_path, :schema_path, :database_config
|
|
17
|
+
def initialize
|
|
18
|
+
@models_path = 'app/models'
|
|
19
|
+
@migrations_path = 'db/migrations'
|
|
20
|
+
@schema_path = 'db/schema.rb'
|
|
21
|
+
@database_config = {}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
def self.configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
def self.configure
|
|
28
|
+
yield(configuration)
|
|
29
|
+
end
|
|
30
|
+
class ModelConfig
|
|
31
|
+
def initialize(name)
|
|
32
|
+
@config = {
|
|
33
|
+
database: :default,
|
|
34
|
+
fields: {},
|
|
35
|
+
virtual_fields: {},
|
|
36
|
+
protection: {},
|
|
37
|
+
relationships: {},
|
|
38
|
+
validations: {},
|
|
39
|
+
scopes: {},
|
|
40
|
+
nests: {}
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
def database(name)
|
|
44
|
+
@config[:database] = name
|
|
45
|
+
end
|
|
46
|
+
def fields(&block)
|
|
47
|
+
FieldsProxy.new(@config[:fields]).instance_eval(&block)
|
|
48
|
+
end
|
|
49
|
+
def virtual(&block)
|
|
50
|
+
FieldsProxy.new(@config[:virtual_fields]).instance_eval(&block)
|
|
51
|
+
end
|
|
52
|
+
def relationships(&block)
|
|
53
|
+
RelationshipProxy.new(@config[:relationships]).instance_eval(&block)
|
|
54
|
+
end
|
|
55
|
+
def nests(*names, **options)
|
|
56
|
+
proxy = NestsProxy.new(@config[:nests])
|
|
57
|
+
names.each { |name| proxy.nest(name, **options) }
|
|
58
|
+
end
|
|
59
|
+
def validate(*args, &block)
|
|
60
|
+
raise Dami::InvalidDSLError.new('validate', 'behavior')
|
|
61
|
+
end
|
|
62
|
+
def protection(*args, &block)
|
|
63
|
+
raise Dami::InvalidDSLError.new('protection', 'behavior')
|
|
64
|
+
end
|
|
65
|
+
def scope(*args, &block)
|
|
66
|
+
raise Dami::InvalidDSLError.new('scope', 'scopes')
|
|
67
|
+
end
|
|
68
|
+
def to_h
|
|
69
|
+
@config
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
class FieldsProxy
|
|
73
|
+
def initialize(target)
|
|
74
|
+
@target = target
|
|
75
|
+
end
|
|
76
|
+
def field(name, type, **options)
|
|
77
|
+
@target[name] = { type: type, **options }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
class RelationshipProxy
|
|
81
|
+
def initialize(target)
|
|
82
|
+
@target = target
|
|
83
|
+
end
|
|
84
|
+
def belongs_to(*names, **options)
|
|
85
|
+
(@target[:belongs_to] ||= {}).merge!(parse_relations(names, options))
|
|
86
|
+
end
|
|
87
|
+
def has_one(*names, **options)
|
|
88
|
+
(@target[:has_one] ||= {}).merge!(parse_relations(names, options))
|
|
89
|
+
end
|
|
90
|
+
def has_many(*names, **options)
|
|
91
|
+
(@target[:has_many] ||= {}).merge!(parse_relations(names, options))
|
|
92
|
+
end
|
|
93
|
+
private
|
|
94
|
+
def parse_relations(names, options)
|
|
95
|
+
relations = {}
|
|
96
|
+
options.each { |key, value| relations[key] = value if value.is_a?(Hash) }
|
|
97
|
+
non_rel_opts = options.reject { |_, v| v.is_a?(Hash) }
|
|
98
|
+
names.each { |name| relations[name] = non_rel_opts }
|
|
99
|
+
relations
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
class BehaviorProxy
|
|
103
|
+
def initialize(model_config)
|
|
104
|
+
@model_config = model_config
|
|
105
|
+
@model_config[:validations] ||= {}
|
|
106
|
+
@model_config[:protection] ||= {}
|
|
107
|
+
@model_config[:protection][:protect] ||= []
|
|
108
|
+
@model_config[:protection][:permit] ||= []
|
|
109
|
+
end
|
|
110
|
+
def validate(&block)
|
|
111
|
+
validator = ValidationBuilder.new(@model_config[:validations], model_config: @model_config)
|
|
112
|
+
validator.instance_eval(&block)
|
|
113
|
+
end
|
|
114
|
+
def protection(&block)
|
|
115
|
+
protector = ProtectionBuilder.new(@model_config[:protection])
|
|
116
|
+
protector.instance_eval(&block)
|
|
117
|
+
end
|
|
118
|
+
def on(operation, &block)
|
|
119
|
+
validator = ValidationBuilder.new(@model_config[:validations], context: operation, model_config: @model_config)
|
|
120
|
+
validator.instance_eval(&block)
|
|
121
|
+
end
|
|
122
|
+
def fields(*args, &block)
|
|
123
|
+
raise Dami::InvalidDSLError.new('fields', 'model')
|
|
124
|
+
end
|
|
125
|
+
def virtual(*args, &block)
|
|
126
|
+
raise Dami::InvalidDSLError.new('virtual', 'model')
|
|
127
|
+
end
|
|
128
|
+
def relationships(*args, &block)
|
|
129
|
+
raise Dami::InvalidDSLError.new('relationships', 'model')
|
|
130
|
+
end
|
|
131
|
+
def nests(*args, &block)
|
|
132
|
+
raise Dami::InvalidDSLError.new('nests', 'model')
|
|
133
|
+
end
|
|
134
|
+
def scope(*args, &block)
|
|
135
|
+
raise Dami::InvalidDSLError.new('scope', 'scopes')
|
|
136
|
+
end
|
|
137
|
+
class ValidationBuilder
|
|
138
|
+
def initialize(config, context: nil, model_config: nil)
|
|
139
|
+
@config = config
|
|
140
|
+
@context = context
|
|
141
|
+
@model_config = model_config
|
|
142
|
+
end
|
|
143
|
+
def rule(field, *rules, **options, &block)
|
|
144
|
+
field = field.to_sym
|
|
145
|
+
@config[field] ||= []
|
|
146
|
+
if_cond = options.delete(:if)
|
|
147
|
+
unless_cond = options.delete(:unless)
|
|
148
|
+
when_cond = options.delete(:when)
|
|
149
|
+
rules.concat(options.map { |k, v| v == true ? k : [k, v] })
|
|
150
|
+
rules.each do |rule_def|
|
|
151
|
+
rule_entry = { rule: rule_def, on: @context ? [@context] : [:create, :update] }
|
|
152
|
+
rule_entry[:if] = if_cond if if_cond
|
|
153
|
+
rule_entry[:unless] = unless_cond if unless_cond
|
|
154
|
+
rule_entry[:when] = when_cond if when_cond
|
|
155
|
+
@config[field] << rule_entry
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
def all(only: nil, except: nil, **kwargs, &block)
|
|
159
|
+
all_fields = []
|
|
160
|
+
if @model_config
|
|
161
|
+
all_fields.concat(@model_config[:fields].keys) if @model_config[:fields]
|
|
162
|
+
all_fields.concat(@model_config[:virtual_fields].keys) if @model_config[:virtual_fields]
|
|
163
|
+
end
|
|
164
|
+
fields_to_validate = if only
|
|
165
|
+
Array(only).map(&:to_sym) & all_fields
|
|
166
|
+
elsif except
|
|
167
|
+
all_fields - Array(except).map(&:to_sym)
|
|
168
|
+
else
|
|
169
|
+
all_fields
|
|
170
|
+
end
|
|
171
|
+
if kwargs.any?
|
|
172
|
+
fields_to_validate.each do |field|
|
|
173
|
+
rule(field, **kwargs)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
if block_given?
|
|
177
|
+
temp_builder = TempRuleBuilder.new(@context)
|
|
178
|
+
temp_builder.instance_eval(&block)
|
|
179
|
+
fields_to_validate.each do |field|
|
|
180
|
+
temp_builder.rules.each do |rule_def|
|
|
181
|
+
rule(field, *rule_def[:args], **rule_def[:kwargs])
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
def on(operation, &block)
|
|
187
|
+
nested = ValidationBuilder.new(@config, context: operation, model_config: @model_config)
|
|
188
|
+
nested.instance_eval(&block)
|
|
189
|
+
end
|
|
190
|
+
class TempRuleBuilder
|
|
191
|
+
attr_reader :rules
|
|
192
|
+
def initialize(context)
|
|
193
|
+
@context = context
|
|
194
|
+
@rules = []
|
|
195
|
+
end
|
|
196
|
+
def rule(*args, **kwargs)
|
|
197
|
+
@rules << { args: args, kwargs: kwargs }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
class ProtectionBuilder
|
|
202
|
+
def initialize(config)
|
|
203
|
+
@config = config
|
|
204
|
+
@config[:protect] ||= []
|
|
205
|
+
@config[:permit] ||= []
|
|
206
|
+
end
|
|
207
|
+
def protect(*fields)
|
|
208
|
+
@config[:protect].concat(fields.map(&:to_sym))
|
|
209
|
+
@config[:protect].uniq!
|
|
210
|
+
end
|
|
211
|
+
def permit(*fields)
|
|
212
|
+
@config[:permit].concat(fields.map(&:to_sym))
|
|
213
|
+
@config[:permit].uniq!
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
class ScopesProxy
|
|
218
|
+
def initialize(target)
|
|
219
|
+
@target = target
|
|
220
|
+
end
|
|
221
|
+
def scope(name, body)
|
|
222
|
+
@target[name] = body
|
|
223
|
+
end
|
|
224
|
+
def fields(*args, &block)
|
|
225
|
+
raise Dami::InvalidDSLError.new('fields', 'model')
|
|
226
|
+
end
|
|
227
|
+
def validate(*args, &block)
|
|
228
|
+
raise Dami::InvalidDSLError.new('validate', 'behavior')
|
|
229
|
+
end
|
|
230
|
+
def protection(*args, &block)
|
|
231
|
+
raise Dami::InvalidDSLError.new('protection', 'behavior')
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
class NestsProxy
|
|
235
|
+
def initialize(target)
|
|
236
|
+
@target = target
|
|
237
|
+
end
|
|
238
|
+
def nest(association_name, **options)
|
|
239
|
+
attributes_key = "#{association_name}_attributes".to_sym
|
|
240
|
+
@target[attributes_key] = {
|
|
241
|
+
association_name: association_name,
|
|
242
|
+
allow_destroy: options.fetch(:allow_destroy, false)
|
|
243
|
+
}.freeze
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
data/lib/dami/core.rb
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# File: lib/dami/core.rb
|
|
2
|
+
|
|
3
|
+
require_relative 'adapters/sqlite/connection'
|
|
4
|
+
|
|
5
|
+
module Dami
|
|
6
|
+
@databases = {}
|
|
7
|
+
@models = {}
|
|
8
|
+
@flows = {}
|
|
9
|
+
@presenter_modules = {}
|
|
10
|
+
|
|
11
|
+
def self.plugin(mod)
|
|
12
|
+
mod.apply(self) if mod.respond_to?(:apply)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.clear_all!
|
|
16
|
+
@databases.clear
|
|
17
|
+
@models.clear
|
|
18
|
+
@flows.clear
|
|
19
|
+
@presenter_modules&.clear
|
|
20
|
+
(@translations || {}).clear
|
|
21
|
+
Plugins::Associations.clear_cache!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def self.present(model_name, &block)
|
|
26
|
+
presenter_module = Module.new
|
|
27
|
+
presenter_module.module_eval(&block) # Changed from instance_eval
|
|
28
|
+
(@presenter_modules ||= {})[model_name] = presenter_module
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.find_presenter_module(model_name)
|
|
32
|
+
(@presenter_modules || {})[model_name]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# RecordProxy#initialize (extended by the Associations plugin) already applies
|
|
36
|
+
# the presenter, the association accessors and the field accessors.
|
|
37
|
+
def self.wrap_record(model_name, record_hash)
|
|
38
|
+
return nil unless record_hash
|
|
39
|
+
::Dami::RecordProxy.new(model_name, record_hash)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.connect(name = :default, adapter:, **config)
|
|
43
|
+
adapter_class = Dami::Adapters.const_get(adapter.to_s.capitalize)
|
|
44
|
+
db_instance = adapter_class.new(config)
|
|
45
|
+
db_instance.connect
|
|
46
|
+
@databases[name] = db_instance
|
|
47
|
+
db_instance
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.database(name = :default)
|
|
51
|
+
@databases.fetch(name) { raise "Database :#{name} not connected" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.find_model(name)
|
|
55
|
+
@models.fetch(name) { raise "Model :#{name} not defined" }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.db(model_name)
|
|
59
|
+
model_config = find_model(model_name)
|
|
60
|
+
db_name = model_config[:database] || :default
|
|
61
|
+
builder_class = create_builder_with_scopes(model_name, model_config[:scopes] || {})
|
|
62
|
+
builder_class.new(model_name, db_name: db_name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Runs a flow inside one transaction.
|
|
66
|
+
# * A Success halt commits.
|
|
67
|
+
# * A Failure halt ROLLS BACK every write the flow made (raising Rollback
|
|
68
|
+
# out of the transaction block is what triggers it).
|
|
69
|
+
# * An unhandled exception rolls back and re-raises.
|
|
70
|
+
# after-commit hooks run only once the transaction has actually committed.
|
|
71
|
+
def self.run(flow_name, **params)
|
|
72
|
+
flow = find_flow(flow_name)
|
|
73
|
+
context = FlowContext.new(params)
|
|
74
|
+
result = nil
|
|
75
|
+
begin
|
|
76
|
+
Dami.database.transaction do
|
|
77
|
+
result = catch(:halt) { flow.call(context) }
|
|
78
|
+
raise Rollback if result.is_a?(Failure)
|
|
79
|
+
end
|
|
80
|
+
rescue Rollback
|
|
81
|
+
# Intentional: the transaction was rolled back; `result` is the Failure.
|
|
82
|
+
end
|
|
83
|
+
context.run_after_commit_hooks! if result.is_a?(Success)
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.create_builder_with_scopes(model_name, scopes)
|
|
88
|
+
return Dami::Query::Builder if scopes.empty?
|
|
89
|
+
Class.new(Dami::Query::Builder) do
|
|
90
|
+
scopes.each do |name, body|
|
|
91
|
+
define_method(name) do |*args|
|
|
92
|
+
instance_exec(*args, &body)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
private_class_method :create_builder_with_scopes
|
|
98
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# File: lib/dami/dsl_guardrails.rb
|
|
2
|
+
|
|
3
|
+
module Dami
|
|
4
|
+
module DSLGuardrails
|
|
5
|
+
# Methods that belong in Dami.behavior
|
|
6
|
+
def validate(*args, &block)
|
|
7
|
+
raise InvalidDSLError.new('validate', 'behavior')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def protection(*args, &block)
|
|
11
|
+
raise InvalidDSLError.new('protection', 'behavior')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def on(*args, &block)
|
|
15
|
+
raise InvalidDSLError.new('on', 'behavior')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Methods that belong in Dami.model
|
|
19
|
+
def fields(*args, &block)
|
|
20
|
+
raise InvalidDSLError.new('fields', 'model')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def virtual(*args, &block)
|
|
24
|
+
raise InvalidDSLError.new('virtual', 'model')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def relationships(*args, &block)
|
|
28
|
+
raise InvalidDSLError.new('relationships', 'model')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def nests(*args, &block)
|
|
32
|
+
raise InvalidDSLError.new('nests', 'model')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Methods that belong in Dami.scopes
|
|
36
|
+
def scope(*args, &block)
|
|
37
|
+
raise InvalidDSLError.new('scope', 'scopes')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Methods that belong in Dami.present
|
|
41
|
+
def helper(*args, &block)
|
|
42
|
+
raise InvalidDSLError.new('helper', 'present')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/dami/errors.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# File: lib/dami/errors.rb
|
|
2
|
+
|
|
3
|
+
module Dami
|
|
4
|
+
# Base error class
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Validation errors
|
|
8
|
+
class ValidationError < Error
|
|
9
|
+
attr_reader :errors
|
|
10
|
+
|
|
11
|
+
def initialize(message = "Validation failed", errors = {})
|
|
12
|
+
@errors = errors
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Protection errors - backward compatible
|
|
18
|
+
class ProtectionError < Error
|
|
19
|
+
attr_reader :fields
|
|
20
|
+
|
|
21
|
+
def initialize(message, fields = nil)
|
|
22
|
+
@fields = fields
|
|
23
|
+
super(message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class UnknownFieldsError < Error
|
|
28
|
+
attr_reader :fields
|
|
29
|
+
|
|
30
|
+
def initialize(message, fields = nil)
|
|
31
|
+
@fields = fields
|
|
32
|
+
super(message)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Database errors
|
|
37
|
+
class ForeignKeyViolation < Error
|
|
38
|
+
attr_reader :table, :column, :value
|
|
39
|
+
|
|
40
|
+
def initialize(message, table: nil, column: nil, value: nil)
|
|
41
|
+
@table = table
|
|
42
|
+
@column = column
|
|
43
|
+
@value = value
|
|
44
|
+
super("Foreign key violation: #{message}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class UniqueConstraintViolation < Error
|
|
49
|
+
attr_reader :table, :column, :value
|
|
50
|
+
|
|
51
|
+
def initialize(message, table: nil, column: nil, value: nil)
|
|
52
|
+
@table = table
|
|
53
|
+
@column = column
|
|
54
|
+
@value = value
|
|
55
|
+
super("Unique constraint violation: #{message}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class NotNullViolation < Error
|
|
60
|
+
attr_reader :table, :column
|
|
61
|
+
|
|
62
|
+
def initialize(message, table: nil, column: nil)
|
|
63
|
+
@table = table
|
|
64
|
+
@column = column
|
|
65
|
+
super("Not-null constraint violation: #{message}")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# DSL Architecture errors
|
|
70
|
+
class InvalidDSLError < Error
|
|
71
|
+
def initialize(method_name, correct_location)
|
|
72
|
+
super(
|
|
73
|
+
"'#{method_name}' is not allowed here. " \
|
|
74
|
+
"Please define it in a Dami.#{correct_location} block."
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Command errors
|
|
80
|
+
class InvalidCommand < Error; end
|
|
81
|
+
|
|
82
|
+
# Raised internally by Dami.run to abort the surrounding transaction when a
|
|
83
|
+
# flow halts with a Failure. Never escapes Dami.run.
|
|
84
|
+
class Rollback < Error; end
|
|
85
|
+
|
|
86
|
+
# Raised when a caller passes something that is not a plain SQL identifier
|
|
87
|
+
# (letters, digits, underscore, dot) where a column or table name is expected.
|
|
88
|
+
class InvalidIdentifier < Error; end
|
|
89
|
+
end
|