cdp-sdk 0.14.2

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