flowable 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'base64'
7
+ require 'date'
8
+
9
+ require_relative 'version'
10
+
11
+ module Flowable
12
+ class Error < StandardError; end
13
+ class NotFoundError < Error; end
14
+ class UnauthorizedError < Error; end
15
+ class ForbiddenError < Error; end
16
+ class BadRequestError < Error; end
17
+ class ConflictError < Error; end
18
+
19
+ class Client
20
+ CMMN_BASE_PATH = '/flowable-rest/cmmn-api'
21
+ BPMN_BASE_PATH = '/flowable-rest/service'
22
+
23
+ attr_reader :host
24
+ attr_reader :port
25
+ attr_reader :username
26
+ attr_reader :base_path
27
+ attr_reader :bpmn_base_path
28
+
29
+ def initialize(host: 'localhost', port: 8080, username:, password:, base_path: CMMN_BASE_PATH,
30
+ bpmn_base_path: BPMN_BASE_PATH, use_ssl: false)
31
+ @host = host
32
+ @port = port
33
+ @username = username
34
+ @password = password
35
+ @base_path = base_path
36
+ @bpmn_base_path = bpmn_base_path
37
+ @use_ssl = use_ssl
38
+ end
39
+
40
+ # CMMN Resource accessors
41
+ def deployments
42
+ @deployments ||= Resources::Deployments.new(self)
43
+ end
44
+
45
+ def case_definitions
46
+ @case_definitions ||= Resources::CaseDefinitions.new(self)
47
+ end
48
+
49
+ def case_instances
50
+ @case_instances ||= Resources::CaseInstances.new(self)
51
+ end
52
+
53
+ def tasks
54
+ @tasks ||= Resources::Tasks.new(self)
55
+ end
56
+
57
+ def plan_item_instances
58
+ @plan_item_instances ||= Resources::PlanItemInstances.new(self)
59
+ end
60
+
61
+ def history
62
+ @history ||= Resources::History.new(self)
63
+ end
64
+
65
+ # BPMN Resource accessors
66
+ def bpmn_deployments
67
+ @bpmn_deployments ||= Resources::BpmnDeployments.new(self)
68
+ end
69
+
70
+ def process_definitions
71
+ @process_definitions ||= Resources::ProcessDefinitions.new(self)
72
+ end
73
+
74
+ def process_instances
75
+ @process_instances ||= Resources::ProcessInstances.new(self)
76
+ end
77
+
78
+ def executions
79
+ @executions ||= Resources::Executions.new(self)
80
+ end
81
+
82
+ def bpmn_history
83
+ @bpmn_history ||= Resources::BpmnHistory.new(self)
84
+ end
85
+
86
+ # HTTP methods
87
+ def get(path, params = {})
88
+ request(:get, path, params: params)
89
+ end
90
+
91
+ def post(path, body = nil)
92
+ request(:post, path, body: body)
93
+ end
94
+
95
+ def put(path, body = nil)
96
+ request(:put, path, body: body)
97
+ end
98
+
99
+ def delete(path, params = {})
100
+ request(:delete, path, params: params)
101
+ end
102
+
103
+ def post_multipart(path, file_path, additional_fields = {})
104
+ uri = build_uri(path)
105
+ boundary = "----Flowable#{rand(1_000_000)}"
106
+
107
+ body = build_multipart_body(file_path, additional_fields, boundary)
108
+
109
+ http = build_http(uri)
110
+ request = Net::HTTP::Post.new(uri.request_uri)
111
+ request['Authorization'] = auth_header
112
+ request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
113
+ request.body = body
114
+
115
+ handle_response(http.request(request))
116
+ end
117
+
118
+ private
119
+
120
+ def request(method, path, params: {}, body: nil)
121
+ uri = build_uri(path, params)
122
+ http = build_http(uri)
123
+
124
+ request = build_request(method, uri, body)
125
+ handle_response(http.request(request))
126
+ end
127
+
128
+ def build_uri(path, params = {})
129
+ # Determine base path - BPMN resources use service path
130
+ effective_base_path = path.start_with?('service/') || path.start_with?('repository/') || path.start_with?('runtime/') || (path.start_with?('history/') && !path.include?('cmmn')) ? @bpmn_base_path : @base_path
131
+
132
+ # For BPMN paths, strip the 'service/' prefix if present since it's in base path
133
+ adjusted_path = path.start_with?('service/') ? path.sub('service/', '') : path
134
+
135
+ uri = URI::HTTP.build(
136
+ host: @host,
137
+ port: @port,
138
+ path: "#{effective_base_path}/#{adjusted_path}".gsub('//', '/')
139
+ )
140
+ uri.query = URI.encode_www_form(params) unless params.empty?
141
+ uri
142
+ end
143
+
144
+ def build_http(uri)
145
+ http = Net::HTTP.new(uri.host, uri.port)
146
+ http.use_ssl = @use_ssl
147
+ http.read_timeout = 30
148
+ http.open_timeout = 10
149
+ http
150
+ end
151
+
152
+ def build_request(method, uri, body)
153
+ request_class = {
154
+ get: Net::HTTP::Get,
155
+ post: Net::HTTP::Post,
156
+ put: Net::HTTP::Put,
157
+ delete: Net::HTTP::Delete
158
+ }[method]
159
+
160
+ request = request_class.new(uri.request_uri)
161
+ request['Authorization'] = auth_header
162
+ request['Accept'] = 'application/json'
163
+ request['Content-Type'] = 'application/json'
164
+
165
+ if body
166
+ request.body = body.is_a?(String) ? body : body.to_json
167
+ end
168
+
169
+ request
170
+ end
171
+
172
+ def auth_header
173
+ credentials = Base64.strict_encode64("#{@username}:#{@password}")
174
+ "Basic #{credentials}"
175
+ end
176
+
177
+ def handle_response(response)
178
+ case response.code.to_i
179
+ when 200, 201
180
+ parse_response(response)
181
+ when 204
182
+ true
183
+ when 400
184
+ raise BadRequestError, parse_error_message(response)
185
+ when 401
186
+ raise UnauthorizedError, 'Invalid credentials'
187
+ when 404
188
+ raise NotFoundError, parse_error_message(response)
189
+ when 409
190
+ raise ConflictError, parse_error_message(response)
191
+ else
192
+ raise Error, "HTTP #{response.code}: #{parse_error_message(response)}"
193
+ end
194
+ end
195
+
196
+ def parse_response(response)
197
+ return nil if response.body.nil? || response.body.empty?
198
+
199
+ body = response.body
200
+ content_type = response['Content-Type'] || ''
201
+
202
+ # Flowable bug workaround: resourcedata endpoint returns XML with Content-Type: application/json
203
+ # Check if body starts with XML declaration and return raw body
204
+ return body if body.start_with?('<?xml')
205
+
206
+ if content_type.include?('application/json')
207
+ # Handle Flowable bug: when Jackson exceeds nesting limit (1000),
208
+ # it appends an error message to incomplete JSON instead of returning proper error
209
+ if body.include?('{"message":"Bad request","exception":"Could not write JSON: Document nesting depth')
210
+ idx = body.index('{"message":"Bad request"')
211
+ # idx is guaranteed to be non-nil here since we already checked body includes the pattern
212
+ if idx.positive?
213
+ # Try to extract valid JSON before the error
214
+ body = body[0...(idx - 1)]
215
+ end
216
+ end
217
+
218
+ JSON.parse(body, max_nesting: 1500)
219
+ else
220
+ body
221
+ end
222
+ end
223
+
224
+ def parse_error_message(response)
225
+ return response.message if response.body.nil? || response.body.empty?
226
+
227
+ parsed = JSON.parse(response.body)
228
+ parsed['errorMessage'] || parsed['message'] || response.body
229
+ rescue JSON::ParserError
230
+ response.body
231
+ end
232
+
233
+ def build_multipart_body(file_path, additional_fields, boundary)
234
+ body = []
235
+
236
+ # Add file
237
+ filename = File.basename(file_path)
238
+ file_content = File.binread(file_path)
239
+
240
+ body << "--#{boundary}"
241
+ body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\""
242
+ body << 'Content-Type: application/octet-stream'
243
+ body << ''
244
+ body << file_content
245
+
246
+ # Add additional fields
247
+ additional_fields.each do |name, value|
248
+ body << "--#{boundary}"
249
+ body << "Content-Disposition: form-data; name=\"#{name}\""
250
+ body << ''
251
+ body << value.to_s
252
+ end
253
+
254
+ body << "--#{boundary}--"
255
+ body.join("\r\n")
256
+ end
257
+ end
258
+ end
259
+
260
+ # High-level DSL
261
+ require_relative 'resources/base'
262
+ require_relative 'resources/deployments'
263
+ require_relative 'resources/case_definitions'
264
+ require_relative 'resources/case_instances'
265
+ require_relative 'resources/tasks'
266
+ require_relative 'resources/plan_item_instances'
267
+ require_relative 'resources/history'
268
+ require_relative 'resources/bpmn_deployments'
269
+ require_relative 'resources/process_definitions'
270
+ require_relative 'resources/process_instances'
271
+ require_relative 'resources/executions'
272
+ require_relative 'resources/bpmn_history'
273
+ require_relative 'workflow'
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowable
4
+ module Resources
5
+ class Base
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ private
13
+
14
+ def paginate_params(options)
15
+ params = {}
16
+ params[:start] = options[:start] if options[:start]
17
+ params[:size] = options[:size] if options[:size]
18
+ params[:sort] = options[:sort] if options[:sort]
19
+ params[:order] = options[:order] if options[:order]
20
+ params
21
+ end
22
+
23
+ def build_variables_array(variables)
24
+ return [] unless variables
25
+
26
+ variables.map do |name, value|
27
+ var = { name: name.to_s, value: value }
28
+ var[:type] = infer_type(value)
29
+ var
30
+ end
31
+ end
32
+
33
+ def infer_type(value)
34
+ case value
35
+ when Integer then 'long'
36
+ when Float then 'double'
37
+ when TrueClass, FalseClass then 'boolean'
38
+ when Date, Time, DateTime then 'date'
39
+ else 'string'
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowable
4
+ module Resources
5
+ class BpmnDeployments < Base
6
+ BASE_PATH = 'service/repository/deployments'
7
+
8
+ # List all BPMN deployments
9
+ # @param options [Hash] Query parameters
10
+ # @option options [String] :name Filter by exact name
11
+ # @option options [String] :nameLike Filter by name pattern (use % wildcard)
12
+ # @option options [String] :category Filter by category
13
+ # @option options [String] :tenantId Filter by tenant
14
+ # @option options [Integer] :start Pagination start (default: 0)
15
+ # @option options [Integer] :size Page size (default: 10)
16
+ # @option options [String] :sort Sort field (id/name/deployTime/tenantId)
17
+ # @option options [String] :order Sort order (asc/desc)
18
+ # @return [Hash] Paginated list of deployments
19
+ def list(**options)
20
+ params = paginate_params(options)
21
+ params[:name] = options[:name] if options[:name]
22
+ params[:nameLike] = options[:nameLike] if options[:nameLike]
23
+ params[:category] = options[:category] if options[:category]
24
+ params[:tenantId] = options[:tenantId] if options[:tenantId]
25
+ params[:withoutTenantId] = options[:withoutTenantId] if options[:withoutTenantId]
26
+
27
+ client.get(BASE_PATH, params)
28
+ end
29
+
30
+ # Get a specific deployment
31
+ # @param deployment_id [String] The deployment ID
32
+ # @return [Hash] Deployment details
33
+ def get(deployment_id)
34
+ client.get("#{BASE_PATH}/#{deployment_id}")
35
+ end
36
+
37
+ # Create a new deployment from a file
38
+ # @param file_path [String] Path to BPMN file (.bpmn, .bpmn20.xml, .bar, .zip)
39
+ # @param name [String] Optional deployment name
40
+ # @param tenant_id [String] Optional tenant ID
41
+ # @param category [String] Optional category
42
+ # @return [Hash] Created deployment
43
+ def create(file_path, name: nil, tenant_id: nil, category: nil)
44
+ additional_fields = {}
45
+ additional_fields[:deploymentName] = name if name
46
+ additional_fields[:tenantId] = tenant_id if tenant_id
47
+ additional_fields[:category] = category if category
48
+
49
+ client.post_multipart(BASE_PATH, file_path, additional_fields)
50
+ end
51
+
52
+ # Delete a deployment
53
+ # @param deployment_id [String] The deployment ID
54
+ # @param cascade [Boolean] Also delete running process instances
55
+ # @return [Boolean] true if successful
56
+ def delete(deployment_id, cascade: false)
57
+ params = {}
58
+ params[:cascade] = true if cascade
59
+ client.delete("#{BASE_PATH}/#{deployment_id}", params)
60
+ end
61
+
62
+ # List resources in a deployment
63
+ # @param deployment_id [String] The deployment ID
64
+ # @return [Array<Hash>] List of resources
65
+ def resources(deployment_id)
66
+ client.get("#{BASE_PATH}/#{deployment_id}/resources")
67
+ end
68
+
69
+ # Get a specific resource from a deployment
70
+ # @param deployment_id [String] The deployment ID
71
+ # @param resource_id [String] The resource ID (URL-encoded if contains /)
72
+ # @return [Hash] Resource details
73
+ def resource(deployment_id, resource_id)
74
+ encoded_resource_id = URI.encode_www_form_component(resource_id)
75
+ client.get("#{BASE_PATH}/#{deployment_id}/resources/#{encoded_resource_id}")
76
+ end
77
+
78
+ # Get the content of a deployment resource
79
+ # @param deployment_id [String] The deployment ID
80
+ # @param resource_id [String] The resource ID
81
+ # @return [String] Raw resource content
82
+ def resource_data(deployment_id, resource_id)
83
+ encoded_resource_id = URI.encode_www_form_component(resource_id)
84
+ client.get("#{BASE_PATH}/#{deployment_id}/resourcedata/#{encoded_resource_id}")
85
+ end
86
+
87
+ alias resource_content resource_data
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowable
4
+ module Resources
5
+ class BpmnHistory < Base
6
+ # --- Historic Process Instances ---
7
+
8
+ # List historic process instances
9
+ # @param options [Hash] Query parameters
10
+ # @option options [String] :processInstanceId Filter by process instance ID
11
+ # @option options [String] :processDefinitionKey Filter by definition key
12
+ # @option options [String] :processDefinitionId Filter by definition ID
13
+ # @option options [String] :businessKey Filter by business key
14
+ # @option options [String] :involvedUser Filter by involved user
15
+ # @option options [Boolean] :finished Only finished instances
16
+ # @option options [Boolean] :includeProcessVariables Include variables
17
+ # @option options [String] :tenantId Filter by tenant
18
+ # @return [Hash] Paginated list of historic process instances
19
+ def process_instances(**options)
20
+ params = paginate_params(options)
21
+ %i[processInstanceId processDefinitionKey processDefinitionId businessKey
22
+ involvedUser superProcessInstanceId startedBy tenantId tenantIdLike].each do |key|
23
+ params[key] = options[key] if options[key]
24
+ end
25
+
26
+ %i[finished includeProcessVariables withoutTenantId].each do |key|
27
+ params[key] = options[key] if options.key?(key)
28
+ end
29
+
30
+ %i[finishedAfter finishedBefore startedAfter startedBefore].each do |key|
31
+ params[key] = format_date(options[key]) if options[key]
32
+ end
33
+
34
+ client.get('service/history/historic-process-instances', params)
35
+ end
36
+
37
+ # Get a specific historic process instance
38
+ # @param process_instance_id [String] The process instance ID
39
+ # @return [Hash] Historic process instance details
40
+ def process_instance(process_instance_id)
41
+ client.get("service/history/historic-process-instances/#{process_instance_id}")
42
+ end
43
+
44
+ # Delete a historic process instance
45
+ # @param process_instance_id [String] The process instance ID
46
+ # @return [Boolean] true if successful
47
+ def delete_process_instance(process_instance_id)
48
+ client.delete("service/history/historic-process-instances/#{process_instance_id}")
49
+ end
50
+
51
+ # Query historic process instances with complex filters
52
+ # @param query [Hash] Query body
53
+ # @return [Hash] Paginated list of historic process instances
54
+ def query_process_instances(query)
55
+ client.post('service/query/historic-process-instances', query)
56
+ end
57
+
58
+ # Get identity links for a historic process instance
59
+ # @param process_instance_id [String] The process instance ID
60
+ # @return [Array<Hash>] List of identity links
61
+ def process_instance_identity_links(process_instance_id)
62
+ client.get("service/history/historic-process-instances/#{process_instance_id}/identitylinks")
63
+ end
64
+
65
+ # --- Historic Activity Instances ---
66
+
67
+ # List historic activity instances
68
+ # @param options [Hash] Query parameters
69
+ # @option options [String] :activityId Filter by activity ID
70
+ # @option options [String] :activityName Filter by activity name
71
+ # @option options [String] :activityType Filter by type (userTask, serviceTask, etc.)
72
+ # @option options [String] :processInstanceId Filter by process instance
73
+ # @option options [String] :processDefinitionId Filter by process definition
74
+ # @option options [Boolean] :finished Only finished activities
75
+ # @return [Hash] Paginated list of historic activities
76
+ def activity_instances(**options)
77
+ params = paginate_params(options)
78
+ %i[activityId activityName activityType processInstanceId processDefinitionId
79
+ executionId taskAssignee tenantId tenantIdLike].each do |key|
80
+ params[key] = options[key] if options[key]
81
+ end
82
+
83
+ params[:finished] = options[:finished] if options.key?(:finished)
84
+ params[:withoutTenantId] = options[:withoutTenantId] if options.key?(:withoutTenantId)
85
+
86
+ client.get('service/history/historic-activity-instances', params)
87
+ end
88
+
89
+ # Query historic activities with complex filters
90
+ # @param query [Hash] Query body
91
+ # @return [Hash] Paginated list of historic activities
92
+ def query_activity_instances(query)
93
+ client.post('service/query/historic-activity-instances', query)
94
+ end
95
+
96
+ # --- Historic Task Instances ---
97
+
98
+ # List historic task instances
99
+ # @param options [Hash] Query parameters
100
+ # @option options [String] :taskId Filter by task ID
101
+ # @option options [String] :processInstanceId Filter by process instance
102
+ # @option options [String] :processDefinitionId Filter by process definition
103
+ # @option options [String] :taskName Filter by name
104
+ # @option options [String] :taskAssignee Filter by assignee
105
+ # @option options [Boolean] :finished Only finished tasks
106
+ # @return [Hash] Paginated list of historic tasks
107
+ def task_instances(**options)
108
+ params = paginate_params(options)
109
+ %i[taskId processInstanceId processDefinitionId processDefinitionKey
110
+ taskName taskNameLike taskDescription taskDescriptionLike
111
+ taskDefinitionKey taskDeleteReason taskDeleteReasonLike
112
+ taskAssignee taskAssigneeLike taskOwner taskOwnerLike
113
+ taskInvolvedUser taskPriority parentTaskId tenantId tenantIdLike].each do |key|
114
+ params[key] = options[key] if options[key]
115
+ end
116
+
117
+ %i[finished processFinished withoutDueDate includeTaskLocalVariables withoutTenantId].each do |key|
118
+ params[key] = options[key] if options.key?(key)
119
+ end
120
+
121
+ %i[dueDate dueDateAfter dueDateBefore taskCompletedOn taskCompletedAfter
122
+ taskCompletedBefore taskCreatedOn taskCreatedBefore taskCreatedAfter].each do |key|
123
+ params[key] = format_date(options[key]) if options[key]
124
+ end
125
+
126
+ client.get('service/history/historic-task-instances', params)
127
+ end
128
+
129
+ # Get a specific historic task
130
+ # @param task_id [String] The task ID
131
+ # @return [Hash] Historic task details
132
+ def task_instance(task_id)
133
+ client.get("service/history/historic-task-instances/#{task_id}")
134
+ end
135
+
136
+ # Delete a historic task
137
+ # @param task_id [String] The task ID
138
+ # @return [Boolean] true if successful
139
+ def delete_task_instance(task_id)
140
+ client.delete("service/history/historic-task-instances/#{task_id}")
141
+ end
142
+
143
+ # Query historic tasks with complex filters
144
+ # @param query [Hash] Query body
145
+ # @return [Hash] Paginated list of historic tasks
146
+ def query_task_instances(query)
147
+ client.post('service/query/historic-task-instances', query)
148
+ end
149
+
150
+ # --- Historic Variable Instances ---
151
+
152
+ # List historic variable instances
153
+ # @param options [Hash] Query parameters
154
+ # @option options [String] :processInstanceId Filter by process instance
155
+ # @option options [String] :taskId Filter by task
156
+ # @option options [Boolean] :excludeTaskVariables Exclude task variables
157
+ # @option options [String] :variableName Filter by variable name
158
+ # @return [Hash] Paginated list of historic variables
159
+ def variable_instances(**options)
160
+ params = paginate_params(options)
161
+ %i[processInstanceId taskId variableName variableNameLike].each do |key|
162
+ params[key] = options[key] if options[key]
163
+ end
164
+
165
+ params[:excludeTaskVariables] = options[:excludeTaskVariables] if options.key?(:excludeTaskVariables)
166
+
167
+ client.get('service/history/historic-variable-instances', params)
168
+ end
169
+
170
+ # Query historic variables with complex filters
171
+ # @param query [Hash] Query body
172
+ # @return [Hash] Paginated list of historic variables
173
+ def query_variable_instances(query)
174
+ client.post('service/query/historic-variable-instances', query)
175
+ end
176
+
177
+ # --- Historic Detail ---
178
+
179
+ # List historic details (variable updates, form properties)
180
+ # @param options [Hash] Query parameters
181
+ # @option options [String] :processInstanceId Filter by process instance
182
+ # @option options [String] :executionId Filter by execution
183
+ # @option options [String] :activityInstanceId Filter by activity instance
184
+ # @option options [String] :taskId Filter by task
185
+ # @option options [Boolean] :selectOnlyFormProperties Only form properties
186
+ # @option options [Boolean] :selectOnlyVariableUpdates Only variable updates
187
+ # @return [Hash] Paginated list of historic details
188
+ def details(**options)
189
+ params = paginate_params(options)
190
+ %i[processInstanceId executionId activityInstanceId taskId].each do |key|
191
+ params[key] = options[key] if options[key]
192
+ end
193
+
194
+ if options.key?(:selectOnlyFormProperties)
195
+ params[:selectOnlyFormProperties] =
196
+ options[:selectOnlyFormProperties]
197
+ end
198
+ if options.key?(:selectOnlyVariableUpdates)
199
+ params[:selectOnlyVariableUpdates] =
200
+ options[:selectOnlyVariableUpdates]
201
+ end
202
+
203
+ client.get('service/history/historic-detail', params)
204
+ end
205
+
206
+ # Query historic details with complex filters
207
+ # @param query [Hash] Query body
208
+ # @return [Hash] Paginated list of historic details
209
+ def query_details(query)
210
+ client.post('service/query/historic-detail', query)
211
+ end
212
+
213
+ # Aliases for backward compatibility
214
+ alias tasks task_instances
215
+ alias activities activity_instances
216
+ alias variables variable_instances
217
+
218
+ private
219
+
220
+ def format_date(date)
221
+ return date if date.is_a?(String)
222
+ return date.iso8601 if date.respond_to?(:iso8601)
223
+
224
+ date.to_s
225
+ end
226
+ end
227
+ end
228
+ end