jira_cache 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +10 -0
- data/.env.example +10 -0
- data/.env.test +10 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +12 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +15 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +21 -0
- data/Guardfile +1 -0
- data/HISTORY.md +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +73 -0
- data/Rakefile +7 -0
- data/VERSION +1 -0
- data/bin/console +11 -0
- data/bin/db/migrate +7 -0
- data/bin/db/psql +7 -0
- data/bin/db/reset +7 -0
- data/bin/setup +7 -0
- data/bin/sync +14 -0
- data/config.ru +7 -0
- data/config/Guardfile +35 -0
- data/config/boot.rb +11 -0
- data/config/db_migrations/001_create_issues.rb +21 -0
- data/docker-compose.yml +25 -0
- data/jira_cache.gemspec +41 -0
- data/lib/jira_cache.rb +53 -0
- data/lib/jira_cache/client.rb +185 -0
- data/lib/jira_cache/data.rb +10 -0
- data/lib/jira_cache/data/issue_repository.rb +94 -0
- data/lib/jira_cache/notifier.rb +30 -0
- data/lib/jira_cache/sync.rb +110 -0
- data/lib/jira_cache/version.rb +3 -0
- data/lib/jira_cache/webhook_app.rb +55 -0
- data/spec/fixtures/responses/get_issue_keys_jql_query_project=/"multiple_requests/"_start_at_0.json +1 -0
- data/spec/fixtures/responses/get_issue_keys_jql_query_project=/"multiple_requests/"_start_at_10.json +1 -0
- data/spec/fixtures/responses/get_issue_keys_jql_query_project=/"multiple_requests/"_start_at_5.json +1 -0
- data/spec/fixtures/responses/get_issue_keys_jql_query_project=/"single_request/"_start_at_0.json +1 -0
- data/spec/fixtures/responses/get_issue_many_worklogs.json +1 -0
- data/spec/fixtures/responses/get_issue_not_found.json +1 -0
- data/spec/fixtures/responses/get_issue_simple.json +1 -0
- data/spec/fixtures/responses/get_issue_worklog_many_worklogs.json +1 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/response_fixture.rb +16 -0
- data/spec/unit/client_spec.rb +130 -0
- data/spec/unit/data/issue_repository_spec.rb +58 -0
- data/spec/unit/notifier_spec.rb +18 -0
- data/spec/unit/sync_spec.rb +116 -0
- data/spec/unit/webhook_app_spec.rb +96 -0
- metadata +280 -0
data/jira_cache.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "jira_cache/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "jira_cache"
|
9
|
+
spec.version = JiraCache::VERSION
|
10
|
+
spec.authors = ["Romain Champourlier"]
|
11
|
+
spec.email = ["pro@rchampourlier.com"]
|
12
|
+
spec.summary = "Fetches data from JIRA and caches it in a local database."
|
13
|
+
spec.homepage = "https://github.com/rchampourlier/jira_cache"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org by setting "allowed_push_host", or
|
17
|
+
# delete this section to allow pushing this gem to any host.
|
18
|
+
unless spec.respond_to?(:metadata)
|
19
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
20
|
+
end
|
21
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
22
|
+
|
23
|
+
spec.files = `git ls-files -z`.split("\x0")
|
24
|
+
spec.executables = []
|
25
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_dependency "i18n"
|
29
|
+
spec.add_dependency "activesupport-inflector"
|
30
|
+
spec.add_dependency "pg"
|
31
|
+
spec.add_dependency "sequel"
|
32
|
+
spec.add_dependency "sequel_pg"
|
33
|
+
spec.add_dependency "rest-client"
|
34
|
+
spec.add_dependency "sinatra"
|
35
|
+
|
36
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
37
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
38
|
+
spec.add_development_dependency "pry"
|
39
|
+
spec.add_development_dependency "awesome_print"
|
40
|
+
spec.add_development_dependency "dotenv"
|
41
|
+
end
|
data/lib/jira_cache.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require "jira_cache/version"
|
2
|
+
require "jira_cache/sync"
|
3
|
+
require "jira_cache/webhook_app"
|
4
|
+
|
5
|
+
# JiraCache enables storing JIRA issues fetched from the API
|
6
|
+
# in a local storage for easier and faster processing.
|
7
|
+
#
|
8
|
+
# This is the main module and it provides some high level
|
9
|
+
# methods to either trigger a full project sync, a single
|
10
|
+
# issue sync or start a Sinatra webhook app to trigger sync
|
11
|
+
# on JIRA"s webhooks.
|
12
|
+
module JiraCache
|
13
|
+
|
14
|
+
# Sync issues using the specified client. If a `project_key` is
|
15
|
+
# specified, only syncs the issues for the corresponding project.
|
16
|
+
def self.sync_issues(client: default_client, project_key: nil)
|
17
|
+
Sync.new(client).sync_issues(project_key: project_key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.sync_issue(issue_key, client: default_client)
|
21
|
+
Sync.new(client).sync_issue(issue_key)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param client [JiraCache::Client]: defaults to a default
|
25
|
+
# client using environment variables for domain, username
|
26
|
+
# and password, a logger writing to STDOUT and a default
|
27
|
+
# `JiraCache::Notifier` instance as notifier.
|
28
|
+
def self.webhook_app(client: default_client)
|
29
|
+
Sinatra.new(JiraCache::WebhookApp) do
|
30
|
+
set(:client, client)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.default_client
|
35
|
+
JiraCache::Client.new(
|
36
|
+
domain: ENV["JIRA_DOMAIN"],
|
37
|
+
username: ENV["JIRA_USERNAME"],
|
38
|
+
password: ENV["JIRA_PASSWORD"],
|
39
|
+
logger: default_logger,
|
40
|
+
notifier: default_notifier
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.default_logger
|
45
|
+
logger = Logger.new(STDOUT)
|
46
|
+
logger.level = Logger::DEBUG
|
47
|
+
logger
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.default_notifier(logger: default_logger)
|
51
|
+
JiraCache::Notifier.new(logger)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "rest-client"
|
3
|
+
require "base64"
|
4
|
+
require "jira_cache/notifier"
|
5
|
+
|
6
|
+
module JiraCache
|
7
|
+
|
8
|
+
# The JIRA API Client.
|
9
|
+
class Client
|
10
|
+
JIRA_MAX_RESULTS = 1000
|
11
|
+
|
12
|
+
EXPANDED_FIELDS = %w(
|
13
|
+
renderedFields
|
14
|
+
changelog
|
15
|
+
).freeze
|
16
|
+
# Other possible fields: names, schema, operations, editmeta
|
17
|
+
|
18
|
+
attr_reader :logger, :notifier
|
19
|
+
|
20
|
+
# Returns a new instance of the client, configured with
|
21
|
+
# the specified parameters.
|
22
|
+
#
|
23
|
+
# @param domain [String] JIRA API domain (e.g. your-project.atlassian.net)
|
24
|
+
# @param username [String] JIRA user"s name, if required
|
25
|
+
# @param password [String] JIRA user"s password, if required
|
26
|
+
# @param logger [Logger] used to log message (defaults to a logger to STDOUT at
|
27
|
+
# info level)
|
28
|
+
# @param notifier [Notifier] a notifier instance that will be used to publish
|
29
|
+
# event notifications (see `JiraCache::Notifier` for more information)
|
30
|
+
#
|
31
|
+
def initialize(domain: ENV["JIRA_DOMAIN"],
|
32
|
+
username: ENV["JIRA_USERNAME"],
|
33
|
+
password: ENV["JIRA_PASSWORD"],
|
34
|
+
notifier: default_notifier,
|
35
|
+
logger: default_logger)
|
36
|
+
check_domain!(domain)
|
37
|
+
check_password!(username, password)
|
38
|
+
@domain = domain
|
39
|
+
@username = username
|
40
|
+
@password = password
|
41
|
+
@notifier = notifier
|
42
|
+
@logger = logger
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fetches the issue represented by id_or_key from the
|
46
|
+
# client.
|
47
|
+
# If the data is already present in the cache,
|
48
|
+
# returns the cached version, unless if :allow_cache
|
49
|
+
# option is false.
|
50
|
+
def issue_data(id_or_key)
|
51
|
+
logger.info "Fetching data for issue #{id_or_key}"
|
52
|
+
issue_data = do_get("/issue/#{id_or_key}",
|
53
|
+
expand: EXPANDED_FIELDS.join(",")
|
54
|
+
).to_hash
|
55
|
+
return nil if issue_not_found?(issue_data)
|
56
|
+
issue_data = complete_worklogs(id_or_key, issue_data)
|
57
|
+
begin
|
58
|
+
notifier.publish "fetched_issue", key: id_or_key, data: issue_data
|
59
|
+
rescue => e
|
60
|
+
logger.error "Notifier failed: #{e}"
|
61
|
+
logger.error e.backtrace
|
62
|
+
end
|
63
|
+
issue_data
|
64
|
+
end
|
65
|
+
|
66
|
+
def issue_keys_for_query(jql_query)
|
67
|
+
start_at = 0
|
68
|
+
issues = []
|
69
|
+
loop do
|
70
|
+
total, page_issues = issue_ids_in_limits(jql_query, start_at)
|
71
|
+
logger.info "Total number of issues: #{total}" if issues.length == 0
|
72
|
+
issues += page_issues
|
73
|
+
logger.info " -- loaded #{page_issues.length} issues"
|
74
|
+
start_at = issues.length
|
75
|
+
break if issues.length == total
|
76
|
+
end
|
77
|
+
issues.collect { |issue| issue["key"] }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Implementation methods
|
81
|
+
# ======================
|
82
|
+
|
83
|
+
# @return [total, issues]
|
84
|
+
# - total: [Int] the total number of issues in the query results
|
85
|
+
# - issues: [Array] array of issues in the response
|
86
|
+
# (max `JIRA_MAX_RESULTS`)
|
87
|
+
def issue_ids_in_limits(jql_query, start_at)
|
88
|
+
results = do_get "/search",
|
89
|
+
jql: jql_query,
|
90
|
+
startAt: start_at,
|
91
|
+
fields: "id",
|
92
|
+
maxResults: JIRA_MAX_RESULTS
|
93
|
+
[results["total"], results["issues"]]
|
94
|
+
end
|
95
|
+
|
96
|
+
def issue_not_found?(issue_data)
|
97
|
+
return false if issue_data["errorMessages"].nil?
|
98
|
+
issue_data["errorMessages"].first == "Issue Does Not Exist"
|
99
|
+
end
|
100
|
+
|
101
|
+
def complete_worklogs(id_or_key, issue_data)
|
102
|
+
if incomplete_worklogs?(issue_data)
|
103
|
+
issue_data["fields"]["worklog"] = issue_worklog_content(id_or_key)
|
104
|
+
end
|
105
|
+
issue_data
|
106
|
+
end
|
107
|
+
|
108
|
+
def incomplete_worklogs?(issue_data)
|
109
|
+
worklog = issue_data["fields"]["worklog"]
|
110
|
+
worklog["total"].to_i > worklog["maxResults"].to_i
|
111
|
+
end
|
112
|
+
|
113
|
+
def issue_worklog_content(id_or_key)
|
114
|
+
do_get("/issue/#{id_or_key}/worklog").to_hash
|
115
|
+
end
|
116
|
+
|
117
|
+
def project_data(id)
|
118
|
+
do_get "/project/#{id}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def projects_data
|
122
|
+
do_get "/project"
|
123
|
+
end
|
124
|
+
|
125
|
+
def do_get(path, params = {})
|
126
|
+
logger.debug "GET #{uri(path)} #{params}"
|
127
|
+
response = RestClient.get uri(path),
|
128
|
+
params: params,
|
129
|
+
content_type: "application/json"
|
130
|
+
begin
|
131
|
+
JSON.parse(response.body)
|
132
|
+
rescue JSON::ParseError
|
133
|
+
response.body
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns the JIRA API"s base URI (build using `config[:domain]`)
|
138
|
+
def uri(path)
|
139
|
+
"https://#{authorization_prefix}#{@domain}/rest/api/2#{path}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def authorization_prefix
|
143
|
+
return "" if missing_credential?
|
144
|
+
"#{CGI.escape(@username)}:#{CGI.escape(@password)}@"
|
145
|
+
end
|
146
|
+
|
147
|
+
def default_logger
|
148
|
+
return @logger unless @logger.nil?
|
149
|
+
@logger = ::Logger.new(STDOUT)
|
150
|
+
@logger.level = ::Logger::FATAL
|
151
|
+
@logger
|
152
|
+
end
|
153
|
+
|
154
|
+
def default_notifier
|
155
|
+
return @notifier unless @notifier.nil?
|
156
|
+
@notifier = JiraCache::Notifier.new(@logger)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns an hash of info on the client
|
160
|
+
def info
|
161
|
+
{
|
162
|
+
domain: @domain,
|
163
|
+
username: @username
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def check_domain!(domain)
|
170
|
+
raise "Missing domain" if domain.nil? || domain.empty?
|
171
|
+
end
|
172
|
+
|
173
|
+
def check_password!(username, password)
|
174
|
+
unless (username.nil? || username.empty?)
|
175
|
+
raise "Missing password (mandatory if username given)" if password.nil? || password.empty?
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def missing_credential?
|
180
|
+
return true if @username.nil? || @username.empty?
|
181
|
+
return true if @password.nil? || @password.empty?
|
182
|
+
false
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "jira_cache/data"
|
3
|
+
require "active_support/inflector"
|
4
|
+
|
5
|
+
module JiraCache
|
6
|
+
module Data
|
7
|
+
|
8
|
+
# Superclass for repositories. Simply provide some shared
|
9
|
+
# methods.
|
10
|
+
class IssueRepository
|
11
|
+
|
12
|
+
# It inserts a new issue row with the specified data.
|
13
|
+
# If the issue already exists (checking on the "key"),
|
14
|
+
# the row is updated instead.
|
15
|
+
def self.insert(key:, data:, synced_at:, deleted_from_jira_at: nil)
|
16
|
+
attributes = {
|
17
|
+
key: key,
|
18
|
+
data: Sequel.pg_json(data),
|
19
|
+
synced_at: synced_at,
|
20
|
+
deleted_from_jira_at: deleted_from_jira_at
|
21
|
+
}
|
22
|
+
if exist_with_key?(key)
|
23
|
+
update_where({ key: key }, attributes)
|
24
|
+
else
|
25
|
+
table.insert row(attributes)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.find_by_key(key)
|
30
|
+
table.where(key: key).first
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.exist_with_key?(key)
|
34
|
+
table.where(key: key).count != 0
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.keys_in_project(project_key)
|
38
|
+
table.where("(data #>> '{fields,project,key}') = ?", project_key).select(:key).map(&:values).flatten
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.keys_for_non_deleted_issues
|
42
|
+
table
|
43
|
+
.where("deleted_from_jira_at IS NULL")
|
44
|
+
.select(:key)
|
45
|
+
.map(&:values)
|
46
|
+
.flatten
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.keys_for_deleted_issues
|
50
|
+
table
|
51
|
+
.where("deleted_from_jira_at IS NOT NULL")
|
52
|
+
.select(:key)
|
53
|
+
.map(&:values)
|
54
|
+
.flatten
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.delete_where(where_data)
|
58
|
+
table.where(where_data).delete
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.update_where(where_data, values)
|
62
|
+
table.where(where_data).update(values)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.first_where(where_data)
|
66
|
+
table.where(where_data).first
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.index
|
70
|
+
table.entries
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.count
|
74
|
+
table.count
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.latest_sync_time
|
78
|
+
table.order(:synced_at).select(:synced_at).last&.dig(:synced_at)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.row(attributes, time = nil)
|
82
|
+
time ||= Time.now
|
83
|
+
attributes.merge(
|
84
|
+
created_at: time,
|
85
|
+
updated_at: time
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.table
|
90
|
+
DB[:jira_cache_issues]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JiraCache
|
2
|
+
|
3
|
+
# This notifiers simply logs messages using the specified
|
4
|
+
# logger.
|
5
|
+
#
|
6
|
+
# If you want to use this mechanism to trigger actions when
|
7
|
+
# events are triggered in JiraCache, you can use the
|
8
|
+
# `JiraCache::Client.set_notifier(notifier)` method and pass
|
9
|
+
# it an instance of a notifier class implementing the
|
10
|
+
# `#publish` method with the same signature as
|
11
|
+
# `JiraCache::Notifier#publish`.
|
12
|
+
class Notifier
|
13
|
+
|
14
|
+
# Initializes a notifier with the specified logger. The
|
15
|
+
# logger is used to log info messages when #publish
|
16
|
+
# is called.
|
17
|
+
def initialize(logger)
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
|
21
|
+
# Simply logs the event name and data.
|
22
|
+
# @param event_name [String] e.g. "fetched_issue"
|
23
|
+
# @param data [Hash]
|
24
|
+
# - :key [String] issue key
|
25
|
+
# - :data [Hash] issue data
|
26
|
+
def publish(event_name, data = nil)
|
27
|
+
@logger.info "[#{event_name}] #{data[:key]}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "jira_cache/data/issue_repository"
|
3
|
+
require "jira_cache/client"
|
4
|
+
|
5
|
+
module JiraCache
|
6
|
+
|
7
|
+
# Performs the sync between JIRA and the local database
|
8
|
+
# where the issues are cached.
|
9
|
+
#
|
10
|
+
# The issues are cached in the database through the
|
11
|
+
# Data::IssueRepository interface. It currently implements
|
12
|
+
# storage into a PostgreSQL database.
|
13
|
+
class Sync
|
14
|
+
attr_reader :client, :logger
|
15
|
+
|
16
|
+
def initialize(client)
|
17
|
+
@client = client
|
18
|
+
@logger = client.logger
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fetches new and updated raw issues, save them
|
22
|
+
# to the `issues` collection. Also mark issues
|
23
|
+
# deleted from JIRA as such.
|
24
|
+
#
|
25
|
+
# @param project_key [String] the JIRA project key
|
26
|
+
def sync_issues(project_key: nil)
|
27
|
+
sync_start = Time.now
|
28
|
+
|
29
|
+
log "Determining which issues to fetch..."
|
30
|
+
remote = remote_keys(project_key: project_key)
|
31
|
+
log " - #{remote.count} remote issues"
|
32
|
+
|
33
|
+
cached = cached_keys(project_key: project_key)
|
34
|
+
log " - #{cached.count} cached issues"
|
35
|
+
|
36
|
+
missing = remote - cached
|
37
|
+
log " => #{missing.count} missing issues"
|
38
|
+
|
39
|
+
updated = updated_keys(project_key: project_key)
|
40
|
+
log " - #{updated.count} updated issues"
|
41
|
+
|
42
|
+
log "Fetching #{missing.count + updated.count} issues"
|
43
|
+
fetch_issues(missing + updated, sync_start)
|
44
|
+
|
45
|
+
deleted = cached - remote
|
46
|
+
mark_deleted(deleted)
|
47
|
+
end
|
48
|
+
|
49
|
+
def sync_issue(key, sync_time: Time.now)
|
50
|
+
data = client.issue_data(key)
|
51
|
+
Data::IssueRepository.insert(
|
52
|
+
key: key,
|
53
|
+
data: data,
|
54
|
+
synced_at: sync_time
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
# IMPLEMENTATION FUNCTIONS
|
59
|
+
|
60
|
+
def remote_keys(project_key: nil)
|
61
|
+
fetch_issue_keys(project_key: project_key)
|
62
|
+
end
|
63
|
+
|
64
|
+
def cached_keys(project_key: nil)
|
65
|
+
Data::IssueRepository.keys_in_project(project_key: project_key)
|
66
|
+
end
|
67
|
+
|
68
|
+
def updated_keys(project_key: nil)
|
69
|
+
time = latest_sync_time(project_key: project_key)
|
70
|
+
fetch_issue_keys(project_key: project_key, updated_since: time)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Fetch from JIRA
|
74
|
+
|
75
|
+
# Fetch issue keys from JIRA using the specified `JiraCache::Client`
|
76
|
+
# instance, for the specified project, with an optional `updated_since`
|
77
|
+
# parameter.
|
78
|
+
#
|
79
|
+
# @param project_key [String]
|
80
|
+
# @param updated_since [Time]
|
81
|
+
# @return [Array] array of issue keys as strings
|
82
|
+
def fetch_issue_keys(project_key: nil, updated_since: nil)
|
83
|
+
query_items = []
|
84
|
+
query_items << "project = \"#{project_key}\"" unless project_key.nil?
|
85
|
+
query_items << "updatedDate > \"#{updated_since.strftime('%Y-%m-%d %H:%M')}\"" unless updated_since.nil?
|
86
|
+
query = query_items.join(" AND ")
|
87
|
+
client.issue_keys_for_query(query)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param issue_keys [Array] array of strings representing the JIRA keys
|
91
|
+
def fetch_issues(issue_keys, sync_time)
|
92
|
+
issue_keys.each do |issue_key|
|
93
|
+
sync_issue(issue_key, sync_time: sync_time)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def mark_deleted(issue_keys)
|
98
|
+
Data::IssueRepository.update_where({ key: issue_keys }, deleted_from_jira_at: Time.now)
|
99
|
+
end
|
100
|
+
|
101
|
+
def latest_sync_time(project_key)
|
102
|
+
Data::IssueRepository.latest_sync_time
|
103
|
+
end
|
104
|
+
|
105
|
+
def log(message)
|
106
|
+
return if logger.nil?
|
107
|
+
logger.info(message)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|