contentful-database-importer 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.
@@ -0,0 +1,88 @@
1
+ require 'contentful/database_importer/support'
2
+ require 'contentful/database_importer/resource_class_methods'
3
+ require 'contentful/database_importer/resource_coercions'
4
+ require 'contentful/database_importer/resource_relationships'
5
+ require 'contentful/database_importer/resource_bootstrap_methods'
6
+ require 'contentful/database_importer/resource_bootstrap_class_methods'
7
+ require 'mimemagic'
8
+
9
+ module Contentful
10
+ module DatabaseImporter
11
+ # Resource for mapping Database Tables to
12
+ # Contentful Content Types and Entries
13
+ module Resource
14
+ include ResourceCoercions
15
+ include ResourceRelationships
16
+ include ResourceBootstrapMethods
17
+
18
+ def self.included(base)
19
+ base.extend(ResourceClassMethods)
20
+ base.extend(ResourceBootstrapClassMethods)
21
+ end
22
+
23
+ attr_reader :bootstrap_fields,
24
+ :excluded_fields,
25
+ :index,
26
+ :associated_assets
27
+
28
+ def initialize(row, index = 0)
29
+ @index = index
30
+ @bootstrap_fields = {}
31
+ @excluded_fields = {}
32
+ @raw = row
33
+ @associated_assets = []
34
+
35
+ row.each do |db_name, value|
36
+ process_row_field(db_name, value)
37
+ end
38
+
39
+ process_relationships
40
+ end
41
+
42
+ def process_row_field(db_name, value)
43
+ field_definition = self.class.fields.find { |f| f[:db_name] == db_name }
44
+
45
+ return unless field_definition
46
+
47
+ value = pre_process(field_definition, value)
48
+ value = coerce(field_definition, value)
49
+
50
+ if field_definition[:exclude_from_output]
51
+ @excluded_fields[field_definition[:maps_to]] = value
52
+ else
53
+ @bootstrap_fields[field_definition[:maps_to]] = value
54
+ end
55
+ end
56
+
57
+ def process_relationships
58
+ self.class.relationship_fields.each do |relationship_field_definition|
59
+ relations = fetch_relations(relationship_field_definition)
60
+ @bootstrap_fields[relationship_field_definition[:maps_to]] = relations
61
+ end
62
+ end
63
+
64
+ def pre_process(field_definition, value)
65
+ return value unless field_definition[:pre_process]
66
+
67
+ transformation = field_definition[:pre_process]
68
+
69
+ return send(transformation, value) if transformation.is_a? ::Symbol
70
+ return transformation.call(value) if transformation.respond_to?(:call)
71
+
72
+ raise
73
+ rescue
74
+ error = 'Pre Process could not be done for '
75
+ error += "#{field_definition[:maps_to]} - #{transformation}"
76
+ raise error
77
+ end
78
+
79
+ def id
80
+ self.class.id_generator = IdGenerator::Base.new(
81
+ self.class.default_generator_options
82
+ ) if self.class.id_generator.nil?
83
+
84
+ self.class.id_generator.run(self, index)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,113 @@
1
+ module Contentful
2
+ module DatabaseImporter
3
+ # Bootstrap related Class Methods
4
+ module ResourceBootstrapClassMethods
5
+ TYPE_MAPPINGS = {
6
+ symbol: 'Symbol',
7
+ text: 'Text',
8
+ date: 'Date',
9
+ object: 'Object',
10
+ location: 'Location',
11
+ link: 'Link',
12
+ integer: 'Integer',
13
+ number: 'Number',
14
+ boolean: 'Boolean'
15
+ }.freeze
16
+
17
+ def definition_type(type)
18
+ if type == :string
19
+ type = :symbol
20
+ elsif link?(type)
21
+ type = :link
22
+ end
23
+
24
+ TYPE_MAPPINGS[type]
25
+ end
26
+
27
+ def link_type(type)
28
+ return 'Asset' if type == :asset
29
+ return 'Entry' if resource?(type)
30
+
31
+ raise 'Type class is not a valid Link'
32
+ end
33
+
34
+ def items_type(field_data)
35
+ return {
36
+ type: 'Link',
37
+ linkType: array_link_type(field_data)
38
+ } if array_link?(field_data)
39
+
40
+ type = definition_type(field_data[:item_type])
41
+
42
+ error = 'Array item type could not be mapped for '
43
+ error += field_data[:maps_to].to_s
44
+ raise error if type.nil?
45
+
46
+ type
47
+ end
48
+
49
+ def array_link?(field_data)
50
+ (field_data[:type] == :array && field_data[:item_type] == :asset) ||
51
+ (resource?(field_data[:type]) &&
52
+ [:many, :through].include?(field_data[:relationship]))
53
+ end
54
+
55
+ def array_link_type(field_data)
56
+ return 'Asset' if field_data[:item_type] == :asset
57
+ return 'Entry' if resource?(field_data[:type])
58
+ end
59
+
60
+ def link?(type)
61
+ type == :asset || resource?(type)
62
+ end
63
+
64
+ def array?(field_data)
65
+ field_data[:type] == :array ||
66
+ (resource?(field_data[:type]) &&
67
+ [:many, :through].include?(field_data[:relationship]))
68
+ end
69
+
70
+ def resource?(other)
71
+ return false unless other.respond_to?(:ancestors)
72
+ other.ancestors.include?(::Contentful::DatabaseImporter::Resource)
73
+ end
74
+
75
+ def link_type?(field_data)
76
+ link?(field_data[:type]) && !array_link?(field_data)
77
+ end
78
+
79
+ def basic_field_definition(field_data)
80
+ {
81
+ id: field_data[:maps_to],
82
+ name: field_data[:name],
83
+ type: definition_type(field_data[:type])
84
+ }
85
+ end
86
+
87
+ def field_definition(field_data)
88
+ definition = basic_field_definition(field_data)
89
+
90
+ definition[:type] = 'Array' if array?(field_data)
91
+ definition[:linkType] = link_type(
92
+ field_data[:type]
93
+ ) if link_type?(field_data)
94
+ definition[:items] = items_type(field_data) if array?(field_data)
95
+
96
+ definition
97
+ end
98
+
99
+ def fields_definition
100
+ fields.map { |f| field_definition(f) }
101
+ end
102
+
103
+ def content_type_definition
104
+ {
105
+ id: content_type_id,
106
+ name: content_type_name,
107
+ displayField: display_field,
108
+ fields: fields_definition
109
+ }
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,22 @@
1
+ module Contentful
2
+ module DatabaseImporter
3
+ # Bootstrap related methods
4
+ module ResourceBootstrapMethods
5
+ def to_bootstrap
6
+ {
7
+ sys: {
8
+ id: id
9
+ },
10
+ fields: bootstrap_fields
11
+ }
12
+ end
13
+
14
+ def to_link
15
+ {
16
+ linkType: 'Entry',
17
+ id: id
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,126 @@
1
+
2
+ module Contentful
3
+ module DatabaseImporter
4
+ # Class Methods for Resource
5
+ module ResourceClassMethods
6
+ attr_accessor :id_generator
7
+
8
+ def table_name
9
+ (@table_name || Support.snake_case(name)).to_sym
10
+ end
11
+
12
+ def table_name=(name)
13
+ @table_name = name
14
+ end
15
+
16
+ def content_type_id
17
+ @content_type_id || Support.snake_case(name)
18
+ end
19
+
20
+ def content_type_id=(ct_id)
21
+ @content_type_id = ct_id
22
+ end
23
+
24
+ def content_type_name
25
+ (@content_type_name || name)
26
+ end
27
+
28
+ def content_type_name=(name)
29
+ @content_type_name = name
30
+ end
31
+
32
+ def default_generator_options
33
+ {
34
+ table_name: table_name,
35
+ content_type_id: content_type_id,
36
+ class_name: name,
37
+ template: '{{content_type_id}}_{{index}}'
38
+ }
39
+ end
40
+
41
+ def id(id_generator_class, options = {})
42
+ @id_generator = id_generator_class.new(
43
+ default_generator_options.merge(options)
44
+ )
45
+ end
46
+
47
+ def display_field
48
+ @display_field || (fields.find do |f|
49
+ f[:type] == :string || f[:type] == :symbol
50
+ end || {})[:maps_to]
51
+ end
52
+
53
+ def display_field=(field_name)
54
+ @display_field = field_name
55
+ end
56
+
57
+ def fields
58
+ @fields || []
59
+ end
60
+
61
+ def relationship_fields
62
+ @fields.select { |f| resource?(f[:type]) }
63
+ end
64
+
65
+ def prepare_standard_field_options(database_name, options)
66
+ {
67
+ db_name: database_name,
68
+ maps_to: options.fetch(:maps_to, database_name),
69
+ name: options.fetch(:name, database_name),
70
+ type: options.fetch(:type),
71
+ pre_process: options.fetch(:pre_process, nil),
72
+ exclude_from_output: options.fetch(:exclude_from_output, false)
73
+ }
74
+ end
75
+
76
+ def prepare_field(database_name, options)
77
+ field = prepare_standard_field_options(database_name, options)
78
+ field[:item_type] = options.fetch(:item_type) if field[:type] == :array
79
+ fetch_relationship_options(
80
+ field,
81
+ options
82
+ ) if options.fetch(:relationship, false)
83
+
84
+ field
85
+ end
86
+
87
+ def field(database_name, options = {})
88
+ @fields ||= []
89
+ @fields << prepare_field(database_name, options)
90
+ end
91
+
92
+ def fetch_many_relationship_options(field, options)
93
+ field[:id_field] = options.fetch(:id_field)
94
+ field[:key] = options.fetch(:key)
95
+ end
96
+ alias fetch_one_relationship_options fetch_many_relationship_options
97
+
98
+ def fetch_through_relationship_options(field, options)
99
+ field[:through] = options.fetch(:through)
100
+ field[:primary_id_field] = options.fetch(:primary_id_field)
101
+ field[:foreign_id_field] = options.fetch(:foreign_id_field)
102
+ field[:primary_key] = options.fetch(:primary_key)
103
+ field[:foreign_key] = options.fetch(:foreign_key)
104
+ end
105
+
106
+ def fetch_relationship_options(field, options)
107
+ field[:relationship] = options.fetch(:relationship)
108
+
109
+ send(
110
+ "fetch_#{options.fetch(:relationship)}_relationship_options",
111
+ field,
112
+ options
113
+ )
114
+ end
115
+
116
+ def all
117
+ entries = []
118
+ rows = Contentful::DatabaseImporter.database[table_name].all
119
+ rows.each_with_index do |row, index|
120
+ entries << new(row, index)
121
+ end
122
+ entries
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,113 @@
1
+ module Contentful
2
+ module DatabaseImporter
3
+ # Coercion methods for Resource
4
+ module ResourceCoercions
5
+ def coerce(field_definition, value)
6
+ return if value.nil?
7
+
8
+ type = field_definition[:type]
9
+
10
+ return coerce_array(field_definition, value) if type == :array
11
+
12
+ send("coerce_#{type}".to_sym, value)
13
+ end
14
+
15
+ def coerce_symbol(value)
16
+ value.to_s
17
+ end
18
+ alias coerce_string coerce_symbol
19
+ alias coerce_text coerce_symbol
20
+
21
+ def coerce_number(value)
22
+ value.to_f
23
+ end
24
+
25
+ def coerce_integer(value)
26
+ value.to_i
27
+ end
28
+
29
+ def coerce_array(field_definition, value)
30
+ item_type = field_definition[:item_type]
31
+
32
+ raise "Can't coerce nested arrays" if item_type == :array
33
+
34
+ value = value.split(',').map(&:strip) if value.is_a?(::String)
35
+ value.map { |v| coerce({ type: item_type }, v) }
36
+ end
37
+
38
+ def coerce_hash_location(value)
39
+ {
40
+ lat: value.fetch(:lat, nil) || value.fetch(:latitude, nil),
41
+ lon: value.fetch(:lon, nil) || value.fetch(:longitude, nil)
42
+ }
43
+ end
44
+
45
+ def coerce_array_location(value)
46
+ {
47
+ lat: value[0],
48
+ lon: value[1]
49
+ }
50
+ end
51
+
52
+ def coerce_location(value)
53
+ return coerce_hash_location(value) if value.is_a?(::Hash)
54
+
55
+ return coerce_array_location(value) if value.is_a?(::Array)
56
+
57
+ if value.is_a?(::String) && value.include?(',')
58
+ parts = value.split(',').map(&:strip).map(&:to_f)
59
+ return coerce_array_location(parts)
60
+ end
61
+
62
+ raise "Can't coerce #{value} to Location"
63
+ end
64
+
65
+ def coerce_boolean(value)
66
+ # rubocop:disable Style/DoubleNegation
67
+ !!value
68
+ # rubocop:enable Style/DoubleNegation
69
+ end
70
+
71
+ def coerce_date(value)
72
+ case value
73
+ when Date, DateTime
74
+ value.iso8601
75
+ when String
76
+ value
77
+ else
78
+ raise "Can't coerce #{value} to ISO8601 Date"
79
+ end
80
+ end
81
+
82
+ def coerce_object(*)
83
+ raise 'Not yet supported by Contentful Bootstrap'
84
+ end
85
+
86
+ def create_associated_asset(name, value)
87
+ extension = value.split('.').last
88
+ associated_assets << {
89
+ id: Support.snake_case(name),
90
+ title: name,
91
+ file: {
92
+ filename: name,
93
+ url: value,
94
+ contentType: MimeMagic.by_extension(extension).type
95
+ }
96
+ }
97
+ end
98
+
99
+ def coerce_asset(value)
100
+ raise 'Only URL Strings supported for Assets' unless value.is_a?(String)
101
+
102
+ name = value.split('/').last.split('.').first
103
+
104
+ create_associated_asset(name, value)
105
+
106
+ {
107
+ linkType: 'Asset',
108
+ id: Support.snake_case(name)
109
+ }
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,69 @@
1
+ module Contentful
2
+ module DatabaseImporter
3
+ # Relationship methods for Resource
4
+ module ResourceRelationships
5
+ def fetch_relations(relationship_field_definition)
6
+ relations = [:many, :one, :through]
7
+ return send(
8
+ "fetch_#{relationship_field_definition[:relationship]}".to_sym,
9
+ relationship_field_definition
10
+ ) if relations.include?(relationship_field_definition[:relationship])
11
+
12
+ raise 'Invalid Relationship type'
13
+ end
14
+
15
+ def fetch_many(relationship_field_definition)
16
+ table_name = relationship_field_definition[:type].table_name
17
+ Contentful::DatabaseImporter.database[table_name].where(
18
+ relationship_field_definition[:key] =>
19
+ @raw[relationship_field_definition[:id_field]]
20
+ ).map do |row|
21
+ relationship_field_definition[:type].new(row).to_link
22
+ end
23
+ end
24
+
25
+ def fetch_one(relationship_field_definition)
26
+ table_name = relationship_field_definition[:type].table_name
27
+ row = Contentful::DatabaseImporter.database[table_name].where(
28
+ relationship_field_definition[:id_field] =>
29
+ @raw[relationship_field_definition[:key]]
30
+ ).first
31
+
32
+ return if row.nil?
33
+
34
+ relationship_field_definition[:type].new(row).to_link
35
+ end
36
+
37
+ def fetch_through_table_rows(relationship_field_definition)
38
+ through_table_name = relationship_field_definition[:through]
39
+
40
+ Contentful::DatabaseImporter.database[through_table_name].where(
41
+ relationship_field_definition[:primary_key] =>
42
+ @raw[relationship_field_definition[:primary_id_field]]
43
+ ).to_a
44
+ end
45
+
46
+ def resolve_through_relationship(through_row, field_definition)
47
+ table_name = field_definition[:type].table_name
48
+
49
+ related = Contentful::DatabaseImporter.database[table_name].where(
50
+ field_definition[:foreign_id_field] =>
51
+ through_row[field_definition[:foreign_key]]
52
+ ).first
53
+
54
+ field_definition[:type].new(related).to_link
55
+ end
56
+
57
+ def fetch_through(relationship_field_definition)
58
+ through = fetch_through_table_rows(relationship_field_definition)
59
+
60
+ through.map do |through_row|
61
+ resolve_through_relationship(
62
+ through_row,
63
+ relationship_field_definition
64
+ )
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,14 @@
1
+ module Contentful
2
+ module DatabaseImporter
3
+ # Helpers
4
+ module Support
5
+ def self.snake_case(string)
6
+ string.gsub(/::/, '/')
7
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
8
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
9
+ .tr('-', '_')
10
+ .downcase
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Contentful
2
+ module DatabaseImporter
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,59 @@
1
+ require 'contentful/database_importer/version'
2
+ require 'contentful/database_importer/config'
3
+ require 'contentful/database_importer/resource'
4
+ require 'contentful/database_importer/id_generator'
5
+ require 'contentful/database_importer/json_generator'
6
+ require 'contentful/bootstrap'
7
+ require 'tempfile'
8
+ require 'sequel'
9
+ require 'json'
10
+
11
+ # Top level space
12
+ module Contentful
13
+ # Database Importer Tool
14
+ module DatabaseImporter
15
+ def self.config
16
+ @config ||= Config.new
17
+ end
18
+
19
+ def self.setup
20
+ yield config if block_given?
21
+
22
+ raise 'Configuration is incomplete' unless config.complete?
23
+ end
24
+
25
+ def self.database
26
+ error = 'Database Configuration not found'
27
+ raise error if config.database_connection.nil?
28
+
29
+ @database ||= ::Sequel.connect(config.database_connection)
30
+ end
31
+
32
+ def self.generate_json
33
+ JsonGenerator.generate_json
34
+ end
35
+
36
+ def self.generate_json!
37
+ JsonGenerator.generate_json!
38
+ end
39
+
40
+ def self.run_bootstrap!(file, json)
41
+ file.write(json)
42
+ file.close
43
+
44
+ Contentful::Bootstrap::CommandRunner.new.create_space(
45
+ config.space_name,
46
+ json_template: file.path
47
+ )
48
+ ensure
49
+ file.unlink
50
+ end
51
+
52
+ def self.run!
53
+ json = generate_json!
54
+
55
+ file = Tempfile.new("import_#{config.space_name}")
56
+ run_bootstrap!(file, json)
57
+ end
58
+ end
59
+ end