contentful-database-importer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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