gooddata 0.6.0 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +13 -5
- data/.rubocop.yml +23 -0
- data/.travis.yml +9 -4
- data/CLI.md +439 -0
- data/Gemfile +0 -1
- data/README.md +2 -2
- data/Rakefile +60 -8
- data/doc/templates/default/module/setup.rb +1 -1
- data/examples.rb +2 -0
- data/gooddata +2 -0
- data/gooddata.gemspec +12 -8
- data/lib/gooddata.rb +0 -2
- data/lib/gooddata/bricks/base_downloader.rb +52 -47
- data/lib/gooddata/bricks/brick.rb +20 -31
- data/lib/gooddata/bricks/bricks.rb +1 -1
- data/lib/gooddata/bricks/middleware/base_middleware.rb +9 -7
- data/lib/gooddata/bricks/middleware/bench_middleware.rb +12 -10
- data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +28 -28
- data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +20 -16
- data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +21 -19
- data/lib/gooddata/bricks/middleware/logger_middleware.rb +10 -8
- data/lib/gooddata/bricks/middleware/restforce_middleware.rb +36 -34
- data/lib/gooddata/bricks/middleware/stdout_middleware.rb +11 -9
- data/lib/gooddata/bricks/middleware/twitter_middleware.rb +14 -12
- data/lib/gooddata/bricks/pipeline.rb +28 -0
- data/lib/gooddata/bricks/utils.rb +10 -8
- data/lib/gooddata/cli/cli.rb +1 -6
- data/lib/gooddata/cli/commands/auth_cmd.rb +1 -1
- data/lib/gooddata/cli/commands/console_cmd.rb +7 -5
- data/lib/gooddata/cli/commands/domain_cmd.rb +45 -0
- data/lib/gooddata/cli/commands/process_cmd.rb +42 -5
- data/lib/gooddata/cli/commands/project_cmd.rb +96 -36
- data/lib/gooddata/cli/commands/projects_cmd.rb +21 -0
- data/lib/gooddata/cli/commands/role_cmd.rb +28 -0
- data/lib/gooddata/cli/commands/run_ruby_cmd.rb +5 -5
- data/lib/gooddata/cli/commands/scaffold_cmd.rb +1 -1
- data/lib/gooddata/cli/commands/{profile_cmd.rb → user_cmd.rb} +7 -9
- data/lib/gooddata/cli/shared.rb +3 -2
- data/lib/gooddata/client.rb +16 -304
- data/lib/gooddata/commands/api.rb +13 -5
- data/lib/gooddata/commands/auth.rb +47 -40
- data/lib/gooddata/commands/base.rb +4 -2
- data/lib/gooddata/commands/commands.rb +1 -1
- data/lib/gooddata/commands/datasets.rb +20 -7
- data/lib/gooddata/commands/domain.rb +23 -0
- data/lib/gooddata/commands/process.rb +23 -117
- data/lib/gooddata/commands/project.rb +147 -0
- data/lib/gooddata/commands/projects.rb +8 -102
- data/lib/gooddata/commands/role.rb +26 -0
- data/lib/gooddata/commands/runners.rb +41 -38
- data/lib/gooddata/commands/scaffold.rb +46 -43
- data/lib/gooddata/commands/user.rb +33 -0
- data/lib/gooddata/connection.rb +43 -353
- data/lib/gooddata/core/connection.rb +389 -0
- data/lib/gooddata/core/core.rb +5 -4
- data/lib/gooddata/core/logging.rb +48 -0
- data/lib/gooddata/core/nil_logger.rb +13 -0
- data/lib/gooddata/core/project.rb +70 -0
- data/lib/gooddata/core/rest.rb +120 -0
- data/lib/gooddata/core/threaded.rb +14 -0
- data/lib/gooddata/core/user.rb +19 -0
- data/lib/gooddata/data/data.rb +2 -1
- data/lib/gooddata/data/guesser.rb +16 -12
- data/lib/gooddata/exceptions/command_failed.rb +1 -1
- data/lib/gooddata/exceptions/exceptions.rb +2 -1
- data/lib/gooddata/exceptions/no_project_error.rb +11 -0
- data/lib/gooddata/exceptions/project_not_found.rb +1 -1
- data/lib/gooddata/extensions/big_decimal.rb +6 -2
- data/lib/gooddata/extract.rb +10 -8
- data/lib/gooddata/goodzilla/goodzilla.rb +61 -59
- data/lib/gooddata/helpers.rb +15 -9
- data/lib/gooddata/models/account_settings.rb +124 -0
- data/lib/gooddata/models/attributes/anchor.rb +37 -0
- data/lib/gooddata/models/attributes/attributes.rb +8 -0
- data/lib/gooddata/models/attributes/date_attribute.rb +25 -0
- data/lib/gooddata/models/attributes/time_attribute.rb +24 -0
- data/lib/gooddata/models/columns/attribute.rb +71 -0
- data/lib/gooddata/models/columns/columns.rb +8 -0
- data/lib/gooddata/models/columns/date_column.rb +63 -0
- data/lib/gooddata/models/columns/fact_model.rb +54 -0
- data/lib/gooddata/models/columns/label.rb +55 -0
- data/lib/gooddata/models/columns/reference.rb +57 -0
- data/lib/gooddata/models/dashboard_builder.rb +26 -0
- data/lib/gooddata/models/data_result.rb +10 -9
- data/lib/gooddata/models/domain.rb +131 -0
- data/lib/gooddata/models/empty_result.rb +5 -8
- data/lib/gooddata/models/facts/facts.rb +8 -0
- data/lib/gooddata/models/facts/time_fact.rb +20 -0
- data/lib/gooddata/models/folders/attribute_folder.rb +20 -0
- data/lib/gooddata/models/folders/fact_folder.rb +20 -0
- data/lib/gooddata/models/folders/folders.rb +8 -0
- data/lib/gooddata/models/invitation.rb +78 -0
- data/lib/gooddata/models/links.rb +6 -6
- data/lib/gooddata/models/md_object.rb +25 -0
- data/lib/gooddata/models/metadata.rb +160 -62
- data/lib/gooddata/models/metadata/attribute.rb +81 -0
- data/lib/gooddata/models/metadata/column.rb +61 -0
- data/lib/gooddata/models/{dashboard.rb → metadata/dashboard.rb} +12 -7
- data/lib/gooddata/models/{data_set.rb → metadata/data_set.rb} +5 -4
- data/lib/gooddata/models/metadata/date_dimension.rb +26 -0
- data/lib/gooddata/models/metadata/display_form.rb +61 -0
- data/lib/gooddata/models/metadata/fact.rb +36 -0
- data/lib/gooddata/models/metadata/folder.rb +24 -0
- data/lib/gooddata/models/metadata/metadata.rb +8 -0
- data/lib/gooddata/models/metadata/metric.rb +197 -0
- data/lib/gooddata/models/metadata/report.rb +115 -0
- data/lib/gooddata/models/{report_definition.rb → metadata/report_definition.rb} +16 -10
- data/lib/gooddata/models/metadata/schema.rb +227 -0
- data/lib/gooddata/models/model.rb +38 -1339
- data/lib/gooddata/models/models.rb +5 -2
- data/lib/gooddata/models/module_constants.rb +29 -0
- data/lib/gooddata/models/process.rb +142 -13
- data/lib/gooddata/models/profile.rb +4 -6
- data/lib/gooddata/models/project.rb +406 -136
- data/lib/gooddata/models/project_blueprint.rb +221 -0
- data/lib/gooddata/models/project_builder.rb +136 -0
- data/lib/gooddata/models/project_creator.rb +138 -0
- data/lib/gooddata/models/project_metadata.rb +11 -10
- data/lib/gooddata/models/project_role.rb +92 -0
- data/lib/gooddata/models/references/date_reference.rb +44 -0
- data/lib/gooddata/models/references/references.rb +8 -0
- data/lib/gooddata/models/references/time_reference.rb +13 -0
- data/lib/gooddata/models/report_data_result.rb +11 -11
- data/lib/gooddata/models/schedule.rb +284 -0
- data/lib/gooddata/models/schema_blueprint.rb +158 -0
- data/lib/gooddata/models/schema_builder.rb +81 -0
- data/lib/gooddata/models/tab_builder.rb +23 -0
- data/lib/gooddata/models/user.rb +165 -0
- data/lib/gooddata/version.rb +1 -1
- data/lib/templates/project/data/devs.csv +1 -1
- data/lib/templates/project/data/repos.csv +1 -1
- data/lib/templates/project/model/model.rb.erb +7 -11
- data/spec/bricks/bricks_spec.rb +2 -0
- data/spec/data/test-ci-data.csv +2 -0
- data/spec/data/test_project_model_spec.json +7 -27
- data/spec/helpers/blueprint_helper.rb +2 -0
- data/spec/helpers/cli_helper.rb +2 -0
- data/spec/helpers/connection_helper.rb +14 -1
- data/spec/helpers/project_helper.rb +16 -0
- data/spec/helpers/schema_helper.rb +16 -0
- data/spec/integration/command_projects_spec.rb +7 -7
- data/spec/integration/create_from_template_spec.rb +2 -2
- data/spec/integration/full_project_spec.rb +160 -7
- data/spec/integration/partial_md_export_import_spec.rb +3 -3
- data/spec/logging_in_logging_out_spec.rb +2 -1
- data/spec/spec_helper.rb +26 -4
- data/spec/unit/bricks/bricks_spec.rb +15 -7
- data/spec/unit/bricks/middleware/bench_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/bulk_salesforce_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/gooddata_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/logger_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/restforce_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/stdout_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/twitter_middleware_spec.rb +2 -0
- data/spec/unit/cli/cli_spec.rb +2 -0
- data/spec/unit/cli/commands/cmd_api_spec.rb +23 -15
- data/spec/unit/cli/commands/cmd_auth_spec.rb +8 -4
- data/spec/unit/cli/commands/cmd_domain_spec.rb +82 -0
- data/spec/unit/cli/commands/cmd_process_spec.rb +29 -13
- data/spec/unit/cli/commands/cmd_project_spec.rb +51 -30
- data/spec/unit/cli/commands/cmd_role_spec.rb +44 -0
- data/spec/unit/cli/commands/cmd_run_ruby_spec.rb +8 -4
- data/spec/unit/cli/commands/cmd_scaffold_spec.rb +48 -11
- data/spec/unit/cli/commands/cmd_user_spec.rb +29 -0
- data/spec/unit/commands/command_api_spec.rb +1 -1
- data/spec/unit/commands/command_auth_spec.rb +100 -18
- data/spec/unit/commands/command_dataset_spec.rb +4 -0
- data/spec/unit/commands/command_process_spec.rb +9 -4
- data/spec/unit/commands/command_projects_spec.rb +10 -6
- data/spec/unit/commands/command_scaffold_spec.rb +5 -1
- data/spec/unit/commands/command_user_spec.rb +22 -0
- data/spec/unit/core/connection_spec.rb +35 -6
- data/spec/unit/core/logging_spec.rb +65 -0
- data/spec/unit/core/nil_logger_spec.rb +9 -0
- data/spec/unit/core/project_spec.rb +51 -0
- data/spec/unit/core/rest_spec.rb +33 -0
- data/spec/unit/data/guesser_spec.rb +5 -0
- data/spec/unit/godzilla/goodzilla_spec.rb +2 -0
- data/spec/unit/models/account_settings_spec.rb +28 -0
- data/spec/unit/models/anchor_spec.rb +32 -0
- data/spec/unit/models/attribute_column_spec.rb +7 -0
- data/spec/unit/models/domain_spec.rb +45 -0
- data/spec/unit/models/invitation_spec.rb +13 -0
- data/spec/unit/models/md_object_spec.rb +47 -0
- data/spec/unit/models/metric.rb +92 -0
- data/spec/unit/{model → models}/model_spec.rb +9 -7
- data/spec/unit/models/project_blueprint_spec.rb +202 -0
- data/spec/unit/models/project_creator.rb +73 -0
- data/spec/unit/models/project_role_spec.rb +90 -0
- data/spec/unit/models/project_spec.rb +143 -0
- data/spec/unit/models/schedule_spec.rb +491 -0
- data/spec/unit/{model → models}/schema_builder_spec.rb +2 -0
- data/spec/unit/{model → models}/tools_spec.rb +13 -7
- data/spec/unit/models/user_spec.rb +16 -0
- data/test/test_upload.rb +2 -0
- metadata +189 -86
- data/lib/gooddata/commands/profile.rb +0 -11
- data/lib/gooddata/models/attribute.rb +0 -29
- data/lib/gooddata/models/display_form.rb +0 -9
- data/lib/gooddata/models/fact.rb +0 -19
- data/lib/gooddata/models/metric.rb +0 -99
- data/lib/gooddata/models/report.rb +0 -89
- data/spec/data/blueprint_valid.json +0 -37
- data/spec/unit/cli/commands/cmd_profile_spec.rb +0 -16
- data/spec/unit/commands/command_profile_spec.rb +0 -18
- data/spec/unit/core/core_spec.rb +0 -7
- data/spec/unit/model/blueprint_spec.rb +0 -132
- data/spec/unit/model/project_blueprint_spec.rb +0 -44
@@ -0,0 +1,115 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require_relative '../metadata'
|
4
|
+
require_relative 'metadata'
|
5
|
+
|
6
|
+
module GoodData
|
7
|
+
class Report < GoodData::MdObject
|
8
|
+
root_key :report
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def [](id, options = {})
|
12
|
+
if id == :all
|
13
|
+
fail 'You have to specify a project ID' if GoodData.project.nil?
|
14
|
+
uri = GoodData.project.md['query'] + '/reports/'
|
15
|
+
GoodData.get(uri)['query']['entries']
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def create(options = {})
|
22
|
+
title = options[:title]
|
23
|
+
summary = options[:summary] || ''
|
24
|
+
rd = options[:rd] || ReportDefinition.create(:top => options[:top], :left => options[:left])
|
25
|
+
rd.save
|
26
|
+
|
27
|
+
report = {
|
28
|
+
'report' => {
|
29
|
+
'content' => {
|
30
|
+
'domains' => [],
|
31
|
+
'definitions' => [rd.uri]
|
32
|
+
},
|
33
|
+
'meta' => {
|
34
|
+
'tags' => '',
|
35
|
+
'deprecated' => '0',
|
36
|
+
'summary' => summary,
|
37
|
+
'title' => title
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
# TODO: write test for report definitions with explicit identifiers
|
42
|
+
report['report']['meta']['identifier'] = options[:identifier] if options[:identifier]
|
43
|
+
Report.new report
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def results
|
48
|
+
content['results']
|
49
|
+
end
|
50
|
+
|
51
|
+
def definitions
|
52
|
+
content['definitions']
|
53
|
+
end
|
54
|
+
|
55
|
+
def latest_report_definition_uri
|
56
|
+
definitions.last
|
57
|
+
end
|
58
|
+
|
59
|
+
def latest_report_definition
|
60
|
+
GoodData::MdObject[latest_report_definition_uri]
|
61
|
+
end
|
62
|
+
|
63
|
+
def remove_definition(definition)
|
64
|
+
def_uri = is_a?(GoodData::ReportDefinition) ? definition.uri : definition
|
65
|
+
content['definitions'] = definitions.reject { |x| x == def_uri }
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO: Cover with test. You would probably need something that will be able to create a report easily from a definition
|
70
|
+
def remove_definition_but_latest
|
71
|
+
to_remove = definitions - [latest_report_definition_uri]
|
72
|
+
to_remove.each do |uri|
|
73
|
+
remove_definition(uri)
|
74
|
+
end
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
def purge_report_of_unused_definitions!
|
79
|
+
full_list = definitions
|
80
|
+
remove_definition_but_latest
|
81
|
+
purged_list = definitions
|
82
|
+
to_remove = full_list - purged_list
|
83
|
+
save
|
84
|
+
to_remove.each { |uri| GoodData.delete(uri) }
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def execute
|
89
|
+
fail 'You have to save the report before executing. If you do not want to do that please use GoodData::ReportDefinition' unless saved?
|
90
|
+
result = GoodData.post '/gdc/xtab2/executor3', 'report_req' => { 'report' => uri }
|
91
|
+
data_result_uri = result['execResult']['dataResult']
|
92
|
+
result = GoodData.get data_result_uri
|
93
|
+
while result['taskState'] && result['taskState']['status'] == 'WAIT'
|
94
|
+
sleep 10
|
95
|
+
result = GoodData.get data_result_uri
|
96
|
+
end
|
97
|
+
ReportDataResult.new(GoodData.get data_result_uri)
|
98
|
+
end
|
99
|
+
|
100
|
+
def exportable?
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
def export(format)
|
105
|
+
result = GoodData.post('/gdc/xtab2/executor3', 'report_req' => { 'report' => uri })
|
106
|
+
result1 = GoodData.post('/gdc/exporter/executor', :result_req => { :format => format, :result => result })
|
107
|
+
png = GoodData.get(result1['uri'], :process => false)
|
108
|
+
while png.code == 202
|
109
|
+
sleep(1)
|
110
|
+
png = GoodData.get(result1['uri'], :process => false)
|
111
|
+
end
|
112
|
+
png
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
+
require_relative '../metadata'
|
3
4
|
require_relative 'metadata'
|
4
5
|
|
5
6
|
# GoodData Module
|
@@ -10,7 +11,7 @@ module GoodData
|
|
10
11
|
root_key :reportDefinition
|
11
12
|
|
12
13
|
class << self
|
13
|
-
def [](id)
|
14
|
+
def [](id, options = {})
|
14
15
|
if id == :all
|
15
16
|
uri = GoodData.project.md['query'] + '/reportdefinition/'
|
16
17
|
result = GoodData.get(uri)
|
@@ -22,7 +23,7 @@ module GoodData
|
|
22
23
|
|
23
24
|
def create_metrics_part(left, top)
|
24
25
|
stuff = Array(left) + Array(top)
|
25
|
-
stuff.select { |item| item.respond_to?(:
|
26
|
+
stuff.select { |item| item.respond_to?(:metric?) && item.metric? }.map do |metric|
|
26
27
|
create_metric_part(metric)
|
27
28
|
end
|
28
29
|
end
|
@@ -47,14 +48,14 @@ module GoodData
|
|
47
48
|
def create_part(stuff)
|
48
49
|
stuff = Array(stuff)
|
49
50
|
parts = stuff.reduce([]) do |memo, item|
|
50
|
-
if item.respond_to?(:
|
51
|
+
if item.respond_to?(:metric?) && item.metric?
|
51
52
|
memo
|
52
53
|
else
|
53
54
|
memo << create_attribute_part(item)
|
54
55
|
end
|
55
56
|
memo
|
56
57
|
end
|
57
|
-
if stuff.any? { |item| item.respond_to?(:
|
58
|
+
if stuff.any? { |item| item.respond_to?(:metric?) && item.metric? }
|
58
59
|
parts << 'metricGroup'
|
59
60
|
end
|
60
61
|
parts
|
@@ -62,7 +63,7 @@ module GoodData
|
|
62
63
|
|
63
64
|
def find(stuff)
|
64
65
|
stuff.map do |item|
|
65
|
-
if item.respond_to?(:
|
66
|
+
if item.respond_to?(:attribute?) && item.attribute?
|
66
67
|
item.display_forms.first
|
67
68
|
elsif item.is_a?(String)
|
68
69
|
x = GoodData::MdObject.get_by_id(item)
|
@@ -112,7 +113,7 @@ module GoodData
|
|
112
113
|
left = Array(options[:left])
|
113
114
|
top = Array(options[:top])
|
114
115
|
|
115
|
-
metrics = (left + top).select { |item| item.respond_to?(:
|
116
|
+
metrics = (left + top).select { |item| item.respond_to?(:metric?) && item.metric? }
|
116
117
|
|
117
118
|
unsaved_metrics = metrics.reject { |i| i.saved? }
|
118
119
|
unsaved_metrics.each { |m| m.title = 'Untitled metric' unless m.title }
|
@@ -120,7 +121,7 @@ module GoodData
|
|
120
121
|
begin
|
121
122
|
unsaved_metrics.each { |m| m.save }
|
122
123
|
rd = GoodData::ReportDefinition.create(options)
|
123
|
-
|
124
|
+
data_result(execute_inline(rd))
|
124
125
|
ensure
|
125
126
|
unsaved_metrics.each { |m| m.delete if m && m.saved? }
|
126
127
|
end
|
@@ -140,7 +141,11 @@ module GoodData
|
|
140
141
|
GoodData.post(uri, data)
|
141
142
|
end
|
142
143
|
|
143
|
-
|
144
|
+
# TODO: refactor the method. It should be instance method
|
145
|
+
# Method used for getting a data_result from a wire representation of
|
146
|
+
# @param result [Hash, Object] Wire data from JSON
|
147
|
+
# @return [GoodData::ReportDataResult]
|
148
|
+
def data_result(result)
|
144
149
|
data_result_uri = result['execResult']['dataResult']
|
145
150
|
result = GoodData.get data_result_uri
|
146
151
|
|
@@ -186,6 +191,8 @@ module GoodData
|
|
186
191
|
}
|
187
192
|
}
|
188
193
|
}
|
194
|
+
# TODO: write test for report definitions with explicit identifiers
|
195
|
+
pars['reportDefinition']['meta']['identifier'] = options[:identifier] if options[:identifier]
|
189
196
|
|
190
197
|
ReportDefinition.new(pars)
|
191
198
|
end
|
@@ -204,8 +211,7 @@ module GoodData
|
|
204
211
|
else
|
205
212
|
ReportDefinition.execute_inline(self)
|
206
213
|
end
|
207
|
-
|
208
|
-
get_data_result(result)
|
214
|
+
ReportDefinition.data_result(result)
|
209
215
|
end
|
210
216
|
end
|
211
217
|
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require_relative '../../helpers'
|
4
|
+
|
5
|
+
require_relative '../attributes/anchor'
|
6
|
+
require_relative '../columns/columns'
|
7
|
+
require_relative '../md_object'
|
8
|
+
|
9
|
+
module GoodData
|
10
|
+
module Model
|
11
|
+
##
|
12
|
+
# Server-side representation of a local data set; includes connection point,
|
13
|
+
# attributes and labels, facts, folders and corresponding pieces of physical
|
14
|
+
# model abstractions.
|
15
|
+
#
|
16
|
+
class Schema < MdObject
|
17
|
+
attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title, :anchor
|
18
|
+
|
19
|
+
def self.load(file)
|
20
|
+
Schema.new JSON.load(open(file))
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(a_config, a_name = 'Default Name', a_title = 'Default Title')
|
24
|
+
super()
|
25
|
+
@fields = []
|
26
|
+
@attributes = []
|
27
|
+
@facts = []
|
28
|
+
@folders = {
|
29
|
+
:facts => {},
|
30
|
+
:attributes => {}
|
31
|
+
}
|
32
|
+
@references = []
|
33
|
+
@labels = []
|
34
|
+
|
35
|
+
a_config[:name] = a_name unless a_config[:name]
|
36
|
+
a_config[:title] = a_config[:name] unless a_config[:title]
|
37
|
+
a_config[:title] = a_title unless a_config[:title]
|
38
|
+
a_config[:title] = a_config[:title].humanize
|
39
|
+
|
40
|
+
fail 'Schema name not specified' unless a_config[:name]
|
41
|
+
@name = a_config[:name]
|
42
|
+
@title = a_config[:title]
|
43
|
+
self.config = (a_config)
|
44
|
+
end
|
45
|
+
|
46
|
+
def config=(config)
|
47
|
+
config[:columns].each do |c|
|
48
|
+
case c[:type].to_s
|
49
|
+
when 'attribute'
|
50
|
+
add_attribute c
|
51
|
+
when 'fact'
|
52
|
+
add_fact c
|
53
|
+
when 'date'
|
54
|
+
add_date c
|
55
|
+
when 'anchor'
|
56
|
+
set_anchor c
|
57
|
+
when 'label'
|
58
|
+
add_label c
|
59
|
+
when 'reference'
|
60
|
+
add_reference c
|
61
|
+
else
|
62
|
+
fail "Unexpected type #{c[:type]} in #{c.inspect}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
@anchor = Anchor.new(nil, self) unless @anchor
|
66
|
+
end
|
67
|
+
|
68
|
+
def type_prefix
|
69
|
+
'dataset'
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Underlying fact table name
|
74
|
+
#
|
75
|
+
def table
|
76
|
+
@table ||= FACT_COLUMN_PREFIX + name
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Generates MAQL DDL script to drop this data set and included pieces
|
81
|
+
#
|
82
|
+
def to_maql_drop
|
83
|
+
maql = ''
|
84
|
+
[attributes, facts].each do |obj|
|
85
|
+
maql += obj.to_maql_drop
|
86
|
+
end
|
87
|
+
maql += "DROP {#{identifier}};\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Generates MAQL DDL script to create this data set and included pieces
|
92
|
+
#
|
93
|
+
def to_maql_create
|
94
|
+
# TODO: Use template (.erb)
|
95
|
+
maql = "# Create the '#{title}' data set\n"
|
96
|
+
maql += "CREATE DATASET {#{identifier}} VISUAL (TITLE \"#{title}\");\n\n"
|
97
|
+
[attributes, facts, { 1 => @anchor }].each do |objects|
|
98
|
+
objects.values.each do |obj|
|
99
|
+
maql += "# Create '#{obj.title}' and add it to the '#{title}' data set.\n"
|
100
|
+
maql += obj.to_maql_create
|
101
|
+
maql += "ALTER DATASET {#{identifier}} ADD {#{obj.identifier}};\n\n"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
labels.each do |label|
|
106
|
+
maql += "# Creating Labels\n"
|
107
|
+
maql += label.to_maql_create
|
108
|
+
end
|
109
|
+
|
110
|
+
references.values.each do |ref|
|
111
|
+
maql += "# Creating references\n"
|
112
|
+
maql += ref.to_maql_create
|
113
|
+
end
|
114
|
+
|
115
|
+
folders_maql = "# Create folders\n"
|
116
|
+
(folders[:attributes].values + folders[:facts].values).each { |folder| folders_maql += folder.to_maql_create }
|
117
|
+
folders_maql + "\n" + maql + "SYNCHRONIZE {#{identifier}};\n"
|
118
|
+
end
|
119
|
+
|
120
|
+
def upload(path, project = nil, mode = 'FULL')
|
121
|
+
if path =~ URI.regexp
|
122
|
+
Tempfile.open('remote_file') do |temp|
|
123
|
+
temp << open(path).read
|
124
|
+
temp.flush
|
125
|
+
upload_data(temp, mode)
|
126
|
+
end
|
127
|
+
else
|
128
|
+
upload_data(path, mode)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def upload_data(path, mode)
|
133
|
+
GoodData::Model.upload_data(path, to_manifest(mode))
|
134
|
+
end
|
135
|
+
|
136
|
+
# Generates the SLI manifest describing the data loading
|
137
|
+
#
|
138
|
+
def to_manifest(mode = 'FULL')
|
139
|
+
{
|
140
|
+
'dataSetSLIManifest' => {
|
141
|
+
'parts' => fields.reduce([]) do |memo, f|
|
142
|
+
val = f.to_manifest_part(mode)
|
143
|
+
memo << val unless val.nil?
|
144
|
+
memo
|
145
|
+
end,
|
146
|
+
'dataSet' => identifier,
|
147
|
+
'file' => 'data.csv', # should be configurable
|
148
|
+
'csvParams' => {
|
149
|
+
'quoteChar' => '"',
|
150
|
+
'escapeChar' => '"',
|
151
|
+
'separatorChar' => ',',
|
152
|
+
'endOfLine' => "\n"
|
153
|
+
}
|
154
|
+
}
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_wire_model
|
159
|
+
{
|
160
|
+
'dataset' => {
|
161
|
+
'identifier' => identifier,
|
162
|
+
'title' => title,
|
163
|
+
'anchor' => @anchor.to_wire_model,
|
164
|
+
'facts' => facts.map { |f| f.to_wire_model },
|
165
|
+
'attributes' => attributes.map { |a| a.to_wire_model },
|
166
|
+
'references' => references.map { |r| r.is_a?(DateReference) ? r.schema_ref : type_prefix + '.' + r.schema_ref }
|
167
|
+
} }
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def add_attribute(column)
|
173
|
+
attribute = Attribute.new column, self
|
174
|
+
fields << attribute
|
175
|
+
attributes << attribute
|
176
|
+
add_attribute_folder(attribute.folder)
|
177
|
+
# folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
|
178
|
+
end
|
179
|
+
|
180
|
+
def add_attribute_folder(name)
|
181
|
+
return if name.nil?
|
182
|
+
return if folders[:attributes].key?(name)
|
183
|
+
folders[:attributes][name] = AttributeFolder.new(name)
|
184
|
+
end
|
185
|
+
|
186
|
+
def add_fact(column)
|
187
|
+
fact = Fact.new column, self
|
188
|
+
fields << fact
|
189
|
+
facts << fact
|
190
|
+
add_fact_folder(fact.folder)
|
191
|
+
# folders[FactFolder.new(fact.folder)] = 1 if fact.folder
|
192
|
+
end
|
193
|
+
|
194
|
+
def add_fact_folder(name)
|
195
|
+
return if name.nil?
|
196
|
+
return if folders[:facts].key?(name)
|
197
|
+
folders[:facts][name] = FactFolder.new(name)
|
198
|
+
end
|
199
|
+
|
200
|
+
def add_label(column)
|
201
|
+
label = Label.new(column, nil, self)
|
202
|
+
labels << label
|
203
|
+
fields << label
|
204
|
+
end
|
205
|
+
|
206
|
+
def add_reference(column)
|
207
|
+
reference = Reference.new(column, self)
|
208
|
+
fields << reference
|
209
|
+
references << reference
|
210
|
+
end
|
211
|
+
|
212
|
+
def add_date(column)
|
213
|
+
date = DateColumn.new column, self
|
214
|
+
@fields << date
|
215
|
+
date.parts.values.each { |p| @fields << p }
|
216
|
+
date.facts.each { |f| facts << f }
|
217
|
+
date.attributes.each { |a| attributes << a }
|
218
|
+
date.references.each { |r| references << r }
|
219
|
+
end
|
220
|
+
|
221
|
+
def set_anchor(column) # rubocop:disable AccessorMethodName
|
222
|
+
@anchor = Anchor.new column, self
|
223
|
+
@fields << @anchor
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -1,9 +1,23 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
-
require_relative '../
|
4
|
-
|
3
|
+
require_relative '../core/connection'
|
4
|
+
require_relative '../core/rest'
|
5
|
+
|
6
|
+
require_relative 'attributes/attributes'
|
7
|
+
require_relative 'columns/columns'
|
8
|
+
require_relative 'facts/facts'
|
9
|
+
require_relative 'folders/folders'
|
10
|
+
require_relative 'metadata/metadata'
|
11
|
+
require_relative 'references/references'
|
12
|
+
|
13
|
+
require_relative 'links'
|
14
|
+
require_relative 'module_constants'
|
15
|
+
require_relative 'metadata/schema'
|
16
|
+
|
17
|
+
require 'fileutils'
|
18
|
+
require 'multi_json'
|
5
19
|
require 'open-uri'
|
6
|
-
require '
|
20
|
+
require 'zip'
|
7
21
|
|
8
22
|
##
|
9
23
|
# Module containing classes that counter-part GoodData server-side meta-data
|
@@ -11,49 +25,25 @@ require 'active_support/all'
|
|
11
25
|
#
|
12
26
|
module GoodData
|
13
27
|
module Model
|
14
|
-
# GoodData REST API categories
|
15
|
-
LDM_CTG = 'ldm'
|
16
|
-
LDM_MANAGE_CTG = 'ldm-manage'
|
17
|
-
|
18
|
-
# Model naming conventions
|
19
|
-
FIELD_PK = 'id'
|
20
|
-
FK_SUFFIX = '_id'
|
21
|
-
FACT_COLUMN_PREFIX = 'f_'
|
22
|
-
DATE_COLUMN_PREFIX = 'dt_'
|
23
|
-
TIME_COLUMN_PREFIX = 'tm_'
|
24
|
-
LABEL_COLUMN_PREFIX = 'nm_'
|
25
|
-
ATTRIBUTE_FOLDER_PREFIX = 'dim'
|
26
|
-
ATTRIBUTE_PREFIX = 'attr'
|
27
|
-
LABEL_PREFIX = 'label'
|
28
|
-
FACT_PREFIX = 'fact'
|
29
|
-
DATE_FACT_PREFIX = 'dt'
|
30
|
-
DATE_ATTRIBUTE = 'date'
|
31
|
-
DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM = 'mdyy'
|
32
|
-
TIME_FACT_PREFIX = 'tm.dt'
|
33
|
-
TIME_ATTRIBUTE_PREFIX = 'attr.time'
|
34
|
-
FACT_FOLDER_PREFIX = 'ffld'
|
35
|
-
|
36
|
-
SKIP_FIELD = false
|
37
|
-
|
38
28
|
class << self
|
39
|
-
def add_dataset(name, columns, project = nil)
|
40
|
-
|
41
|
-
|
42
|
-
end
|
29
|
+
# def add_dataset(name, columns, project = nil)
|
30
|
+
# Schema.new('columns' => columns, 'name' => name)
|
31
|
+
# add_schema(schema, project)
|
32
|
+
# end
|
43
33
|
|
44
|
-
def add_schema(schema, project = nil)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
34
|
+
# def add_schema(schema, project = nil)
|
35
|
+
# unless schema.respond_to?(:to_maql_create) || schema.is_a?(String)
|
36
|
+
# fail(ArgumentError, "Schema object or schema file path expected, got '#{schema}'")
|
37
|
+
# end
|
38
|
+
# schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
|
39
|
+
# project = GoodData.project unless project
|
40
|
+
# ldm_links = GoodData.get project.md[LDM_CTG]
|
41
|
+
# ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
|
42
|
+
# GoodData.post ldm_uri, 'manage' => { 'maql' => schema.to_maql_create }
|
43
|
+
# end
|
54
44
|
|
55
45
|
# Load given file into a data set described by the given schema
|
56
|
-
def upload_data(path, manifest, options={})
|
46
|
+
def upload_data(path, manifest, options = {})
|
57
47
|
project = options[:project] || GoodData.project
|
58
48
|
# mode = options[:mode] || "FULL"
|
59
49
|
path = path.path if path.respond_to? :path
|
@@ -63,7 +53,7 @@ module GoodData
|
|
63
53
|
dir = Dir.mktmpdir
|
64
54
|
begin
|
65
55
|
Zip::File.open("#{dir}/upload.zip", Zip::File::CREATE) do |zip|
|
66
|
-
# TODO make sure schema columns match CSV column names
|
56
|
+
# TODO: make sure schema columns match CSV column names
|
67
57
|
zip.get_output_stream('upload_info.json') { |f| f.puts JSON.pretty_generate(manifest) }
|
68
58
|
if inline_data
|
69
59
|
zip.get_output_stream('data.csv') do |f|
|
@@ -83,10 +73,11 @@ module GoodData
|
|
83
73
|
end
|
84
74
|
|
85
75
|
# kick the load
|
86
|
-
pull = {'pullIntegration' => File.basename(dir)}
|
76
|
+
pull = { 'pullIntegration' => File.basename(dir) }
|
87
77
|
link = project.md.links('etl')['pull']
|
88
78
|
task = GoodData.post link, pull
|
89
|
-
|
79
|
+
# TODO: Refactor the task status out
|
80
|
+
while GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'RUNNING' || GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'PREPARED'
|
90
81
|
sleep 30
|
91
82
|
end
|
92
83
|
if GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'ERROR'
|
@@ -103,1302 +94,10 @@ module GoodData
|
|
103
94
|
d = Marshal.load(Marshal.dump(a_schema_blueprint))
|
104
95
|
d[:columns] = d[:columns] + b_schema_blueprint[:columns]
|
105
96
|
d[:columns].uniq!
|
106
|
-
columns_that_failed_to_merge = d[:columns].group_by { |x| x[:name] }.map { |k, v| [k, v.count] }.
|
97
|
+
columns_that_failed_to_merge = d[:columns].group_by { |x| x[:name] }.map { |k, v| [k, v.count] }.select { |x| x[1] > 1 }
|
107
98
|
fail "Columns #{columns_that_failed_to_merge} failed to merge. When merging columns with the same name they have to be identical." unless columns_that_failed_to_merge.empty?
|
108
99
|
d
|
109
100
|
end
|
110
101
|
end
|
111
|
-
|
112
|
-
class ProjectBlueprint
|
113
|
-
attr_accessor :data
|
114
|
-
|
115
|
-
def self.from_json(spec)
|
116
|
-
if spec.is_a?(String)
|
117
|
-
ProjectBlueprint.new(MultiJson.load(File.read(spec), :symbolize_keys => true))
|
118
|
-
else
|
119
|
-
ProjectBlueprint.new(spec)
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def change(&block)
|
124
|
-
builder = ProjectBuilder.create_from_data(self)
|
125
|
-
block.call(builder)
|
126
|
-
builder
|
127
|
-
@data = builder.to_hash
|
128
|
-
self
|
129
|
-
end
|
130
|
-
|
131
|
-
def datasets
|
132
|
-
data[:datasets].map { |d| SchemaBlueprint.new(d) }
|
133
|
-
end
|
134
|
-
|
135
|
-
def add_dataset(a_dataset, index=nil)
|
136
|
-
if index.nil? || index > datasets.length
|
137
|
-
data[:datasets] << a_dataset.to_hash
|
138
|
-
else
|
139
|
-
data[:datasets].insert(index, a_dataset.to_hash)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def remove_dataset(dataset_name)
|
144
|
-
x = data[:datasets].find { |d| d[:name] == dataset_name }
|
145
|
-
index = data[:datasets].index(x)
|
146
|
-
data[:datasets].delete_at(index)
|
147
|
-
end
|
148
|
-
|
149
|
-
def date_dimensions
|
150
|
-
data[:date_dimensions]
|
151
|
-
end
|
152
|
-
|
153
|
-
def get_dataset(name)
|
154
|
-
ds = data[:datasets].find { |d| d[:name] == name }
|
155
|
-
SchemaBlueprint.new(ds) unless ds.nil?
|
156
|
-
end
|
157
|
-
|
158
|
-
def initialize(init_data)
|
159
|
-
@data = init_data
|
160
|
-
end
|
161
|
-
|
162
|
-
def model_validate
|
163
|
-
if datasets.count == 1
|
164
|
-
[]
|
165
|
-
else
|
166
|
-
x = datasets.reduce([]) { |memo, schema| schema.has_anchor? ? memo << [schema.name, schema.anchor[:name]] : memo }
|
167
|
-
refs = datasets.reduce([]) do |memo, dataset|
|
168
|
-
memo.concat(dataset.references)
|
169
|
-
end
|
170
|
-
refs.reduce([]) do |memo, ref|
|
171
|
-
x.include?([ref[:dataset], ref[:reference]]) ? memo : memo.concat([ref])
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def model_valid?
|
177
|
-
errors = model_validate
|
178
|
-
errors.empty? ? true : false
|
179
|
-
end
|
180
|
-
|
181
|
-
def merge!(a_blueprint)
|
182
|
-
temp_blueprint = dup
|
183
|
-
a_blueprint.datasets.each do |dataset|
|
184
|
-
local_dataset = temp_blueprint.get_dataset(dataset.name)
|
185
|
-
if local_dataset.nil?
|
186
|
-
temp_blueprint.add_dataset(dataset.dup)
|
187
|
-
else
|
188
|
-
index = temp_blueprint.datasets.index(local_dataset)
|
189
|
-
local_dataset.merge!(dataset)
|
190
|
-
temp_blueprint.remove_dataset(local_dataset.name)
|
191
|
-
temp_blueprint.add_dataset(local_dataset, index)
|
192
|
-
end
|
193
|
-
end
|
194
|
-
@data = temp_blueprint.data
|
195
|
-
self
|
196
|
-
end
|
197
|
-
|
198
|
-
def dup
|
199
|
-
deep_copy = Marshal.load(Marshal.dump(data))
|
200
|
-
ProjectBlueprint.new(deep_copy)
|
201
|
-
end
|
202
|
-
|
203
|
-
def title
|
204
|
-
data[:title]
|
205
|
-
end
|
206
|
-
|
207
|
-
def to_wire_model
|
208
|
-
{
|
209
|
-
'diffRequest' => {
|
210
|
-
'targetModel' => {
|
211
|
-
'projectModel' => {
|
212
|
-
'datasets' => datasets.map { |d| d.to_wire_model },
|
213
|
-
'dateDimensions' => date_dimensions.map { |d|
|
214
|
-
{
|
215
|
-
'dateDimension' => {
|
216
|
-
'name' => d[:name],
|
217
|
-
'title' => d[:title] || d[:name].humanize
|
218
|
-
}
|
219
|
-
} }
|
220
|
-
}}}}
|
221
|
-
end
|
222
|
-
|
223
|
-
def to_hash
|
224
|
-
@data
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
class SchemaBlueprint
|
229
|
-
attr_accessor :data
|
230
|
-
|
231
|
-
def change(&block)
|
232
|
-
builder = SchemaBuilder.create_from_data(self)
|
233
|
-
block.call(builder)
|
234
|
-
builder
|
235
|
-
@data = builder.to_hash
|
236
|
-
self
|
237
|
-
end
|
238
|
-
|
239
|
-
def initialize(init_data)
|
240
|
-
@data = init_data
|
241
|
-
end
|
242
|
-
|
243
|
-
def upload(source, options={})
|
244
|
-
project = options[:project] || GoodData.project
|
245
|
-
fail 'You have to specify a project into which you want to load.' if project.nil?
|
246
|
-
mode = options[:load] || 'FULL'
|
247
|
-
project.upload(source, to_schema, mode)
|
248
|
-
end
|
249
|
-
|
250
|
-
def merge!(a_blueprint)
|
251
|
-
new_blueprint = GoodData::Model.merge_dataset_columns(self, a_blueprint)
|
252
|
-
@data = new_blueprint
|
253
|
-
self
|
254
|
-
end
|
255
|
-
|
256
|
-
def name
|
257
|
-
data[:name]
|
258
|
-
end
|
259
|
-
|
260
|
-
def title
|
261
|
-
data[:title]
|
262
|
-
end
|
263
|
-
|
264
|
-
def to_hash
|
265
|
-
data
|
266
|
-
end
|
267
|
-
|
268
|
-
def columns
|
269
|
-
data[:columns]
|
270
|
-
end
|
271
|
-
|
272
|
-
def has_anchor?
|
273
|
-
columns.any? { |c| c[:type].to_s == 'anchor' }
|
274
|
-
end
|
275
|
-
|
276
|
-
def anchor
|
277
|
-
find_column_by_type(:anchor, :first)
|
278
|
-
end
|
279
|
-
|
280
|
-
def references
|
281
|
-
find_column_by_type(:reference)
|
282
|
-
end
|
283
|
-
|
284
|
-
def attributes
|
285
|
-
find_column_by_type(:attribute)
|
286
|
-
end
|
287
|
-
|
288
|
-
def facts
|
289
|
-
find_column_by_type(:fact)
|
290
|
-
end
|
291
|
-
|
292
|
-
def find_column_by_type(type, all=:all)
|
293
|
-
type = type.to_s
|
294
|
-
if all == :all
|
295
|
-
columns.find_all { |c| c[:type].to_s == type }
|
296
|
-
else
|
297
|
-
columns.find { |c| c[:type].to_s == type }
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
def find_column_by_name(type, all=:all)
|
302
|
-
type = type.to_s
|
303
|
-
if all == :all
|
304
|
-
columns.find_all { |c| c[:name].to_s == type }
|
305
|
-
else
|
306
|
-
columns.find { |c| c[:name].to_s == type }
|
307
|
-
end
|
308
|
-
end
|
309
|
-
|
310
|
-
def to_schema
|
311
|
-
Schema.new(to_hash)
|
312
|
-
end
|
313
|
-
|
314
|
-
def to_manifest
|
315
|
-
to_schema.to_manifest
|
316
|
-
end
|
317
|
-
|
318
|
-
def pretty_print(printer)
|
319
|
-
printer.text "Schema <#{object_id}>:\n"
|
320
|
-
printer.text " Name: #{name}\n"
|
321
|
-
printer.text " Columns: \n"
|
322
|
-
printer.text columns.map { |c| " #{c[:name]}: #{c[:type]}" }.join("\n")
|
323
|
-
end
|
324
|
-
|
325
|
-
def dup
|
326
|
-
deep_copy = Marshal.load(Marshal.dump(data))
|
327
|
-
SchemaBlueprint.new(deep_copy)
|
328
|
-
end
|
329
|
-
|
330
|
-
def to_wire_model
|
331
|
-
to_schema.to_wire_model
|
332
|
-
end
|
333
|
-
|
334
|
-
def ==(other)
|
335
|
-
to_hash == other.to_hash
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
class ProjectBuilder
|
340
|
-
attr_reader :title, :datasets, :reports, :metrics, :uploads, :users, :assert_report, :date_dimensions
|
341
|
-
|
342
|
-
class << self
|
343
|
-
def create_from_data(blueprint, title = 'Title')
|
344
|
-
pb = ProjectBuilder.new(title)
|
345
|
-
pb.data = blueprint.to_hash
|
346
|
-
pb
|
347
|
-
end
|
348
|
-
|
349
|
-
def create(title, options={}, &block)
|
350
|
-
pb = ProjectBuilder.new(title)
|
351
|
-
block.call(pb)
|
352
|
-
pb
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
def initialize(title)
|
357
|
-
@title = title
|
358
|
-
@datasets = []
|
359
|
-
@reports = []
|
360
|
-
@assert_tests = []
|
361
|
-
@metrics = []
|
362
|
-
@uploads = []
|
363
|
-
@users = []
|
364
|
-
@dashboards = []
|
365
|
-
@date_dimensions = []
|
366
|
-
end
|
367
|
-
|
368
|
-
def add_date_dimension(name, options = {})
|
369
|
-
dimension = {
|
370
|
-
urn: options[:urn],
|
371
|
-
name: name,
|
372
|
-
title: options[:title]
|
373
|
-
}
|
374
|
-
|
375
|
-
@date_dimensions << dimension
|
376
|
-
end
|
377
|
-
|
378
|
-
def add_dataset(name, &block)
|
379
|
-
builder = GoodData::Model::SchemaBuilder.new(name)
|
380
|
-
block.call(builder)
|
381
|
-
if @datasets.any? { |item| item[:name] == name }
|
382
|
-
ds = @datasets.find { |item| item[:name] == name }
|
383
|
-
index = @datasets.index(ds)
|
384
|
-
stuff = GoodData::Model.merge_dataset_columns(ds, builder.to_hash)
|
385
|
-
@datasets.delete_at(index)
|
386
|
-
@datasets.insert(index, stuff)
|
387
|
-
else
|
388
|
-
@datasets << builder.to_hash
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
def add_report(title, options={})
|
393
|
-
@reports << {:title => title}.merge(options)
|
394
|
-
end
|
395
|
-
|
396
|
-
def add_metric(title, options={})
|
397
|
-
@metrics << {:title => title}.merge(options)
|
398
|
-
end
|
399
|
-
|
400
|
-
def add_dashboard(title, &block)
|
401
|
-
db = DashboardBuilder.new(title)
|
402
|
-
block.call(db)
|
403
|
-
@dashboards << db.to_hash
|
404
|
-
end
|
405
|
-
|
406
|
-
def load_metrics(file)
|
407
|
-
new_metrics = MultiJson.load(open(file).read, :symbolize_keys => true)
|
408
|
-
@metrics = @metrics + new_metrics
|
409
|
-
end
|
410
|
-
|
411
|
-
def load_datasets(file)
|
412
|
-
new_metrics = MultiJson.load(open(file).read, :symbolize_keys => true)
|
413
|
-
@datasets = @datasets + new_metrics
|
414
|
-
end
|
415
|
-
|
416
|
-
def assert_report(report, result)
|
417
|
-
@assert_tests << {:report => report, :result => result}
|
418
|
-
end
|
419
|
-
|
420
|
-
def upload(data, options={})
|
421
|
-
mode = options[:mode] || 'FULL'
|
422
|
-
dataset = options[:dataset]
|
423
|
-
@uploads << {
|
424
|
-
:source => data,
|
425
|
-
:mode => mode,
|
426
|
-
:dataset => dataset
|
427
|
-
}
|
428
|
-
end
|
429
|
-
|
430
|
-
def add_users(users)
|
431
|
-
@users << users
|
432
|
-
end
|
433
|
-
|
434
|
-
def to_json(options={})
|
435
|
-
eliminate_empty = options[:eliminate_empty] || false
|
436
|
-
|
437
|
-
if eliminate_empty
|
438
|
-
JSON.pretty_generate(to_hash.reject { |k, v| v.is_a?(Enumerable) && v.empty? })
|
439
|
-
else
|
440
|
-
JSON.pretty_generate(to_hash)
|
441
|
-
end
|
442
|
-
end
|
443
|
-
|
444
|
-
def to_hash
|
445
|
-
{
|
446
|
-
:title => @title,
|
447
|
-
:datasets => @datasets,
|
448
|
-
:uploads => @uploads,
|
449
|
-
:dashboards => @dashboards,
|
450
|
-
:metrics => @metrics,
|
451
|
-
:reports => @reports,
|
452
|
-
:users => @users,
|
453
|
-
:assert_tests => @assert_tests,
|
454
|
-
:date_dimensions => @date_dimensions
|
455
|
-
}
|
456
|
-
end
|
457
|
-
|
458
|
-
def get_dataset(name)
|
459
|
-
datasets.find { |d| d.name == name }
|
460
|
-
end
|
461
|
-
end
|
462
|
-
|
463
|
-
class DashboardBuilder
|
464
|
-
def initialize(title)
|
465
|
-
@title = title
|
466
|
-
@tabs = []
|
467
|
-
end
|
468
|
-
|
469
|
-
def add_tab(tab, &block)
|
470
|
-
tb = TabBuilder.new(tab)
|
471
|
-
block.call(tb)
|
472
|
-
@tabs << tb
|
473
|
-
tb
|
474
|
-
end
|
475
|
-
|
476
|
-
def to_hash
|
477
|
-
{
|
478
|
-
:name => @name,
|
479
|
-
:tabs => @tabs.map { |tab| tab.to_hash }
|
480
|
-
}
|
481
|
-
end
|
482
|
-
end
|
483
|
-
|
484
|
-
class TabBuilder
|
485
|
-
def initialize(title)
|
486
|
-
@title = title
|
487
|
-
@stuff = []
|
488
|
-
end
|
489
|
-
|
490
|
-
def add_report(options={})
|
491
|
-
@stuff << {:type => :report}.merge(options)
|
492
|
-
end
|
493
|
-
|
494
|
-
def to_hash
|
495
|
-
{
|
496
|
-
:title => @title,
|
497
|
-
:items => @stuff
|
498
|
-
}
|
499
|
-
end
|
500
|
-
end
|
501
|
-
|
502
|
-
class SchemaBuilder
|
503
|
-
attr_accessor :data
|
504
|
-
|
505
|
-
class << self
|
506
|
-
def create_from_data(blueprint)
|
507
|
-
sc = SchemaBuilder.new
|
508
|
-
sc.data = blueprint.to_hash
|
509
|
-
sc
|
510
|
-
end
|
511
|
-
end
|
512
|
-
|
513
|
-
def initialize(name=nil)
|
514
|
-
@data = {
|
515
|
-
:name => name,
|
516
|
-
:columns => []
|
517
|
-
}
|
518
|
-
end
|
519
|
-
|
520
|
-
def name
|
521
|
-
data[:name]
|
522
|
-
end
|
523
|
-
|
524
|
-
def columns
|
525
|
-
data[:columns]
|
526
|
-
end
|
527
|
-
|
528
|
-
def add_column(column_def)
|
529
|
-
columns.push(column_def)
|
530
|
-
self
|
531
|
-
end
|
532
|
-
|
533
|
-
def add_anchor(name, options={})
|
534
|
-
add_column({:type => :anchor, :name => name}.merge(options))
|
535
|
-
self
|
536
|
-
end
|
537
|
-
|
538
|
-
def add_attribute(name, options={})
|
539
|
-
add_column({:type => :attribute, :name => name}.merge(options))
|
540
|
-
self
|
541
|
-
end
|
542
|
-
|
543
|
-
def add_fact(name, options={})
|
544
|
-
add_column({:type => :fact, :name => name}.merge(options))
|
545
|
-
self
|
546
|
-
end
|
547
|
-
|
548
|
-
def add_label(name, options={})
|
549
|
-
add_column({:type => :label, :name => name}.merge(options))
|
550
|
-
self
|
551
|
-
end
|
552
|
-
|
553
|
-
def add_date(name, options={})
|
554
|
-
add_column({:type => :date, :name => name}.merge(options))
|
555
|
-
end
|
556
|
-
|
557
|
-
def add_reference(name, options={})
|
558
|
-
add_column({:type => :reference, :name => name}.merge(options))
|
559
|
-
end
|
560
|
-
|
561
|
-
def to_json
|
562
|
-
JSON.pretty_generate(to_hash)
|
563
|
-
end
|
564
|
-
|
565
|
-
def to_hash
|
566
|
-
data
|
567
|
-
end
|
568
|
-
|
569
|
-
def to_schema
|
570
|
-
Schema.new(to_hash)
|
571
|
-
end
|
572
|
-
end
|
573
|
-
|
574
|
-
class ProjectCreator
|
575
|
-
class << self
|
576
|
-
def migrate(options={})
|
577
|
-
spec = options[:spec] || fail('You need to provide spec for migration')
|
578
|
-
spec = spec.to_hash
|
579
|
-
|
580
|
-
token = options[:token]
|
581
|
-
project = options[:project] || GoodData::Project.create(:title => spec[:title], :auth_token => token)
|
582
|
-
fail('You need to specify token for project creation') if token.nil? && project.nil?
|
583
|
-
|
584
|
-
begin
|
585
|
-
GoodData.with_project(project) do |p|
|
586
|
-
# migrate_date_dimensions(p, spec[:date_dimensions] || [])
|
587
|
-
migrate_datasets(p, spec)
|
588
|
-
load(p, spec)
|
589
|
-
migrate_metrics(p, spec[:metrics] || [])
|
590
|
-
migrate_reports(p, spec[:reports] || [])
|
591
|
-
migrate_dashboards(p, spec[:dashboards] || [])
|
592
|
-
migrate_users(p, spec[:users] || [])
|
593
|
-
execute_tests(p, spec[:assert_tests] || [])
|
594
|
-
p
|
595
|
-
end
|
596
|
-
end
|
597
|
-
end
|
598
|
-
|
599
|
-
def migrate_date_dimensions(project, spec)
|
600
|
-
spec.each do |dd|
|
601
|
-
Model.add_schema(DateDimension.new(dd), project)
|
602
|
-
end
|
603
|
-
end
|
604
|
-
|
605
|
-
def migrate_datasets(project, spec)
|
606
|
-
bp = ProjectBlueprint.new(spec)
|
607
|
-
# schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
|
608
|
-
# project = GoodData.project unless project
|
609
|
-
uri = "/gdc/projects/#{GoodData.project.pid}/model/diff"
|
610
|
-
result = GoodData.post(uri, bp.to_wire_model)
|
611
|
-
link = result['asyncTask']['link']['poll']
|
612
|
-
response = GoodData.get(link, :process => false)
|
613
|
-
# pp response
|
614
|
-
while response.code != 200
|
615
|
-
sleep 1
|
616
|
-
GoodData.connection.retryable(:tries => 3, :on => RestClient::InternalServerError) do
|
617
|
-
sleep 1
|
618
|
-
response = GoodData.get(link, :process => false)
|
619
|
-
# pp response
|
620
|
-
end
|
621
|
-
end
|
622
|
-
response = GoodData.get(link)
|
623
|
-
ldm_links = GoodData.get project.md[LDM_CTG]
|
624
|
-
ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
|
625
|
-
chunks = response['projectModelDiff']['updateScripts'].find_all { |script| script['updateScript']['preserveData'] == true && script['updateScript']['cascadeDrops'] == false }.map { |x| x['updateScript']['maqlDdlChunks'] }.flatten
|
626
|
-
chunks.each do |chunk|
|
627
|
-
GoodData.post ldm_uri, {'manage' => {'maql' => chunk}}
|
628
|
-
end
|
629
|
-
|
630
|
-
bp.datasets.each do |ds|
|
631
|
-
schema = ds.to_schema
|
632
|
-
GoodData::ProjectMetadata["manifest_#{schema.name}"] = schema.to_manifest.to_json
|
633
|
-
end
|
634
|
-
end
|
635
|
-
|
636
|
-
def migrate_reports(project, spec)
|
637
|
-
spec.each do |report|
|
638
|
-
project.add_report(report)
|
639
|
-
end
|
640
|
-
end
|
641
|
-
|
642
|
-
def migrate_dashboards(project, spec)
|
643
|
-
spec.each do |dash|
|
644
|
-
project.add_dashboard(dash)
|
645
|
-
end
|
646
|
-
end
|
647
|
-
|
648
|
-
def migrate_metrics(project, spec)
|
649
|
-
spec.each do |metric|
|
650
|
-
project.add_metric(metric)
|
651
|
-
end
|
652
|
-
end
|
653
|
-
|
654
|
-
def migrate_users(project, spec)
|
655
|
-
spec.each do |user|
|
656
|
-
puts "Would migrate user #{user}"
|
657
|
-
# project.add_user(user)
|
658
|
-
end
|
659
|
-
end
|
660
|
-
|
661
|
-
def load(project, spec)
|
662
|
-
if spec.has_key?(:uploads)
|
663
|
-
spec[:uploads].each do |load|
|
664
|
-
schema = GoodData::Model::Schema.new(spec[:datasets].detect { |d| d[:name] == load[:dataset] })
|
665
|
-
project.upload(load[:source], schema, load[:mode])
|
666
|
-
end
|
667
|
-
end
|
668
|
-
end
|
669
|
-
|
670
|
-
def execute_tests(project, spec)
|
671
|
-
spec.each do |assert|
|
672
|
-
result = GoodData::ReportDefinition.execute(assert[:report])
|
673
|
-
fail "Test did not pass. Got #{result.table.inspect}, expected #{assert[:result].inspect}" if result.table != assert[:result]
|
674
|
-
end
|
675
|
-
end
|
676
|
-
end
|
677
|
-
end
|
678
|
-
|
679
|
-
class MdObject
|
680
|
-
attr_accessor :name, :title
|
681
|
-
|
682
|
-
def visual
|
683
|
-
"TITLE \"#{title_esc}\""
|
684
|
-
end
|
685
|
-
|
686
|
-
def title_esc
|
687
|
-
title.gsub(/"/, "\\\"")
|
688
|
-
end
|
689
|
-
|
690
|
-
##
|
691
|
-
# Generates an identifier from the object name by transliterating
|
692
|
-
# non-Latin character and then dropping non-alphanumerical characters.
|
693
|
-
#
|
694
|
-
def identifier
|
695
|
-
@identifier ||= "#{self.type_prefix}.#{name}"
|
696
|
-
end
|
697
|
-
end
|
698
|
-
|
699
|
-
##
|
700
|
-
# Server-side representation of a local data set; includes connection point,
|
701
|
-
# attributes and labels, facts, folders and corresponding pieces of physical
|
702
|
-
# model abstractions.
|
703
|
-
#
|
704
|
-
class Schema < MdObject
|
705
|
-
attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title, :anchor
|
706
|
-
|
707
|
-
def self.load(file)
|
708
|
-
Schema.new JSON.load(open(file))
|
709
|
-
end
|
710
|
-
|
711
|
-
def initialize(config, name = 'Default Name', title = 'Default Title')
|
712
|
-
super()
|
713
|
-
@fields = []
|
714
|
-
@attributes = []
|
715
|
-
@facts = []
|
716
|
-
@folders = {
|
717
|
-
:facts => {},
|
718
|
-
:attributes => {}
|
719
|
-
}
|
720
|
-
@references = []
|
721
|
-
@labels = []
|
722
|
-
|
723
|
-
config[:name] = name unless config[:name]
|
724
|
-
config[:title] = config[:name] unless config[:title]
|
725
|
-
config[:title] = title unless config[:title]
|
726
|
-
config[:title] = config[:title].humanize
|
727
|
-
|
728
|
-
fail 'Schema name not specified' unless config[:name]
|
729
|
-
self.name = config[:name]
|
730
|
-
self.title = config[:title]
|
731
|
-
self.config = config
|
732
|
-
end
|
733
|
-
|
734
|
-
def config=(config)
|
735
|
-
config[:columns].each do |c|
|
736
|
-
case c[:type].to_s
|
737
|
-
when 'attribute'
|
738
|
-
add_attribute c
|
739
|
-
when 'fact'
|
740
|
-
add_fact c
|
741
|
-
when 'date'
|
742
|
-
add_date c
|
743
|
-
when 'anchor'
|
744
|
-
set_anchor c
|
745
|
-
when 'label'
|
746
|
-
add_label c
|
747
|
-
when 'reference'
|
748
|
-
add_reference c
|
749
|
-
else
|
750
|
-
fail "Unexpected type #{c[:type]} in #{c.inspect}"
|
751
|
-
end
|
752
|
-
end
|
753
|
-
@anchor = Anchor.new(nil, self) unless @anchor
|
754
|
-
end
|
755
|
-
|
756
|
-
def type_prefix
|
757
|
-
'dataset'
|
758
|
-
end
|
759
|
-
|
760
|
-
##
|
761
|
-
# Underlying fact table name
|
762
|
-
#
|
763
|
-
def table
|
764
|
-
@table ||= FACT_COLUMN_PREFIX + name
|
765
|
-
end
|
766
|
-
|
767
|
-
##
|
768
|
-
# Generates MAQL DDL script to drop this data set and included pieces
|
769
|
-
#
|
770
|
-
def to_maql_drop
|
771
|
-
maql = ''
|
772
|
-
[attributes, facts].each do |obj|
|
773
|
-
maql += obj.to_maql_drop
|
774
|
-
end
|
775
|
-
maql += "DROP {#{self.identifier}};\n"
|
776
|
-
end
|
777
|
-
|
778
|
-
##
|
779
|
-
# Generates MAQL DDL script to create this data set and included pieces
|
780
|
-
#
|
781
|
-
def to_maql_create
|
782
|
-
# TODO: Use template (.erb)
|
783
|
-
maql = "# Create the '#{self.title}' data set\n"
|
784
|
-
maql += "CREATE DATASET {#{self.identifier}} VISUAL (TITLE \"#{self.title}\");\n\n"
|
785
|
-
[attributes, facts, {1 => @anchor}].each do |objects|
|
786
|
-
objects.values.each do |obj|
|
787
|
-
maql += "# Create '#{obj.title}' and add it to the '#{self.title}' data set.\n"
|
788
|
-
maql += obj.to_maql_create
|
789
|
-
maql += "ALTER DATASET {#{self.identifier}} ADD {#{obj.identifier}};\n\n"
|
790
|
-
end
|
791
|
-
end
|
792
|
-
|
793
|
-
labels.each do |label|
|
794
|
-
maql += "# Creating Labels\n"
|
795
|
-
maql += label.to_maql_create
|
796
|
-
end
|
797
|
-
|
798
|
-
references.values.each do |ref|
|
799
|
-
maql += "# Creating references\n"
|
800
|
-
maql += ref.to_maql_create
|
801
|
-
end
|
802
|
-
|
803
|
-
folders_maql = "# Create folders\n"
|
804
|
-
(folders[:attributes].values + folders[:facts].values).each { |folder| folders_maql += folder.to_maql_create }
|
805
|
-
folders_maql + "\n" + maql + "SYNCHRONIZE {#{identifier}};\n"
|
806
|
-
end
|
807
|
-
|
808
|
-
def upload(path, project = nil, mode = 'FULL')
|
809
|
-
if path =~ URI::regexp
|
810
|
-
Tempfile.open('remote_file') do |temp|
|
811
|
-
temp << open(path).read
|
812
|
-
temp.flush
|
813
|
-
upload_data(temp, mode)
|
814
|
-
end
|
815
|
-
else
|
816
|
-
upload_data(path, mode)
|
817
|
-
end
|
818
|
-
end
|
819
|
-
|
820
|
-
def upload_data(path, mode)
|
821
|
-
GoodData::Model.upload_data(path, to_manifest(mode))
|
822
|
-
end
|
823
|
-
|
824
|
-
# Generates the SLI manifest describing the data loading
|
825
|
-
#
|
826
|
-
def to_manifest(mode = 'FULL')
|
827
|
-
{
|
828
|
-
'dataSetSLIManifest' => {
|
829
|
-
'parts' => fields.reduce([]) { |memo, f| val = f.to_manifest_part(mode); memo << val unless val.nil?; memo },
|
830
|
-
'dataSet' => self.identifier,
|
831
|
-
'file' => 'data.csv', # should be configurable
|
832
|
-
'csvParams' => {
|
833
|
-
'quoteChar' => '"',
|
834
|
-
'escapeChar' => '"',
|
835
|
-
'separatorChar' => ',',
|
836
|
-
'endOfLine' => "\n"
|
837
|
-
}
|
838
|
-
}
|
839
|
-
}
|
840
|
-
end
|
841
|
-
|
842
|
-
def to_wire_model
|
843
|
-
{
|
844
|
-
'dataset' => {
|
845
|
-
'identifier' => identifier,
|
846
|
-
'title' => title,
|
847
|
-
'anchor' => @anchor.to_wire_model,
|
848
|
-
'facts' => facts.map { |f| f.to_wire_model },
|
849
|
-
'attributes' => attributes.map { |a| a.to_wire_model },
|
850
|
-
'references' => references.map { |r| r.is_a?(DateReference) ? r.schema_ref : type_prefix + '.' + r.schema_ref }}
|
851
|
-
}
|
852
|
-
end
|
853
|
-
|
854
|
-
private
|
855
|
-
|
856
|
-
def add_attribute(column)
|
857
|
-
attribute = Attribute.new column, self
|
858
|
-
fields << attribute
|
859
|
-
attributes << attribute
|
860
|
-
add_attribute_folder(attribute.folder)
|
861
|
-
# folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
|
862
|
-
end
|
863
|
-
|
864
|
-
def add_attribute_folder(name)
|
865
|
-
return if name.nil?
|
866
|
-
return if folders[:attributes].has_key?(name)
|
867
|
-
folders[:attributes][name] = AttributeFolder.new(name)
|
868
|
-
end
|
869
|
-
|
870
|
-
def add_fact(column)
|
871
|
-
fact = Fact.new column, self
|
872
|
-
fields << fact
|
873
|
-
facts << fact
|
874
|
-
add_fact_folder(fact.folder)
|
875
|
-
# folders[FactFolder.new(fact.folder)] = 1 if fact.folder
|
876
|
-
end
|
877
|
-
|
878
|
-
def add_fact_folder(name)
|
879
|
-
return if name.nil?
|
880
|
-
return if folders[:facts].has_key?(name)
|
881
|
-
folders[:facts][name] = FactFolder.new(name)
|
882
|
-
end
|
883
|
-
|
884
|
-
def add_label(column)
|
885
|
-
label = Label.new(column, nil, self)
|
886
|
-
labels << label
|
887
|
-
fields << label
|
888
|
-
end
|
889
|
-
|
890
|
-
def add_reference(column)
|
891
|
-
reference = Reference.new(column, self)
|
892
|
-
fields << reference
|
893
|
-
references << reference
|
894
|
-
end
|
895
|
-
|
896
|
-
def add_date(column)
|
897
|
-
date = DateColumn.new column, self
|
898
|
-
@fields << date
|
899
|
-
date.parts.values.each { |p| @fields << p }
|
900
|
-
date.facts.each { |f| facts << f }
|
901
|
-
date.attributes.each { |a| attributes << a }
|
902
|
-
date.references.each { |r| references << r }
|
903
|
-
end
|
904
|
-
|
905
|
-
def set_anchor(column)
|
906
|
-
@anchor = Anchor.new column, self
|
907
|
-
@fields << @anchor
|
908
|
-
end
|
909
|
-
end
|
910
|
-
|
911
|
-
##
|
912
|
-
# This is a base class for server-side LDM elements such as attributes, labels and
|
913
|
-
# facts
|
914
|
-
#
|
915
|
-
class Column < MdObject
|
916
|
-
attr_accessor :folder, :name, :title, :schema
|
917
|
-
|
918
|
-
def initialize(hash, schema)
|
919
|
-
super()
|
920
|
-
raise ArgumentError.new("Schema must be provided, got #{schema.class}") unless schema.is_a? Schema
|
921
|
-
raise('Data set fields must have their names defined') if hash[:name].nil?
|
922
|
-
|
923
|
-
@name = hash[:name]
|
924
|
-
@title = hash[:title] || hash[:name].humanize
|
925
|
-
@folder = hash[:folder]
|
926
|
-
@schema = schema
|
927
|
-
end
|
928
|
-
|
929
|
-
##
|
930
|
-
# Generates an identifier from the object name by transliterating
|
931
|
-
# non-Latin character and then dropping non-alphanumerical characters.
|
932
|
-
#
|
933
|
-
def identifier
|
934
|
-
@identifier ||= "#{self.type_prefix}.#{@schema.name}.#{name}"
|
935
|
-
end
|
936
|
-
|
937
|
-
def to_maql_drop
|
938
|
-
"DROP {#{self.identifier}};\n"
|
939
|
-
end
|
940
|
-
|
941
|
-
def visual
|
942
|
-
visual = super
|
943
|
-
visual += ", FOLDER {#{folder_prefix}.#{(folder)}}" if folder
|
944
|
-
visual
|
945
|
-
end
|
946
|
-
|
947
|
-
def to_csv_header(row)
|
948
|
-
name
|
949
|
-
end
|
950
|
-
|
951
|
-
def to_csv_data(headers, row)
|
952
|
-
row[name]
|
953
|
-
end
|
954
|
-
|
955
|
-
|
956
|
-
# Overriden to prevent long strings caused by the @schema attribute
|
957
|
-
#
|
958
|
-
def inspect
|
959
|
-
to_s.sub(/>$/, " @title=#{@title.inspect}, @name=#{@name.inspect}, @folder=#{@folder.inspect}," \
|
960
|
-
" @schema=#{@schema.to_s.sub(/>$/, ' @title=' + @schema.name.inspect + '>')}" \
|
961
|
-
">")
|
962
|
-
end
|
963
|
-
end
|
964
|
-
|
965
|
-
##
|
966
|
-
# GoodData attribute abstraction
|
967
|
-
#
|
968
|
-
class Attribute < Column
|
969
|
-
attr_reader :primary_label, :labels
|
970
|
-
|
971
|
-
def type_prefix;
|
972
|
-
ATTRIBUTE_PREFIX;
|
973
|
-
end
|
974
|
-
|
975
|
-
def folder_prefix;
|
976
|
-
ATTRIBUTE_FOLDER_PREFIX;
|
977
|
-
end
|
978
|
-
|
979
|
-
def initialize(hash, schema)
|
980
|
-
super hash, schema
|
981
|
-
@labels = []
|
982
|
-
@primary_label = Label.new hash, self, schema
|
983
|
-
end
|
984
|
-
|
985
|
-
def table
|
986
|
-
@table ||= 'd_' + @schema.name + '_' + name
|
987
|
-
end
|
988
|
-
|
989
|
-
def key;
|
990
|
-
"#{@name}#{FK_SUFFIX}";
|
991
|
-
end
|
992
|
-
|
993
|
-
def to_maql_create
|
994
|
-
maql = "CREATE ATTRIBUTE {#{identifier}} VISUAL (#{visual})" \
|
995
|
-
+ " AS KEYS {#{table}.#{Model::FIELD_PK}} FULLSET;\n"
|
996
|
-
maql += @primary_label.to_maql_create if @primary_label
|
997
|
-
maql
|
998
|
-
end
|
999
|
-
|
1000
|
-
def to_manifest_part(mode)
|
1001
|
-
{
|
1002
|
-
'referenceKey' => 1,
|
1003
|
-
'populates' => [@primary_label.identifier],
|
1004
|
-
'mode' => mode,
|
1005
|
-
'columnName' => name
|
1006
|
-
}
|
1007
|
-
end
|
1008
|
-
|
1009
|
-
def to_wire_model
|
1010
|
-
{
|
1011
|
-
'attribute' => {
|
1012
|
-
'identifier' => identifier,
|
1013
|
-
'title' => title,
|
1014
|
-
'labels' => labels.map do |l|
|
1015
|
-
{
|
1016
|
-
'label' => {
|
1017
|
-
'identifier' => l.identifier,
|
1018
|
-
'title' => l.title,
|
1019
|
-
'type' => 'GDC.text'
|
1020
|
-
}
|
1021
|
-
}
|
1022
|
-
end
|
1023
|
-
}
|
1024
|
-
}
|
1025
|
-
end
|
1026
|
-
end
|
1027
|
-
|
1028
|
-
##
|
1029
|
-
# GoodData display form abstraction. Represents a default representation
|
1030
|
-
# of an attribute column or an additional representation defined in a LABEL
|
1031
|
-
# field
|
1032
|
-
#
|
1033
|
-
class Label < Column
|
1034
|
-
attr_accessor :attribute
|
1035
|
-
|
1036
|
-
def type_prefix;
|
1037
|
-
'label';
|
1038
|
-
end
|
1039
|
-
|
1040
|
-
# def initialize(hash, schema)
|
1041
|
-
def initialize(hash, attribute, schema)
|
1042
|
-
super hash, schema
|
1043
|
-
attribute = attribute.nil? ? schema.fields.find { |field| field.name === hash[:reference] } : attribute
|
1044
|
-
@attribute = attribute
|
1045
|
-
attribute.labels << self
|
1046
|
-
end
|
1047
|
-
|
1048
|
-
def to_maql_create
|
1049
|
-
'# LABEL FROM LABEL'
|
1050
|
-
"ALTER ATTRIBUTE {#{@attribute.identifier}} ADD LABELS {#{identifier}}" \
|
1051
|
-
+ " VISUAL (TITLE #{title.inspect}) AS {#{column}};\n"
|
1052
|
-
end
|
1053
|
-
|
1054
|
-
def to_manifest_part(mode)
|
1055
|
-
{
|
1056
|
-
'populates' => [identifier],
|
1057
|
-
'mode' => mode,
|
1058
|
-
'columnName' => name
|
1059
|
-
}
|
1060
|
-
end
|
1061
|
-
|
1062
|
-
def column
|
1063
|
-
"#{@attribute.table}.#{LABEL_COLUMN_PREFIX}#{name}"
|
1064
|
-
end
|
1065
|
-
|
1066
|
-
alias :inspect_orig :inspect
|
1067
|
-
|
1068
|
-
def inspect
|
1069
|
-
inspect_orig.sub(/>$/, " @attribute=#{@attribute.to_s.sub(/>$/, " @name=#{@attribute.name}")}>")
|
1070
|
-
end
|
1071
|
-
end
|
1072
|
-
|
1073
|
-
##
|
1074
|
-
# A GoodData attribute that represents a data set's connection point or a data set
|
1075
|
-
# without a connection point
|
1076
|
-
#
|
1077
|
-
class Anchor < Attribute
|
1078
|
-
def initialize(column, schema)
|
1079
|
-
if column then
|
1080
|
-
super
|
1081
|
-
else
|
1082
|
-
super({:type => 'anchor', :name => 'id'}, schema)
|
1083
|
-
@labels = []
|
1084
|
-
@primary_label = nil
|
1085
|
-
end
|
1086
|
-
end
|
1087
|
-
|
1088
|
-
def table
|
1089
|
-
@table ||= 'f_' + @schema.name
|
1090
|
-
end
|
1091
|
-
|
1092
|
-
def to_maql_create
|
1093
|
-
maql = super
|
1094
|
-
maql += "\n# Connect '#{self.title}' to all attributes of this data set\n"
|
1095
|
-
@schema.attributes.values.each do |c|
|
1096
|
-
maql += "ALTER ATTRIBUTE {#{c.identifier}} ADD KEYS " \
|
1097
|
-
+ "{#{table}.#{c.key}};\n"
|
1098
|
-
end
|
1099
|
-
maql
|
1100
|
-
end
|
1101
|
-
end
|
1102
|
-
|
1103
|
-
##
|
1104
|
-
# GoodData fact abstraction
|
1105
|
-
#
|
1106
|
-
class Fact < Column
|
1107
|
-
def type_prefix;
|
1108
|
-
FACT_PREFIX;
|
1109
|
-
end
|
1110
|
-
|
1111
|
-
def column_prefix;
|
1112
|
-
FACT_COLUMN_PREFIX;
|
1113
|
-
end
|
1114
|
-
|
1115
|
-
def folder_prefix;
|
1116
|
-
FACT_FOLDER_PREFIX;
|
1117
|
-
end
|
1118
|
-
|
1119
|
-
def table
|
1120
|
-
@schema.table
|
1121
|
-
end
|
1122
|
-
|
1123
|
-
def column
|
1124
|
-
@column ||= table + '.' + column_prefix + name
|
1125
|
-
end
|
1126
|
-
|
1127
|
-
def to_maql_create
|
1128
|
-
"CREATE FACT {#{self.identifier}} VISUAL (#{visual})" \
|
1129
|
-
+ " AS {#{column}};\n"
|
1130
|
-
end
|
1131
|
-
|
1132
|
-
def to_manifest_part(mode)
|
1133
|
-
{
|
1134
|
-
'populates' => [identifier],
|
1135
|
-
'mode' => mode,
|
1136
|
-
'columnName' => name
|
1137
|
-
}
|
1138
|
-
end
|
1139
|
-
|
1140
|
-
def to_wire_model
|
1141
|
-
{
|
1142
|
-
'fact' => {
|
1143
|
-
'identifier' => identifier,
|
1144
|
-
'title' => title
|
1145
|
-
}
|
1146
|
-
}
|
1147
|
-
end
|
1148
|
-
end
|
1149
|
-
|
1150
|
-
##
|
1151
|
-
# Reference to another data set
|
1152
|
-
#
|
1153
|
-
class Reference < Column
|
1154
|
-
attr_accessor :reference, :schema_ref
|
1155
|
-
|
1156
|
-
def initialize(column, schema)
|
1157
|
-
super column, schema
|
1158
|
-
# pp column
|
1159
|
-
@name = column[:name]
|
1160
|
-
@reference = column[:reference]
|
1161
|
-
@schema_ref = column[:dataset]
|
1162
|
-
@schema = schema
|
1163
|
-
end
|
1164
|
-
|
1165
|
-
##
|
1166
|
-
# Generates an identifier of the referencing attribute using the
|
1167
|
-
# schema name derived from schemaReference and column name derived
|
1168
|
-
# from the reference key.
|
1169
|
-
#
|
1170
|
-
def identifier
|
1171
|
-
@identifier ||= "#{ATTRIBUTE_PREFIX}.#{@schema_ref}.#{@reference}"
|
1172
|
-
end
|
1173
|
-
|
1174
|
-
def key;
|
1175
|
-
"#{@name}_id";
|
1176
|
-
end
|
1177
|
-
|
1178
|
-
def label_column
|
1179
|
-
"#{LABEL_PREFIX}.#{@schema_ref}.#{@reference}"
|
1180
|
-
end
|
1181
|
-
|
1182
|
-
def to_maql_create
|
1183
|
-
"ALTER ATTRIBUTE {#{self.identifier}} ADD KEYS {#{@schema.table}.#{key}};\n"
|
1184
|
-
end
|
1185
|
-
|
1186
|
-
def to_maql_drop
|
1187
|
-
"ALTER ATTRIBUTE {#{self.identifier} DROP KEYS {#{@schema.table}.#{key}};\n"
|
1188
|
-
end
|
1189
|
-
|
1190
|
-
def to_manifest_part(mode)
|
1191
|
-
{
|
1192
|
-
'populates' => [label_column],
|
1193
|
-
'mode' => mode,
|
1194
|
-
'columnName' => name,
|
1195
|
-
'referenceKey' => 1
|
1196
|
-
}
|
1197
|
-
end
|
1198
|
-
end
|
1199
|
-
|
1200
|
-
##
|
1201
|
-
# Date as a reference to a date dimension
|
1202
|
-
#
|
1203
|
-
class DateReference < Reference
|
1204
|
-
attr_accessor :format, :output_format, :urn
|
1205
|
-
|
1206
|
-
def initialize(column, schema)
|
1207
|
-
super column, schema
|
1208
|
-
@output_format = column['format'] || 'dd/MM/yyyy'
|
1209
|
-
@format = @output_format.gsub('yyyy', '%Y').gsub('MM', '%m').gsub('dd', '%d')
|
1210
|
-
@urn = column[:urn] || 'URN:GOODDATA:DATE'
|
1211
|
-
end
|
1212
|
-
|
1213
|
-
def identifier
|
1214
|
-
@identifier ||= "#{@schema_ref}.#{DATE_ATTRIBUTE}"
|
1215
|
-
end
|
1216
|
-
|
1217
|
-
def to_manifest_part(mode)
|
1218
|
-
{
|
1219
|
-
'populates' => ["#{identifier}.#{DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM}"],
|
1220
|
-
'mode' => mode,
|
1221
|
-
'constraints' => {'date' => output_format},
|
1222
|
-
'columnName' => name,
|
1223
|
-
'referenceKey' => 1
|
1224
|
-
}
|
1225
|
-
end
|
1226
|
-
|
1227
|
-
# def to_maql_create
|
1228
|
-
# # urn:chefs_warehouse_fiscal:date
|
1229
|
-
# super_maql = super
|
1230
|
-
# maql = ""
|
1231
|
-
# # maql = "# Include date dimensions\n"
|
1232
|
-
# # maql += "INCLUDE TEMPLATE \"#{urn}\" MODIFY (IDENTIFIER \"#{name}\", TITLE \"#{title || name}\");\n"
|
1233
|
-
# maql += super_maql
|
1234
|
-
# end
|
1235
|
-
end
|
1236
|
-
|
1237
|
-
##
|
1238
|
-
# Date field that's not connected to a date dimension
|
1239
|
-
#
|
1240
|
-
class DateAttribute < Attribute
|
1241
|
-
def key;
|
1242
|
-
"#{DATE_COLUMN_PREFIX}#{super}";
|
1243
|
-
end
|
1244
|
-
|
1245
|
-
def to_manifest_part(mode)
|
1246
|
-
{
|
1247
|
-
'populates' => ['label.stuff.mmddyy'],
|
1248
|
-
'format' => 'unknown',
|
1249
|
-
'mode' => mode,
|
1250
|
-
'referenceKey' => 1
|
1251
|
-
}
|
1252
|
-
end
|
1253
|
-
end
|
1254
|
-
|
1255
|
-
##
|
1256
|
-
# Fact representation of a time of a day
|
1257
|
-
#
|
1258
|
-
class TimeFact < Fact
|
1259
|
-
def column_prefix;
|
1260
|
-
TIME_COLUMN_PREFIX;
|
1261
|
-
end
|
1262
|
-
|
1263
|
-
def type_prefix;
|
1264
|
-
TIME_FACT_PREFIX;
|
1265
|
-
end
|
1266
|
-
end
|
1267
|
-
|
1268
|
-
##
|
1269
|
-
# Time as a reference to a time-of-a-day dimension
|
1270
|
-
#
|
1271
|
-
class TimeReference < Reference
|
1272
|
-
end
|
1273
|
-
|
1274
|
-
##
|
1275
|
-
# Time field that's not connected to a time-of-a-day dimension
|
1276
|
-
#
|
1277
|
-
class TimeAttribute < Attribute
|
1278
|
-
def type_prefix;
|
1279
|
-
TIME_ATTRIBUTE_PREFIX;
|
1280
|
-
end
|
1281
|
-
|
1282
|
-
def key;
|
1283
|
-
"#{TIME_COLUMN_PREFIX}#{super}";
|
1284
|
-
end
|
1285
|
-
|
1286
|
-
def table;
|
1287
|
-
@table ||= "#{super}_tm";
|
1288
|
-
end
|
1289
|
-
end
|
1290
|
-
|
1291
|
-
##
|
1292
|
-
# Date column. A container holding the following
|
1293
|
-
# parts: date fact, a date reference or attribute and an optional time component
|
1294
|
-
# that contains a time fact and a time reference or attribute.
|
1295
|
-
#
|
1296
|
-
class DateColumn < Column
|
1297
|
-
attr_reader :parts, :facts, :attributes, :references
|
1298
|
-
|
1299
|
-
def initialize(column, schema)
|
1300
|
-
super column, schema
|
1301
|
-
@parts = {}; @facts = []; @attributes = []; @references = []
|
1302
|
-
|
1303
|
-
# @facts << @parts[:date_fact] = DateFact.new(column, schema)
|
1304
|
-
if column[:dataset] then
|
1305
|
-
@parts[:date_ref] = DateReference.new column, schema
|
1306
|
-
@references << @parts[:date_ref]
|
1307
|
-
else
|
1308
|
-
@attributes << @parts[:date_attr] = DateAttribute.new(column, schema)
|
1309
|
-
end
|
1310
|
-
# if column['datetime'] then
|
1311
|
-
# puts "*** datetime"
|
1312
|
-
# @facts << @parts[:time_fact] = TimeFact.new(column, schema)
|
1313
|
-
# if column['schema_reference'] then
|
1314
|
-
# @parts[:time_ref] = TimeReference.new column, schema
|
1315
|
-
# else
|
1316
|
-
# @attributes << @parts[:time_attr] = TimeAttribute.new(column, schema)
|
1317
|
-
# end
|
1318
|
-
# end
|
1319
|
-
end
|
1320
|
-
|
1321
|
-
def to_maql_create
|
1322
|
-
@parts.values.map { |v| v.to_maql_create }.join "\n"
|
1323
|
-
end
|
1324
|
-
|
1325
|
-
def to_maql_drop
|
1326
|
-
@parts.values.map { |v| v.to_maql_drop }.join "\n"
|
1327
|
-
end
|
1328
|
-
|
1329
|
-
def to_csv_header(row)
|
1330
|
-
SKIP_FIELD
|
1331
|
-
end
|
1332
|
-
|
1333
|
-
def to_csv_data(headers, row)
|
1334
|
-
SKIP_FIELD
|
1335
|
-
end
|
1336
|
-
|
1337
|
-
def to_manifest_part(mode)
|
1338
|
-
nil
|
1339
|
-
end
|
1340
|
-
end
|
1341
|
-
|
1342
|
-
##
|
1343
|
-
# Base class for GoodData attribute and fact folder abstractions
|
1344
|
-
#
|
1345
|
-
class Folder < MdObject
|
1346
|
-
def initialize(title)
|
1347
|
-
# TODO: should a super be here?
|
1348
|
-
# how to deal with name vs title?
|
1349
|
-
@title = title
|
1350
|
-
@name = GoodData::Helpers.sanitize_string(title)
|
1351
|
-
end
|
1352
|
-
|
1353
|
-
def to_maql_create
|
1354
|
-
"CREATE FOLDER {#{type_prefix}.#{name}}" \
|
1355
|
-
+ " VISUAL (#{visual}) TYPE #{type};\n"
|
1356
|
-
end
|
1357
|
-
end
|
1358
|
-
|
1359
|
-
##
|
1360
|
-
# GoodData attribute folder abstraction
|
1361
|
-
#
|
1362
|
-
class AttributeFolder < Folder
|
1363
|
-
def type;
|
1364
|
-
'ATTRIBUTE'
|
1365
|
-
end
|
1366
|
-
|
1367
|
-
def type_prefix;
|
1368
|
-
'dim'
|
1369
|
-
end
|
1370
|
-
end
|
1371
|
-
|
1372
|
-
##
|
1373
|
-
# GoodData fact folder abstraction
|
1374
|
-
#
|
1375
|
-
class FactFolder < Folder
|
1376
|
-
def type;
|
1377
|
-
'FACT'
|
1378
|
-
end
|
1379
|
-
|
1380
|
-
def type_prefix;
|
1381
|
-
'ffld'
|
1382
|
-
end
|
1383
|
-
end
|
1384
|
-
|
1385
|
-
class DateDimension < MdObject
|
1386
|
-
def initialize(spec={})
|
1387
|
-
super()
|
1388
|
-
@name = spec[:name]
|
1389
|
-
@title = spec[:title] || @name
|
1390
|
-
@urn = spec[:urn] || 'URN:GOODDATA:DATE'
|
1391
|
-
end
|
1392
|
-
|
1393
|
-
def to_maql_create
|
1394
|
-
# urn = "urn:chefs_warehouse_fiscal:date"
|
1395
|
-
# title = "title"
|
1396
|
-
# name = "name"
|
1397
|
-
|
1398
|
-
maql = ''
|
1399
|
-
maql += "INCLUDE TEMPLATE \"#{@urn}\" MODIFY (IDENTIFIER \"#{@name}\", TITLE \"#{@title}\");"
|
1400
|
-
maql
|
1401
|
-
end
|
1402
|
-
end
|
1403
102
|
end
|
1404
|
-
end
|
103
|
+
end
|