From dcc8757bf16dbf09c58ddbc278a1a173f4036c83 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Thu, 20 Feb 2025 15:40:34 +0100 Subject: [PATCH 1/4] Replaces rate-limit, stripe-billing and usage-estimator with commerce service --- deployment/index.ts | 37 +--- deployment/services/app.ts | 8 +- .../services/{billing.ts => commerce.ts} | 30 ++-- deployment/services/graphql.ts | 20 +-- deployment/services/rate-limit.ts | 70 -------- deployment/services/usage-estimation.ts | 57 ------ deployment/services/usage.ts | 18 +- docker/docker.hcl | 67 ++----- integration-tests/.env | 3 +- .../docker-compose.integration.yaml | 40 ++--- integration-tests/package.json | 2 +- packages/services/api/package.json | 1 + packages/services/api/src/create.ts | 52 ++---- .../services/api/src/modules/billing/index.ts | 13 -- .../modules/billing/module.graphql.mappers.ts | 5 - .../src/modules/billing/providers/tokens.ts | 7 - .../{billing => commerce}/constants.ts | 0 .../api/src/modules/commerce/index.ts | 22 +++ .../commerce/module.graphql.mappers.ts | 5 + .../{billing => commerce}/module.graphql.ts | 22 +++ .../providers/billing.provider.ts | 76 ++++---- .../commerce/providers/commerce-client.ts | 38 ++++ .../providers/rate-limit.provider.ts | 33 ++-- .../providers/usage-estimation.provider.ts | 25 +-- .../resolvers/BillingDetails.ts | 0 .../resolvers/BillingInvoice.ts | 0 .../resolvers/BillingPaymentMethod.ts | 0 .../resolvers/Mutation/downgradeToHobby.ts | 0 .../Mutation/generateStripePortalLink.ts | 0 .../resolvers/Mutation/updateOrgRateLimit.ts | 0 .../resolvers/Mutation/upgradeToPro.ts | 0 .../resolvers/Organization.ts | 27 +++ .../resolvers/Query/billingPlans.ts | 0 .../resolvers/Query/usageEstimation.ts | 0 .../providers/organization-manager.ts | 2 +- .../Mutation/inviteToOrganizationByEmail.ts | 2 +- .../api/src/modules/rate-limit/index.ts | 13 -- .../src/modules/rate-limit/module.graphql.ts | 17 -- .../modules/rate-limit/providers/tokens.ts | 9 - .../rate-limit/resolvers/Organization.ts | 30 ---- .../schema/providers/schema-publisher.ts | 2 +- .../providers/in-memory-rate-limiter.ts | 2 +- .../api/src/modules/usage-estimation/index.ts | 12 -- .../usage-estimation/module.graphql.ts | 17 -- .../usage-estimation/providers/tokens.ts | 9 - .../.env.template | 7 +- packages/services/commerce/.gitignore | 4 + packages/services/commerce/LICENSE | 21 +++ packages/services/commerce/README.md | 18 ++ .../{stripe-billing => commerce}/package.json | 7 +- packages/services/commerce/src/api.ts | 12 ++ .../{rate-limit => commerce}/src/dev.ts | 0 .../src/environment.ts | 105 +++++++---- .../src/index.ts | 78 +++++--- .../src => commerce/src/rate-limit}/emails.ts | 2 +- .../services/commerce/src/rate-limit/index.ts | 33 ++++ .../src/rate-limit}/limiter.ts | 62 +++---- .../src/rate-limit}/metrics.ts | 0 .../src/stripe-billing/billing.ts} | 29 ++- .../src/stripe-billing/index.ts} | 94 ++++------ packages/services/commerce/src/trpc.ts | 17 ++ .../src/usage-estimator}/estimator.ts | 32 ++-- .../commerce/src/usage-estimator/index.ts | 53 ++++++ .../src/usage-estimator}/metrics.ts | 0 .../tsconfig.json | 0 packages/services/rate-limit/.env.template | 12 -- packages/services/rate-limit/README.md | 29 --- packages/services/rate-limit/package.json | 32 ---- packages/services/rate-limit/src/api.ts | 49 ----- .../services/rate-limit/src/environment.ts | 161 ----------------- packages/services/rate-limit/src/index.ts | 143 --------------- packages/services/rate-limit/tsconfig.json | 9 - packages/services/server/.env.template | 6 +- packages/services/server/README.md | 24 ++- packages/services/server/src/api.ts | 15 +- packages/services/server/src/environment.ts | 22 +-- packages/services/server/src/index.ts | 25 ++- packages/services/service-common/src/trpc.ts | 5 +- .../services/stripe-billing/.env.template | 9 - packages/services/stripe-billing/README.md | 24 --- packages/services/stripe-billing/src/dev.ts | 8 - packages/services/stripe-billing/src/index.ts | 132 -------------- packages/services/stripe-billing/src/types.ts | 0 packages/services/tokens/README.md | 1 - packages/services/usage-estimator/README.md | 28 --- .../services/usage-estimator/package.json | 29 --- packages/services/usage-estimator/src/api.ts | 73 -------- packages/services/usage-estimator/src/dev.ts | 8 - .../usage-estimator/src/environment.ts | 143 --------------- .../services/usage-estimator/tsconfig.json | 9 - packages/services/usage/.env.template | 2 +- packages/services/usage/README.md | 2 +- packages/services/usage/src/environment.ts | 6 +- packages/services/usage/src/index.ts | 6 +- packages/services/usage/src/rate-limit.ts | 8 +- pnpm-lock.yaml | 168 ++++++------------ tsconfig.json | 4 +- 97 files changed, 735 insertions(+), 1824 deletions(-) rename deployment/services/{billing.ts => commerce.ts} (73%) delete mode 100644 deployment/services/rate-limit.ts delete mode 100644 deployment/services/usage-estimation.ts delete mode 100644 packages/services/api/src/modules/billing/index.ts delete mode 100644 packages/services/api/src/modules/billing/module.graphql.mappers.ts delete mode 100644 packages/services/api/src/modules/billing/providers/tokens.ts rename packages/services/api/src/modules/{billing => commerce}/constants.ts (100%) create mode 100644 packages/services/api/src/modules/commerce/index.ts create mode 100644 packages/services/api/src/modules/commerce/module.graphql.mappers.ts rename packages/services/api/src/modules/{billing => commerce}/module.graphql.ts (84%) rename packages/services/api/src/modules/{billing => commerce}/providers/billing.provider.ts (59%) create mode 100644 packages/services/api/src/modules/commerce/providers/commerce-client.ts rename packages/services/api/src/modules/{rate-limit => commerce}/providers/rate-limit.provider.ts (61%) rename packages/services/api/src/modules/{usage-estimation => commerce}/providers/usage-estimation.provider.ts (70%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/BillingDetails.ts (100%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/BillingInvoice.ts (100%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/BillingPaymentMethod.ts (100%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/Mutation/downgradeToHobby.ts (100%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/Mutation/generateStripePortalLink.ts (100%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/Mutation/updateOrgRateLimit.ts (100%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/Mutation/upgradeToPro.ts (100%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/Organization.ts (79%) rename packages/services/api/src/modules/{billing => commerce}/resolvers/Query/billingPlans.ts (100%) rename packages/services/api/src/modules/{usage-estimation => commerce}/resolvers/Query/usageEstimation.ts (100%) delete mode 100644 packages/services/api/src/modules/rate-limit/index.ts delete mode 100644 packages/services/api/src/modules/rate-limit/module.graphql.ts delete mode 100644 packages/services/api/src/modules/rate-limit/providers/tokens.ts delete mode 100644 packages/services/api/src/modules/rate-limit/resolvers/Organization.ts rename packages/services/api/src/modules/{rate-limit => shared}/providers/in-memory-rate-limiter.ts (98%) delete mode 100644 packages/services/api/src/modules/usage-estimation/index.ts delete mode 100644 packages/services/api/src/modules/usage-estimation/module.graphql.ts delete mode 100644 packages/services/api/src/modules/usage-estimation/providers/tokens.ts rename packages/services/{usage-estimator => commerce}/.env.template (61%) create mode 100644 packages/services/commerce/.gitignore create mode 100644 packages/services/commerce/LICENSE create mode 100644 packages/services/commerce/README.md rename packages/services/{stripe-billing => commerce}/package.json (80%) create mode 100644 packages/services/commerce/src/api.ts rename packages/services/{rate-limit => commerce}/src/dev.ts (100%) rename packages/services/{stripe-billing => commerce}/src/environment.ts (71%) rename packages/services/{usage-estimator => commerce}/src/index.ts (53%) rename packages/services/{rate-limit/src => commerce/src/rate-limit}/emails.ts (99%) create mode 100644 packages/services/commerce/src/rate-limit/index.ts rename packages/services/{rate-limit/src => commerce/src/rate-limit}/limiter.ts (84%) rename packages/services/{rate-limit/src => commerce/src/rate-limit}/metrics.ts (100%) rename packages/services/{stripe-billing/src/billing-sync.ts => commerce/src/stripe-billing/billing.ts} (80%) rename packages/services/{stripe-billing/src/api.ts => commerce/src/stripe-billing/index.ts} (78%) create mode 100644 packages/services/commerce/src/trpc.ts rename packages/services/{usage-estimator/src => commerce/src/usage-estimator}/estimator.ts (72%) create mode 100644 packages/services/commerce/src/usage-estimator/index.ts rename packages/services/{usage-estimator/src => commerce/src/usage-estimator}/metrics.ts (100%) rename packages/services/{stripe-billing => commerce}/tsconfig.json (100%) delete mode 100644 packages/services/rate-limit/.env.template delete mode 100644 packages/services/rate-limit/README.md delete mode 100644 packages/services/rate-limit/package.json delete mode 100644 packages/services/rate-limit/src/api.ts delete mode 100644 packages/services/rate-limit/src/environment.ts delete mode 100644 packages/services/rate-limit/src/index.ts delete mode 100644 packages/services/rate-limit/tsconfig.json delete mode 100644 packages/services/stripe-billing/.env.template delete mode 100644 packages/services/stripe-billing/README.md delete mode 100644 packages/services/stripe-billing/src/dev.ts delete mode 100644 packages/services/stripe-billing/src/index.ts delete mode 100644 packages/services/stripe-billing/src/types.ts delete mode 100644 packages/services/usage-estimator/README.md delete mode 100644 packages/services/usage-estimator/package.json delete mode 100644 packages/services/usage-estimator/src/api.ts delete mode 100644 packages/services/usage-estimator/src/dev.ts delete mode 100644 packages/services/usage-estimator/src/environment.ts delete mode 100644 packages/services/usage-estimator/tsconfig.json diff --git a/deployment/index.ts b/deployment/index.ts index bd7ae18656..50456772ff 100644 --- a/deployment/index.ts +++ b/deployment/index.ts @@ -1,10 +1,10 @@ import * as pulumi from '@pulumi/pulumi'; import { deployApp } from './services/app'; -import { deployStripeBilling } from './services/billing'; import { deployCFBroker } from './services/cf-broker'; import { deployCFCDN } from './services/cf-cdn'; import { deployClickhouse } from './services/clickhouse'; import { deployCloudFlareSecurityTransform } from './services/cloudflare-security'; +import { deployCommerce } from './services/commerce'; import { deployDatabaseCleanupJob } from './services/database-cleanup'; import { deployDbMigrations } from './services/db-migrations'; import { configureDocker } from './services/docker'; @@ -17,7 +17,6 @@ import { deployObservability } from './services/observability'; import { deploySchemaPolicy } from './services/policy'; import { deployPostgres } from './services/postgres'; import { deployProxy } from './services/proxy'; -import { deployRateLimit } from './services/rate-limit'; import { deployRedis } from './services/redis'; import { deployS3, deployS3AuditLog, deployS3Mirror } from './services/s3'; import { deploySchema } from './services/schema'; @@ -27,7 +26,6 @@ import { configureSlackApp } from './services/slack-app'; import { deploySuperTokens } from './services/supertokens'; import { deployTokens } from './services/tokens'; import { deployUsage } from './services/usage'; -import { deployUsageEstimation } from './services/usage-estimation'; import { deployUsageIngestor } from './services/usage-ingestor'; import { deployWebhooks } from './services/webhooks'; import { configureZendesk } from './services/zendesk'; @@ -148,37 +146,16 @@ const emails = deployEmails({ observability, }); -const usageEstimator = deployUsageEstimation({ - image: docker.factory.getImageId('usage-estimator', imagesTag), +const commerce = deployCommerce({ + image: docker.factory.getImageId('commerce', imagesTag), docker, environment, clickhouse, dbMigrations, sentry, observability, -}); - -const billing = deployStripeBilling({ - image: docker.factory.getImageId('stripe-billing', imagesTag), - docker, - postgres, - environment, - dbMigrations, - usageEstimator, - sentry, - observability, -}); - -const rateLimit = deployRateLimit({ - image: docker.factory.getImageId('rate-limit', imagesTag), - docker, - environment, - dbMigrations, - usageEstimator, emails, postgres, - sentry, - observability, }); const usage = deployUsage({ @@ -188,7 +165,7 @@ const usage = deployUsage({ tokens, kafka, dbMigrations, - rateLimit, + commerce, sentry, observability, }); @@ -241,9 +218,7 @@ const graphql = deployGraphQL({ redis, usage, cdn, - usageEstimator, - rateLimit, - billing, + commerce, emails, supertokens, s3, @@ -302,7 +277,7 @@ const app = deployApp({ image: docker.factory.getImageId('app', imagesTag), docker, zendesk, - billing, + commerce, github: githubApp, slackApp, sentry, diff --git a/deployment/services/app.ts b/deployment/services/app.ts index aa176ecb14..11c6921f9c 100644 --- a/deployment/services/app.ts +++ b/deployment/services/app.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'; import * as pulumi from '@pulumi/pulumi'; import { serviceLocalEndpoint } from '../utils/local-endpoint'; import { ServiceDeployment } from '../utils/service-deployment'; -import { StripeBilling } from './billing'; +import { CommerceService } from './commerce'; import { DbMigrations } from './db-migrations'; import { Docker } from './docker'; import { Environment } from './environment'; @@ -23,7 +23,7 @@ export function deployApp({ zendesk, github, slackApp, - billing, + commerce, sentry, environment, }: { @@ -35,7 +35,7 @@ export function deployApp({ zendesk: Zendesk; github: GitHubApp; slackApp: SlackApp; - billing: StripeBilling; + commerce: CommerceService; sentry: Sentry; publishAppDeploymentCommand: pulumi.Resource | undefined; }) { @@ -79,7 +79,7 @@ export function deployApp({ ) .withSecret('INTEGRATION_SLACK_CLIENT_ID', slackApp.secret, 'clientId') .withSecret('INTEGRATION_SLACK_CLIENT_SECRET', slackApp.secret, 'clientSecret') - .withSecret('STRIPE_PUBLIC_KEY', billing.secret, 'stripePublicKey') + .withSecret('STRIPE_PUBLIC_KEY', commerce.stripeSecret, 'stripePublicKey') .withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn') .deploy(); } diff --git a/deployment/services/billing.ts b/deployment/services/commerce.ts similarity index 73% rename from deployment/services/billing.ts rename to deployment/services/commerce.ts index 46be04520d..aa02df2133 100644 --- a/deployment/services/billing.ts +++ b/deployment/services/commerce.ts @@ -2,38 +2,41 @@ import * as pulumi from '@pulumi/pulumi'; import { serviceLocalEndpoint } from '../utils/local-endpoint'; import { ServiceSecret } from '../utils/secrets'; import { ServiceDeployment } from '../utils/service-deployment'; +import { Clickhouse } from './clickhouse'; import { DbMigrations } from './db-migrations'; import { Docker } from './docker'; +import { Emails } from './emails'; import { Environment } from './environment'; import { Observability } from './observability'; import { Postgres } from './postgres'; import { Sentry } from './sentry'; -import { UsageEstimator } from './usage-estimation'; -export type StripeBillingService = ReturnType; +export type CommerceService = ReturnType; class StripeSecret extends ServiceSecret<{ stripePrivateKey: pulumi.Output | string; stripePublicKey: string | pulumi.Output; }> {} -export function deployStripeBilling({ +export function deployCommerce({ observability, environment, dbMigrations, - usageEstimator, + emails, image, docker, postgres, + clickhouse, sentry, }: { observability: Observability; - usageEstimator: UsageEstimator; image: string; environment: Environment; dbMigrations: DbMigrations; docker: Docker; + emails: Emails; postgres: Postgres; + clickhouse: Clickhouse; sentry: Sentry; }) { const billingConfig = new pulumi.Config('billing'); @@ -42,7 +45,7 @@ export function deployStripeBilling({ stripePublicKey: billingConfig.require('stripePublicKey'), }); const { deployment, service } = new ServiceDeployment( - 'stripe-billing', + 'commerce', { image, imagePullSecret: docker.secret, @@ -53,7 +56,9 @@ export function deployStripeBilling({ env: { ...environment.envVars, SENTRY: sentry.enabled ? '1' : '0', - USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service), + EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service), + WEB_APP_URL: `https://${environment.appDns}/`, + OPENTELEMETRY_TRACE_USAGE_REQUESTS: observability.enabledForUsageService ? '1' : '', OPENTELEMETRY_COLLECTOR_ENDPOINT: observability.enabled && observability.tracingEndpoint ? observability.tracingEndpoint @@ -62,7 +67,7 @@ export function deployStripeBilling({ exposesMetrics: true, port: 4000, }, - [dbMigrations, usageEstimator.service, usageEstimator.deployment], + [dbMigrations], ) .withSecret('STRIPE_SECRET_KEY', stripeSecret, 'stripePrivateKey') .withSecret('POSTGRES_HOST', postgres.pgBouncerSecret, 'host') @@ -71,14 +76,17 @@ export function deployStripeBilling({ .withSecret('POSTGRES_PASSWORD', postgres.pgBouncerSecret, 'password') .withSecret('POSTGRES_DB', postgres.pgBouncerSecret, 'database') .withSecret('POSTGRES_SSL', postgres.pgBouncerSecret, 'ssl') + .withSecret('CLICKHOUSE_HOST', clickhouse.secret, 'host') + .withSecret('CLICKHOUSE_PORT', clickhouse.secret, 'port') + .withSecret('CLICKHOUSE_USERNAME', clickhouse.secret, 'username') + .withSecret('CLICKHOUSE_PASSWORD', clickhouse.secret, 'password') + .withSecret('CLICKHOUSE_PROTOCOL', clickhouse.secret, 'protocol') .withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn') .deploy(); return { deployment, service, - secret: stripeSecret, + stripeSecret, }; } - -export type StripeBilling = ReturnType; diff --git a/deployment/services/graphql.ts b/deployment/services/graphql.ts index 7b8adb89b6..dfb4abaec8 100644 --- a/deployment/services/graphql.ts +++ b/deployment/services/graphql.ts @@ -2,9 +2,9 @@ import * as pulumi from '@pulumi/pulumi'; import { serviceLocalEndpoint } from '../utils/local-endpoint'; import { ServiceSecret } from '../utils/secrets'; import { ServiceDeployment } from '../utils/service-deployment'; -import { StripeBillingService } from './billing'; import { CDN } from './cf-cdn'; import { Clickhouse } from './clickhouse'; +import { CommerceService } from './commerce'; import { DbMigrations } from './db-migrations'; import { Docker } from './docker'; import { Emails } from './emails'; @@ -13,7 +13,6 @@ import { GitHubApp } from './github'; import { Observability } from './observability'; import { SchemaPolicy } from './policy'; import { Postgres } from './postgres'; -import { RateLimitService } from './rate-limit'; import { Redis } from './redis'; import { S3 } from './s3'; import { Schema } from './schema'; @@ -21,7 +20,6 @@ import { Sentry } from './sentry'; import { Supertokens } from './supertokens'; import { Tokens } from './tokens'; import { Usage } from './usage'; -import { UsageEstimator } from './usage-estimation'; import { Webhooks } from './webhooks'; import { Zendesk } from './zendesk'; @@ -43,10 +41,8 @@ export function deployGraphQL({ cdn, redis, usage, - usageEstimator, + commerce, dbMigrations, - rateLimit, - billing, emails, supertokens, s3, @@ -75,10 +71,8 @@ export function deployGraphQL({ s3Mirror: S3; s3AuditLog: S3; usage: Usage; - usageEstimator: UsageEstimator; dbMigrations: DbMigrations; - rateLimit: RateLimitService; - billing: StripeBillingService; + commerce: CommerceService; emails: Emails; supertokens: Supertokens; zendesk: Zendesk; @@ -125,15 +119,13 @@ export function deployGraphQL({ ...apiEnv, SENTRY: sentry.enabled ? '1' : '0', REQUEST_LOGGING: '0', // disabled - BILLING_ENDPOINT: serviceLocalEndpoint(billing.service), + COMMERCE_ENDPOINT: serviceLocalEndpoint(commerce.service), TOKENS_ENDPOINT: serviceLocalEndpoint(tokens.service), WEBHOOKS_ENDPOINT: serviceLocalEndpoint(webhooks.service), SCHEMA_ENDPOINT: serviceLocalEndpoint(schema.service), SCHEMA_POLICY_ENDPOINT: serviceLocalEndpoint(schemaPolicy.service), HIVE_USAGE_ENDPOINT: serviceLocalEndpoint(usage.service), - RATE_LIMIT_ENDPOINT: serviceLocalEndpoint(rateLimit.service), EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service), - USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service), WEB_APP_URL: `https://${environment.appDns}`, GRAPHQL_PUBLIC_ORIGIN: `https://${environment.appDns}`, CDN_CF: '1', @@ -166,8 +158,8 @@ export function deployGraphQL({ redis.service, clickhouse.deployment, clickhouse.service, - rateLimit.deployment, - rateLimit.service, + commerce.deployment, + commerce.service, ], ) // GitHub App diff --git a/deployment/services/rate-limit.ts b/deployment/services/rate-limit.ts deleted file mode 100644 index ea97753990..0000000000 --- a/deployment/services/rate-limit.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { serviceLocalEndpoint } from '../utils/local-endpoint'; -import { ServiceDeployment } from '../utils/service-deployment'; -import { DbMigrations } from './db-migrations'; -import { Docker } from './docker'; -import { Emails } from './emails'; -import { Environment } from './environment'; -import { Observability } from './observability'; -import { Postgres } from './postgres'; -import { Sentry } from './sentry'; -import { UsageEstimator } from './usage-estimation'; - -export type RateLimitService = ReturnType; - -export function deployRateLimit({ - environment, - dbMigrations, - usageEstimator, - emails, - image, - docker, - postgres, - sentry, - observability, -}: { - observability: Observability; - usageEstimator: UsageEstimator; - environment: Environment; - dbMigrations: DbMigrations; - emails: Emails; - image: string; - docker: Docker; - postgres: Postgres; - sentry: Sentry; -}) { - return new ServiceDeployment( - 'rate-limiter', - { - imagePullSecret: docker.secret, - replicas: environment.isProduction ? 3 : 1, - readinessProbe: '/_readiness', - livenessProbe: '/_health', - startupProbe: '/_health', - env: { - ...environment.envVars, - SENTRY: sentry.enabled ? '1' : '0', - LIMIT_CACHE_UPDATE_INTERVAL_MS: environment.isProduction ? '60000' : '86400000', - USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service), - EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service), - WEB_APP_URL: `https://${environment.appDns}/`, - OPENTELEMETRY_TRACE_USAGE_REQUESTS: observability.enabledForUsageService ? '1' : '', - OPENTELEMETRY_COLLECTOR_ENDPOINT: - observability.enabled && observability.tracingEndpoint - ? observability.tracingEndpoint - : '', - }, - exposesMetrics: true, - port: 4000, - image, - }, - [dbMigrations, usageEstimator.service, usageEstimator.deployment], - ) - .withSecret('POSTGRES_HOST', postgres.pgBouncerSecret, 'host') - .withSecret('POSTGRES_PORT', postgres.pgBouncerSecret, 'port') - .withSecret('POSTGRES_USER', postgres.pgBouncerSecret, 'user') - .withSecret('POSTGRES_PASSWORD', postgres.pgBouncerSecret, 'password') - .withSecret('POSTGRES_DB', postgres.pgBouncerSecret, 'database') - .withSecret('POSTGRES_SSL', postgres.pgBouncerSecret, 'ssl') - .withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn') - .deploy(); -} diff --git a/deployment/services/usage-estimation.ts b/deployment/services/usage-estimation.ts deleted file mode 100644 index 29e1cc5454..0000000000 --- a/deployment/services/usage-estimation.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ServiceDeployment } from '../utils/service-deployment'; -import { Clickhouse } from './clickhouse'; -import { DbMigrations } from './db-migrations'; -import { Docker } from './docker'; -import { Environment } from './environment'; -import { Observability } from './observability'; -import { Sentry } from './sentry'; - -export type UsageEstimator = ReturnType; - -export function deployUsageEstimation({ - image, - docker, - environment, - clickhouse, - dbMigrations, - sentry, - observability, -}: { - observability: Observability; - image: string; - docker: Docker; - environment: Environment; - clickhouse: Clickhouse; - dbMigrations: DbMigrations; - sentry: Sentry; -}) { - return new ServiceDeployment( - 'usage-estimator', - { - image, - imagePullSecret: docker.secret, - replicas: environment.isProduction ? 3 : 1, - readinessProbe: '/_readiness', - livenessProbe: '/_health', - startupProbe: '/_health', - env: { - ...environment.envVars, - SENTRY: sentry.enabled ? '1' : '0', - OPENTELEMETRY_COLLECTOR_ENDPOINT: - observability.enabled && observability.tracingEndpoint - ? observability.tracingEndpoint - : '', - }, - exposesMetrics: true, - port: 4000, - }, - [dbMigrations], - ) - .withSecret('CLICKHOUSE_HOST', clickhouse.secret, 'host') - .withSecret('CLICKHOUSE_PORT', clickhouse.secret, 'port') - .withSecret('CLICKHOUSE_USERNAME', clickhouse.secret, 'username') - .withSecret('CLICKHOUSE_PASSWORD', clickhouse.secret, 'password') - .withSecret('CLICKHOUSE_PROTOCOL', clickhouse.secret, 'protocol') - .withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn') - .deploy(); -} diff --git a/deployment/services/usage.ts b/deployment/services/usage.ts index dc97e92c34..fd146d33a8 100644 --- a/deployment/services/usage.ts +++ b/deployment/services/usage.ts @@ -1,12 +1,12 @@ import * as pulumi from '@pulumi/pulumi'; import { serviceLocalEndpoint } from '../utils/local-endpoint'; import { ServiceDeployment } from '../utils/service-deployment'; +import { CommerceService } from './commerce'; import { DbMigrations } from './db-migrations'; import { Docker } from './docker'; import { Environment } from './environment'; import { Kafka } from './kafka'; import { Observability } from './observability'; -import { RateLimitService } from './rate-limit'; import { Sentry } from './sentry'; import { Tokens } from './tokens'; @@ -17,7 +17,7 @@ export function deployUsage({ tokens, kafka, dbMigrations, - rateLimit, + commerce, image, docker, observability, @@ -29,7 +29,7 @@ export function deployUsage({ tokens: Tokens; kafka: Kafka; dbMigrations: DbMigrations; - rateLimit: RateLimitService; + commerce: CommerceService; docker: Docker; sentry: Sentry; }) { @@ -66,7 +66,7 @@ export function deployUsage({ KAFKA_BUFFER_DYNAMIC: kafkaBufferDynamic, KAFKA_TOPIC: kafka.config.topic, TOKENS_ENDPOINT: serviceLocalEndpoint(tokens.service), - RATE_LIMIT_ENDPOINT: serviceLocalEndpoint(rateLimit.service), + COMMERCE_ENDPOINT: serviceLocalEndpoint(commerce.service), OPENTELEMETRY_COLLECTOR_ENDPOINT: observability.enabled && observability.enabledForUsageService && @@ -85,13 +85,9 @@ export function deployUsage({ maxReplicas, }, }, - [ - dbMigrations, - tokens.deployment, - tokens.service, - rateLimit.deployment, - rateLimit.service, - ].filter(Boolean), + [dbMigrations, tokens.deployment, tokens.service, commerce.deployment, commerce.service].filter( + Boolean, + ), ) .withSecret('KAFKA_SASL_USERNAME', kafka.secret, 'saslUsername') .withSecret('KAFKA_SASL_PASSWORD', kafka.secret, 'saslPassword') diff --git a/docker/docker.hcl b/docker/docker.hcl index 4365d62f8c..0910aadbdc 100644 --- a/docker/docker.hcl +++ b/docker/docker.hcl @@ -123,27 +123,6 @@ target "emails" { ] } -target "rate-limit" { - inherits = ["service-base", get_target()] - contexts = { - dist = "${PWD}/packages/services/rate-limit/dist" - shared = "${PWD}/docker/shared" - } - args = { - SERVICE_DIR_NAME = "@hive/rate-limit" - IMAGE_TITLE = "graphql-hive/rate-limit" - IMAGE_DESCRIPTION = "The rate limit service of the GraphQL Hive project." - PORT = "3009" - HEALTHCHECK_CMD = "wget --spider -q http://127.0.0.1:$${PORT}/_readiness" - } - tags = [ - local_image_tag("rate-limit"), - stable_image_tag("rate-limit"), - image_tag("rate-limit", COMMIT_SHA), - image_tag("rate-limit", BRANCH_NAME) - ] -} - target "schema" { inherits = ["service-base", get_target()] contexts = { @@ -225,24 +204,24 @@ target "storage" { ] } -target "stripe-billing" { +target "commerce" { inherits = ["service-base", get_target()] contexts = { - dist = "${PWD}/packages/services/stripe-billing/dist" + dist = "${PWD}/packages/services/commerce/dist" shared = "${PWD}/docker/shared" } args = { - SERVICE_DIR_NAME = "@hive/stripe-billing" - IMAGE_TITLE = "graphql-hive/stripe-billing" - IMAGE_DESCRIPTION = "The stripe billing service of the GraphQL Hive project." + SERVICE_DIR_NAME = "@hive/commerce" + IMAGE_TITLE = "graphql-hive/commerce" + IMAGE_DESCRIPTION = "The commerce service of the GraphQL Hive project." PORT = "3010" HEALTHCHECK_CMD = "wget --spider -q http://127.0.0.1:$${PORT}/_readiness" } tags = [ - local_image_tag("stripe-billing"), - stable_image_tag("stripe-billing"), - image_tag("stripe-billing", COMMIT_SHA), - image_tag("stripe-billing", BRANCH_NAME) + local_image_tag("commerce"), + stable_image_tag("commerce"), + image_tag("commerce", COMMIT_SHA), + image_tag("commerce", BRANCH_NAME) ] } @@ -267,27 +246,6 @@ target "tokens" { ] } -target "usage-estimator" { - inherits = ["service-base", get_target()] - contexts = { - dist = "${PWD}/packages/services/usage-estimator/dist" - shared = "${PWD}/docker/shared" - } - args = { - SERVICE_DIR_NAME = "@hive/usage-estimator" - IMAGE_TITLE = "graphql-hive/usage-estimator" - IMAGE_DESCRIPTION = "The usage estimator service of the GraphQL Hive project." - PORT = "3008" - HEALTHCHECK_CMD = "wget --spider -q http://127.0.0.1:$${PORT}/_readiness" - } - tags = [ - local_image_tag("usage-estimator"), - stable_image_tag("usage-estimator"), - image_tag("usage-estimator", COMMIT_SHA), - image_tag("usage-estimator", BRANCH_NAME) - ] -} - target "usage-ingestor" { inherits = ["service-base", get_target()] contexts = { @@ -432,17 +390,15 @@ target "cli" { group "build" { targets = [ "emails", - "rate-limit", "schema", "policy", "storage", "tokens", - "usage-estimator", "usage-ingestor", "usage", "webhooks", "server", - "stripe-billing", + "commerce", "composition-federation-2", "app" ] @@ -450,13 +406,12 @@ group "build" { group "integration-tests" { targets = [ + "commerce", "emails", - "rate-limit", "schema", "policy", "storage", "tokens", - "usage-estimator", "usage-ingestor", "usage", "webhooks", diff --git a/integration-tests/.env b/integration-tests/.env index 130b2eacd9..0130121965 100644 --- a/integration-tests/.env +++ b/integration-tests/.env @@ -15,8 +15,7 @@ CLICKHOUSE_USER=clickhouse CLICKHOUSE_PASSWORD=wowverysecuremuchsecret CDN_AUTH_PRIVATE_KEY=6b4721a99bd2ef6c00ce4328f34d95d7 EMAIL_PROVIDER=mock -USAGE_ESTIMATOR_ENDPOINT=http://usage-estimator:3008 -RATE_LIMIT_ENDPOINT=http://rate-limit:3009 +COMMERCE_ENDPOINT=http://commerce:3009 CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS=500 CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE=1000 EXTERNAL_COMPOSITION_SECRET=secretsecret diff --git a/integration-tests/docker-compose.integration.yaml b/integration-tests/docker-compose.integration.yaml index 23bb858251..8d33d8c67d 100644 --- a/integration-tests/docker-compose.integration.yaml +++ b/integration-tests/docker-compose.integration.yaml @@ -111,30 +111,8 @@ services: LOG_LEVEL: debug SECRET: '${EXTERNAL_COMPOSITION_SECRET}' - usage-estimator: - image: '${DOCKER_REGISTRY}usage-estimator${DOCKER_TAG}' - networks: - - 'stack' - ports: - - 3008:3008 - depends_on: - clickhouse: - condition: service_healthy - migrations: - condition: service_completed_successfully - environment: - NODE_ENV: production - LOG_LEVEL: debug - CLICKHOUSE_PROTOCOL: 'http' - CLICKHOUSE_HOST: 'clickhouse' - CLICKHOUSE_PORT: '8123' - CLICKHOUSE_USERNAME: '${CLICKHOUSE_USER}' - CLICKHOUSE_PASSWORD: '${CLICKHOUSE_PASSWORD}' - PORT: 3008 - FF_CLICKHOUSE_V2_TABLES: ${FF_CLICKHOUSE_V2_TABLES:-} - - rate-limit: - image: '${DOCKER_REGISTRY}rate-limit${DOCKER_TAG}' + commerce: + image: '${DOCKER_REGISTRY}commerce${DOCKER_TAG}' networks: - 'stack' ports: @@ -144,8 +122,6 @@ services: condition: service_healthy migrations: condition: service_completed_successfully - usage-estimator: - condition: service_healthy emails: condition: service_healthy environment: @@ -157,8 +133,13 @@ services: POSTGRES_DB: '${POSTGRES_DB}' POSTGRES_USER: '${POSTGRES_USER}' POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}' - USAGE_ESTIMATOR_ENDPOINT: http://usage-estimator:3008 + CLICKHOUSE_PROTOCOL: 'http' + CLICKHOUSE_HOST: 'clickhouse' + CLICKHOUSE_PORT: '8123' + CLICKHOUSE_USERNAME: '${CLICKHOUSE_USER}' + CLICKHOUSE_PASSWORD: '${CLICKHOUSE_PASSWORD}' EMAILS_ENDPOINT: http://emails:3011 + STRIPE_SECRET_KEY: empty PORT: 3009 # Overrides only to `docker-compose.community.yaml` from now on: @@ -180,8 +161,7 @@ services: GITHUB_APP_PRIVATE_KEY: 5f938d51a065476c4dc1b04aeba13afb FEEDBACK_SLACK_TOKEN: '' FEEDBACK_SLACK_CHANNEL: '#hive' - USAGE_ESTIMATOR_ENDPOINT: '${USAGE_ESTIMATOR_ENDPOINT}' - RATE_LIMIT_ENDPOINT: '${RATE_LIMIT_ENDPOINT}' + COMMERCE_ENDPOINT: '${COMMERCE_ENDPOINT}' EMAIL_PROVIDER: '${EMAIL_PROVIDER}' LOG_LEVEL: debug # Auth @@ -239,7 +219,7 @@ services: usage: environment: - RATE_LIMIT_ENDPOINT: '${RATE_LIMIT_ENDPOINT}' + COMMERCE_ENDPOINT: '${COMMERCE_ENDPOINT}' RATE_LIMIT_TTL: 1000 LOG_LEVEL: debug depends_on: diff --git a/integration-tests/package.json b/integration-tests/package.json index 28fb423017..4c4df11009 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -17,7 +17,7 @@ "@graphql-hive/apollo": "workspace:*", "@graphql-hive/core": "workspace:*", "@graphql-typed-document-node/core": "3.2.0", - "@hive/rate-limit": "workspace:*", + "@hive/commerce": "workspace:*", "@hive/schema": "workspace:*", "@hive/server": "workspace:*", "@hive/storage": "workspace:*", diff --git a/packages/services/api/package.json b/packages/services/api/package.json index eb658f8fb7..59ead2a821 100644 --- a/packages/services/api/package.json +++ b/packages/services/api/package.json @@ -64,6 +64,7 @@ "prom-client": "15.1.3", "redlock": "5.0.0-beta.2", "slonik": "30.4.4", + "stripe": "17.5.0", "supertokens-node": "16.7.5", "tslib": "2.8.1", "undici": "6.21.1", diff --git a/packages/services/api/src/create.ts b/packages/services/api/src/create.ts index 9329aa949f..e93d50bb55 100644 --- a/packages/services/api/src/create.ts +++ b/packages/services/api/src/create.ts @@ -10,12 +10,15 @@ import { AuditLogRecorder } from './modules/audit-logs/providers/audit-log-recor import { AuditLogS3Config } from './modules/audit-logs/providers/audit-logs-manager'; import { authModule } from './modules/auth'; import { Session } from './modules/auth/lib/authz'; -import { billingModule } from './modules/billing'; -import { BILLING_CONFIG, BillingConfig } from './modules/billing/providers/tokens'; import { cdnModule } from './modules/cdn'; import { AwsClient } from './modules/cdn/providers/aws'; import { CDN_CONFIG, CDNConfig } from './modules/cdn/providers/tokens'; import { collectionModule } from './modules/collection'; +import { commerceModule } from './modules/commerce'; +import { + CommerceConfig, + provideCommerceConfig, +} from './modules/commerce/providers/commerce-client'; import { integrationsModule } from './modules/integrations'; import { GITHUB_APP_CONFIG, @@ -33,11 +36,6 @@ import { SchemaPolicyServiceConfig, } from './modules/policy/providers/tokens'; import { projectModule } from './modules/project'; -import { rateLimitModule } from './modules/rate-limit'; -import { - RATE_LIMIT_SERVICE_CONFIG, - RateLimitServiceConfig, -} from './modules/rate-limit/providers/tokens'; import { schemaModule } from './modules/schema'; import { ArtifactStorageWriter } from './modules/schema/providers/artifact-storage-writer'; import { provideSchemaModuleConfig, SchemaModuleConfig } from './modules/schema/providers/config'; @@ -51,6 +49,10 @@ import { DistributedCache } from './modules/shared/providers/distributed-cache'; import { Emails, EMAILS_ENDPOINT } from './modules/shared/providers/emails'; import { HttpClient } from './modules/shared/providers/http-client'; import { IdTranslator } from './modules/shared/providers/id-translator'; +import { + InMemoryRateLimiter, + InMemoryRateLimitStore, +} from './modules/shared/providers/in-memory-rate-limiter'; import { Logger } from './modules/shared/providers/logger'; import { Mutex } from './modules/shared/providers/mutex'; import { PG_POOL_CONFIG } from './modules/shared/providers/pg-pool'; @@ -64,11 +66,6 @@ import { provideSupportConfig, SupportConfig } from './modules/support/providers import { targetModule } from './modules/target'; import { tokenModule } from './modules/token'; import { TOKENS_CONFIG, TokensConfig } from './modules/token/providers/tokens'; -import { usageEstimationModule } from './modules/usage-estimation'; -import { - USAGE_ESTIMATION_SERVICE_CONFIG, - UsageEstimationServiceConfig, -} from './modules/usage-estimation/providers/tokens'; const modules = [ sharedModule, @@ -84,9 +81,7 @@ const modules = [ alertsModule, cdnModule, adminModule, - usageEstimationModule, - rateLimitModule, - billingModule, + commerceModule, oidcIntegrationsModule, schemaPolicyModule, collectionModule, @@ -96,11 +91,10 @@ const modules = [ export function createRegistry({ app, + commerce, tokens, webhooks, schemaService, - usageEstimationService, - rateLimitService, schemaPolicyService, logger, storage, @@ -112,7 +106,6 @@ export function createRegistry({ s3Mirror, s3AuditLogs, encryptionSecret, - billing, schemaConfig, supportConfig, emailsEndpoint, @@ -124,11 +117,10 @@ export function createRegistry({ storage: Storage; clickHouse: ClickHouseConfig; redis: Redis; + commerce: CommerceConfig; tokens: TokensConfig; webhooks: WebhooksConfig; schemaService: SchemaServiceConfig; - usageEstimationService: UsageEstimationServiceConfig; - rateLimitService: RateLimitServiceConfig; schemaPolicyService: SchemaPolicyServiceConfig; githubApp: GitHubApplicationConfig | null; cdn: CDNConfig | null; @@ -157,7 +149,6 @@ export function createRegistry({ app: { baseUrl: string; } | null; - billing: BillingConfig; schemaConfig: SchemaModuleConfig; supportConfig: SupportConfig | null; emailsEndpoint?: string; @@ -214,6 +205,8 @@ export function createRegistry({ DistributedCache, CryptoProvider, Emails, + InMemoryRateLimitStore, + InMemoryRateLimiter, { provide: AuditLogS3Config, useValue: auditLogS3Config, @@ -242,11 +235,7 @@ export function createRegistry({ useValue: tokens, scope: Scope.Singleton, }, - { - provide: BILLING_CONFIG, - useValue: billing, - scope: Scope.Singleton, - }, + { provide: WEBHOOKS_CONFIG, useValue: webhooks, @@ -257,16 +246,6 @@ export function createRegistry({ useValue: schemaService, scope: Scope.Singleton, }, - { - provide: USAGE_ESTIMATION_SERVICE_CONFIG, - useValue: usageEstimationService, - scope: Scope.Singleton, - }, - { - provide: RATE_LIMIT_SERVICE_CONFIG, - useValue: rateLimitService, - scope: Scope.Singleton, - }, { provide: SCHEMA_POLICY_SERVICE_CONFIG, useValue: schemaPolicyService, @@ -315,6 +294,7 @@ export function createRegistry({ { provide: PUB_SUB_CONFIG, scope: Scope.Singleton, useValue: pubSub }, encryptionSecretProvider(encryptionSecret), provideSchemaModuleConfig(schemaConfig), + provideCommerceConfig(commerce), { provide: Session, useFactory(context: { session: Session }) { diff --git a/packages/services/api/src/modules/billing/index.ts b/packages/services/api/src/modules/billing/index.ts deleted file mode 100644 index 85650ed0dc..0000000000 --- a/packages/services/api/src/modules/billing/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createModule } from 'graphql-modules'; -import { AuditLogManager } from '../audit-logs/providers/audit-logs-manager'; -import { BillingProvider } from './providers/billing.provider'; -import { resolvers } from './resolvers.generated'; -import typeDefs from './module.graphql'; - -export const billingModule = createModule({ - id: 'billing', - dirname: __dirname, - typeDefs, - resolvers, - providers: [BillingProvider, AuditLogManager], -}); diff --git a/packages/services/api/src/modules/billing/module.graphql.mappers.ts b/packages/services/api/src/modules/billing/module.graphql.mappers.ts deleted file mode 100644 index 92a84198e2..0000000000 --- a/packages/services/api/src/modules/billing/module.graphql.mappers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { StripeTypes } from '@hive/stripe-billing'; - -export type BillingPaymentMethodMapper = StripeTypes.PaymentMethod.Card; -export type BillingDetailsMapper = StripeTypes.PaymentMethod.BillingDetails; -export type BillingInvoiceMapper = StripeTypes.Invoice | StripeTypes.UpcomingInvoice; diff --git a/packages/services/api/src/modules/billing/providers/tokens.ts b/packages/services/api/src/modules/billing/providers/tokens.ts deleted file mode 100644 index 5503aa069b..0000000000 --- a/packages/services/api/src/modules/billing/providers/tokens.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { InjectionToken } from 'graphql-modules'; - -export type BillingConfig = { - endpoint: string | null; -}; - -export const BILLING_CONFIG = new InjectionToken('billing-config'); diff --git a/packages/services/api/src/modules/billing/constants.ts b/packages/services/api/src/modules/commerce/constants.ts similarity index 100% rename from packages/services/api/src/modules/billing/constants.ts rename to packages/services/api/src/modules/commerce/constants.ts diff --git a/packages/services/api/src/modules/commerce/index.ts b/packages/services/api/src/modules/commerce/index.ts new file mode 100644 index 0000000000..ce79426b7f --- /dev/null +++ b/packages/services/api/src/modules/commerce/index.ts @@ -0,0 +1,22 @@ +import { createModule } from 'graphql-modules'; +import { AuditLogManager } from '../audit-logs/providers/audit-logs-manager'; +import { BillingProvider } from './providers/billing.provider'; +import { provideCommerceClient } from './providers/commerce-client'; +import { RateLimitProvider } from './providers/rate-limit.provider'; +import { UsageEstimationProvider } from './providers/usage-estimation.provider'; +import { resolvers } from './resolvers.generated'; +import typeDefs from './module.graphql'; + +export const commerceModule = createModule({ + id: 'commerce', + dirname: __dirname, + typeDefs, + resolvers, + providers: [ + BillingProvider, + RateLimitProvider, + UsageEstimationProvider, + AuditLogManager, + provideCommerceClient(), + ], +}); diff --git a/packages/services/api/src/modules/commerce/module.graphql.mappers.ts b/packages/services/api/src/modules/commerce/module.graphql.mappers.ts new file mode 100644 index 0000000000..6f852bd245 --- /dev/null +++ b/packages/services/api/src/modules/commerce/module.graphql.mappers.ts @@ -0,0 +1,5 @@ +import type { Stripe } from 'stripe'; + +export type BillingPaymentMethodMapper = Stripe.PaymentMethod.Card; +export type BillingDetailsMapper = Stripe.PaymentMethod.BillingDetails; +export type BillingInvoiceMapper = Stripe.Invoice | Stripe.UpcomingInvoice; diff --git a/packages/services/api/src/modules/billing/module.graphql.ts b/packages/services/api/src/modules/commerce/module.graphql.ts similarity index 84% rename from packages/services/api/src/modules/billing/module.graphql.ts rename to packages/services/api/src/modules/commerce/module.graphql.ts index 46417ad26e..d2bb26b667 100644 --- a/packages/services/api/src/modules/billing/module.graphql.ts +++ b/packages/services/api/src/modules/commerce/module.graphql.ts @@ -6,6 +6,7 @@ export default gql` billingConfiguration: BillingConfiguration! viewerCanDescribeBilling: Boolean! viewerCanModifyBilling: Boolean! + rateLimit: RateLimit! } type BillingConfiguration { @@ -54,6 +55,7 @@ export default gql` extend type Query { billingPlans: [BillingPlan!]! + usageEstimation(input: UsageEstimationInput!): UsageEstimation! } type BillingPlan { @@ -106,4 +108,24 @@ export default gql` newPlan: BillingPlanType! organization: Organization! } + + type RateLimit { + limitedForOperations: Boolean! + operations: SafeInt! + retentionInDays: Int! + } + + input RateLimitInput { + operations: SafeInt! + } + + input UsageEstimationInput { + year: Int! + month: Int! + organizationSlug: String! + } + + type UsageEstimation { + operations: SafeInt! + } `; diff --git a/packages/services/api/src/modules/billing/providers/billing.provider.ts b/packages/services/api/src/modules/commerce/providers/billing.provider.ts similarity index 59% rename from packages/services/api/src/modules/billing/providers/billing.provider.ts rename to packages/services/api/src/modules/commerce/providers/billing.provider.ts index e0c63a1fb6..c194cb2497 100644 --- a/packages/services/api/src/modules/billing/providers/billing.provider.ts +++ b/packages/services/api/src/modules/commerce/providers/billing.provider.ts @@ -1,52 +1,48 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; -import type { StripeBillingApi, StripeBillingApiInput } from '@hive/stripe-billing'; -import { createTRPCProxyClient, httpLink } from '@trpc/client'; +import LRU from 'lru-cache'; import { OrganizationBilling } from '../../../shared/entities'; import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder'; import { Session } from '../../auth/lib/authz'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; import { Storage } from '../../shared/providers/storage'; -import type { BillingConfig } from './tokens'; -import { BILLING_CONFIG } from './tokens'; +import { + COMMERCE_TRPC_CLIENT, + type CommerceTrpcClient, + type CommerceTrpcClientInputs, +} from './commerce-client'; + +type BillingInput = CommerceTrpcClientInputs['stripeBilling']; @Injectable({ - global: true, scope: Scope.Operation, + global: true, }) export class BillingProvider { private logger: Logger; - private billingService; enabled = false; constructor( + @Inject(COMMERCE_TRPC_CLIENT) private client: CommerceTrpcClient, logger: Logger, private auditLog: AuditLogRecorder, private storage: Storage, private idTranslator: IdTranslator, private session: Session, - @Inject(BILLING_CONFIG) billingConfig: BillingConfig, ) { - this.logger = logger.child({ source: 'BillingProvider' }); - this.billingService = billingConfig.endpoint - ? createTRPCProxyClient({ - links: [httpLink({ url: `${billingConfig.endpoint}/trpc`, fetch })], - }) - : null; - - if (billingConfig.endpoint) { - this.enabled = true; - } + this.logger = logger.child({ source: 'CommerceProvider' }); + + this.enabled = !!client; } - async upgradeToPro(input: StripeBillingApiInput['createSubscriptionForOrganization']) { + async upgradeToPro(input: BillingInput['createSubscriptionForOrganization']) { this.logger.debug('Upgrading to PRO (input=%o)', input); - if (!this.billingService) { + if (!this.client) { throw new Error(`Billing service is not configured!`); } - const result = this.billingService.createSubscriptionForOrganization.mutate(input); + const result = this.client.stripeBilling.createSubscriptionForOrganization.mutate(input); await this.auditLog.record({ eventType: 'SUBSCRIPTION_CREATED', @@ -62,21 +58,21 @@ export class BillingProvider { return result; } - syncOrganization(input: StripeBillingApiInput['syncOrganizationToStripe']) { - if (!this.billingService) { + syncOrganization(input: BillingInput['syncOrganizationToStripe']) { + if (!this.client) { throw new Error(`Billing service is not configured!`); } - return this.billingService.syncOrganizationToStripe.mutate(input); + return this.client.stripeBilling.syncOrganizationToStripe.mutate(input); } async getAvailablePrices() { this.logger.debug('Getting available prices'); - if (!this.billingService) { + if (!this.client) { return null; } - return await this.billingService.availablePrices.query(); + return await this.client.stripeBilling.availablePrices.query(); } async getOrganizationBillingParticipant(selector: { @@ -89,36 +85,36 @@ export class BillingProvider { }); } - getActiveSubscription(input: StripeBillingApiInput['activeSubscription']) { + getActiveSubscription(input: BillingInput['activeSubscription']) { this.logger.debug('Fetching active subscription (input=%o)', input); - if (!this.billingService) { + if (!this.client) { throw new Error(`Billing service is not configured!`); } - return this.billingService.activeSubscription.query(input); + return this.client.stripeBilling.activeSubscription.query(input); } - invoices(input: StripeBillingApiInput['invoices']) { + invoices(input: BillingInput['invoices']) { this.logger.debug('Fetching invoices (input=%o)', input); - if (!this.billingService) { + if (!this.client) { throw new Error(`Billing service is not configured!`); } - return this.billingService.invoices.query(input); + return this.client.stripeBilling.invoices.query(input); } - upcomingInvoice(input: StripeBillingApiInput['upcomingInvoice']) { + upcomingInvoice(input: BillingInput['upcomingInvoice']) { this.logger.debug('Fetching upcoming invoices (input=%o)', input); - if (!this.billingService) { + if (!this.client) { throw new Error(`Billing service is not configured!`); } - return this.billingService.upcomingInvoice.query(input); + return this.client.stripeBilling.upcomingInvoice.query(input); } - async downgradeToHobby(input: StripeBillingApiInput['cancelSubscriptionForOrganization']) { + async downgradeToHobby(input: BillingInput['cancelSubscriptionForOrganization']) { this.logger.debug('Downgrading to Hobby (input=%o)', input); - if (!this.billingService) { + if (!this.client) { throw new Error(`Billing service is not configured!`); } @@ -131,13 +127,13 @@ export class BillingProvider { }, }); - return await this.billingService.cancelSubscriptionForOrganization.mutate(input); + return await this.client.stripeBilling.cancelSubscriptionForOrganization.mutate(input); } - public async generateStripePortalLink(args: { organizationSlug: string }) { + async generateStripePortalLink(args: { organizationSlug: string }) { this.logger.debug('Generating Stripe portal link for id:' + args.organizationSlug); - if (!this.billingService) { + if (!this.client) { throw new Error(`Billing service is not configured!`); } @@ -153,7 +149,7 @@ export class BillingProvider { }, }); - return await this.billingService.generateStripePortalLink.mutate({ + return await this.client.stripeBilling.generateStripePortalLink.mutate({ organizationId, }); } diff --git a/packages/services/api/src/modules/commerce/providers/commerce-client.ts b/packages/services/api/src/modules/commerce/providers/commerce-client.ts new file mode 100644 index 0000000000..c32bedae7e --- /dev/null +++ b/packages/services/api/src/modules/commerce/providers/commerce-client.ts @@ -0,0 +1,38 @@ +import { FactoryProvider, InjectionToken, Scope, ValueProvider } from 'graphql-modules'; +import type { CommerceRouter } from '@hive/commerce'; +import { createTRPCProxyClient, httpLink, type CreateTRPCProxyClient } from '@trpc/client'; +import type { inferRouterInputs } from '@trpc/server'; + +export type CommerceTrpcClient = CreateTRPCProxyClient | null; +export type CommerceTrpcClientInputs = inferRouterInputs; +export type CommerceConfig = { + endpoint: string | null; +}; + +export const COMMERCE_TRPC_CLIENT = new InjectionToken('commerce-trpc-client'); +export const COMMERCE_CONFIG = new InjectionToken('commerce-config'); + +export function provideCommerceConfig(config: CommerceConfig): ValueProvider { + return { + provide: COMMERCE_CONFIG, + useValue: config, + scope: Scope.Singleton, + }; +} + +export function provideCommerceClient(): FactoryProvider { + return { + provide: COMMERCE_TRPC_CLIENT, + scope: Scope.Singleton, + deps: [COMMERCE_CONFIG], + useFactory(config: CommerceConfig) { + if (!config.endpoint) { + return null; + } + + return createTRPCProxyClient({ + links: [httpLink({ url: `${config.endpoint}/trpc`, fetch })], + }); + }, + }; +} diff --git a/packages/services/api/src/modules/rate-limit/providers/rate-limit.provider.ts b/packages/services/api/src/modules/commerce/providers/rate-limit.provider.ts similarity index 61% rename from packages/services/api/src/modules/rate-limit/providers/rate-limit.provider.ts rename to packages/services/api/src/modules/commerce/providers/rate-limit.provider.ts index f5c82e495e..41faaa09ba 100644 --- a/packages/services/api/src/modules/rate-limit/providers/rate-limit.provider.ts +++ b/packages/services/api/src/modules/commerce/providers/rate-limit.provider.ts @@ -1,10 +1,13 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import LRU from 'lru-cache'; -import type { RateLimitApi, RateLimitApiInput } from '@hive/rate-limit'; -import { createTRPCProxyClient, httpLink } from '@trpc/client'; import { Logger } from '../../shared/providers/logger'; -import type { RateLimitServiceConfig } from './tokens'; -import { RATE_LIMIT_SERVICE_CONFIG } from './tokens'; +import { + COMMERCE_TRPC_CLIENT, + type CommerceTrpcClient, + type CommerceTrpcClientInputs, +} from './commerce-client'; + +type RateLimitApiInput = CommerceTrpcClientInputs['rateLimit']; const RETENTION_CACHE_TTL_IN_SECONDS = 120; @@ -14,7 +17,6 @@ const RETENTION_CACHE_TTL_IN_SECONDS = 120; }) export class RateLimitProvider { private logger: Logger; - private rateLimit; private retentionCache = new LRU({ max: 500, ttl: RETENTION_CACHE_TTL_IN_SECONDS * 1000, @@ -23,24 +25,13 @@ export class RateLimitProvider { constructor( logger: Logger, - @Inject(RATE_LIMIT_SERVICE_CONFIG) - rateLimitServiceConfig: RateLimitServiceConfig, + @Inject(COMMERCE_TRPC_CLIENT) private client: CommerceTrpcClient, ) { this.logger = logger.child({ service: 'RateLimitProvider' }); - this.rateLimit = rateLimitServiceConfig.endpoint - ? createTRPCProxyClient({ - links: [ - httpLink({ - url: `${rateLimitServiceConfig.endpoint}/trpc`, - fetch, - }), - ], - }) - : null; } async checkRateLimit(input: RateLimitApiInput['checkRateLimit']) { - if (this.rateLimit === null) { + if (this.client === null) { this.logger.warn( `Unable to check rate-limit for input: %o , service information is not available`, input, @@ -54,11 +45,11 @@ export class RateLimitProvider { this.logger.debug(`Checking rate limit for target id="${input.id}", type=${input.type}`); - return await this.rateLimit.checkRateLimit.query(input); + return await this.client.rateLimit.checkRateLimit.query(input); } async getRetention(input: RateLimitApiInput['getRetention']) { - if (this.rateLimit === null) { + if (this.client === null) { return null; } @@ -68,7 +59,7 @@ export class RateLimitProvider { this.logger.debug(`Fetching retention for target id="${input.targetId}"`); - const value = await this.rateLimit.getRetention.query(input); + const value = await this.client.rateLimit.getRetention.query(input); this.retentionCache.set(input.targetId, value); return value; diff --git a/packages/services/api/src/modules/usage-estimation/providers/usage-estimation.provider.ts b/packages/services/api/src/modules/commerce/providers/usage-estimation.provider.ts similarity index 70% rename from packages/services/api/src/modules/usage-estimation/providers/usage-estimation.provider.ts rename to packages/services/api/src/modules/commerce/providers/usage-estimation.provider.ts index 73c66859e9..9cb35b513e 100644 --- a/packages/services/api/src/modules/usage-estimation/providers/usage-estimation.provider.ts +++ b/packages/services/api/src/modules/commerce/providers/usage-estimation.provider.ts @@ -1,39 +1,24 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import { traceFn } from '@hive/service-common'; -import type { UsageEstimatorApi } from '@hive/usage-estimator'; -import { createTRPCProxyClient, httpLink } from '@trpc/client'; import { Session } from '../../auth/lib/authz'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; -import type { UsageEstimationServiceConfig } from './tokens'; -import { USAGE_ESTIMATION_SERVICE_CONFIG } from './tokens'; +import { COMMERCE_TRPC_CLIENT, type CommerceTrpcClient } from './commerce-client'; @Injectable({ scope: Scope.Operation, - global: true, }) export class UsageEstimationProvider { private logger: Logger; - private usageEstimator; constructor( logger: Logger, - @Inject(USAGE_ESTIMATION_SERVICE_CONFIG) - usageEstimationConfig: UsageEstimationServiceConfig, + @Inject(COMMERCE_TRPC_CLIENT) + private client: CommerceTrpcClient, private idTranslator: IdTranslator, private session: Session, ) { this.logger = logger.child({ service: 'UsageEstimationProvider' }); - this.usageEstimator = usageEstimationConfig.endpoint - ? createTRPCProxyClient({ - links: [ - httpLink({ - url: `${usageEstimationConfig.endpoint}/trpc`, - fetch, - }), - ], - }) - : null; } @traceFn('UsageEstimation.estimateOperations', { @@ -51,13 +36,13 @@ export class UsageEstimationProvider { }): Promise { this.logger.debug('Estimation operations, input: %o', input); - if (!this.usageEstimator) { + if (!this.client) { this.logger.warn('Usage estimator is not available due to missing configuration'); return null; } - const result = await this.usageEstimator.estimateOperationsForOrganization.query({ + const result = await this.client.usageEstimator.estimateOperationsForOrganization.query({ organizationId: input.organizationId, year: input.year, month: input.month, diff --git a/packages/services/api/src/modules/billing/resolvers/BillingDetails.ts b/packages/services/api/src/modules/commerce/resolvers/BillingDetails.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/BillingDetails.ts rename to packages/services/api/src/modules/commerce/resolvers/BillingDetails.ts diff --git a/packages/services/api/src/modules/billing/resolvers/BillingInvoice.ts b/packages/services/api/src/modules/commerce/resolvers/BillingInvoice.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/BillingInvoice.ts rename to packages/services/api/src/modules/commerce/resolvers/BillingInvoice.ts diff --git a/packages/services/api/src/modules/billing/resolvers/BillingPaymentMethod.ts b/packages/services/api/src/modules/commerce/resolvers/BillingPaymentMethod.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/BillingPaymentMethod.ts rename to packages/services/api/src/modules/commerce/resolvers/BillingPaymentMethod.ts diff --git a/packages/services/api/src/modules/billing/resolvers/Mutation/downgradeToHobby.ts b/packages/services/api/src/modules/commerce/resolvers/Mutation/downgradeToHobby.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/Mutation/downgradeToHobby.ts rename to packages/services/api/src/modules/commerce/resolvers/Mutation/downgradeToHobby.ts diff --git a/packages/services/api/src/modules/billing/resolvers/Mutation/generateStripePortalLink.ts b/packages/services/api/src/modules/commerce/resolvers/Mutation/generateStripePortalLink.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/Mutation/generateStripePortalLink.ts rename to packages/services/api/src/modules/commerce/resolvers/Mutation/generateStripePortalLink.ts diff --git a/packages/services/api/src/modules/billing/resolvers/Mutation/updateOrgRateLimit.ts b/packages/services/api/src/modules/commerce/resolvers/Mutation/updateOrgRateLimit.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/Mutation/updateOrgRateLimit.ts rename to packages/services/api/src/modules/commerce/resolvers/Mutation/updateOrgRateLimit.ts diff --git a/packages/services/api/src/modules/billing/resolvers/Mutation/upgradeToPro.ts b/packages/services/api/src/modules/commerce/resolvers/Mutation/upgradeToPro.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/Mutation/upgradeToPro.ts rename to packages/services/api/src/modules/commerce/resolvers/Mutation/upgradeToPro.ts diff --git a/packages/services/api/src/modules/billing/resolvers/Organization.ts b/packages/services/api/src/modules/commerce/resolvers/Organization.ts similarity index 79% rename from packages/services/api/src/modules/billing/resolvers/Organization.ts rename to packages/services/api/src/modules/commerce/resolvers/Organization.ts index 9ac21565f6..4b8e4c8cf9 100644 --- a/packages/services/api/src/modules/billing/resolvers/Organization.ts +++ b/packages/services/api/src/modules/commerce/resolvers/Organization.ts @@ -1,10 +1,13 @@ +import { Logger } from '../../shared/providers/logger'; import { BillingProvider } from '../providers/billing.provider'; +import { RateLimitProvider } from '../providers/rate-limit.provider'; import type { BillingPlanType, OrganizationResolvers } from './../../../__generated__/types'; export const Organization: Pick< OrganizationResolvers, | 'billingConfiguration' | 'plan' + | 'rateLimit' | 'viewerCanDescribeBilling' | 'viewerCanModifyBilling' | '__isTypeOf' @@ -110,4 +113,28 @@ export const Organization: Pick< }, }); }, + rateLimit: async (org, _args, { injector }) => { + let limitedForOperations = false; + const logger = injector.get(Logger); + + try { + const operationsRateLimit = await injector.get(RateLimitProvider).checkRateLimit({ + entityType: 'organization', + id: org.id, + type: 'operations-reporting', + token: null, + }); + + logger.debug('Fetched rate-limit info:', { orgId: org.id, operationsRateLimit }); + limitedForOperations = operationsRateLimit.usagePercentage >= 1; + } catch (e) { + logger.error('Failed to fetch rate-limit info:', org.id, e); + } + + return { + limitedForOperations, + operations: org.monthlyRateLimit.operations, + retentionInDays: org.monthlyRateLimit.retentionInDays, + }; + }, }; diff --git a/packages/services/api/src/modules/billing/resolvers/Query/billingPlans.ts b/packages/services/api/src/modules/commerce/resolvers/Query/billingPlans.ts similarity index 100% rename from packages/services/api/src/modules/billing/resolvers/Query/billingPlans.ts rename to packages/services/api/src/modules/commerce/resolvers/Query/billingPlans.ts diff --git a/packages/services/api/src/modules/usage-estimation/resolvers/Query/usageEstimation.ts b/packages/services/api/src/modules/commerce/resolvers/Query/usageEstimation.ts similarity index 100% rename from packages/services/api/src/modules/usage-estimation/resolvers/Query/usageEstimation.ts rename to packages/services/api/src/modules/commerce/resolvers/Query/usageEstimation.ts diff --git a/packages/services/api/src/modules/organization/providers/organization-manager.ts b/packages/services/api/src/modules/organization/providers/organization-manager.ts index 40271ecd8e..cb461a4c04 100644 --- a/packages/services/api/src/modules/organization/providers/organization-manager.ts +++ b/packages/services/api/src/modules/organization/providers/organization-manager.ts @@ -7,7 +7,7 @@ import { cache } from '../../../shared/helpers'; import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder'; import { Session } from '../../auth/lib/authz'; import { AuthManager } from '../../auth/providers/auth-manager'; -import { BillingProvider } from '../../billing/providers/billing.provider'; +import { BillingProvider } from '../../commerce/providers/billing.provider'; import { OIDCIntegrationsProvider } from '../../oidc-integrations/providers/oidc-integrations.provider'; import { Emails, mjml } from '../../shared/providers/emails'; import { IdTranslator } from '../../shared/providers/id-translator'; diff --git a/packages/services/api/src/modules/organization/resolvers/Mutation/inviteToOrganizationByEmail.ts b/packages/services/api/src/modules/organization/resolvers/Mutation/inviteToOrganizationByEmail.ts index 2e5c3d0617..839c4d2d57 100644 --- a/packages/services/api/src/modules/organization/resolvers/Mutation/inviteToOrganizationByEmail.ts +++ b/packages/services/api/src/modules/organization/resolvers/Mutation/inviteToOrganizationByEmail.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { InMemoryRateLimiter } from '../../../rate-limit/providers/in-memory-rate-limiter'; import { IdTranslator } from '../../../shared/providers/id-translator'; +import { InMemoryRateLimiter } from '../../../shared/providers/in-memory-rate-limiter'; import { OrganizationManager } from '../../providers/organization-manager'; import type { MutationResolvers } from './../../../../__generated__/types'; diff --git a/packages/services/api/src/modules/rate-limit/index.ts b/packages/services/api/src/modules/rate-limit/index.ts deleted file mode 100644 index 44df668452..0000000000 --- a/packages/services/api/src/modules/rate-limit/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createModule } from 'graphql-modules'; -import { InMemoryRateLimiter, InMemoryRateLimitStore } from './providers/in-memory-rate-limiter'; -import { RateLimitProvider } from './providers/rate-limit.provider'; -import { resolvers } from './resolvers.generated'; -import typeDefs from './module.graphql'; - -export const rateLimitModule = createModule({ - id: 'rate-limit', - dirname: __dirname, - typeDefs, - resolvers, - providers: [RateLimitProvider, InMemoryRateLimitStore, InMemoryRateLimiter], -}); diff --git a/packages/services/api/src/modules/rate-limit/module.graphql.ts b/packages/services/api/src/modules/rate-limit/module.graphql.ts deleted file mode 100644 index aac3b5ff79..0000000000 --- a/packages/services/api/src/modules/rate-limit/module.graphql.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { gql } from 'graphql-modules'; - -export default gql` - type RateLimit { - limitedForOperations: Boolean! - operations: SafeInt! - retentionInDays: Int! - } - - input RateLimitInput { - operations: SafeInt! - } - - extend type Organization { - rateLimit: RateLimit! - } -`; diff --git a/packages/services/api/src/modules/rate-limit/providers/tokens.ts b/packages/services/api/src/modules/rate-limit/providers/tokens.ts deleted file mode 100644 index c9c8252e7c..0000000000 --- a/packages/services/api/src/modules/rate-limit/providers/tokens.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { InjectionToken } from 'graphql-modules'; - -export interface RateLimitServiceConfig { - endpoint: string | null; -} - -export const RATE_LIMIT_SERVICE_CONFIG = new InjectionToken( - 'rate-limit-service-config', -); diff --git a/packages/services/api/src/modules/rate-limit/resolvers/Organization.ts b/packages/services/api/src/modules/rate-limit/resolvers/Organization.ts deleted file mode 100644 index 75d4f76589..0000000000 --- a/packages/services/api/src/modules/rate-limit/resolvers/Organization.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Logger } from '../../shared/providers/logger'; -import { RateLimitProvider } from '../providers/rate-limit.provider'; -import type { OrganizationResolvers } from './../../../__generated__/types'; - -export const Organization: Pick = { - rateLimit: async (org, _args, { injector }) => { - let limitedForOperations = false; - const logger = injector.get(Logger); - - try { - const operationsRateLimit = await injector.get(RateLimitProvider).checkRateLimit({ - entityType: 'organization', - id: org.id, - type: 'operations-reporting', - token: null, - }); - - logger.debug('Fetched rate-limit info:', { orgId: org.id, operationsRateLimit }); - limitedForOperations = operationsRateLimit.usagePercentage >= 1; - } catch (e) { - logger.error('Failed to fetch rate-limit info:', org.id, e); - } - - return { - limitedForOperations, - operations: org.monthlyRateLimit.operations, - retentionInDays: org.monthlyRateLimit.retentionInDays, - }; - }, -}; diff --git a/packages/services/api/src/modules/schema/providers/schema-publisher.ts b/packages/services/api/src/modules/schema/providers/schema-publisher.ts index 23b660ab8e..5f397f5677 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -21,12 +21,12 @@ import { isGitHubRepositoryString } from '../../../shared/is-github-repository-s import { bolderize } from '../../../shared/markdown'; import { AlertsManager } from '../../alerts/providers/alerts-manager'; import { InsufficientPermissionError, Session } from '../../auth/lib/authz'; +import { RateLimitProvider } from '../../commerce/providers/rate-limit.provider'; import { GitHubIntegrationManager, type GitHubCheckRun, } from '../../integrations/providers/github-integration-manager'; import { OperationsReader } from '../../operations/providers/operations-reader'; -import { RateLimitProvider } from '../../rate-limit/providers/rate-limit.provider'; import { DistributedCache } from '../../shared/providers/distributed-cache'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; diff --git a/packages/services/api/src/modules/rate-limit/providers/in-memory-rate-limiter.ts b/packages/services/api/src/modules/shared/providers/in-memory-rate-limiter.ts similarity index 98% rename from packages/services/api/src/modules/rate-limit/providers/in-memory-rate-limiter.ts rename to packages/services/api/src/modules/shared/providers/in-memory-rate-limiter.ts index 29e5f8981a..870a49ab9e 100644 --- a/packages/services/api/src/modules/rate-limit/providers/in-memory-rate-limiter.ts +++ b/packages/services/api/src/modules/shared/providers/in-memory-rate-limiter.ts @@ -2,7 +2,7 @@ import { Injectable, Scope } from 'graphql-modules'; import LRU from 'lru-cache'; import { HiveError } from '../../../shared/errors'; import { Session } from '../../auth/lib/authz'; -import { Logger } from '../../shared/providers/logger'; +import { Logger } from './logger'; @Injectable({ scope: Scope.Singleton, diff --git a/packages/services/api/src/modules/usage-estimation/index.ts b/packages/services/api/src/modules/usage-estimation/index.ts deleted file mode 100644 index a472344361..0000000000 --- a/packages/services/api/src/modules/usage-estimation/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createModule } from 'graphql-modules'; -import { UsageEstimationProvider } from './providers/usage-estimation.provider'; -import { resolvers } from './resolvers.generated'; -import typeDefs from './module.graphql'; - -export const usageEstimationModule = createModule({ - id: 'usage-estimation', - dirname: __dirname, - typeDefs, - resolvers, - providers: [UsageEstimationProvider], -}); diff --git a/packages/services/api/src/modules/usage-estimation/module.graphql.ts b/packages/services/api/src/modules/usage-estimation/module.graphql.ts deleted file mode 100644 index a7c2b3d3ad..0000000000 --- a/packages/services/api/src/modules/usage-estimation/module.graphql.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { gql } from 'graphql-modules'; - -export default gql` - extend type Query { - usageEstimation(input: UsageEstimationInput!): UsageEstimation! - } - - input UsageEstimationInput { - year: Int! - month: Int! - organizationSlug: String! - } - - type UsageEstimation { - operations: SafeInt! - } -`; diff --git a/packages/services/api/src/modules/usage-estimation/providers/tokens.ts b/packages/services/api/src/modules/usage-estimation/providers/tokens.ts deleted file mode 100644 index f154b9b718..0000000000 --- a/packages/services/api/src/modules/usage-estimation/providers/tokens.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { InjectionToken } from 'graphql-modules'; - -export interface UsageEstimationServiceConfig { - endpoint: string | null; -} - -export const USAGE_ESTIMATION_SERVICE_CONFIG = new InjectionToken( - 'usage-estimation-service-config', -); diff --git a/packages/services/usage-estimator/.env.template b/packages/services/commerce/.env.template similarity index 61% rename from packages/services/usage-estimator/.env.template rename to packages/services/commerce/.env.template index ceaaa83131..8757592479 100644 --- a/packages/services/usage-estimator/.env.template +++ b/packages/services/commerce/.env.template @@ -1,12 +1,15 @@ +PORT=4013 +OPENTELEMETRY_COLLECTOR_ENDPOINT="" CLICKHOUSE_PROTOCOL="http" CLICKHOUSE_HOST="localhost" CLICKHOUSE_PORT="8123" CLICKHOUSE_USERNAME="test" CLICKHOUSE_PASSWORD="test" -PORT=4011 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DB=registry -OPENTELEMETRY_COLLECTOR_ENDPOINT="" \ No newline at end of file +EMAILS_ENDPOINT="http://localhost:6260" +WEB_APP_URL="http://localhost:3000" +STRIPE_SECRET_KEY="empty" diff --git a/packages/services/commerce/.gitignore b/packages/services/commerce/.gitignore new file mode 100644 index 0000000000..4c9d7c35a4 --- /dev/null +++ b/packages/services/commerce/.gitignore @@ -0,0 +1,4 @@ +*.log +.DS_Store +node_modules +dist diff --git a/packages/services/commerce/LICENSE b/packages/services/commerce/LICENSE new file mode 100644 index 0000000000..1cf5b9c7d2 --- /dev/null +++ b/packages/services/commerce/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 The Guild + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/services/commerce/README.md b/packages/services/commerce/README.md new file mode 100644 index 0000000000..16b2a6622b --- /dev/null +++ b/packages/services/commerce/README.md @@ -0,0 +1,18 @@ +# `@hive/commerce` + +This service takes care of commerce part of Hive Cloud. + +## Configuration + +| Name | Required | Description | Example Value | +| ----------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| `PORT` | No | The port this service is running on. | `4001` | +| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | +| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | +| `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | +| `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) | +| `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `usage-service` | +| `PROMETHEUS_METRICS_PORT` | No | Port on which prometheus metrics are exposed | Defaults to `10254` | +| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) | +| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | +| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | diff --git a/packages/services/stripe-billing/package.json b/packages/services/commerce/package.json similarity index 80% rename from packages/services/stripe-billing/package.json rename to packages/services/commerce/package.json index 156be7e2a7..ff8129e5dd 100644 --- a/packages/services/stripe-billing/package.json +++ b/packages/services/commerce/package.json @@ -1,7 +1,6 @@ { - "name": "@hive/stripe-billing", + "name": "@hive/commerce", "type": "module", - "description": "A microservice for Hive Cloud, that syncs usage information to Stripe (metered billing)", "license": "MIT", "private": true, "scripts": { @@ -10,6 +9,8 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { + "@hive/api": "workspace:*", + "@hive/emails": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", "@sentry/node": "7.120.2", @@ -17,7 +18,7 @@ "@trpc/server": "10.45.2", "date-fns": "4.1.0", "dotenv": "16.4.7", - "got": "14.4.5", + "fastify": "4.29.0", "pino-pretty": "11.3.0", "reflect-metadata": "0.2.2", "stripe": "17.5.0", diff --git a/packages/services/commerce/src/api.ts b/packages/services/commerce/src/api.ts new file mode 100644 index 0000000000..ae46a14701 --- /dev/null +++ b/packages/services/commerce/src/api.ts @@ -0,0 +1,12 @@ +import { rateLimitRouter } from './rate-limit'; +import { stripeBillingRouter } from './stripe-billing'; +import { router } from './trpc'; +import { usageEstimatorRouter } from './usage-estimator'; + +export const commerceRouter = router({ + usageEstimator: usageEstimatorRouter, + rateLimit: rateLimitRouter, + stripeBilling: stripeBillingRouter, +}); + +export type CommerceRouter = typeof commerceRouter; diff --git a/packages/services/rate-limit/src/dev.ts b/packages/services/commerce/src/dev.ts similarity index 100% rename from packages/services/rate-limit/src/dev.ts rename to packages/services/commerce/src/dev.ts diff --git a/packages/services/stripe-billing/src/environment.ts b/packages/services/commerce/src/environment.ts similarity index 71% rename from packages/services/stripe-billing/src/environment.ts rename to packages/services/commerce/src/environment.ts index 2e232f53f3..11a1a98963 100644 --- a/packages/services/stripe-billing/src/environment.ts +++ b/packages/services/commerce/src/environment.ts @@ -8,10 +8,12 @@ const numberFromNumberOrNumberString = (input: unknown): number | undefined => { if (isNumberString(input)) return Number(input); }; -const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1)); +export const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1)); -// treat an empty string (`''`) as undefined -const emptyString = (input: T) => { +/** + * treat an empty string (`''`) as undefined + */ +export const emptyString = (input: T) => { return zod.preprocess((value: unknown) => { if (value === '') return undefined; return value; @@ -22,8 +24,6 @@ const EnvironmentModel = zod.object({ PORT: emptyString(NumberFromString.optional()), ENVIRONMENT: emptyString(zod.string().optional()), RELEASE: emptyString(zod.string().optional()), - HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()), - USAGE_ESTIMATOR_ENDPOINT: zod.string().url(), }); const SentryModel = zod.union([ @@ -36,23 +36,10 @@ const SentryModel = zod.union([ }), ]); -const PostgresModel = zod.object({ - POSTGRES_HOST: zod.string(), - POSTGRES_PORT: NumberFromString, - POSTGRES_PASSWORD: emptyString(zod.string().optional()), - POSTGRES_USER: zod.string(), - POSTGRES_DB: zod.string(), - POSTGRES_SSL: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), -}); - -const StripeModel = zod.object({ - STRIPE_SECRET_KEY: zod.string(), - STRIPE_SYNC_INTERVAL_MS: emptyString(NumberFromString.optional()), -}); - const PrometheusModel = zod.object({ PROMETHEUS_METRICS: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()), PROMETHEUS_METRICS_LABEL_INSTANCE: emptyString(zod.string().optional()), + PROMETHEUS_METRICS_PORT: emptyString(NumberFromString.optional()), }); const LogModel = zod.object({ @@ -74,20 +61,48 @@ const LogModel = zod.object({ ), }); -const configs = { - base: EnvironmentModel.safeParse(process.env), +const ClickHouseModel = zod.object({ + CLICKHOUSE_PROTOCOL: zod.union([zod.literal('http'), zod.literal('https')]), + CLICKHOUSE_HOST: zod.string(), + CLICKHOUSE_PORT: NumberFromString, + CLICKHOUSE_USERNAME: zod.string(), + CLICKHOUSE_PASSWORD: zod.string(), +}); - sentry: SentryModel.safeParse(process.env), +const PostgresModel = zod.object({ + POSTGRES_SSL: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), + POSTGRES_HOST: zod.string(), + POSTGRES_PORT: NumberFromString, + POSTGRES_DB: zod.string(), + POSTGRES_USER: zod.string(), + POSTGRES_PASSWORD: emptyString(zod.string().optional()), +}); - postgres: PostgresModel.safeParse(process.env), +const HiveServicesModel = zod.object({ + EMAILS_ENDPOINT: emptyString(zod.string().url().optional()), + WEB_APP_URL: emptyString(zod.string().url().optional()), +}); - stripe: StripeModel.safeParse(process.env), +const RateLimitModel = zod.object({ + LIMIT_CACHE_UPDATE_INTERVAL_MS: emptyString(NumberFromString.optional()), +}); - prometheus: PrometheusModel.safeParse(process.env), +const StripeModel = zod.object({ + STRIPE_SECRET_KEY: zod.string(), + STRIPE_SYNC_INTERVAL_MS: emptyString(NumberFromString.optional()), +}); +const configs = { + base: EnvironmentModel.safeParse(process.env), + sentry: SentryModel.safeParse(process.env), + clickhouse: ClickHouseModel.safeParse(process.env), + postgres: PostgresModel.safeParse(process.env), + prometheus: PrometheusModel.safeParse(process.env), log: LogModel.safeParse(process.env), - tracing: OpenTelemetryConfigurationModel.safeParse(process.env), + hiveServices: HiveServicesModel.safeParse(process.env), + rateLimit: RateLimitModel.safeParse(process.env), + stripe: StripeModel.safeParse(process.env), }; const environmentErrors: Array = []; @@ -112,27 +127,32 @@ function extractConfig(config: zod.SafeParseReturnType val === true); reportReadiness(isReady); void res.status(isReady ? 200 : 400).send(); }, @@ -112,7 +144,7 @@ async function main() { port: env.http.port, host: '::', }); - await estimator.start(); + await Promise.all([usageEstimator.start(), rateLimiter.start(), stripeBilling.start()]); } catch (error) { server.log.fatal(error); Sentry.captureException(error, { diff --git a/packages/services/rate-limit/src/emails.ts b/packages/services/commerce/src/rate-limit/emails.ts similarity index 99% rename from packages/services/rate-limit/src/emails.ts rename to packages/services/commerce/src/rate-limit/emails.ts index 08747e7b5b..72981f5680 100644 --- a/packages/services/rate-limit/src/emails.ts +++ b/packages/services/commerce/src/rate-limit/emails.ts @@ -1,6 +1,6 @@ import type { EmailsApi } from '@hive/emails'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; -import { env } from './environment'; +import { env } from '../environment'; export function createEmailScheduler(config?: { endpoint: string }) { const api = config?.endpoint diff --git a/packages/services/commerce/src/rate-limit/index.ts b/packages/services/commerce/src/rate-limit/index.ts new file mode 100644 index 0000000000..b1d435b128 --- /dev/null +++ b/packages/services/commerce/src/rate-limit/index.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { publicProcedure, router } from '../trpc'; + +const VALIDATION = z + .object({ + id: z.string().min(1), + entityType: z.enum(['organization', 'target']), + type: z.enum(['operations-reporting']), + /** + * Token is optional, and used only when an additional blocking (WAF) process is needed. + */ + token: z.string().nullish().optional(), + }) + .required(); + +export const rateLimitRouter = router({ + getRetention: publicProcedure + .input( + z + .object({ + targetId: z.string().nonempty(), + }) + .required(), + ) + .query(({ ctx, input }) => { + return ctx.rateLimiter.getRetention(input.targetId); + }), + checkRateLimit: publicProcedure.input(VALIDATION).query(({ ctx, input }) => { + return ctx.rateLimiter.checkLimit(input); + }), +}); + +export type RateLimitRouter = typeof rateLimitRouter; diff --git a/packages/services/rate-limit/src/limiter.ts b/packages/services/commerce/src/rate-limit/limiter.ts similarity index 84% rename from packages/services/rate-limit/src/limiter.ts rename to packages/services/commerce/src/rate-limit/limiter.ts index 9e55ffb7f2..041d4c6da4 100644 --- a/packages/services/rate-limit/src/limiter.ts +++ b/packages/services/commerce/src/rate-limit/limiter.ts @@ -1,11 +1,10 @@ import { endOfMonth, startOfMonth } from 'date-fns'; +import type { Storage } from '@hive/api'; import type { ServiceLogger } from '@hive/service-common'; import { traceInline } from '@hive/service-common'; -import { createStorage as createPostgreSQLStorage, Interceptor } from '@hive/storage'; -import type { UsageEstimatorApi } from '@hive/usage-estimator'; import * as Sentry from '@sentry/node'; -import { createTRPCProxyClient, httpLink } from '@trpc/client'; -import type { RateLimitInput } from './api'; +import { UsageEstimator } from '../usage-estimator/estimator'; +// import type { RateLimitInput } from './api'; import { createEmailScheduler } from './emails'; import { rateLimitOperationsEventOrg } from './metrics'; @@ -44,7 +43,7 @@ export type CachedRateLimitInfo = { // to prevent applying lower retention value then expected. const RETENTION_IN_DAYS_FALLBACK = 365; -export type Limiter = ReturnType; +export type RateLimiter = ReturnType; type OrganizationId = string; type TargetId = string; @@ -54,34 +53,15 @@ export function createRateLimiter(config: { rateLimitConfig: { interval: number; }; - rateEstimator: { - endpoint: string; - }; emails?: { endpoint: string; }; - storage: { - connectionString: string; - additionalInterceptors?: Interceptor[]; - }; + usageEstimator: UsageEstimator; + storage: Storage; }) { - const rateEstimator = createTRPCProxyClient({ - links: [ - httpLink({ - url: `${config.rateEstimator.endpoint}/trpc`, - fetch, - }), - ], - }); const emails = createEmailScheduler(config.emails); - const { logger } = config; - const postgres$ = createPostgreSQLStorage( - config.storage.connectionString, - 1, - config.storage.additionalInterceptors, - ); - let initialized = false; + const { logger, usageEstimator, storage } = config; let intervalHandle: ReturnType | null = null; let targetIdToOrgLookup = new Map(); @@ -93,18 +73,17 @@ export function createRateLimiter(config: { startTime: startOfMonth(now), endTime: endOfMonth(now), }; - const windowAsString = { - startTime: startOfMonth(now).toUTCString(), - endTime: endOfMonth(now).toUTCString(), + const timeWindow = { + startTime: startOfMonth(now), + endTime: endOfMonth(now), }; config.logger.info( - `Calculating rate-limit information based on window: ${windowAsString.startTime} -> ${windowAsString.endTime}`, + `Calculating rate-limit information based on window: ${timeWindow.startTime} -> ${timeWindow.endTime}`, ); - const storage = await postgres$; const [records, operations] = await Promise.all([ storage.getGetOrganizationsAndTargetsWithLimitInfo(), - rateEstimator.estimateOperationsForAllTargets.query(windowAsString), + usageEstimator.estimateOperationsForAllTargets(timeWindow), ]); const totalTargets = records.reduce((acc, record) => acc + record.targets.length, 0); @@ -213,10 +192,12 @@ export function createRateLimiter(config: { return orgId ? cachedResult.get(orgId) : undefined; } + let initialized = false; + return { logger, - async readiness() { - return initialized && (await (await postgres$).isReady()); + readiness() { + return initialized; }, getRetention(targetId: string) { const orgData = getOrganizationFromCache(targetId); @@ -236,7 +217,10 @@ export function createRateLimiter(config: { return orgData.retentionInDays; }, - checkLimit(input: RateLimitInput): RateLimitCheckResponse { + checkLimit(input: { + entityType: 'organization' | 'target'; + id: string; + }): RateLimitCheckResponse { const orgId = input.entityType === 'organization' ? input.id : targetIdToOrgLookup.get(input.id); @@ -258,10 +242,7 @@ export function createRateLimiter(config: { return UNKNOWN_RATE_LIMIT_OBJ; } - if (input.type === 'operations-reporting') { - return orgData.operations; - } - return UNKNOWN_RATE_LIMIT_OBJ; + return orgData.operations; }, async start() { logger.info( @@ -294,7 +275,6 @@ export function createRateLimiter(config: { clearInterval(intervalHandle); intervalHandle = null; } - await (await postgres$).destroy(); logger.info('Rate Limiter stopped'); }, }; diff --git a/packages/services/rate-limit/src/metrics.ts b/packages/services/commerce/src/rate-limit/metrics.ts similarity index 100% rename from packages/services/rate-limit/src/metrics.ts rename to packages/services/commerce/src/rate-limit/metrics.ts diff --git a/packages/services/stripe-billing/src/billing-sync.ts b/packages/services/commerce/src/stripe-billing/billing.ts similarity index 80% rename from packages/services/stripe-billing/src/billing-sync.ts rename to packages/services/commerce/src/stripe-billing/billing.ts index 0d7907261b..a4e834b3ca 100644 --- a/packages/services/stripe-billing/src/billing-sync.ts +++ b/packages/services/commerce/src/stripe-billing/billing.ts @@ -1,27 +1,20 @@ import { Stripe } from 'stripe'; +import { Storage } from '@hive/api'; import { ServiceLogger } from '@hive/service-common'; -import { createStorage as createPostgreSQLStorage, Interceptor } from '@hive/storage'; +import type { UsageEstimator } from '../usage-estimator/estimator'; + +export type StripeBilling = ReturnType; export function createStripeBilling(config: { logger: ServiceLogger; - rateEstimator: { - endpoint: string; - }; - storage: { - connectionString: string; - additionalInterceptors?: Interceptor[]; - }; + usageEstimator: UsageEstimator; + storage: Storage; stripe: { token: string; syncIntervalMs: number; }; }) { const logger = config.logger; - const postgres$ = createPostgreSQLStorage( - config.storage.connectionString, - 10, - config.storage.additionalInterceptors, - ); const stripeApi = new Stripe(config.stripe.token, { apiVersion: '2024-12-18.acacia', typescript: true, @@ -71,11 +64,11 @@ export function createStripeBilling(config: { } return { - postgres$, - loadStripeData$, - stripeApi, + storage: config.storage, + stripe: stripeApi, + stripeData$: loadStripeData$, async readiness() { - return await (await postgres$).isReady(); + return true; }, async start() { logger.info( @@ -86,8 +79,6 @@ export function createStripeBilling(config: { logger.info(`Stripe is configured correctly, prices info: %o`, stripeData); }, async stop() { - await (await postgres$).destroy(); - logger.info(`Stripe Billing Sync stopped...`); }, }; diff --git a/packages/services/stripe-billing/src/api.ts b/packages/services/commerce/src/stripe-billing/index.ts similarity index 78% rename from packages/services/stripe-billing/src/api.ts rename to packages/services/commerce/src/stripe-billing/index.ts index add73474bd..fae0b85c34 100644 --- a/packages/services/stripe-billing/src/api.ts +++ b/packages/services/commerce/src/stripe-billing/index.ts @@ -1,38 +1,20 @@ import { addDays, startOfMonth } from 'date-fns'; import { Stripe } from 'stripe'; import { z } from 'zod'; -import { FastifyRequest, handleTRPCError } from '@hive/service-common'; -import { createStorage } from '@hive/storage'; -import type { inferRouterInputs } from '@trpc/server'; -import { initTRPC } from '@trpc/server'; - -export type Context = { - storage$: ReturnType; - stripe: Stripe; - stripeData$: Promise<{ - operationsPrice: Stripe.Price; - basePrice: Stripe.Price; - }>; - req: FastifyRequest; -}; - -export { Stripe as StripeTypes }; - -const t = initTRPC.context().create(); -const procedure = t.procedure.use(handleTRPCError); - -export const stripeBillingApiRouter = t.router({ - availablePrices: procedure.query(async ({ ctx }) => { - return await ctx.stripeData$; +import { publicProcedure, router } from '../trpc'; + +export const stripeBillingRouter = router({ + availablePrices: publicProcedure.query(async ({ ctx }) => { + return await ctx.stripeBilling.stripeData$; }), - invoices: procedure + invoices: publicProcedure .input( z.object({ organizationId: z.string().nonempty(), }), ) .query(async ({ ctx, input }) => { - const storage = await ctx.storage$; + const storage = ctx.stripeBilling.storage; const organizationBillingRecord = await storage.getOrganizationBilling({ organizationId: input.organizationId, }); @@ -41,21 +23,21 @@ export const stripeBillingApiRouter = t.router({ throw new Error(`Organization does not have a subscription record!`); } - const invoices = await ctx.stripe.invoices.list({ + const invoices = await ctx.stripeBilling.stripe.invoices.list({ customer: organizationBillingRecord.externalBillingReference, expand: ['data.charge'], }); return invoices.data; }), - upcomingInvoice: procedure + upcomingInvoice: publicProcedure .input( z.object({ organizationId: z.string().nonempty(), }), ) .query(async ({ ctx, input }) => { - const storage = await ctx.storage$; + const storage = ctx.stripeBilling.storage; const organizationBillingRecord = await storage.getOrganizationBilling({ organizationId: input.organizationId, }); @@ -65,7 +47,7 @@ export const stripeBillingApiRouter = t.router({ } try { - const upcomingInvoice = await ctx.stripe.invoices.retrieveUpcoming({ + const upcomingInvoice = await ctx.stripeBilling.stripe.invoices.retrieveUpcoming({ customer: organizationBillingRecord.externalBillingReference, }); @@ -74,14 +56,14 @@ export const stripeBillingApiRouter = t.router({ return null; } }), - activeSubscription: procedure + activeSubscription: publicProcedure .input( z.object({ organizationId: z.string().nonempty(), }), ) .query(async ({ ctx, input }) => { - const storage = await ctx.storage$; + const storage = ctx.stripeBilling.storage; const organizationBillingRecord = await storage.getOrganizationBilling({ organizationId: input.organizationId, }); @@ -90,7 +72,7 @@ export const stripeBillingApiRouter = t.router({ throw new Error(`Organization does not have a subscription record!`); } - const customer = await ctx.stripe.customers.retrieve( + const customer = await ctx.stripeBilling.stripe.customers.retrieve( organizationBillingRecord.externalBillingReference, ); @@ -102,7 +84,7 @@ export const stripeBillingApiRouter = t.router({ return null; } - const subscriptions = await ctx.stripe.subscriptions + const subscriptions = await ctx.stripeBilling.stripe.subscriptions .list({ customer: organizationBillingRecord.externalBillingReference, }) @@ -110,7 +92,7 @@ export const stripeBillingApiRouter = t.router({ const actualSubscription = subscriptions[0] || null; - const paymentMethod = await ctx.stripe.paymentMethods.list({ + const paymentMethod = await ctx.stripeBilling.stripe.paymentMethods.list({ customer: customer.id, type: 'card', }); @@ -120,7 +102,7 @@ export const stripeBillingApiRouter = t.router({ subscription: actualSubscription, }; }), - syncOrganizationToStripe: procedure + syncOrganizationToStripe: publicProcedure .input( z.object({ organizationId: z.string().nonempty(), @@ -133,7 +115,7 @@ export const stripeBillingApiRouter = t.router({ }), ) .mutation(async ({ ctx, input }) => { - const storage = await ctx.storage$; + const storage = ctx.stripeBilling.storage; const [organizationBillingRecord, organization, stripePrices] = await Promise.all([ storage.getOrganizationBilling({ organizationId: input.organizationId, @@ -141,11 +123,11 @@ export const stripeBillingApiRouter = t.router({ storage.getOrganization({ organizationId: input.organizationId, }), - ctx.stripeData$, + ctx.stripeBilling.stripeData$, ]); if (organizationBillingRecord && organization) { - const allSubscriptions = await ctx.stripe.subscriptions.list({ + const allSubscriptions = await ctx.stripeBilling.stripe.subscriptions.list({ customer: organizationBillingRecord.externalBillingReference, }); @@ -154,7 +136,7 @@ export const stripeBillingApiRouter = t.router({ if (actualSubscription) { for (const item of actualSubscription.items.data) { if (item.plan.id === stripePrices.operationsPrice.id) { - await ctx.stripe.subscriptionItems.update(item.id, { + await ctx.stripeBilling.stripe.subscriptionItems.update(item.id, { quantity: input.reserved.operations, }); } @@ -168,7 +150,7 @@ export const stripeBillingApiRouter = t.router({ } if (Object.keys(updateParams).length > 0) { - await ctx.stripe.customers.update( + await ctx.stripeBilling.stripe.customers.update( organizationBillingRecord.externalBillingReference, updateParams, ); @@ -179,14 +161,14 @@ export const stripeBillingApiRouter = t.router({ ); } }), - generateStripePortalLink: procedure + generateStripePortalLink: publicProcedure .input( z.object({ organizationId: z.string().nonempty(), }), ) .mutation(async ({ ctx, input }) => { - const storage = await ctx.storage$; + const storage = ctx.stripeBilling.storage; const organizationBillingRecord = await storage.getOrganizationBilling({ organizationId: input.organizationId, }); @@ -197,21 +179,21 @@ export const stripeBillingApiRouter = t.router({ ); } - const session = await ctx.stripe.billingPortal.sessions.create({ + const session = await ctx.stripeBilling.stripe.billingPortal.sessions.create({ customer: organizationBillingRecord.externalBillingReference, return_url: 'https://app.graphql-hive.com/', }); return session.url; }), - cancelSubscriptionForOrganization: procedure + cancelSubscriptionForOrganization: publicProcedure .input( z.object({ organizationId: z.string().nonempty(), }), ) .mutation(async ({ ctx, input }) => { - const storage = await ctx.storage$; + const storage = ctx.stripeBilling.storage; const organizationBillingRecord = await storage.getOrganizationBilling({ organizationId: input.organizationId, }); @@ -222,7 +204,7 @@ export const stripeBillingApiRouter = t.router({ ); } - const subscriptions = await ctx.stripe.subscriptions + const subscriptions = await ctx.stripeBilling.stripe.subscriptions .list({ customer: organizationBillingRecord.externalBillingReference, }) @@ -235,13 +217,13 @@ export const stripeBillingApiRouter = t.router({ } const actualSubscription = subscriptions[0]; - const response = await ctx.stripe.subscriptions.cancel(actualSubscription.id, { + const response = await ctx.stripeBilling.stripe.subscriptions.cancel(actualSubscription.id, { prorate: true, }); return response; }), - createSubscriptionForOrganization: procedure + createSubscriptionForOrganization: publicProcedure .input( z.object({ paymentMethodId: z.string().nullish(), @@ -256,7 +238,7 @@ export const stripeBillingApiRouter = t.router({ }), ) .mutation(async ({ ctx, input }) => { - const storage = await ctx.storage$; + const storage = ctx.stripeBilling.storage; let organizationBillingRecord = await storage.getOrganizationBilling({ organizationId: input.organizationId, }); @@ -270,7 +252,7 @@ export const stripeBillingApiRouter = t.router({ const customerId = organizationBillingRecord?.externalBillingReference ? organizationBillingRecord.externalBillingReference - : await ctx.stripe.customers + : await ctx.stripeBilling.stripe.customers .create({ metadata: { external_reference_id: input.organizationId, @@ -289,7 +271,7 @@ export const stripeBillingApiRouter = t.router({ } const existingPaymentMethods = ( - await ctx.stripe.paymentMethods.list({ + await ctx.stripeBilling.stripe.paymentMethods.list({ customer: customerId, type: 'card', }) @@ -306,7 +288,7 @@ export const stripeBillingApiRouter = t.router({ paymentMethodId = paymentMethodConfiguredAlready.id; } else { paymentMethodId = ( - await ctx.stripe.paymentMethods.attach(input.paymentMethodId, { + await ctx.stripeBilling.stripe.paymentMethods.attach(input.paymentMethodId, { customer: customerId, }) ).id; @@ -321,9 +303,9 @@ export const stripeBillingApiRouter = t.router({ ); } - const stripePrices = await ctx.stripeData$; + const stripePrices = await ctx.stripeBilling.stripeData$; - const subscription = await ctx.stripe.subscriptions.create({ + const subscription = await ctx.stripeBilling.stripe.subscriptions.create({ metadata: { hive_subscription: 'true', }, @@ -352,6 +334,4 @@ export const stripeBillingApiRouter = t.router({ }), }); -export type StripeBillingApi = typeof stripeBillingApiRouter; - -export type StripeBillingApiInput = inferRouterInputs; +export type StripeBillingRouter = typeof stripeBillingRouter; diff --git a/packages/services/commerce/src/trpc.ts b/packages/services/commerce/src/trpc.ts new file mode 100644 index 0000000000..b8de6a2152 --- /dev/null +++ b/packages/services/commerce/src/trpc.ts @@ -0,0 +1,17 @@ +import { handleTRPCError, type FastifyRequest } from '@hive/service-common'; +import { initTRPC } from '@trpc/server'; +import type { RateLimiter } from './rate-limit/limiter'; +import type { StripeBilling } from './stripe-billing/billing'; +import type { UsageEstimator } from './usage-estimator/estimator'; + +export type Context = { + req: FastifyRequest; + usageEstimator: UsageEstimator; + rateLimiter: RateLimiter; + stripeBilling: StripeBilling; +}; + +const t = initTRPC.context().create(); + +export const router = t.router; +export const publicProcedure = t.procedure.use(handleTRPCError); diff --git a/packages/services/usage-estimator/src/estimator.ts b/packages/services/commerce/src/usage-estimator/estimator.ts similarity index 72% rename from packages/services/usage-estimator/src/estimator.ts rename to packages/services/commerce/src/usage-estimator/estimator.ts index 208f25b16d..4d0d96e311 100644 --- a/packages/services/usage-estimator/src/estimator.ts +++ b/packages/services/commerce/src/usage-estimator/estimator.ts @@ -1,7 +1,9 @@ +import 'reflect-metadata'; import { ClickHouse, HttpClient, OperationsReader, sql } from '@hive/api'; import type { ServiceLogger } from '@hive/service-common'; +import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics'; -export type Estimator = ReturnType; +export type UsageEstimator = ReturnType; export function createEstimator(config: { logger: ServiceLogger; @@ -11,18 +13,24 @@ export function createEstimator(config: { port: number; username: string; password: string; - onReadEnd?: ( - label: string, - timings: { - totalSeconds: number; - elapsedSeconds?: number; - }, - ) => void; }; }) { const { logger } = config; const httpClient = new HttpClient(); - const clickhouse = new ClickHouse(config.clickhouse, httpClient, config.logger); + const clickhouse = new ClickHouse( + { + ...config.clickhouse, + onReadEnd(query, timings) { + clickHouseReadDuration.labels({ query }).observe(timings.totalSeconds); + + if (timings.elapsedSeconds !== undefined) { + clickHouseElapsedDuration.labels({ query }).observe(timings.elapsedSeconds); + } + }, + }, + httpClient, + config.logger, + ); const operationsReader = new OperationsReader(clickhouse, logger); return { @@ -43,7 +51,7 @@ export function createEstimator(config: { }, }); - return await clickhouse.query<{ + const result = await clickhouse.query<{ total: string; target: string; }>({ @@ -58,6 +66,8 @@ export function createEstimator(config: { queryId: 'usage_estimator_count_operations_all', timeout: 60_000, }); + + return Object.fromEntries(result.data.map(item => [item.target, parseInt(item.total)])); }, async estimateCollectedOperationsForOrganization(input: { organizationId: string; @@ -69,7 +79,7 @@ export function createEstimator(config: { total: string; }>({ query: sql` - SELECT + SELECT sum(total) as total FROM monthly_overview PREWHERE organization = ${input.organizationId} AND date=${startOfMonth} diff --git a/packages/services/commerce/src/usage-estimator/index.ts b/packages/services/commerce/src/usage-estimator/index.ts new file mode 100644 index 0000000000..7835a5e734 --- /dev/null +++ b/packages/services/commerce/src/usage-estimator/index.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { publicProcedure, router } from '../trpc'; + +export const usageEstimatorRouter = router({ + estimateOperationsForOrganization: publicProcedure + .input( + z + .object({ + month: z.number().min(1).max(12), + year: z + .number() + .min(new Date().getFullYear() - 1) + .max(new Date().getFullYear()), + organizationId: z.string().min(1), + }) + .required(), + ) + .query(async ({ ctx, input }) => { + const estimationResponse = + await ctx.usageEstimator.estimateCollectedOperationsForOrganization({ + organizationId: input.organizationId, + month: input.month, + year: input.year, + }); + + if (!estimationResponse.data.length) { + return { + totalOperations: 0, + }; + } + + return { + totalOperations: parseInt(estimationResponse.data[0].total), + }; + }), + estimateOperationsForAllTargets: publicProcedure + .input( + z + .object({ + startTime: z.string().min(1), + endTime: z.string().min(1), + }) + .required(), + ) + .query(async ({ ctx, input }) => { + return await ctx.usageEstimator.estimateOperationsForAllTargets({ + startTime: new Date(input.startTime), + endTime: new Date(input.endTime), + }); + }), +}); + +export type UsageEstimatorRouter = typeof usageEstimatorRouter; diff --git a/packages/services/usage-estimator/src/metrics.ts b/packages/services/commerce/src/usage-estimator/metrics.ts similarity index 100% rename from packages/services/usage-estimator/src/metrics.ts rename to packages/services/commerce/src/usage-estimator/metrics.ts diff --git a/packages/services/stripe-billing/tsconfig.json b/packages/services/commerce/tsconfig.json similarity index 100% rename from packages/services/stripe-billing/tsconfig.json rename to packages/services/commerce/tsconfig.json diff --git a/packages/services/rate-limit/.env.template b/packages/services/rate-limit/.env.template deleted file mode 100644 index 653b852d2e..0000000000 --- a/packages/services/rate-limit/.env.template +++ /dev/null @@ -1,12 +0,0 @@ -PORT=4012 -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_DB=registry -USAGE_ESTIMATOR_ENDPOINT=http://localhost:4011 -EMAILS_ENDPOINT=http://localhost:6260 -WEB_APP_URL=http://localhost:3000 -OPENTELEMETRY_COLLECTOR_ENDPOINT="" -LIMIT_CACHE_UPDATE_INTERVAL_MS=2000 -OPENTELEMETRY_TRACE_USAGE_REQUESTS=1 \ No newline at end of file diff --git a/packages/services/rate-limit/README.md b/packages/services/rate-limit/README.md deleted file mode 100644 index 1c8d4cd59a..0000000000 --- a/packages/services/rate-limit/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Rate Limit - -The rate limit service is responsible of enforcing account limitations. If you are self-hosting Hive -you don't need this service. - -## Configuration - -| Name | Required | Description | Example Value | -| ------------------------------------ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| `PORT` | **Yes** | The HTTP port of the service. | `4012` | -| `LIMIT_CACHE_UPDATE_INTERVAL_MS` | No | The cache update interval limit in milliseconds. | `60_000` | -| `POSTGRES_HOST` | **Yes** | Host of the postgres database | `127.0.0.1` | -| `POSTGRES_PORT` | **Yes** | Port of the postgres database | `5432` | -| `POSTGRES_DB` | **Yes** | Name of the postgres database. | `registry` | -| `POSTGRES_USER` | **Yes** | User name for accessing the postgres database. | `postgres` | -| `POSTGRES_PASSWORD` | No | Password for accessing the postgres database. | `postgres` | -| `USAGE_ESTIMATOR_ENDPOINT` | **Yes** | The endpoint of the usage estimator service. | `http://127.0.0.1:4011` | -| `EMAILS_ENDPOINT` | No (if not provided no limit emails will be sent.) | The endpoint of the GraphQL Hive Email service. | `http://127.0.0.1:6260` | -| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | -| `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | -| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | -| `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) | -| `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `rate-limit` | -| `PROMETHEUS_METRICS_PORT` | No | Port on which prometheus metrics are exposed | Defaults to `10254` | -| `WEB_APP_URL` | No | The base url of the web app | `https://your-instance.com` | -| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) | -| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | -| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | -| `OPENTELEMETRY_TRACE_USAGE_REQUESTS` | No | If enabled, requests send to this service from `usage` service will be monitored with OTEL. | `1` (enabled, or ``) | diff --git a/packages/services/rate-limit/package.json b/packages/services/rate-limit/package.json deleted file mode 100644 index eda0a8805d..0000000000 --- a/packages/services/rate-limit/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@hive/rate-limit", - "type": "module", - "description": "A microservice for Hive Cloud, that exposes information about rate limits per given org/target.", - "license": "MIT", - "private": true, - "scripts": { - "build": "tsx ../../../scripts/runify.ts", - "dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/dev.ts", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@hive/emails": "workspace:*", - "@hive/service-common": "workspace:*", - "@hive/storage": "workspace:*", - "@sentry/node": "7.120.2", - "@trpc/client": "10.45.2", - "@trpc/server": "10.45.2", - "date-fns": "4.1.0", - "dotenv": "16.4.7", - "got": "14.4.5", - "pino-pretty": "11.3.0", - "reflect-metadata": "0.2.2", - "tslib": "2.8.1", - "zod": "3.24.1" - }, - "buildOptions": { - "external": [ - "pg-native" - ] - } -} diff --git a/packages/services/rate-limit/src/api.ts b/packages/services/rate-limit/src/api.ts deleted file mode 100644 index d16c4d2bc9..0000000000 --- a/packages/services/rate-limit/src/api.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { z } from 'zod'; -import { handleTRPCError } from '@hive/service-common'; -import type { FastifyRequest } from '@hive/service-common'; -import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; -import { initTRPC } from '@trpc/server'; -import type { Limiter } from './limiter'; - -export interface Context { - req: FastifyRequest; - limiter: Limiter; -} - -const t = initTRPC.context().create(); -const procedure = t.procedure.use(handleTRPCError); - -export type RateLimitInput = z.infer; - -const VALIDATION = z - .object({ - id: z.string().min(1), - entityType: z.enum(['organization', 'target']), - type: z.enum(['operations-reporting']), - /** - * Token is optional, and used only when an additional blocking (WAF) process is needed. - */ - token: z.string().nullish().optional(), - }) - .required(); - -export const rateLimitApiRouter = t.router({ - getRetention: procedure - .input( - z - .object({ - targetId: z.string().nonempty(), - }) - .required(), - ) - .query(({ ctx, input }) => { - return ctx.limiter.getRetention(input.targetId); - }), - checkRateLimit: procedure.input(VALIDATION).query(({ ctx, input }) => { - return ctx.limiter.checkLimit(input); - }), -}); - -export type RateLimitApi = typeof rateLimitApiRouter; -export type RateLimitApiInput = inferRouterInputs; -export type RateLimitApiOutput = inferRouterOutputs; diff --git a/packages/services/rate-limit/src/environment.ts b/packages/services/rate-limit/src/environment.ts deleted file mode 100644 index 1dbbd4df18..0000000000 --- a/packages/services/rate-limit/src/environment.ts +++ /dev/null @@ -1,161 +0,0 @@ -import zod from 'zod'; -import { OpenTelemetryConfigurationModel } from '@hive/service-common'; - -const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success; - -const numberFromNumberOrNumberString = (input: unknown): number | undefined => { - if (typeof input == 'number') return input; - if (isNumberString(input)) return Number(input); -}; - -const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1)); - -// treat an empty string (`''`) as undefined -const emptyString = (input: T) => { - return zod.preprocess((value: unknown) => { - if (value === '') return undefined; - return value; - }, input); -}; - -const EnvironmentModel = zod.object({ - PORT: emptyString(NumberFromString.optional()), - ENVIRONMENT: emptyString(zod.string().optional()), - RELEASE: emptyString(zod.string().optional()), - USAGE_ESTIMATOR_ENDPOINT: zod.string().url(), - EMAILS_ENDPOINT: emptyString(zod.string().url().optional()), - LIMIT_CACHE_UPDATE_INTERVAL_MS: emptyString(NumberFromString.optional()), - WEB_APP_URL: emptyString(zod.string().url().optional()), -}); - -const SentryModel = zod.union([ - zod.object({ - SENTRY: emptyString(zod.literal('0').optional()), - }), - zod.object({ - SENTRY: zod.literal('1'), - SENTRY_DSN: zod.string(), - }), -]); - -const PostgresModel = zod.object({ - POSTGRES_SSL: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), - POSTGRES_HOST: zod.string(), - POSTGRES_PORT: NumberFromString, - POSTGRES_DB: zod.string(), - POSTGRES_USER: zod.string(), - POSTGRES_PASSWORD: emptyString(zod.string().optional()), -}); - -const PrometheusModel = zod.object({ - PROMETHEUS_METRICS: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()), - PROMETHEUS_METRICS_LABEL_INSTANCE: zod.string().optional(), - PROMETHEUS_METRICS_PORT: emptyString(NumberFromString.optional()), -}); - -const LogModel = zod.object({ - LOG_LEVEL: emptyString( - zod - .union([ - zod.literal('trace'), - zod.literal('debug'), - zod.literal('info'), - zod.literal('warn'), - zod.literal('error'), - zod.literal('fatal'), - zod.literal('silent'), - ]) - .optional(), - ), - REQUEST_LOGGING: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()).default( - '1', - ), -}); - -const configs = { - base: EnvironmentModel.safeParse(process.env), - - sentry: SentryModel.safeParse(process.env), - - postgres: PostgresModel.safeParse(process.env), - - prometheus: PrometheusModel.safeParse(process.env), - - log: LogModel.safeParse(process.env), - tracing: zod - .object({ - ...OpenTelemetryConfigurationModel.shape, - OPENTELEMETRY_TRACE_USAGE_REQUESTS: emptyString(zod.literal('1').optional()), - }) - - .safeParse(process.env), -}; - -const environmentErrors: Array = []; - -for (const config of Object.values(configs)) { - if (config.success === false) { - environmentErrors.push(JSON.stringify(config.error.format(), null, 4)); - } -} - -if (environmentErrors.length) { - const fullError = environmentErrors.join(`\n`); - console.error('❌ Invalid environment variables:', fullError); - process.exit(1); -} - -function extractConfig(config: zod.SafeParseReturnType): Output { - if (!config.success) { - throw new Error('Something went wrong.'); - } - return config.data; -} - -const base = extractConfig(configs.base); -const postgres = extractConfig(configs.postgres); -const sentry = extractConfig(configs.sentry); -const prometheus = extractConfig(configs.prometheus); -const log = extractConfig(configs.log); -const tracing = extractConfig(configs.tracing); - -export const env = { - environment: base.ENVIRONMENT, - release: base.RELEASE ?? 'local', - hiveServices: { - usageEstimator: { endpoint: base.USAGE_ESTIMATOR_ENDPOINT }, - emails: base.EMAILS_ENDPOINT ? { endpoint: base.EMAILS_ENDPOINT } : null, - webAppUrl: base.WEB_APP_URL ?? 'http://localhost:3000', - }, - limitCacheUpdateIntervalMs: base.LIMIT_CACHE_UPDATE_INTERVAL_MS ?? 60_000, - http: { - port: base.PORT ?? 4012, - }, - tracing: { - enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT, - collectorEndpoint: tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT, - traceRequestsFromUsageService: tracing.OPENTELEMETRY_TRACE_USAGE_REQUESTS === '1', - }, - postgres: { - host: postgres.POSTGRES_HOST, - port: postgres.POSTGRES_PORT, - db: postgres.POSTGRES_DB, - user: postgres.POSTGRES_USER, - password: postgres.POSTGRES_PASSWORD, - ssl: postgres.POSTGRES_SSL === '1', - }, - sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null, - log: { - level: log.LOG_LEVEL ?? 'info', - requests: log.REQUEST_LOGGING === '1', - }, - prometheus: - prometheus.PROMETHEUS_METRICS === '1' - ? { - labels: { - instance: prometheus.PROMETHEUS_METRICS_LABEL_INSTANCE ?? 'rate-limit', - }, - port: prometheus.PROMETHEUS_METRICS_PORT ?? 10_254, - } - : null, -} as const; diff --git a/packages/services/rate-limit/src/index.ts b/packages/services/rate-limit/src/index.ts deleted file mode 100644 index c631e6aad2..0000000000 --- a/packages/services/rate-limit/src/index.ts +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env node -import 'reflect-metadata'; -import { hostname } from 'os'; -import { - configureTracing, - createServer, - registerShutdown, - registerTRPC, - reportReadiness, - SamplingDecision, - startMetrics, - TracingInstance, -} from '@hive/service-common'; -import { createConnectionString } from '@hive/storage'; -import * as Sentry from '@sentry/node'; -import { Context, rateLimitApiRouter } from './api'; -import { env } from './environment'; -import { createRateLimiter } from './limiter'; - -async function main() { - let tracing: TracingInstance | undefined; - - if (env.tracing.enabled && env.tracing.collectorEndpoint) { - tracing = configureTracing({ - collectorEndpoint: env.tracing.collectorEndpoint, - serviceName: 'rate-limit', - sampler: (ctx, traceId, spanName, spanKind, attributes) => { - if ( - attributes['requesting.service'] === 'usage' && - !env.tracing.traceRequestsFromUsageService - ) { - return { - decision: SamplingDecision.NOT_RECORD, - }; - } - - return { - decision: SamplingDecision.RECORD_AND_SAMPLED, - }; - }, - }); - - tracing.instrumentNodeFetch(); - tracing.build(); - tracing.start(); - } - - if (env.sentry) { - Sentry.init({ - serverName: hostname(), - dist: 'rate-limit', - enabled: !!env.sentry, - environment: env.environment, - dsn: env.sentry.dsn, - release: env.release, - }); - } - - const server = await createServer({ - name: 'rate-limit', - sentryErrorHandler: true, - log: { - level: env.log.level, - requests: env.log.requests, - }, - }); - - if (tracing) { - await server.register(...tracing.instrumentFastify()); - } - - try { - const limiter = createRateLimiter({ - logger: server.log, - rateLimitConfig: { - interval: env.limitCacheUpdateIntervalMs, - }, - rateEstimator: env.hiveServices.usageEstimator, - emails: env.hiveServices.emails ?? undefined, - storage: { - connectionString: createConnectionString(env.postgres), - additionalInterceptors: tracing ? [tracing.instrumentSlonik()] : undefined, - }, - }); - - await registerTRPC(server, { - router: rateLimitApiRouter, - createContext({ req }): Context { - return { - req, - limiter, - }; - }, - }); - - registerShutdown({ - logger: server.log, - async onShutdown() { - await Promise.all([limiter.stop(), server.close()]); - }, - }); - - server.route({ - method: ['GET', 'HEAD'], - url: '/_health', - handler(_, res) { - void res.status(200).send(); - }, - }); - - server.route({ - method: ['GET', 'HEAD'], - url: '/_readiness', - async handler(_, res) { - const isReady = await limiter.readiness(); - reportReadiness(isReady); - void res.status(isReady ? 200 : 400).send(); - }, - }); - - if (env.prometheus) { - await startMetrics(env.prometheus.labels.instance, env.prometheus.port); - } - await server.listen({ - port: env.http.port, - host: '::', - }); - await limiter.start(); - } catch (error) { - server.log.fatal(error); - Sentry.captureException(error, { - level: 'fatal', - }); - } -} - -main().catch(err => { - Sentry.captureException(err, { - level: 'fatal', - }); - console.error(err); - process.exit(1); -}); diff --git a/packages/services/rate-limit/tsconfig.json b/packages/services/rate-limit/tsconfig.json deleted file mode 100644 index a6fa5e097c..0000000000 --- a/packages/services/rate-limit/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "rootDir": "../.." - }, - "files": ["src/index.ts"] -} diff --git a/packages/services/server/.env.template b/packages/services/server/.env.template index 4fa1a9130f..2f724735e9 100644 --- a/packages/services/server/.env.template +++ b/packages/services/server/.env.template @@ -14,9 +14,7 @@ CLICKHOUSE_PASSWORD="test" TOKENS_ENDPOINT="http://localhost:6001" SCHEMA_ENDPOINT="http://localhost:6500" SCHEMA_POLICY_ENDPOINT="http://localhost:6600" -USAGE_ESTIMATOR_ENDPOINT="http://localhost:4011" -RATE_LIMIT_ENDPOINT="http://localhost:4012" -BILLING_ENDPOINT="http://localhost:4013" +COMMERCE_ENDPOINT="http://localhost:4013" WEBHOOKS_ENDPOINT="http://localhost:6250" EMAILS_ENDPOINT="http://localhost:6260" REDIS_HOST="localhost" @@ -84,4 +82,4 @@ AUTH_OKTA_CLIENT_SECRET="" OPENTELEMETRY_COLLECTOR_ENDPOINT="" -HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret \ No newline at end of file +HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret diff --git a/packages/services/server/README.md b/packages/services/server/README.md index e332a19f39..c73bfcbf3f 100644 --- a/packages/services/server/README.md +++ b/packages/services/server/README.md @@ -10,7 +10,6 @@ The GraphQL API for GraphQL Hive. | `ENCRYPTION_SECRET` | **Yes** | Secret for encrypting stuff. | `8ebe95cg21c1fee355e9fa32c8c33141` | | `WEB_APP_URL` | **Yes** | The url of the web app. | `http://127.0.0.1:3000` | | `GRAPHQL_PUBLIC_ORIGIN` | **Yes** | The origin of the GraphQL server. | `http://127.0.0.1:4013` | -| `RATE_LIMIT_ENDPOINT` | **Yes** | The endpoint of the rate limiting service. | `http://127.0.0.1:4012` | | `EMAILS_ENDPOINT` | **Yes** | The endpoint of the GraphQL Hive Email service. | `http://127.0.0.1:6260` | | `TOKENS_ENDPOINT` | **Yes** | The endpoint of the tokens service. | `http://127.0.0.1:6001` | | `WEBHOOKS_ENDPOINT` | **Yes** | The endpoint of the webhooks service. | `http://127.0.0.1:6250` | @@ -95,15 +94,14 @@ The GraphQL API for GraphQL Hive. If you are self-hosting GraphQL Hive, you can ignore this section. It is only required for the Cloud version. -| Name | Required | Description | Example Value | -| -------------------------- | ----------------------------- | -------------------------------------------- | ------------------------------- | -| `BILLING_ENDPOINT` | **Yes** | The endpoint of the Hive Billing service. | `http://127.0.0.1:4013` | -| `USAGE_ESTIMATOR_ENDPOINT` | No | The endpoint of the usage estimator service. | `4011` | -| `CDN_CF` | No | Whether the CDN is enabled. | `1` (enabled) or `0` (disabled) | -| `CDN_CF_BASE_URL` | No (**Yes** if `CDN` is `1`) | The base URL of the cdn. | `https://cdn.graphql-hive.com` | -| `HIVE` | No | The internal endpoint key. | `iliketurtles` | -| `HIVE_API_TOKEN` | No (**Yes** if `HIVE` is set) | The internal endpoint key. | `iliketurtles` | -| `HIVE_USAGE` | No | The internal endpoint key. | `1` (enabled) or `0` (disabled) | -| `HIVE_USAGE_ENDPOINT` | No | The endpoint used for usage reporting. | `http://127.0.0.1:4001` | -| `HIVE_REPORTING` | No | The internal endpoint key. | `iliketurtles` | -| `HIVE_REPORTING_ENDPOINT` | No | The internal endpoint key. | `http://127.0.0.1:4000/graphql` | +| Name | Required | Description | Example Value | +| ------------------------- | ----------------------------- | -------------------------------------- | ------------------------------- | +| `COMMERCE_ENDPOINT` | **Yes** | The endpoint of the commerce service. | `http://127.0.0.1:4012` | +| `CDN_CF` | No | Whether the CDN is enabled. | `1` (enabled) or `0` (disabled) | +| `CDN_CF_BASE_URL` | No (**Yes** if `CDN` is `1`) | The base URL of the cdn. | `https://cdn.graphql-hive.com` | +| `HIVE` | No | The internal endpoint key. | `iliketurtles` | +| `HIVE_API_TOKEN` | No (**Yes** if `HIVE` is set) | The internal endpoint key. | `iliketurtles` | +| `HIVE_USAGE` | No | The internal endpoint key. | `1` (enabled) or `0` (disabled) | +| `HIVE_USAGE_ENDPOINT` | No | The endpoint used for usage reporting. | `http://127.0.0.1:4001` | +| `HIVE_REPORTING` | No | The internal endpoint key. | `iliketurtles` | +| `HIVE_REPORTING_ENDPOINT` | No | The internal endpoint key. | `http://127.0.0.1:4000/graphql` | diff --git a/packages/services/server/src/api.ts b/packages/services/server/src/api.ts index 96cdf73a9f..d0188ef90b 100644 --- a/packages/services/server/src/api.ts +++ b/packages/services/server/src/api.ts @@ -2,23 +2,12 @@ import { CryptoProvider } from 'packages/services/api/src/modules/shared/provide import { z } from 'zod'; import type { Storage } from '@hive/api'; import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '@hive/api'; -import type { inferAsyncReturnType } from '@trpc/server'; import { initTRPC } from '@trpc/server'; -export async function createContext({ - storage, - crypto, -}: { +export type Context = { storage: Storage; crypto: CryptoProvider; -}) { - return { - storage, - crypto, - }; -} - -export type Context = inferAsyncReturnType; +}; const oidcDefaultScopes = [ OrganizationAccessScope.READ, diff --git a/packages/services/server/src/environment.ts b/packages/services/server/src/environment.ts index 1f5a0b690f..3d061d826d 100644 --- a/packages/services/server/src/environment.ts +++ b/packages/services/server/src/environment.ts @@ -30,12 +30,8 @@ const EnvironmentModel = zod.object({ 'GRAPHQL_PUBLIC_ORIGIN is required (see: https://github.com/graphql-hive/platform/pull/4288#issue-2195509699)', }) .url(), - RATE_LIMIT_ENDPOINT: emptyString(zod.string().url().optional()), SCHEMA_POLICY_ENDPOINT: emptyString(zod.string().url().optional()), TOKENS_ENDPOINT: zod.string().url(), - USAGE_ESTIMATOR_ENDPOINT: emptyString(zod.string().url().optional()), - USAGE_ESTIMATOR_RETENTION_PURGE_INTERVAL_MINUTES: emptyString(NumberFromString.optional()), - BILLING_ENDPOINT: emptyString(zod.string().url().optional()), EMAILS_ENDPOINT: emptyString(zod.string().url().optional()), WEBHOOKS_ENDPOINT: zod.string().url(), SCHEMA_ENDPOINT: zod.string().url(), @@ -48,6 +44,10 @@ const EnvironmentModel = zod.object({ ), }); +const CommerceModel = zod.object({ + COMMERCE_ENDPOINT: emptyString(zod.string().url().optional()), +}); + const SentryModel = zod.union([ zod.object({ SENTRY: emptyString(zod.literal('0').optional()), @@ -261,6 +261,7 @@ const processEnv = process.env; const configs = { base: EnvironmentModel.safeParse(processEnv), + commerce: CommerceModel.safeParse(processEnv), sentry: SentryModel.safeParse(processEnv), postgres: PostgresModel.safeParse(processEnv), clickhouse: ClickHouseModel.safeParse(processEnv), @@ -306,6 +307,7 @@ function extractConfig(config: zod.SafeParseReturnType = null; - if (!env.hiveServices.usageEstimator) { - server.log.debug('Usage estimation is disabled. Skip scheduling purge tasks.'); + if (!env.hiveServices.commerce) { + server.log.debug('Commerce service is disabled. Skip scheduling purge tasks.'); } else { server.log.debug( - `Usage estimation is enabled. Start scheduling purge tasks every ${env.hiveServices.usageEstimator.dateRetentionPurgeIntervalMinutes} minutes.`, + `Commerce service is enabled. Start scheduling purge tasks every ${env.hiveServices.commerce.dateRetentionPurgeIntervalMinutes} minutes.`, ); dbPurgeTaskRunner = createTaskRunner({ run: traceInline( @@ -220,7 +220,7 @@ export async function main() { } }, ), - interval: env.hiveServices.usageEstimator.dateRetentionPurgeIntervalMinutes * 60 * 1000, + interval: env.hiveServices.commerce.dateRetentionPurgeIntervalMinutes * 60 * 1000, logger: server.log, }); @@ -317,8 +317,8 @@ export async function main() { tokens: { endpoint: env.hiveServices.tokens.endpoint, }, - billing: { - endpoint: env.hiveServices.billing ? env.hiveServices.billing.endpoint : null, + commerce: { + endpoint: env.hiveServices.commerce ? env.hiveServices.commerce.endpoint : null, }, emailsEndpoint: env.hiveServices.emails ? env.hiveServices.emails.endpoint : undefined, webhooks: { @@ -327,12 +327,6 @@ export async function main() { schemaService: { endpoint: env.hiveServices.schema.endpoint, }, - usageEstimationService: { - endpoint: env.hiveServices.usageEstimator ? env.hiveServices.usageEstimator.endpoint : null, - }, - rateLimitService: { - endpoint: env.hiveServices.rateLimit ? env.hiveServices.rateLimit.endpoint : null, - }, schemaPolicyService: { endpoint: env.hiveServices.schemaPolicy ? env.hiveServices.schemaPolicy.endpoint : null, }, @@ -486,7 +480,10 @@ export async function main() { await registerTRPC(server, { router: internalApiRouter, createContext() { - return createContext({ storage, crypto }); + return { + storage, + crypto, + }; }, }); diff --git a/packages/services/service-common/src/trpc.ts b/packages/services/service-common/src/trpc.ts index 4dc087ea75..cf8bedc813 100644 --- a/packages/services/service-common/src/trpc.ts +++ b/packages/services/service-common/src/trpc.ts @@ -5,7 +5,10 @@ import { experimental_standaloneMiddleware, type AnyRouter } from '@trpc/server' import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify'; import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; -export function registerTRPC( +export function registerTRPC< + TRouter extends AnyRouter, + TContext extends TRouter['_def']['_config']['$types']['ctx'], +>( server: FastifyInstance, { router, diff --git a/packages/services/stripe-billing/.env.template b/packages/services/stripe-billing/.env.template deleted file mode 100644 index 3be7b0210e..0000000000 --- a/packages/services/stripe-billing/.env.template +++ /dev/null @@ -1,9 +0,0 @@ -PORT=4013 -USAGE_ESTIMATOR_ENDPOINT=http://localhost:4011 -STRIPE_SECRET_KEY="" -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_DB=registry -OPENTELEMETRY_COLLECTOR_ENDPOINT="" \ No newline at end of file diff --git a/packages/services/stripe-billing/README.md b/packages/services/stripe-billing/README.md deleted file mode 100644 index a1a2c798db..0000000000 --- a/packages/services/stripe-billing/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# `@hive/stripe-billing` - -Optional service for billing customers with Stripe. - -## Configuration - -| Name | Required | Description | Example Value | -| ---------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| `PORT` | **Yes** | The port this service is running on. | `4013` | -| `USAGE_ESTIMATOR_ENDPOINT` | **Yes** | The endpoint of the usage estimator service. | `4011` | -| `POSTGRES_HOST` | **Yes** | Host of the postgres database | `127.0.0.1` | -| `POSTGRES_PORT` | **Yes** | Port of the postgres database | `5432` | -| `POSTGRES_DB` | **Yes** | Name of the postgres database. | `registry` | -| `POSTGRES_USER` | **Yes** | User name for accessing the postgres database. | `postgres` | -| `POSTGRES_PASSWORD` | No | Password for accessing the postgres database. | `postgres` | -| `POSTGRES_SSL` | No | Whether the postgres connection should be established via SSL. | `1` (enabled) or `0` (disabled) | -| `STRIPE_SECRET_KEY` | **Yes** | The stripe secret key. | `sk_test_abcd` | -| `STRIPE_SYNC_INTERVAL_MS` | No | The stripe sync interval in milliseconds (Default: `600_000`) | `1_000` | -| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | -| `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | -| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | -| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) | -| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | -| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | diff --git a/packages/services/stripe-billing/src/dev.ts b/packages/services/stripe-billing/src/dev.ts deleted file mode 100644 index bec25773ff..0000000000 --- a/packages/services/stripe-billing/src/dev.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { config } from 'dotenv'; - -config({ - debug: true, - encoding: 'utf8', -}); - -await import('./index'); diff --git a/packages/services/stripe-billing/src/index.ts b/packages/services/stripe-billing/src/index.ts deleted file mode 100644 index 1fd50da712..0000000000 --- a/packages/services/stripe-billing/src/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env node -import 'reflect-metadata'; -import { hostname } from 'os'; -import { - configureTracing, - createServer, - registerShutdown, - registerTRPC, - reportReadiness, - startMetrics, - TracingInstance, -} from '@hive/service-common'; -import { createConnectionString } from '@hive/storage'; -import * as Sentry from '@sentry/node'; -import { Context, stripeBillingApiRouter } from './api'; -import { createStripeBilling } from './billing-sync'; -import { env } from './environment'; - -async function main() { - let tracing: TracingInstance | undefined; - - if (env.tracing.enabled && env.tracing.collectorEndpoint) { - tracing = configureTracing({ - collectorEndpoint: env.tracing.collectorEndpoint, - serviceName: 'stripe-billing', - }); - - tracing.instrumentNodeFetch(); - tracing.build(); - tracing.start(); - } - - if (env.sentry) { - Sentry.init({ - serverName: hostname(), - dist: 'stripe-billing', - enabled: !!env.sentry, - environment: env.environment, - dsn: env.sentry.dsn, - release: env.release, - }); - } - - const server = await createServer({ - name: 'stripe-billing', - sentryErrorHandler: true, - log: { - level: env.log.level, - requests: env.log.requests, - }, - }); - - if (tracing) { - await server.register(...tracing.instrumentFastify()); - } - - try { - const { readiness, start, stop, stripeApi, postgres$, loadStripeData$ } = createStripeBilling({ - logger: server.log, - stripe: { - token: env.stripe.secretKey, - syncIntervalMs: env.stripe.syncIntervalMs, - }, - rateEstimator: { - endpoint: env.hiveServices.usageEstimator.endpoint, - }, - storage: { - connectionString: createConnectionString(env.postgres), - additionalInterceptors: tracing ? [tracing.instrumentSlonik()] : [], - }, - }); - - registerShutdown({ - logger: server.log, - async onShutdown() { - await Promise.all([stop(), server.close()]); - }, - }); - - await registerTRPC(server, { - router: stripeBillingApiRouter, - createContext({ req }): Context { - return { - storage$: postgres$, - stripe: stripeApi, - stripeData$: loadStripeData$, - req, - }; - }, - }); - - server.route({ - method: ['GET', 'HEAD'], - url: '/_health', - handler(_, res) { - void res.status(200).send(); - }, - }); - - server.route({ - method: ['GET', 'HEAD'], - url: '/_readiness', - async handler(_, res) { - const isReady = await readiness(); - reportReadiness(isReady); - void res.status(isReady ? 200 : 400).send(); - }, - }); - - if (env.prometheus) { - await startMetrics(env.prometheus.labels.instance); - } - await server.listen({ - port: env.http.port, - host: '::', - }); - await start(); - } catch (error) { - server.log.fatal(error); - Sentry.captureException(error, { - level: 'fatal', - }); - } -} - -main().catch(err => { - Sentry.captureException(err, { - level: 'fatal', - }); - console.error(err); - process.exit(1); -}); diff --git a/packages/services/stripe-billing/src/types.ts b/packages/services/stripe-billing/src/types.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/services/tokens/README.md b/packages/services/tokens/README.md index 9f21ea4812..c90be55cfb 100644 --- a/packages/services/tokens/README.md +++ b/packages/services/tokens/README.md @@ -18,7 +18,6 @@ APIs (usage service and GraphQL API). | `REDIS_PORT` | **Yes** | The port of your redis instance. | `6379` | | `REDIS_PASSWORD` | **Yes** | The password of your redis instance. | `"apollorocks"` | | `REDIS_TLS_ENABLED` | **No** | Enable TLS for redis connection (rediss://). | `"0"` | -| `RATE_LIMIT_ENDPOINT` | **Yes** | The endpoint of the rate limiting service. | `http://127.0.0.1:4012` | | `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | | `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | | `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | diff --git a/packages/services/usage-estimator/README.md b/packages/services/usage-estimator/README.md deleted file mode 100644 index 507019b8cd..0000000000 --- a/packages/services/usage-estimator/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# `@hive/usage-estimator` - -This service takes care of estimating the usage of an account. - -## Configuration - -| Name | Required | Description | Example Value | -| ----------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| `CLICKHOUSE_PROTOCOL` | **Yes** | The clickhouse protocol for connecting to the clickhouse instance. | `http` | -| `CLICKHOUSE_HOST` | **Yes** | The host of the clickhouse instance. | `127.0.0.1` | -| `CLICKHOUSE_PORT` | **Yes** | The port of the clickhouse instance | `8123` | -| `CLICKHOUSE_USERNAME` | **Yes** | The username for accessing the clickhouse instance. | `test` | -| `CLICKHOUSE_PASSWORD` | **Yes** | The password for accessing the clickhouse instance. | `test` | -| `PORT` | **Yes** | The port this service is running on. | `4011` | -| `POSTGRES_HOST` | **Yes** | Host of the postgres database | `127.0.0.1` | -| `POSTGRES_PORT` | **Yes** | Port of the postgres database | `5432` | -| `POSTGRES_DB` | **Yes** | Name of the postgres database. | `registry` | -| `POSTGRES_USER` | **Yes** | User name for accessing the postgres database. | `postgres` | -| `POSTGRES_PASSWORD` | No | Password for accessing the postgres database. | `postgres` | -| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | -| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | -| `SENTRY_ENABLED` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | -| `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) | -| `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `rate-limit` | -| `PROMETHEUS_METRICS_PORT` | No | Port on which prometheus metrics are exposed | Defaults to `10254` | -| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) | -| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | -| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | diff --git a/packages/services/usage-estimator/package.json b/packages/services/usage-estimator/package.json deleted file mode 100644 index 3b82fe772e..0000000000 --- a/packages/services/usage-estimator/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@hive/usage-estimator", - "type": "module", - "description": "A microservice for Hive Cloud, that calculates and exposes usage information.", - "license": "MIT", - "private": true, - "scripts": { - "build": "tsx ../../../scripts/runify.ts", - "dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/dev.ts", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@hive/api": "workspace:*", - "@hive/service-common": "workspace:*", - "@sentry/node": "7.120.2", - "@trpc/server": "10.45.2", - "dotenv": "16.4.7", - "got": "14.4.5", - "pino-pretty": "11.3.0", - "reflect-metadata": "0.2.2", - "tslib": "2.8.1", - "zod": "3.24.1" - }, - "buildOptions": { - "external": [ - "pg-native" - ] - } -} diff --git a/packages/services/usage-estimator/src/api.ts b/packages/services/usage-estimator/src/api.ts deleted file mode 100644 index a5ed8c4b5b..0000000000 --- a/packages/services/usage-estimator/src/api.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { z } from 'zod'; -import { handleTRPCError } from '@hive/service-common'; -import type { FastifyRequest } from '@hive/service-common'; -import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; -import { initTRPC } from '@trpc/server'; -import type { Estimator } from './estimator'; - -export function createContext(estimator: Estimator, req: FastifyRequest) { - return { - estimator, - req, - }; -} - -const t = initTRPC.context>().create(); -const procedure = t.procedure.use(handleTRPCError); - -export const usageEstimatorApiRouter = t.router({ - estimateOperationsForOrganization: procedure - .input( - z - .object({ - month: z.number().min(1).max(12), - year: z - .number() - .min(new Date().getFullYear() - 1) - .max(new Date().getFullYear()), - organizationId: z.string().min(1), - }) - .required(), - ) - .query(async ({ ctx, input }) => { - const estimationResponse = await ctx.estimator.estimateCollectedOperationsForOrganization({ - organizationId: input.organizationId, - month: input.month, - year: input.year, - }); - - if (!estimationResponse.data.length) { - return { - totalOperations: 0, - }; - } - - return { - totalOperations: parseInt(estimationResponse.data[0].total), - }; - }), - estimateOperationsForAllTargets: procedure - .input( - z - .object({ - startTime: z.string().min(1), - endTime: z.string().min(1), - }) - .required(), - ) - .query(async ({ ctx, input }) => { - const estimationResponse = await ctx.estimator.estimateOperationsForAllTargets({ - startTime: new Date(input.startTime), - endTime: new Date(input.endTime), - }); - - return Object.fromEntries( - estimationResponse.data.map(item => [item.target, parseInt(item.total)]), - ); - }), -}); - -export type UsageEstimatorApi = typeof usageEstimatorApiRouter; - -export type UsageEstimatorApiInput = inferRouterInputs; -export type UsageEstimatorApiOutput = inferRouterOutputs; diff --git a/packages/services/usage-estimator/src/dev.ts b/packages/services/usage-estimator/src/dev.ts deleted file mode 100644 index bec25773ff..0000000000 --- a/packages/services/usage-estimator/src/dev.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { config } from 'dotenv'; - -config({ - debug: true, - encoding: 'utf8', -}); - -await import('./index'); diff --git a/packages/services/usage-estimator/src/environment.ts b/packages/services/usage-estimator/src/environment.ts deleted file mode 100644 index a191089326..0000000000 --- a/packages/services/usage-estimator/src/environment.ts +++ /dev/null @@ -1,143 +0,0 @@ -import zod from 'zod'; -import { OpenTelemetryConfigurationModel } from '@hive/service-common'; - -const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success; - -const numberFromNumberOrNumberString = (input: unknown): number | undefined => { - if (typeof input == 'number') return input; - if (isNumberString(input)) return Number(input); -}; - -const NumberFromString = zod.preprocess(numberFromNumberOrNumberString, zod.number().min(1)); - -// treat an empty string (`''`) as undefined -const emptyString = (input: T) => { - return zod.preprocess((value: unknown) => { - if (value === '') return undefined; - return value; - }, input); -}; - -const EnvironmentModel = zod.object({ - PORT: emptyString(NumberFromString.optional()), - ENVIRONMENT: emptyString(zod.string().optional()), - RELEASE: emptyString(zod.string().optional()), -}); - -const SentryModel = zod.union([ - zod.object({ - SENTRY: emptyString(zod.literal('0').optional()), - }), - zod.object({ - SENTRY: zod.literal('1'), - SENTRY_DSN: zod.string(), - }), -]); - -const PrometheusModel = zod.object({ - PROMETHEUS_METRICS: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()), - PROMETHEUS_METRICS_LABEL_INSTANCE: emptyString(zod.string().optional()), - PROMETHEUS_METRICS_PORT: emptyString(NumberFromString.optional()), -}); - -const LogModel = zod.object({ - LOG_LEVEL: emptyString( - zod - .union([ - zod.literal('trace'), - zod.literal('debug'), - zod.literal('info'), - zod.literal('warn'), - zod.literal('error'), - zod.literal('fatal'), - zod.literal('silent'), - ]) - .optional(), - ), - REQUEST_LOGGING: emptyString(zod.union([zod.literal('0'), zod.literal('1')]).optional()).default( - '1', - ), -}); - -const ClickHouseModel = zod.object({ - CLICKHOUSE_PROTOCOL: zod.union([zod.literal('http'), zod.literal('https')]), - CLICKHOUSE_HOST: zod.string(), - CLICKHOUSE_PORT: NumberFromString, - CLICKHOUSE_USERNAME: zod.string(), - CLICKHOUSE_PASSWORD: zod.string(), -}); - -const configs = { - base: EnvironmentModel.safeParse(process.env), - - sentry: SentryModel.safeParse(process.env), - - clickhouse: ClickHouseModel.safeParse(process.env), - - prometheus: PrometheusModel.safeParse(process.env), - - log: LogModel.safeParse(process.env), - - tracing: OpenTelemetryConfigurationModel.safeParse(process.env), -}; - -const environmentErrors: Array = []; - -for (const config of Object.values(configs)) { - if (config.success === false) { - environmentErrors.push(JSON.stringify(config.error.format(), null, 4)); - } -} - -if (environmentErrors.length) { - const fullError = environmentErrors.join(`\n`); - console.error('❌ Invalid environment variables:', fullError); - process.exit(1); -} - -function extractConfig(config: zod.SafeParseReturnType): Output { - if (!config.success) { - throw new Error('Something went wrong.'); - } - return config.data; -} - -const base = extractConfig(configs.base); -const clickhouse = extractConfig(configs.clickhouse); -const sentry = extractConfig(configs.sentry); -const prometheus = extractConfig(configs.prometheus); -const log = extractConfig(configs.log); -const tracing = extractConfig(configs.tracing); - -export const env = { - environment: base.ENVIRONMENT, - release: base.RELEASE ?? 'local', - http: { - port: base.PORT ?? 4012, - }, - tracing: { - enabled: !!tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT, - collectorEndpoint: tracing.OPENTELEMETRY_COLLECTOR_ENDPOINT, - }, - clickhouse: { - protocol: clickhouse.CLICKHOUSE_PROTOCOL, - host: clickhouse.CLICKHOUSE_HOST, - port: clickhouse.CLICKHOUSE_PORT, - username: clickhouse.CLICKHOUSE_USERNAME, - password: clickhouse.CLICKHOUSE_PASSWORD, - }, - sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null, - log: { - level: log.LOG_LEVEL ?? 'info', - requests: log.REQUEST_LOGGING === '1', - }, - prometheus: - prometheus.PROMETHEUS_METRICS === '1' - ? { - labels: { - instance: prometheus.PROMETHEUS_METRICS_LABEL_INSTANCE ?? 'rate-limit', - }, - port: prometheus.PROMETHEUS_METRICS_PORT ?? 10_254, - } - : null, -} as const; diff --git a/packages/services/usage-estimator/tsconfig.json b/packages/services/usage-estimator/tsconfig.json deleted file mode 100644 index a6fa5e097c..0000000000 --- a/packages/services/usage-estimator/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "rootDir": "../.." - }, - "files": ["src/index.ts"] -} diff --git a/packages/services/usage/.env.template b/packages/services/usage/.env.template index 1a74f2f6a1..fcb1a37050 100644 --- a/packages/services/usage/.env.template +++ b/packages/services/usage/.env.template @@ -6,5 +6,5 @@ KAFKA_BUFFER_INTERVAL="5000" KAFKA_BUFFER_DYNAMIC=1 KAFKA_TOPIC="usage_reports_v2" PORT=4001 -RATE_LIMIT_ENDPOINT="http://localhost:4012" +COMMERCE_ENDPOINT="http://localhost:4013" OPENTELEMETRY_COLLECTOR_ENDPOINT="" diff --git a/packages/services/usage/README.md b/packages/services/usage/README.md index 9c2bb6c95e..8541115910 100644 --- a/packages/services/usage/README.md +++ b/packages/services/usage/README.md @@ -11,7 +11,7 @@ The data is written to a Kafka broker, form Kafka the data is feed into clickhou | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `PORT` | No | The port this service is running on. | `4001` | | `TOKENS_ENDPOINT` | **Yes** | The endpoint of the tokens service. | `http://127.0.0.1:6001` | -| `RATE_LIMIT_ENDPOINT` | No | The endpoint of the rate limiting service. | `http://127.0.0.1:4012` | +| `COMMERCE_ENDPOINT` | No | The endpoint of the commerce service. | `http://127.0.0.1:4012` | | `KAFKA_TOPIC` | **Yes** | The kafka topic. | `usage_reports_v2` | | `KAFKA_CONSUMER_GROUP` | **Yes** | The kafka consumer group. | `usage_reports_v2` | | `KAFKA_BROKER` | **Yes** | The address of the Kafka broker. | `127.0.0.1:29092` | diff --git a/packages/services/usage/src/environment.ts b/packages/services/usage/src/environment.ts index deb174060e..6d13c764a4 100644 --- a/packages/services/usage/src/environment.ts +++ b/packages/services/usage/src/environment.ts @@ -22,7 +22,7 @@ const emptyString = (input: T) => { const EnvironmentModel = zod.object({ PORT: emptyString(NumberFromString.optional()), TOKENS_ENDPOINT: zod.string().url(), - RATE_LIMIT_ENDPOINT: emptyString(zod.string().url().optional()), + COMMERCE_ENDPOINT: emptyString(zod.string().url().optional()), RATE_LIMIT_TTL: emptyString(NumberFromString.optional()).default(30_000), ENVIRONMENT: emptyString(zod.string().optional()), RELEASE: emptyString(zod.string().optional()), @@ -146,9 +146,9 @@ export const env = { tokens: { endpoint: base.TOKENS_ENDPOINT, }, - rateLimit: base.RATE_LIMIT_ENDPOINT + commerce: base.COMMERCE_ENDPOINT ? { - endpoint: base.RATE_LIMIT_ENDPOINT, + endpoint: base.COMMERCE_ENDPOINT, ttl: base.RATE_LIMIT_TTL, } : null, diff --git a/packages/services/usage/src/index.ts b/packages/services/usage/src/index.ts index c4433b8304..d7a75aed46 100644 --- a/packages/services/usage/src/index.ts +++ b/packages/services/usage/src/index.ts @@ -105,10 +105,10 @@ async function main() { }); const rateLimit = createUsageRateLimit( - env.hive.rateLimit + env.hive.commerce ? { - endpoint: env.hive.rateLimit.endpoint, - ttlMs: env.hive.rateLimit.ttl, + endpoint: env.hive.commerce.endpoint, + ttlMs: env.hive.commerce.ttl, logger: server.log, } : { diff --git a/packages/services/usage/src/rate-limit.ts b/packages/services/usage/src/rate-limit.ts index 85b32e4031..c8068f7e28 100644 --- a/packages/services/usage/src/rate-limit.ts +++ b/packages/services/usage/src/rate-limit.ts @@ -1,5 +1,5 @@ import { LRUCache } from 'lru-cache'; -import type { RateLimitApi } from '@hive/rate-limit'; +import type { CommerceRouter } from '@hive/commerce'; import { ServiceLogger } from '@hive/service-common'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; import { rateLimitDuration } from './metrics'; @@ -69,7 +69,7 @@ export function createUsageRateLimit( }; } const endpoint = config.endpoint.replace(/\/$/, ''); - const rateLimit = createTRPCProxyClient({ + const commerceClient = createTRPCProxyClient({ links: [ httpLink({ url: `${endpoint}/trpc`, @@ -98,7 +98,7 @@ export function createUsageRateLimit( async fetchMethod(input) { const { targetId, token } = rateLimitCacheKey.decodeCacheKey(input); const timer = rateLimitDuration.startTimer(); - const result = await rateLimit.checkRateLimit + const result = await commerceClient.rateLimit.checkRateLimit .query({ id: targetId, type: 'operations-reporting', @@ -130,7 +130,7 @@ export function createUsageRateLimit( // even if multiple requests are waiting for it. fetchMethod(targetId) { const timer = rateLimitDuration.startTimer(); - return rateLimit.getRetention.query({ targetId }).finally(() => { + return commerceClient.rateLimit.getRetention.query({ targetId }).finally(() => { timer({ type: 'retention', }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2bedbd7ed6..1612277235 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,9 +295,9 @@ importers: '@graphql-typed-document-node/core': specifier: 3.2.0 version: 3.2.0(graphql@16.9.0) - '@hive/rate-limit': + '@hive/commerce': specifier: workspace:* - version: link:../packages/services/rate-limit + version: link:../packages/services/commerce '@hive/schema': specifier: workspace:* version: link:../packages/services/schema @@ -831,6 +831,9 @@ importers: slonik: specifier: 30.4.4 version: 30.4.4(patch_hash=408d2a91c53799f60fa2e59860bc29067d20318cbf5844306888d0098b88d299) + stripe: + specifier: 17.5.0 + version: 17.5.0 supertokens-node: specifier: 16.7.5 version: 16.7.5(encoding@0.1.13) @@ -925,6 +928,51 @@ importers: specifier: 3.24.1 version: 3.24.1 + packages/services/commerce: + devDependencies: + '@hive/api': + specifier: workspace:* + version: link:../api + '@hive/emails': + specifier: workspace:* + version: link:../emails + '@hive/service-common': + specifier: workspace:* + version: link:../service-common + '@hive/storage': + specifier: workspace:* + version: link:../storage + '@sentry/node': + specifier: 7.120.2 + version: 7.120.2 + '@trpc/client': + specifier: 10.45.2 + version: 10.45.2(@trpc/server@10.45.2) + '@trpc/server': + specifier: 10.45.2 + version: 10.45.2 + date-fns: + specifier: 4.1.0 + version: 4.1.0 + dotenv: + specifier: 16.4.7 + version: 16.4.7 + fastify: + specifier: 4.29.0 + version: 4.29.0 + pino-pretty: + specifier: 11.3.0 + version: 11.3.0 + reflect-metadata: + specifier: 0.2.2 + version: 0.2.2 + stripe: + specifier: 17.5.0 + version: 17.5.0 + zod: + specifier: 3.24.1 + version: 3.24.1 + packages/services/demo/federation: dependencies: '@apollo/subgraph': @@ -1067,48 +1115,6 @@ importers: specifier: 3.4.0 version: 3.4.0(zod@3.24.1) - packages/services/rate-limit: - devDependencies: - '@hive/emails': - specifier: workspace:* - version: link:../emails - '@hive/service-common': - specifier: workspace:* - version: link:../service-common - '@hive/storage': - specifier: workspace:* - version: link:../storage - '@sentry/node': - specifier: 7.120.2 - version: 7.120.2 - '@trpc/client': - specifier: 10.45.2 - version: 10.45.2(@trpc/server@10.45.2) - '@trpc/server': - specifier: 10.45.2 - version: 10.45.2 - date-fns: - specifier: 4.1.0 - version: 4.1.0 - dotenv: - specifier: 16.4.7 - version: 16.4.7 - got: - specifier: 14.4.5 - version: 14.4.5(patch_hash=f7660444905ddadee251ff98241119fb54f5fec1e673a428192da361d5636299) - pino-pretty: - specifier: 11.3.0 - version: 11.3.0 - reflect-metadata: - specifier: 0.2.2 - version: 0.2.2 - tslib: - specifier: 2.8.1 - version: 2.8.1 - zod: - specifier: 3.24.1 - version: 3.24.1 - packages/services/schema: devDependencies: '@apollo/federation': @@ -1422,45 +1428,6 @@ importers: specifier: 3.24.1 version: 3.24.1 - packages/services/stripe-billing: - devDependencies: - '@hive/service-common': - specifier: workspace:* - version: link:../service-common - '@hive/storage': - specifier: workspace:* - version: link:../storage - '@sentry/node': - specifier: 7.120.2 - version: 7.120.2 - '@trpc/client': - specifier: 10.45.2 - version: 10.45.2(@trpc/server@10.45.2) - '@trpc/server': - specifier: 10.45.2 - version: 10.45.2 - date-fns: - specifier: 4.1.0 - version: 4.1.0 - dotenv: - specifier: 16.4.7 - version: 16.4.7 - got: - specifier: 14.4.5 - version: 14.4.5(patch_hash=f7660444905ddadee251ff98241119fb54f5fec1e673a428192da361d5636299) - pino-pretty: - specifier: 11.3.0 - version: 11.3.0 - reflect-metadata: - specifier: 0.2.2 - version: 0.2.2 - stripe: - specifier: 17.5.0 - version: 17.5.0 - zod: - specifier: 3.24.1 - version: 3.24.1 - packages/services/tokens: devDependencies: '@hive/service-common': @@ -1569,39 +1536,6 @@ importers: specifier: 16.9.0 version: 16.9.0 - packages/services/usage-estimator: - devDependencies: - '@hive/api': - specifier: workspace:* - version: link:../api - '@hive/service-common': - specifier: workspace:* - version: link:../service-common - '@sentry/node': - specifier: 7.120.2 - version: 7.120.2 - '@trpc/server': - specifier: 10.45.2 - version: 10.45.2 - dotenv: - specifier: 16.4.7 - version: 16.4.7 - got: - specifier: 14.4.5 - version: 14.4.5(patch_hash=f7660444905ddadee251ff98241119fb54f5fec1e673a428192da361d5636299) - pino-pretty: - specifier: 11.3.0 - version: 11.3.0 - reflect-metadata: - specifier: 0.2.2 - version: 0.2.2 - tslib: - specifier: 2.8.1 - version: 2.8.1 - zod: - specifier: 3.24.1 - version: 3.24.1 - packages/services/usage-ingestor: devDependencies: '@graphql-hive/core': @@ -26913,7 +26847,7 @@ snapshots: proxy-addr: 2.0.7 rfdc: 1.4.1 secure-json-parse: 2.7.0 - semver: 7.6.2 + semver: 7.6.3 toad-cache: 3.7.0 fastq@1.17.1: diff --git a/tsconfig.json b/tsconfig.json index a702f36101..53c58df10e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -50,17 +50,15 @@ ], "@hive/cdn-script/aws": ["./packages/services/cdn-worker/src/aws.ts"], "@hive/server": ["./packages/services/server/src/api.ts"], - "@hive/stripe-billing": ["./packages/services/stripe-billing/src/api.ts"], "@hive/schema": ["./packages/services/schema/src/api.ts"], "@hive/usage-common": ["./packages/services/usage-common/src/index.ts"], - "@hive/usage-estimator": ["./packages/services/usage-estimator/src/api.ts"], "@hive/usage": ["./packages/services/usage/src/index.ts"], "@hive/usage-ingestor": ["./packages/services/usage-ingestor/src/index.ts"], - "@hive/rate-limit": ["./packages/services/rate-limit/src/api.ts"], "@hive/policy": ["./packages/services/policy/src/api.ts"], "@hive/tokens": ["./packages/services/tokens/src/api.ts"], "@hive/webhooks": ["./packages/services/webhooks/src/api.ts"], "@hive/emails": ["./packages/services/emails/src/api.ts"], + "@hive/commerce": ["./packages/services/commerce/src/api.ts"], "@hive/storage": ["./packages/services/storage/src/index.ts"], "@graphql-hive/yoga": ["./packages/libraries/yoga/src/index.ts"], "@graphql-hive/apollo": ["./packages/libraries/apollo/src/index.ts"], From 498e23308fdbcd23d0cf4e6f348060085881db4a Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Thu, 20 Feb 2025 15:53:20 +0100 Subject: [PATCH 2/4] om --- .../api/src/modules/commerce/providers/billing.provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/services/api/src/modules/commerce/providers/billing.provider.ts b/packages/services/api/src/modules/commerce/providers/billing.provider.ts index c194cb2497..59726123bf 100644 --- a/packages/services/api/src/modules/commerce/providers/billing.provider.ts +++ b/packages/services/api/src/modules/commerce/providers/billing.provider.ts @@ -1,5 +1,4 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; -import LRU from 'lru-cache'; import { OrganizationBilling } from '../../../shared/entities'; import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder'; import { Session } from '../../auth/lib/authz'; From 875f63ff6183b48a4657214f2839c235fe52e622 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Thu, 20 Feb 2025 15:58:36 +0100 Subject: [PATCH 3/4] no --- packages/services/commerce/README.md | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 packages/services/commerce/README.md diff --git a/packages/services/commerce/README.md b/packages/services/commerce/README.md deleted file mode 100644 index 16b2a6622b..0000000000 --- a/packages/services/commerce/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# `@hive/commerce` - -This service takes care of commerce part of Hive Cloud. - -## Configuration - -| Name | Required | Description | Example Value | -| ----------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| `PORT` | No | The port this service is running on. | `4001` | -| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | -| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | -| `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | -| `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) | -| `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `usage-service` | -| `PROMETHEUS_METRICS_PORT` | No | Port on which prometheus metrics are exposed | Defaults to `10254` | -| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) | -| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | -| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | From 76a4f1cff59e73dcb829a7f4a37e6c47bbd898e9 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Thu, 20 Feb 2025 16:00:27 +0100 Subject: [PATCH 4/4] o --- packages/services/commerce/src/index.ts | 1 + packages/services/commerce/src/rate-limit/limiter.ts | 12 ++++-------- .../commerce/src/usage-estimator/estimator.ts | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/services/commerce/src/index.ts b/packages/services/commerce/src/index.ts index e14046d2e6..614fa66f07 100644 --- a/packages/services/commerce/src/index.ts +++ b/packages/services/commerce/src/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import 'reflect-metadata'; import { hostname } from 'os'; import { configureTracing, diff --git a/packages/services/commerce/src/rate-limit/limiter.ts b/packages/services/commerce/src/rate-limit/limiter.ts index 041d4c6da4..2b04d624e9 100644 --- a/packages/services/commerce/src/rate-limit/limiter.ts +++ b/packages/services/commerce/src/rate-limit/limiter.ts @@ -69,10 +69,6 @@ export function createRateLimiter(config: { const fetchAndCalculateUsageInformation = traceInline('Calculate Rate Limit', {}, async () => { const now = new Date(); - const window = { - startTime: startOfMonth(now), - endTime: endOfMonth(now), - }; const timeWindow = { startTime: startOfMonth(now), endTime: endOfMonth(now), @@ -149,8 +145,8 @@ export function createRateLimiter(config: { email: orgRecord.orgEmail, }, period: { - start: window.startTime.getTime(), - end: window.endTime.getTime(), + start: timeWindow.startTime.getTime(), + end: timeWindow.endTime.getTime(), }, usage: { quota: orgRecord.operations.quota, @@ -166,8 +162,8 @@ export function createRateLimiter(config: { email: orgRecord.orgEmail, }, period: { - start: window.startTime.getTime(), - end: window.endTime.getTime(), + start: timeWindow.startTime.getTime(), + end: timeWindow.endTime.getTime(), }, usage: { quota: orgRecord.operations.quota, diff --git a/packages/services/commerce/src/usage-estimator/estimator.ts b/packages/services/commerce/src/usage-estimator/estimator.ts index 4d0d96e311..bb85540c92 100644 --- a/packages/services/commerce/src/usage-estimator/estimator.ts +++ b/packages/services/commerce/src/usage-estimator/estimator.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import { ClickHouse, HttpClient, OperationsReader, sql } from '@hive/api'; import type { ServiceLogger } from '@hive/service-common'; import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics';