cdp-sdk 0.14.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97a8b2da1a07af9656615dc77738e79594b7081079bc2f8a78d94b310306b3b2
4
+ data.tar.gz: 3ded4dda9290e4b64e3f7a0d0ea3c9a3cdf5ed100b88ab4b297a91304f0ae5ef
5
+ SHA512:
6
+ metadata.gz: 8aecaabd802e0277a8ccb7191e32e37db63577e616f8e3ef29034495f0bb84ce3145f336608b696e7d301ded2e532d2ff1ec2002b81790b44c341a5c8ca386f2
7
+ data.tar.gz: 5a0baf5992476d3ab861b3758f02d64b8a04481c69c97622e348145fab9d9c514b41cd7f488981b525100040db77349b4cad0da32fea910fed1ecee81a75e923
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDP
4
+ class Builder
5
+ def initialize(tenant_id:, schema:)
6
+ @tenant_id = tenant_id
7
+
8
+ filtered_schema = schema.to_hash
9
+ filtered_schema[:entity_types].each do |entity_type|
10
+ entity_type[:field_types] = entity_type[:field_types].each_with_object({}) do |field_type, memo|
11
+ memo[field_type[:name]] = field_type.except(:custom_data)
12
+ end
13
+ end
14
+
15
+ filtered_schema[:event_types].each do |event_type|
16
+ event_type[:field_types] = event_type[:field_types].each_with_object({}) do |field_type, memo|
17
+ memo[field_type[:name]] = field_type.except(:custom_data)
18
+ end
19
+ end
20
+
21
+ @schema = filtered_schema
22
+ @builders = generate_builders_map
23
+ end
24
+
25
+ def if_created(source)
26
+ Builders::AfterCreated.new(self, source)
27
+ end
28
+
29
+ def create_transaction
30
+ CDP::Transaction.new(
31
+ tenant_id: @tenant_id,
32
+ commands: []
33
+ )
34
+ end
35
+
36
+ def method_missing(method_name, *_args)
37
+ method_name_str = method_name.to_s.upcase
38
+
39
+ return @builders[method_name_str] if @builders.key?(method_name_str)
40
+
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ # def get_schema!(tenant_id)
47
+ # context = Microservice::Toolkit::Context.create(tenant_id)
48
+ # response = CDP::SchemaClient::Client.get_schema(context, tenant_id)
49
+
50
+ # raise "Failed to get schema from tenant_id: #{tenant_id}" unless response[:success]
51
+
52
+ # response[:schema]
53
+ # end
54
+
55
+ def generate_builders_map
56
+ builders = {}
57
+
58
+ event_types = @schema[:event_types]
59
+ event_types.each do |event_schema|
60
+ builders[event_schema[:name]] = Builders::Event.new(event_schema: event_schema, tenant_id: @tenant_id)
61
+ end
62
+
63
+ entity_types = @schema[:entity_types]
64
+
65
+ entity_types.each do |entity_schema|
66
+ entity_relations = @schema[:relation_types].select do |relation|
67
+ relation[:source_entity_type_name] == entity_schema[:name]
68
+ end
69
+
70
+ builders[entity_schema[:name]] =
71
+ Builders::Entity.new(entity_schema: entity_schema, tenant_id: @tenant_id, entity_relations: entity_relations)
72
+ end
73
+
74
+ builders
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,28 @@
1
+ module CDP
2
+ module Builders
3
+ class AfterCreated
4
+ def initialize(builder, source)
5
+ raise ArgumentError, 'Unsupported multiple commands' if source.commands.size > 1
6
+ raise ArgumentError, 'Only supported on upsert' if source.commands.first&.upsert_entity.blank?
7
+ @builder = builder
8
+ @source = source
9
+ end
10
+
11
+ def relate_to(target)
12
+ raise ArgumentError, 'Unsupported multiple commands' if target.commands.size > 1
13
+ raise ArgumentError, 'Supported only create entity' unless target.commands.first&.create_entity.present?
14
+
15
+ @source.commands.first.upsert_entity.if_created_relate_with << target.commands.first
16
+ @source
17
+ end
18
+
19
+ def create_event(target)
20
+ raise ArgumentError, 'Unsupported multiple commands' if target.commands.size > 1
21
+ raise ArgumentError, 'Supported only create event' unless target.commands.first&.create_event.present?
22
+
23
+ @source.commands.first.upsert_entity.if_created_create_event << target.commands.first
24
+ @source
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cdp/builders/value'
4
+
5
+ module CDP
6
+ module Builders
7
+ class Entity
8
+ include Value
9
+
10
+ ID_FIELD = 'entity_uuid'
11
+ private_constant :ID_FIELD
12
+
13
+ def initialize(tenant_id:, entity_schema:, entity_relations:)
14
+ @tenant_id = tenant_id
15
+ @entity_schema = entity_schema
16
+ @entity_relations = entity_relations
17
+ end
18
+
19
+ def create(attributes)
20
+ validate_attributes_type(attributes)
21
+
22
+ validation_result = Validator.new(field_schema: @entity_schema).create(attributes: attributes)
23
+
24
+ raise ::CDP::InvalidEntityError, validation_result.errors if validation_result.failed?
25
+
26
+ entity_uuid = attributes.delete :entity_uuid
27
+ create_entity = CDP::CreateEntity.new(
28
+ entity_type: @entity_schema[:name],
29
+ entity_uuid: entity_uuid,
30
+ timestamp: Time.now.utc,
31
+ payload: build_payload_with_ref(@entity_schema, attributes)
32
+ )
33
+ build_execute_request({ create_entity: create_entity })
34
+ end
35
+
36
+ def update(attributes, condition:)
37
+ validate_condition(condition)
38
+ validate_attributes_type(attributes)
39
+
40
+ validation_result = Validator.new(field_schema: @entity_schema).update(attributes: attributes)
41
+ raise ::CDP::InvalidEntityError, validation_result.errors if validation_result.failed?
42
+
43
+ update_entity = CDP::UpdateEntity.new(
44
+ entity_type: @entity_schema[:name],
45
+ timestamp: Time.now.utc,
46
+ field_condition: build_condition(condition),
47
+ payload: build_payload(attributes)
48
+ )
49
+ build_execute_request({ update_entity: update_entity })
50
+ end
51
+
52
+ def upsert(payload, unique_by:)
53
+ validate_attributes_type(payload)
54
+
55
+ validation_result = Validator.new(field_schema: @entity_schema).upsert(attributes: payload)
56
+ raise ::CDP::InvalidEntityError, validation_result.errors if validation_result.failed?
57
+
58
+ upsert_entity = CDP::UpsertEntity.new(
59
+ entity_type: @entity_schema[:name],
60
+ timestamp: Time.now.utc,
61
+ unique_by: normalize_unique_by(payload, unique_by),
62
+ payload: build_payload_with_ref(@entity_schema, payload)
63
+ )
64
+ build_execute_request({ upsert_entity: upsert_entity })
65
+ end
66
+
67
+ def create_if_not_exists(payload, unique_by:)
68
+ create_if_not_exists = CDP::CreateIfNotExistsEntity.new(
69
+ entity_type: @entity_schema[:name],
70
+ timestamp: Time.now.utc,
71
+ unique_by: normalize_unique_by(payload, unique_by),
72
+ payload: build_payload_with_ref(@entity_schema, payload)
73
+ )
74
+ build_execute_request({ create_if_not_exists_entity: create_if_not_exists })
75
+ end
76
+
77
+ def delete(condition:)
78
+ validate_condition(condition)
79
+
80
+ delete_entity = CDP::DeleteEntity.new(
81
+ entity_type: @entity_schema[:name],
82
+ timestamp: Time.now.utc,
83
+ field_condition: build_condition(condition)
84
+ )
85
+ build_execute_request({ delete_entity: delete_entity })
86
+ end
87
+
88
+ def append_set_values(condition:, field_name:, values:, timestamp: Time.now.utc)
89
+ validate_condition(condition)
90
+ validate_set_field(field_name)
91
+
92
+ append_values = CDP::AppendValuesToEntitySet.new(
93
+ entity_type: @entity_schema[:name],
94
+ timestamp: timestamp,
95
+ field_condition: build_condition(condition),
96
+ field_name: field_name,
97
+ values: build(@entity_schema, field_name, values).array_value
98
+ )
99
+ build_execute_request({ append_values_to_entity_set: append_values })
100
+ end
101
+
102
+ def delete_set_values(condition:, field_name:, values:, timestamp: Time.now.utc)
103
+ validate_condition(condition)
104
+ validate_set_field(field_name)
105
+
106
+ delete_values = CDP::DeleteValuesFromEntitySet.new(
107
+ entity_type: @entity_schema[:name],
108
+ timestamp: timestamp,
109
+ field_condition: build_condition(condition),
110
+ field_name: field_name,
111
+ values: build(@entity_schema, field_name, values).array_value
112
+ )
113
+ build_execute_request({ delete_values_from_entity_set: delete_values })
114
+ end
115
+
116
+ def method_missing(method_name, args)
117
+ method_name_str = method_name.to_s.downcase
118
+
119
+ builders = entity_relations_builders
120
+ if builders.include?(method_name_str)
121
+ source_id = args.fetch(:source_id)
122
+ target_id = args.fetch(:target_id)
123
+ return builders[method_name_str].call(source_id, target_id)
124
+ end
125
+
126
+ super
127
+ end
128
+
129
+ def exists?(_condition)
130
+ raise NoMethodError, "The 'exists?' for entities is not implemented"
131
+ end
132
+
133
+ private
134
+
135
+ def normalize_unique_by(payload, unnormalized_unique_by)
136
+ unique_by = Array(unnormalized_unique_by).compact.map(&:to_sym)
137
+ raise ArgumentError, 'Field unique by empty' if unique_by.empty?
138
+
139
+ payload_keys = payload.symbolize_keys.keys
140
+ raise ArgumentError, 'Field unique by is required in payload' unless unique_by.all? do |field|
141
+ payload_keys.include?(field)
142
+ end
143
+
144
+ unique_by
145
+ end
146
+
147
+ def validate_attributes_type(attributes)
148
+ raise ArgumentError, 'attributes should be a Hash' unless attributes.is_a?(Hash)
149
+ end
150
+
151
+ def validate_condition(condition)
152
+ raise ArgumentError, 'condition should be a Hash' unless condition.is_a?(Hash)
153
+
154
+ is_correlator = Validator.new(field_schema: @entity_schema).is_correlator?(field_name: condition[:field_name])
155
+ raise InvalidCorrelatorError, "Field: #{condition[:field_name]} is not a correlator." unless is_correlator
156
+ end
157
+
158
+ def validate_set_field(field_name)
159
+ field_type = @entity_schema[:field_types][field_name.to_s]
160
+ raise CDP::SchemaMissingFieldError, "invalid field #{field_name}, is not on schema" unless field_type
161
+
162
+ unless field_type[:data_type] == 'SET'
163
+ raise CDP::InvalidFieldTypeError,
164
+ "invalid field #{field_name} type, is not a SET"
165
+ end
166
+ end
167
+
168
+ def entity_relations_builders
169
+ @entity_relations.each_with_object({}) do |relation, memo|
170
+ target_type = relation[:target_entity_type_name]
171
+ memo["add_#{target_type.downcase}_relation"] = lambda { |source_id, target_id|
172
+ add_relation(target_type, source_id, target_id)
173
+ }
174
+
175
+ memo["remove_#{target_type.downcase}_relation"] = lambda { |source_id, target_id|
176
+ remove_relation(target_type, source_id, target_id)
177
+ }
178
+ end
179
+ end
180
+
181
+ def add_relation(target_entity_type, source_id, target_id)
182
+ create_entity_relation = CDP::CreateEntityRelation.new(
183
+ source_entity_type: @entity_schema[:name],
184
+ source_entity_id: source_id,
185
+ target_entity_type: target_entity_type,
186
+ target_entity_id: target_id,
187
+ timestamp: Time.now.utc
188
+ )
189
+ build_execute_request({ create_entity_relation: create_entity_relation })
190
+ end
191
+
192
+ def remove_relation(target_entity_type, source_id, target_id)
193
+ delete_entity_relation = CDP::DeleteEntityRelation.new(
194
+ source_entity_type: @entity_schema[:name],
195
+ source_entity_id: source_id,
196
+ target_entity_type: target_entity_type,
197
+ target_entity_id: target_id,
198
+ timestamp: Time.now.utc
199
+ )
200
+ build_execute_request({ delete_entity_relation: delete_entity_relation })
201
+ end
202
+
203
+ def build_execute_request(command_payload)
204
+ CDP::Transaction.new(
205
+ tenant_id: @tenant_id,
206
+ commands: [CDP::Command.new(command_payload.merge(id: SecureRandom.uuid))]
207
+ )
208
+ end
209
+
210
+ def build_condition(condition)
211
+ field_name, field_value = condition.values
212
+ field_value = if ID_FIELD == field_name
213
+ CDP::Value.create_string(field_value)
214
+ else
215
+ build(@entity_schema, field_name, field_value)
216
+ end
217
+
218
+ CDP::FieldCondition.new(
219
+ field_name: field_name,
220
+ field_value: field_value
221
+ )
222
+ end
223
+
224
+ def build_payload(attributes)
225
+ attributes.each_with_object({}) do |(name, value), memo|
226
+ memo[name] = build(@entity_schema, name, value)
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cdp/builders/value'
4
+
5
+ module CDP
6
+ module Builders
7
+ class Event
8
+ include Value
9
+
10
+ def initialize(tenant_id:, event_schema:)
11
+ @tenant_id = tenant_id
12
+ @event_schema = event_schema
13
+ end
14
+
15
+ def create(event)
16
+ event = event.symbolize_keys
17
+
18
+ raise ArgumentError, 'attribute event_identifier is missing' unless event.key?(:event_identifier)
19
+
20
+ raise ArgumentError, 'attribute event_identifier is empty' if event[:event_identifier].empty?
21
+
22
+ payload = event.except(:event_uuid, :event_identifier, :event_timestamp, :entity_uuid)
23
+
24
+ validation_result = Validator.new(field_schema: @event_schema).create(attributes: payload)
25
+
26
+ raise ::CDP::InvalidEntityError, validation_result.errors if validation_result.failed?
27
+
28
+ CDP::Transaction.new(
29
+ tenant_id: @tenant_id,
30
+ commands: [
31
+ CDP::Command.new(
32
+ id: SecureRandom.uuid,
33
+ create_event: CDP::CreateEvent.new(
34
+ event_type: @event_schema[:name],
35
+ event_uuid: event[:event_uuid] || SecureRandom.uuid,
36
+ event_identifier: event[:event_identifier],
37
+ event_timestamp: event[:event_timestamp] || Time.now.utc,
38
+ entity_uuid: event[:entity_uuid],
39
+ payload: build_payload_with_ref(@event_schema, payload)
40
+ )
41
+ )
42
+ ]
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDP
4
+ module Builders
5
+ module Value
6
+ module_function
7
+
8
+ VALID_COMMAND_TYPE = %i[upsert_entity create_entity update_entity create_if_not_exists_entity].freeze
9
+
10
+ def build_payload_with_ref(schema, attributes, unsupported_types=[])
11
+ attributes.each_with_object({}) do |(name, value), memo|
12
+ if value.class == CDP::Transaction
13
+ raise ArgumentError, 'Unsupported multiple commands' if value.commands.size > 1
14
+ raise ArgumentError, 'Unsupported command' unless supported_command(value.commands.first)
15
+
16
+ memo[name] = CDP::Value.new(
17
+ reference: CDP::CommandReference.new(id: value.commands.first.id)
18
+ )
19
+ next
20
+ end
21
+ memo[name] = Value.build(schema, name, value, unsupported_types)
22
+ end
23
+ end
24
+
25
+ def supported_command(command)
26
+ VALID_COMMAND_TYPE.include?(command.command_type)
27
+ end
28
+
29
+ def build(schema, field_name, field_value, unsupported_types=[])
30
+ field_type = schema[:field_types][field_name.to_s]
31
+
32
+ raise CDP::SchemaMissingFieldError, "invalid field #{field_name}, is not on schema" unless field_type
33
+ raise ArgumentError, "unsupported type #{field_type[:data_type]} (#{field_type[:name]})" if unsupported_types.include?(field_type[:data_type])
34
+
35
+ case field_type[:data_type]
36
+ when 'STRING'
37
+ CDP::Value.create_string(field_value)
38
+ when 'TEXT'
39
+ CDP::Value.create_text(field_value)
40
+ when 'BOOLEAN'
41
+ CDP::Value.create_bool(field_value)
42
+ when 'INTEGER'
43
+ CDP::Value.create_int(field_value)
44
+ when 'FLOAT'
45
+ CDP::Value.create_float(field_value)
46
+ when 'TIMESTAMP'
47
+ CDP::Value.create_timestamp(field_value)
48
+ when 'SET'
49
+ CDP::Value.create_set_values(field_value)
50
+ else
51
+ raise "invalid value type: #{field_type[:name]} (#{field_type[:data_type]}) = '#{field_value}'"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/cdp/client.rb ADDED
@@ -0,0 +1,64 @@
1
+ require_relative '../cdp/retryable'
2
+ require_relative '../configuration'
3
+
4
+ module CDP
5
+ class Client
6
+ include CDP::Retryable
7
+ include Configuration
8
+ attr_reader :legacy
9
+
10
+ AVAILABLE_PRIORITIES = %i[low normal high]
11
+
12
+ def initialize(service_name:, priority: :normal, url: CDP_CORE_SERVICE_URL, secure: :this_channel_is_insecure, retries: 3)
13
+ validate_priorities(priority)
14
+ @service_name = service_name
15
+ @priority = priority
16
+ @retries = retries
17
+ @service = CDP::Service::Stub.new(url, secure)
18
+ @search_service = CDP::SearchService::Stub.new(url, secure)
19
+ @legacy = CDP::Legacy::Service::Stub.new(url, secure)
20
+ end
21
+
22
+ def execute_sync(transaction, priority: @priority)
23
+ validate_priorities(priority)
24
+ response = retry_block(limit: @retries) {
25
+ @service.execute_sync(transaction, metadata(priority: priority))
26
+ }
27
+ response
28
+ end
29
+
30
+ def execute_async(transaction, priority: @priority)
31
+ validate_priorities(priority)
32
+ response = retry_block(limit: @retries) {
33
+ @service.execute_async(transaction, metadata(priority: priority))
34
+ }
35
+ response
36
+ end
37
+
38
+ def search(request:)
39
+ retry_block(limit: @retries) {
40
+ @search_service.execute(request)
41
+ }
42
+ end
43
+
44
+ def method_missing(method_name, *_args)
45
+ Query::Actions.new(self, method_name)
46
+ end
47
+
48
+ private
49
+
50
+ def validate_priorities(priority)
51
+ raise ArgumentError, 'invalid priority' unless AVAILABLE_PRIORITIES.include?(priority)
52
+ end
53
+
54
+ def metadata(priority:)
55
+ {
56
+ metadata: {
57
+ service_name: @service_name,
58
+ priority: priority.to_s,
59
+ version: CDP::VERSION
60
+ }
61
+ }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ require_relative '../cdp/retryable'
2
+ require_relative '../configuration'
3
+
4
+ module CDP
5
+ class Persistency
6
+ class << self
7
+ include CDP::Retryable
8
+ include Configuration
9
+
10
+ def new(tenant_id:)
11
+ schema = get_schema(tenant_id)
12
+ CDP::Builder.new(tenant_id: tenant_id, schema: schema)
13
+ end
14
+
15
+ def execute(requests:, retries: 3)
16
+ raise ArgumentError, 'requests should be an Array' unless requests.is_a?(Array)
17
+
18
+ response = retry_block(limit: retries) {
19
+ client.execute(requests)
20
+ }
21
+ end
22
+
23
+ def client
24
+ @client ||= default_client
25
+ end
26
+
27
+ def client=(client)
28
+ @client = client
29
+ end
30
+
31
+ private
32
+
33
+ def default_client
34
+ CDP::PersistencyService::Stub.new(CDP_CORE_SERVICE_URL, :this_channel_is_insecure)
35
+ end
36
+
37
+ def get_schema(tenant_id)
38
+ ctx = Mstk::Context.create(tenant_id)
39
+ response = CDP::SchemaClient::Client.get_schema(ctx, tenant_id)
40
+ success, error, schema = response.values_at(:success, :error, :schema)
41
+
42
+ raise "Could not get schema for tenant '#{@tenant_id}': '#{error}'" unless success
43
+
44
+ schema
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDP
4
+ module Query
5
+ class Actions
6
+ def initialize(service, object_type)
7
+ @service = service
8
+ @object_type = object_type.to_s.upcase
9
+ end
10
+
11
+ def exists?(tenant_id:, condition:)
12
+ request = CDP::SearchRequest.new(
13
+ tenant_id: tenant_id,
14
+ exists: CDP::Exists.new(
15
+ type: @object_type,
16
+ field_condition: build_condition(condition)
17
+ )
18
+ )
19
+
20
+ @service.search(request: request)
21
+ end
22
+
23
+
24
+ def distinct(tenant_id:, fields:, condition:)
25
+ request = CDP::SearchRequest.new(
26
+ tenant_id: tenant_id,
27
+ distinct: CDP::Distinct.new(
28
+ type: @object_type,
29
+ fields: Array(fields),
30
+ field_condition: build_condition(condition)
31
+ )
32
+ )
33
+
34
+ @service.search(request: request)
35
+ end
36
+
37
+ private
38
+
39
+ def build_condition(condition)
40
+ raise ArgumentError, 'condition should be a Hash' unless condition.is_a?(Hash)
41
+
42
+ raise ArgumentError, 'condition should has one value' unless condition.length != 0
43
+
44
+ field_name, field_value = condition.first
45
+
46
+ field_value = Array(field_value)
47
+
48
+ CDP::FieldCondition.new(
49
+ field_name: field_name,
50
+ field_value: CDP::Value.create_set_values(field_value)
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ require 'grpc'
2
+
3
+ module CDP
4
+ module Retryable
5
+ DEFAULT_CDP_CLIENT_RETRYABLE_ERRORS = 'Errno::ECONNRESET,Errno::ECONNREFUSED,GRPC::ResourceExhausted,GRPC::Unavailable'
6
+
7
+ CDP_CLIENT_RETRYABLE_ERRORS = [
8
+ ENV.fetch('CDP_CLIENT_RETRYABLE_ERRORS', DEFAULT_CDP_CLIENT_RETRYABLE_ERRORS).split(',')
9
+ ].flatten
10
+
11
+ def retry_block(limit: 3, backoff: 1)
12
+ tries = 0
13
+
14
+ begin
15
+ yield
16
+ rescue => e
17
+ if CDP_CLIENT_RETRYABLE_ERRORS.include?(e.class.to_s) && tries < limit
18
+ sleep backoff * tries
19
+ tries += 1
20
+ retry
21
+ end
22
+
23
+ raise
24
+ end
25
+ end
26
+ end
27
+ end