@drawbridge/drawbridge-utils 0.0.111 → 0.0.112

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.
@@ -126,6 +126,25 @@ var HOOKS = Object.freeze({
126
126
  // Vendor data a campaign draws on. Named for what every store platform has,
127
127
  // not for what Shopify calls it: Shopify says discounts, Stripe says coupons
128
128
  // and promotion codes, BigCommerce says coupons and promotions.
129
+ //
130
+ // ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
131
+ //
132
+ // ({ connection, cursor, limit, search, settings, token })
133
+ // -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
134
+ //
135
+ // Lifted from what the Shopify product picker already does, rather than
136
+ // invented: that endpoint takes cursor/limit/search and returns items plus a
137
+ // pageInfo, and ListResource consumes exactly that. It was a good contract
138
+ // written once for one vendor; this makes it the contract.
139
+ //
140
+ // `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
141
+ // so normalising to { id, title } is what lets a picker work with no
142
+ // per-vendor configuration.
143
+ //
144
+ // HOW a vendor searches is its own business, which is the point of a hook.
145
+ // Shopify pushes the term into its GraphQL query, Klaviyo has a filter
146
+ // parameter, and Mailchimp's /lists has no name filter at all so its hook
147
+ // matches against what it fetched. The caller never learns which.
129
148
  catalog: Object.freeze([
130
149
  // The named groups a contact can be synced INTO. Klaviyo calls them lists,
131
150
  // Mailchimp calls them audiences; `audiences` is the industry-generic term
@@ -451,21 +470,29 @@ var klaviyo_default2 = {
451
470
  // more would show a picker missing the one they wanted — with nothing to
452
471
  // indicate anything was cut. Follows links.next, bounded so a runaway
453
472
  // cursor cannot spin forever.
454
- "catalog.audiences": async ({ fetcher, token }) => {
473
+ "catalog.audiences": async ({ cursor, fetcher, limit = 100, search, token }) => {
455
474
  var _a, _b;
456
475
  const audiences = [];
457
- let path = "/lists?page%5Bsize%5D=10";
476
+ let next = cursor ? "/lists?page%5Bsize%5D=10&page%5Bcursor%5D=" + encodeURIComponent(cursor) : "/lists?page%5Bsize%5D=10";
458
477
  let pages = 0;
459
- while (path && pages < 20) {
460
- const body = await api(path, { fetcher, token });
478
+ while (next && audiences.length < limit && pages < 20) {
479
+ const body = await api(next, { fetcher, token });
461
480
  for (const list of (body == null ? void 0 : body.data) || []) {
462
- audiences.push({ label: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id, value: list.id });
481
+ audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
463
482
  }
464
- const next = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
465
- path = next ? String(next).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
483
+ const link = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
484
+ next = link ? String(link).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
466
485
  pages = pages + 1;
467
486
  }
468
- return audiences;
487
+ const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
488
+ const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
489
+ return {
490
+ items,
491
+ pageInfo: {
492
+ endCursor: next,
493
+ hasNextPage: Boolean(next)
494
+ }
495
+ };
469
496
  },
470
497
  "auth.disconnect": async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
471
498
  const token = (settings == null ? void 0 : settings.refreshToken) || (settings == null ? void 0 : settings.accessToken);
@@ -632,34 +659,37 @@ var mailchimp_default2 = {
632
659
  // the same silent truncation Klaviyo has, at a different number. Paged
633
660
  // against total_items so an account past a thousand still resolves.
634
661
  hooks: {
635
- "catalog.audiences": async ({ fetcher = fetch, settings }) => {
662
+ "catalog.audiences": async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
636
663
  const key = settings == null ? void 0 : settings.apiKey;
637
- const headers = {
638
- // Basic with any username — Mailchimp reads only the password half.
639
- authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64")
640
- };
641
- const audiences = [];
642
- let offset = 0;
643
- let total = null;
644
- while (total === null || offset < total && offset < 5e3) {
645
- const response = await fetcher(
646
- base(key) + "/lists?count=1000&offset=" + offset + "&fields=lists.id,lists.name,total_items",
647
- { headers, signal: AbortSignal.timeout(15e3) }
648
- );
649
- if (!response.ok) {
650
- throw Object.assign(
651
- new Error("Mailchimp refused the request (" + response.status + ")"),
652
- { status: response.status }
653
- );
664
+ const count = Math.min(limit, 1e3);
665
+ const offset = Number(cursor || 0);
666
+ const response = await fetcher(
667
+ base(key) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
668
+ {
669
+ // Basic with any username — Mailchimp reads only the password half.
670
+ headers: { authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64") },
671
+ signal: AbortSignal.timeout(15e3)
654
672
  }
655
- const body = await response.json();
656
- for (const list of (body == null ? void 0 : body.lists) || []) {
657
- audiences.push({ label: (list == null ? void 0 : list.name) || list.id, value: list.id });
658
- }
659
- total = (body == null ? void 0 : body.total_items) ?? audiences.length;
660
- offset = offset + 1e3;
673
+ );
674
+ if (!response.ok) {
675
+ throw Object.assign(
676
+ new Error("Mailchimp refused the request (" + response.status + ")"),
677
+ { status: response.status }
678
+ );
661
679
  }
662
- return audiences;
680
+ const body = await response.json();
681
+ const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
682
+ const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
683
+ const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
684
+ const nextOffset = offset + count;
685
+ const more = nextOffset < Number((body == null ? void 0 : body.total_items) || 0);
686
+ return {
687
+ items,
688
+ pageInfo: {
689
+ endCursor: more ? String(nextOffset) : null,
690
+ hasNextPage: more
691
+ }
692
+ };
663
693
  }
664
694
  },
665
695
  icon: mailchimp_default,
@@ -97,6 +97,25 @@ const HOOKS = Object.freeze({
97
97
  // Vendor data a campaign draws on. Named for what every store platform has,
98
98
  // not for what Shopify calls it: Shopify says discounts, Stripe says coupons
99
99
  // and promotion codes, BigCommerce says coupons and promotions.
100
+ //
101
+ // ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
102
+ //
103
+ // ({ connection, cursor, limit, search, settings, token })
104
+ // -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
105
+ //
106
+ // Lifted from what the Shopify product picker already does, rather than
107
+ // invented: that endpoint takes cursor/limit/search and returns items plus a
108
+ // pageInfo, and ListResource consumes exactly that. It was a good contract
109
+ // written once for one vendor; this makes it the contract.
110
+ //
111
+ // `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
112
+ // so normalising to { id, title } is what lets a picker work with no
113
+ // per-vendor configuration.
114
+ //
115
+ // HOW a vendor searches is its own business, which is the point of a hook.
116
+ // Shopify pushes the term into its GraphQL query, Klaviyo has a filter
117
+ // parameter, and Mailchimp's /lists has no name filter at all so its hook
118
+ // matches against what it fetched. The caller never learns which.
100
119
  catalog : Object.freeze([
101
120
  // The named groups a contact can be synced INTO. Klaviyo calls them lists,
102
121
  // Mailchimp calls them audiences; `audiences` is the industry-generic term
@@ -473,34 +492,56 @@ var klaviyo = {
473
492
  // more would show a picker missing the one they wanted — with nothing to
474
493
  // indicate anything was cut. Follows links.next, bounded so a runaway
475
494
  // cursor cannot spin forever.
476
- 'catalog.audiences' : async ({ fetcher, token }) => {
495
+ 'catalog.audiences' : async ({ cursor, fetcher, limit = 100, search, token }) => {
477
496
 
497
+ // Klaviyo pages by cursor and caps page[size] at 10, so `limit` is what
498
+ // the CALLER wants back rather than what one request can carry — the
499
+ // loop keeps pulling until it has that many or the vendor runs out.
478
500
  const audiences = [];
479
501
 
480
- let path = '/lists?page%5Bsize%5D=10';
502
+ let next = cursor
503
+ ? '/lists?page%5Bsize%5D=10&page%5Bcursor%5D=' + encodeURIComponent( cursor )
504
+ : '/lists?page%5Bsize%5D=10';
505
+
481
506
  let pages = 0;
482
507
 
483
- while( path && pages < 20 ){
508
+ while( next && audiences.length < limit && pages < 20 ){
484
509
 
485
- const body = await api( path, { fetcher, token });
510
+ const body = await api( next, { fetcher, token });
486
511
 
487
512
  for( const list of ( body?.data || [] ) ){
488
513
 
489
- audiences.push({ label : list?.attributes?.name || list.id, value : list.id });
514
+ audiences.push({ id : list.id, title : list?.attributes?.name || list.id });
490
515
 
491
516
  }
492
517
 
493
- const next = body?.links?.next;
518
+ const link = body?.links?.next;
494
519
 
495
- // Klaviyo returns an absolute url; the shared caller prefixes its own
496
- // base, so only the part after /api travels on.
497
- path = next ? String( next ).replace( /^https:\/\/a\.klaviyo\.com\/api/, '' ) : null;
520
+ // Klaviyo returns an absolute url; only the part after /api travels on,
521
+ // because the shared caller prefixes its own base.
522
+ next = link ? String( link ).replace( /^https:\/\/a\.klaviyo\.com\/api/, '' ) : null;
498
523
 
499
524
  pages = pages + 1;
500
525
 
501
526
  }
502
527
 
503
- return audiences;
528
+ // Klaviyo's list endpoint has no name filter, so a search term is applied
529
+ // to what came back. Honest about its own limits: with more lists than
530
+ // `limit`, a term matching only later ones finds nothing — which is why
531
+ // a genuinely large catalog wants server-side filtering, not this.
532
+ const term = String( search?.value || '' ).trim().toLowerCase();
533
+
534
+ const items = term
535
+ ? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
536
+ : audiences;
537
+
538
+ return {
539
+ items,
540
+ pageInfo : {
541
+ endCursor : next,
542
+ hasNextPage : Boolean( next )
543
+ }
544
+ };
504
545
 
505
546
  },
506
547
  'auth.disconnect' : async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
@@ -698,50 +739,56 @@ var mailchimp = {
698
739
  // the same silent truncation Klaviyo has, at a different number. Paged
699
740
  // against total_items so an account past a thousand still resolves.
700
741
  hooks : {
701
- 'catalog.audiences' : async ({ fetcher = fetch, settings }) => {
742
+ 'catalog.audiences' : async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
702
743
 
703
744
  const key = settings?.apiKey;
704
745
 
705
- const headers = {
706
- // Basic with any username Mailchimp reads only the password half.
707
- authorization : 'Basic ' + Buffer.from( 'drawbridge:' + key ).toString( 'base64' )
708
- };
709
-
710
- const audiences = [];
711
-
712
- let offset = 0;
713
- let total = null;
746
+ // count DEFAULTS TO 10 and maxes at 1000 in Mailchimp's own spec, so
747
+ // leaving it unset returns ten audiences and looks entirely successful.
748
+ const count = Math.min( limit, 1000 );
749
+ const offset = Number( cursor || 0 );
750
+
751
+ const response = await fetcher(
752
+ base( key ) + '/lists?count=' + count + '&offset=' + offset + '&fields=lists.id,lists.name,total_items',
753
+ {
754
+ // Basic with any username — Mailchimp reads only the password half.
755
+ headers : { authorization : 'Basic ' + Buffer.from( 'drawbridge:' + key ).toString( 'base64' ) },
756
+ signal : AbortSignal.timeout( 15000 )
757
+ }
758
+ );
714
759
 
715
- while( total === null || ( offset < total && offset < 5000 ) ){
760
+ if( ! response.ok ){
716
761
 
717
- const response = await fetcher(
718
- base( key ) + '/lists?count=1000&offset=' + offset + '&fields=lists.id,lists.name,total_items',
719
- { headers, signal : AbortSignal.timeout( 15000 ) }
762
+ throw Object.assign(
763
+ new Error( 'Mailchimp refused the request (' + response.status + ')' ),
764
+ { status : response.status }
720
765
  );
721
766
 
722
- if( ! response.ok ){
767
+ }
723
768
 
724
- throw Object.assign(
725
- new Error( 'Mailchimp refused the request (' + response.status + ')' ),
726
- { status : response.status }
727
- );
769
+ const body = await response.json();
728
770
 
729
- }
771
+ const audiences = ( body?.lists || [] ).map( ( list ) => ({ id : list.id, title : list?.name || list.id }) );
730
772
 
731
- const body = await response.json();
773
+ // Mailchimp's /lists takes no name filter, so a term is matched against
774
+ // this page rather than the account. Stated plainly because it is a real
775
+ // limit: a term matching only an audience on a later page finds nothing.
776
+ const term = String( search?.value || '' ).trim().toLowerCase();
732
777
 
733
- for( const list of ( body?.lists || [] ) ){
778
+ const items = term
779
+ ? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
780
+ : audiences;
734
781
 
735
- audiences.push({ label : list?.name || list.id, value : list.id });
782
+ const nextOffset = offset + count;
783
+ const more = nextOffset < Number( body?.total_items || 0 );
736
784
 
785
+ return {
786
+ items,
787
+ pageInfo : {
788
+ endCursor : more ? String( nextOffset ) : null,
789
+ hasNextPage : more
737
790
  }
738
-
739
- total = body?.total_items ?? audiences.length;
740
- offset = offset + 1000;
741
-
742
- }
743
-
744
- return audiences;
791
+ };
745
792
 
746
793
  }
747
794
  },
@@ -97,6 +97,25 @@ const HOOKS = Object.freeze({
97
97
  // Vendor data a campaign draws on. Named for what every store platform has,
98
98
  // not for what Shopify calls it: Shopify says discounts, Stripe says coupons
99
99
  // and promotion codes, BigCommerce says coupons and promotions.
100
+ //
101
+ // ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
102
+ //
103
+ // ({ connection, cursor, limit, search, settings, token })
104
+ // -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
105
+ //
106
+ // Lifted from what the Shopify product picker already does, rather than
107
+ // invented: that endpoint takes cursor/limit/search and returns items plus a
108
+ // pageInfo, and ListResource consumes exactly that. It was a good contract
109
+ // written once for one vendor; this makes it the contract.
110
+ //
111
+ // `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
112
+ // so normalising to { id, title } is what lets a picker work with no
113
+ // per-vendor configuration.
114
+ //
115
+ // HOW a vendor searches is its own business, which is the point of a hook.
116
+ // Shopify pushes the term into its GraphQL query, Klaviyo has a filter
117
+ // parameter, and Mailchimp's /lists has no name filter at all so its hook
118
+ // matches against what it fetched. The caller never learns which.
100
119
  catalog : Object.freeze([
101
120
  // The named groups a contact can be synced INTO. Klaviyo calls them lists,
102
121
  // Mailchimp calls them audiences; `audiences` is the industry-generic term
@@ -473,34 +492,56 @@ var klaviyo = {
473
492
  // more would show a picker missing the one they wanted — with nothing to
474
493
  // indicate anything was cut. Follows links.next, bounded so a runaway
475
494
  // cursor cannot spin forever.
476
- 'catalog.audiences' : async ({ fetcher, token }) => {
495
+ 'catalog.audiences' : async ({ cursor, fetcher, limit = 100, search, token }) => {
477
496
 
497
+ // Klaviyo pages by cursor and caps page[size] at 10, so `limit` is what
498
+ // the CALLER wants back rather than what one request can carry — the
499
+ // loop keeps pulling until it has that many or the vendor runs out.
478
500
  const audiences = [];
479
501
 
480
- let path = '/lists?page%5Bsize%5D=10';
502
+ let next = cursor
503
+ ? '/lists?page%5Bsize%5D=10&page%5Bcursor%5D=' + encodeURIComponent( cursor )
504
+ : '/lists?page%5Bsize%5D=10';
505
+
481
506
  let pages = 0;
482
507
 
483
- while( path && pages < 20 ){
508
+ while( next && audiences.length < limit && pages < 20 ){
484
509
 
485
- const body = await api( path, { fetcher, token });
510
+ const body = await api( next, { fetcher, token });
486
511
 
487
512
  for( const list of ( body?.data || [] ) ){
488
513
 
489
- audiences.push({ label : list?.attributes?.name || list.id, value : list.id });
514
+ audiences.push({ id : list.id, title : list?.attributes?.name || list.id });
490
515
 
491
516
  }
492
517
 
493
- const next = body?.links?.next;
518
+ const link = body?.links?.next;
494
519
 
495
- // Klaviyo returns an absolute url; the shared caller prefixes its own
496
- // base, so only the part after /api travels on.
497
- path = next ? String( next ).replace( /^https:\/\/a\.klaviyo\.com\/api/, '' ) : null;
520
+ // Klaviyo returns an absolute url; only the part after /api travels on,
521
+ // because the shared caller prefixes its own base.
522
+ next = link ? String( link ).replace( /^https:\/\/a\.klaviyo\.com\/api/, '' ) : null;
498
523
 
499
524
  pages = pages + 1;
500
525
 
501
526
  }
502
527
 
503
- return audiences;
528
+ // Klaviyo's list endpoint has no name filter, so a search term is applied
529
+ // to what came back. Honest about its own limits: with more lists than
530
+ // `limit`, a term matching only later ones finds nothing — which is why
531
+ // a genuinely large catalog wants server-side filtering, not this.
532
+ const term = String( search?.value || '' ).trim().toLowerCase();
533
+
534
+ const items = term
535
+ ? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
536
+ : audiences;
537
+
538
+ return {
539
+ items,
540
+ pageInfo : {
541
+ endCursor : next,
542
+ hasNextPage : Boolean( next )
543
+ }
544
+ };
504
545
 
505
546
  },
506
547
  'auth.disconnect' : async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
@@ -698,50 +739,56 @@ var mailchimp = {
698
739
  // the same silent truncation Klaviyo has, at a different number. Paged
699
740
  // against total_items so an account past a thousand still resolves.
700
741
  hooks : {
701
- 'catalog.audiences' : async ({ fetcher = fetch, settings }) => {
742
+ 'catalog.audiences' : async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
702
743
 
703
744
  const key = settings?.apiKey;
704
745
 
705
- const headers = {
706
- // Basic with any username Mailchimp reads only the password half.
707
- authorization : 'Basic ' + Buffer.from( 'drawbridge:' + key ).toString( 'base64' )
708
- };
709
-
710
- const audiences = [];
711
-
712
- let offset = 0;
713
- let total = null;
746
+ // count DEFAULTS TO 10 and maxes at 1000 in Mailchimp's own spec, so
747
+ // leaving it unset returns ten audiences and looks entirely successful.
748
+ const count = Math.min( limit, 1000 );
749
+ const offset = Number( cursor || 0 );
750
+
751
+ const response = await fetcher(
752
+ base( key ) + '/lists?count=' + count + '&offset=' + offset + '&fields=lists.id,lists.name,total_items',
753
+ {
754
+ // Basic with any username — Mailchimp reads only the password half.
755
+ headers : { authorization : 'Basic ' + Buffer.from( 'drawbridge:' + key ).toString( 'base64' ) },
756
+ signal : AbortSignal.timeout( 15000 )
757
+ }
758
+ );
714
759
 
715
- while( total === null || ( offset < total && offset < 5000 ) ){
760
+ if( ! response.ok ){
716
761
 
717
- const response = await fetcher(
718
- base( key ) + '/lists?count=1000&offset=' + offset + '&fields=lists.id,lists.name,total_items',
719
- { headers, signal : AbortSignal.timeout( 15000 ) }
762
+ throw Object.assign(
763
+ new Error( 'Mailchimp refused the request (' + response.status + ')' ),
764
+ { status : response.status }
720
765
  );
721
766
 
722
- if( ! response.ok ){
767
+ }
723
768
 
724
- throw Object.assign(
725
- new Error( 'Mailchimp refused the request (' + response.status + ')' ),
726
- { status : response.status }
727
- );
769
+ const body = await response.json();
728
770
 
729
- }
771
+ const audiences = ( body?.lists || [] ).map( ( list ) => ({ id : list.id, title : list?.name || list.id }) );
730
772
 
731
- const body = await response.json();
773
+ // Mailchimp's /lists takes no name filter, so a term is matched against
774
+ // this page rather than the account. Stated plainly because it is a real
775
+ // limit: a term matching only an audience on a later page finds nothing.
776
+ const term = String( search?.value || '' ).trim().toLowerCase();
732
777
 
733
- for( const list of ( body?.lists || [] ) ){
778
+ const items = term
779
+ ? audiences.filter( ( entry ) => entry.title.toLowerCase().includes( term ) )
780
+ : audiences;
734
781
 
735
- audiences.push({ label : list?.name || list.id, value : list.id });
782
+ const nextOffset = offset + count;
783
+ const more = nextOffset < Number( body?.total_items || 0 );
736
784
 
785
+ return {
786
+ items,
787
+ pageInfo : {
788
+ endCursor : more ? String( nextOffset ) : null,
789
+ hasNextPage : more
737
790
  }
738
-
739
- total = body?.total_items ?? audiences.length;
740
- offset = offset + 1000;
741
-
742
- }
743
-
744
- return audiences;
791
+ };
745
792
 
746
793
  }
747
794
  },
@@ -73,6 +73,25 @@ var HOOKS = Object.freeze({
73
73
  // Vendor data a campaign draws on. Named for what every store platform has,
74
74
  // not for what Shopify calls it: Shopify says discounts, Stripe says coupons
75
75
  // and promotion codes, BigCommerce says coupons and promotions.
76
+ //
77
+ // ONE SHAPE FOR ALL OF THEM — searchable and cursor-paged:
78
+ //
79
+ // ({ connection, cursor, limit, search, settings, token })
80
+ // -> { items : [ { id, title, ... } ], pageInfo : { endCursor, hasNextPage } }
81
+ //
82
+ // Lifted from what the Shopify product picker already does, rather than
83
+ // invented: that endpoint takes cursor/limit/search and returns items plus a
84
+ // pageInfo, and ListResource consumes exactly that. It was a good contract
85
+ // written once for one vendor; this makes it the contract.
86
+ //
87
+ // `title` is not arbitrary — ListResource searches `[ 'title' ]` by default,
88
+ // so normalising to { id, title } is what lets a picker work with no
89
+ // per-vendor configuration.
90
+ //
91
+ // HOW a vendor searches is its own business, which is the point of a hook.
92
+ // Shopify pushes the term into its GraphQL query, Klaviyo has a filter
93
+ // parameter, and Mailchimp's /lists has no name filter at all so its hook
94
+ // matches against what it fetched. The caller never learns which.
76
95
  catalog: Object.freeze([
77
96
  // The named groups a contact can be synced INTO. Klaviyo calls them lists,
78
97
  // Mailchimp calls them audiences; `audiences` is the industry-generic term
@@ -398,21 +417,29 @@ var klaviyo_default2 = {
398
417
  // more would show a picker missing the one they wanted — with nothing to
399
418
  // indicate anything was cut. Follows links.next, bounded so a runaway
400
419
  // cursor cannot spin forever.
401
- "catalog.audiences": async ({ fetcher, token }) => {
420
+ "catalog.audiences": async ({ cursor, fetcher, limit = 100, search, token }) => {
402
421
  var _a, _b;
403
422
  const audiences = [];
404
- let path = "/lists?page%5Bsize%5D=10";
423
+ let next = cursor ? "/lists?page%5Bsize%5D=10&page%5Bcursor%5D=" + encodeURIComponent(cursor) : "/lists?page%5Bsize%5D=10";
405
424
  let pages = 0;
406
- while (path && pages < 20) {
407
- const body = await api(path, { fetcher, token });
425
+ while (next && audiences.length < limit && pages < 20) {
426
+ const body = await api(next, { fetcher, token });
408
427
  for (const list of (body == null ? void 0 : body.data) || []) {
409
- audiences.push({ label: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id, value: list.id });
428
+ audiences.push({ id: list.id, title: ((_a = list == null ? void 0 : list.attributes) == null ? void 0 : _a.name) || list.id });
410
429
  }
411
- const next = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
412
- path = next ? String(next).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
430
+ const link = (_b = body == null ? void 0 : body.links) == null ? void 0 : _b.next;
431
+ next = link ? String(link).replace(/^https:\/\/a\.klaviyo\.com\/api/, "") : null;
413
432
  pages = pages + 1;
414
433
  }
415
- return audiences;
434
+ const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
435
+ const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
436
+ return {
437
+ items,
438
+ pageInfo: {
439
+ endCursor: next,
440
+ hasNextPage: Boolean(next)
441
+ }
442
+ };
416
443
  },
417
444
  "auth.disconnect": async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
418
445
  const token = (settings == null ? void 0 : settings.refreshToken) || (settings == null ? void 0 : settings.accessToken);
@@ -579,34 +606,37 @@ var mailchimp_default2 = {
579
606
  // the same silent truncation Klaviyo has, at a different number. Paged
580
607
  // against total_items so an account past a thousand still resolves.
581
608
  hooks: {
582
- "catalog.audiences": async ({ fetcher = fetch, settings }) => {
609
+ "catalog.audiences": async ({ cursor, fetcher = fetch, limit = 100, search, settings }) => {
583
610
  const key = settings == null ? void 0 : settings.apiKey;
584
- const headers = {
585
- // Basic with any username — Mailchimp reads only the password half.
586
- authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64")
587
- };
588
- const audiences = [];
589
- let offset = 0;
590
- let total = null;
591
- while (total === null || offset < total && offset < 5e3) {
592
- const response = await fetcher(
593
- base(key) + "/lists?count=1000&offset=" + offset + "&fields=lists.id,lists.name,total_items",
594
- { headers, signal: AbortSignal.timeout(15e3) }
595
- );
596
- if (!response.ok) {
597
- throw Object.assign(
598
- new Error("Mailchimp refused the request (" + response.status + ")"),
599
- { status: response.status }
600
- );
611
+ const count = Math.min(limit, 1e3);
612
+ const offset = Number(cursor || 0);
613
+ const response = await fetcher(
614
+ base(key) + "/lists?count=" + count + "&offset=" + offset + "&fields=lists.id,lists.name,total_items",
615
+ {
616
+ // Basic with any username — Mailchimp reads only the password half.
617
+ headers: { authorization: "Basic " + Buffer.from("drawbridge:" + key).toString("base64") },
618
+ signal: AbortSignal.timeout(15e3)
601
619
  }
602
- const body = await response.json();
603
- for (const list of (body == null ? void 0 : body.lists) || []) {
604
- audiences.push({ label: (list == null ? void 0 : list.name) || list.id, value: list.id });
605
- }
606
- total = (body == null ? void 0 : body.total_items) ?? audiences.length;
607
- offset = offset + 1e3;
620
+ );
621
+ if (!response.ok) {
622
+ throw Object.assign(
623
+ new Error("Mailchimp refused the request (" + response.status + ")"),
624
+ { status: response.status }
625
+ );
608
626
  }
609
- return audiences;
627
+ const body = await response.json();
628
+ const audiences = ((body == null ? void 0 : body.lists) || []).map((list) => ({ id: list.id, title: (list == null ? void 0 : list.name) || list.id }));
629
+ const term = String((search == null ? void 0 : search.value) || "").trim().toLowerCase();
630
+ const items = term ? audiences.filter((entry) => entry.title.toLowerCase().includes(term)) : audiences;
631
+ const nextOffset = offset + count;
632
+ const more = nextOffset < Number((body == null ? void 0 : body.total_items) || 0);
633
+ return {
634
+ items,
635
+ pageInfo: {
636
+ endCursor: more ? String(nextOffset) : null,
637
+ hasNextPage: more
638
+ }
639
+ };
610
640
  }
611
641
  },
612
642
  icon: mailchimp_default,
package/package.json CHANGED
@@ -200,5 +200,5 @@
200
200
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
201
201
  },
202
202
  "types": "dist/index.d.ts",
203
- "version": "0.0.111"
203
+ "version": "0.0.112"
204
204
  }