tapimo 0.2.0 → 0.4.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/README.md +36 -8
- package/dist/ApiKey.d.ts +1 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +5 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +28 -1
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +34 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7532 -4850
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +11 -3
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +11 -2
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +14 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +63 -53
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +3 -0
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +151 -0
- package/dist/analytics/Analytics.d.ts.map +1 -0
- package/dist/analytics/Analytics.js +212 -0
- package/dist/analytics/Analytics.js.map +1 -0
- package/dist/analytics/tables/requestEvents.d.ts +215 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
- package/dist/analytics/tables/requestEvents.js +264 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -0
- package/dist/apps/data/App.d.ts +1049 -624
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +36 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +54 -0
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingRouteProviders.js +355 -0
- package/dist/apps/data/FundingRouteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -0
- package/dist/apps/data/index.d.ts.map +1 -1
- package/dist/apps/data/index.js +1 -0
- package/dist/apps/data/index.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +843 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +443 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +2 -8
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +29 -36
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +29 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transfers.d.ts +56 -56
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +45 -45
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +8 -8
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/docs/App.d.ts +5 -3
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +28 -96
- package/dist/apps/docs/App.js.map +1 -1
- package/dist/apps/docs/reference.d.ts +21 -0
- package/dist/apps/docs/reference.d.ts.map +1 -0
- package/dist/apps/docs/reference.js +112 -0
- package/dist/apps/docs/reference.js.map +1 -0
- package/dist/apps/index.d.ts +3 -2
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +3 -2
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/{platform → management}/App.d.ts +3015 -738
- package/dist/apps/management/App.d.ts.map +1 -0
- package/dist/apps/{platform → management}/App.js +20 -10
- package/dist/apps/management/App.js.map +1 -0
- package/dist/apps/management/Billing.d.ts +279 -0
- package/dist/apps/management/Billing.d.ts.map +1 -0
- package/dist/apps/management/Billing.js +477 -0
- package/dist/apps/management/Billing.js.map +1 -0
- package/dist/apps/management/index.d.ts +3 -0
- package/dist/apps/management/index.d.ts.map +1 -0
- package/dist/apps/management/index.js +3 -0
- package/dist/apps/management/index.js.map +1 -0
- package/dist/apps/{platform → management}/routes/api-keys.d.ts +341 -49
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/api-keys.js +116 -14
- package/dist/apps/management/routes/api-keys.js.map +1 -0
- package/dist/apps/management/routes/billing.d.ts +1520 -0
- package/dist/apps/management/routes/billing.d.ts.map +1 -0
- package/dist/apps/management/routes/billing.js +795 -0
- package/dist/apps/management/routes/billing.js.map +1 -0
- package/dist/apps/{platform → management}/routes/invitations.d.ts +202 -84
- package/dist/apps/management/routes/invitations.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/invitations.js +59 -1
- package/dist/apps/management/routes/invitations.js.map +1 -0
- package/dist/apps/{platform → management}/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/me.d.ts.map +1 -0
- package/dist/apps/management/routes/me.js.map +1 -0
- package/dist/apps/management/routes/members.d.ts.map +1 -0
- package/dist/apps/management/routes/members.js.map +1 -0
- package/dist/apps/{platform → management}/routes/orgs.d.ts +50 -36
- package/dist/apps/management/routes/orgs.d.ts.map +1 -0
- package/dist/apps/{platform → management}/routes/orgs.js +14 -3
- package/dist/apps/management/routes/orgs.js.map +1 -0
- package/dist/apps/{platform → management}/routes/projects.d.ts +97 -97
- package/dist/apps/{platform → management}/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js.map +1 -0
- package/dist/apps/management/routes/scopes.d.ts +107 -0
- package/dist/apps/management/routes/scopes.d.ts.map +1 -0
- package/dist/apps/management/routes/scopes.js +66 -0
- package/dist/apps/management/routes/scopes.js.map +1 -0
- package/dist/apps/management/routes/usage.d.ts +588 -0
- package/dist/apps/management/routes/usage.d.ts.map +1 -0
- package/dist/apps/management/routes/usage.js +391 -0
- package/dist/apps/management/routes/usage.js.map +1 -0
- package/dist/apps/mcp.d.ts +76 -0
- package/dist/apps/mcp.d.ts.map +1 -0
- package/dist/apps/mcp.js +188 -0
- package/dist/apps/mcp.js.map +1 -0
- package/dist/apps/relay/App.d.ts +4 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +118 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +2 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/cli/Cli.d.ts +8 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +11 -3
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +4 -0
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +80 -1
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +7 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +126 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +79 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +88 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/billingSettings.d.ts +55 -0
- package/dist/db/tables/billingSettings.d.ts.map +1 -0
- package/dist/db/tables/billingSettings.js +70 -0
- package/dist/db/tables/billingSettings.js.map +1 -0
- package/dist/db/tables/invitations.js +2 -2
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +10 -4
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.js +2 -2
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/requestUsage.d.ts +121 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -0
- package/dist/db/tables/requestUsage.js +150 -0
- package/dist/db/tables/requestUsage.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +155 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/stripeCustomers.d.ts +81 -0
- package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
- package/dist/db/tables/stripeCustomers.js +120 -0
- package/dist/db/tables/stripeCustomers.js.map +1 -0
- package/dist/db/tables/users.js +2 -2
- package/dist/db/tables/users.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +5 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +19 -5
- package/dist/handlers/internal/sponsorship.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +10 -3
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +14 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +18 -4
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +149 -77
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +4 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +10 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +39 -8
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Fees.d.ts +27 -0
- package/dist/internal/Fees.d.ts.map +1 -0
- package/dist/internal/Fees.js +48 -0
- package/dist/internal/Fees.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +915 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +625 -0
- package/dist/internal/FundingRoutes.js.map +1 -0
- package/dist/internal/Id.d.ts +15 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +22 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/Log.d.ts +2 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +3 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -1
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +25 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +51 -0
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +8 -0
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +7 -0
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +8 -7
- package/src/ApiKey.ts +9 -2
- package/src/ApiKeys.test.ts +51 -0
- package/src/ApiKeys.ts +50 -1
- package/src/App.test.ts +33 -86718
- package/src/App.ts +26 -8
- package/src/Client.test-d.ts +353 -2
- package/src/Scope.ts +16 -3
- package/src/admin/apps/scopes.test.ts +6 -0
- package/src/admin/apps/scopes.ts +3 -0
- package/src/admin/ui/src/components/Page.tsx +2 -3
- package/src/admin/ui/src/routes/__root.tsx +2 -2
- package/src/admin/ui/src/routes/api-keys.tsx +2 -2
- package/src/admin/ui.generated.ts +13 -13
- package/src/analytics/Analytics.test.ts +527 -0
- package/src/analytics/Analytics.ts +318 -0
- package/src/analytics/tables/requestEvents.ts +523 -0
- package/src/apps/composition.test.ts +26 -13
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +37 -0
- package/src/apps/data/FundingRouteProviders.test.ts +281 -0
- package/src/apps/data/FundingRouteProviders.ts +459 -0
- package/src/apps/data/index.ts +1 -0
- package/src/apps/data/routes/funding.test.ts +314 -0
- package/src/apps/data/routes/funding.ts +698 -0
- package/src/apps/data/routes/receipts.ts +2 -9
- package/src/apps/data/routes/rpc.test.ts +78 -2
- package/src/apps/data/routes/rpc.ts +31 -1
- package/src/apps/data/routes/webhooks.test.ts +1 -1
- package/src/apps/data/routes/webhooks.ts +11 -8
- package/src/apps/docs/App.ts +30 -101
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +3 -2
- package/src/apps/{platform → management}/App.ts +56 -17
- package/src/apps/management/Billing.test.ts +530 -0
- package/src/apps/management/Billing.ts +682 -0
- package/src/apps/management/index.ts +2 -0
- package/src/apps/{platform → management}/routes/api-keys.test.ts +172 -9
- package/src/apps/{platform → management}/routes/api-keys.ts +137 -21
- package/src/apps/management/routes/billing.test.ts +1100 -0
- package/src/apps/management/routes/billing.ts +966 -0
- package/src/apps/{platform → management}/routes/invitations.test.ts +55 -7
- package/src/apps/{platform → management}/routes/invitations.ts +69 -1
- package/src/apps/management/routes/me.test.ts +351 -0
- package/src/apps/{platform → management}/routes/orgs.ts +26 -4
- package/src/apps/management/routes/scopes.test.ts +29 -0
- package/src/apps/management/routes/scopes.ts +85 -0
- package/src/apps/management/routes/usage.test.ts +422 -0
- package/src/apps/management/routes/usage.ts +527 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/relay/App.test.ts +457 -8
- package/src/apps/relay/App.ts +120 -12
- package/src/apps/relay/Sponsorships.test.ts +1 -1
- package/src/apps/relay/Sponsorships.ts +2 -1
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +17 -2
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +89 -1
- package/src/db/Db.ts +137 -0
- package/src/db/Schema.ts +104 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +2 -2
- package/src/db/tables/organizations.test.ts +1 -1
- package/src/db/tables/organizations.ts +15 -5
- package/src/db/tables/projects.ts +2 -2
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +108 -1
- package/src/db/tables/sponsoredTransactions.ts +230 -2
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +2 -2
- package/src/handlers/internal/sponsorship.ts +34 -7
- package/src/handlers/internal/utils.ts +19 -7
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +175 -25
- package/src/internal/Auth.ts +176 -87
- package/src/internal/Cache.ts +5 -1
- package/src/internal/EdgeCache.test.ts +27 -4
- package/src/internal/EdgeCache.ts +47 -9
- package/src/internal/Fees.test.ts +32 -0
- package/src/internal/Fees.ts +51 -0
- package/src/internal/FundingRoutes.test.ts +213 -0
- package/src/internal/FundingRoutes.ts +898 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +1 -0
- package/src/internal/Log.ts +5 -0
- package/src/internal/OpenApi.ts +12 -1
- package/src/internal/RateLimit.test.ts +84 -0
- package/src/internal/RateLimit.ts +70 -0
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.ts +8 -0
- package/src/internal/Webhooks.test.ts +3 -3
- package/src/internal/Webhooks.ts +3 -3
- package/dist/apps/platform/App.d.ts.map +0 -1
- package/dist/apps/platform/App.js.map +0 -1
- package/dist/apps/platform/index.d.ts +0 -2
- package/dist/apps/platform/index.d.ts.map +0 -1
- package/dist/apps/platform/index.js +0 -2
- package/dist/apps/platform/index.js.map +0 -1
- package/dist/apps/platform/routes/api-keys.d.ts.map +0 -1
- package/dist/apps/platform/routes/api-keys.js.map +0 -1
- package/dist/apps/platform/routes/invitations.d.ts.map +0 -1
- package/dist/apps/platform/routes/invitations.js.map +0 -1
- package/dist/apps/platform/routes/me.d.ts.map +0 -1
- package/dist/apps/platform/routes/me.js.map +0 -1
- package/dist/apps/platform/routes/members.d.ts.map +0 -1
- package/dist/apps/platform/routes/members.js.map +0 -1
- package/dist/apps/platform/routes/orgs.d.ts.map +0 -1
- package/dist/apps/platform/routes/orgs.js.map +0 -1
- package/dist/apps/platform/routes/projects.js.map +0 -1
- package/src/apps/platform/index.ts +0 -1
- package/src/apps/platform/routes/me.test.ts +0 -121
- /package/dist/apps/{platform → management}/routes/me.js +0 -0
- /package/dist/apps/{platform → management}/routes/members.d.ts +0 -0
- /package/dist/apps/{platform → management}/routes/members.js +0 -0
- /package/dist/apps/{platform → management}/routes/projects.js +0 -0
- /package/src/apps/{platform → management}/routes/me.ts +0 -0
- /package/src/apps/{platform → management}/routes/members.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/members.ts +0 -0
- /package/src/apps/{platform → management}/routes/orgs.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/projects.test.ts +0 -0
- /package/src/apps/{platform → management}/routes/projects.ts +0 -0
package/src/db/Db.ts
CHANGED
|
@@ -10,11 +10,14 @@ import {
|
|
|
10
10
|
} from 'kysely'
|
|
11
11
|
import * as pg from 'pg'
|
|
12
12
|
|
|
13
|
+
import type * as BillingSettings from './tables/billingSettings.js'
|
|
13
14
|
import type * as Invitations from './tables/invitations.js'
|
|
14
15
|
import type * as Memberships from './tables/memberships.js'
|
|
15
16
|
import type * as Organizations from './tables/organizations.js'
|
|
16
17
|
import type * as Projects from './tables/projects.js'
|
|
18
|
+
import type * as RequestUsage from './tables/requestUsage.js'
|
|
17
19
|
import type * as SponsoredTransactions from './tables/sponsoredTransactions.js'
|
|
20
|
+
import type * as StripeCustomers from './tables/stripeCustomers.js'
|
|
18
21
|
import type * as Users from './tables/users.js'
|
|
19
22
|
import type * as VerifiedTokens from './tables/verifiedTokens.js'
|
|
20
23
|
import type * as WebhookDeliveries from './tables/webhookDeliveries.js'
|
|
@@ -32,11 +35,15 @@ export type Database = {
|
|
|
32
35
|
/** SQL as applied; null on rows written before the column existed. */
|
|
33
36
|
sql: string | null
|
|
34
37
|
}
|
|
38
|
+
billing_settings: BillingSettings.Table
|
|
35
39
|
invitations: Invitations.Table
|
|
36
40
|
memberships: Memberships.Table
|
|
37
41
|
organizations: Organizations.Table
|
|
38
42
|
projects: Projects.Table
|
|
43
|
+
request_usage_buckets: RequestUsage.BucketTable
|
|
44
|
+
request_usage_meter_events: RequestUsage.MeterEventTable
|
|
39
45
|
sponsored_transactions: SponsoredTransactions.Table
|
|
46
|
+
stripe_customers: StripeCustomers.Table
|
|
40
47
|
users: Users.Table
|
|
41
48
|
verified_token_lists: VerifiedTokens.ListTable
|
|
42
49
|
verified_tokens: VerifiedTokens.Table
|
|
@@ -383,6 +390,136 @@ export const migrations: readonly Migration[] = [
|
|
|
383
390
|
up: (db) =>
|
|
384
391
|
db.schema.createIndex('sponsored_transactions_org_idx').ifNotExists().on('sponsoredTransactions').columns(['orgId', 'createdAt']), // prettier-ignore
|
|
385
392
|
},
|
|
393
|
+
{
|
|
394
|
+
name: '0027_stripe_customers',
|
|
395
|
+
up: (db) =>
|
|
396
|
+
db.schema
|
|
397
|
+
.createTable('stripeCustomers')
|
|
398
|
+
.ifNotExists()
|
|
399
|
+
.addColumn('orgId', 'text', (c) => c.primaryKey())
|
|
400
|
+
.addColumn('stripeCustomerId', 'text', (c) => c.notNull())
|
|
401
|
+
.addColumn('status', 'text', (c) => c.notNull())
|
|
402
|
+
.addColumn('createdAt', 'text', (c) => c.notNull())
|
|
403
|
+
.addColumn('updatedAt', 'text', (c) => c.notNull())
|
|
404
|
+
.addUniqueConstraint('stripe_customers_stripe_customer_id_key', ['stripeCustomerId']),
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: '0028_billing_settings',
|
|
408
|
+
up: (db) =>
|
|
409
|
+
db.schema
|
|
410
|
+
.createTable('billingSettings')
|
|
411
|
+
.ifNotExists()
|
|
412
|
+
.addColumn('orgId', 'text', (c) => c.primaryKey())
|
|
413
|
+
.addColumn('spendLimit', 'text')
|
|
414
|
+
.addColumn('txFeeLimit', 'text')
|
|
415
|
+
.addColumn('currency', 'text', (c) => c.notNull().defaultTo('usd'))
|
|
416
|
+
.addColumn('period', 'text', (c) => c.notNull().defaultTo('month'))
|
|
417
|
+
.addColumn('createdAt', 'text', (c) => c.notNull())
|
|
418
|
+
.addColumn('updatedAt', 'text', (c) => c.notNull()),
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: '0029_sponsored_transactions_fee_max',
|
|
422
|
+
up: (db) => raw(db, sql`ALTER TABLE sponsored_transactions ADD COLUMN IF NOT EXISTS fee_max text`), // prettier-ignore
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: '0030_sponsored_transactions_meter_reported_at',
|
|
426
|
+
up: (db) => raw(db, sql`ALTER TABLE sponsored_transactions ADD COLUMN IF NOT EXISTS meter_reported_at text`), // prettier-ignore
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: '0031_sponsored_transactions_billing_index',
|
|
430
|
+
// Partial: the spend-limit aggregate only ever scans billable rows.
|
|
431
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS sponsored_transactions_billing_idx ON sponsored_transactions (org_id, status, created_at) WHERE billable`), // prettier-ignore
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: '0032_sponsored_transactions_fee_amount_rescale',
|
|
435
|
+
// Rows finalized before fee scaling shipped stored attodollars (1e12× the
|
|
436
|
+
// token unit); `div` is exact ceiling division (`Fees.fromGas`). The
|
|
437
|
+
// threshold keeps re-runs idempotent: real scaled fees sit far below
|
|
438
|
+
// 1e9 ($1,000); attodollar values far above.
|
|
439
|
+
up: (db) => raw(db, sql`UPDATE sponsored_transactions SET fee_amount = div(fee_amount::numeric + 999999999999, 1000000000000)::text WHERE status = 'finalized' AND fee_amount IS NOT NULL AND fee_amount::numeric >= 1000000000`), // prettier-ignore
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: '0033_sponsored_transactions_unreported_index',
|
|
443
|
+
// Partial queue index: the per-minute reporter scans only unreported
|
|
444
|
+
// finalized billable rows, which marking immediately evicts.
|
|
445
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS sponsored_transactions_unreported_idx ON sponsored_transactions (finalized_at, id) WHERE billable AND status = 'finalized' AND meter_reported_at IS NULL`), // prettier-ignore
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: '0034_sponsored_transactions_currency',
|
|
449
|
+
up: (db) => raw(db, sql`ALTER TABLE sponsored_transactions ADD COLUMN IF NOT EXISTS currency text`), // prettier-ignore
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: '0035_sponsored_transactions_currency_backfill',
|
|
453
|
+
// Every fee payer to date sponsors in pathUSD, so rows predating the
|
|
454
|
+
// currency snapshot are USD. The predicate keeps re-runs idempotent.
|
|
455
|
+
up: (db) => raw(db, sql`UPDATE sponsored_transactions SET currency = 'usd' WHERE currency IS NULL`), // prettier-ignore
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: '0036_stripe_customers_environment',
|
|
459
|
+
up: (db) => raw(db, sql`ALTER TABLE stripe_customers ADD COLUMN IF NOT EXISTS environment text NOT NULL DEFAULT 'production'`), // prettier-ignore
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: '0037_stripe_customers_org_environment_key',
|
|
463
|
+
// Enables a second (sandbox) row per org; targeted by the upsert conflict
|
|
464
|
+
// arbiter so it survives the org pkey drop in the follow-up release.
|
|
465
|
+
up: (db) => raw(db, sql`CREATE UNIQUE INDEX IF NOT EXISTS stripe_customers_org_environment_key ON stripe_customers (org_id, environment)`), // prettier-ignore
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: '0038_billing_settings_environment',
|
|
469
|
+
up: (db) => raw(db, sql`ALTER TABLE billing_settings ADD COLUMN IF NOT EXISTS environment text NOT NULL DEFAULT 'production'`), // prettier-ignore
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: '0039_billing_settings_org_environment_key',
|
|
473
|
+
up: (db) => raw(db, sql`CREATE UNIQUE INDEX IF NOT EXISTS billing_settings_org_environment_key ON billing_settings (org_id, environment)`), // prettier-ignore
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: '0040_stripe_customers_drop_org_pkey',
|
|
477
|
+
// Release B: the `(org_id, environment)` unique index now guards uniqueness,
|
|
478
|
+
// so dropping the org pkey unblocks a second (sandbox) row per org.
|
|
479
|
+
up: (db) => raw(db, sql`ALTER TABLE stripe_customers DROP CONSTRAINT IF EXISTS stripe_customers_pkey`), // prettier-ignore
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: '0041_billing_settings_drop_org_pkey',
|
|
483
|
+
up: (db) => raw(db, sql`ALTER TABLE billing_settings DROP CONSTRAINT IF EXISTS billing_settings_pkey`), // prettier-ignore
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: '0042_request_usage_buckets',
|
|
487
|
+
up: (db) =>
|
|
488
|
+
db.schema
|
|
489
|
+
.createTable('requestUsageBuckets')
|
|
490
|
+
.ifNotExists()
|
|
491
|
+
.addColumn('orgId', 'text', (c) => c.notNull())
|
|
492
|
+
.addColumn('environment', 'text', (c) => c.notNull())
|
|
493
|
+
.addColumn('bucketStart', 'text', (c) => c.notNull())
|
|
494
|
+
.addColumn('reportedCount', 'bigint', (c) => c.notNull().defaultTo(0))
|
|
495
|
+
.addColumn('updatedAt', 'text', (c) => c.notNull())
|
|
496
|
+
.addPrimaryKeyConstraint('request_usage_buckets_pkey', ['orgId', 'environment', 'bucketStart']), // prettier-ignore
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: '0043_request_usage_meter_events',
|
|
500
|
+
up: (db) =>
|
|
501
|
+
db.schema
|
|
502
|
+
.createTable('requestUsageMeterEvents')
|
|
503
|
+
.ifNotExists()
|
|
504
|
+
.addColumn('identifier', 'text', (c) => c.primaryKey())
|
|
505
|
+
.addColumn('orgId', 'text', (c) => c.notNull())
|
|
506
|
+
.addColumn('environment', 'text', (c) => c.notNull())
|
|
507
|
+
.addColumn('bucketStart', 'text', (c) => c.notNull())
|
|
508
|
+
.addColumn('sequence', 'integer', (c) => c.notNull())
|
|
509
|
+
.addColumn('deltaCount', 'bigint', (c) => c.notNull())
|
|
510
|
+
.addColumn('stripeCustomerId', 'text', (c) => c.notNull())
|
|
511
|
+
.addColumn('state', 'text', (c) => c.notNull())
|
|
512
|
+
.addColumn('reason', 'text')
|
|
513
|
+
.addColumn('error', 'text')
|
|
514
|
+
.addColumn('firstAttemptedAt', 'text', (c) => c.notNull())
|
|
515
|
+
.addColumn('reportedAt', 'text')
|
|
516
|
+
.addUniqueConstraint('request_usage_meter_events_bucket_sequence_key', ['orgId', 'environment', 'bucketStart', 'sequence']), // prettier-ignore
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: '0044_request_usage_meter_events_pending_index',
|
|
520
|
+
// Partial: the reporter scans only unreported (pending) rows.
|
|
521
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS request_usage_meter_events_pending_idx ON request_usage_meter_events (first_attempted_at, identifier) WHERE state = 'pending'`), // prettier-ignore
|
|
522
|
+
},
|
|
386
523
|
]
|
|
387
524
|
|
|
388
525
|
/**
|
package/src/db/Schema.ts
CHANGED
|
@@ -8,6 +8,31 @@ import type * as Webhooks from '../internal/Webhooks.js'
|
|
|
8
8
|
// Domain-owned unions and jsonb values stay typed by their owning module via
|
|
9
9
|
// `z.custom` (type-only imports) so the two cannot drift.
|
|
10
10
|
|
|
11
|
+
/** An organization's billing configuration; source-agnostic (limits apply to any billing source). */
|
|
12
|
+
export const BillingSettings = z.object({
|
|
13
|
+
createdAt: z.string().check(z.describe('When the settings row was created (ISO 8601).')),
|
|
14
|
+
currency: z
|
|
15
|
+
.enum(['usd'])
|
|
16
|
+
.check(z.describe('ISO 4217 currency (lowercase) both limits are denominated in.')),
|
|
17
|
+
environment: z
|
|
18
|
+
.enum(['production', 'sandbox'])
|
|
19
|
+
.check(z.describe('Environment these settings apply to; `production` or `sandbox`.')),
|
|
20
|
+
orgId: z.string().check(z.describe('Organization id (`org_…`) these settings belong to.')),
|
|
21
|
+
period: z
|
|
22
|
+
.enum(['month'])
|
|
23
|
+
.check(z.describe('Window the spend limit applies over; `month` is a UTC calendar month.')),
|
|
24
|
+
spendLimit: z
|
|
25
|
+
.nullable(z.string())
|
|
26
|
+
.check(z.describe('Spend limit per period as a decimal string in `currency` units, or null for no limit.')), // prettier-ignore
|
|
27
|
+
txFeeLimit: z
|
|
28
|
+
.nullable(z.string())
|
|
29
|
+
.check(z.describe('Per-transaction fee cap as a decimal string in `currency` units, or null for the platform default.')), // prettier-ignore
|
|
30
|
+
updatedAt: z.string().check(z.describe('When the settings last changed (ISO 8601).')),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/** Columns of the `billing_settings` table. */
|
|
34
|
+
export type BillingSettings = z.output<typeof BillingSettings>
|
|
35
|
+
|
|
11
36
|
/** A pending, expiring offer of organization membership. */
|
|
12
37
|
export const Invitation = z.object({
|
|
13
38
|
acceptedAt: z
|
|
@@ -104,12 +129,18 @@ export const SponsoredTransaction = z.object({
|
|
|
104
129
|
.check(z.describe('Whether the sponsorship accrues billable spend; false for sandbox.')),
|
|
105
130
|
chainId: z.number().check(z.describe('Chain the sponsored transaction targets.')),
|
|
106
131
|
createdAt: z.string().check(z.describe('When the sponsorship was recorded (ISO 8601).')),
|
|
132
|
+
currency: z
|
|
133
|
+
.nullable(z.string())
|
|
134
|
+
.check(z.describe('Lowercase fee currency the sponsorship settles in (`usd`), or null when the fee token is unverified.')), // prettier-ignore
|
|
107
135
|
environment: z
|
|
108
136
|
.enum(['production', 'sandbox'])
|
|
109
137
|
.check(z.describe('Key environment the sponsorship was requested under.')),
|
|
110
138
|
feeAmount: z
|
|
111
139
|
.nullable(z.string())
|
|
112
140
|
.check(z.describe('Actual fee paid in fee-token base units, or null until finalized.')),
|
|
141
|
+
feeMax: z
|
|
142
|
+
.nullable(z.string())
|
|
143
|
+
.check(z.describe('Signed fee cap (`gas × maxFeePerGas`) in base units; pending rows count it against spend limits.')), // prettier-ignore
|
|
113
144
|
feeToken: z
|
|
114
145
|
.nullable(z.string())
|
|
115
146
|
.check(z.describe('Fee token address, or null when the chain default applied.')),
|
|
@@ -122,6 +153,9 @@ export const SponsoredTransaction = z.object({
|
|
|
122
153
|
z.describe('Opaque sponsored-transaction id (`stx_…`).'),
|
|
123
154
|
z.meta({ examples: ['stx_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
124
155
|
),
|
|
156
|
+
meterReportedAt: z
|
|
157
|
+
.nullable(z.string())
|
|
158
|
+
.check(z.describe('When the row was reported to the billing meter (ISO 8601), or null; exactly-once marker.')), // prettier-ignore
|
|
125
159
|
orgId: z.string().check(z.describe('Organization id (`org_…`) the spend attributes to.')),
|
|
126
160
|
projectId: z.string().check(z.describe('Project id (`prj_…`) the spend attributes to.')),
|
|
127
161
|
signPayload: z
|
|
@@ -141,6 +175,74 @@ export const SponsoredTransaction = z.object({
|
|
|
141
175
|
/** Columns of the `sponsored_transactions` table. */
|
|
142
176
|
export type SponsoredTransaction = z.output<typeof SponsoredTransaction>
|
|
143
177
|
|
|
178
|
+
/** A per-hour request-count watermark: the running total already reported to the meter. */
|
|
179
|
+
export const RequestUsageBucket = z.object({
|
|
180
|
+
bucketStart: z.string().check(z.describe('UTC hour start this bucket covers (ISO 8601).')),
|
|
181
|
+
environment: z
|
|
182
|
+
.enum(['production', 'sandbox'])
|
|
183
|
+
.check(z.describe('Environment the requests were served under; `production` or `sandbox`.')),
|
|
184
|
+
orgId: z.string().check(z.describe('Organization id (`org_…`) the requests attribute to.')),
|
|
185
|
+
reportedCount: z
|
|
186
|
+
.number()
|
|
187
|
+
.check(z.describe('Cumulative request count already reported to the meter for this bucket.')),
|
|
188
|
+
updatedAt: z.string().check(z.describe('When the watermark last advanced (ISO 8601).')),
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
/** Columns of the `request_usage_buckets` table. */
|
|
192
|
+
export type RequestUsageBucket = z.output<typeof RequestUsageBucket>
|
|
193
|
+
|
|
194
|
+
/** One request-count meter event: a frozen additive delta with its exactly-once state. */
|
|
195
|
+
export const RequestUsageMeterEvent = z.object({
|
|
196
|
+
bucketStart: z.string().check(z.describe('UTC hour start this event bills for (ISO 8601).')),
|
|
197
|
+
deltaCount: z
|
|
198
|
+
.number()
|
|
199
|
+
.check(z.describe('Frozen request count this event bills; a positive additive delta.')),
|
|
200
|
+
environment: z
|
|
201
|
+
.enum(['production', 'sandbox'])
|
|
202
|
+
.check(z.describe('Environment the requests were served under; `production` or `sandbox`.')),
|
|
203
|
+
error: z.nullable(z.string()).check(z.describe('Last failure detail while `pending`, or null.')),
|
|
204
|
+
firstAttemptedAt: z.string().check(z.describe('When this event was first claimed (ISO 8601).')),
|
|
205
|
+
identifier: z
|
|
206
|
+
.string()
|
|
207
|
+
.check(z.describe('Deterministic id; also the Stripe meter-event identifier and idempotency key.')), // prettier-ignore
|
|
208
|
+
orgId: z.string().check(z.describe('Organization id (`org_…`) the requests attribute to.')),
|
|
209
|
+
reason: z
|
|
210
|
+
.nullable(z.string())
|
|
211
|
+
.check(z.describe('Skip reason when `state` is `skipped` (`no_customer` | `customer_canceled`), or null.')), // prettier-ignore
|
|
212
|
+
reportedAt: z
|
|
213
|
+
.nullable(z.string())
|
|
214
|
+
.check(z.describe('When Stripe acknowledged the event (ISO 8601), or null.')),
|
|
215
|
+
sequence: z
|
|
216
|
+
.number()
|
|
217
|
+
.check(z.describe('Delta sequence within the bucket; `0` is the initial seal, `1..n` later deltas.')), // prettier-ignore
|
|
218
|
+
state: z
|
|
219
|
+
.enum(['pending', 'reported', 'skipped'])
|
|
220
|
+
.check(z.describe('Exactly-once state: `pending` retries, `reported` is done, `skipped` is audited.')), // prettier-ignore
|
|
221
|
+
stripeCustomerId: z.string().check(z.describe('Stripe customer id the event bills against.')),
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
/** Columns of the `request_usage_meter_events` table. */
|
|
225
|
+
export type RequestUsageMeterEvent = z.output<typeof RequestUsageMeterEvent>
|
|
226
|
+
|
|
227
|
+
/** An organization's Stripe billing source: the customer link and derived status. */
|
|
228
|
+
export const StripeCustomer = z.object({
|
|
229
|
+
createdAt: z.string().check(z.describe('When the Stripe source was connected (ISO 8601).')),
|
|
230
|
+
environment: z
|
|
231
|
+
.enum(['production', 'sandbox'])
|
|
232
|
+
.check(z.describe('Environment this Stripe source backs; `production` (live) or `sandbox` (test).')), // prettier-ignore
|
|
233
|
+
orgId: z.string().check(z.describe('Organization id (`org_…`) this Stripe source belongs to.')),
|
|
234
|
+
status: z
|
|
235
|
+
.enum(['active', 'canceled', 'none', 'past_due'])
|
|
236
|
+
.check(z.describe('Billing status derived from Stripe state; gates production sponsorship.')),
|
|
237
|
+
stripeCustomerId: z
|
|
238
|
+
.string()
|
|
239
|
+
.check(z.describe('Stripe customer id backing this source; never deleted by us.')),
|
|
240
|
+
updatedAt: z.string().check(z.describe('When the billing status last changed (ISO 8601).')),
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
/** Columns of the `stripe_customers` table. */
|
|
244
|
+
export type StripeCustomer = z.output<typeof StripeCustomer>
|
|
245
|
+
|
|
144
246
|
/** A developer identity. */
|
|
145
247
|
export const User = z.object({
|
|
146
248
|
address: z
|
|
@@ -233,7 +335,7 @@ export const WebhookSubscription = z.object({
|
|
|
233
335
|
.string()
|
|
234
336
|
.check(
|
|
235
337
|
z.describe('Subscription id (`wh_…`, lexically time-ordered).'),
|
|
236
|
-
z.meta({ examples: ['
|
|
338
|
+
z.meta({ examples: ['wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx'] }),
|
|
237
339
|
),
|
|
238
340
|
lastDeliveryAt: z
|
|
239
341
|
.nullable(z.string())
|
|
@@ -282,7 +384,7 @@ export const WebhookDelivery = z.object({
|
|
|
282
384
|
.string()
|
|
283
385
|
.check(
|
|
284
386
|
z.describe('Delivery id (`whd_…`, lexically time-ordered).'),
|
|
285
|
-
z.meta({ examples: ['
|
|
387
|
+
z.meta({ examples: ['whd_001718668800000_C1yJkk7CuML98HFthEDk9PsY'] }),
|
|
286
388
|
),
|
|
287
389
|
requestUrl: z.string().check(z.describe('Subscriber callback URL the attempt targeted.')),
|
|
288
390
|
responseMs: z
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
|
|
3
|
+
import { postgresUrl } from '../../../test/containers.js'
|
|
4
|
+
import * as Db from '../Db.js'
|
|
5
|
+
import * as BillingSettings from './billingSettings.js'
|
|
6
|
+
|
|
7
|
+
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
8
|
+
|
|
9
|
+
/** Redacts nondeterministic row fields (timestamps). */
|
|
10
|
+
function redact(record: BillingSettings.Record) {
|
|
11
|
+
return { ...record, createdAt: '<timestamp>', updatedAt: '<timestamp>' }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('upsert', () => {
|
|
15
|
+
test('behavior: inserts defaults for fields not provided', async () => {
|
|
16
|
+
const db = create()
|
|
17
|
+
await db.migrate()
|
|
18
|
+
const record = await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '250' })
|
|
19
|
+
expect(redact(record)).toMatchInlineSnapshot(`
|
|
20
|
+
{
|
|
21
|
+
"createdAt": "<timestamp>",
|
|
22
|
+
"currency": "usd",
|
|
23
|
+
"environment": "production",
|
|
24
|
+
"orgId": "org_1",
|
|
25
|
+
"period": "month",
|
|
26
|
+
"spendLimit": "250",
|
|
27
|
+
"txFeeLimit": null,
|
|
28
|
+
"updatedAt": "<timestamp>",
|
|
29
|
+
}
|
|
30
|
+
`)
|
|
31
|
+
await db.close()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('behavior: absent fields keep their stored value', async () => {
|
|
35
|
+
const db = create()
|
|
36
|
+
await db.migrate()
|
|
37
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '250' })
|
|
38
|
+
const record = await BillingSettings.upsert(db, { orgId: 'org_1', txFeeLimit: '0.50' })
|
|
39
|
+
expect(record.spendLimit).toBe('250')
|
|
40
|
+
expect(record.txFeeLimit).toBe('0.50')
|
|
41
|
+
await db.close()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('behavior: null clears a limit without touching the other', async () => {
|
|
45
|
+
const db = create()
|
|
46
|
+
await db.migrate()
|
|
47
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '250', txFeeLimit: '0.50' }) // prettier-ignore
|
|
48
|
+
const record = await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: null })
|
|
49
|
+
expect(record.spendLimit).toBeNull()
|
|
50
|
+
expect(record.txFeeLimit).toBe('0.50')
|
|
51
|
+
await db.close()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('get', () => {
|
|
56
|
+
test('behavior: returns the stored row, undefined for orgs without settings', async () => {
|
|
57
|
+
const db = create()
|
|
58
|
+
await db.migrate()
|
|
59
|
+
expect(await BillingSettings.get(db, 'org_1')).toBeUndefined()
|
|
60
|
+
const record = await BillingSettings.upsert(db, { orgId: 'org_1', txFeeLimit: '1' })
|
|
61
|
+
expect(await BillingSettings.get(db, 'org_1')).toStrictEqual(record)
|
|
62
|
+
await db.close()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('behavior: production and sandbox settings coexist per org and read disjointly', async () => {
|
|
66
|
+
const db = create()
|
|
67
|
+
await db.migrate()
|
|
68
|
+
const production = await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '250' })
|
|
69
|
+
const sandbox = await BillingSettings.upsert(db, { environment: 'sandbox', orgId: 'org_1', spendLimit: '10' }) // prettier-ignore
|
|
70
|
+
expect(sandbox.environment).toBe('sandbox')
|
|
71
|
+
expect(await BillingSettings.get(db, 'org_1')).toStrictEqual(production)
|
|
72
|
+
expect(await BillingSettings.get(db, 'org_1', 'sandbox')).toStrictEqual(sandbox)
|
|
73
|
+
await db.close()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('deleteByOrg', () => {
|
|
78
|
+
test('behavior: deletes the row and reports absence', async () => {
|
|
79
|
+
const db = create()
|
|
80
|
+
await db.migrate()
|
|
81
|
+
await BillingSettings.upsert(db, { orgId: 'org_1', spendLimit: '250' })
|
|
82
|
+
expect(await BillingSettings.deleteByOrg(db, 'org_1')).toBe(true)
|
|
83
|
+
expect(await BillingSettings.deleteByOrg(db, 'org_1')).toBe(false)
|
|
84
|
+
expect(await BillingSettings.get(db, 'org_1')).toBeUndefined()
|
|
85
|
+
await db.close()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Selectable } from 'kysely'
|
|
2
|
+
|
|
3
|
+
import type * as Db from '../Db.js'
|
|
4
|
+
import type * as db_Schema from '../Schema.js'
|
|
5
|
+
|
|
6
|
+
/** Columns of the `billing_settings` table, derived from `Schema.BillingSettings`. */
|
|
7
|
+
export type Table = db_Schema.BillingSettings
|
|
8
|
+
|
|
9
|
+
/** A stored billing-settings row. */
|
|
10
|
+
export type Record = Selectable<Table>
|
|
11
|
+
|
|
12
|
+
/** Currencies limits may be denominated in; single-member today, widens without schema changes. */
|
|
13
|
+
export const currencies = ['usd'] as const satisfies readonly Record['currency'][]
|
|
14
|
+
|
|
15
|
+
/** Spend-limit windows; single-member today, widens without schema changes. */
|
|
16
|
+
export const periods = ['month'] as const satisfies readonly Record['period'][]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reads an organization's billing settings.
|
|
20
|
+
*
|
|
21
|
+
* @param db - The database.
|
|
22
|
+
* @param orgId - The organization id (`org_…`).
|
|
23
|
+
* @param environment - The environment to read; defaults to `production`.
|
|
24
|
+
* @returns The stored record, or `undefined` when the org has none.
|
|
25
|
+
*/
|
|
26
|
+
export function get(
|
|
27
|
+
db: Db.Db,
|
|
28
|
+
orgId: string,
|
|
29
|
+
environment: Record['environment'] = 'production',
|
|
30
|
+
): Promise<Record | undefined> {
|
|
31
|
+
return db.kysely
|
|
32
|
+
.selectFrom('billing_settings')
|
|
33
|
+
.selectAll()
|
|
34
|
+
.where('orgId', '=', orgId)
|
|
35
|
+
.where('environment', '=', environment)
|
|
36
|
+
.executeTakeFirst()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Upserts an organization's billing settings with PATCH semantics: absent
|
|
41
|
+
* fields keep their stored value, `null` clears the limit.
|
|
42
|
+
*
|
|
43
|
+
* @param db - The database.
|
|
44
|
+
* @param input - The settings to apply.
|
|
45
|
+
* @returns The stored record.
|
|
46
|
+
*/
|
|
47
|
+
export async function upsert(db: Db.Db, input: upsert.Input): Promise<Record> {
|
|
48
|
+
const now = new Date().toISOString()
|
|
49
|
+
const environment = input.environment ?? 'production'
|
|
50
|
+
return await db.kysely
|
|
51
|
+
.insertInto('billing_settings')
|
|
52
|
+
.values({
|
|
53
|
+
createdAt: now,
|
|
54
|
+
currency: input.currency ?? 'usd',
|
|
55
|
+
environment,
|
|
56
|
+
orgId: input.orgId,
|
|
57
|
+
period: input.period ?? 'month',
|
|
58
|
+
spendLimit: input.spendLimit ?? null,
|
|
59
|
+
txFeeLimit: input.txFeeLimit ?? null,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
})
|
|
62
|
+
// Targets the `(org_id, environment)` unique index, which exists both
|
|
63
|
+
// before and after the org pkey drop release.
|
|
64
|
+
.onConflict((oc) =>
|
|
65
|
+
oc.columns(['orgId', 'environment']).doUpdateSet({
|
|
66
|
+
...(input.currency !== undefined ? { currency: input.currency } : {}),
|
|
67
|
+
...(input.period !== undefined ? { period: input.period } : {}),
|
|
68
|
+
...(input.spendLimit !== undefined ? { spendLimit: input.spendLimit } : {}),
|
|
69
|
+
...(input.txFeeLimit !== undefined ? { txFeeLimit: input.txFeeLimit } : {}),
|
|
70
|
+
updatedAt: now,
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
.returningAll()
|
|
74
|
+
.executeTakeFirstOrThrow()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export declare namespace upsert {
|
|
78
|
+
/** Fields accepted when upserting billing settings; absent = unchanged, null = cleared. */
|
|
79
|
+
type Input = {
|
|
80
|
+
/** Currency both limits are denominated in. */
|
|
81
|
+
currency?: Record['currency'] | undefined
|
|
82
|
+
/** Environment these settings apply to; defaults to `production`. */
|
|
83
|
+
environment?: Record['environment'] | undefined
|
|
84
|
+
/** Organization id (`org_…`) the settings belong to. */
|
|
85
|
+
orgId: string
|
|
86
|
+
/** Spend-limit window. */
|
|
87
|
+
period?: Record['period'] | undefined
|
|
88
|
+
/** Spend limit per period (decimal string in `currency` units), or null to clear. */
|
|
89
|
+
spendLimit?: string | null | undefined
|
|
90
|
+
/** Per-transaction fee cap (decimal string in `currency` units), or null to clear. */
|
|
91
|
+
txFeeLimit?: string | null | undefined
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Deletes an organization's billing settings.
|
|
97
|
+
*
|
|
98
|
+
* @param db - The database.
|
|
99
|
+
* @param orgId - The organization id (`org_…`).
|
|
100
|
+
* @returns Whether a settings row existed and was deleted.
|
|
101
|
+
*/
|
|
102
|
+
export async function deleteByOrg(db: Db.Db, orgId: string): Promise<boolean> {
|
|
103
|
+
const result = await db.kysely
|
|
104
|
+
.deleteFrom('billing_settings')
|
|
105
|
+
.where('orgId', '=', orgId)
|
|
106
|
+
.executeTakeFirst()
|
|
107
|
+
return result.numDeletedRows > 0n
|
|
108
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Selectable } from 'kysely'
|
|
2
|
-
import { nanoid } from 'nanoid'
|
|
3
2
|
|
|
3
|
+
import * as Id from '../../internal/Id.js'
|
|
4
4
|
import type * as Db from '../Db.js'
|
|
5
5
|
import type * as db_Schema from '../Schema.js'
|
|
6
6
|
import * as Memberships from './memberships.js'
|
|
@@ -31,7 +31,7 @@ export function create(db: Db.Db, input: create.Input): Promise<Record> {
|
|
|
31
31
|
createdAt: now,
|
|
32
32
|
email: input.email.toLowerCase(),
|
|
33
33
|
expiresAt: input.expiresAt,
|
|
34
|
-
id:
|
|
34
|
+
id: Id.generate('inv'),
|
|
35
35
|
invitedBy: input.invitedBy,
|
|
36
36
|
orgId: input.orgId,
|
|
37
37
|
revokedAt: null,
|
|
@@ -12,7 +12,7 @@ function redact(record: Organizations.Record) {
|
|
|
12
12
|
return {
|
|
13
13
|
...record,
|
|
14
14
|
createdAt: '<timestamp>',
|
|
15
|
-
id: /^org_[
|
|
15
|
+
id: /^org_[0-9A-Za-z]{24}$/.test(record.id) ? 'org_<id>' : record.id,
|
|
16
16
|
updatedAt: '<timestamp>',
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Selectable } from 'kysely'
|
|
2
|
-
import { nanoid } from 'nanoid'
|
|
3
2
|
|
|
3
|
+
import * as Id from '../../internal/Id.js'
|
|
4
4
|
import type * as Db from '../Db.js'
|
|
5
5
|
import type * as db_Schema from '../Schema.js'
|
|
6
|
+
import type * as Memberships from './memberships.js'
|
|
6
7
|
|
|
7
8
|
/** Columns of the `organizations` table, derived from `Schema.Organization`. */
|
|
8
9
|
export type Table = db_Schema.Organization
|
|
@@ -25,7 +26,7 @@ export function create(db: Db.Db, input: create.Input): Promise<Record> {
|
|
|
25
26
|
.values({
|
|
26
27
|
createdAt: now,
|
|
27
28
|
createdBy: input.createdBy ?? null,
|
|
28
|
-
id: input.id ??
|
|
29
|
+
id: input.id ?? Id.generate('org'),
|
|
29
30
|
name: input.name,
|
|
30
31
|
updatedAt: now,
|
|
31
32
|
userId: input.userId ?? null,
|
|
@@ -64,7 +65,7 @@ export function createOwned(db: Db.Db, input: createOwned.Input): Promise<Record
|
|
|
64
65
|
.values({
|
|
65
66
|
createdAt: now,
|
|
66
67
|
createdBy: input.userId,
|
|
67
|
-
id:
|
|
68
|
+
id: Id.generate('org'),
|
|
68
69
|
name: input.name,
|
|
69
70
|
updatedAt: now,
|
|
70
71
|
userId: input.userId,
|
|
@@ -109,9 +110,13 @@ export function deleteOrganization(db: Db.Db, id: string): Promise<boolean> {
|
|
|
109
110
|
.where('ownerType', '=', 'api_key')
|
|
110
111
|
.where('ownerId', '=', id)
|
|
111
112
|
.execute()
|
|
113
|
+
await trx.deleteFrom('billing_settings').where('orgId', '=', id).execute()
|
|
112
114
|
await trx.deleteFrom('invitations').where('orgId', '=', id).execute()
|
|
113
115
|
await trx.deleteFrom('memberships').where('orgId', '=', id).execute()
|
|
114
116
|
await trx.deleteFrom('projects').where('orgId', '=', id).execute()
|
|
117
|
+
await trx.deleteFrom('request_usage_buckets').where('orgId', '=', id).execute()
|
|
118
|
+
await trx.deleteFrom('request_usage_meter_events').where('orgId', '=', id).execute()
|
|
119
|
+
await trx.deleteFrom('stripe_customers').where('orgId', '=', id).execute()
|
|
115
120
|
const result = await trx.deleteFrom('organizations').where('id', '=', id).executeTakeFirst()
|
|
116
121
|
return result.numDeletedRows > 0n
|
|
117
122
|
})
|
|
@@ -155,18 +160,23 @@ export function listByUser(db: Db.Db, userId: string): Promise<Record[]> {
|
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
/**
|
|
158
|
-
* Lists organizations `userId` is a member of (any role), newest first.
|
|
163
|
+
* Lists organizations `userId` is a member of (any role), newest first. Each
|
|
164
|
+
* row carries the caller's membership `role`.
|
|
159
165
|
*
|
|
160
166
|
* @param db - The database.
|
|
161
167
|
* @param userId - The member user id (`usr_…`).
|
|
162
168
|
* @returns The records.
|
|
163
169
|
*/
|
|
164
|
-
export function listByMember(
|
|
170
|
+
export function listByMember(
|
|
171
|
+
db: Db.Db,
|
|
172
|
+
userId: string,
|
|
173
|
+
): Promise<(Record & { role: Memberships.Role })[]> {
|
|
165
174
|
return db.kysely
|
|
166
175
|
.selectFrom('organizations')
|
|
167
176
|
.innerJoin('memberships', 'memberships.orgId', 'organizations.id')
|
|
168
177
|
.where('memberships.userId', '=', userId)
|
|
169
178
|
.selectAll('organizations')
|
|
179
|
+
.select('memberships.role')
|
|
170
180
|
.orderBy('organizations.createdAt', 'desc')
|
|
171
181
|
.execute()
|
|
172
182
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Selectable } from 'kysely'
|
|
2
|
-
import { nanoid } from 'nanoid'
|
|
3
2
|
|
|
3
|
+
import * as Id from '../../internal/Id.js'
|
|
4
4
|
import type * as Db from '../Db.js'
|
|
5
5
|
import type * as db_Schema from '../Schema.js'
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ export function create(db: Db.Db, input: create.Input): Promise<Record> {
|
|
|
23
23
|
.insertInto('projects')
|
|
24
24
|
.values({
|
|
25
25
|
createdAt: now,
|
|
26
|
-
id:
|
|
26
|
+
id: Id.generate('prj'),
|
|
27
27
|
name: input.name,
|
|
28
28
|
orgId: input.orgId,
|
|
29
29
|
updatedAt: now,
|