database-exporter 0.0.1

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.travis.yml +6 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +7 -0
  6. data/Gemfile.lock +74 -0
  7. data/LICENSE +22 -0
  8. data/README.md +682 -0
  9. data/Rakefile +7 -0
  10. data/bin/database-exporter +48 -0
  11. data/database_exporter.gemspec +34 -0
  12. data/example_data/contentful_model.json +316 -0
  13. data/example_data/contentful_structure.json +89 -0
  14. data/example_data/example_settings.yml +25 -0
  15. data/example_data/mapping.json +119 -0
  16. data/lib/cli.rb +13 -0
  17. data/lib/configuration.rb +69 -0
  18. data/lib/converters/content_types_structure_creator.rb +58 -0
  19. data/lib/converters/contentful_model_to_json.rb +78 -0
  20. data/lib/database/export.rb +74 -0
  21. data/lib/database/modules/json_export.rb +79 -0
  22. data/lib/database/modules/relations_export.rb +270 -0
  23. data/lib/database/modules/utils.rb +20 -0
  24. data/lib/migrator.rb +29 -0
  25. data/lib/version.rb +3 -0
  26. data/spec/fixtures/database/data/assets/image/image_1.json +9 -0
  27. data/spec/fixtures/database/data/assets/image/image_2.json +9 -0
  28. data/spec/fixtures/database/data/assets/image/image_3.json +9 -0
  29. data/spec/fixtures/database/data/assets/image/image_4.json +9 -0
  30. data/spec/fixtures/database/data/collections/comment.json +18 -0
  31. data/spec/fixtures/database/data/collections/job_skills.json +13 -0
  32. data/spec/fixtures/database/data/collections/jobs.json +44 -0
  33. data/spec/fixtures/database/data/collections/profile.json +19 -0
  34. data/spec/fixtures/database/data/collections/user.json +36 -0
  35. data/spec/fixtures/database/data/entries/comment/comment_1.json +9 -0
  36. data/spec/fixtures/database/data/entries/comment/comment_2.json +9 -0
  37. data/spec/fixtures/database/data/entries/comment/comment_3.json +9 -0
  38. data/spec/fixtures/database/data/entries/comment/comment_4.json +9 -0
  39. data/spec/fixtures/database/data/entries/comment/comment_5.json +9 -0
  40. data/spec/fixtures/database/data/entries/job_skills/job_skills_1.json +7 -0
  41. data/spec/fixtures/database/data/entries/job_skills/job_skills_10.json +7 -0
  42. data/spec/fixtures/database/data/entries/job_skills/job_skills_2.json +7 -0
  43. data/spec/fixtures/database/data/entries/job_skills/job_skills_3.json +7 -0
  44. data/spec/fixtures/database/data/entries/job_skills/job_skills_4.json +7 -0
  45. data/spec/fixtures/database/data/entries/job_skills/job_skills_5.json +7 -0
  46. data/spec/fixtures/database/data/entries/job_skills/job_skills_6.json +7 -0
  47. data/spec/fixtures/database/data/entries/job_skills/job_skills_7.json +7 -0
  48. data/spec/fixtures/database/data/entries/job_skills/job_skills_8.json +7 -0
  49. data/spec/fixtures/database/data/entries/job_skills/job_skills_9.json +7 -0
  50. data/spec/fixtures/database/data/entries/jobs/jobs_1.json +56 -0
  51. data/spec/fixtures/database/data/entries/jobs/jobs_2.json +55 -0
  52. data/spec/fixtures/database/data/entries/jobs/jobs_4.json +49 -0
  53. data/spec/fixtures/database/data/entries/profile/profile_1.json +12 -0
  54. data/spec/fixtures/database/data/entries/profile/profile_2.json +12 -0
  55. data/spec/fixtures/database/data/entries/user/user_1.json +24 -0
  56. data/spec/fixtures/database/data/entries/user/user_2.json +20 -0
  57. data/spec/fixtures/database/data/helpers/job_add_id_comments.json +11 -0
  58. data/spec/fixtures/database/data/helpers/job_add_id_job_add_skills.json +24 -0
  59. data/spec/fixtures/database/data/helpers/user_id_job_adds.json +9 -0
  60. data/spec/fixtures/database/data/helpers/user_id_profiles.json +8 -0
  61. data/spec/fixtures/database/data/table_names.json +10 -0
  62. data/spec/fixtures/database/table_names.json +4 -0
  63. data/spec/fixtures/development.sqlite3 +0 -0
  64. data/spec/fixtures/json_responses/transformed_row.json +7 -0
  65. data/spec/fixtures/json_row/row.json +6 -0
  66. data/spec/fixtures/settings/contentful_model.json +316 -0
  67. data/spec/fixtures/settings/contentful_structure.json +89 -0
  68. data/spec/fixtures/settings/contentful_structure_test.json +82 -0
  69. data/spec/fixtures/settings/mapping.json +119 -0
  70. data/spec/fixtures/settings/settings.yml +27 -0
  71. data/spec/lib/configuration_spec.rb +17 -0
  72. data/spec/lib/database/export_spec.rb +49 -0
  73. data/spec/lib/database/json_export_spec.rb +49 -0
  74. data/spec/lib/database/relations_export_spec.rb +201 -0
  75. data/spec/lib/migrator_spec.rb +112 -0
  76. data/spec/spec_helper.rb +12 -0
  77. data/spec/support/db_rows_json.rb +9 -0
  78. data/spec/support/shared_configuration.rb +27 -0
  79. metadata +358 -0
@@ -0,0 +1,25 @@
1
+ #PATH to all data
2
+ data_dir: PATH_TO_ALL_DATA
3
+
4
+ #Connecting to a database
5
+ adapter: postgres
6
+ host: localhost
7
+ database: database_name
8
+ user: username
9
+ password: password
10
+
11
+ # Extract data from models:
12
+ mapped:
13
+ tables:
14
+ - :table_name_1
15
+ - :table_name_2
16
+ - :table_name_3
17
+ - :table_name_4
18
+
19
+ ## MAPPING ##
20
+ mapping_dir: example_data/mapping.json
21
+ contentful_structure_dir: example_data/contentful_structure.json
22
+
23
+ ## CONVERT
24
+ content_model_json: example_data/contentful_model.json
25
+ converted_model_dir: example_data/contentful_structure.json
@@ -0,0 +1,119 @@
1
+ {
2
+ "JobAdds": {
3
+ "content_type": "Jobs",
4
+ "type": "entry",
5
+ "fields": {
6
+ "title": "name",
7
+ "description": "specification"
8
+ },
9
+ "links": {
10
+ "aggregate_belongs": [
11
+ {
12
+ "relation_to": "Users",
13
+ "primary_id": "user_id",
14
+ "field": "first_name"
15
+ }
16
+ ],
17
+ "aggregate_many": [
18
+ {
19
+ "primary_id": "job_add_id",
20
+ "relation_to": "Comments",
21
+ "field": "subject",
22
+ "save_as": "subjects_comments"
23
+ }
24
+ ],
25
+ "belongs_to": [
26
+ {
27
+ "relation_to": "Images",
28
+ "foreign_id": "image_id"
29
+ },
30
+ {
31
+ "relation_to": "Users",
32
+ "foreign_id": "user_id"
33
+ }
34
+ ],
35
+ "many": [
36
+ {
37
+ "relation_to": "Comments",
38
+ "primary_id": "job_add_id"
39
+ }
40
+ ],
41
+ "many_through": [
42
+ {
43
+ "relation_to": "Skills",
44
+ "primary_id": "job_add_id",
45
+ "foreign_id": "skill_id",
46
+ "through": "JobAddSkills"
47
+ }
48
+ ]
49
+ }
50
+ },
51
+ "Comments": {
52
+ "content_type": "Comment",
53
+ "type": "entry",
54
+ "fields": {
55
+ "title": "subject",
56
+ "body": "content"
57
+ },
58
+ "links": {
59
+ }
60
+ },
61
+ "Skills": {
62
+ "content_type": "Job Skills",
63
+ "type": "entry",
64
+ "fields": {
65
+ },
66
+ "links": {
67
+ }
68
+ },
69
+ "Profiles": {
70
+ "content_type": "Profile",
71
+ "type": "entry",
72
+ "fields": {
73
+ },
74
+ "links": {
75
+ "belongs_to": [
76
+ {
77
+ "relation_to": "Users",
78
+ "foreign_id": "user_id"
79
+ }
80
+ ]
81
+ }
82
+ },
83
+ "Users": {
84
+ "content_type": "User",
85
+ "type": "entry",
86
+ "fields": {
87
+ },
88
+ "links": {
89
+ "aggregate_has_one": [
90
+ {
91
+ "relation_to": "Profiles",
92
+ "primary_id": "user_id",
93
+ "field": "nickname",
94
+ "save_as": "custom_nick"
95
+ }
96
+ ],
97
+ "has_one": [
98
+ {
99
+ "relation_to": "Profiles",
100
+ "primary_id": "user_id"
101
+ }
102
+ ],
103
+ "many": [
104
+ {
105
+ "relation_to": "JobAdds",
106
+ "primary_id": "user_id"
107
+ }
108
+ ]
109
+ }
110
+ },
111
+ "Images": {
112
+ "content_type": "Image",
113
+ "type": "asset",
114
+ "fields": {
115
+ },
116
+ "links": {
117
+ }
118
+ }
119
+ }
data/lib/cli.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'migrator'
2
+ require 'yaml'
3
+
4
+ module Command
5
+ class CLI < Escort::ActionCommand::Base
6
+
7
+ def execute
8
+ setting_file = YAML.load_file(global_options[:file])
9
+ Migrator.new(setting_file).run(command_name)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,69 @@
1
+ require 'sequel'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module Contentful
5
+ class Configuration
6
+ attr_reader :config,
7
+ :data_dir,
8
+ :collections_dir,
9
+ :entries_dir,
10
+ :assets_dir,
11
+ :contentful_structure,
12
+ :db,
13
+ :helpers_dir,
14
+ :converted_model_dir,
15
+ :content_types
16
+
17
+ def initialize(settings)
18
+ @config = settings
19
+ validate_required_parameters
20
+ @data_dir = config['data_dir']
21
+ @collections_dir = "#{data_dir}/collections"
22
+ @entries_dir = "#{data_dir}/entries"
23
+ @assets_dir = "#{data_dir}/assets"
24
+ @helpers_dir = "#{data_dir}/helpers"
25
+ @contentful_structure = load_contentful_structure_file
26
+ @db = adapter_setup
27
+ @converted_model_dir = settings['converted_model_dir']
28
+ @content_types = config['content_model_json']
29
+ end
30
+
31
+ def validate_required_parameters
32
+ fail ArgumentError, 'Set PATH to data_dir. Folder where all data will be stored. View README' if config['data_dir'].nil?
33
+ fail ArgumentError, 'Set PATH to contentful structure JSON file. View README' if config['contentful_structure_dir'].nil?
34
+ fail ArgumentError, 'Set PATH to mapping structure JSON file. View README' if config['mapping_dir'].nil?
35
+ fail ArgumentError, 'Set PATH to Content model JSON file, which is downloaded structure from Contentful. View README' if config['converted_model_dir'].nil?
36
+ fail ArgumentError, 'Set PATH to converted contentful model and saved as JSON file. View README' if config['content_model_json'].nil?
37
+ define_adapter
38
+ end
39
+
40
+ def define_adapter
41
+ %w(adapter host database).each do |param|
42
+ fail ArgumentError, "Set database connection parameters [adapter, host, database, user, password]. Missing the '#{param}' parameter! Password and User are optional. View README!" unless config[param]
43
+ end
44
+ end
45
+
46
+ # If contentful_structure JSON file exists, it will load the file. If not, it will automatically create an empty file.
47
+ # This file is required to convert contentful model to contentful import structure.
48
+ def load_contentful_structure_file
49
+ file_exists? ? load_existing_contentful_structure_file : create_empty_contentful_structure_file
50
+ end
51
+
52
+ def file_exists?
53
+ File.exists?(config['contentful_structure_dir'])
54
+ end
55
+
56
+ def create_empty_contentful_structure_file
57
+ File.open(settings['contentful_structure_dir'], 'w') { |file| file.write({}) }
58
+ load_existing_contentful_structure_file
59
+ end
60
+
61
+ def load_existing_contentful_structure_file
62
+ JSON.parse(File.read(config['contentful_structure_dir']), symbolize_names: true).with_indifferent_access
63
+ end
64
+
65
+ def adapter_setup
66
+ Sequel.connect(:adapter => config['adapter'], :user => config['user'], :host => config['host'], :database => config['database'], :password => config['password'])
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,58 @@
1
+ module Contentful
2
+ module Converter
3
+ class ContentTypesStructureCreator
4
+
5
+ attr_reader :config
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def create_content_type_json_file(content_type_name, values)
12
+ collection = {
13
+ id: values[:id],
14
+ name: values[:name],
15
+ description: values[:description],
16
+ displayField: values[:displayField],
17
+ fields: create_fields(values[:fields])
18
+ }
19
+ write_json_to_file("#{config.collections_dir}/#{content_type_name}.json", collection)
20
+ end
21
+
22
+ def create_fields(fields)
23
+ fields.each_with_object([]) do |(field, value), results|
24
+ results << {
25
+ name: create_field(field, value).capitalize,
26
+ id: create_field(field, value),
27
+ type: create_type_field(value),
28
+ link_type: create_link_type_field(value),
29
+ link: create_link_field(value)
30
+ }.compact
31
+ end
32
+ end
33
+
34
+ def create_field(field, value)
35
+ value.is_a?(Hash) ? value[:id] : field
36
+ end
37
+
38
+ def create_link_type_field(value)
39
+ value.is_a?(Hash) ? value[:link_type] : nil
40
+ end
41
+
42
+ def create_type_field(value)
43
+ value.is_a?(Hash) ? value[:type] : value
44
+ end
45
+
46
+ def create_link_field(value)
47
+ value.is_a?(Hash) ? value[:link] : nil
48
+ end
49
+
50
+ def write_json_to_file(path, data)
51
+ File.open(path, 'w') do |file|
52
+ file.write(JSON.pretty_generate(data))
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,78 @@
1
+ require_relative 'content_types_structure_creator'
2
+
3
+ module Contentful
4
+ module Converter
5
+ class ContentfulModelToJson
6
+
7
+ attr_reader :config, :logger
8
+
9
+ FIELD_TYPE = %w( Link Array )
10
+
11
+ def initialize(settings)
12
+ @config = settings
13
+ @logger = Logger.new(STDOUT)
14
+ end
15
+
16
+ def create_content_type_json
17
+ logger.info 'Create JSON files with content types structure...'
18
+ config.contentful_structure.each do |content_type, values|
19
+ content_type_name = content_type_name(content_type)
20
+ create_directory(config.collections_dir)
21
+ ContentTypesStructureCreator.new(config).create_content_type_json_file(content_type_name, values)
22
+ end
23
+ logger.info 'Done!'
24
+ end
25
+
26
+ def convert_to_import_form
27
+ logger.info 'Converting Contentful model to Contentful import structure...'
28
+ File.open(config.converted_model_dir, 'w') { |file| file.write({}) }
29
+ content_type_file = JSON.parse(File.read(config.content_types))['items']
30
+ content_type_file.each do |content_type|
31
+ parsed_content_type = {
32
+ id: content_type['sys']['id'],
33
+ name: content_type['name'],
34
+ description: content_type['description'],
35
+ displayField: content_type['displayField'],
36
+ fields: create_content_type_fields(content_type)
37
+ }
38
+ import_form = JSON.parse(File.read(config.converted_model_dir))
39
+ File.open(config.converted_model_dir, 'w') do |file|
40
+ file.write(JSON.pretty_generate(import_form.merge!(content_type['name'] => parsed_content_type)))
41
+ end
42
+ end
43
+ logger.info "Done! Contentful import structure file saved in #{config.converted_model_dir}"
44
+ end
45
+
46
+ def create_content_type_fields(content_type)
47
+ content_type['fields'].each_with_object({}) do |(field, _value), results|
48
+ id = link_id(field)
49
+ results[id] = case field['type']
50
+ when 'Link'
51
+ {id: field['id'], type: field['linkType'], link: 'Link'}
52
+ when 'Array'
53
+ {id: field['id'], type: field['type'], link_type: field['items']['linkType'], link: field['items']['type']}
54
+ else
55
+ field['type']
56
+ end
57
+ end
58
+ end
59
+
60
+ def link_id(field)
61
+ if FIELD_TYPE.include? field['type']
62
+ field['name'].capitalize
63
+ else
64
+ field['id']
65
+ end
66
+ end
67
+
68
+ def content_type_name(content_type)
69
+ I18n.transliterate(content_type).underscore.tr(' ', '_')
70
+ end
71
+
72
+ def create_directory(path)
73
+ FileUtils.mkdir_p(path) unless File.directory?(path)
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,74 @@
1
+ require 'active_support/core_ext/string'
2
+ require 'active_support/core_ext/hash/compact'
3
+ require 'active_support/core_ext/hash'
4
+ require 'fileutils'
5
+ require 'sequel'
6
+ require 'logger'
7
+ require_relative 'modules/json_export'
8
+ require_relative 'modules/relations_export'
9
+ require_relative 'modules/utils'
10
+
11
+ module Contentful
12
+ module Exporter
13
+ module Database
14
+ class Export
15
+
16
+ Encoding.default_external = 'utf-8'
17
+
18
+ include Contentful::Exporter::Database::JsonExport
19
+ include Contentful::Exporter::Database::RelationsExport
20
+ include Contentful::Exporter::Database::Utils
21
+
22
+ Sequel::Model.plugin :json_serializer
23
+ Sequel.datetime_class = DateTime
24
+
25
+ attr_reader :config, :mapping, :tables, :logger
26
+
27
+ def initialize(settings)
28
+ @config = settings
29
+ @mapping = mapping_structure
30
+ @tables = load_tables
31
+ @logger = Logger.new(STDOUT)
32
+ end
33
+
34
+ def tables_name
35
+ create_directory(config.data_dir)
36
+ write_json_to_file("#{config.data_dir}/table_names.json", config.db.tables)
37
+ logger.info "File with name of tables saved to #{"#{config.data_dir}/table_names.json"}"
38
+ end
39
+
40
+ def save_data_as_json
41
+ tables.each do |table|
42
+ logger.info "Extracting data from #{"#{table} table"}..."
43
+ model_name = table.to_s.camelize
44
+ fail ArgumentError, "Missing model name in your mapping.json file. To extract data from #{table}, define structure for this model in mapping.json file or remove #{table} from settings.yml - mapped tables! View README." if missing_model_structure?(model_name)
45
+ content_type_name = mapping[model_name][:content_type]
46
+ save_object_to_file(table, content_type_name, model_name, asset?(model_name) ? config.assets_dir : config.entries_dir)
47
+ end
48
+ end
49
+
50
+ def create_data_relations
51
+ relations_from_mapping.each do |model_name, relations|
52
+ generate_relations_helper_indexes(relations)
53
+ map_relations_to_links(model_name, relations)
54
+ end
55
+ end
56
+
57
+ def mapping_structure
58
+ fail ArgumentError, 'Set PATH to contentful structure JSON file. Check README' unless config.config['mapping_dir']
59
+ JSON.parse(File.read(config.config['mapping_dir']), symbolize_names: true).with_indifferent_access
60
+ end
61
+
62
+ def load_tables
63
+ fail ArgumentError, 'Before importing data from tables, define their names. Check README!' unless config.config['mapped'] && config.config['mapped']['tables']
64
+ config.config['mapped']['tables']
65
+ end
66
+
67
+ def missing_model_structure?(model_name)
68
+ mapping[model_name].nil?
69
+ end
70
+
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,79 @@
1
+ require 'i18n'
2
+
3
+ module Contentful
4
+ module Exporter
5
+ module Database
6
+ module JsonExport
7
+
8
+ def asset?(model_name)
9
+ mapping[model_name] && mapping[model_name][:type] == 'asset'
10
+ end
11
+
12
+ def save_object_to_file(table, content_type_name, model_name, type)
13
+ content_type_name = I18n.transliterate(content_type_name).underscore.tr(' ', '_')
14
+ create_directory("#{type}/#{content_type_name}")
15
+ config.db[table].each_with_index do |row, index|
16
+ index = index + 1
17
+ result = transform_row_into_hash(model_name, content_type_name, row, index)
18
+ write_json_to_file("#{type}/#{content_type_name}/#{result[:id]}.json", result)
19
+ end
20
+ end
21
+
22
+ def transform_row_into_hash(model_name, content_type_name, row, index)
23
+ id = row[:id] || index
24
+ logger.info "Saving #{content_type_name} - id: #{id}"
25
+ db_object = map_fields(model_name, row)
26
+ db_object[:id] = model_id(model_name, content_type_name, id)
27
+ db_object[:database_id] = id
28
+ db_object
29
+ end
30
+
31
+ def model_id(model_name, content_type_name, id)
32
+ prefix = mapping[model_name][:prefix_id] || ''
33
+ "#{prefix}#{content_type_name}_#{id}"
34
+ end
35
+
36
+ def map_fields(model_name, row)
37
+ row.each_with_object({}) do |(field_name, field_value), result|
38
+ field_name = mapped_field_name(field_name, model_name)
39
+ formatted_value = formatted_field_value(field_name, field_value, model_name)
40
+ result[field_name] = formatted_value
41
+ copy_field_value(field_name, formatted_value, model_name, result) if copy_field?(field_name, model_name)
42
+ end
43
+ end
44
+
45
+ def formatted_field_value(field_name, field_value, model_name)
46
+ has_mapping_value?(field_name, model_name) ? format_value(field_value) : field_value
47
+ end
48
+
49
+ def copy_field_value(field_name, field_value, model_name, result)
50
+ copy_field = mapping[model_name][:copy][field_name]
51
+ result[copy_field] = format_value(field_value.to_s)
52
+ end
53
+
54
+ def format_value(field_value)
55
+ formatted_value = I18n.transliterate(field_value).tr(' ', '_').underscore
56
+ formatted_value.underscore.gsub(/\W/, '-').gsub(/\W\z/, '').gsub(/\A\W/, '').gsub('_', '-').gsub('--', '-').gsub('--', '-')
57
+ end
58
+
59
+ def mapped_field_name(field_name, model_name)
60
+ has_mapping_for?(field_name, model_name) ? mapping[model_name][:fields][field_name] : field_name
61
+ end
62
+
63
+ def has_mapping_for?(field_name, model_name)
64
+ mapping[model_name] && mapping[model_name][:fields][field_name].present?
65
+ end
66
+
67
+ def has_mapping_value?(field_name, model_name)
68
+ mapping[model_name] && mapping[model_name][:format] && mapping[model_name][:format][field_name].present?
69
+ end
70
+
71
+ def copy_field?(field_name, model_name)
72
+ mapping[model_name] && mapping[model_name][:copy] && mapping[model_name][:copy][field_name].present?
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+ end
79
+