From 67ffdd89f693f23fa25520871c5006a7e23149e5 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Tue, 29 Apr 2025 19:24:21 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/db/schema/consumer.ts | 139 +++++++++++++++++++++++++++++ apps/api/src/db/schema/index.ts | 1 + apps/api/src/db/types.ts | 7 ++ 3 files changed, 147 insertions(+) create mode 100644 apps/api/src/db/schema/consumer.ts diff --git a/apps/api/src/db/schema/consumer.ts b/apps/api/src/db/schema/consumer.ts new file mode 100644 index 00000000..58e164e4 --- /dev/null +++ b/apps/api/src/db/schema/consumer.ts @@ -0,0 +1,139 @@ +import { relations } from '@fisch0920/drizzle-orm' +import { + boolean, + index, + jsonb, + pgTable, + text +} from '@fisch0920/drizzle-orm/pg-core' + +import { deployments } from './deployment' +import { projects } from './project' +import { users } from './user' +import { + createInsertSchema, + createSelectSchema, + createUpdateSchema, + cuid, + deploymentId, + id, + projectId, + stripeId, + timestamps +} from './utils' + +// TODO: Consumers should be valid for any enabled project like in RapidAPI and GCP. +// This may require a separate model to aggregate User Applications. +// https://docs.rapidapi.com/docs/keys#section-different-api-keys-per-application + +export const consumers = pgTable( + 'consumers', + { + id, + ...timestamps, + + token: text().notNull(), + plan: text(), + + activated: boolean().default(false).notNull(), + enabled: boolean().default(true).notNull(), + + env: text().default('dev').notNull(), + coupon: text(), + + // stripe subscription status (synced via webhooks) + status: text(), + + // only used during initial creation + source: text(), + + userId: cuid() + .notNull() + .references(() => users.id), + + // The project this user is subscribed to + projectId: projectId() + .notNull() + .references(() => projects.id, { + onDelete: 'cascade' + }), + + // The specific deployment this user is subscribed to + // (since pricing can change across deployment versions) + deploymentId: deploymentId() + .notNull() + .references(() => deployments.id, { + onDelete: 'cascade' + }), + + stripeSubscriptionId: stripeId().notNull(), + stripeSubscriptionBaseItemId: stripeId(), + stripeSubscriptionRequestItemId: stripeId(), + + // [metricSlug: string]: string + stripeSubscriptionMetricItems: jsonb() + .$type>() + .default({}) + .notNull(), + + // Denormalized from User or possibly separate for stripe connect + // TODO: is this necessary? + _stripeCustomerId: stripeId().notNull() + }, + (table) => [ + index('consumer_token_idx').on(table.token), + index('consumer_env_idx').on(table.env), + index('consumer_userId_idx').on(table.userId), + index('consumer_projectId_idx').on(table.projectId), + index('consumer_deploymentId_idx').on(table.deploymentId), + index('consumer_createdAt_idx').on(table.createdAt), + index('consumer_updatedAt_idx').on(table.updatedAt) + ] +) + +export const consumersRelations = relations(consumers, ({ one }) => ({ + user: one(users, { + fields: [consumers.userId], + references: [users.id] + }), + project: one(projects, { + fields: [consumers.projectId], + references: [projects.id] + }), + deployment: one(deployments, { + fields: [consumers.deploymentId], + references: [deployments.id] + }) +})) + +const stripeValidSubscriptionStatuses = new Set([ + 'trialing', + 'active', + 'incomplete', + 'past_due' +]) + +export const consumerInsertSchema = createInsertSchema(consumers).pick({ + token: true, + plan: true, + env: true, + coupon: true, + source: true, + userId: true, + projectId: true, + deploymentId: true +}) + +export const consumerSelectSchema = + createSelectSchema(consumers).openapi('Consumer') + +export const consumerUpdateSchema = createUpdateSchema(consumers).refine( + (data) => { + return { + ...data, + enabled: + data.plan === 'free' || + (data.status && stripeValidSubscriptionStatuses.has(data.status)) + } + } +) diff --git a/apps/api/src/db/schema/index.ts b/apps/api/src/db/schema/index.ts index e772b256..1af77e5c 100644 --- a/apps/api/src/db/schema/index.ts +++ b/apps/api/src/db/schema/index.ts @@ -1,3 +1,4 @@ +export * from './consumer' export * from './deployment' export * from './project' export * from './team' diff --git a/apps/api/src/db/types.ts b/apps/api/src/db/types.ts index f55551dd..ae5c8887 100644 --- a/apps/api/src/db/types.ts +++ b/apps/api/src/db/types.ts @@ -37,3 +37,10 @@ export type DeploymentWithProject = BuildQueryResult< Tables['deployments'], { with: { project: true } } > + +export type Consumer = z.infer +export type ConsumerWithProjectAndDeployment = BuildQueryResult< + Tables, + Tables['consumers'], + { with: { project: true; deployment: true } } +>