legion-data 1.2.0 → 1.3.7

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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -0
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +41 -12
  5. data/CHANGELOG.md +80 -1
  6. data/CLAUDE.md +199 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +1 -1
  9. data/README.md +157 -50
  10. data/exe/legionio_migrate +0 -0
  11. data/legion-data.gemspec +13 -13
  12. data/lib/legion/data/connection.rb +39 -25
  13. data/lib/legion/data/encryption/cipher.rb +49 -0
  14. data/lib/legion/data/encryption/key_provider.rb +45 -0
  15. data/lib/legion/data/encryption/sequel_plugin.rb +54 -0
  16. data/lib/legion/data/event_store/projection.rb +56 -0
  17. data/lib/legion/data/event_store.rb +112 -0
  18. data/lib/legion/data/local.rb +77 -0
  19. data/lib/legion/data/migration.rb +5 -3
  20. data/lib/legion/data/migrations/001_add_schema_columns.rb +9 -3
  21. data/lib/legion/data/migrations/002_add_nodes.rb +10 -12
  22. data/lib/legion/data/migrations/003_add_settings.rb +10 -10
  23. data/lib/legion/data/migrations/004_add_extensions.rb +15 -17
  24. data/lib/legion/data/migrations/005_add_runners.rb +13 -13
  25. data/lib/legion/data/migrations/006_add_functions.rb +13 -15
  26. data/lib/legion/data/migrations/007_add_default_extensions.rb +2 -0
  27. data/lib/legion/data/migrations/008_add_tasks.rb +15 -21
  28. data/lib/legion/data/migrations/009_add_digital_workers.rb +45 -0
  29. data/lib/legion/data/migrations/010_add_value_metrics.rb +19 -0
  30. data/lib/legion/data/migrations/011_add_extensions_registry.rb +30 -0
  31. data/lib/legion/data/migrations/012_add_apollo_tables.rb +66 -0
  32. data/lib/legion/data/migrations/013_add_relationships.rb +21 -0
  33. data/lib/legion/data/migrations/014_add_relationship_columns.rb +27 -0
  34. data/lib/legion/data/migrations/015_add_rbac_tables.rb +49 -0
  35. data/lib/legion/data/migrations/016_add_worker_health.rb +33 -0
  36. data/lib/legion/data/migrations/017_add_audit_log.rb +30 -0
  37. data/lib/legion/data/migrations/018_add_governance_events.rb +21 -0
  38. data/lib/legion/data/migrations/019_add_audit_hash_chain.rb +29 -0
  39. data/lib/legion/data/migrations/020_add_webhooks.rb +37 -0
  40. data/lib/legion/data/model.rb +4 -1
  41. data/lib/legion/data/models/apollo_access_log.rb +13 -0
  42. data/lib/legion/data/models/apollo_entry.rb +18 -0
  43. data/lib/legion/data/models/apollo_expertise.rb +12 -0
  44. data/lib/legion/data/models/apollo_relation.rb +14 -0
  45. data/lib/legion/data/models/audit_log.rb +34 -0
  46. data/lib/legion/data/models/digital_worker.rb +44 -0
  47. data/lib/legion/data/models/function.rb +2 -2
  48. data/lib/legion/data/models/node.rb +16 -0
  49. data/lib/legion/data/models/rbac_cross_team_grant.rb +33 -0
  50. data/lib/legion/data/models/rbac_role_assignment.rb +29 -0
  51. data/lib/legion/data/models/rbac_runner_grant.rb +21 -0
  52. data/lib/legion/data/models/relationship.rb +13 -0
  53. data/lib/legion/data/settings.rb +54 -25
  54. data/lib/legion/data/version.rb +3 -1
  55. data/lib/legion/data.rb +20 -1
  56. metadata +51 -32
  57. data/.github/workflows/rubocop-analysis.yml +0 -28
  58. data/.github/workflows/sourcehawk-scan.yml +0 -20
  59. data/CODE_OF_CONDUCT.md +0 -75
  60. data/CONTRIBUTING.md +0 -55
  61. data/INDIVIDUAL_CONTRIBUTOR_LICENSE.md +0 -30
  62. data/NOTICE.txt +0 -9
  63. data/SECURITY.md +0 -9
  64. data/attribution.txt +0 -1
  65. data/sourcehawk.yml +0 -4
data/legion-data.gemspec CHANGED
@@ -6,28 +6,28 @@ Gem::Specification.new do |spec|
6
6
  spec.name = 'legion-data'
7
7
  spec.version = Legion::Data::VERSION
8
8
  spec.authors = ['Esity']
9
- spec.email = %w[matthewdiverson@gmail.com ruby@optum.com]
9
+ spec.email = ['matthewdiverson@gmail.com']
10
10
 
11
11
  spec.summary = 'Manages the connects to the backend database'
12
12
  spec.description = 'A LegionIO gem to connect to a persistent data store'
13
- spec.homepage = 'https://github.com/Optum/legion-data'
13
+ spec.homepage = 'https://github.com/LegionIO/legion-data'
14
14
  spec.license = 'Apache-2.0'
15
- spec.required_ruby_version = '>= 2.5'
15
+ spec.required_ruby_version = '>= 3.4'
16
16
  spec.require_paths = ['lib']
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
- spec.test_files = spec.files.select { |p| p =~ %r{^test/.*_test.rb} }
19
- spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md]
18
+ spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md]
20
19
  spec.metadata = {
21
- 'bug_tracker_uri' => 'https://github.com/Optum/legion-data/issues',
22
- 'changelog_uri' => 'https://github.com/Optum/legion-data/src/main/CHANGELOG.md',
23
- 'documentation_uri' => 'https://github.com/Optum/legion-data',
24
- 'homepage_uri' => 'https://github.com/Optum/LegionIO',
25
- 'source_code_uri' => 'https://github.com/Optum/legion-data',
26
- 'wiki_uri' => 'https://github.com/Optum/legion-data/wiki'
20
+ 'bug_tracker_uri' => 'https://github.com/LegionIO/legion-data/issues',
21
+ 'changelog_uri' => 'https://github.com/LegionIO/legion-data/blob/main/CHANGELOG.md',
22
+ 'documentation_uri' => 'https://github.com/LegionIO/legion-data',
23
+ 'homepage_uri' => 'https://github.com/LegionIO/LegionIO',
24
+ 'source_code_uri' => 'https://github.com/LegionIO/legion-data',
25
+ 'wiki_uri' => 'https://github.com/LegionIO/legion-data/wiki',
26
+ 'rubygems_mfa_required' => 'true'
27
27
  }
28
28
 
29
29
  spec.add_dependency 'legion-logging'
30
30
  spec.add_dependency 'legion-settings'
31
- spec.add_dependency 'mysql2'
32
- spec.add_dependency 'sequel'
31
+ spec.add_dependency 'sequel', '>= 5.70'
32
+ spec.add_dependency 'sqlite3', '>= 2.0'
33
33
  end
@@ -1,27 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sequel'
2
4
 
3
5
  module Legion
4
6
  module Data
5
7
  module Connection
8
+ ADAPTERS = %i[sqlite mysql2 postgres].freeze
9
+
6
10
  class << self
7
11
  attr_accessor :sequel
8
12
 
9
13
  def adapter
10
- @adapter ||= RUBY_ENGINE == 'jruby' ? :jdbc : :mysql2
14
+ @adapter ||= Legion::Settings[:data][:adapter]&.to_sym || :sqlite
11
15
  end
12
16
 
13
17
  def setup
14
- @sequel = if adapter == :mysql2
15
- ::Sequel.connect(adapter: adapter, **creds_builder)
18
+ @sequel = if adapter == :sqlite
19
+ ::Sequel.sqlite(sqlite_path)
16
20
  else
17
- ::Sequel.connect("jdbc:mysql://#{creds_builder[:host]}:#{creds_builder[:port]}/#{creds_builder[:database]}?user=#{creds_builder[:username]}&password=#{creds_builder[:password]}&serverTimezone=UTC") # rubocop:disable Layout/LineLength
21
+ begin
22
+ ::Sequel.connect(adapter: adapter, **creds_builder)
23
+ rescue StandardError => e
24
+ raise unless dev_fallback?
25
+
26
+ if defined?(Legion::Logging)
27
+ Legion::Logging.warn(
28
+ "Shared DB unreachable (#{e.message}), dev_mode fallback to SQLite"
29
+ )
30
+ end
31
+ @adapter = :sqlite
32
+ ::Sequel.sqlite(sqlite_path)
33
+ end
18
34
  end
19
35
  Legion::Settings[:data][:connected] = true
20
- return if Legion::Settings[:data][:connection].nil? || Legion::Settings[:data][:connection][:log].nil?
21
-
22
- @sequel.logger = Legion::Logging
23
- @sequel.sql_log_level = Legion::Settings[:data][:connection][:sql_log_level]
24
- @sequel.log_warn_duration = Legion::Settings[:data][:connection][:log_warn_duration]
36
+ configure_logging
25
37
  end
26
38
 
27
39
  def shutdown
@@ -30,15 +42,9 @@ module Legion
30
42
  end
31
43
 
32
44
  def creds_builder(final_creds = {})
33
- final_creds.merge! Legion::Data::Settings.creds
45
+ final_creds.merge! Legion::Data::Settings.creds(adapter)
34
46
  final_creds.merge! Legion::Settings[:data][:creds] if Legion::Settings[:data][:creds].is_a? Hash
35
47
 
36
- # if Legion::Settings[:data][:connection][:max_connections].is_a? Integer
37
- # final_creds[:max_connections] = Legion::Settings[:data][:connection][:max_connections]
38
- # end
39
-
40
- # final_creds[:preconnect] = :concurrently if Legion::Settings[:data][:connection][:preconnect]
41
-
42
48
  return final_creds if Legion::Settings[:vault].nil?
43
49
 
44
50
  if Legion::Settings[:vault][:connected] && ::Vault.sys.mounts.key?(:database)
@@ -50,15 +56,23 @@ module Legion
50
56
  final_creds
51
57
  end
52
58
 
53
- def default_creds
54
- {
55
- host: '127.0.0.1',
56
- port: 3306,
57
- username: 'legion',
58
- password: 'legion',
59
- database: 'legion',
60
- max_connections: 4
61
- }
59
+ private
60
+
61
+ def dev_fallback?
62
+ data_settings = Legion::Settings[:data]
63
+ data_settings[:dev_mode] == true && data_settings[:dev_fallback] != false
64
+ end
65
+
66
+ def sqlite_path
67
+ Legion::Settings[:data][:creds][:database] || 'legionio.db'
68
+ end
69
+
70
+ def configure_logging
71
+ return if Legion::Settings[:data][:connection].nil? || Legion::Settings[:data][:connection][:log].nil?
72
+
73
+ @sequel.logger = Legion::Logging
74
+ @sequel.sql_log_level = Legion::Settings[:data][:connection][:sql_log_level]
75
+ @sequel.log_warn_duration = Legion::Settings[:data][:connection][:log_warn_duration]
62
76
  end
63
77
  end
64
78
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Legion
6
+ module Data
7
+ module Encryption
8
+ module Cipher
9
+ VERSION_BYTE = "\x01".b.freeze
10
+ IV_LENGTH = 12
11
+ TAG_LENGTH = 16
12
+
13
+ class << self
14
+ def encrypt(plaintext, key:, aad: '')
15
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
16
+ iv = OpenSSL::Random.random_bytes(IV_LENGTH)
17
+ cipher.key = key
18
+ cipher.iv = iv
19
+ cipher.auth_data = aad
20
+
21
+ ciphertext = cipher.update(plaintext.to_s) + cipher.final
22
+ tag = cipher.auth_tag(TAG_LENGTH)
23
+
24
+ VERSION_BYTE + iv + ciphertext + tag
25
+ end
26
+
27
+ def decrypt(blob, key:, aad: '')
28
+ raise ArgumentError, 'data too short' if blob.bytesize < 1 + IV_LENGTH + TAG_LENGTH
29
+
30
+ version = blob.byteslice(0, 1)
31
+ raise ArgumentError, "unsupported version: #{version.unpack1('C')}" unless version == VERSION_BYTE
32
+
33
+ iv = blob.byteslice(1, IV_LENGTH)
34
+ tag = blob.byteslice(-TAG_LENGTH, TAG_LENGTH)
35
+ ciphertext = blob.byteslice(1 + IV_LENGTH, blob.bytesize - 1 - IV_LENGTH - TAG_LENGTH)
36
+
37
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
38
+ cipher.key = key
39
+ cipher.iv = iv
40
+ cipher.auth_tag = tag
41
+ cipher.auth_data = aad
42
+
43
+ cipher.update(ciphertext) + cipher.final
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Legion
6
+ module Data
7
+ module Encryption
8
+ class KeyProvider
9
+ def initialize(mode: :auto)
10
+ @mode = mode
11
+ @key_cache = {}
12
+ end
13
+
14
+ def key_for(tenant_id: nil)
15
+ cache_key = tenant_id || '__default__'
16
+ @key_cache[cache_key] ||= derive_key(tenant_id)
17
+ end
18
+
19
+ def clear_cache!
20
+ @key_cache.clear
21
+ end
22
+
23
+ private
24
+
25
+ def derive_key(tenant_id)
26
+ if tenant_id && crypt_available?
27
+ Legion::Crypt::PartitionKeys.derive(tenant_id: tenant_id)
28
+ elsif crypt_available?
29
+ Legion::Crypt.default_encryption_key
30
+ else
31
+ local_key
32
+ end
33
+ end
34
+
35
+ def crypt_available?
36
+ defined?(Legion::Crypt::PartitionKeys)
37
+ end
38
+
39
+ def local_key
40
+ OpenSSL::Digest.digest('SHA256', 'legion-dev-encryption-key')
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cipher'
4
+ require_relative 'key_provider'
5
+
6
+ module Legion
7
+ module Data
8
+ module Encryption
9
+ module SequelPlugin
10
+ module ClassMethods
11
+ def encrypted_columns
12
+ @encrypted_columns ||= {}
13
+ end
14
+
15
+ def encrypted_column(name, key_scope: :default)
16
+ col_scope = key_scope
17
+ encrypted_columns[name] = { key_scope: col_scope }
18
+
19
+ define_method(name) do
20
+ raw = super()
21
+ return nil if raw.nil?
22
+
23
+ provider = self.class.encryption_key_provider
24
+ tenant = col_scope == :tenant ? self[:tenant_id] : nil
25
+ key = provider.key_for(tenant_id: tenant)
26
+ aad = "#{self.class.table_name}:#{pk}:#{name}"
27
+ Legion::Data::Encryption::Cipher.decrypt(raw.b, key: key, aad: aad)
28
+ end
29
+
30
+ define_method(:"#{name}=") do |value|
31
+ if value.nil?
32
+ super(nil)
33
+ else
34
+ provider = self.class.encryption_key_provider
35
+ tenant = col_scope == :tenant ? self[:tenant_id] : nil
36
+ key = provider.key_for(tenant_id: tenant)
37
+ aad = "#{self.class.table_name}:#{pk || 0}:#{name}"
38
+ encrypted = Legion::Data::Encryption::Cipher.encrypt(value.to_s, key: key, aad: aad)
39
+ super(Sequel.blob(encrypted))
40
+ end
41
+ end
42
+ end
43
+
44
+ def encryption_key_provider
45
+ @encryption_key_provider ||= KeyProvider.new
46
+ end
47
+ end
48
+
49
+ module InstanceMethods
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Data
5
+ module EventStore
6
+ class Projection
7
+ attr_reader :state
8
+
9
+ def initialize
10
+ @state = {}
11
+ end
12
+
13
+ def apply(_event)
14
+ raise NotImplementedError, "#{self.class} must implement #apply"
15
+ end
16
+
17
+ def self.build_from(stream, since: nil)
18
+ projection = new
19
+ events = EventStore.read_stream(stream, since: since)
20
+ events.each { |e| projection.apply(e) }
21
+ projection
22
+ end
23
+ end
24
+
25
+ class ConsentState < Projection
26
+ def apply(event)
27
+ scope = event.dig(:data, :scope)
28
+ return unless scope
29
+
30
+ case event[:type]
31
+ when 'consent.granted', 'consent.modified'
32
+ @state[scope] = event.dig(:data, :tier)
33
+ when 'consent.revoked'
34
+ @state.delete(scope)
35
+ end
36
+ end
37
+ end
38
+
39
+ class GovernanceTimeline < Projection
40
+ def initialize
41
+ super
42
+ @state = []
43
+ end
44
+
45
+ def apply(event)
46
+ @state << {
47
+ type: event[:type],
48
+ stream: event[:stream],
49
+ at: event[:created_at],
50
+ data: event[:data]
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Legion
6
+ module Data
7
+ module EventStore
8
+ GOVERNANCE_EVENT_TYPES = %w[
9
+ consent.granted consent.revoked consent.modified
10
+ extinction.triggered extinction.resolved
11
+ worker.registered worker.retired worker.transferred
12
+ scope.approved scope.violated scope.reconciled
13
+ audit.retention_applied audit.exported
14
+ ].freeze
15
+
16
+ class << self
17
+ def append(stream:, type:, data: {}, metadata: {})
18
+ return { error: 'db unavailable' } unless db_ready?
19
+
20
+ conn = Legion::Data.connection
21
+ conn.transaction do
22
+ last = conn[:governance_events]
23
+ .where(stream_id: stream)
24
+ .order(Sequel.desc(:sequence_number))
25
+ .first
26
+
27
+ seq = (last&.[](:sequence_number) || 0) + 1
28
+ prev_hash = last&.[](:event_hash) || ('0' * 64)
29
+
30
+ data_json = Legion::JSON.dump(data)
31
+ metadata_json = Legion::JSON.dump(metadata)
32
+ event_hash = compute_hash(stream, seq, type, data_json, prev_hash)
33
+
34
+ conn[:governance_events].insert(
35
+ stream_id: stream,
36
+ event_type: type,
37
+ sequence_number: seq,
38
+ data_json: data_json,
39
+ metadata_json: metadata_json,
40
+ event_hash: event_hash,
41
+ previous_hash: prev_hash,
42
+ created_at: Time.now
43
+ )
44
+
45
+ { stream: stream, sequence: seq, hash: event_hash }
46
+ end
47
+ end
48
+
49
+ def read_stream(stream, since: nil)
50
+ return [] unless db_ready?
51
+
52
+ ds = Legion::Data.connection[:governance_events].where(stream_id: stream)
53
+ ds = ds.where { created_at >= since } if since
54
+ ds.order(:sequence_number).all.map { |e| deserialize(e) }
55
+ end
56
+
57
+ def read_by_type(type, since: nil, limit: 100)
58
+ return [] unless db_ready?
59
+
60
+ ds = Legion::Data.connection[:governance_events].where(event_type: type)
61
+ ds = ds.where { created_at >= since } if since
62
+ ds.order(Sequel.desc(:created_at)).limit(limit).all.map { |e| deserialize(e) }
63
+ end
64
+
65
+ def verify_chain(stream)
66
+ return { valid: false, error: 'db unavailable' } unless db_ready?
67
+
68
+ events = Legion::Data.connection[:governance_events]
69
+ .where(stream_id: stream)
70
+ .order(:sequence_number)
71
+ .all
72
+
73
+ prev_hash = '0' * 64
74
+ events.each do |e|
75
+ expected = compute_hash(stream, e[:sequence_number], e[:event_type], e[:data_json], prev_hash)
76
+ return { valid: false, broken_at: e[:sequence_number] } unless e[:event_hash] == expected
77
+ return { valid: false, broken_at: e[:sequence_number] } unless e[:previous_hash] == prev_hash
78
+
79
+ prev_hash = e[:event_hash]
80
+ end
81
+
82
+ { valid: true, length: events.size }
83
+ end
84
+
85
+ private
86
+
87
+ def compute_hash(stream, seq, type, data_json, prev_hash)
88
+ Digest::SHA256.hexdigest("#{stream}:#{seq}:#{type}:#{data_json}:#{prev_hash}")
89
+ end
90
+
91
+ def deserialize(event)
92
+ {
93
+ id: event[:id],
94
+ stream: event[:stream_id],
95
+ type: event[:event_type],
96
+ sequence: event[:sequence_number],
97
+ data: Legion::JSON.load(event[:data_json] || '{}'),
98
+ metadata: Legion::JSON.load(event[:metadata_json] || '{}'),
99
+ hash: event[:event_hash],
100
+ created_at: event[:created_at]
101
+ }
102
+ end
103
+
104
+ def db_ready?
105
+ defined?(Legion::Data) && Legion::Data.connection&.table_exists?(:governance_events)
106
+ rescue StandardError
107
+ false
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+ require 'sequel/extensions/migration'
5
+
6
+ module Legion
7
+ module Data
8
+ module Local
9
+ class << self
10
+ attr_reader :connection, :db_path
11
+
12
+ def setup(database: nil, **)
13
+ return if @connected
14
+
15
+ db_file = database || local_settings[:database] || 'legionio_local.db'
16
+ @db_path = db_file
17
+ @connection = ::Sequel.sqlite(db_file)
18
+ @connected = true
19
+ run_migrations
20
+ Legion::Logging.info "Legion::Data::Local connected to #{db_file}" if defined?(Legion::Logging)
21
+ end
22
+
23
+ def shutdown
24
+ @connection&.disconnect
25
+ @connection = nil
26
+ @connected = false
27
+ end
28
+
29
+ def connected?
30
+ @connected == true
31
+ end
32
+
33
+ def register_migrations(name:, path:)
34
+ @registered_migrations ||= {}
35
+ @registered_migrations[name] = path
36
+ end
37
+
38
+ def registered_migrations
39
+ @registered_migrations || {}
40
+ end
41
+
42
+ def model(table_name)
43
+ raise 'Legion::Data::Local not connected' unless connected?
44
+
45
+ ::Sequel::Model(connection[table_name])
46
+ end
47
+
48
+ def reset!
49
+ @connection = nil
50
+ @connected = false
51
+ @db_path = nil
52
+ @registered_migrations = nil
53
+ end
54
+
55
+ private
56
+
57
+ def run_migrations
58
+ return unless local_settings.dig(:migrations, :auto_migrate) != false
59
+
60
+ registered_migrations.each_value do |path|
61
+ next unless File.directory?(path)
62
+
63
+ ::Sequel::TimestampMigrator.new(@connection, path).run
64
+ rescue StandardError => e
65
+ Legion::Logging.warn "Local migration failed for #{path}: #{e.message}" if defined?(Legion::Logging)
66
+ end
67
+ end
68
+
69
+ def local_settings
70
+ return {} unless defined?(Legion::Settings)
71
+
72
+ Legion::Settings[:data]&.dig(:local) || {}
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sequel/extensions/migration'
2
4
 
3
5
  module Legion
4
6
  module Data
5
7
  module Migration
6
8
  class << self
7
- def migrate(connection = Legion::Data.connection, path = "#{__dir__}/migrations", **opts)
8
- Legion::Settings[:data][:migrations][:version] = Sequel::Migrator.run(connection, path, **opts)
9
- Legion::Logging.info("Legion::Data::Migration ran successfully to version #{Legion::Settings[:data][:migrations][:version]}") # rubocop:disable Layout/LineLength
9
+ def migrate(connection = Legion::Data.connection, path = "#{__dir__}/migrations", **)
10
+ Legion::Settings[:data][:migrations][:version] = Sequel::Migrator.run(connection, path, **)
11
+ Legion::Logging.info("Legion::Data::Migration ran successfully to version #{Legion::Settings[:data][:migrations][:version]}")
10
12
  Legion::Settings[:data][:migrations][:ran] = true
11
13
  end
12
14
  end
@@ -1,10 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sequel/extensions/migration'
2
4
 
3
5
  Sequel.migration do
4
6
  up do
5
- run 'ALTER TABLE `schema_info` ADD `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `version`;'
6
- run 'ALTER TABLE `schema_info` ADD `updated_at` TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP AFTER `created_at`;'
7
- run 'ALTER TABLE `schema_info` ADD `catalog` VARCHAR(255) NULL DEFAULT NULL AFTER `version`;'
7
+ alter_table(:schema_info) do
8
+ # SQLite does not support non-constant defaults in ALTER TABLE ADD COLUMN,
9
+ # so we omit the default here and let the application set timestamps.
10
+ add_column :created_at, DateTime, null: true
11
+ add_column :updated_at, DateTime, null: true
12
+ add_column :catalog, String, size: 255, null: true
13
+ end
8
14
  end
9
15
 
10
16
  down do
@@ -1,17 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Sequel.migration do
2
4
  up do
3
- run "CREATE TABLE `nodes` (
4
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
5
- `name` varchar(128) NOT NULL DEFAULT '',
6
- `status` varchar(255) NOT NULL DEFAULT 'unknown',
7
- `active` tinyint(1) unsigned NOT NULL DEFAULT '1',
8
- `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
- `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
10
- PRIMARY KEY (`id`),
11
- UNIQUE KEY `name` (`name`),
12
- KEY `active` (`active`),
13
- KEY `status` (`status`)
14
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8;"
5
+ create_table(:nodes) do
6
+ primary_key :id
7
+ String :name, size: 128, null: false, default: '', unique: true
8
+ String :status, size: 255, null: false, default: 'unknown', index: true
9
+ TrueClass :active, null: false, default: true, index: true
10
+ DateTime :created, null: false, default: Sequel::CURRENT_TIMESTAMP
11
+ DateTime :updated, null: true
12
+ end
15
13
  end
16
14
 
17
15
  down do
@@ -1,15 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Sequel.migration do
2
4
  up do
3
- run "CREATE TABLE `settings` (
4
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
5
- `key` varchar(128) NOT NULL,
6
- `value` varchar(256) NOT NULL,
7
- `encrypted` tinyint(1) unsigned NOT NULL DEFAULT '0',
8
- `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
- `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
10
- PRIMARY KEY (`id`),
11
- UNIQUE KEY `key` (`key`)
12
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8;"
5
+ create_table(:settings) do
6
+ primary_key :id
7
+ String :key, size: 128, null: false, unique: true
8
+ String :value, size: 256, null: false
9
+ TrueClass :encrypted, null: false, default: false
10
+ DateTime :created, null: false, default: Sequel::CURRENT_TIMESTAMP
11
+ DateTime :updated, null: true
12
+ end
13
13
  end
14
14
 
15
15
  down do
@@ -1,22 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Sequel.migration do
2
4
  up do
3
- run "CREATE TABLE `extensions` (
4
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
5
- `active` tinyint(1) unsigned NOT NULL DEFAULT '1',
6
- `name` varchar(128) NOT NULL,
7
- `namespace` varchar(128) NOT NULL DEFAULT '',
8
- `exchange` varchar(255) DEFAULT NULL,
9
- `uri` varchar(256) DEFAULT NULL,
10
- `schema_version` int(11) unsigned NOT NULL DEFAULT 0,
11
- `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
12
- `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
- PRIMARY KEY (`id`),
14
- UNIQUE KEY `name_namespace` (`name`,`namespace`),
15
- KEY `active` (`active`),
16
- KEY `name` (`name`),
17
- KEY `namespace` (`namespace`),
18
- key `schema_version` (`schema_version`)
19
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8;"
5
+ create_table(:extensions) do
6
+ primary_key :id
7
+ TrueClass :active, null: false, default: true, index: true
8
+ String :name, size: 128, null: false, index: true
9
+ String :namespace, size: 128, null: false, default: '', index: true
10
+ String :exchange, size: 255, null: true
11
+ String :uri, size: 256, null: true
12
+ Integer :schema_version, null: false, default: 0, index: true
13
+ DateTime :updated, null: true
14
+ DateTime :created, null: false, default: Sequel::CURRENT_TIMESTAMP
15
+
16
+ unique %i[name namespace]
17
+ end
20
18
  end
21
19
 
22
20
  down do