Class: Pakyow::Data::Sources::Relational

Inherits:
Base
  • Object
show all
Extended by:
Support::ClassState, Support::Makeable
Defined in:
lib/pakyow/data/sources/relational.rb,
lib/pakyow/data/sources/relational/command.rb,
lib/pakyow/data/sources/relational/migrator.rb,
lib/pakyow/data/sources/relational/association.rb,
lib/pakyow/data/sources/relational/associations/has_one.rb,
lib/pakyow/data/sources/relational/associations/through.rb,
lib/pakyow/data/sources/relational/associations/has_many.rb,
lib/pakyow/data/sources/relational/associations/belongs_to.rb

Overview

A relational data source through which you interact with a persistence layer such as a sql database, redis, or http. Defines the schema, queries, and other adapter-specific metadata (e.g. sql table).

Each adapter provides its own interface for interacting with the underlying persistence layer. For example, the sql adapter exposes Sequel::Dataset provided by the fantastic Sequel gem.

In normal use, the underlying dataset is inaccessible from outside of the source. Instead, access to the dataset occurs through queries defined on the source that interact with the dataset and return a result.

Results are always returned as a new source instance (or when used from the app, a Proxy object). Access to the underlying value is provided through methods such as one, to_a, and each. (@see Pakyow::Data::Container#wrap_defined_queries!)

Mutations occur through commands. Commands do not implement validation other than checking for required attributes and checking that the given attributes are defined on the source. Use the input verifier pattern to verify and validate input before passing it to a command (@see Pakyow::Verifier).

Examples:

source :posts, adapter: :sql, connection: :default do
  table :posts

  primary_id
  timestamps

  attribute :title, :string

  command :create do |params|
    insert(params)
  end

  def by_id(id)
    where(id: id)
  end
end

data.posts.create(title: "foo")
data.posts.by_id(1).first
=> #<Pakyow::Data::Object @values={:id => 1, :title => "foo", :created_at => "2018-11-30 10:55:05 -0800", :updated_at => "2018-11-30 10:55:05 -0800"}>

Defined Under Namespace

Modules: Associations Classes: Association, Command, Migrator

Constant Summary collapse

IVARS_TO_RELOAD =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

%i(
  @results @result
)
MODIFIER_METHODS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

%i(as including limit order).freeze
NESTED_METHODS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

%i(including).freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes inherited from Base

#original_results

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

instance, plural_name, #pp, #qualifications, singular_name, #source_from_self

Constructor Details

#initializeRelational

Returns a new instance of Relational.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/pakyow/data/sources/relational.rb', line 69

def initialize(*)
  super

  @wrap_as = self.class.singular_name
  @included = []

  if default_query = self.class.__default_query
    result = if default_query.is_a?(Proc)
      instance_exec(&default_query)
    else
      public_send(self.class.__default_query)
    end

    result = case result
    when self.class
      result.__getobj__
    else
      result
    end

    __setobj__(result)
  end
end

Class Attribute Details

.adapterObject (readonly)

Returns the value of attribute adapter.



430
431
432
# File 'lib/pakyow/data/sources/relational.rb', line 430

def adapter
  @adapter
end

.connectionObject (readonly)

Returns the value of attribute connection.



430
431
432
# File 'lib/pakyow/data/sources/relational.rb', line 430

def connection
  @connection
end

.nameObject (readonly)

Returns the value of attribute name.



430
431
432
# File 'lib/pakyow/data/sources/relational.rb', line 430

def name
  @name
end

Instance Attribute Details

#includedObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



67
68
69
# File 'lib/pakyow/data/sources/relational.rb', line 67

def included
  @included
end

Class Method Details

.association_with_name?(name) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


582
583
584
585
586
# File 'lib/pakyow/data/sources/relational.rb', line 582

def association_with_name?(name)
  associations.values.flatten.find { |association|
    association.name == name
  }
end

.attribute(name, type = :string, **options) ⇒ Object



486
487
488
489
490
491
# File 'lib/pakyow/data/sources/relational.rb', line 486

def attribute(name, type = :string, **options)
  attributes[name.to_sym] = {
    type: type,
    options: options
  }
end

.belongs_to(association_name, query: nil, source: association_name) ⇒ Object



501
502
503
504
505
506
507
# File 'lib/pakyow/data/sources/relational.rb', line 501

def belongs_to(association_name, query: nil, source: association_name)
  Associations::BelongsTo.new(
    name: association_name, query: query, source: self, associated_source_name: source
  ).tap do |association|
    @associations[:belongs_to] << association
  end
end

.command(command_name, provides_dataset: true, creates: false, updates: false, deletes: false, &block) ⇒ Object



432
433
434
435
436
437
438
439
440
# File 'lib/pakyow/data/sources/relational.rb', line 432

def command(command_name, provides_dataset: true, creates: false, updates: false, deletes: false, &block)
  @commands[command_name] = {
    block: block,
    provides_dataset: provides_dataset,
    creates: creates,
    updates: updates,
    deletes: deletes
  }
end

.default_primary_key_typeObject



482
483
484
# File 'lib/pakyow/data/sources/relational.rb', line 482

def default_primary_key_type
  :integer
end

.find_association_to_source(source) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



575
576
577
578
579
# File 'lib/pakyow/data/sources/relational.rb', line 575

def find_association_to_source(source)
  associations.values.flatten.find { |association|
    association.associated_source == source.class
  }
end

.has_many(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise) ⇒ Object

rubocop:disable Naming/PredicateName



510
511
512
513
514
515
516
517
518
519
520
# File 'lib/pakyow/data/sources/relational.rb', line 510

def has_many(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise)
  Associations::HasMany.new(
    name: association_name, query: query, source: self, associated_source_name: source, as: as, dependent: dependent
  ).tap do |association|
    @associations[:has_many] << association

    if through
      setup_as_through(association, through: through)
    end
  end
end

.has_one(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise) ⇒ Object

rubocop:disable Naming/PredicateName



524
525
526
527
528
529
530
531
532
533
534
# File 'lib/pakyow/data/sources/relational.rb', line 524

def has_one(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise)
  Associations::HasOne.new(
    name: association_name, query: query, source: self, associated_source_name: source, as: as, dependent: dependent
  ).tap do |association|
    @associations[:has_one] << association

    if through
      setup_as_through(association, through: through)
    end
  end
end

.make(name, adapter: Pakyow.config.data.default_adapter, connection: Pakyow.config.data.default_connection, state: nil, parent: nil, primary_id: true, timestamps: true, **kwargs, &block) ⇒ Object



545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/pakyow/data/sources/relational.rb', line 545

def make(name, adapter: Pakyow.config.data.default_adapter, connection: Pakyow.config.data.default_connection, state: nil, parent: nil, primary_id: true, timestamps: true, **kwargs, &block)
  super(name, state: state, parent: parent, adapter: adapter, connection: connection, attributes: {}, **kwargs) do
    adapter_class = Connection.adapter(adapter)

    if adapter_class.const_defined?("SourceExtension")
      # Extend the source with any adapter-specific behavior.
      #
      extension_module = adapter_class.const_get("SourceExtension")
      unless ancestors.include?(extension_module)
        include(extension_module)
      end

      # Define default fields
      #
      self.primary_id if primary_id
      self.timestamps if timestamps
    end

    # Call the original block.
    #
    class_eval(&block) if block_given?
  end
end

.primary_idObject



460
461
462
463
# File 'lib/pakyow/data/sources/relational.rb', line 460

def primary_id
  primary_key :id
  attribute :id, default_primary_key_type
end

.primary_key(field) ⇒ Object



465
466
467
# File 'lib/pakyow/data/sources/relational.rb', line 465

def primary_key(field)
  @primary_key_field = field
end

.primary_key_attributeObject



478
479
480
# File 'lib/pakyow/data/sources/relational.rb', line 478

def primary_key_attribute
  attributes[@primary_key_field]
end

.primary_key_typeObject



469
470
471
472
473
474
475
476
# File 'lib/pakyow/data/sources/relational.rb', line 469

def primary_key_type
  case primary_key_attribute
  when Hash
    primary_key_attribute[:type]
  else
    primary_key_attribute.meta[:mapping]
  end
end

.qualifications(query_name) ⇒ Object



497
498
499
# File 'lib/pakyow/data/sources/relational.rb', line 497

def qualifications(query_name)
  @qualifications.dig(query_name) || {}
end

.queriesObject



442
443
444
# File 'lib/pakyow/data/sources/relational.rb', line 442

def queries
  instance_methods - superclass.instance_methods
end

.query(query_name = nil, &block) ⇒ Object



446
447
448
# File 'lib/pakyow/data/sources/relational.rb', line 446

def query(query_name = nil, &block)
  @__default_query = query_name || block
end

.setup_as_through(association, through:) ⇒ Object

rubocop:enable Naming/PredicateName



537
538
539
540
541
542
543
# File 'lib/pakyow/data/sources/relational.rb', line 537

def setup_as_through(association, through:)
  Associations::Through.new(association, joining_source_name: through).tap do |through_association|
    associations[association.specific_type][
      associations[association.specific_type].index(association)
    ] = through_association
  end
end

.source_from_sourceObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



570
571
572
# File 'lib/pakyow/data/sources/relational.rb', line 570

def source_from_source(*)
  super.tap(&:reload)
end

.subscribe(query_name, qualifications) ⇒ Object



493
494
495
# File 'lib/pakyow/data/sources/relational.rb', line 493

def subscribe(query_name, qualifications)
  @qualifications[query_name] = qualifications
end

.timestamps(create: :created_at, update: :updated_at) ⇒ Object



450
451
452
453
454
455
456
457
458
# File 'lib/pakyow/data/sources/relational.rb', line 450

def timestamps(create: :created_at, update: :updated_at)
  @timestamp_fields = {
    create: create,
    update: update
  }

  attribute create, :datetime
  attribute update, :datetime
end

Instance Method Details

#as(object) ⇒ Object



117
118
119
120
121
# File 'lib/pakyow/data/sources/relational.rb', line 117

def as(object)
  tap do
    @wrap_as = object
  end
end

#block_for_nested_source?(maybe_nested_name) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


256
257
258
# File 'lib/pakyow/data/sources/relational.rb', line 256

def block_for_nested_source?(maybe_nested_name)
  NESTED_METHODS.include?(maybe_nested_name)
end

#command(command_name) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/pakyow/data/sources/relational.rb', line 184

def command(command_name)
  if command = self.class.commands[command_name]
    Command.new(
      command_name,
      block: command[:block],
      source: self,
      provides_dataset: command[:provides_dataset],
      creates: command[:creates],
      updates: command[:updates],
      deletes: command[:deletes]
    )
  else
    raise(
      UnknownCommand.new_with_message(command: command_name).tap do |error|
        error.context = self.class
      end
    )
  end
end

#command?(maybe_command_name) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


237
238
239
# File 'lib/pakyow/data/sources/relational.rb', line 237

def command?(maybe_command_name)
  self.class.commands.include?(maybe_command_name)
end

#countObject



204
205
206
207
208
209
210
# File 'lib/pakyow/data/sources/relational.rb', line 204

def count
  if self.class.respond_to?(:count)
    self.class.count(__getobj__)
  else
    super
  end
end

#including(association_name, &block) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/pakyow/data/sources/relational.rb', line 93

def including(association_name, &block)
  tap do
    association_name = association_name.to_sym

    association_to_include = self.class.associations.values.flatten.find { |association|
      association.name == association_name
    } || raise(UnknownAssociation.new("unknown association `#{association_name}'").tap { |error| error.context = self.class })

    included_source = association_to_include.associated_source.instance

    if association_to_include.query
      included_source = included_source.send(association_to_include.query)
    end

    final_source = if block_given?
      included_source.instance_exec(&block) || included_source
    else
      included_source
    end

    @included << [association_to_include, final_source]
  end
end

#limit(count) ⇒ Object



123
124
125
# File 'lib/pakyow/data/sources/relational.rb', line 123

def limit(count)
  __setobj__(__getobj__.limit(count)); self
end

#modifier?(maybe_modifier_name) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


249
250
251
# File 'lib/pakyow/data/sources/relational.rb', line 249

def modifier?(maybe_modifier_name)
  MODIFIER_METHODS.include?(maybe_modifier_name)
end

#on_commit(&block) ⇒ Object



176
177
178
# File 'lib/pakyow/data/sources/relational.rb', line 176

def on_commit(&block)
  self.class.container.connection.adapter.connection.after_commit(&block)
end

#on_rollback(&block) ⇒ Object



180
181
182
# File 'lib/pakyow/data/sources/relational.rb', line 180

def on_rollback(&block)
  self.class.container.connection.adapter.connection.after_rollback(&block)
end

#oneObject



156
157
158
159
160
161
162
163
164
165
166
# File 'lib/pakyow/data/sources/relational.rb', line 156

def one
  return @results.first if instance_variable_defined?(:@results)
  return @result if instance_variable_defined?(:@result)

  if result = self.class.one(__getobj__)
    include_results!([result])
    @result = finalize(result)
  else
    nil
  end
end

#order(*ordering) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/pakyow/data/sources/relational.rb', line 127

def order(*ordering)
  __setobj__(
    __getobj__.order(
      *ordering.flat_map { |order|
        case order
        when Array
          Sequel.public_send(order[1].to_sym, order[0].to_sym)
        when Hash
          order.each_pair.map { |key, value|
            Sequel.public_send(value.to_sym, key.to_sym)
          }
        else
          Sequel.asc(order.to_s.to_sym)
        end
      }
    )
  ); self
end

#query?(maybe_query_name) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


242
243
244
# File 'lib/pakyow/data/sources/relational.rb', line 242

def query?(maybe_query_name)
  self.class.queries.include?(maybe_query_name)
end

#reloadObject



217
218
219
220
221
222
223
224
225
# File 'lib/pakyow/data/sources/relational.rb', line 217

def reload
  IVARS_TO_RELOAD.select { |ivar|
    instance_variable_defined?(ivar)
  }.each do |ivar|
    remove_instance_variable(ivar)
  end

  self
end

#source_nameObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



232
233
234
# File 'lib/pakyow/data/sources/relational.rb', line 232

def source_name
  self.class.__object_name.name
end

#to_aObject Also known as: all



146
147
148
149
150
151
152
153
# File 'lib/pakyow/data/sources/relational.rb', line 146

def to_a
  return @results if instance_variable_defined?(:@results)
  @results = self.class.to_a(__getobj__)
  include_results!(@results)
  @results.map! { |result|
    finalize(result)
  }
end

#to_jsonObject



227
228
229
# File 'lib/pakyow/data/sources/relational.rb', line 227

def to_json(*)
  to_a.to_json
end

#transaction(&block) ⇒ Object



168
169
170
# File 'lib/pakyow/data/sources/relational.rb', line 168

def transaction(&block)
  self.class.container.connection.transaction(&block)
end

#transaction?Boolean

Returns:

  • (Boolean)


172
173
174
# File 'lib/pakyow/data/sources/relational.rb', line 172

def transaction?
  self.class.container.connection.adapter.connection.in_transaction?
end