@drawbridge/drawbridge-utils 0.0.170 → 0.0.172
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.
- package/dist/connections/index.cjs +55 -39
- package/dist/connections/index.d.cts +158 -53
- package/dist/connections/index.d.ts +158 -53
- package/dist/connections/index.js +55 -39
- package/dist/providers.cjs +55 -39
- package/dist/providers.js +55 -39
- package/package.json +2 -2
|
@@ -854,6 +854,13 @@ const row = ({ connection, data, manifest, row : described }) => ({
|
|
|
854
854
|
|
|
855
855
|
const segmentRowWrites = ({ connection, data, manifest, row : described, segment }) => {
|
|
856
856
|
|
|
857
|
+
// NO ID, NO ROW. `String( undefined )` is the string 'undefined', which is a
|
|
858
|
+
// perfectly valid string as far as the api's schema is concerned — so a row
|
|
859
|
+
// built without a vendor id would store and then render as a live-looking
|
|
860
|
+
// link to an object that does not exist. The three hooks all guard before
|
|
861
|
+
// they get here; this is the guard that does not depend on them remembering.
|
|
862
|
+
if( described?.id === undefined || described?.id === null ) return [];
|
|
863
|
+
|
|
857
864
|
const built = row({ connection, data, manifest, row : described });
|
|
858
865
|
|
|
859
866
|
return [
|
|
@@ -1234,8 +1241,6 @@ var attentive = {
|
|
|
1234
1241
|
// status() already stops a connection reaching Active without a
|
|
1235
1242
|
// segment; this is the belt to that braces. A workflow saved before the
|
|
1236
1243
|
// segment was chosen must not silently write into nothing.
|
|
1237
|
-
if( ! segment ) return { message : 'No Attentive segment is chosen for this connection.', skipped : true };
|
|
1238
|
-
|
|
1239
1244
|
const email = lead?.canonical?.email?.value || lead?.email;
|
|
1240
1245
|
|
|
1241
1246
|
// E.164 OR NOTHING. Attentive requires it, and a national-format number
|
|
@@ -1243,9 +1248,22 @@ var attentive = {
|
|
|
1243
1248
|
// and the email carries the sync instead.
|
|
1244
1249
|
const phone = toE164( lead?.canonical?.phone?.value || lead?.phone );
|
|
1245
1250
|
|
|
1251
|
+
// WHAT WENT TO THE VENDOR, so a sync run in the activity feed can
|
|
1252
|
+
// answer what was actually sent. The three contact syncs were the only
|
|
1253
|
+
// hook family carrying none, and a merchant opening one saw
|
|
1254
|
+
// request : null. Attentive identifies on either channel, so both are
|
|
1255
|
+
// recorded — which of the two carried the sync is the first question
|
|
1256
|
+
// anyone debugging one of these asks.
|
|
1257
|
+
const request = { email : email || null, phone : phone || null, segment : settings?.segment || null };
|
|
1258
|
+
|
|
1259
|
+
// status() already stops a connection reaching Active without a
|
|
1260
|
+
// segment; this is the belt to that braces. A workflow saved before the
|
|
1261
|
+
// segment was chosen must not silently write into nothing.
|
|
1262
|
+
if( ! segment ) return { message : 'No Attentive segment is chosen for this connection.', request, skipped : true };
|
|
1263
|
+
|
|
1246
1264
|
// Their `user` requires phone OR email. With neither there is nobody to
|
|
1247
1265
|
// subscribe, so this is a skip rather than a failure.
|
|
1248
|
-
if( ! email && ! phone ) return { message : 'That lead has no email address or phone number to sync.', skipped : true };
|
|
1266
|
+
if( ! email && ! phone ) return { message : 'That lead has no email address or phone number to sync.', request, skipped : true };
|
|
1249
1267
|
|
|
1250
1268
|
const user = {
|
|
1251
1269
|
...( email && { email }),
|
|
@@ -1285,6 +1303,7 @@ var attentive = {
|
|
|
1285
1303
|
|
|
1286
1304
|
return {
|
|
1287
1305
|
message : 'Attentive accepted an unsubscribe for this contact — they have opted out.',
|
|
1306
|
+
request,
|
|
1288
1307
|
response : { accepted : true, unsubscribed : true }
|
|
1289
1308
|
};
|
|
1290
1309
|
|
|
@@ -1332,6 +1351,7 @@ var attentive = {
|
|
|
1332
1351
|
// queued them — a merchant who reads "synced" and looks for the person
|
|
1333
1352
|
// in Attentive a second later has been told the wrong thing.
|
|
1334
1353
|
message : 'Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.',
|
|
1354
|
+
request,
|
|
1335
1355
|
response : {
|
|
1336
1356
|
accepted : true,
|
|
1337
1357
|
...( membership?.batchJobId && { batchJobId : membership.batchJobId })
|
|
@@ -3353,7 +3373,7 @@ var drawbridge = {
|
|
|
3353
3373
|
text : { max : 2000, type : 'string' }
|
|
3354
3374
|
},
|
|
3355
3375
|
triggers : [ 'schedule.day', 'schedule.week', 'schedule.month' ],
|
|
3356
|
-
usage : { actions :
|
|
3376
|
+
usage : { actions : channels.email.actionsPerSend }
|
|
3357
3377
|
}),
|
|
3358
3378
|
|
|
3359
3379
|
// To organization MEMBERS. Never suppressed — an entrant's opt-out must
|
|
@@ -3379,10 +3399,14 @@ var drawbridge = {
|
|
|
3379
3399
|
subject : { max : 150, type : 'string' }
|
|
3380
3400
|
},
|
|
3381
3401
|
triggers : [ 'lead.insert' ],
|
|
3382
|
-
//
|
|
3383
|
-
//
|
|
3384
|
-
//
|
|
3385
|
-
|
|
3402
|
+
// EVERY STEP IN A BILLABLE WORKFLOW COUNTS, team mail included. This
|
|
3403
|
+
// leaves our SendGrid account and costs the same to send as any other
|
|
3404
|
+
// email, so it carries the same weight. Whether anything is charged at
|
|
3405
|
+
// all is the workflow's `billable` tag, not the step's audience — and
|
|
3406
|
+
// this step triggers on lead.insert, so a merchant notifying
|
|
3407
|
+
// themselves on every entry spends against their allowance twice per
|
|
3408
|
+
// lead, once for the entry and once for the notification.
|
|
3409
|
+
usage : { actions : channels.email.actionsPerSend }
|
|
3386
3410
|
}),
|
|
3387
3411
|
|
|
3388
3412
|
// To a LEAD. Suppression applies and the send is billed.
|
|
@@ -3514,7 +3538,30 @@ const api$1 = async ( path, { fetcher = fetch, method = 'GET', payload, token }
|
|
|
3514
3538
|
|
|
3515
3539
|
// The segment's name at Klaviyo, spelled once. It is a LABEL — the definition
|
|
3516
3540
|
// below keys on the id — so a rename never has to touch a profile.
|
|
3517
|
-
|
|
3541
|
+
//
|
|
3542
|
+
// THE ID IS IN THE LABEL TOO, and that part is not cosmetic. Nothing makes a
|
|
3543
|
+
// THE ID IS IN THE NAME BECAUSE THE NAME IS NOT AN IDENTITY. Keyed on title
|
|
3544
|
+
// alone, a second segment called "VIP" found the first one's Klaviyo segment by
|
|
3545
|
+
// name and adopted it: both rows then pointed at one segment whose definition
|
|
3546
|
+
// computes the FIRST one's membership, so the merchant sent to the wrong people,
|
|
3547
|
+
// and deleting either segment deleted that object out from under the other with
|
|
3548
|
+
// nothing to repair it.
|
|
3549
|
+
//
|
|
3550
|
+
// The api now carries a unique index on (organization, title) and a 409 on both
|
|
3551
|
+
// create and rename, so that pair can no longer be created. The suffix stays
|
|
3552
|
+
// anyway: it is what makes the damage impossible rather than merely unlikely,
|
|
3553
|
+
// and it is the only part of this that holds if a title ever reaches Klaviyo
|
|
3554
|
+
// from somewhere that did not go through the route.
|
|
3555
|
+
//
|
|
3556
|
+
// KLAVIYO IS NOT DOING THIS FOR US. Create Segment describes `name` only as "A
|
|
3557
|
+
// helpful name to label the segment" and documents no uniqueness constraint and
|
|
3558
|
+
// no duplicate-name error (developers.klaviyo.com/en/reference/create_segment,
|
|
3559
|
+
// fetched 2026-09-12), so the search cannot lean on the vendor. What IS
|
|
3560
|
+
// documented is that filter comparisons are case-sensitive
|
|
3561
|
+
// (developers.klaviyo.com/en/docs/filtering_, same date), which is why the
|
|
3562
|
+
// find-by-name below re-checks with === and agrees with the vendor rather than
|
|
3563
|
+
// guessing.
|
|
3564
|
+
const segmentName = ( title, id ) => 'Drawbridge: ' + title + ' (' + String( id ).slice( -6 ) + ')';
|
|
3518
3565
|
|
|
3519
3566
|
// THE GRANT THIS NEEDS. Klaviyo's scopes are set on the app and a token carries
|
|
3520
3567
|
// only what the merchant consented to, so a connection made before segments were
|
|
@@ -3835,11 +3882,16 @@ var klaviyo = {
|
|
|
3835
3882
|
// incomplete() already stops a connection reaching Active without a
|
|
3836
3883
|
// list; this is the belt to that braces. A workflow saved before the
|
|
3837
3884
|
// list was chosen must not silently write into nothing.
|
|
3838
|
-
|
|
3885
|
+
// WHAT WENT TO THE VENDOR, so a sync run in the activity feed can answer
|
|
3886
|
+
// what was actually sent. The three contact syncs were the only hook
|
|
3887
|
+
// family that carried none, so a merchant opening one saw request : null.
|
|
3888
|
+
const request = { email : lead?.canonical?.email?.value || lead?.email || null, list : settings?.list || null };
|
|
3889
|
+
|
|
3890
|
+
if( ! list ) return { message : 'No Klaviyo list is chosen for this connection.', request, skipped : true };
|
|
3839
3891
|
|
|
3840
3892
|
const email = lead?.canonical?.email?.value || lead?.email;
|
|
3841
3893
|
|
|
3842
|
-
if( ! email ) return { message : 'That lead has no email address to sync.', skipped : true };
|
|
3894
|
+
if( ! email ) return { message : 'That lead has no email address to sync.', request, skipped : true };
|
|
3843
3895
|
|
|
3844
3896
|
// A Klaviyo profile is keyed on an email, so IDENTITY comes from the
|
|
3845
3897
|
// lead. RANKING comes from the contact behind it, because only a
|
|
@@ -3934,7 +3986,7 @@ var klaviyo = {
|
|
|
3934
3986
|
|
|
3935
3987
|
const profileId = profile?.data?.id;
|
|
3936
3988
|
|
|
3937
|
-
if( ! profileId ) return { message : 'Klaviyo returned no profile id.', skipped : true };
|
|
3989
|
+
if( ! profileId ) return { message : 'Klaviyo returned no profile id.', request, skipped : true };
|
|
3938
3990
|
|
|
3939
3991
|
// SUPPRESSED PEOPLE ARE SYNCED AS UNSUBSCRIBED, NEVER OMITTED.
|
|
3940
3992
|
//
|
|
@@ -3977,6 +4029,7 @@ var klaviyo = {
|
|
|
3977
4029
|
message : suppressed
|
|
3978
4030
|
? 'Synced to Klaviyo as unsubscribed — this contact has opted out.'
|
|
3979
4031
|
: 'Synced to the Klaviyo list.',
|
|
4032
|
+
request,
|
|
3980
4033
|
// Recorded on the run for support to read back, not a write
|
|
3981
4034
|
// instruction — the hook has already written what it needed to.
|
|
3982
4035
|
response : { klaviyoProfileId : profileId }
|
|
@@ -4024,7 +4077,7 @@ var klaviyo = {
|
|
|
4024
4077
|
|
|
4025
4078
|
}
|
|
4026
4079
|
|
|
4027
|
-
const name = segmentName( segment.title );
|
|
4080
|
+
const name = segmentName( segment.title, segment.id );
|
|
4028
4081
|
const existing = segmentRowFor({ connection, segment });
|
|
4029
4082
|
|
|
4030
4083
|
let id = null;
|
|
@@ -4634,13 +4687,29 @@ const subscriberHash = ( email ) => createHash( 'md5' )
|
|
|
4634
4687
|
// server-side truncation is the worse outcome because register would then look
|
|
4635
4688
|
// for a name Mailchimp had silently changed.
|
|
4636
4689
|
//
|
|
4637
|
-
//
|
|
4638
|
-
//
|
|
4639
|
-
//
|
|
4640
|
-
//
|
|
4690
|
+
// THE SEGMENT ID IS IN THE NAME, and it has to be. Nothing makes a Drawbridge
|
|
4691
|
+
// segment title unique — no index on the collection, no check in the route, no
|
|
4692
|
+
// check in the form — so two segments called "VIP" are ordinary. Keyed on title
|
|
4693
|
+
// alone, the second one's register finds the first one's tag, adopts it, and
|
|
4694
|
+
// both rows point at one object: the merchant's two segments are one tag, and
|
|
4695
|
+
// deleting either segment deletes the tag out from under the other, leaving a
|
|
4696
|
+
// row pointing at an id that no longer exists and nothing to repair it.
|
|
4697
|
+
//
|
|
4698
|
+
// The last six characters of the id are enough to separate them and short
|
|
4699
|
+
// enough to leave the title readable in the merchant's own audience.
|
|
4700
|
+
//
|
|
4701
|
+
// A rename now changes the name and keeps the suffix, so register's find-by-name
|
|
4702
|
+
// still matches after one — and the truncation below cuts the TITLE rather than
|
|
4703
|
+
// the suffix for the same reason.
|
|
4641
4704
|
const TAG_NAME_LIMIT = 100;
|
|
4642
4705
|
|
|
4643
|
-
const tagName = ( title ) =>
|
|
4706
|
+
const tagName = ( title, id ) => {
|
|
4707
|
+
|
|
4708
|
+
const suffix = ' (' + String( id ).slice( -6 ) + ')';
|
|
4709
|
+
|
|
4710
|
+
return ( 'Drawbridge: ' + title ).slice( 0, TAG_NAME_LIMIT - suffix.length ) + suffix;
|
|
4711
|
+
|
|
4712
|
+
};
|
|
4644
4713
|
|
|
4645
4714
|
// Mailchimp — contact sync, not a sender.
|
|
4646
4715
|
var mailchimp = {
|
|
@@ -4797,16 +4866,24 @@ var mailchimp = {
|
|
|
4797
4866
|
|
|
4798
4867
|
const audience = settings?.audience;
|
|
4799
4868
|
|
|
4869
|
+
// WHAT WENT TO THE VENDOR, recorded on the step so the activity feed
|
|
4870
|
+
// can answer "what did we actually send". Every other hook family
|
|
4871
|
+
// carries one; the three contact syncs did not, so a merchant opening
|
|
4872
|
+
// a sync run saw `request : null` and learned nothing — which is
|
|
4873
|
+
// exactly how it was reported. The audience and the address are the
|
|
4874
|
+
// whole identity of this write.
|
|
4875
|
+
const request = { audience : settings?.audience || null, email : lead?.canonical?.email?.value || lead?.email || null };
|
|
4876
|
+
|
|
4800
4877
|
// status() already stops a connection reaching Active without an
|
|
4801
4878
|
// audience; this is the belt to that braces. A workflow saved before
|
|
4802
4879
|
// the audience was chosen must not silently write into nothing.
|
|
4803
|
-
if( ! audience ) return { message : 'No Mailchimp audience is chosen for this connection.', skipped : true };
|
|
4880
|
+
if( ! audience ) return { message : 'No Mailchimp audience is chosen for this connection.', request, skipped : true };
|
|
4804
4881
|
|
|
4805
4882
|
const email = lead?.canonical?.email?.value || lead?.email;
|
|
4806
4883
|
|
|
4807
4884
|
// A Mailchimp member IS an email address — there is no other identity
|
|
4808
4885
|
// to write, so a lead without one is skipped rather than failed.
|
|
4809
|
-
if( ! email ) return { message : 'That lead has no email address to sync.', skipped : true };
|
|
4886
|
+
if( ! email ) return { message : 'That lead has no email address to sync.', request, skipped : true };
|
|
4810
4887
|
|
|
4811
4888
|
const hash = subscriberHash( email );
|
|
4812
4889
|
|
|
@@ -4927,19 +5004,21 @@ var mailchimp = {
|
|
|
4927
5004
|
}
|
|
4928
5005
|
},
|
|
4929
5006
|
{
|
|
4930
|
-
|
|
5007
|
+
// THE ID AS WELL AS THE TITLE, because the tag name carries it —
|
|
5008
|
+
// see tagName. Without it every name here would end in the
|
|
5009
|
+
// string 'undefined' and match nothing register wrote.
|
|
5010
|
+
$project : { _id : 0, id : 1, title : 1 }
|
|
4931
5011
|
}
|
|
4932
5012
|
]
|
|
4933
5013
|
});
|
|
4934
5014
|
|
|
4935
|
-
const joined = new Set( segments.map( ( entry ) => entry.
|
|
5015
|
+
const joined = new Set( segments.map( ( entry ) => entry.id ) );
|
|
4936
5016
|
|
|
4937
5017
|
const tags = ( owned || [] )
|
|
4938
|
-
.
|
|
4939
|
-
.
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
status : joined.has( title ) ? 'active' : 'inactive'
|
|
5018
|
+
.filter( ( entry ) => entry.id && entry.title )
|
|
5019
|
+
.map( ( entry ) => ({
|
|
5020
|
+
name : tagName( entry.title, entry.id ),
|
|
5021
|
+
status : joined.has( entry.id ) ? 'active' : 'inactive'
|
|
4943
5022
|
}) );
|
|
4944
5023
|
|
|
4945
5024
|
if( tags.length > 0 ){
|
|
@@ -4962,6 +5041,7 @@ var mailchimp = {
|
|
|
4962
5041
|
message : suppressed
|
|
4963
5042
|
? 'Synced to Mailchimp as unsubscribed — this contact has opted out.'
|
|
4964
5043
|
: 'Synced to the Mailchimp audience.',
|
|
5044
|
+
request,
|
|
4965
5045
|
// Recorded on the run for support to read back, not a write
|
|
4966
5046
|
// instruction — the hook has already written what it needed to.
|
|
4967
5047
|
response : { mailchimpMemberId : member?.id || hash }
|
|
@@ -5008,7 +5088,7 @@ var mailchimp = {
|
|
|
5008
5088
|
|
|
5009
5089
|
if( ! segment?.id || segment.system ) return { message : 'That segment is not one this connection publishes.', skipped : true };
|
|
5010
5090
|
|
|
5011
|
-
const name = tagName( segment.title );
|
|
5091
|
+
const name = tagName( segment.title, segment.id );
|
|
5012
5092
|
const existing = segmentRowFor({ connection, segment });
|
|
5013
5093
|
|
|
5014
5094
|
let id = null;
|
|
@@ -5736,23 +5816,33 @@ var shopify = {
|
|
|
5736
5816
|
|
|
5737
5817
|
const request = { email : context?.email || null, lead : context?.lead || null, shop : connection.shop };
|
|
5738
5818
|
|
|
5819
|
+
// KEPT even though nothing below sends the address anywhere. A code
|
|
5820
|
+
// that reaches nobody is a wasted action and a merchant's wasted
|
|
5821
|
+
// allowance: issuing is only half a workflow, and the email step after
|
|
5822
|
+
// it needs somewhere to send the code.
|
|
5739
5823
|
if( ! context?.email ) return { message : 'Lead email is missing.', request, response : { skipped : true }, skipped : true };
|
|
5740
5824
|
if( ! context?.lead ) return { message : 'Lead id is missing.', request, response : { skipped : true }, skipped : true };
|
|
5741
5825
|
if( ! discount?.id ) return { message : 'Discount is not configured on this step.', request, response : { skipped : true }, skipped : true };
|
|
5742
5826
|
|
|
5743
5827
|
const adminAccessToken = await adminToken();
|
|
5744
5828
|
|
|
5745
|
-
//
|
|
5746
|
-
//
|
|
5747
|
-
//
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5829
|
+
// NOTHING IS SENT TO SHOPIFY TO LINK THE CODE TO THE PERSON, because
|
|
5830
|
+
// there is nothing to send: discountRedeemCodeBulkAdd takes a discount
|
|
5831
|
+
// and a list of codes, and no customer
|
|
5832
|
+
// (shopify.dev/docs/api/admin-graphql/2026-04/mutations/discountRedeemCodeBulkAdd).
|
|
5833
|
+
// The link lives entirely on our side — `lead.shopifyDiscountCode`
|
|
5834
|
+
// below — and attribution happens when the order comes back carrying
|
|
5835
|
+
// the code, matched org-scoped against that field by commerce.order.
|
|
5836
|
+
//
|
|
5837
|
+
// This step used to call getOrCreateCustomer first, on the stated
|
|
5838
|
+
// reasoning that "the customer must exist before a code is mapped to
|
|
5839
|
+
// them". No code was ever mapped to them: the result was assigned and
|
|
5840
|
+
// discarded, createDiscountCode never received it, and redemption
|
|
5841
|
+
// attribution never consulted it. All it did was create a customer in
|
|
5842
|
+
// the merchant's store as a side effect of issuing a discount — which
|
|
5843
|
+
// QA reported as surprising, and was right to. A merchant who wants the
|
|
5844
|
+
// buyer to exist at the store adds the Create customer step, which is
|
|
5845
|
+
// what that step is for.
|
|
5756
5846
|
const discountCode = await shopify.admin.createDiscountCode({
|
|
5757
5847
|
adminAccessToken,
|
|
5758
5848
|
code : 'DB-' + generateDiscountCode(),
|
|
@@ -6413,7 +6503,7 @@ var shopify = {
|
|
|
6413
6503
|
//
|
|
6414
6504
|
// The shell has already refused a missing or inactive Shopify connection,
|
|
6415
6505
|
// so what is left is the two things only this hook can know are wrong.
|
|
6416
|
-
product : async ( { connection, context, workflow }, { mintId } = {} ) => {
|
|
6506
|
+
product : async ( { connection, context, workflow }, { mintId, read } = {} ) => {
|
|
6417
6507
|
|
|
6418
6508
|
const request = {
|
|
6419
6509
|
numericId : context?.id || null,
|
|
@@ -6429,11 +6519,30 @@ var shopify = {
|
|
|
6429
6519
|
|
|
6430
6520
|
const providerId = 'gid://shopify/Product/' + context.id;
|
|
6431
6521
|
|
|
6432
|
-
//
|
|
6433
|
-
//
|
|
6434
|
-
//
|
|
6435
|
-
//
|
|
6436
|
-
|
|
6522
|
+
// THE ROW'S OWN ID WHEN IT ALREADY EXISTS, a fresh one only when it
|
|
6523
|
+
// does not.
|
|
6524
|
+
//
|
|
6525
|
+
// The write below is an upsert whose `id` sits in $setOnInsert, so a
|
|
6526
|
+
// product we have seen before keeps the id it was first given and a
|
|
6527
|
+
// newly minted one is silently discarded. Minting unconditionally
|
|
6528
|
+
// therefore published an id belonging to no row: the enqueue named a
|
|
6529
|
+
// product the worker then had to fall back off, and the step's
|
|
6530
|
+
// response showed QA a value that matched nothing they could look up
|
|
6531
|
+
// — which is exactly how it was reported.
|
|
6532
|
+
//
|
|
6533
|
+
// Reading first costs one indexed lookup on the same key the upsert
|
|
6534
|
+
// matches. A concurrent redelivery can still have both runs miss and
|
|
6535
|
+
// mint, which is why the job carries the PROVIDER identity too and the
|
|
6536
|
+
// worker falls back to provider + shop — stable either way.
|
|
6537
|
+
const query = {
|
|
6538
|
+
'provider.id' : providerId,
|
|
6539
|
+
'provider.slug' : 'shopify',
|
|
6540
|
+
'source.domain' : connection.shop
|
|
6541
|
+
};
|
|
6542
|
+
|
|
6543
|
+
const existing = read?.get ? await read.get({ collection : 'product', query }) : null;
|
|
6544
|
+
|
|
6545
|
+
const productId = existing?.id || mintId();
|
|
6437
6546
|
|
|
6438
6547
|
return {
|
|
6439
6548
|
enqueues : [ {
|
|
@@ -6464,11 +6573,7 @@ var shopify = {
|
|
|
6464
6573
|
},
|
|
6465
6574
|
operation : 'update',
|
|
6466
6575
|
options : { upsert : true },
|
|
6467
|
-
query
|
|
6468
|
-
'provider.id' : providerId,
|
|
6469
|
-
'provider.slug' : 'shopify',
|
|
6470
|
-
'source.domain' : connection.shop
|
|
6471
|
-
}
|
|
6576
|
+
query
|
|
6472
6577
|
} ]
|
|
6473
6578
|
};
|
|
6474
6579
|
|
|
@@ -6941,10 +7046,10 @@ var shopify = {
|
|
|
6941
7046
|
queue : 'connection'
|
|
6942
7047
|
} ],
|
|
6943
7048
|
message : ( scopesMissing?.length
|
|
6944
|
-
? 'Health check: ping ok,
|
|
7049
|
+
? 'Health check: ping ok, scopes reconciled — connection errored, granted scopes are missing: ' + scopesMissing.join( ', ' ) + '.'
|
|
6945
7050
|
: refreshTokenRotated
|
|
6946
|
-
? 'Health check passed — refresh token rotated, ping ok,
|
|
6947
|
-
: 'Health check passed — ping ok,
|
|
7051
|
+
? 'Health check passed — refresh token rotated, ping ok, scopes reconciled.'
|
|
7052
|
+
: 'Health check passed — ping ok, scopes reconciled.'
|
|
6948
7053
|
)
|
|
6949
7054
|
+ ( sourceRepaired ? ' Billing source repaired from the live shop.' : '' )
|
|
6950
7055
|
+ ( metered === null ? ' No usage line on the store\'s approved plan — order billing needs the merchant to approve the updated plan.' : '' ),
|
|
@@ -542,6 +542,7 @@ var row = ({ connection: connection2, data: data2, manifest, row: described }) =
|
|
|
542
542
|
};
|
|
543
543
|
};
|
|
544
544
|
var segmentRowWrites = ({ connection: connection2, data: data2, manifest, row: described, segment }) => {
|
|
545
|
+
if ((described == null ? void 0 : described.id) === void 0 || (described == null ? void 0 : described.id) === null) return [];
|
|
545
546
|
const built = row({ connection: connection2, data: data2, manifest, row: described });
|
|
546
547
|
return [
|
|
547
548
|
// PUSH IF ABSENT. The $ne guard is what makes a second concurrent register
|
|
@@ -789,10 +790,11 @@ var attentive_default2 = {
|
|
|
789
790
|
sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
|
|
790
791
|
var _a, _b, _c, _d;
|
|
791
792
|
const segment = settings == null ? void 0 : settings.segment;
|
|
792
|
-
if (!segment) return { message: "No Attentive segment is chosen for this connection.", skipped: true };
|
|
793
793
|
const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
|
|
794
794
|
const phone = toE164(((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.phone) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.phone));
|
|
795
|
-
|
|
795
|
+
const request2 = { email: email || null, phone: phone || null, segment: (settings == null ? void 0 : settings.segment) || null };
|
|
796
|
+
if (!segment) return { message: "No Attentive segment is chosen for this connection.", request: request2, skipped: true };
|
|
797
|
+
if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", request: request2, skipped: true };
|
|
796
798
|
const user = {
|
|
797
799
|
...email && { email },
|
|
798
800
|
...phone && { phone }
|
|
@@ -814,6 +816,7 @@ var attentive_default2 = {
|
|
|
814
816
|
});
|
|
815
817
|
return {
|
|
816
818
|
message: "Attentive accepted an unsubscribe for this contact \u2014 they have opted out.",
|
|
819
|
+
request: request2,
|
|
817
820
|
response: { accepted: true, unsubscribed: true }
|
|
818
821
|
};
|
|
819
822
|
}
|
|
@@ -854,6 +857,7 @@ var attentive_default2 = {
|
|
|
854
857
|
// queued them — a merchant who reads "synced" and looks for the person
|
|
855
858
|
// in Attentive a second later has been told the wrong thing.
|
|
856
859
|
message: "Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.",
|
|
860
|
+
request: request2,
|
|
857
861
|
response: {
|
|
858
862
|
accepted: true,
|
|
859
863
|
...(membership == null ? void 0 : membership.batchJobId) && { batchJobId: membership.batchJobId }
|
|
@@ -2870,7 +2874,7 @@ var drawbridge_default2 = {
|
|
|
2870
2874
|
text: { max: 2e3, type: "string" }
|
|
2871
2875
|
},
|
|
2872
2876
|
triggers: ["schedule.day", "schedule.week", "schedule.month"],
|
|
2873
|
-
usage: { actions:
|
|
2877
|
+
usage: { actions: channels.email.actionsPerSend }
|
|
2874
2878
|
}),
|
|
2875
2879
|
// To organization MEMBERS. Never suppressed — an entrant's opt-out must
|
|
2876
2880
|
// not silence an alert to staff — and not billed.
|
|
@@ -2895,10 +2899,14 @@ var drawbridge_default2 = {
|
|
|
2895
2899
|
subject: { max: 150, type: "string" }
|
|
2896
2900
|
},
|
|
2897
2901
|
triggers: ["lead.insert"],
|
|
2898
|
-
//
|
|
2899
|
-
//
|
|
2900
|
-
//
|
|
2901
|
-
|
|
2902
|
+
// EVERY STEP IN A BILLABLE WORKFLOW COUNTS, team mail included. This
|
|
2903
|
+
// leaves our SendGrid account and costs the same to send as any other
|
|
2904
|
+
// email, so it carries the same weight. Whether anything is charged at
|
|
2905
|
+
// all is the workflow's `billable` tag, not the step's audience — and
|
|
2906
|
+
// this step triggers on lead.insert, so a merchant notifying
|
|
2907
|
+
// themselves on every entry spends against their allowance twice per
|
|
2908
|
+
// lead, once for the entry and once for the notification.
|
|
2909
|
+
usage: { actions: channels.email.actionsPerSend }
|
|
2902
2910
|
}),
|
|
2903
2911
|
// To a LEAD. Suppression applies and the send is billed.
|
|
2904
2912
|
send: () => ({
|
|
@@ -3007,7 +3015,7 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
|
|
|
3007
3015
|
}
|
|
3008
3016
|
return response.status === 204 ? null : response.json();
|
|
3009
3017
|
};
|
|
3010
|
-
var segmentName = (title) => "Drawbridge: " + title;
|
|
3018
|
+
var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
|
|
3011
3019
|
var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
|
|
3012
3020
|
var klaviyo_default2 = {
|
|
3013
3021
|
// OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
|
|
@@ -3254,11 +3262,12 @@ var klaviyo_default2 = {
|
|
|
3254
3262
|
// different thing from deleting the profile.
|
|
3255
3263
|
remove: false,
|
|
3256
3264
|
sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
|
|
3257
|
-
var _a, _b, _c;
|
|
3265
|
+
var _a, _b, _c, _d, _e;
|
|
3258
3266
|
const list = settings == null ? void 0 : settings.list;
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3267
|
+
const request2 = { email: ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email) || null, list: (settings == null ? void 0 : settings.list) || null };
|
|
3268
|
+
if (!list) return { message: "No Klaviyo list is chosen for this connection.", request: request2, skipped: true };
|
|
3269
|
+
const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
|
|
3270
|
+
if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
|
|
3262
3271
|
const person = (context == null ? void 0 : context.contact) || null;
|
|
3263
3272
|
const totals = (person == null ? void 0 : person.totals) || {};
|
|
3264
3273
|
const count = (value) => typeof value === "number" ? value : (value == null ? void 0 : value.total) || 0;
|
|
@@ -3315,8 +3324,8 @@ var klaviyo_default2 = {
|
|
|
3315
3324
|
},
|
|
3316
3325
|
token
|
|
3317
3326
|
});
|
|
3318
|
-
const profileId = (
|
|
3319
|
-
if (!profileId) return { message: "Klaviyo returned no profile id.", skipped: true };
|
|
3327
|
+
const profileId = (_e = profile == null ? void 0 : profile.data) == null ? void 0 : _e.id;
|
|
3328
|
+
if (!profileId) return { message: "Klaviyo returned no profile id.", request: request2, skipped: true };
|
|
3320
3329
|
await api2("/profile-subscription-bulk-create-jobs/", {
|
|
3321
3330
|
fetcher,
|
|
3322
3331
|
method: "POST",
|
|
@@ -3345,6 +3354,7 @@ var klaviyo_default2 = {
|
|
|
3345
3354
|
// Merged into `context` for later steps in this run.
|
|
3346
3355
|
context: { klaviyoProfileId: profileId },
|
|
3347
3356
|
message: suppressed ? "Synced to Klaviyo as unsubscribed \u2014 this contact has opted out." : "Synced to the Klaviyo list.",
|
|
3357
|
+
request: request2,
|
|
3348
3358
|
// Recorded on the run for support to read back, not a write
|
|
3349
3359
|
// instruction — the hook has already written what it needed to.
|
|
3350
3360
|
response: { klaviyoProfileId: profileId }
|
|
@@ -3380,7 +3390,7 @@ var klaviyo_default2 = {
|
|
|
3380
3390
|
skipped: true
|
|
3381
3391
|
};
|
|
3382
3392
|
}
|
|
3383
|
-
const name = segmentName(segment.title);
|
|
3393
|
+
const name = segmentName(segment.title, segment.id);
|
|
3384
3394
|
const existing = segmentRowFor({ connection: connection2, segment });
|
|
3385
3395
|
let id = null;
|
|
3386
3396
|
if (existing == null ? void 0 : existing.id) {
|
|
@@ -3787,7 +3797,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
|
|
|
3787
3797
|
};
|
|
3788
3798
|
var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
3789
3799
|
var TAG_NAME_LIMIT = 100;
|
|
3790
|
-
var tagName = (title) =>
|
|
3800
|
+
var tagName = (title, id) => {
|
|
3801
|
+
const suffix = " (" + String(id).slice(-6) + ")";
|
|
3802
|
+
return ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT - suffix.length) + suffix;
|
|
3803
|
+
};
|
|
3791
3804
|
var mailchimp_default2 = {
|
|
3792
3805
|
// OAUTH 2, authorization code. Every url below is quoted from
|
|
3793
3806
|
// mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
|
|
@@ -3925,15 +3938,16 @@ var mailchimp_default2 = {
|
|
|
3925
3938
|
// why there is no create-or-update branch here. Quoted from Mailchimp's
|
|
3926
3939
|
// Marketing API reference for the list-members resource.
|
|
3927
3940
|
sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
|
|
3928
|
-
var _a, _b, _c;
|
|
3941
|
+
var _a, _b, _c, _d, _e;
|
|
3929
3942
|
const audience = settings == null ? void 0 : settings.audience;
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3943
|
+
const request2 = { audience: (settings == null ? void 0 : settings.audience) || null, email: ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email) || null };
|
|
3944
|
+
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", request: request2, skipped: true };
|
|
3945
|
+
const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
|
|
3946
|
+
if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
|
|
3933
3947
|
const hash = subscriberHash(email);
|
|
3934
3948
|
const [firstName, ...restOfName] = String((lead == null ? void 0 : lead.name) || "").trim().split(/\s+/).filter(Boolean);
|
|
3935
3949
|
const lastName = restOfName.join(" ");
|
|
3936
|
-
const phone = ((
|
|
3950
|
+
const phone = ((_e = lead == null ? void 0 : lead.phone) == null ? void 0 : _e.number) || null;
|
|
3937
3951
|
const mergeFields = {
|
|
3938
3952
|
...firstName && { FNAME: firstName },
|
|
3939
3953
|
...lastName && { LNAME: lastName },
|
|
@@ -3967,14 +3981,17 @@ var mailchimp_default2 = {
|
|
|
3967
3981
|
}
|
|
3968
3982
|
},
|
|
3969
3983
|
{
|
|
3970
|
-
|
|
3984
|
+
// THE ID AS WELL AS THE TITLE, because the tag name carries it —
|
|
3985
|
+
// see tagName. Without it every name here would end in the
|
|
3986
|
+
// string 'undefined' and match nothing register wrote.
|
|
3987
|
+
$project: { _id: 0, id: 1, title: 1 }
|
|
3971
3988
|
}
|
|
3972
3989
|
]
|
|
3973
3990
|
});
|
|
3974
|
-
const joined = new Set(segments.map((entry) => entry.
|
|
3975
|
-
const tags = (owned || []).
|
|
3976
|
-
name: tagName(title),
|
|
3977
|
-
status: joined.has(
|
|
3991
|
+
const joined = new Set(segments.map((entry) => entry.id));
|
|
3992
|
+
const tags = (owned || []).filter((entry) => entry.id && entry.title).map((entry) => ({
|
|
3993
|
+
name: tagName(entry.title, entry.id),
|
|
3994
|
+
status: joined.has(entry.id) ? "active" : "inactive"
|
|
3978
3995
|
}));
|
|
3979
3996
|
if (tags.length > 0) {
|
|
3980
3997
|
await api3("/lists/" + audience + "/members/" + hash + "/tags", {
|
|
@@ -3990,6 +4007,7 @@ var mailchimp_default2 = {
|
|
|
3990
4007
|
// Merged into `context` for later steps in this run.
|
|
3991
4008
|
context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
|
|
3992
4009
|
message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
|
|
4010
|
+
request: request2,
|
|
3993
4011
|
// Recorded on the run for support to read back, not a write
|
|
3994
4012
|
// instruction — the hook has already written what it needed to.
|
|
3995
4013
|
response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash }
|
|
@@ -4025,7 +4043,7 @@ var mailchimp_default2 = {
|
|
|
4025
4043
|
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
|
|
4026
4044
|
const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
|
|
4027
4045
|
if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
|
|
4028
|
-
const name = tagName(segment.title);
|
|
4046
|
+
const name = tagName(segment.title, segment.id);
|
|
4029
4047
|
const existing = segmentRowFor({ connection: connection2, segment });
|
|
4030
4048
|
let id = null;
|
|
4031
4049
|
if (existing == null ? void 0 : existing.id) {
|
|
@@ -4578,10 +4596,6 @@ var shopify_default2 = {
|
|
|
4578
4596
|
if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
|
|
4579
4597
|
if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
|
|
4580
4598
|
const adminAccessToken = await adminToken();
|
|
4581
|
-
if (!context.shopifyCustomerId) {
|
|
4582
|
-
const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
|
|
4583
|
-
if (!(customer == null ? void 0 : customer.id)) return { message: "Shopify did not return a customer id \u2014 create/lookup failed.", request: request2, response: { skipped: true }, skipped: true };
|
|
4584
|
-
}
|
|
4585
4599
|
const discountCode = await shopify.admin.createDiscountCode({
|
|
4586
4600
|
adminAccessToken,
|
|
4587
4601
|
code: "DB-" + generateDiscountCode(),
|
|
@@ -5025,7 +5039,7 @@ var shopify_default2 = {
|
|
|
5025
5039
|
//
|
|
5026
5040
|
// The shell has already refused a missing or inactive Shopify connection,
|
|
5027
5041
|
// so what is left is the two things only this hook can know are wrong.
|
|
5028
|
-
product: async ({ connection: connection2, context, workflow }, { mintId } = {}) => {
|
|
5042
|
+
product: async ({ connection: connection2, context, workflow }, { mintId, read } = {}) => {
|
|
5029
5043
|
const request2 = {
|
|
5030
5044
|
numericId: (context == null ? void 0 : context.id) || null,
|
|
5031
5045
|
organizationId: workflow.organization,
|
|
@@ -5034,7 +5048,13 @@ var shopify_default2 = {
|
|
|
5034
5048
|
if (!(context == null ? void 0 : context.id)) return { message: "Skipped \u2014 product webhook payload had no id.", request: request2, response: { skipped: true }, skipped: true };
|
|
5035
5049
|
if (!connection2.shop) return { message: "Skipped \u2014 Shopify connection is missing shop domain.", request: request2, response: { skipped: true }, skipped: true };
|
|
5036
5050
|
const providerId = "gid://shopify/Product/" + context.id;
|
|
5037
|
-
const
|
|
5051
|
+
const query = {
|
|
5052
|
+
"provider.id": providerId,
|
|
5053
|
+
"provider.slug": "shopify",
|
|
5054
|
+
"source.domain": connection2.shop
|
|
5055
|
+
};
|
|
5056
|
+
const existing = (read == null ? void 0 : read.get) ? await read.get({ collection: "product", query }) : null;
|
|
5057
|
+
const productId = (existing == null ? void 0 : existing.id) || mintId();
|
|
5038
5058
|
return {
|
|
5039
5059
|
enqueues: [{
|
|
5040
5060
|
data: { product: productId, providerId, shop: connection2.shop },
|
|
@@ -5064,11 +5084,7 @@ var shopify_default2 = {
|
|
|
5064
5084
|
},
|
|
5065
5085
|
operation: "update",
|
|
5066
5086
|
options: { upsert: true },
|
|
5067
|
-
query
|
|
5068
|
-
"provider.id": providerId,
|
|
5069
|
-
"provider.slug": "shopify",
|
|
5070
|
-
"source.domain": connection2.shop
|
|
5071
|
-
}
|
|
5087
|
+
query
|
|
5072
5088
|
}]
|
|
5073
5089
|
};
|
|
5074
5090
|
}
|
|
@@ -5338,7 +5354,7 @@ var shopify_default2 = {
|
|
|
5338
5354
|
options: { jobId: "connection.update.register." + workflow.connection + "." + randomUUID2() },
|
|
5339
5355
|
queue: "connection"
|
|
5340
5356
|
}],
|
|
5341
|
-
message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok,
|
|
5357
|
+
message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, scopes reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, scopes reconciled." : "Health check passed \u2014 ping ok, scopes reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
|
|
5342
5358
|
request: request2,
|
|
5343
5359
|
response: {
|
|
5344
5360
|
billingProbeFailed,
|