@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 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.61"
160
+ "version": "0.0.63"
164
161
  }