@drawbridge/drawbridge-utils 0.0.61 → 0.0.63
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/index.cjs +44 -0
- package/dist/index.d.cts +84 -1
- package/dist/index.d.ts +84 -1
- package/dist/index.js +42 -0
- package/package.json +1 -4
package/dist/index.cjs
CHANGED
|
@@ -43,10 +43,12 @@ __export(index_exports, {
|
|
|
43
43
|
gigabyte: () => gigabyte,
|
|
44
44
|
incrementUsageTotals: () => incrementUsageTotals,
|
|
45
45
|
infinite: () => infinite,
|
|
46
|
+
isDuplicateKey: () => isDuplicateKey,
|
|
46
47
|
isInfinite: () => isInfinite,
|
|
47
48
|
megabyte: () => megabyte,
|
|
48
49
|
nanoid: () => nanoid,
|
|
49
50
|
percentage: () => percentage,
|
|
51
|
+
recordAction: () => recordAction,
|
|
50
52
|
reducers: () => reducers,
|
|
51
53
|
regex: () => regex,
|
|
52
54
|
shareUrls: () => shareUrls,
|
|
@@ -390,6 +392,46 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
390
392
|
query: { id: usageId }
|
|
391
393
|
});
|
|
392
394
|
};
|
|
395
|
+
var isDuplicateKey = (error) => (error == null ? void 0 : error.code) === 11e3 || (error == null ? void 0 : error.code) === "11000" || /E11000|duplicate key/i.test((error == null ? void 0 : error.message) || "");
|
|
396
|
+
var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
397
|
+
if (!controller || !organization || !usageId || !type || !key) return;
|
|
398
|
+
const amount = Number(units);
|
|
399
|
+
if (!Number.isFinite(amount) || amount === 0) return;
|
|
400
|
+
let inserted = false;
|
|
401
|
+
try {
|
|
402
|
+
const { lastErrorObject } = await controller.update({
|
|
403
|
+
collection: "action",
|
|
404
|
+
data: {
|
|
405
|
+
$setOnInsert: {
|
|
406
|
+
organization,
|
|
407
|
+
usage: usageId,
|
|
408
|
+
type,
|
|
409
|
+
units: amount,
|
|
410
|
+
key,
|
|
411
|
+
meta,
|
|
412
|
+
...traceId && { traceId }
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
options: {
|
|
416
|
+
bypassDocumentValidation: true,
|
|
417
|
+
includeResultMetadata: true,
|
|
418
|
+
upsert: true,
|
|
419
|
+
...session && { session }
|
|
420
|
+
},
|
|
421
|
+
query: {
|
|
422
|
+
key,
|
|
423
|
+
usage: usageId
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (isDuplicateKey(error) && !session) return;
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
if (inserted) {
|
|
432
|
+
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
433
|
+
}
|
|
434
|
+
};
|
|
393
435
|
var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
|
|
394
436
|
var reducers = {
|
|
395
437
|
fields: (data2 = [], callback = () => ({})) => data2.reduce(
|
|
@@ -483,10 +525,12 @@ var currency = (val) => (0, import_currency_codes.code)(val);
|
|
|
483
525
|
gigabyte,
|
|
484
526
|
incrementUsageTotals,
|
|
485
527
|
infinite,
|
|
528
|
+
isDuplicateKey,
|
|
486
529
|
isInfinite,
|
|
487
530
|
megabyte,
|
|
488
531
|
nanoid,
|
|
489
532
|
percentage,
|
|
533
|
+
recordAction,
|
|
490
534
|
reducers,
|
|
491
535
|
regex,
|
|
492
536
|
shareUrls,
|
package/dist/index.d.cts
CHANGED
|
@@ -379,6 +379,89 @@ const incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
379
379
|
|
|
380
380
|
};
|
|
381
381
|
|
|
382
|
+
// True for a MongoDB duplicate-key error (unique-index violation).
|
|
383
|
+
const isDuplicateKey = ( error ) =>
|
|
384
|
+
error?.code === 11000
|
|
385
|
+
|| error?.code === '11000'
|
|
386
|
+
|| /E11000|duplicate key/i.test( error?.message || '' );
|
|
387
|
+
|
|
388
|
+
// Record a billable action as a row in the append-only `action` ledger — the
|
|
389
|
+
// source of truth for usage billing (summed per period at changeover).
|
|
390
|
+
//
|
|
391
|
+
// Idempotent via an UPSERT on the unique { usage, key }: a duplicate event (the
|
|
392
|
+
// segment realtime path AND the sweep firing for the same membership, or a retry)
|
|
393
|
+
// matches the existing row and no-ops — no duplicate-key error — so this is safe
|
|
394
|
+
// to call INSIDE a caller's transaction (a thrown E11000 would abort the whole
|
|
395
|
+
// transaction). The display cache `usage.totals.actions` is bumped ONLY when the
|
|
396
|
+
// upsert actually inserts a new row, so a duplicate never double-counts.
|
|
397
|
+
//
|
|
398
|
+
// Pass `session` to make the ledger row + cache bump atomic with the caller's own
|
|
399
|
+
// writes (e.g. the membership writes in a segment-sweep chunk, or the submission
|
|
400
|
+
// docs) — all-or-nothing. Without a session it runs standalone.
|
|
401
|
+
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
402
|
+
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
403
|
+
// meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
|
|
404
|
+
const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
405
|
+
|
|
406
|
+
if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
|
|
407
|
+
|
|
408
|
+
const amount = Number( units );
|
|
409
|
+
|
|
410
|
+
if( ! Number.isFinite( amount ) || amount === 0 ) return;
|
|
411
|
+
|
|
412
|
+
let inserted = false;
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
|
|
416
|
+
const { lastErrorObject } = await controller.update({
|
|
417
|
+
collection : 'action',
|
|
418
|
+
data : {
|
|
419
|
+
$setOnInsert : {
|
|
420
|
+
organization,
|
|
421
|
+
usage : usageId,
|
|
422
|
+
type,
|
|
423
|
+
units : amount,
|
|
424
|
+
key,
|
|
425
|
+
meta,
|
|
426
|
+
...( traceId && { traceId })
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
options : {
|
|
430
|
+
bypassDocumentValidation : true,
|
|
431
|
+
includeResultMetadata : true,
|
|
432
|
+
upsert : true,
|
|
433
|
+
...( session && { session })
|
|
434
|
+
},
|
|
435
|
+
query : {
|
|
436
|
+
key,
|
|
437
|
+
usage : usageId
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
inserted = Boolean( lastErrorObject?.upserted );
|
|
442
|
+
|
|
443
|
+
} catch ( error ) {
|
|
444
|
+
|
|
445
|
+
// A concurrent upsert of the same { usage, key } can still race to E11000.
|
|
446
|
+
// Standalone: the other writer recorded it — skip. Inside a transaction:
|
|
447
|
+
// the txn is already aborted, so re-throw to let the caller retry the whole
|
|
448
|
+
// unit (which then sees the row and no-ops).
|
|
449
|
+
if( isDuplicateKey( error ) && ! session ) return;
|
|
450
|
+
|
|
451
|
+
throw error;
|
|
452
|
+
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Only a genuinely new row bumps the display cache (a matched duplicate must
|
|
456
|
+
// not double-count). Joined to the caller's transaction when a session is given.
|
|
457
|
+
if( inserted ){
|
|
458
|
+
|
|
459
|
+
await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
|
|
460
|
+
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
};
|
|
464
|
+
|
|
382
465
|
const percentage = ( value1, value2, decimals = 1 ) => ( ( ( ( value1 || 0 ) / ( value2 || 0 ) ) * 100 ) || 0 ).toFixed( decimals );
|
|
383
466
|
|
|
384
467
|
const reducers = {
|
|
@@ -477,4 +560,4 @@ const currencies = data.map( ( item ) => ({
|
|
|
477
560
|
|
|
478
561
|
const currency = ( val ) => code( val );
|
|
479
562
|
|
|
480
|
-
export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isInfinite, megabyte, nanoid, percentage, reducers, regex, shareUrls, urlRoot };
|
|
563
|
+
export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isDuplicateKey, isInfinite, megabyte, nanoid, percentage, recordAction, reducers, regex, shareUrls, urlRoot };
|
package/dist/index.d.ts
CHANGED
|
@@ -379,6 +379,89 @@ const incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
379
379
|
|
|
380
380
|
};
|
|
381
381
|
|
|
382
|
+
// True for a MongoDB duplicate-key error (unique-index violation).
|
|
383
|
+
const isDuplicateKey = ( error ) =>
|
|
384
|
+
error?.code === 11000
|
|
385
|
+
|| error?.code === '11000'
|
|
386
|
+
|| /E11000|duplicate key/i.test( error?.message || '' );
|
|
387
|
+
|
|
388
|
+
// Record a billable action as a row in the append-only `action` ledger — the
|
|
389
|
+
// source of truth for usage billing (summed per period at changeover).
|
|
390
|
+
//
|
|
391
|
+
// Idempotent via an UPSERT on the unique { usage, key }: a duplicate event (the
|
|
392
|
+
// segment realtime path AND the sweep firing for the same membership, or a retry)
|
|
393
|
+
// matches the existing row and no-ops — no duplicate-key error — so this is safe
|
|
394
|
+
// to call INSIDE a caller's transaction (a thrown E11000 would abort the whole
|
|
395
|
+
// transaction). The display cache `usage.totals.actions` is bumped ONLY when the
|
|
396
|
+
// upsert actually inserts a new row, so a duplicate never double-counts.
|
|
397
|
+
//
|
|
398
|
+
// Pass `session` to make the ledger row + cache bump atomic with the caller's own
|
|
399
|
+
// writes (e.g. the membership writes in a segment-sweep chunk, or the submission
|
|
400
|
+
// docs) — all-or-nothing. Without a session it runs standalone.
|
|
401
|
+
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
402
|
+
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
403
|
+
// meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
|
|
404
|
+
const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
405
|
+
|
|
406
|
+
if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
|
|
407
|
+
|
|
408
|
+
const amount = Number( units );
|
|
409
|
+
|
|
410
|
+
if( ! Number.isFinite( amount ) || amount === 0 ) return;
|
|
411
|
+
|
|
412
|
+
let inserted = false;
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
|
|
416
|
+
const { lastErrorObject } = await controller.update({
|
|
417
|
+
collection : 'action',
|
|
418
|
+
data : {
|
|
419
|
+
$setOnInsert : {
|
|
420
|
+
organization,
|
|
421
|
+
usage : usageId,
|
|
422
|
+
type,
|
|
423
|
+
units : amount,
|
|
424
|
+
key,
|
|
425
|
+
meta,
|
|
426
|
+
...( traceId && { traceId })
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
options : {
|
|
430
|
+
bypassDocumentValidation : true,
|
|
431
|
+
includeResultMetadata : true,
|
|
432
|
+
upsert : true,
|
|
433
|
+
...( session && { session })
|
|
434
|
+
},
|
|
435
|
+
query : {
|
|
436
|
+
key,
|
|
437
|
+
usage : usageId
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
inserted = Boolean( lastErrorObject?.upserted );
|
|
442
|
+
|
|
443
|
+
} catch ( error ) {
|
|
444
|
+
|
|
445
|
+
// A concurrent upsert of the same { usage, key } can still race to E11000.
|
|
446
|
+
// Standalone: the other writer recorded it — skip. Inside a transaction:
|
|
447
|
+
// the txn is already aborted, so re-throw to let the caller retry the whole
|
|
448
|
+
// unit (which then sees the row and no-ops).
|
|
449
|
+
if( isDuplicateKey( error ) && ! session ) return;
|
|
450
|
+
|
|
451
|
+
throw error;
|
|
452
|
+
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Only a genuinely new row bumps the display cache (a matched duplicate must
|
|
456
|
+
// not double-count). Joined to the caller's transaction when a session is given.
|
|
457
|
+
if( inserted ){
|
|
458
|
+
|
|
459
|
+
await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
|
|
460
|
+
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
};
|
|
464
|
+
|
|
382
465
|
const percentage = ( value1, value2, decimals = 1 ) => ( ( ( ( value1 || 0 ) / ( value2 || 0 ) ) * 100 ) || 0 ).toFixed( decimals );
|
|
383
466
|
|
|
384
467
|
const reducers = {
|
|
@@ -477,4 +560,4 @@ const currencies = data.map( ( item ) => ({
|
|
|
477
560
|
|
|
478
561
|
const currency = ( val ) => code( val );
|
|
479
562
|
|
|
480
|
-
export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isInfinite, megabyte, nanoid, percentage, reducers, regex, shareUrls, urlRoot };
|
|
563
|
+
export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isDuplicateKey, isInfinite, megabyte, nanoid, percentage, recordAction, reducers, regex, shareUrls, urlRoot };
|
package/dist/index.js
CHANGED
|
@@ -336,6 +336,46 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
336
336
|
query: { id: usageId }
|
|
337
337
|
});
|
|
338
338
|
};
|
|
339
|
+
var isDuplicateKey = (error) => (error == null ? void 0 : error.code) === 11e3 || (error == null ? void 0 : error.code) === "11000" || /E11000|duplicate key/i.test((error == null ? void 0 : error.message) || "");
|
|
340
|
+
var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
341
|
+
if (!controller || !organization || !usageId || !type || !key) return;
|
|
342
|
+
const amount = Number(units);
|
|
343
|
+
if (!Number.isFinite(amount) || amount === 0) return;
|
|
344
|
+
let inserted = false;
|
|
345
|
+
try {
|
|
346
|
+
const { lastErrorObject } = await controller.update({
|
|
347
|
+
collection: "action",
|
|
348
|
+
data: {
|
|
349
|
+
$setOnInsert: {
|
|
350
|
+
organization,
|
|
351
|
+
usage: usageId,
|
|
352
|
+
type,
|
|
353
|
+
units: amount,
|
|
354
|
+
key,
|
|
355
|
+
meta,
|
|
356
|
+
...traceId && { traceId }
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
options: {
|
|
360
|
+
bypassDocumentValidation: true,
|
|
361
|
+
includeResultMetadata: true,
|
|
362
|
+
upsert: true,
|
|
363
|
+
...session && { session }
|
|
364
|
+
},
|
|
365
|
+
query: {
|
|
366
|
+
key,
|
|
367
|
+
usage: usageId
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (isDuplicateKey(error) && !session) return;
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
if (inserted) {
|
|
376
|
+
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
377
|
+
}
|
|
378
|
+
};
|
|
339
379
|
var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
|
|
340
380
|
var reducers = {
|
|
341
381
|
fields: (data2 = [], callback = () => ({})) => data2.reduce(
|
|
@@ -428,10 +468,12 @@ export {
|
|
|
428
468
|
gigabyte,
|
|
429
469
|
incrementUsageTotals,
|
|
430
470
|
infinite,
|
|
471
|
+
isDuplicateKey,
|
|
431
472
|
isInfinite,
|
|
432
473
|
megabyte,
|
|
433
474
|
nanoid,
|
|
434
475
|
percentage,
|
|
476
|
+
recordAction,
|
|
435
477
|
reducers,
|
|
436
478
|
regex,
|
|
437
479
|
shareUrls,
|
package/package.json
CHANGED
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
"tsup": "8.5.1",
|
|
18
18
|
"typescript": "5.9.3"
|
|
19
19
|
},
|
|
20
|
-
"overrides": {
|
|
21
|
-
"esbuild": "0.28.1"
|
|
22
|
-
},
|
|
23
20
|
"peerDependencies": {
|
|
24
21
|
"@node-oauth/oauth2-server": "^5.3.0"
|
|
25
22
|
},
|
|
@@ -160,5 +157,5 @@
|
|
|
160
157
|
"build": "tsup && npm publish"
|
|
161
158
|
},
|
|
162
159
|
"types": "dist/index.d.ts",
|
|
163
|
-
"version": "0.0.
|
|
160
|
+
"version": "0.0.63"
|
|
164
161
|
}
|