@@ -80,6 +80,17 @@ def prepare(query: str) -> str:
80
80
@dataclass
81
81
class ShopifyBulkQuery :
82
82
config : Mapping [str , Any ]
83
+ parent_stream_name : Optional [str ] = None
84
+ parent_stream_cursor : Optional [str ] = None
85
+
86
+ @property
87
+ def has_parent_stream (self ) -> bool :
88
+ return True if self .parent_stream_name and self .parent_stream_cursor else False
89
+
90
+ @property
91
+ def parent_cursor_key (self ) -> Optional [str ]:
92
+ if self .has_parent_stream :
93
+ return f"{ self .parent_stream_name } _{ self .parent_stream_cursor } "
83
94
84
95
@property
85
96
def shop_id (self ) -> int :
@@ -132,6 +143,38 @@ def query_nodes(self) -> Optional[Union[List[Field], List[str]]]:
132
143
"""
133
144
return ["__typename" , "id" ]
134
145
146
+ def _inject_parent_cursor_field (self , nodes : List [Field ], key : str = "updatedAt" , index : int = 2 ) -> List [Field ]:
147
+ if self .has_parent_stream :
148
+ # inject parent cursor key as alias to the `updatedAt` parent cursor field
149
+ nodes .insert (index , Field (name = "updatedAt" , alias = self .parent_cursor_key ))
150
+
151
+ return nodes
152
+
153
+ def _add_parent_record_state (self , record : MutableMapping [str , Any ], items : List [dict ], to_rfc3339 : bool = False ) -> List [dict ]:
154
+ """
155
+ Adds a parent cursor value to each item in the list.
156
+
157
+ This method iterates over a list of dictionaries and adds a new key-value pair to each dictionary.
158
+ The key is the value of `self.query_name`, and the value is another dictionary with a single key "updated_at"
159
+ and the provided `parent_cursor_value`.
160
+
161
+ Args:
162
+ items (List[dict]): A list of dictionaries to which the parent cursor value will be added.
163
+ parent_cursor_value (str): The value to be set for the "updated_at" key in the nested dictionary.
164
+
165
+ Returns:
166
+ List[dict]: The modified list of dictionaries with the added parent cursor values.
167
+ """
168
+
169
+ if self .has_parent_stream :
170
+ parent_cursor_value : Optional [str ] = record .get (self .parent_cursor_key , None )
171
+ parent_state = self .tools ._datetime_str_to_rfc3339 (parent_cursor_value ) if to_rfc3339 and parent_cursor_value else None
172
+
173
+ for item in items :
174
+ item [self .parent_stream_name ] = {self .parent_stream_cursor : parent_state }
175
+
176
+ return items
177
+
135
178
def get (self , filter_field : Optional [str ] = None , start : Optional [str ] = None , end : Optional [str ] = None ) -> str :
136
179
# define filter query string, if passed
137
180
filter_query = f"{ filter_field } :>='{ start } ' AND { filter_field } :<='{ end } '" if filter_field else None
@@ -285,15 +328,22 @@ def query_nodes(self) -> List[Field]:
285
328
List of available fields:
286
329
https://shopify.dev/docs/api/admin-graphql/unstable/objects/Metafield
287
330
"""
331
+
332
+ nodes = super ().query_nodes
333
+
288
334
# define metafield node
289
335
metafield_node = self .get_edge_node ("metafields" , self .metafield_fields )
290
336
291
337
if isinstance (self .type .value , list ):
292
- return [ "__typename" , "id" , self .get_edge_node (self .type .value [1 ], ["__typename" , "id" , metafield_node ])]
338
+ nodes = [ * nodes , self .get_edge_node (self .type .value [1 ], [* nodes , metafield_node ])]
293
339
elif isinstance (self .type .value , str ):
294
- return [ "__typename" , "id" , metafield_node ]
340
+ nodes = [ * nodes , metafield_node ]
295
341
296
- def record_process_components (self , record : MutableMapping [str , Any ]) -> Iterable [MutableMapping [str , Any ]]:
342
+ nodes = self ._inject_parent_cursor_field (nodes )
343
+
344
+ return nodes
345
+
346
+ def _process_metafield (self , record : MutableMapping [str , Any ]) -> MutableMapping [str , Any ]:
297
347
# resolve parent id from `str` to `int`
298
348
record ["owner_id" ] = self .tools .resolve_str_id (record .get (BULK_PARENT_KEY ))
299
349
# add `owner_resource` field
@@ -304,7 +354,28 @@ def record_process_components(self, record: MutableMapping[str, Any]) -> Iterabl
304
354
record ["createdAt" ] = self .tools .from_iso8601_to_rfc3339 (record , "createdAt" )
305
355
record ["updatedAt" ] = self .tools .from_iso8601_to_rfc3339 (record , "updatedAt" )
306
356
record = self .tools .fields_names_to_snake_case (record )
307
- yield record
357
+ return record
358
+
359
+ def _process_components (self , entity : List [dict ]) -> Iterable [MutableMapping [str , Any ]]:
360
+ for item in entity :
361
+ # resolve the id from string
362
+ item ["admin_graphql_api_id" ] = item .get ("id" )
363
+ item ["id" ] = self .tools .resolve_str_id (item .get ("id" ))
364
+ yield self ._process_metafield (item )
365
+
366
+ def record_process_components (self , record : MutableMapping [str , Any ]) -> Iterable [MutableMapping [str , Any ]]:
367
+ # get the joined record components collected for the record
368
+ record_components = record .get ("record_components" , {})
369
+ # process record components
370
+ if not record_components :
371
+ yield self ._process_metafield (record )
372
+ else :
373
+ metafields = record_components .get ("Metafield" , [])
374
+ if len (metafields ) > 0 :
375
+ if self .has_parent_stream :
376
+ # add parent state to each metafield
377
+ metafields = self ._add_parent_record_state (record , metafields , to_rfc3339 = True )
378
+ yield from self ._process_components (metafields )
308
379
309
380
310
381
class MetafieldCollection (Metafield ):
@@ -343,7 +414,9 @@ class MetafieldCustomer(Metafield):
343
414
customers(query: "updated_at:>='2023-02-07T00:00:00+00:00' AND updated_at:<='2023-12-04T00:00:00+00:00'", sortKey: UPDATED_AT) {
344
415
edges {
345
416
node {
417
+ __typename
346
418
id
419
+ customer_updated_at: updatedAt
347
420
metafields {
348
421
edges {
349
422
node {
@@ -366,6 +439,11 @@ class MetafieldCustomer(Metafield):
366
439
367
440
type = MetafieldType .CUSTOMERS
368
441
442
+ record_composition = {
443
+ "new_record" : "Customer" ,
444
+ "record_components" : ["Metafield" ],
445
+ }
446
+
369
447
370
448
class MetafieldLocation (Metafield ):
371
449
"""
@@ -464,7 +542,9 @@ class MetafieldProduct(Metafield):
464
542
products(query: "updated_at:>='2023-02-07T00:00:00+00:00' AND updated_at:<='2023-12-04T00:00:00+00:00'", sortKey: UPDATED_AT) {
465
543
edges {
466
544
node {
545
+ __typename
467
546
id
547
+ product_updated_at: updatedAt
468
548
metafields {
469
549
edges {
470
550
node {
@@ -487,6 +567,11 @@ class MetafieldProduct(Metafield):
487
567
488
568
type = MetafieldType .PRODUCTS
489
569
570
+ record_composition = {
571
+ "new_record" : "Product" ,
572
+ "record_components" : ["Metafield" ],
573
+ }
574
+
490
575
491
576
class MetafieldProductImage (Metafield ):
492
577
"""
@@ -496,6 +581,7 @@ class MetafieldProductImage(Metafield):
496
581
node {
497
582
__typename
498
583
id
584
+ product_updated_at: updatedAt
499
585
media {
500
586
edges {
501
587
node {
@@ -527,6 +613,13 @@ class MetafieldProductImage(Metafield):
527
613
}
528
614
"""
529
615
616
+ type = MetafieldType .PRODUCT_IMAGES
617
+
618
+ record_composition = {
619
+ "new_record" : "Product" ,
620
+ "record_components" : ["Metafield" ],
621
+ }
622
+
530
623
@property
531
624
def query_nodes (self ) -> List [Field ]:
532
625
"""
@@ -537,19 +630,16 @@ def query_nodes(self) -> List[Field]:
537
630
More info here:
538
631
https://shopify.dev/docs/api/release-notes/2024-04#productimage-value-removed
539
632
"""
633
+
540
634
# define metafield node
541
635
metafield_node = self .get_edge_node ("metafields" , self .metafield_fields )
542
- media_fields : List [Field ] = [
543
- "__typename" ,
544
- "id" ,
545
- InlineFragment (type = "MediaImage" , fields = [metafield_node ]),
546
- ]
547
- # define media node
636
+ media_fields : List [Field ] = ["__typename" , "id" , InlineFragment (type = "MediaImage" , fields = [metafield_node ])]
548
637
media_node = self .get_edge_node ("media" , media_fields )
638
+
549
639
fields : List [Field ] = ["__typename" , "id" , media_node ]
550
- return fields
640
+ fields = self . _inject_parent_cursor_field ( fields )
551
641
552
- type = MetafieldType . PRODUCT_IMAGES
642
+ return fields
553
643
554
644
555
645
class MetafieldProductVariant (Metafield ):
@@ -2238,6 +2328,7 @@ class ProductImage(ShopifyBulkQuery):
2238
2328
node {
2239
2329
__typename
2240
2330
id
2331
+ products_updated_at: updatedAt
2241
2332
# THE MEDIA NODE IS NEEDED TO PROVIDE THE CURSORS
2242
2333
media {
2243
2334
edges {
@@ -2314,8 +2405,7 @@ class ProductImage(ShopifyBulkQuery):
2314
2405
# media property fields
2315
2406
media_fields : List [Field ] = [Field (name = "edges" , fields = [Field (name = "node" , fields = media_fragment )])]
2316
2407
2317
- # main query
2318
- query_nodes : List [Field ] = [
2408
+ nodes : List [Field ] = [
2319
2409
"__typename" ,
2320
2410
"id" ,
2321
2411
Field (name = "media" , fields = media_fields ),
@@ -2330,6 +2420,10 @@ class ProductImage(ShopifyBulkQuery):
2330
2420
"record_components" : ["MediaImage" , "Image" ],
2331
2421
}
2332
2422
2423
+ @property
2424
+ def query_nodes (self ) -> List [Field ]:
2425
+ return self ._inject_parent_cursor_field (self .nodes )
2426
+
2333
2427
def _process_component (self , entity : List [dict ]) -> List [dict ]:
2334
2428
for item in entity :
2335
2429
# remove the `__parentId` from the object
@@ -2405,6 +2499,8 @@ def record_process_components(self, record: MutableMapping[str, Any]) -> Iterabl
2405
2499
2406
2500
# add the product_id to each `Image`
2407
2501
record ["images" ] = self ._add_product_id (record .get ("images" , []), record .get ("id" ))
2502
+ # add the product cursor to each `Image`
2503
+ record ["images" ] = self ._add_parent_record_state (record , record .get ("images" , []), to_rfc3339 = True )
2408
2504
record ["images" ] = self ._merge_with_media (record_components )
2409
2505
record .pop ("record_components" )
2410
2506
@@ -2413,7 +2509,6 @@ def record_process_components(self, record: MutableMapping[str, Any]) -> Iterabl
2413
2509
if len (images ) > 0 :
2414
2510
# convert dates from ISO-8601 to RFC-3339
2415
2511
record ["images" ] = self ._convert_datetime_to_rfc3339 (images )
2416
-
2417
2512
yield from self ._emit_complete_records (images )
2418
2513
2419
2514
0 commit comments