activeitem 0.0.6 → 0.0.8
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +15 -0
- data/lib/active_item/base.rb +4 -0
- data/lib/active_item/errors.rb +10 -0
- data/lib/active_item/relation.rb +290 -2
- data/lib/active_item/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e8035a54e42976c535c533e7187609138bf20f626e3ae031d311e1306ed7bf2
|
|
4
|
+
data.tar.gz: dabe50cd80d38b1708f028b126056c2e89f61ecaafe1fea3390108fde9cd1439
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d167c59a7ae52de90c7a084740a96c271e88c8d74bdd389544c606977bf652d35e73614026c248ed5ad1b42ba44e8ee7b623060dc5b1852a519ccb93082614c
|
|
7
|
+
data.tar.gz: d3fb65a11b3fcfe603ca2f7abbd7c2dc54da5cc119ddb0ca423e535e6210d50503a59401e7f112f395c50b817cf8ec6a154526ca73370c35b78f6492823b75f4
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.8
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Array partition key fan-out** — `where(partition_key: [val1, val2, ...])` now queries each partition in parallel and merges results, instead of raising `ArgumentError`
|
|
8
|
+
- Works with `.to_a`, `.page(cursor, per_page:)`, `.count`, `.order(:desc)`, `.not(...)`, and all other chainable methods
|
|
9
|
+
- Pagination uses a `sort_val|id` cursor format for cross-partition consistency
|
|
10
|
+
- Thread-pooled execution (max 10 concurrent queries) matching existing `preload_has_many_counts` pattern
|
|
11
|
+
|
|
12
|
+
## 0.0.7
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `destroy!` — raises `ActiveItem::RecordNotDestroyed` when the record cannot be destroyed (e.g., callbacks halt the chain)
|
|
17
|
+
|
|
3
18
|
## 0.0.6
|
|
4
19
|
|
|
5
20
|
### Added
|
data/lib/active_item/base.rb
CHANGED
data/lib/active_item/errors.rb
CHANGED
|
@@ -18,6 +18,16 @@ module ActiveItem
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Raised by destroy! when the record is not destroyed.
|
|
22
|
+
class RecordNotDestroyed < StandardError
|
|
23
|
+
attr_reader :record
|
|
24
|
+
|
|
25
|
+
def initialize(message = nil, record = nil)
|
|
26
|
+
@record = record
|
|
27
|
+
super(message || 'Failed to destroy the record')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
# Raised when a record cannot be deleted because dependent associations
|
|
22
32
|
# with :restrict_with_exception still exist.
|
|
23
33
|
class DeleteRestrictionError < StandardError
|
data/lib/active_item/relation.rb
CHANGED
|
@@ -507,10 +507,20 @@ module ActiveItem
|
|
|
507
507
|
return [[], nil] if conditions[:_empty]
|
|
508
508
|
|
|
509
509
|
normalized_conditions = normalize_conditions(conditions)
|
|
510
|
-
exclusive_start_key = decode_cursor(cursor)
|
|
511
510
|
|
|
512
511
|
effective_index = index_name || resolved_model.send(:detect_index_for_conditions, normalized_conditions)
|
|
513
512
|
|
|
513
|
+
# Array partition key fan-out uses a simple string cursor (sort_val|id), not DynamoDB LastEvaluatedKey
|
|
514
|
+
if effective_index && normalized_conditions.any? && normalized_conditions.values.first.is_a?(Array)
|
|
515
|
+
index_config = resolved_model.indexes[effective_index] || {}
|
|
516
|
+
ruby_partition_key = normalized_conditions.keys.first.to_s
|
|
517
|
+
dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
|
|
518
|
+
return fanout_paginated_query(effective_index, normalized_conditions, index_config, dynamo_partition_key,
|
|
519
|
+
normalized_conditions.values.first, cursor, per_page)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
exclusive_start_key = decode_cursor(cursor)
|
|
523
|
+
|
|
514
524
|
if effective_index && normalized_conditions.any?
|
|
515
525
|
paginated_query_with_index(effective_index, normalized_conditions, exclusive_start_key, per_page)
|
|
516
526
|
else
|
|
@@ -755,6 +765,9 @@ module ActiveItem
|
|
|
755
765
|
|
|
756
766
|
effective_index = (index_name || resolved_model.send(:detect_index_for_conditions, normalized_conditions) if normalized_conditions.any?)
|
|
757
767
|
|
|
768
|
+
# Array partition key: fan-out count queries in parallel
|
|
769
|
+
return fanout_count_query(effective_index, normalized_conditions) if effective_index && normalized_conditions.any? && normalized_conditions.values.first.is_a?(Array)
|
|
770
|
+
|
|
758
771
|
total = 0
|
|
759
772
|
exclusive_start_key = nil
|
|
760
773
|
|
|
@@ -1034,7 +1047,7 @@ module ActiveItem
|
|
|
1034
1047
|
index_config = resolved_model.indexes[idx_name] || {}
|
|
1035
1048
|
dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
|
|
1036
1049
|
|
|
1037
|
-
|
|
1050
|
+
return fanout_query(idx_name, normalized_conditions, index_config, dynamo_partition_key, partition_value) if partition_value.is_a?(Array)
|
|
1038
1051
|
|
|
1039
1052
|
raise ArgumentError, 'Range values not supported for partition key queries. Use scan instead.' if partition_value.is_a?(Range)
|
|
1040
1053
|
|
|
@@ -1120,6 +1133,281 @@ module ActiveItem
|
|
|
1120
1133
|
all_items
|
|
1121
1134
|
end
|
|
1122
1135
|
|
|
1136
|
+
# Fan-out parallel queries when partition key is an Array.
|
|
1137
|
+
# Queries each partition key in parallel, merges and optionally sorts results.
|
|
1138
|
+
def fanout_query(idx_name, _normalized_conditions, index_config, dynamo_partition_key, partition_values)
|
|
1139
|
+
return [] if partition_values.empty?
|
|
1140
|
+
|
|
1141
|
+
sort_key = index_config[:sort_key]&.to_s
|
|
1142
|
+
remaining_conditions = conditions.to_a[1..]
|
|
1143
|
+
filter_parts, filter_names, filter_values = build_fanout_filters(sort_key, remaining_conditions)
|
|
1144
|
+
|
|
1145
|
+
all_items = []
|
|
1146
|
+
mutex = Mutex.new
|
|
1147
|
+
max_concurrency = 10
|
|
1148
|
+
|
|
1149
|
+
partition_values.each_slice(max_concurrency) do |batch|
|
|
1150
|
+
threads = batch.map do |pk_val|
|
|
1151
|
+
Thread.new do
|
|
1152
|
+
params = {
|
|
1153
|
+
table_name: resolved_model.table_name,
|
|
1154
|
+
index_name: idx_name,
|
|
1155
|
+
key_condition_expression: '#pk = :pk_val',
|
|
1156
|
+
expression_attribute_names: { '#pk' => dynamo_partition_key }.merge(filter_names),
|
|
1157
|
+
expression_attribute_values: { ':pk_val' => pk_val }.merge(filter_values)
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1161
|
+
params[:filter_expression] = filter_parts.join(' AND ') if filter_parts.any?
|
|
1162
|
+
params[:limit] = limit_value if limit_value
|
|
1163
|
+
params[:scan_index_forward] = (order_direction != :desc) unless order_direction.nil?
|
|
1164
|
+
apply_projection_expression!(params)
|
|
1165
|
+
|
|
1166
|
+
items = []
|
|
1167
|
+
exclusive_start_key = nil
|
|
1168
|
+
loop do
|
|
1169
|
+
params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
|
1170
|
+
response = resolved_model.dynamodb.query(params)
|
|
1171
|
+
items.concat(response.items.map { |item| resolved_model.instantiate(item) })
|
|
1172
|
+
exclusive_start_key = response.last_evaluated_key
|
|
1173
|
+
break unless exclusive_start_key
|
|
1174
|
+
break if limit_value && items.length >= limit_value
|
|
1175
|
+
end
|
|
1176
|
+
mutex.synchronize { all_items.concat(items) }
|
|
1177
|
+
end
|
|
1178
|
+
end
|
|
1179
|
+
threads.each(&:join)
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
all_items = apply_ilike_filter(all_items)
|
|
1183
|
+
all_items = sort_fanout_results(all_items, sort_key)
|
|
1184
|
+
all_items = all_items.first(limit_value) if limit_value
|
|
1185
|
+
all_items
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
# Fan-out paginated query across multiple partition keys.
|
|
1189
|
+
# Uses a composite cursor that encodes sort_key|id position for cross-partition pagination.
|
|
1190
|
+
def fanout_paginated_query(idx_name, _normalized_conditions, index_config, dynamo_partition_key, partition_values,
|
|
1191
|
+
cursor, per_page)
|
|
1192
|
+
return [[], nil] if partition_values.empty?
|
|
1193
|
+
|
|
1194
|
+
sort_key = index_config[:sort_key]&.to_s
|
|
1195
|
+
remaining_conditions = conditions.to_a[1..]
|
|
1196
|
+
filter_parts, filter_names, filter_values = build_fanout_filters(sort_key, remaining_conditions)
|
|
1197
|
+
cursor_sort_val, cursor_id = decode_fanout_cursor(cursor)
|
|
1198
|
+
|
|
1199
|
+
all_items = []
|
|
1200
|
+
mutex = Mutex.new
|
|
1201
|
+
max_concurrency = 10
|
|
1202
|
+
|
|
1203
|
+
partition_values.each_slice(max_concurrency) do |batch|
|
|
1204
|
+
threads = batch.map do |pk_val|
|
|
1205
|
+
Thread.new do
|
|
1206
|
+
params = {
|
|
1207
|
+
table_name: resolved_model.table_name,
|
|
1208
|
+
index_name: idx_name,
|
|
1209
|
+
key_condition_expression: '#pk = :pk_val',
|
|
1210
|
+
expression_attribute_names: { '#pk' => dynamo_partition_key }.merge(filter_names),
|
|
1211
|
+
expression_attribute_values: { ':pk_val' => pk_val }.merge(filter_values)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1215
|
+
|
|
1216
|
+
# Apply cursor range condition to skip already-seen items
|
|
1217
|
+
if cursor_sort_val && sort_key
|
|
1218
|
+
sk_placeholder = '#fanout_sk'
|
|
1219
|
+
val_placeholder = ':fanout_sk_val'
|
|
1220
|
+
op = order_direction == :desc ? '<=' : '>='
|
|
1221
|
+
params[:key_condition_expression] += " AND #{sk_placeholder} #{op} #{val_placeholder}"
|
|
1222
|
+
params[:expression_attribute_names][sk_placeholder] = sort_key
|
|
1223
|
+
params[:expression_attribute_values][val_placeholder] = cursor_sort_val
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
params[:filter_expression] = filter_parts.join(' AND ') if filter_parts.any?
|
|
1227
|
+
params[:scan_index_forward] = (order_direction != :desc) unless order_direction.nil?
|
|
1228
|
+
# Fetch enough to fill a page after merge
|
|
1229
|
+
params[:limit] = per_page + 1
|
|
1230
|
+
|
|
1231
|
+
items = []
|
|
1232
|
+
exclusive_start_key = nil
|
|
1233
|
+
loop do
|
|
1234
|
+
params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
|
1235
|
+
response = resolved_model.dynamodb.query(params)
|
|
1236
|
+
items.concat(response.items.map { |item| resolved_model.instantiate(item) })
|
|
1237
|
+
exclusive_start_key = response.last_evaluated_key
|
|
1238
|
+
break unless exclusive_start_key
|
|
1239
|
+
break if items.length >= per_page + 1
|
|
1240
|
+
end
|
|
1241
|
+
mutex.synchronize { all_items.concat(items) }
|
|
1242
|
+
end
|
|
1243
|
+
end
|
|
1244
|
+
threads.each(&:join)
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
all_items = apply_ilike_filter(all_items)
|
|
1248
|
+
all_items = sort_fanout_results(all_items, sort_key)
|
|
1249
|
+
|
|
1250
|
+
# Skip items at or before the cursor position
|
|
1251
|
+
if cursor_sort_val && cursor_id
|
|
1252
|
+
all_items = all_items.drop_while do |item|
|
|
1253
|
+
sk_val = if sort_key
|
|
1254
|
+
item.respond_to?(sort_key) ? item.send(sort_key) : item.send(underscore(sort_key))
|
|
1255
|
+
end
|
|
1256
|
+
sk_val ||= item.respond_to?(:created_at) ? item.created_at : nil
|
|
1257
|
+
cmp = compare_with_nil(sk_val, cursor_sort_val)
|
|
1258
|
+
if order_direction == :desc
|
|
1259
|
+
cmp.positive? || (cmp.zero? && item.id >= cursor_id)
|
|
1260
|
+
else
|
|
1261
|
+
cmp.negative? || (cmp.zero? && item.id <= cursor_id)
|
|
1262
|
+
end
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
page_items = all_items.first(per_page)
|
|
1267
|
+
next_cursor = (encode_fanout_cursor(page_items.last, sort_key) if all_items.length > per_page && page_items.last)
|
|
1268
|
+
|
|
1269
|
+
[page_items, next_cursor]
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
def build_fanout_filters(sort_key, remaining_conditions)
|
|
1273
|
+
filter_parts = []
|
|
1274
|
+
filter_names = {}
|
|
1275
|
+
filter_values = {}
|
|
1276
|
+
|
|
1277
|
+
# Exclude sort key conditions — those go into key_condition_expression
|
|
1278
|
+
non_sort_conditions = remaining_conditions.reject do |k, _|
|
|
1279
|
+
sort_key && (resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key)
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
non_sort_conditions.each_with_index do |(attr, val), idx|
|
|
1283
|
+
expr, names, values = resolved_model.send(:build_condition_expression, attr, val, idx, ilike: ilike)
|
|
1284
|
+
filter_parts << expr
|
|
1285
|
+
filter_names.merge!(names)
|
|
1286
|
+
filter_values.merge!(values) if values.any?
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
not_conditions.each_with_index do |(attr, val), idx|
|
|
1290
|
+
expr, names, values = build_not_condition_expression(attr, val, "not#{idx}")
|
|
1291
|
+
filter_parts << expr
|
|
1292
|
+
filter_names.merge!(names)
|
|
1293
|
+
filter_values.merge!(values) if values.any?
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
[filter_parts, filter_names, filter_values]
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
def apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1300
|
+
return unless sort_key && remaining_conditions.any?
|
|
1301
|
+
|
|
1302
|
+
sort_condition = remaining_conditions.find do |k, _|
|
|
1303
|
+
resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key
|
|
1304
|
+
end
|
|
1305
|
+
return unless sort_condition
|
|
1306
|
+
|
|
1307
|
+
_, sort_value = sort_condition
|
|
1308
|
+
if sort_value.is_a?(Range)
|
|
1309
|
+
range_condition = resolved_model.send(:build_sort_key_range_condition, sort_key, sort_value)
|
|
1310
|
+
params[:key_condition_expression] += " AND #{range_condition[:expression]}"
|
|
1311
|
+
params[:expression_attribute_names].merge!(range_condition[:names])
|
|
1312
|
+
params[:expression_attribute_values].merge!(range_condition[:values])
|
|
1313
|
+
else
|
|
1314
|
+
params[:key_condition_expression] += ' AND #sk = :sk_val'
|
|
1315
|
+
params[:expression_attribute_names]['#sk'] = sort_key
|
|
1316
|
+
params[:expression_attribute_values][':sk_val'] = sort_value
|
|
1317
|
+
end
|
|
1318
|
+
end
|
|
1319
|
+
|
|
1320
|
+
def sort_fanout_results(items, sort_key)
|
|
1321
|
+
return items unless sort_key
|
|
1322
|
+
|
|
1323
|
+
sorted = items.sort_by do |item|
|
|
1324
|
+
sk_val = item.respond_to?(sort_key) ? item.send(sort_key) : item.send(underscore(sort_key))
|
|
1325
|
+
sk_val ||= ''
|
|
1326
|
+
if order_direction == :desc
|
|
1327
|
+
[sk_val, item.id].map { |v| v.is_a?(String) ? v : v.to_s }
|
|
1328
|
+
else
|
|
1329
|
+
[sk_val, item.id]
|
|
1330
|
+
end
|
|
1331
|
+
end
|
|
1332
|
+
|
|
1333
|
+
order_direction == :desc ? sorted.reverse : sorted
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
def decode_fanout_cursor(cursor)
|
|
1337
|
+
return [nil, nil] if cursor.nil? || cursor.empty?
|
|
1338
|
+
|
|
1339
|
+
sort_val, id = cursor.split('|', 2)
|
|
1340
|
+
[sort_val, id]
|
|
1341
|
+
rescue StandardError
|
|
1342
|
+
[nil, nil]
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
def encode_fanout_cursor(item, sort_key)
|
|
1346
|
+
sk_val = if sort_key
|
|
1347
|
+
item.respond_to?(sort_key) ? item.send(sort_key) : item.send(underscore(sort_key))
|
|
1348
|
+
end
|
|
1349
|
+
sk_val ||= item.respond_to?(:created_at) ? item.created_at : nil
|
|
1350
|
+
"#{sk_val}|#{item.id}"
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
def compare_with_nil(left, right)
|
|
1354
|
+
return 0 if left.nil? && right.nil?
|
|
1355
|
+
return -1 if left.nil?
|
|
1356
|
+
return 1 if right.nil?
|
|
1357
|
+
|
|
1358
|
+
left <=> right
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
def underscore(str)
|
|
1362
|
+
str.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
def fanout_count_query(idx_name, normalized_conditions)
|
|
1366
|
+
partition_values = normalized_conditions.values.first
|
|
1367
|
+
index_config = resolved_model.indexes[idx_name] || {}
|
|
1368
|
+
ruby_partition_key = normalized_conditions.keys.first.to_s
|
|
1369
|
+
dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
|
|
1370
|
+
sort_key = index_config[:sort_key]&.to_s
|
|
1371
|
+
remaining_conditions = conditions.to_a[1..]
|
|
1372
|
+
filter_parts, filter_names, filter_values = build_fanout_filters(sort_key, remaining_conditions)
|
|
1373
|
+
|
|
1374
|
+
total = 0
|
|
1375
|
+
mutex = Mutex.new
|
|
1376
|
+
max_concurrency = 10
|
|
1377
|
+
|
|
1378
|
+
partition_values.each_slice(max_concurrency) do |batch|
|
|
1379
|
+
threads = batch.map do |pk_val|
|
|
1380
|
+
Thread.new do
|
|
1381
|
+
params = {
|
|
1382
|
+
table_name: resolved_model.table_name,
|
|
1383
|
+
index_name: idx_name,
|
|
1384
|
+
select: 'COUNT',
|
|
1385
|
+
key_condition_expression: '#pk = :pk_val',
|
|
1386
|
+
expression_attribute_names: { '#pk' => dynamo_partition_key }.merge(filter_names),
|
|
1387
|
+
expression_attribute_values: { ':pk_val' => pk_val }.merge(filter_values)
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1391
|
+
params[:filter_expression] = filter_parts.join(' AND ') if filter_parts.any?
|
|
1392
|
+
|
|
1393
|
+
count = 0
|
|
1394
|
+
exclusive_start_key = nil
|
|
1395
|
+
loop do
|
|
1396
|
+
params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
|
1397
|
+
response = resolved_model.dynamodb.query(params)
|
|
1398
|
+
count += response.count
|
|
1399
|
+
exclusive_start_key = response.last_evaluated_key
|
|
1400
|
+
break unless exclusive_start_key
|
|
1401
|
+
end
|
|
1402
|
+
mutex.synchronize { total += count }
|
|
1403
|
+
end
|
|
1404
|
+
end
|
|
1405
|
+
threads.each(&:join)
|
|
1406
|
+
end
|
|
1407
|
+
|
|
1408
|
+
limit_value ? [total, limit_value].min : total
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1123
1411
|
def scan_with_conditions_normalized(normalized_conditions)
|
|
1124
1412
|
filter_parts = []
|
|
1125
1413
|
filter_values = {}
|
data/lib/active_item/version.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeitem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -41,7 +41,7 @@ cert_chain:
|
|
|
41
41
|
mR0ObwJ14vsQVbJibq0eRHIg8G4yV19pvEdJCli02eLl1+451M63HZAqBNuyJ9Ny
|
|
42
42
|
I1fxbbEBAzf7WHfoKdwFMuRZq7hpdLykCA8YQJFlLLFoXT0g41ug9iOKBtGg
|
|
43
43
|
-----END CERTIFICATE-----
|
|
44
|
-
date: 2026-
|
|
44
|
+
date: 2026-06-06 00:00:00.000000000 Z
|
|
45
45
|
dependencies:
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: activemodel
|
metadata.gz.sig
CHANGED
|
Binary file
|