@happyvertical/smrt-subscriptions 0.30.0
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/AGENTS.md +26 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/collections/SubscriptionPlanCollection.d.ts +9 -0
- package/dist/collections/SubscriptionPlanCollection.d.ts.map +1 -0
- package/dist/collections/TenantSubscriptionCollection.d.ts +29 -0
- package/dist/collections/TenantSubscriptionCollection.d.ts.map +1 -0
- package/dist/collections/TenantUsageMetricCollection.d.ts +28 -0
- package/dist/collections/TenantUsageMetricCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +4 -0
- package/dist/collections/index.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1271 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +1291 -0
- package/dist/models/SubscriptionPlan.d.ts +30 -0
- package/dist/models/SubscriptionPlan.d.ts.map +1 -0
- package/dist/models/TenantSubscription.d.ts +59 -0
- package/dist/models/TenantSubscription.d.ts.map +1 -0
- package/dist/models/TenantUsageMetric.d.ts +35 -0
- package/dist/models/TenantUsageMetric.d.ts.map +1 -0
- package/dist/models/index.d.ts +4 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/subscription-resolver.d.ts +96 -0
- package/dist/services/subscription-resolver.d.ts.map +1 -0
- package/dist/services/threshold-evaluator.d.ts +4 -0
- package/dist/services/threshold-evaluator.d.ts.map +1 -0
- package/dist/services/usage-meter.d.ts +32 -0
- package/dist/services/usage-meter.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +883 -0
- package/dist/svelte/PlanPicker.svelte +82 -0
- package/dist/svelte/PlanPicker.svelte.d.ts +10 -0
- package/dist/svelte/PlanPicker.svelte.d.ts.map +1 -0
- package/dist/svelte/SubscriptionSummary.svelte +65 -0
- package/dist/svelte/SubscriptionSummary.svelte.d.ts +8 -0
- package/dist/svelte/SubscriptionSummary.svelte.d.ts.map +1 -0
- package/dist/svelte/UsageThresholds.svelte +71 -0
- package/dist/svelte/UsageThresholds.svelte.d.ts +8 -0
- package/dist/svelte/UsageThresholds.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +5 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +9 -0
- package/dist/svelte/index.d.ts +4 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +3 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +59 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +80 -0
- package/scripts/migrate-1454-drop-legacy-conflict-index.ts +110 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
import { ObjectRegistry, smrt, SmrtObject, SmrtCollection, foreignKey } from "@happyvertical/smrt-core";
|
|
2
|
+
import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
|
|
3
|
+
ObjectRegistry.registerPackageManifest(
|
|
4
|
+
new URL("./manifest.json", import.meta.url)
|
|
5
|
+
);
|
|
6
|
+
const THRESHOLD_WINDOWS = [
|
|
7
|
+
"day",
|
|
8
|
+
"week",
|
|
9
|
+
"month",
|
|
10
|
+
"year",
|
|
11
|
+
"rolling"
|
|
12
|
+
];
|
|
13
|
+
const THRESHOLD_ENFORCEMENTS = [
|
|
14
|
+
"observe",
|
|
15
|
+
"warn",
|
|
16
|
+
"block"
|
|
17
|
+
];
|
|
18
|
+
function parseJsonArray(value, fallback = []) {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(value);
|
|
24
|
+
return Array.isArray(parsed) ? parsed : fallback;
|
|
25
|
+
} catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function parseJsonObject(value, fallback) {
|
|
30
|
+
if (!value) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(value);
|
|
35
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
|
|
36
|
+
} catch {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function stringifyJson(value) {
|
|
41
|
+
return JSON.stringify(value ?? null);
|
|
42
|
+
}
|
|
43
|
+
function normalizeFeatureGrants(grants) {
|
|
44
|
+
return grants.map(
|
|
45
|
+
(grant) => typeof grant === "string" ? { featureKey: grant, enabled: true } : grant
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function normalizeSubscriber(input) {
|
|
49
|
+
const rawExternalId = input.subscriberExternalId;
|
|
50
|
+
const externalIdAbsent = rawExternalId === null || rawExternalId === void 0;
|
|
51
|
+
const externalId = externalIdAbsent ? "" : String(rawExternalId);
|
|
52
|
+
const rawKind = input.subscriberKind;
|
|
53
|
+
const kind = rawKind === null || rawKind === void 0 ? "tenant" : rawKind;
|
|
54
|
+
if (kind !== "tenant" && kind !== "external") {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`subscriberKind must be "tenant" or "external" (got ${JSON.stringify(rawKind)})`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (kind === "tenant") {
|
|
60
|
+
if (externalId !== "") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'subscriberExternalId is set but subscriberKind is "tenant"; set subscriberKind="external" explicitly or omit subscriberExternalId'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return { kind: "tenant", tenantId: input.tenantId };
|
|
66
|
+
}
|
|
67
|
+
if (externalId === "") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'subscriberKind="external" requires a non-empty subscriberExternalId'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
kind: "external",
|
|
74
|
+
tenantId: input.tenantId,
|
|
75
|
+
externalId
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function subscriberToColumns(subscriber) {
|
|
79
|
+
if (subscriber.kind === "tenant") {
|
|
80
|
+
return {
|
|
81
|
+
tenantId: subscriber.tenantId,
|
|
82
|
+
subscriberKind: "tenant",
|
|
83
|
+
subscriberExternalId: ""
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
tenantId: subscriber.tenantId,
|
|
88
|
+
subscriberKind: "external",
|
|
89
|
+
subscriberExternalId: subscriber.externalId
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function assertSubscriberInvariant(modelName, fields) {
|
|
93
|
+
if (fields.subscriberKind !== "tenant" && fields.subscriberKind !== "external") {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${modelName}: subscriberKind must be "tenant" or "external" (got ${describe(
|
|
96
|
+
fields.subscriberKind
|
|
97
|
+
)})`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (typeof fields.subscriberExternalId !== "string") {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${modelName}: subscriberExternalId must be a string (got ${describe(
|
|
103
|
+
fields.subscriberExternalId
|
|
104
|
+
)})`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (fields.subscriberKind === "tenant" && fields.subscriberExternalId !== "") {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`${modelName}: subscriberExternalId must be empty when subscriberKind is "tenant"`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (fields.subscriberKind === "external" && fields.subscriberExternalId === "") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`${modelName}: subscriberKind="external" requires a non-empty subscriberExternalId`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function describe(value) {
|
|
119
|
+
if (value === null) return "null";
|
|
120
|
+
if (value === void 0) return "undefined";
|
|
121
|
+
if (typeof value === "string") return `"${value}"`;
|
|
122
|
+
return String(value);
|
|
123
|
+
}
|
|
124
|
+
function getWindowForThreshold(thresholdWindow, now = /* @__PURE__ */ new Date()) {
|
|
125
|
+
switch (thresholdWindow) {
|
|
126
|
+
case "day":
|
|
127
|
+
return utcWindow(now, "day");
|
|
128
|
+
case "week":
|
|
129
|
+
return utcWindow(now, "week");
|
|
130
|
+
case "month":
|
|
131
|
+
return utcWindow(now, "month");
|
|
132
|
+
case "year":
|
|
133
|
+
return utcWindow(now, "year");
|
|
134
|
+
case "rolling":
|
|
135
|
+
return {
|
|
136
|
+
start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3),
|
|
137
|
+
end: now
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function getWindowKey(window) {
|
|
142
|
+
return `${window.start.toISOString()}..${window.end.toISOString()}`;
|
|
143
|
+
}
|
|
144
|
+
function isValidThreshold(threshold) {
|
|
145
|
+
return typeof threshold.metricKey === "string" && threshold.metricKey.length > 0 && Number.isFinite(threshold.limit) && threshold.limit >= 0 && isThresholdWindow(threshold.window) && isThresholdEnforcement(threshold.enforcement) && (threshold.warningRatio === void 0 || Number.isFinite(threshold.warningRatio) && threshold.warningRatio >= 0 && threshold.warningRatio <= 1);
|
|
146
|
+
}
|
|
147
|
+
function isThresholdWindow(value) {
|
|
148
|
+
return typeof value === "string" && THRESHOLD_WINDOWS.includes(value);
|
|
149
|
+
}
|
|
150
|
+
function isThresholdEnforcement(value) {
|
|
151
|
+
return typeof value === "string" && THRESHOLD_ENFORCEMENTS.includes(value);
|
|
152
|
+
}
|
|
153
|
+
function utcWindow(now, unit) {
|
|
154
|
+
const start = new Date(
|
|
155
|
+
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
|
156
|
+
);
|
|
157
|
+
if (unit === "week") {
|
|
158
|
+
const day = start.getUTCDay();
|
|
159
|
+
const mondayOffset = day === 0 ? -6 : 1 - day;
|
|
160
|
+
start.setUTCDate(start.getUTCDate() + mondayOffset);
|
|
161
|
+
}
|
|
162
|
+
if (unit === "month") {
|
|
163
|
+
start.setUTCDate(1);
|
|
164
|
+
}
|
|
165
|
+
if (unit === "year") {
|
|
166
|
+
start.setUTCMonth(0, 1);
|
|
167
|
+
}
|
|
168
|
+
const end = new Date(start);
|
|
169
|
+
switch (unit) {
|
|
170
|
+
case "day":
|
|
171
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
172
|
+
break;
|
|
173
|
+
case "week":
|
|
174
|
+
end.setUTCDate(end.getUTCDate() + 7);
|
|
175
|
+
break;
|
|
176
|
+
case "month":
|
|
177
|
+
end.setUTCMonth(end.getUTCMonth() + 1);
|
|
178
|
+
break;
|
|
179
|
+
case "year":
|
|
180
|
+
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
return { start, end };
|
|
184
|
+
}
|
|
185
|
+
var __defProp$2 = Object.defineProperty;
|
|
186
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
187
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
188
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
189
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
190
|
+
if (decorator = decorators[i])
|
|
191
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
192
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
193
|
+
return result;
|
|
194
|
+
};
|
|
195
|
+
let SubscriptionPlan = class extends SmrtObject {
|
|
196
|
+
tenantId = null;
|
|
197
|
+
planKey = "";
|
|
198
|
+
name = "";
|
|
199
|
+
description = "";
|
|
200
|
+
status = "active";
|
|
201
|
+
sortOrder = 0;
|
|
202
|
+
priceAmount = 0;
|
|
203
|
+
currency = "USD";
|
|
204
|
+
billingInterval = "month";
|
|
205
|
+
externalProvider = "stripe";
|
|
206
|
+
stripeProductId = "";
|
|
207
|
+
stripePriceId = "";
|
|
208
|
+
features = "[]";
|
|
209
|
+
thresholds = "[]";
|
|
210
|
+
metadata = "{}";
|
|
211
|
+
constructor(options = {}) {
|
|
212
|
+
super(options);
|
|
213
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
214
|
+
if (options.planKey !== void 0) this.planKey = options.planKey;
|
|
215
|
+
if (options.name !== void 0) this.name = options.name;
|
|
216
|
+
if (options.description !== void 0)
|
|
217
|
+
this.description = options.description;
|
|
218
|
+
if (options.status !== void 0) this.status = options.status;
|
|
219
|
+
if (options.sortOrder !== void 0) this.sortOrder = options.sortOrder;
|
|
220
|
+
if (options.priceAmount !== void 0)
|
|
221
|
+
this.priceAmount = options.priceAmount;
|
|
222
|
+
if (options.currency !== void 0) this.currency = options.currency;
|
|
223
|
+
if (options.billingInterval !== void 0) {
|
|
224
|
+
this.billingInterval = options.billingInterval;
|
|
225
|
+
}
|
|
226
|
+
if (options.externalProvider !== void 0) {
|
|
227
|
+
this.externalProvider = options.externalProvider;
|
|
228
|
+
}
|
|
229
|
+
if (options.stripeProductId !== void 0) {
|
|
230
|
+
this.stripeProductId = options.stripeProductId;
|
|
231
|
+
}
|
|
232
|
+
if (options.stripePriceId !== void 0) {
|
|
233
|
+
this.stripePriceId = options.stripePriceId;
|
|
234
|
+
}
|
|
235
|
+
if (options.features !== void 0) this.features = options.features;
|
|
236
|
+
if (options.thresholds !== void 0) this.thresholds = options.thresholds;
|
|
237
|
+
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
238
|
+
}
|
|
239
|
+
isActive() {
|
|
240
|
+
return this.status === "active";
|
|
241
|
+
}
|
|
242
|
+
getFeatureGrants() {
|
|
243
|
+
return normalizeFeatureGrants(
|
|
244
|
+
parseJsonArray(this.features)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
setFeatureGrants(grants) {
|
|
248
|
+
this.features = stringifyJson(normalizeFeatureGrants(grants));
|
|
249
|
+
}
|
|
250
|
+
getFeatureKeys() {
|
|
251
|
+
return this.getFeatureGrants().filter((grant) => grant.enabled !== false).map((grant) => grant.featureKey);
|
|
252
|
+
}
|
|
253
|
+
getThresholds() {
|
|
254
|
+
return parseJsonArray(this.thresholds).filter(
|
|
255
|
+
(threshold) => threshold.metricKey
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
setThresholds(thresholds) {
|
|
259
|
+
this.thresholds = stringifyJson(thresholds);
|
|
260
|
+
}
|
|
261
|
+
getMetadata() {
|
|
262
|
+
return parseJsonObject(this.metadata, {});
|
|
263
|
+
}
|
|
264
|
+
setMetadata(metadata) {
|
|
265
|
+
this.metadata = stringifyJson(metadata);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
__decorateClass$2([
|
|
269
|
+
tenantId({ nullable: true })
|
|
270
|
+
], SubscriptionPlan.prototype, "tenantId", 2);
|
|
271
|
+
SubscriptionPlan = __decorateClass$2([
|
|
272
|
+
TenantScoped({ mode: "optional" }),
|
|
273
|
+
smrt({
|
|
274
|
+
tableName: "_smrt_subscription_plans",
|
|
275
|
+
api: { include: ["list", "get", "create", "update"] },
|
|
276
|
+
cli: true,
|
|
277
|
+
mcp: { include: ["list", "get"] },
|
|
278
|
+
conflictColumns: ["plan_key"]
|
|
279
|
+
})
|
|
280
|
+
], SubscriptionPlan);
|
|
281
|
+
class SubscriptionPlanCollection extends SmrtCollection {
|
|
282
|
+
static _itemClass = SubscriptionPlan;
|
|
283
|
+
async findByPlanKey(planKey) {
|
|
284
|
+
const plans = await this.list({ where: { planKey }, limit: 1 });
|
|
285
|
+
return plans[0] ?? null;
|
|
286
|
+
}
|
|
287
|
+
async findActive() {
|
|
288
|
+
return this.list({
|
|
289
|
+
where: { status: "active" },
|
|
290
|
+
orderBy: "sortOrder ASC"
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async findByStripePriceId(stripePriceId) {
|
|
294
|
+
const plans = await this.list({ where: { stripePriceId }, limit: 1 });
|
|
295
|
+
return plans[0] ?? null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
var __defProp$1 = Object.defineProperty;
|
|
299
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
300
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
301
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
302
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
303
|
+
if (decorator = decorators[i])
|
|
304
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
305
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
306
|
+
return result;
|
|
307
|
+
};
|
|
308
|
+
let TenantSubscription = class extends SmrtObject {
|
|
309
|
+
tenantId;
|
|
310
|
+
/**
|
|
311
|
+
* Discriminator for the subscriber identity.
|
|
312
|
+
*
|
|
313
|
+
* - `'tenant'` (default): the subscriber IS the owning `tenantId` — the
|
|
314
|
+
* pre-polymorphic shape. All existing rows continue to behave this way.
|
|
315
|
+
* - `'external'`: the subscriber is `subscriberExternalId`, scoped under the
|
|
316
|
+
* issuing `tenantId`. Used for B2C buyers, anonymous-email subscribers,
|
|
317
|
+
* agent identities, and any other caller-defined identity.
|
|
318
|
+
*/
|
|
319
|
+
subscriberKind = "tenant";
|
|
320
|
+
/**
|
|
321
|
+
* Caller-namespaced opaque identifier for the subscriber when
|
|
322
|
+
* `subscriberKind === 'external'`. Empty string when kind is `'tenant'`.
|
|
323
|
+
*
|
|
324
|
+
* The package treats this as opaque — no FK, no inferred semantics. Callers
|
|
325
|
+
* are expected to namespace (e.g. `buyer-contact:abc123`,
|
|
326
|
+
* `agent:hermes-7`, `email:foo@example.com`).
|
|
327
|
+
*/
|
|
328
|
+
subscriberExternalId = "";
|
|
329
|
+
planId = "";
|
|
330
|
+
status = "incomplete";
|
|
331
|
+
startedAt = /* @__PURE__ */ new Date();
|
|
332
|
+
currentPeriodStart = null;
|
|
333
|
+
currentPeriodEnd = null;
|
|
334
|
+
trialEndsAt = null;
|
|
335
|
+
cancelAtPeriodEnd = false;
|
|
336
|
+
canceledAt = null;
|
|
337
|
+
externalProvider = "stripe";
|
|
338
|
+
stripeCustomerId = "";
|
|
339
|
+
stripeSubscriptionId = "";
|
|
340
|
+
stripeCheckoutSessionId = "";
|
|
341
|
+
metadata = "{}";
|
|
342
|
+
constructor(options = {}) {
|
|
343
|
+
super(options);
|
|
344
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
345
|
+
if (options.subscriberKind !== void 0)
|
|
346
|
+
this.subscriberKind = options.subscriberKind;
|
|
347
|
+
if (options.subscriberExternalId !== void 0)
|
|
348
|
+
this.subscriberExternalId = options.subscriberExternalId;
|
|
349
|
+
assertSubscriberInvariant("TenantSubscription", {
|
|
350
|
+
subscriberKind: this.subscriberKind,
|
|
351
|
+
subscriberExternalId: this.subscriberExternalId
|
|
352
|
+
});
|
|
353
|
+
if (options.planId !== void 0) this.planId = options.planId;
|
|
354
|
+
if (options.status !== void 0) this.status = options.status;
|
|
355
|
+
if (options.startedAt !== void 0) this.startedAt = options.startedAt;
|
|
356
|
+
if (options.currentPeriodStart !== void 0) {
|
|
357
|
+
this.currentPeriodStart = options.currentPeriodStart;
|
|
358
|
+
}
|
|
359
|
+
if (options.currentPeriodEnd !== void 0) {
|
|
360
|
+
this.currentPeriodEnd = options.currentPeriodEnd;
|
|
361
|
+
}
|
|
362
|
+
if (options.trialEndsAt !== void 0)
|
|
363
|
+
this.trialEndsAt = options.trialEndsAt;
|
|
364
|
+
if (options.cancelAtPeriodEnd !== void 0) {
|
|
365
|
+
this.cancelAtPeriodEnd = options.cancelAtPeriodEnd;
|
|
366
|
+
}
|
|
367
|
+
if (options.canceledAt !== void 0) this.canceledAt = options.canceledAt;
|
|
368
|
+
if (options.externalProvider !== void 0) {
|
|
369
|
+
this.externalProvider = options.externalProvider;
|
|
370
|
+
}
|
|
371
|
+
if (options.stripeCustomerId !== void 0) {
|
|
372
|
+
this.stripeCustomerId = options.stripeCustomerId;
|
|
373
|
+
}
|
|
374
|
+
if (options.stripeSubscriptionId !== void 0) {
|
|
375
|
+
this.stripeSubscriptionId = options.stripeSubscriptionId;
|
|
376
|
+
}
|
|
377
|
+
if (options.stripeCheckoutSessionId !== void 0) {
|
|
378
|
+
this.stripeCheckoutSessionId = options.stripeCheckoutSessionId;
|
|
379
|
+
}
|
|
380
|
+
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Validates the subscriber XOR invariant before every save. Critical for the
|
|
384
|
+
* generated update path: `SmrtCollection.update()` mutates the existing
|
|
385
|
+
* instance via `Object.assign()` and calls `save()` directly — the
|
|
386
|
+
* constructor never re-runs, so without this hook a partial update like
|
|
387
|
+
* `{ subscriberExternalId: 'buyer:alice' }` on a tenant-kind row would
|
|
388
|
+
* persist a malformed conflict key and let two tenant-kind rows coexist
|
|
389
|
+
* under the same tenant.
|
|
390
|
+
*/
|
|
391
|
+
async validateBeforeSave() {
|
|
392
|
+
await super.validateBeforeSave();
|
|
393
|
+
assertSubscriberInvariant("TenantSubscription", {
|
|
394
|
+
subscriberKind: this.subscriberKind,
|
|
395
|
+
subscriberExternalId: this.subscriberExternalId
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
isEntitled(now = /* @__PURE__ */ new Date()) {
|
|
399
|
+
if (this.status !== "active" && this.status !== "trialing") {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
if (this.currentPeriodEnd && this.currentPeriodEnd.getTime() < now.getTime()) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Project this row's polymorphic subscriber columns onto the
|
|
409
|
+
* {@link Subscriber} discriminated union. Returns `null` when the owning
|
|
410
|
+
* tenant is absent — that's an invalid row that should not be acted on.
|
|
411
|
+
*/
|
|
412
|
+
getSubscriber() {
|
|
413
|
+
if (!this.tenantId) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
if (this.subscriberKind === "external") {
|
|
417
|
+
if (!this.subscriberExternalId) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
kind: "external",
|
|
422
|
+
tenantId: this.tenantId,
|
|
423
|
+
externalId: this.subscriberExternalId
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return { kind: "tenant", tenantId: this.tenantId };
|
|
427
|
+
}
|
|
428
|
+
getMetadata() {
|
|
429
|
+
return parseJsonObject(this.metadata, {});
|
|
430
|
+
}
|
|
431
|
+
setMetadata(metadata) {
|
|
432
|
+
this.metadata = stringifyJson(metadata);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
__decorateClass$1([
|
|
436
|
+
tenantId()
|
|
437
|
+
], TenantSubscription.prototype, "tenantId", 2);
|
|
438
|
+
__decorateClass$1([
|
|
439
|
+
foreignKey("SubscriptionPlan")
|
|
440
|
+
], TenantSubscription.prototype, "planId", 2);
|
|
441
|
+
TenantSubscription = __decorateClass$1([
|
|
442
|
+
TenantScoped({ mode: "required" }),
|
|
443
|
+
smrt({
|
|
444
|
+
tableName: "_smrt_tenant_subscriptions",
|
|
445
|
+
api: { include: ["list", "get", "create", "update"] },
|
|
446
|
+
cli: true,
|
|
447
|
+
mcp: { include: ["list", "get"] },
|
|
448
|
+
// The conflict key includes the subscriber discriminator so an issuing tenant
|
|
449
|
+
// can host many distinct external subscribers without collisions on the
|
|
450
|
+
// unique index:
|
|
451
|
+
// - tenant-kind rows always have subscriber_external_id = '' → uniqueness
|
|
452
|
+
// reduces to (tenant_id, 'tenant', '') = at most one tenant-kind row per
|
|
453
|
+
// tenant (preserves the pre-polymorphic invariant).
|
|
454
|
+
// - external-kind rows are unique on (tenant_id, 'external', externalId) =
|
|
455
|
+
// at most one active subscription per (issuer, external subscriber).
|
|
456
|
+
//
|
|
457
|
+
// UPGRADE NOTE: databases created against the pre-#1454 conflict key
|
|
458
|
+
// ['tenant_id'] still carry the legacy `_smrt_tenant_subscriptions_tenant_id_idx`.
|
|
459
|
+
// `smrt db:migrate` does not sweep orphan indexes by default, so the legacy
|
|
460
|
+
// index persists and continues to reject external subscriptions until it's
|
|
461
|
+
// dropped. Run scripts/migrate-1454-drop-legacy-conflict-index.ts once on
|
|
462
|
+
// upgrade, or pass `--drop-indexes` to `db:migrate`. Fresh databases need
|
|
463
|
+
// neither — the generated schema already reflects the new key.
|
|
464
|
+
conflictColumns: ["tenant_id", "subscriber_kind", "subscriber_external_id"]
|
|
465
|
+
})
|
|
466
|
+
], TenantSubscription);
|
|
467
|
+
class TenantSubscriptionCollection extends SmrtCollection {
|
|
468
|
+
static _itemClass = TenantSubscription;
|
|
469
|
+
/**
|
|
470
|
+
* List subscriptions whose owning tenant scope is `tenantId`. Includes both
|
|
471
|
+
* `'tenant'`-kind (subscriber == owner) and `'external'`-kind (subscriber is
|
|
472
|
+
* an external identity scoped under this tenant) rows.
|
|
473
|
+
*/
|
|
474
|
+
async findByTenant(tenantId2) {
|
|
475
|
+
return this.list({
|
|
476
|
+
where: { tenantId: tenantId2 },
|
|
477
|
+
orderBy: "created_at DESC"
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Legacy single-tenant accessor: returns the current subscription where the
|
|
482
|
+
* tenant IS the subscriber (`subscriberKind = 'tenant'`). External-kind rows
|
|
483
|
+
* scoped under this tenant are intentionally excluded so existing callers
|
|
484
|
+
* are not surprised by buyer/agent subscriptions.
|
|
485
|
+
*/
|
|
486
|
+
async findCurrentForTenant(tenantId2, now = /* @__PURE__ */ new Date()) {
|
|
487
|
+
return this.findCurrentForSubscriber({ kind: "tenant", tenantId: tenantId2 }, now);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Polymorphic current-subscription accessor.
|
|
491
|
+
*
|
|
492
|
+
* For `'tenant'` subscribers we match rows with `subscriberKind = 'tenant'`
|
|
493
|
+
* under the same `tenantId`. For `'external'` subscribers we match by
|
|
494
|
+
* `(tenantId, subscriberExternalId)` with `subscriberKind = 'external'`.
|
|
495
|
+
*/
|
|
496
|
+
async findCurrentForSubscriber(subscriber, now = /* @__PURE__ */ new Date()) {
|
|
497
|
+
const where = {
|
|
498
|
+
tenantId: subscriber.tenantId,
|
|
499
|
+
subscriberKind: subscriber.kind
|
|
500
|
+
};
|
|
501
|
+
if (subscriber.kind === "external") {
|
|
502
|
+
where.subscriberExternalId = subscriber.externalId;
|
|
503
|
+
}
|
|
504
|
+
const subscriptions = await this.list({
|
|
505
|
+
where,
|
|
506
|
+
orderBy: "created_at DESC"
|
|
507
|
+
});
|
|
508
|
+
return subscriptions.find((subscription) => subscription.isEntitled(now)) ?? subscriptions[0] ?? null;
|
|
509
|
+
}
|
|
510
|
+
async findByStripeSubscriptionId(stripeSubscriptionId) {
|
|
511
|
+
const subscriptions = await this.list({
|
|
512
|
+
where: { stripeSubscriptionId },
|
|
513
|
+
limit: 1
|
|
514
|
+
});
|
|
515
|
+
return subscriptions[0] ?? null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
var __defProp = Object.defineProperty;
|
|
519
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
520
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
521
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
522
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
523
|
+
if (decorator = decorators[i])
|
|
524
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
525
|
+
if (kind && result) __defProp(target, key, result);
|
|
526
|
+
return result;
|
|
527
|
+
};
|
|
528
|
+
let TenantUsageMetric = class extends SmrtObject {
|
|
529
|
+
tenantId;
|
|
530
|
+
/**
|
|
531
|
+
* Discriminator for which subscriber identity recorded this usage.
|
|
532
|
+
* `'tenant'` (default) preserves the legacy shape; `'external'` indicates
|
|
533
|
+
* the subscriber is `subscriberExternalId` under the issuing `tenantId`.
|
|
534
|
+
*/
|
|
535
|
+
subscriberKind = "tenant";
|
|
536
|
+
/**
|
|
537
|
+
* Opaque caller-namespaced subscriber id when `subscriberKind === 'external'`.
|
|
538
|
+
* Empty string when kind is `'tenant'`.
|
|
539
|
+
*/
|
|
540
|
+
subscriberExternalId = "";
|
|
541
|
+
metricKey = "";
|
|
542
|
+
quantity = 0;
|
|
543
|
+
windowStart = /* @__PURE__ */ new Date();
|
|
544
|
+
windowEnd = /* @__PURE__ */ new Date();
|
|
545
|
+
source = "";
|
|
546
|
+
sourceId = "";
|
|
547
|
+
dimensions = "{}";
|
|
548
|
+
constructor(options = {}) {
|
|
549
|
+
super(options);
|
|
550
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
551
|
+
if (options.subscriberKind !== void 0)
|
|
552
|
+
this.subscriberKind = options.subscriberKind;
|
|
553
|
+
if (options.subscriberExternalId !== void 0)
|
|
554
|
+
this.subscriberExternalId = options.subscriberExternalId;
|
|
555
|
+
assertSubscriberInvariant("TenantUsageMetric", {
|
|
556
|
+
subscriberKind: this.subscriberKind,
|
|
557
|
+
subscriberExternalId: this.subscriberExternalId
|
|
558
|
+
});
|
|
559
|
+
if (options.metricKey !== void 0) this.metricKey = options.metricKey;
|
|
560
|
+
if (options.quantity !== void 0) this.quantity = options.quantity;
|
|
561
|
+
if (options.windowStart !== void 0)
|
|
562
|
+
this.windowStart = options.windowStart;
|
|
563
|
+
if (options.windowEnd !== void 0) this.windowEnd = options.windowEnd;
|
|
564
|
+
if (options.source !== void 0) this.source = options.source;
|
|
565
|
+
if (options.sourceId !== void 0) this.sourceId = options.sourceId;
|
|
566
|
+
if (options.dimensions !== void 0) this.dimensions = options.dimensions;
|
|
567
|
+
}
|
|
568
|
+
/** See `TenantSubscription.validateBeforeSave` for the rationale. */
|
|
569
|
+
async validateBeforeSave() {
|
|
570
|
+
await super.validateBeforeSave();
|
|
571
|
+
assertSubscriberInvariant("TenantUsageMetric", {
|
|
572
|
+
subscriberKind: this.subscriberKind,
|
|
573
|
+
subscriberExternalId: this.subscriberExternalId
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Project this row's polymorphic subscriber columns onto the
|
|
578
|
+
* {@link Subscriber} discriminated union.
|
|
579
|
+
*/
|
|
580
|
+
getSubscriber() {
|
|
581
|
+
if (!this.tenantId) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
if (this.subscriberKind === "external") {
|
|
585
|
+
if (!this.subscriberExternalId) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
kind: "external",
|
|
590
|
+
tenantId: this.tenantId,
|
|
591
|
+
externalId: this.subscriberExternalId
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return { kind: "tenant", tenantId: this.tenantId };
|
|
595
|
+
}
|
|
596
|
+
getDimensions() {
|
|
597
|
+
return parseJsonObject(this.dimensions, {});
|
|
598
|
+
}
|
|
599
|
+
setDimensions(dimensions) {
|
|
600
|
+
this.dimensions = stringifyJson(dimensions);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
__decorateClass([
|
|
604
|
+
tenantId()
|
|
605
|
+
], TenantUsageMetric.prototype, "tenantId", 2);
|
|
606
|
+
TenantUsageMetric = __decorateClass([
|
|
607
|
+
TenantScoped({ mode: "required" }),
|
|
608
|
+
smrt({
|
|
609
|
+
tableName: "_smrt_tenant_usage_metrics",
|
|
610
|
+
api: { include: ["list", "get", "create"] },
|
|
611
|
+
cli: true,
|
|
612
|
+
mcp: { include: ["list", "get"] }
|
|
613
|
+
})
|
|
614
|
+
], TenantUsageMetric);
|
|
615
|
+
class TenantUsageMetricCollection extends SmrtCollection {
|
|
616
|
+
static _itemClass = TenantUsageMetric;
|
|
617
|
+
async recordUsage(options) {
|
|
618
|
+
const subscriber = normalizeSubscriber({
|
|
619
|
+
tenantId: options.tenantId,
|
|
620
|
+
subscriberKind: options.subscriberKind,
|
|
621
|
+
subscriberExternalId: options.subscriberExternalId
|
|
622
|
+
});
|
|
623
|
+
const columns = subscriberToColumns(subscriber);
|
|
624
|
+
const metric = await this.create({
|
|
625
|
+
tenantId: columns.tenantId,
|
|
626
|
+
subscriberKind: columns.subscriberKind,
|
|
627
|
+
subscriberExternalId: columns.subscriberExternalId,
|
|
628
|
+
metricKey: options.metricKey,
|
|
629
|
+
quantity: options.quantity,
|
|
630
|
+
windowStart: options.windowStart,
|
|
631
|
+
windowEnd: options.windowEnd,
|
|
632
|
+
source: options.source ?? "",
|
|
633
|
+
sourceId: options.sourceId ?? "",
|
|
634
|
+
dimensions: stringifyJson(options.dimensions ?? {})
|
|
635
|
+
});
|
|
636
|
+
return metric;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Legacy accessor — finds metrics for `subscriberKind = 'tenant'` under the
|
|
640
|
+
* given tenant. External-kind rows scoped to the same tenant are excluded.
|
|
641
|
+
*/
|
|
642
|
+
async findByTenantAndMetric(tenantId2, metricKey) {
|
|
643
|
+
return this.list({
|
|
644
|
+
where: { tenantId: tenantId2, metricKey, subscriberKind: "tenant" },
|
|
645
|
+
orderBy: "windowStart DESC"
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
async summarizeUsage(options) {
|
|
649
|
+
const subscriber = normalizeSubscriber({
|
|
650
|
+
tenantId: options.tenantId,
|
|
651
|
+
subscriberKind: options.subscriberKind,
|
|
652
|
+
subscriberExternalId: options.subscriberExternalId
|
|
653
|
+
});
|
|
654
|
+
const columns = subscriberToColumns(subscriber);
|
|
655
|
+
const where = {
|
|
656
|
+
tenantId: columns.tenantId,
|
|
657
|
+
subscriberKind: columns.subscriberKind,
|
|
658
|
+
metricKey: options.metricKey,
|
|
659
|
+
"windowStart <": options.window.end.toISOString(),
|
|
660
|
+
"windowEnd >": options.window.start.toISOString()
|
|
661
|
+
};
|
|
662
|
+
if (subscriber.kind === "external") {
|
|
663
|
+
where.subscriberExternalId = columns.subscriberExternalId;
|
|
664
|
+
}
|
|
665
|
+
const metrics = await this.list({ where });
|
|
666
|
+
const baseSummary = {
|
|
667
|
+
tenantId: columns.tenantId,
|
|
668
|
+
metricKey: options.metricKey,
|
|
669
|
+
quantity: metrics.reduce((sum, metric) => sum + metric.quantity, 0),
|
|
670
|
+
windowStart: options.window.start,
|
|
671
|
+
windowEnd: options.window.end
|
|
672
|
+
};
|
|
673
|
+
if (subscriber.kind === "external") {
|
|
674
|
+
return {
|
|
675
|
+
...baseSummary,
|
|
676
|
+
subscriberKind: "external",
|
|
677
|
+
subscriberExternalId: subscriber.externalId
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return baseSummary;
|
|
681
|
+
}
|
|
682
|
+
async summarizeUsageBatch(options) {
|
|
683
|
+
const metricKeys = Array.from(new Set(options.metricKeys));
|
|
684
|
+
if (metricKeys.length === 0) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
const subscriber = normalizeSubscriber({
|
|
688
|
+
tenantId: options.tenantId,
|
|
689
|
+
subscriberKind: options.subscriberKind,
|
|
690
|
+
subscriberExternalId: options.subscriberExternalId
|
|
691
|
+
});
|
|
692
|
+
const columns = subscriberToColumns(subscriber);
|
|
693
|
+
const metricPlaceholders = metricKeys.map(() => "?").join(", ");
|
|
694
|
+
const params = [
|
|
695
|
+
columns.tenantId,
|
|
696
|
+
columns.subscriberKind,
|
|
697
|
+
...metricKeys,
|
|
698
|
+
options.window.end.toISOString(),
|
|
699
|
+
options.window.start.toISOString()
|
|
700
|
+
];
|
|
701
|
+
let subscriberSql = "";
|
|
702
|
+
if (subscriber.kind === "external") {
|
|
703
|
+
subscriberSql = "AND subscriber_external_id = ?";
|
|
704
|
+
params.splice(2, 0, columns.subscriberExternalId);
|
|
705
|
+
}
|
|
706
|
+
const result = await this.db.query(
|
|
707
|
+
`SELECT
|
|
708
|
+
metric_key,
|
|
709
|
+
COALESCE(SUM(quantity), 0) AS quantity
|
|
710
|
+
FROM _smrt_tenant_usage_metrics
|
|
711
|
+
WHERE tenant_id = ?
|
|
712
|
+
AND subscriber_kind = ?
|
|
713
|
+
${subscriberSql}
|
|
714
|
+
AND metric_key IN (${metricPlaceholders})
|
|
715
|
+
AND window_start < ?
|
|
716
|
+
AND window_end > ?
|
|
717
|
+
GROUP BY metric_key`,
|
|
718
|
+
...params
|
|
719
|
+
);
|
|
720
|
+
const quantityByMetric = /* @__PURE__ */ new Map();
|
|
721
|
+
for (const row of resultRows(result)) {
|
|
722
|
+
const metricKey = String(row.metric_key ?? row.metricKey ?? "");
|
|
723
|
+
if (metricKey) {
|
|
724
|
+
quantityByMetric.set(metricKey, numberFromRow(row, "quantity"));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return metricKeys.map((metricKey) => {
|
|
728
|
+
const baseSummary = {
|
|
729
|
+
tenantId: columns.tenantId,
|
|
730
|
+
metricKey,
|
|
731
|
+
quantity: quantityByMetric.get(metricKey) ?? 0,
|
|
732
|
+
windowStart: options.window.start,
|
|
733
|
+
windowEnd: options.window.end
|
|
734
|
+
};
|
|
735
|
+
if (subscriber.kind === "external") {
|
|
736
|
+
return {
|
|
737
|
+
...baseSummary,
|
|
738
|
+
subscriberKind: "external",
|
|
739
|
+
subscriberExternalId: subscriber.externalId
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return baseSummary;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Summarize persisted AI usage for a tenant within a window.
|
|
747
|
+
*
|
|
748
|
+
* Reads the framework `_smrt_ai_usage` system table via a raw aggregate
|
|
749
|
+
* query. This deliberately uses `this.db.query` rather than the inherited
|
|
750
|
+
* `query()`: those rows are framework usage records, not `TenantUsageMetric`
|
|
751
|
+
* instances, so model hydration would discard the aggregate columns. Tenant
|
|
752
|
+
* scoping is applied explicitly through the `tenant_id` filter.
|
|
753
|
+
*
|
|
754
|
+
* Named distinctly from the inherited `SmrtClass.summarizeAiUsage` (which
|
|
755
|
+
* returns grouped stats across all tenants) to avoid overriding it.
|
|
756
|
+
*/
|
|
757
|
+
async summarizeTenantAiUsage(options) {
|
|
758
|
+
const result = await this.db.query(
|
|
759
|
+
`SELECT
|
|
760
|
+
COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens,
|
|
761
|
+
COALESCE(SUM(completion_tokens), 0) AS completion_tokens,
|
|
762
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
763
|
+
COALESCE(SUM(estimated_cost), 0) AS estimated_cost,
|
|
764
|
+
COUNT(*) AS request_count
|
|
765
|
+
FROM _smrt_ai_usage
|
|
766
|
+
WHERE tenant_id = ?
|
|
767
|
+
AND created_at >= ?
|
|
768
|
+
AND created_at < ?`,
|
|
769
|
+
options.tenantId,
|
|
770
|
+
options.window.start.toISOString(),
|
|
771
|
+
options.window.end.toISOString()
|
|
772
|
+
);
|
|
773
|
+
const row = firstRow(result);
|
|
774
|
+
return {
|
|
775
|
+
tenantId: options.tenantId,
|
|
776
|
+
promptTokens: numberFromRow(row, "prompt_tokens"),
|
|
777
|
+
completionTokens: numberFromRow(row, "completion_tokens"),
|
|
778
|
+
totalTokens: numberFromRow(row, "total_tokens"),
|
|
779
|
+
estimatedCost: numberFromRow(row, "estimated_cost"),
|
|
780
|
+
requestCount: numberFromRow(row, "request_count"),
|
|
781
|
+
windowStart: options.window.start,
|
|
782
|
+
windowEnd: options.window.end
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function firstRow(result) {
|
|
787
|
+
const rows = resultRows(result);
|
|
788
|
+
return rows[0] ?? {};
|
|
789
|
+
}
|
|
790
|
+
function resultRows(result) {
|
|
791
|
+
return Array.isArray(result) ? result : result?.rows ?? [];
|
|
792
|
+
}
|
|
793
|
+
function numberFromRow(row, key) {
|
|
794
|
+
const value = row[key];
|
|
795
|
+
if (typeof value === "number") {
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
if (typeof value === "bigint") {
|
|
799
|
+
return Number(value);
|
|
800
|
+
}
|
|
801
|
+
if (typeof value === "string") {
|
|
802
|
+
return Number.parseFloat(value) || 0;
|
|
803
|
+
}
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
function evaluateThreshold(threshold, usage) {
|
|
807
|
+
const ratio = threshold.limit === 0 ? usage.quantity > 0 ? Infinity : 0 : usage.quantity / threshold.limit;
|
|
808
|
+
const warningRatio = threshold.warningRatio ?? 0.8;
|
|
809
|
+
const blocked = threshold.enforcement === "block" && (threshold.limit === 0 ? usage.quantity > 0 : usage.quantity >= threshold.limit);
|
|
810
|
+
const warned = !blocked && ratio >= warningRatio;
|
|
811
|
+
return {
|
|
812
|
+
threshold,
|
|
813
|
+
usage,
|
|
814
|
+
ratio,
|
|
815
|
+
state: blocked ? "blocked" : warned ? "warn" : "ok",
|
|
816
|
+
allowed: !blocked,
|
|
817
|
+
remaining: Math.max(0, threshold.limit - usage.quantity)
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function evaluateThresholds(thresholds, usage) {
|
|
821
|
+
const usageByMetric = new Map(
|
|
822
|
+
usage.map((summary) => [summary.metricKey, summary])
|
|
823
|
+
);
|
|
824
|
+
return thresholds.map((threshold) => {
|
|
825
|
+
const summary = usageByMetric.get(threshold.metricKey) ?? emptyUsageSummary$2(threshold.metricKey);
|
|
826
|
+
return evaluateThreshold(threshold, summary);
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
function emptyUsageSummary$2(metricKey) {
|
|
830
|
+
const now = /* @__PURE__ */ new Date();
|
|
831
|
+
return {
|
|
832
|
+
tenantId: "",
|
|
833
|
+
metricKey,
|
|
834
|
+
quantity: 0,
|
|
835
|
+
windowStart: now,
|
|
836
|
+
windowEnd: now
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
class TenantUsageMeter {
|
|
840
|
+
constructor(metrics, classOptions = {}) {
|
|
841
|
+
this.metrics = metrics;
|
|
842
|
+
this.classOptions = classOptions;
|
|
843
|
+
}
|
|
844
|
+
metrics;
|
|
845
|
+
classOptions;
|
|
846
|
+
static async create(classOptions = {}) {
|
|
847
|
+
const metrics = await TenantUsageMetricCollection.create(classOptions);
|
|
848
|
+
return new TenantUsageMeter(metrics, classOptions);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Record one usage row.
|
|
852
|
+
*
|
|
853
|
+
* Accepts the polymorphic subscriber fields directly on the options object
|
|
854
|
+
* (`subscriberKind`/`subscriberExternalId`); when omitted defaults to a
|
|
855
|
+
* `'tenant'`-kind row keyed off `tenantId`. The collection layer enforces
|
|
856
|
+
* the XOR invariant — passing `subscriberKind: 'external'` without a
|
|
857
|
+
* non-empty `subscriberExternalId` throws.
|
|
858
|
+
*/
|
|
859
|
+
async record(options) {
|
|
860
|
+
await this.metrics.recordUsage(options);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Summarize usage over a window for a given subscriber.
|
|
864
|
+
*
|
|
865
|
+
* The `ai.*` short-circuit only fires for `'tenant'`-kind subscribers since
|
|
866
|
+
* the `_smrt_ai_usage` system table is tenant-scoped; external subscribers
|
|
867
|
+
* fall through to the normal `_smrt_tenant_usage_metrics` aggregation.
|
|
868
|
+
*/
|
|
869
|
+
async summarize(options) {
|
|
870
|
+
normalizeSubscriber({
|
|
871
|
+
tenantId: options.tenantId,
|
|
872
|
+
subscriberKind: options.subscriberKind,
|
|
873
|
+
subscriberExternalId: options.subscriberExternalId
|
|
874
|
+
});
|
|
875
|
+
const aiSummary = await this.trySummarizeAiMetric(options);
|
|
876
|
+
if (aiSummary) {
|
|
877
|
+
return aiSummary;
|
|
878
|
+
}
|
|
879
|
+
return this.metrics.summarizeUsage(options);
|
|
880
|
+
}
|
|
881
|
+
async summarizeBatch(options) {
|
|
882
|
+
const subscriber = normalizeSubscriber({
|
|
883
|
+
tenantId: options.tenantId,
|
|
884
|
+
subscriberKind: options.subscriberKind,
|
|
885
|
+
subscriberExternalId: options.subscriberExternalId
|
|
886
|
+
});
|
|
887
|
+
const metricKeys = Array.from(new Set(options.metricKeys));
|
|
888
|
+
if (metricKeys.length === 0) {
|
|
889
|
+
return [];
|
|
890
|
+
}
|
|
891
|
+
const useTenantAiSummary = subscriber.kind === "tenant";
|
|
892
|
+
const aiMetricKeys = useTenantAiSummary ? metricKeys.filter((metricKey) => metricKey.startsWith("ai.")) : [];
|
|
893
|
+
const persistedMetricKeys = useTenantAiSummary ? metricKeys.filter((metricKey) => !metricKey.startsWith("ai.")) : metricKeys;
|
|
894
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
895
|
+
if (aiMetricKeys.length > 0) {
|
|
896
|
+
const aiSummary = await this.summarizeAiUsage({
|
|
897
|
+
tenantId: options.tenantId,
|
|
898
|
+
window: options.window
|
|
899
|
+
});
|
|
900
|
+
const quantityByMetric = aiQuantityByMetric(aiSummary);
|
|
901
|
+
for (const metricKey of aiMetricKeys) {
|
|
902
|
+
summaries.set(metricKey, {
|
|
903
|
+
tenantId: options.tenantId,
|
|
904
|
+
metricKey,
|
|
905
|
+
quantity: quantityByMetric[metricKey] ?? 0,
|
|
906
|
+
windowStart: options.window.start,
|
|
907
|
+
windowEnd: options.window.end
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const persistedSummaries = await this.metrics.summarizeUsageBatch({
|
|
912
|
+
...options,
|
|
913
|
+
metricKeys: persistedMetricKeys
|
|
914
|
+
});
|
|
915
|
+
for (const summary of persistedSummaries) {
|
|
916
|
+
summaries.set(summary.metricKey, summary);
|
|
917
|
+
}
|
|
918
|
+
return metricKeys.map(
|
|
919
|
+
(metricKey) => summaries.get(metricKey) ?? emptyUsageSummary$1(subscriber, metricKey, options.window)
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
async summarizeAiUsage(options) {
|
|
923
|
+
return this.metrics.summarizeTenantAiUsage(options);
|
|
924
|
+
}
|
|
925
|
+
getOptions() {
|
|
926
|
+
return this.classOptions;
|
|
927
|
+
}
|
|
928
|
+
async trySummarizeAiMetric(options) {
|
|
929
|
+
if (!options.metricKey.startsWith("ai.")) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const kind = options.subscriberKind ?? "tenant";
|
|
933
|
+
if (kind !== "tenant") {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
const summary = await this.summarizeAiUsage({
|
|
937
|
+
tenantId: options.tenantId,
|
|
938
|
+
window: options.window
|
|
939
|
+
});
|
|
940
|
+
const quantityByMetric = {
|
|
941
|
+
...aiQuantityByMetric(summary)
|
|
942
|
+
};
|
|
943
|
+
return {
|
|
944
|
+
tenantId: options.tenantId,
|
|
945
|
+
metricKey: options.metricKey,
|
|
946
|
+
quantity: quantityByMetric[options.metricKey] ?? 0,
|
|
947
|
+
windowStart: options.window.start,
|
|
948
|
+
windowEnd: options.window.end
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function aiQuantityByMetric(summary) {
|
|
953
|
+
return {
|
|
954
|
+
"ai.tokens.prompt": summary.promptTokens,
|
|
955
|
+
"ai.tokens.completion": summary.completionTokens,
|
|
956
|
+
"ai.tokens.total": summary.totalTokens,
|
|
957
|
+
"ai.cost.estimated": summary.estimatedCost,
|
|
958
|
+
"ai.requests": summary.requestCount
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function emptyUsageSummary$1(subscriber, metricKey, window) {
|
|
962
|
+
const summary = {
|
|
963
|
+
tenantId: subscriber.tenantId,
|
|
964
|
+
metricKey,
|
|
965
|
+
quantity: 0,
|
|
966
|
+
windowStart: window.start,
|
|
967
|
+
windowEnd: window.end
|
|
968
|
+
};
|
|
969
|
+
if (subscriber.kind === "external") {
|
|
970
|
+
return {
|
|
971
|
+
...summary,
|
|
972
|
+
subscriberKind: "external",
|
|
973
|
+
subscriberExternalId: subscriber.externalId
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
return summary;
|
|
977
|
+
}
|
|
978
|
+
class SubscriptionResolver {
|
|
979
|
+
constructor(readers) {
|
|
980
|
+
this.readers = readers;
|
|
981
|
+
}
|
|
982
|
+
readers;
|
|
983
|
+
/**
|
|
984
|
+
* Build the default resolver readers once for a request or app lifecycle and
|
|
985
|
+
* reuse the returned resolver across entitlement checks.
|
|
986
|
+
*/
|
|
987
|
+
static async create(classOptions = {}) {
|
|
988
|
+
const [plans, subscriptions, usage] = await Promise.all([
|
|
989
|
+
SubscriptionPlanCollection.create(classOptions),
|
|
990
|
+
TenantSubscriptionCollection.create(classOptions),
|
|
991
|
+
TenantUsageMeter.create(classOptions)
|
|
992
|
+
]);
|
|
993
|
+
return new SubscriptionResolver({ plans, subscriptions, usage });
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Load the subscription/plan pair used by entitlement resolution. Callers
|
|
997
|
+
* that need both the entitlement snapshot and the backing records can load
|
|
998
|
+
* this once, then pass it back via `options.context`.
|
|
999
|
+
*/
|
|
1000
|
+
async loadEntitlementContext(subscriberOrTenantId, options = {}) {
|
|
1001
|
+
const subscriber = toSubscriber(subscriberOrTenantId);
|
|
1002
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1003
|
+
const subscription = await this.resolveSubscription(
|
|
1004
|
+
subscriber,
|
|
1005
|
+
now,
|
|
1006
|
+
options.context
|
|
1007
|
+
);
|
|
1008
|
+
const plan = await this.resolvePlan(subscription, options.context);
|
|
1009
|
+
return { subscription, plan };
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Polymorphic entitlement resolution. Works for both `'tenant'`-kind and
|
|
1013
|
+
* `'external'`-kind subscribers and is the preferred surface — the
|
|
1014
|
+
* `resolveTenantEntitlements(tenantId)` method below is a thin wrapper.
|
|
1015
|
+
*/
|
|
1016
|
+
async resolveEntitlements(subscriber, options = {}) {
|
|
1017
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1018
|
+
const { subscription, plan } = await this.loadEntitlementContext(
|
|
1019
|
+
subscriber,
|
|
1020
|
+
{ ...options, now }
|
|
1021
|
+
);
|
|
1022
|
+
if (!subscription) {
|
|
1023
|
+
return emptyResolution(subscriber);
|
|
1024
|
+
}
|
|
1025
|
+
if (!plan || !plan.isActive() || !subscription.isEntitled(now)) {
|
|
1026
|
+
return {
|
|
1027
|
+
...emptyResolution(subscriber),
|
|
1028
|
+
planId: plan?.id ?? subscription.planId ?? null,
|
|
1029
|
+
planKey: plan?.planKey ?? null,
|
|
1030
|
+
subscriptionId: subscription.id ?? null,
|
|
1031
|
+
status: subscription.status
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const thresholds = plan.getThresholds().filter(isValidThreshold);
|
|
1035
|
+
const thresholdEvaluations = await this.resolveThresholdEvaluations(
|
|
1036
|
+
subscriber,
|
|
1037
|
+
thresholds,
|
|
1038
|
+
now,
|
|
1039
|
+
options
|
|
1040
|
+
);
|
|
1041
|
+
return {
|
|
1042
|
+
tenantId: subscriber.tenantId,
|
|
1043
|
+
subscriber,
|
|
1044
|
+
planId: plan.id ?? null,
|
|
1045
|
+
planKey: plan.planKey,
|
|
1046
|
+
subscriptionId: subscription.id ?? null,
|
|
1047
|
+
status: subscription.status,
|
|
1048
|
+
featureKeys: plan.getFeatureKeys(),
|
|
1049
|
+
thresholds,
|
|
1050
|
+
thresholdEvaluations,
|
|
1051
|
+
allowed: thresholdEvaluations.every((evaluation) => evaluation.allowed)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Legacy single-tenant wrapper around {@link resolveEntitlements}. Kept so
|
|
1056
|
+
* existing tenant-only callers don't need to update their call sites.
|
|
1057
|
+
*/
|
|
1058
|
+
async resolveTenantEntitlements(tenantId2, options = {}) {
|
|
1059
|
+
return this.resolveEntitlements({ kind: "tenant", tenantId: tenantId2 }, options);
|
|
1060
|
+
}
|
|
1061
|
+
async isFeatureEnabled(subscriberOrTenantId, featureKey, options = {}) {
|
|
1062
|
+
const resolution = await this.resolveEntitlements(
|
|
1063
|
+
toSubscriber(subscriberOrTenantId),
|
|
1064
|
+
options
|
|
1065
|
+
);
|
|
1066
|
+
return resolution.featureKeys.includes(featureKey);
|
|
1067
|
+
}
|
|
1068
|
+
async assertWithinThresholds(subscriberOrTenantId, options = {}) {
|
|
1069
|
+
const subscriber = toSubscriber(subscriberOrTenantId);
|
|
1070
|
+
const resolution = await this.resolveEntitlements(subscriber, options);
|
|
1071
|
+
const blocked = resolution.thresholdEvaluations.find(
|
|
1072
|
+
(evaluation) => !evaluation.allowed
|
|
1073
|
+
);
|
|
1074
|
+
if (blocked) {
|
|
1075
|
+
const subject = subscriber.kind === "external" ? `external:${subscriber.externalId}` : `Tenant ${subscriber.tenantId}`;
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`${subject} exceeded subscription threshold ${blocked.threshold.metricKey}`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async resolveSubscription(subscriber, now, context) {
|
|
1082
|
+
if (hasContextValue(context, "subscription")) {
|
|
1083
|
+
const subscription = context?.subscription ?? null;
|
|
1084
|
+
assertSubscriptionMatchesSubscriber(subscription, subscriber);
|
|
1085
|
+
return subscription;
|
|
1086
|
+
}
|
|
1087
|
+
return this.findCurrentSubscription(subscriber, now);
|
|
1088
|
+
}
|
|
1089
|
+
async resolvePlan(subscription, context) {
|
|
1090
|
+
if (!subscription?.planId) {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
if (hasContextValue(context, "plan")) {
|
|
1094
|
+
const plan = context?.plan ?? null;
|
|
1095
|
+
assertPlanMatchesSubscription(plan, subscription);
|
|
1096
|
+
return plan;
|
|
1097
|
+
}
|
|
1098
|
+
return this.readers.plans.get({ id: subscription.planId });
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Bridge the legacy and polymorphic reader contracts.
|
|
1102
|
+
*
|
|
1103
|
+
* Prefers `findCurrentForSubscriber` when the reader provides it (the
|
|
1104
|
+
* preferred shape). For `'tenant'` subscribers we fall back to the legacy
|
|
1105
|
+
* `findCurrentForTenant`. For `'external'` subscribers the reader MUST
|
|
1106
|
+
* implement `findCurrentForSubscriber` — otherwise we have no way to scope
|
|
1107
|
+
* the lookup and we throw rather than silently returning the tenant's
|
|
1108
|
+
* primary subscription.
|
|
1109
|
+
*/
|
|
1110
|
+
async findCurrentSubscription(subscriber, now) {
|
|
1111
|
+
if (this.readers.subscriptions.findCurrentForSubscriber) {
|
|
1112
|
+
return this.readers.subscriptions.findCurrentForSubscriber(
|
|
1113
|
+
subscriber,
|
|
1114
|
+
now
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if (subscriber.kind === "tenant") {
|
|
1118
|
+
return this.readers.subscriptions.findCurrentForTenant(
|
|
1119
|
+
subscriber.tenantId,
|
|
1120
|
+
now
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
"External-subscriber resolution requires a TenantSubscriptionReader that implements findCurrentForSubscriber()"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
async resolveThresholdEvaluations(subscriber, thresholds, now, options) {
|
|
1128
|
+
if (thresholds.length === 0) {
|
|
1129
|
+
return [];
|
|
1130
|
+
}
|
|
1131
|
+
if (!this.readers.usage.summarizeBatch) {
|
|
1132
|
+
const evaluations2 = [];
|
|
1133
|
+
for (const threshold of thresholds) {
|
|
1134
|
+
const window = options.usageWindows?.[threshold.window] ?? getWindowForThreshold(threshold.window, now);
|
|
1135
|
+
const usage = await this.readers.usage.summarize({
|
|
1136
|
+
tenantId: subscriber.tenantId,
|
|
1137
|
+
subscriberKind: subscriber.kind,
|
|
1138
|
+
subscriberExternalId: subscriber.kind === "external" ? subscriber.externalId : void 0,
|
|
1139
|
+
metricKey: threshold.metricKey,
|
|
1140
|
+
window
|
|
1141
|
+
});
|
|
1142
|
+
evaluations2.push(evaluateThreshold(threshold, usage));
|
|
1143
|
+
}
|
|
1144
|
+
return evaluations2;
|
|
1145
|
+
}
|
|
1146
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1147
|
+
thresholds.forEach((threshold, index) => {
|
|
1148
|
+
const window = options.usageWindows?.[threshold.window] ?? getWindowForThreshold(threshold.window, now);
|
|
1149
|
+
const key = getWindowKey(window);
|
|
1150
|
+
const group = groups.get(key) ?? { window, entries: [] };
|
|
1151
|
+
group.entries.push({ index, threshold });
|
|
1152
|
+
groups.set(key, group);
|
|
1153
|
+
});
|
|
1154
|
+
const evaluations = new Array(thresholds.length);
|
|
1155
|
+
await Promise.all(
|
|
1156
|
+
Array.from(groups.values()).map(async ({ window, entries }) => {
|
|
1157
|
+
const metricKeys = uniqueMetricKeys(
|
|
1158
|
+
entries.map((entry) => entry.threshold.metricKey)
|
|
1159
|
+
);
|
|
1160
|
+
const summaries = await this.readers.usage.summarizeBatch?.({
|
|
1161
|
+
tenantId: subscriber.tenantId,
|
|
1162
|
+
subscriberKind: subscriber.kind,
|
|
1163
|
+
subscriberExternalId: subscriber.kind === "external" ? subscriber.externalId : void 0,
|
|
1164
|
+
metricKeys,
|
|
1165
|
+
window
|
|
1166
|
+
});
|
|
1167
|
+
const summaryByMetric = new Map(
|
|
1168
|
+
(summaries ?? []).map((summary) => [summary.metricKey, summary])
|
|
1169
|
+
);
|
|
1170
|
+
for (const { index, threshold } of entries) {
|
|
1171
|
+
const usage = summaryByMetric.get(threshold.metricKey) ?? emptyUsageSummary(subscriber, threshold.metricKey, window);
|
|
1172
|
+
evaluations[index] = evaluateThreshold(threshold, usage);
|
|
1173
|
+
}
|
|
1174
|
+
})
|
|
1175
|
+
);
|
|
1176
|
+
return evaluations;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function toSubscriber(input) {
|
|
1180
|
+
if (typeof input === "string") {
|
|
1181
|
+
return { kind: "tenant", tenantId: input };
|
|
1182
|
+
}
|
|
1183
|
+
return input;
|
|
1184
|
+
}
|
|
1185
|
+
function emptyResolution(subscriber) {
|
|
1186
|
+
return {
|
|
1187
|
+
tenantId: subscriber.tenantId,
|
|
1188
|
+
subscriber,
|
|
1189
|
+
planId: null,
|
|
1190
|
+
planKey: null,
|
|
1191
|
+
subscriptionId: null,
|
|
1192
|
+
status: "none",
|
|
1193
|
+
featureKeys: [],
|
|
1194
|
+
thresholds: [],
|
|
1195
|
+
thresholdEvaluations: [],
|
|
1196
|
+
allowed: false
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function hasContextValue(context, key) {
|
|
1200
|
+
return context?.[key] !== void 0;
|
|
1201
|
+
}
|
|
1202
|
+
function assertSubscriptionMatchesSubscriber(subscription, subscriber) {
|
|
1203
|
+
if (!subscription) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const subscriptionSubscriber = subscription.getSubscriber();
|
|
1207
|
+
if (!subscriptionSubscriber || !sameSubscriber(subscriptionSubscriber, subscriber)) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
"Provided entitlement context subscription does not match requested subscriber"
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function assertPlanMatchesSubscription(plan, subscription) {
|
|
1214
|
+
if (!plan) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (!plan.id || plan.id !== subscription.planId) {
|
|
1218
|
+
throw new Error(
|
|
1219
|
+
"Provided entitlement context plan does not match subscription.planId"
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
function sameSubscriber(left, right) {
|
|
1224
|
+
if (left.kind !== right.kind || left.tenantId !== right.tenantId) {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
if (left.kind === "tenant") {
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
return right.kind === "external" && left.externalId === right.externalId;
|
|
1231
|
+
}
|
|
1232
|
+
function uniqueMetricKeys(metricKeys) {
|
|
1233
|
+
return Array.from(new Set(metricKeys));
|
|
1234
|
+
}
|
|
1235
|
+
function emptyUsageSummary(subscriber, metricKey, window) {
|
|
1236
|
+
const summary = {
|
|
1237
|
+
tenantId: subscriber.tenantId,
|
|
1238
|
+
metricKey,
|
|
1239
|
+
quantity: 0,
|
|
1240
|
+
windowStart: window.start,
|
|
1241
|
+
windowEnd: window.end
|
|
1242
|
+
};
|
|
1243
|
+
if (subscriber.kind === "external") {
|
|
1244
|
+
return {
|
|
1245
|
+
...summary,
|
|
1246
|
+
subscriberKind: "external",
|
|
1247
|
+
subscriberExternalId: subscriber.externalId
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
return summary;
|
|
1251
|
+
}
|
|
1252
|
+
export {
|
|
1253
|
+
SubscriptionPlan,
|
|
1254
|
+
SubscriptionPlanCollection,
|
|
1255
|
+
SubscriptionResolver,
|
|
1256
|
+
TenantSubscription,
|
|
1257
|
+
TenantSubscriptionCollection,
|
|
1258
|
+
TenantUsageMeter,
|
|
1259
|
+
TenantUsageMetric,
|
|
1260
|
+
TenantUsageMetricCollection,
|
|
1261
|
+
assertSubscriberInvariant,
|
|
1262
|
+
evaluateThreshold,
|
|
1263
|
+
evaluateThresholds,
|
|
1264
|
+
getWindowForThreshold,
|
|
1265
|
+
getWindowKey,
|
|
1266
|
+
isValidThreshold,
|
|
1267
|
+
normalizeFeatureGrants,
|
|
1268
|
+
normalizeSubscriber,
|
|
1269
|
+
subscriberToColumns
|
|
1270
|
+
};
|
|
1271
|
+
//# sourceMappingURL=index.js.map
|