@drawbridge/drawbridge-utils 0.0.67 → 0.0.69
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/billing.cjs +46 -42
- package/dist/billing.d.cts +65 -4
- package/dist/billing.d.ts +65 -4
- package/dist/billing.js +46 -42
- package/dist/usage.cjs +2 -43
- package/dist/usage.d.cts +6 -84
- package/dist/usage.d.ts +6 -84
- package/dist/usage.js +1 -41
- package/package.json +1 -11
- package/dist/shopify.cjs +0 -253
- package/dist/shopify.d.cts +0 -259
- package/dist/shopify.d.ts +0 -259
- package/dist/shopify.js +0 -213
package/dist/billing.cjs
CHANGED
|
@@ -157,45 +157,6 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
157
157
|
});
|
|
158
158
|
};
|
|
159
159
|
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) || "");
|
|
160
|
-
var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
161
|
-
if (!controller || !organization || !usageId || !type || !key) return;
|
|
162
|
-
const amount = Number(units);
|
|
163
|
-
if (!Number.isFinite(amount) || amount === 0) return;
|
|
164
|
-
let inserted = false;
|
|
165
|
-
try {
|
|
166
|
-
const { lastErrorObject } = await controller.update({
|
|
167
|
-
collection: "action",
|
|
168
|
-
data: {
|
|
169
|
-
$setOnInsert: {
|
|
170
|
-
organization,
|
|
171
|
-
usage: usageId,
|
|
172
|
-
type,
|
|
173
|
-
units: amount,
|
|
174
|
-
key,
|
|
175
|
-
meta,
|
|
176
|
-
...traceId && { traceId }
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
options: {
|
|
180
|
-
bypassDocumentValidation: true,
|
|
181
|
-
includeResultMetadata: true,
|
|
182
|
-
upsert: true,
|
|
183
|
-
...session && { session }
|
|
184
|
-
},
|
|
185
|
-
query: {
|
|
186
|
-
key,
|
|
187
|
-
usage: usageId
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (isDuplicateKey(error) && !session) return;
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
if (inserted) {
|
|
196
|
-
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
160
|
|
|
200
161
|
// lib/billing.js
|
|
201
162
|
var import_drawbridge_telemetry2 = require("@drawbridge/drawbridge-telemetry");
|
|
@@ -346,9 +307,52 @@ var grant = {
|
|
|
346
307
|
admin: ({ db, user, amount }) => credit({ db, user, amount, type: "ai", category: "promotional", source: "admin" })
|
|
347
308
|
};
|
|
348
309
|
var action = {
|
|
349
|
-
// Append a billable action (idempotent on { usage, key }
|
|
350
|
-
//
|
|
351
|
-
|
|
310
|
+
// Append a billable action to the ledger (idempotent UPSERT on { usage, key }:
|
|
311
|
+
// a duplicate event — the segment realtime path AND the sweep, or a retry —
|
|
312
|
+
// matches the existing row and no-ops, so it's safe inside a caller's
|
|
313
|
+
// transaction), then bump the usage.totals.actions display cache ONLY on a
|
|
314
|
+
// genuinely new row. Pass `session` to join the caller's transaction.
|
|
315
|
+
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
316
|
+
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
317
|
+
record: async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
318
|
+
if (!controller || !organization || !usageId || !type || !key) return;
|
|
319
|
+
const amount = Number(units);
|
|
320
|
+
if (!Number.isFinite(amount) || amount === 0) return;
|
|
321
|
+
let inserted = false;
|
|
322
|
+
try {
|
|
323
|
+
const { lastErrorObject } = await controller.update({
|
|
324
|
+
collection: "action",
|
|
325
|
+
data: {
|
|
326
|
+
$setOnInsert: {
|
|
327
|
+
organization,
|
|
328
|
+
usage: usageId,
|
|
329
|
+
type,
|
|
330
|
+
units: amount,
|
|
331
|
+
key,
|
|
332
|
+
meta,
|
|
333
|
+
...traceId && { traceId }
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
options: {
|
|
337
|
+
bypassDocumentValidation: true,
|
|
338
|
+
includeResultMetadata: true,
|
|
339
|
+
upsert: true,
|
|
340
|
+
...session && { session }
|
|
341
|
+
},
|
|
342
|
+
query: {
|
|
343
|
+
key,
|
|
344
|
+
usage: usageId
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (isDuplicateKey(error) && !session) return;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
if (inserted) {
|
|
353
|
+
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
354
|
+
}
|
|
355
|
+
},
|
|
352
356
|
// Authoritative per-period total to bill — sum(units) over the period's ledger
|
|
353
357
|
// rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
|
|
354
358
|
sum: async ({ controller, usageId }) => {
|
package/dist/billing.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { credit, debit } from './transactions.cjs';
|
|
2
|
-
import {
|
|
2
|
+
import { isDuplicateKey, incrementUsageTotals } from './usage.cjs';
|
|
3
3
|
import { createLogger } from '@drawbridge/drawbridge-telemetry';
|
|
4
4
|
|
|
5
5
|
// Billing — the single source of truth for what Drawbridge charges, and the
|
|
@@ -273,9 +273,70 @@ const grant = {
|
|
|
273
273
|
// to the Stripe meter. See the action-ledger design.
|
|
274
274
|
const action = {
|
|
275
275
|
|
|
276
|
-
// Append a billable action (idempotent on { usage, key }
|
|
277
|
-
//
|
|
278
|
-
|
|
276
|
+
// Append a billable action to the ledger (idempotent UPSERT on { usage, key }:
|
|
277
|
+
// a duplicate event — the segment realtime path AND the sweep, or a retry —
|
|
278
|
+
// matches the existing row and no-ops, so it's safe inside a caller's
|
|
279
|
+
// transaction), then bump the usage.totals.actions display cache ONLY on a
|
|
280
|
+
// genuinely new row. Pass `session` to join the caller's transaction.
|
|
281
|
+
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
282
|
+
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
283
|
+
record : async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
284
|
+
|
|
285
|
+
if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
|
|
286
|
+
|
|
287
|
+
const amount = Number( units );
|
|
288
|
+
|
|
289
|
+
if( ! Number.isFinite( amount ) || amount === 0 ) return;
|
|
290
|
+
|
|
291
|
+
let inserted = false;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
|
|
295
|
+
const { lastErrorObject } = await controller.update({
|
|
296
|
+
collection : 'action',
|
|
297
|
+
data : {
|
|
298
|
+
$setOnInsert : {
|
|
299
|
+
organization,
|
|
300
|
+
usage : usageId,
|
|
301
|
+
type,
|
|
302
|
+
units : amount,
|
|
303
|
+
key,
|
|
304
|
+
meta,
|
|
305
|
+
...( traceId && { traceId })
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
options : {
|
|
309
|
+
bypassDocumentValidation : true,
|
|
310
|
+
includeResultMetadata : true,
|
|
311
|
+
upsert : true,
|
|
312
|
+
...( session && { session })
|
|
313
|
+
},
|
|
314
|
+
query : {
|
|
315
|
+
key,
|
|
316
|
+
usage : usageId
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
inserted = Boolean( lastErrorObject?.upserted );
|
|
321
|
+
|
|
322
|
+
} catch ( error ) {
|
|
323
|
+
|
|
324
|
+
// A concurrent upsert of the same { usage, key } can still race to E11000.
|
|
325
|
+
// Standalone: the other writer recorded it — skip. Inside a transaction:
|
|
326
|
+
// the txn is already aborted, so re-throw to let the caller retry the unit.
|
|
327
|
+
if( isDuplicateKey( error ) && ! session ) return;
|
|
328
|
+
|
|
329
|
+
throw error;
|
|
330
|
+
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if( inserted ){
|
|
334
|
+
|
|
335
|
+
await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
},
|
|
279
340
|
|
|
280
341
|
// Authoritative per-period total to bill — sum(units) over the period's ledger
|
|
281
342
|
// rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
|
package/dist/billing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { credit, debit } from './transactions.js';
|
|
2
|
-
import {
|
|
2
|
+
import { isDuplicateKey, incrementUsageTotals } from './usage.js';
|
|
3
3
|
import { createLogger } from '@drawbridge/drawbridge-telemetry';
|
|
4
4
|
|
|
5
5
|
// Billing — the single source of truth for what Drawbridge charges, and the
|
|
@@ -273,9 +273,70 @@ const grant = {
|
|
|
273
273
|
// to the Stripe meter. See the action-ledger design.
|
|
274
274
|
const action = {
|
|
275
275
|
|
|
276
|
-
// Append a billable action (idempotent on { usage, key }
|
|
277
|
-
//
|
|
278
|
-
|
|
276
|
+
// Append a billable action to the ledger (idempotent UPSERT on { usage, key }:
|
|
277
|
+
// a duplicate event — the segment realtime path AND the sweep, or a retry —
|
|
278
|
+
// matches the existing row and no-ops, so it's safe inside a caller's
|
|
279
|
+
// transaction), then bump the usage.totals.actions display cache ONLY on a
|
|
280
|
+
// genuinely new row. Pass `session` to join the caller's transaction.
|
|
281
|
+
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
282
|
+
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
283
|
+
record : async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
284
|
+
|
|
285
|
+
if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
|
|
286
|
+
|
|
287
|
+
const amount = Number( units );
|
|
288
|
+
|
|
289
|
+
if( ! Number.isFinite( amount ) || amount === 0 ) return;
|
|
290
|
+
|
|
291
|
+
let inserted = false;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
|
|
295
|
+
const { lastErrorObject } = await controller.update({
|
|
296
|
+
collection : 'action',
|
|
297
|
+
data : {
|
|
298
|
+
$setOnInsert : {
|
|
299
|
+
organization,
|
|
300
|
+
usage : usageId,
|
|
301
|
+
type,
|
|
302
|
+
units : amount,
|
|
303
|
+
key,
|
|
304
|
+
meta,
|
|
305
|
+
...( traceId && { traceId })
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
options : {
|
|
309
|
+
bypassDocumentValidation : true,
|
|
310
|
+
includeResultMetadata : true,
|
|
311
|
+
upsert : true,
|
|
312
|
+
...( session && { session })
|
|
313
|
+
},
|
|
314
|
+
query : {
|
|
315
|
+
key,
|
|
316
|
+
usage : usageId
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
inserted = Boolean( lastErrorObject?.upserted );
|
|
321
|
+
|
|
322
|
+
} catch ( error ) {
|
|
323
|
+
|
|
324
|
+
// A concurrent upsert of the same { usage, key } can still race to E11000.
|
|
325
|
+
// Standalone: the other writer recorded it — skip. Inside a transaction:
|
|
326
|
+
// the txn is already aborted, so re-throw to let the caller retry the unit.
|
|
327
|
+
if( isDuplicateKey( error ) && ! session ) return;
|
|
328
|
+
|
|
329
|
+
throw error;
|
|
330
|
+
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if( inserted ){
|
|
334
|
+
|
|
335
|
+
await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
},
|
|
279
340
|
|
|
280
341
|
// Authoritative per-period total to bill — sum(units) over the period's ledger
|
|
281
342
|
// rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
|
package/dist/billing.js
CHANGED
|
@@ -128,45 +128,6 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
128
128
|
});
|
|
129
129
|
};
|
|
130
130
|
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) || "");
|
|
131
|
-
var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
132
|
-
if (!controller || !organization || !usageId || !type || !key) return;
|
|
133
|
-
const amount = Number(units);
|
|
134
|
-
if (!Number.isFinite(amount) || amount === 0) return;
|
|
135
|
-
let inserted = false;
|
|
136
|
-
try {
|
|
137
|
-
const { lastErrorObject } = await controller.update({
|
|
138
|
-
collection: "action",
|
|
139
|
-
data: {
|
|
140
|
-
$setOnInsert: {
|
|
141
|
-
organization,
|
|
142
|
-
usage: usageId,
|
|
143
|
-
type,
|
|
144
|
-
units: amount,
|
|
145
|
-
key,
|
|
146
|
-
meta,
|
|
147
|
-
...traceId && { traceId }
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
options: {
|
|
151
|
-
bypassDocumentValidation: true,
|
|
152
|
-
includeResultMetadata: true,
|
|
153
|
-
upsert: true,
|
|
154
|
-
...session && { session }
|
|
155
|
-
},
|
|
156
|
-
query: {
|
|
157
|
-
key,
|
|
158
|
-
usage: usageId
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (isDuplicateKey(error) && !session) return;
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
if (inserted) {
|
|
167
|
-
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
131
|
|
|
171
132
|
// lib/billing.js
|
|
172
133
|
import { createLogger } from "@drawbridge/drawbridge-telemetry";
|
|
@@ -317,9 +278,52 @@ var grant = {
|
|
|
317
278
|
admin: ({ db, user, amount }) => credit({ db, user, amount, type: "ai", category: "promotional", source: "admin" })
|
|
318
279
|
};
|
|
319
280
|
var action = {
|
|
320
|
-
// Append a billable action (idempotent on { usage, key }
|
|
321
|
-
//
|
|
322
|
-
|
|
281
|
+
// Append a billable action to the ledger (idempotent UPSERT on { usage, key }:
|
|
282
|
+
// a duplicate event — the segment realtime path AND the sweep, or a retry —
|
|
283
|
+
// matches the existing row and no-ops, so it's safe inside a caller's
|
|
284
|
+
// transaction), then bump the usage.totals.actions display cache ONLY on a
|
|
285
|
+
// genuinely new row. Pass `session` to join the caller's transaction.
|
|
286
|
+
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
287
|
+
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
288
|
+
record: async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
289
|
+
if (!controller || !organization || !usageId || !type || !key) return;
|
|
290
|
+
const amount = Number(units);
|
|
291
|
+
if (!Number.isFinite(amount) || amount === 0) return;
|
|
292
|
+
let inserted = false;
|
|
293
|
+
try {
|
|
294
|
+
const { lastErrorObject } = await controller.update({
|
|
295
|
+
collection: "action",
|
|
296
|
+
data: {
|
|
297
|
+
$setOnInsert: {
|
|
298
|
+
organization,
|
|
299
|
+
usage: usageId,
|
|
300
|
+
type,
|
|
301
|
+
units: amount,
|
|
302
|
+
key,
|
|
303
|
+
meta,
|
|
304
|
+
...traceId && { traceId }
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
options: {
|
|
308
|
+
bypassDocumentValidation: true,
|
|
309
|
+
includeResultMetadata: true,
|
|
310
|
+
upsert: true,
|
|
311
|
+
...session && { session }
|
|
312
|
+
},
|
|
313
|
+
query: {
|
|
314
|
+
key,
|
|
315
|
+
usage: usageId
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (isDuplicateKey(error) && !session) return;
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
if (inserted) {
|
|
324
|
+
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
325
|
+
}
|
|
326
|
+
},
|
|
323
327
|
// Authoritative per-period total to bill — sum(units) over the period's ledger
|
|
324
328
|
// rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
|
|
325
329
|
sum: async ({ controller, usageId }) => {
|
package/dist/usage.cjs
CHANGED
|
@@ -20,8 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
var usage_exports = {};
|
|
21
21
|
__export(usage_exports, {
|
|
22
22
|
incrementUsageTotals: () => incrementUsageTotals,
|
|
23
|
-
isDuplicateKey: () => isDuplicateKey
|
|
24
|
-
recordAction: () => recordAction
|
|
23
|
+
isDuplicateKey: () => isDuplicateKey
|
|
25
24
|
});
|
|
26
25
|
module.exports = __toCommonJS(usage_exports);
|
|
27
26
|
var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
@@ -44,48 +43,8 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
44
43
|
});
|
|
45
44
|
};
|
|
46
45
|
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) || "");
|
|
47
|
-
var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
48
|
-
if (!controller || !organization || !usageId || !type || !key) return;
|
|
49
|
-
const amount = Number(units);
|
|
50
|
-
if (!Number.isFinite(amount) || amount === 0) return;
|
|
51
|
-
let inserted = false;
|
|
52
|
-
try {
|
|
53
|
-
const { lastErrorObject } = await controller.update({
|
|
54
|
-
collection: "action",
|
|
55
|
-
data: {
|
|
56
|
-
$setOnInsert: {
|
|
57
|
-
organization,
|
|
58
|
-
usage: usageId,
|
|
59
|
-
type,
|
|
60
|
-
units: amount,
|
|
61
|
-
key,
|
|
62
|
-
meta,
|
|
63
|
-
...traceId && { traceId }
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
options: {
|
|
67
|
-
bypassDocumentValidation: true,
|
|
68
|
-
includeResultMetadata: true,
|
|
69
|
-
upsert: true,
|
|
70
|
-
...session && { session }
|
|
71
|
-
},
|
|
72
|
-
query: {
|
|
73
|
-
key,
|
|
74
|
-
usage: usageId
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (isDuplicateKey(error) && !session) return;
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
if (inserted) {
|
|
83
|
-
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
46
|
// Annotate the CommonJS export names for ESM import in node:
|
|
87
47
|
0 && (module.exports = {
|
|
88
48
|
incrementUsageTotals,
|
|
89
|
-
isDuplicateKey
|
|
90
|
-
recordAction
|
|
49
|
+
isDuplicateKey
|
|
91
50
|
});
|
package/dist/usage.d.cts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// Usage helpers — the per-org `usage` doc totals and
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// index.js for backward compatibility with existing import sites.
|
|
1
|
+
// Usage helpers — the per-org `usage` doc totals $inc and a duplicate-key guard.
|
|
2
|
+
// Generic, non-billing primitives (incrementUsageTotals is used for revenue /
|
|
3
|
+
// submissions / entries totals too), so they live here rather than under the
|
|
4
|
+
// billing namespace; billing.action.record (lib/billing.js) uses both to write
|
|
5
|
+
// the action ledger. Re-exported from index.js for existing import sites.
|
|
7
6
|
|
|
8
7
|
// Centralized $inc helper for the per-org usage doc. Replaces inline
|
|
9
8
|
// $inc-with-bypass-validation patterns in drawbridge-api + drawbridge-sync.
|
|
@@ -49,81 +48,4 @@ const isDuplicateKey = ( error ) =>
|
|
|
49
48
|
|| error?.code === '11000'
|
|
50
49
|
|| /E11000|duplicate key/i.test( error?.message || '' );
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
// source of truth for usage billing (summed per period at changeover).
|
|
54
|
-
//
|
|
55
|
-
// Idempotent via an UPSERT on the unique { usage, key }: a duplicate event (the
|
|
56
|
-
// segment realtime path AND the sweep firing for the same membership, or a retry)
|
|
57
|
-
// matches the existing row and no-ops — no duplicate-key error — so this is safe
|
|
58
|
-
// to call INSIDE a caller's transaction (a thrown E11000 would abort the whole
|
|
59
|
-
// transaction). The display cache `usage.totals.actions` is bumped ONLY when the
|
|
60
|
-
// upsert actually inserts a new row, so a duplicate never double-counts.
|
|
61
|
-
//
|
|
62
|
-
// Pass `session` to make the ledger row + cache bump atomic with the caller's own
|
|
63
|
-
// writes (e.g. the membership writes in a segment-sweep chunk, or the submission
|
|
64
|
-
// docs) — all-or-nothing. Without a session it runs standalone.
|
|
65
|
-
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
66
|
-
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
67
|
-
// meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
|
|
68
|
-
const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
69
|
-
|
|
70
|
-
if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
|
|
71
|
-
|
|
72
|
-
const amount = Number( units );
|
|
73
|
-
|
|
74
|
-
if( ! Number.isFinite( amount ) || amount === 0 ) return;
|
|
75
|
-
|
|
76
|
-
let inserted = false;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
|
|
80
|
-
const { lastErrorObject } = await controller.update({
|
|
81
|
-
collection : 'action',
|
|
82
|
-
data : {
|
|
83
|
-
$setOnInsert : {
|
|
84
|
-
organization,
|
|
85
|
-
usage : usageId,
|
|
86
|
-
type,
|
|
87
|
-
units : amount,
|
|
88
|
-
key,
|
|
89
|
-
meta,
|
|
90
|
-
...( traceId && { traceId })
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
options : {
|
|
94
|
-
bypassDocumentValidation : true,
|
|
95
|
-
includeResultMetadata : true,
|
|
96
|
-
upsert : true,
|
|
97
|
-
...( session && { session })
|
|
98
|
-
},
|
|
99
|
-
query : {
|
|
100
|
-
key,
|
|
101
|
-
usage : usageId
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
inserted = Boolean( lastErrorObject?.upserted );
|
|
106
|
-
|
|
107
|
-
} catch ( error ) {
|
|
108
|
-
|
|
109
|
-
// A concurrent upsert of the same { usage, key } can still race to E11000.
|
|
110
|
-
// Standalone: the other writer recorded it — skip. Inside a transaction:
|
|
111
|
-
// the txn is already aborted, so re-throw to let the caller retry the whole
|
|
112
|
-
// unit (which then sees the row and no-ops).
|
|
113
|
-
if( isDuplicateKey( error ) && ! session ) return;
|
|
114
|
-
|
|
115
|
-
throw error;
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Only a genuinely new row bumps the display cache (a matched duplicate must
|
|
120
|
-
// not double-count). Joined to the caller's transaction when a session is given.
|
|
121
|
-
if( inserted ){
|
|
122
|
-
|
|
123
|
-
await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export { incrementUsageTotals, isDuplicateKey, recordAction };
|
|
51
|
+
export { incrementUsageTotals, isDuplicateKey };
|
package/dist/usage.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// Usage helpers — the per-org `usage` doc totals and
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// index.js for backward compatibility with existing import sites.
|
|
1
|
+
// Usage helpers — the per-org `usage` doc totals $inc and a duplicate-key guard.
|
|
2
|
+
// Generic, non-billing primitives (incrementUsageTotals is used for revenue /
|
|
3
|
+
// submissions / entries totals too), so they live here rather than under the
|
|
4
|
+
// billing namespace; billing.action.record (lib/billing.js) uses both to write
|
|
5
|
+
// the action ledger. Re-exported from index.js for existing import sites.
|
|
7
6
|
|
|
8
7
|
// Centralized $inc helper for the per-org usage doc. Replaces inline
|
|
9
8
|
// $inc-with-bypass-validation patterns in drawbridge-api + drawbridge-sync.
|
|
@@ -49,81 +48,4 @@ const isDuplicateKey = ( error ) =>
|
|
|
49
48
|
|| error?.code === '11000'
|
|
50
49
|
|| /E11000|duplicate key/i.test( error?.message || '' );
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
// source of truth for usage billing (summed per period at changeover).
|
|
54
|
-
//
|
|
55
|
-
// Idempotent via an UPSERT on the unique { usage, key }: a duplicate event (the
|
|
56
|
-
// segment realtime path AND the sweep firing for the same membership, or a retry)
|
|
57
|
-
// matches the existing row and no-ops — no duplicate-key error — so this is safe
|
|
58
|
-
// to call INSIDE a caller's transaction (a thrown E11000 would abort the whole
|
|
59
|
-
// transaction). The display cache `usage.totals.actions` is bumped ONLY when the
|
|
60
|
-
// upsert actually inserts a new row, so a duplicate never double-counts.
|
|
61
|
-
//
|
|
62
|
-
// Pass `session` to make the ledger row + cache bump atomic with the caller's own
|
|
63
|
-
// writes (e.g. the membership writes in a segment-sweep chunk, or the submission
|
|
64
|
-
// docs) — all-or-nothing. Without a session it runs standalone.
|
|
65
|
-
// key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
|
|
66
|
-
// units billable units (default 1; N for batch, e.g. accepted entries)
|
|
67
|
-
// meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
|
|
68
|
-
const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
69
|
-
|
|
70
|
-
if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
|
|
71
|
-
|
|
72
|
-
const amount = Number( units );
|
|
73
|
-
|
|
74
|
-
if( ! Number.isFinite( amount ) || amount === 0 ) return;
|
|
75
|
-
|
|
76
|
-
let inserted = false;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
|
|
80
|
-
const { lastErrorObject } = await controller.update({
|
|
81
|
-
collection : 'action',
|
|
82
|
-
data : {
|
|
83
|
-
$setOnInsert : {
|
|
84
|
-
organization,
|
|
85
|
-
usage : usageId,
|
|
86
|
-
type,
|
|
87
|
-
units : amount,
|
|
88
|
-
key,
|
|
89
|
-
meta,
|
|
90
|
-
...( traceId && { traceId })
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
options : {
|
|
94
|
-
bypassDocumentValidation : true,
|
|
95
|
-
includeResultMetadata : true,
|
|
96
|
-
upsert : true,
|
|
97
|
-
...( session && { session })
|
|
98
|
-
},
|
|
99
|
-
query : {
|
|
100
|
-
key,
|
|
101
|
-
usage : usageId
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
inserted = Boolean( lastErrorObject?.upserted );
|
|
106
|
-
|
|
107
|
-
} catch ( error ) {
|
|
108
|
-
|
|
109
|
-
// A concurrent upsert of the same { usage, key } can still race to E11000.
|
|
110
|
-
// Standalone: the other writer recorded it — skip. Inside a transaction:
|
|
111
|
-
// the txn is already aborted, so re-throw to let the caller retry the whole
|
|
112
|
-
// unit (which then sees the row and no-ops).
|
|
113
|
-
if( isDuplicateKey( error ) && ! session ) return;
|
|
114
|
-
|
|
115
|
-
throw error;
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Only a genuinely new row bumps the display cache (a matched duplicate must
|
|
120
|
-
// not double-count). Joined to the caller's transaction when a session is given.
|
|
121
|
-
if( inserted ){
|
|
122
|
-
|
|
123
|
-
await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export { incrementUsageTotals, isDuplicateKey, recordAction };
|
|
51
|
+
export { incrementUsageTotals, isDuplicateKey };
|
package/dist/usage.js
CHANGED
|
@@ -19,47 +19,7 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
|
19
19
|
});
|
|
20
20
|
};
|
|
21
21
|
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) || "");
|
|
22
|
-
var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
|
|
23
|
-
if (!controller || !organization || !usageId || !type || !key) return;
|
|
24
|
-
const amount = Number(units);
|
|
25
|
-
if (!Number.isFinite(amount) || amount === 0) return;
|
|
26
|
-
let inserted = false;
|
|
27
|
-
try {
|
|
28
|
-
const { lastErrorObject } = await controller.update({
|
|
29
|
-
collection: "action",
|
|
30
|
-
data: {
|
|
31
|
-
$setOnInsert: {
|
|
32
|
-
organization,
|
|
33
|
-
usage: usageId,
|
|
34
|
-
type,
|
|
35
|
-
units: amount,
|
|
36
|
-
key,
|
|
37
|
-
meta,
|
|
38
|
-
...traceId && { traceId }
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
options: {
|
|
42
|
-
bypassDocumentValidation: true,
|
|
43
|
-
includeResultMetadata: true,
|
|
44
|
-
upsert: true,
|
|
45
|
-
...session && { session }
|
|
46
|
-
},
|
|
47
|
-
query: {
|
|
48
|
-
key,
|
|
49
|
-
usage: usageId
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (isDuplicateKey(error) && !session) return;
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
if (inserted) {
|
|
58
|
-
await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
22
|
export {
|
|
62
23
|
incrementUsageTotals,
|
|
63
|
-
isDuplicateKey
|
|
64
|
-
recordAction
|
|
24
|
+
isDuplicateKey
|
|
65
25
|
};
|