kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: WIP add drizzle postgres db schemas
rodzic
6743740d6f
commit
9efa186ea0
|
@ -0,0 +1,14 @@
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
|
import { env } from '@/lib/env'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './src/db/schema',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: env.DATABASE_URL
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "agentic-platform-api",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Agentic platform API.",
|
||||||
|
"author": "Travis Fischer <travis@transitivebullsh.it>",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/transitive-bullshit/agentic-platform.git",
|
||||||
|
"directory": "apps/api"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"source": "./src/index.ts",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"sideEffects": false,
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"clean": "del dist",
|
||||||
|
"test": "run-s test:*",
|
||||||
|
"test:lint": "eslint .",
|
||||||
|
"test:typecheck": "tsc --noEmit",
|
||||||
|
"test:unit": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
|
"drizzle-orm": "^0.42.0",
|
||||||
|
"drizzle-zod": "^0.7.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"postgres": "^3.4.5",
|
||||||
|
"type-fest": "catalog:",
|
||||||
|
"zod": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"drizzle-kit": "^0.31.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { drizzle, type NodePgClient } from 'drizzle-orm/node-postgres'
|
||||||
|
import postgres from 'postgres'
|
||||||
|
|
||||||
|
import { env } from '@/lib/env'
|
||||||
|
|
||||||
|
import * as schema from './schema'
|
||||||
|
|
||||||
|
let _postgresClient: NodePgClient | undefined
|
||||||
|
const postgresClient =
|
||||||
|
_postgresClient ?? (_postgresClient = postgres(env.DATABASE_URL))
|
||||||
|
|
||||||
|
export const db = drizzle({ client: postgresClient, schema })
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { relations } from 'drizzle-orm'
|
||||||
|
import { boolean, index, jsonb, pgTable, text } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
import type { Coupon, PricingPlan } from './types'
|
||||||
|
import { projects } from './project'
|
||||||
|
import { teams } from './team'
|
||||||
|
import { users } from './user'
|
||||||
|
import {
|
||||||
|
createInsertSchema,
|
||||||
|
createSelectSchema,
|
||||||
|
createUpdateSchema,
|
||||||
|
timestamps
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
export const deployments = pgTable(
|
||||||
|
'deployments',
|
||||||
|
{
|
||||||
|
// namespace/projectName@hash
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
...timestamps,
|
||||||
|
|
||||||
|
hash: text().notNull(),
|
||||||
|
version: text(),
|
||||||
|
|
||||||
|
enabled: boolean().notNull().default(true),
|
||||||
|
published: boolean().notNull().default(false),
|
||||||
|
|
||||||
|
description: text().notNull().default(''),
|
||||||
|
readme: text().notNull().default(''),
|
||||||
|
|
||||||
|
userId: text()
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
teamId: text().references(() => teams.id),
|
||||||
|
projectId: text()
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, {
|
||||||
|
onDelete: 'cascade'
|
||||||
|
}),
|
||||||
|
|
||||||
|
// TODO: tools?
|
||||||
|
// services: jsonb().$type<Service[]>().default([]),
|
||||||
|
|
||||||
|
// Environment variables & secrets
|
||||||
|
build: jsonb().$type<object>(),
|
||||||
|
env: jsonb().$type<object>(),
|
||||||
|
|
||||||
|
// TODO: metadata config (logo, keywords, etc)
|
||||||
|
// TODO: webhooks
|
||||||
|
// TODO: third-party auth provider config?
|
||||||
|
|
||||||
|
// Backend API URL
|
||||||
|
_url: text().notNull(),
|
||||||
|
|
||||||
|
pricingPlans: jsonb().$type<PricingPlan[]>(),
|
||||||
|
coupons: jsonb().$type<Coupon[]>()
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('deployment_userId_idx').on(table.userId),
|
||||||
|
index('deployment_teamId_idx').on(table.teamId),
|
||||||
|
index('deployment_projectId_idx').on(table.projectId),
|
||||||
|
index('deployment_enabled_idx').on(table.enabled),
|
||||||
|
index('deployment_published_idx').on(table.published),
|
||||||
|
index('deployment_version_idx').on(table.version),
|
||||||
|
index('deployment_createdAt_idx').on(table.createdAt),
|
||||||
|
index('deployment_updatedAt_idx').on(table.updatedAt)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [deployments.userId],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
team: one(teams, {
|
||||||
|
fields: [deployments.teamId],
|
||||||
|
references: [teams.id]
|
||||||
|
}),
|
||||||
|
project: one(projects, {
|
||||||
|
fields: [deployments.projectId],
|
||||||
|
references: [projects.id]
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// TODO: virtual hasFreeTier
|
||||||
|
// TODO: virtual url
|
||||||
|
// TODO: virtual openApiUrl
|
||||||
|
// TODO: virtual saasUrl
|
||||||
|
// TODO: virtual authProviders?
|
||||||
|
// TODO: virtual openapi spec? (hide openapi.servers)
|
||||||
|
|
||||||
|
export type Deployment = typeof deployments.$inferSelect
|
||||||
|
|
||||||
|
// TODO: narrow
|
||||||
|
export const deploymentInsertSchema = createInsertSchema(deployments, {
|
||||||
|
// TODO: validate deployment id
|
||||||
|
// id: (schema) =>
|
||||||
|
// schema.refine((id) => validators.deployment(id), 'Invalid deployment id')
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deploymentSelectSchema = createSelectSchema(deployments).omit({
|
||||||
|
_url: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: narrow
|
||||||
|
export const deploymentUpdateSchema = createUpdateSchema(deployments).pick({
|
||||||
|
enabled: true,
|
||||||
|
published: true,
|
||||||
|
version: true,
|
||||||
|
description: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: add admin select schema which includes all fields?
|
|
@ -0,0 +1,7 @@
|
||||||
|
export * from './deployment'
|
||||||
|
export * from './project'
|
||||||
|
export * from './team'
|
||||||
|
export * from './team-members'
|
||||||
|
export type * from './types'
|
||||||
|
export * from './user'
|
||||||
|
export * from './utils'
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { relations } from 'drizzle-orm'
|
||||||
|
import {
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
pgTable,
|
||||||
|
text
|
||||||
|
} from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
import { getProviderToken } from '@/lib/auth/get-provider-token'
|
||||||
|
|
||||||
|
import type { Webhook } from './types'
|
||||||
|
import { deployments } from './deployment'
|
||||||
|
import { teams } from './team'
|
||||||
|
import { users } from './user'
|
||||||
|
import {
|
||||||
|
createInsertSchema,
|
||||||
|
createSelectSchema,
|
||||||
|
createUpdateSchema,
|
||||||
|
timestamps
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
export const projects = pgTable(
|
||||||
|
'projects',
|
||||||
|
{
|
||||||
|
// namespace/projectName
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
...timestamps,
|
||||||
|
|
||||||
|
name: text().notNull(),
|
||||||
|
alias: text(),
|
||||||
|
|
||||||
|
userId: text()
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
teamId: text().notNull(),
|
||||||
|
|
||||||
|
// Most recently published Deployment if one exists
|
||||||
|
lastPublishedDeploymentId: text(),
|
||||||
|
|
||||||
|
// Most recent Deployment if one exists
|
||||||
|
lastDeploymentId: text(),
|
||||||
|
|
||||||
|
applicationFeePercent: integer().notNull().default(20),
|
||||||
|
|
||||||
|
// TODO: This is going to need to vary from dev to prod
|
||||||
|
isStripeConnectEnabled: boolean().notNull().default(false),
|
||||||
|
|
||||||
|
// All deployments share the same underlying proxy secret
|
||||||
|
_secret: text(),
|
||||||
|
|
||||||
|
// Auth token used to access the saasify API on behalf of this project
|
||||||
|
_providerToken: text().notNull(),
|
||||||
|
|
||||||
|
// TODO: Full-text search
|
||||||
|
_text: text().default(''),
|
||||||
|
|
||||||
|
_webhooks: jsonb().$type<Webhook[]>().default([]),
|
||||||
|
|
||||||
|
// Stripe products corresponding to the stripe plans across deployments
|
||||||
|
stripeBaseProduct: text(),
|
||||||
|
stripeRequestProduct: text(),
|
||||||
|
|
||||||
|
// [metricSlug: string]: string
|
||||||
|
stripeMetricProducts: jsonb().$type<Record<string, string>>().default({}),
|
||||||
|
|
||||||
|
// Stripe coupons associated with this project, mapping from unique coupon
|
||||||
|
// hash to stripe coupon id.
|
||||||
|
// `[hash: string]: string`
|
||||||
|
_stripeCoupons: jsonb().$type<Record<string, string>>().default({}),
|
||||||
|
|
||||||
|
// Stripe billing plans associated with this project (created lazily),
|
||||||
|
// mapping from unique plan hash to stripe plan ids for base and request
|
||||||
|
// respectively.
|
||||||
|
// `[hash: string]: { basePlan: string, requestPlan: string }`
|
||||||
|
_stripePlans: jsonb()
|
||||||
|
.$type<Record<string, { basePlan: string; requestPlan: string }>>()
|
||||||
|
.default({}),
|
||||||
|
|
||||||
|
// Connected Stripe account (standard or express).
|
||||||
|
// If not defined, then subscriptions for this project route through our
|
||||||
|
// main Stripe account.
|
||||||
|
// NOTE: the connected account is shared between dev and prod, so we're not using
|
||||||
|
// the stripeID utility.
|
||||||
|
// TODO: is it wise to share this between dev and prod?
|
||||||
|
// TODO: is it okay for this to be public?
|
||||||
|
_stripeAccount: text()
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('project_userId_idx').on(table.userId),
|
||||||
|
index('project_teamId_idx').on(table.teamId),
|
||||||
|
index('project_teamId_idx').on(table.teamId),
|
||||||
|
index('project_createdAt_idx').on(table.createdAt),
|
||||||
|
index('project_updatedAt_idx').on(table.updatedAt)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const projectsRelations = relations(projects, ({ one, many }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [projects.userId],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
team: one(teams, {
|
||||||
|
fields: [projects.teamId],
|
||||||
|
references: [teams.id]
|
||||||
|
}),
|
||||||
|
lastPublishedDeployment: one(deployments, {
|
||||||
|
fields: [projects.lastPublishedDeploymentId],
|
||||||
|
references: [deployments.id],
|
||||||
|
relationName: 'lastPublishedDeployment'
|
||||||
|
}),
|
||||||
|
lastDeployment: one(deployments, {
|
||||||
|
fields: [projects.lastDeploymentId],
|
||||||
|
references: [deployments.id],
|
||||||
|
relationName: 'lastDeployment'
|
||||||
|
}),
|
||||||
|
deployments: many(deployments, { relationName: 'deployments' }),
|
||||||
|
publishedDeployments: many(deployments, {
|
||||||
|
relationName: 'publishedDeployments'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
export type Project = typeof projects.$inferSelect
|
||||||
|
|
||||||
|
export const projectInsertSchema = createInsertSchema(projects, {
|
||||||
|
// TODO: validate project id
|
||||||
|
// id: (schema) =>
|
||||||
|
// schema.refine((id) => validators.project(id), 'Invalid project id')
|
||||||
|
// TODO: validate project name
|
||||||
|
// name: (schema) =>
|
||||||
|
// schema.refine((name) => validators.projectName(name), 'Invalid project name')
|
||||||
|
})
|
||||||
|
.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
userId: true
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
_providerToken: getProviderToken(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const projectSelectSchema = createSelectSchema(projects).omit({
|
||||||
|
_secret: true,
|
||||||
|
_providerToken: true,
|
||||||
|
_text: true,
|
||||||
|
_webhooks: true,
|
||||||
|
_stripeCoupons: true,
|
||||||
|
_stripePlans: true,
|
||||||
|
_stripeAccount: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: narrow update schema
|
||||||
|
export const projectUpdateSchema = createUpdateSchema(projects)
|
||||||
|
|
||||||
|
export const projectDebugSelectSchema = createSelectSchema(projects).pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
alias: true,
|
||||||
|
userId: true,
|
||||||
|
teamId: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
isStripeConnectEnabled: true,
|
||||||
|
lastPublishedDeploymentId: true,
|
||||||
|
lastDeploymentId: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: virtual saasUrl
|
||||||
|
// TODO: virtual aliasUrl
|
||||||
|
// TODO: virtual stripeConnectParams
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { relations } from 'drizzle-orm'
|
||||||
|
import {
|
||||||
|
index,
|
||||||
|
pgTable,
|
||||||
|
primaryKey,
|
||||||
|
text,
|
||||||
|
uniqueIndex
|
||||||
|
} from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
import { teams } from './team'
|
||||||
|
import { users } from './user'
|
||||||
|
import { teamMemberRoleEnum, timestamps } from './utils'
|
||||||
|
|
||||||
|
export const teamMembers = pgTable(
|
||||||
|
'team_members',
|
||||||
|
{
|
||||||
|
...timestamps,
|
||||||
|
|
||||||
|
userId: text()
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
teamId: text()
|
||||||
|
.notNull()
|
||||||
|
.references(() => teams.id, { onDelete: 'cascade' }),
|
||||||
|
role: teamMemberRoleEnum().default('user').notNull()
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
primaryKey({ columns: [table.userId, table.teamId] }),
|
||||||
|
uniqueIndex('team_member_user_idx').on(table.userId),
|
||||||
|
uniqueIndex('team_member_team_idx').on(table.teamId),
|
||||||
|
index('team_member_createdAt_idx').on(table.createdAt),
|
||||||
|
index('team_member_updatedAt_idx').on(table.updatedAt)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const teamMembersRelations = relations(teamMembers, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [teamMembers.userId],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
team: one(teams, {
|
||||||
|
fields: [teamMembers.teamId],
|
||||||
|
references: [teams.id]
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
export type TeamMember = typeof teamMembers.$inferSelect
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { relations } from 'drizzle-orm'
|
||||||
|
import { index, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
import { teamMembers } from './team-members'
|
||||||
|
import { users } from './user'
|
||||||
|
import {
|
||||||
|
createInsertSchema,
|
||||||
|
createSelectSchema,
|
||||||
|
createUpdateSchema,
|
||||||
|
id,
|
||||||
|
timestamps
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
export const teams = pgTable(
|
||||||
|
'teams',
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
...timestamps,
|
||||||
|
|
||||||
|
slug: text().notNull().unique(),
|
||||||
|
name: text().notNull(),
|
||||||
|
|
||||||
|
ownerId: text('owner').notNull()
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex('team_slug_idx').on(table.slug),
|
||||||
|
index('team_createdAt_idx').on(table.createdAt),
|
||||||
|
index('team_updatedAt_idx').on(table.updatedAt)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const teamsRelations = relations(teams, ({ one, many }) => ({
|
||||||
|
owner: one(users, {
|
||||||
|
fields: [teams.ownerId],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
members: many(teamMembers)
|
||||||
|
}))
|
||||||
|
|
||||||
|
export type Team = typeof teams.$inferSelect
|
||||||
|
|
||||||
|
export const teamInsertSchema = createInsertSchema(teams, {
|
||||||
|
slug: (schema) => schema.min(3).max(20) // TODO
|
||||||
|
})
|
||||||
|
export const teamSelectSchema = createSelectSchema(teams)
|
||||||
|
export const teamUpdateSchema = createUpdateSchema(teams).omit({ slug: true })
|
|
@ -0,0 +1,146 @@
|
||||||
|
export type AuthProviderType =
|
||||||
|
| 'github'
|
||||||
|
| 'google'
|
||||||
|
| 'spotify'
|
||||||
|
| 'twitter'
|
||||||
|
| 'linkedin'
|
||||||
|
| 'stripe'
|
||||||
|
|
||||||
|
export type AuthProvider = {
|
||||||
|
provider: AuthProviderType
|
||||||
|
|
||||||
|
/** Provider-specific user id */
|
||||||
|
id: string
|
||||||
|
|
||||||
|
/** Provider-specific username */
|
||||||
|
username?: string
|
||||||
|
|
||||||
|
/** Standard oauth2 access token */
|
||||||
|
accessToken?: string
|
||||||
|
|
||||||
|
/** Standard oauth2 refresh token */
|
||||||
|
refreshToken?: string
|
||||||
|
|
||||||
|
/** Stripe public key */
|
||||||
|
publicKey?: string
|
||||||
|
|
||||||
|
/** OAuth scope(s) */
|
||||||
|
scope?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthProviders = {
|
||||||
|
github?: AuthProvider
|
||||||
|
google?: AuthProvider
|
||||||
|
spotify?: AuthProvider
|
||||||
|
twitter?: AuthProvider
|
||||||
|
linkedin?: AuthProvider
|
||||||
|
stripeTest?: AuthProvider
|
||||||
|
stripeLive?: AuthProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Webhook = {
|
||||||
|
url: string
|
||||||
|
events: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RateLimit = {
|
||||||
|
enabled: boolean
|
||||||
|
|
||||||
|
// informal description that overrides any other properties
|
||||||
|
desc?: string
|
||||||
|
|
||||||
|
interval: number // seconds
|
||||||
|
maxPerInterval: number // unitless
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingPlanTier = {
|
||||||
|
unitAmount?: number
|
||||||
|
flatAmount?: number
|
||||||
|
upTo: string
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
unitAmount: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
flatAmount: number
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type PricingPlanMetric = {
|
||||||
|
// slug acts as a primary key for metrics
|
||||||
|
slug: string
|
||||||
|
|
||||||
|
amount: number
|
||||||
|
|
||||||
|
label: string
|
||||||
|
unitLabel: string
|
||||||
|
|
||||||
|
// TODO: should this default be 'licensed' or 'metered'?
|
||||||
|
// methinks licensed for "sites", "jobs", etc...
|
||||||
|
// TODO: this should probably be explicit since its easy to confuse
|
||||||
|
usageType: 'licensed' | 'metered'
|
||||||
|
|
||||||
|
billingScheme: 'per_unit' | 'tiered'
|
||||||
|
|
||||||
|
tiersMode: 'graduated' | 'volume'
|
||||||
|
tiers: PricingPlanTier[]
|
||||||
|
|
||||||
|
// TODO (low priority): add aggregateUsage
|
||||||
|
|
||||||
|
rateLimit?: RateLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingPlan = {
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
|
||||||
|
desc?: string
|
||||||
|
features: string[]
|
||||||
|
|
||||||
|
auth: boolean
|
||||||
|
amount: number
|
||||||
|
trialPeriodDays?: number
|
||||||
|
|
||||||
|
requests: PricingPlanMetric
|
||||||
|
metrics: PricingPlanMetric[]
|
||||||
|
|
||||||
|
rateLimit?: RateLimit
|
||||||
|
|
||||||
|
// used to uniquely identify this plan across deployments
|
||||||
|
baseId: string
|
||||||
|
|
||||||
|
// used to uniquely identify this plan across deployments
|
||||||
|
requestsId: string
|
||||||
|
|
||||||
|
// [metricSlug: string]: string
|
||||||
|
metricIds: Record<string, string>
|
||||||
|
|
||||||
|
// NOTE: the stripe billing plan id(s) for this PricingPlan are referenced
|
||||||
|
// in the Project._stripePlans mapping via the plan's hash.
|
||||||
|
// NOTE: all metered billing usage is stored in stripe
|
||||||
|
stripeBasePlan: string
|
||||||
|
stripeRequestPlan: string
|
||||||
|
|
||||||
|
// [metricSlug: string]: string
|
||||||
|
stripeMetricPlans: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Coupon = {
|
||||||
|
// used to uniquely identify this coupon across deployments
|
||||||
|
id: string
|
||||||
|
|
||||||
|
valid: boolean
|
||||||
|
stripeCoupon: string
|
||||||
|
|
||||||
|
name?: string
|
||||||
|
|
||||||
|
currency?: string
|
||||||
|
amount_off?: number
|
||||||
|
percent_off?: number
|
||||||
|
|
||||||
|
duration: string
|
||||||
|
duration_in_months?: number
|
||||||
|
|
||||||
|
redeem_by?: Date
|
||||||
|
max_redemptions?: number
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { relations } from 'drizzle-orm'
|
||||||
|
import {
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
jsonb,
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex
|
||||||
|
} from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
import { sha256 } from '@/lib/utils'
|
||||||
|
|
||||||
|
import type { AuthProviders } from './types'
|
||||||
|
import { teams } from './team'
|
||||||
|
import {
|
||||||
|
createInsertSchema,
|
||||||
|
createSelectSchema,
|
||||||
|
createUpdateSchema,
|
||||||
|
id,
|
||||||
|
timestamps,
|
||||||
|
userRoleEnum
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
export const users = pgTable(
|
||||||
|
'users',
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
...timestamps,
|
||||||
|
|
||||||
|
username: text().notNull().unique(),
|
||||||
|
role: userRoleEnum().default('user').notNull(),
|
||||||
|
|
||||||
|
email: text().unique(),
|
||||||
|
password: text(),
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
firstName: text(),
|
||||||
|
lastName: text(),
|
||||||
|
image: text(),
|
||||||
|
|
||||||
|
emailConfirmed: boolean().default(false),
|
||||||
|
emailConfirmedAt: timestamp(),
|
||||||
|
emailConfirmToken: text().unique().default(sha256()),
|
||||||
|
passwordResetToken: text().unique(),
|
||||||
|
|
||||||
|
isStripeConnectEnabledByDefault: boolean().default(true),
|
||||||
|
|
||||||
|
// third-party auth providers
|
||||||
|
providers: jsonb().$type<AuthProviders>().default({}),
|
||||||
|
|
||||||
|
stripeCustomerId: text()
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex('user_email_idx').on(table.email),
|
||||||
|
uniqueIndex('user_username_idx').on(table.username),
|
||||||
|
uniqueIndex('user_emailConfirmToken_idx').on(table.emailConfirmToken),
|
||||||
|
uniqueIndex('user_passwordResetToken_idx').on(table.passwordResetToken),
|
||||||
|
index('user_createdAt_idx').on(table.createdAt),
|
||||||
|
index('user_updatedAt_idx').on(table.updatedAt)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const usersRelations = relations(users, ({ many }) => ({
|
||||||
|
teamsOwned: many(teams)
|
||||||
|
|
||||||
|
// TODO: team memberships
|
||||||
|
}))
|
||||||
|
|
||||||
|
export type User = typeof users.$inferSelect
|
||||||
|
|
||||||
|
export const userInsertSchema = createInsertSchema(users).pick({
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
password: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
image: true
|
||||||
|
})
|
||||||
|
|
||||||
|
export const userSelectSchema = createSelectSchema(users)
|
||||||
|
export const userUpdateSchema = createUpdateSchema(users)
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
import { sql } from 'drizzle-orm'
|
||||||
|
import { pgEnum, text, timestamp } from 'drizzle-orm/pg-core'
|
||||||
|
import { createSchemaFactory } from 'drizzle-zod'
|
||||||
|
|
||||||
|
export const id = text('id').primaryKey().$defaultFn(createId)
|
||||||
|
|
||||||
|
export const timestamps = {
|
||||||
|
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updatedAt')
|
||||||
|
.notNull()
|
||||||
|
.default(sql`now()`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userRoleEnum = pgEnum('UserRole', ['user', 'admin'])
|
||||||
|
export const teamMemberRoleEnum = pgEnum('TeamMemberRole', ['user', 'admin'])
|
||||||
|
|
||||||
|
export const { createInsertSchema, createSelectSchema, createUpdateSchema } =
|
||||||
|
createSchemaFactory({
|
||||||
|
coerce: {
|
||||||
|
// Coerce dates / strings to timetamps
|
||||||
|
date: true
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,9 @@
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
|
import { env } from '@/lib/env'
|
||||||
|
|
||||||
|
export function getProviderToken(project: { id: string }) {
|
||||||
|
// TODO: Possibly in the future store stripe account ID as well and require
|
||||||
|
// provider tokens to refresh after account changes?
|
||||||
|
return jwt.sign({ projectId: project.id }, env.JWT_SECRET)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const envSchema = z.object({
|
||||||
|
NODE_ENV: z
|
||||||
|
.enum(['development', 'test', 'production'])
|
||||||
|
.default('development'),
|
||||||
|
DATABASE_URL: z.string().url(),
|
||||||
|
JWT_SECRET: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-process-env
|
||||||
|
export const env = envSchema.parse(process.env)
|
||||||
|
|
||||||
|
export const isProd = env.NODE_ENV === 'production'
|
|
@ -0,0 +1 @@
|
||||||
|
export type Operation = 'create' | 'read' | 'update' | 'delete' | 'debug'
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createHash, randomUUID } from 'node:crypto'
|
||||||
|
|
||||||
|
export function sha256(input: string = randomUUID()) {
|
||||||
|
return createHash('sha256').update(input).digest('hex')
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
import '@fisch0920/config/ts-reset'
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "@fisch0920/config/tsconfig-node",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src", "drizzle.config.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
|
@ -1,3 +1,15 @@
|
||||||
import { config } from '@fisch0920/config/eslint'
|
import { config } from '@fisch0920/config/eslint'
|
||||||
|
import drizzle from 'eslint-plugin-drizzle'
|
||||||
|
|
||||||
export default [ ...config ]
|
export default [
|
||||||
|
...config,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
plugins: {
|
||||||
|
drizzle
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...drizzle.configs.recommended.rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "agentic-platform",
|
"name": "agentic-platform",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Travis Fischer <travis@transitivebullsh.it>",
|
"author": "Travis Fischer <travis@transitivebullsh.it>",
|
||||||
"license": "PROPRIETARY",
|
"license": "UNLICENSED",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/transitive-bullshit/agentic-platform.git"
|
"url": "git+https://github.com/transitive-bullshit/agentic-platform.git"
|
||||||
|
@ -33,6 +33,7 @@
|
||||||
"del-cli": "catalog:",
|
"del-cli": "catalog:",
|
||||||
"dotenv": "catalog:",
|
"dotenv": "catalog:",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
|
"eslint-plugin-drizzle": "^0.2.3",
|
||||||
"lint-staged": "catalog:",
|
"lint-staged": "catalog:",
|
||||||
"npm-run-all2": "catalog:",
|
"npm-run-all2": "catalog:",
|
||||||
"only-allow": "catalog:",
|
"only-allow": "catalog:",
|
||||||
|
|
883
pnpm-lock.yaml
883
pnpm-lock.yaml
Plik diff jest za duży
Load Diff
|
@ -1,19 +1,19 @@
|
||||||
packages:
|
packages:
|
||||||
- packages/*
|
- packages/*
|
||||||
- services/*
|
- apps/*
|
||||||
catalog:
|
catalog:
|
||||||
'@ai-sdk/openai': ^1.3.6
|
'@ai-sdk/openai': ^1.3.6
|
||||||
'@fisch0920/config': ^1.0.2
|
'@fisch0920/config': ^1.0.3
|
||||||
'@modelcontextprotocol/sdk': ^1.8.0
|
'@modelcontextprotocol/sdk': ^1.8.0
|
||||||
'@types/node': ^22.14.0
|
'@types/node': ^22.14.1
|
||||||
ai: ^4.2.11
|
ai: ^4.2.11
|
||||||
cleye: ^1.3.4
|
cleye: ^1.3.4
|
||||||
del-cli: ^6.0.0
|
del-cli: ^6.0.0
|
||||||
dotenv: ^16.4.7
|
dotenv: ^16.5.0
|
||||||
eslint: ^9.23.0
|
eslint: ^9.25.0
|
||||||
exit-hook: ^4.0.0
|
exit-hook: ^4.0.0
|
||||||
ky: ^1.8.0
|
ky: ^1.8.0
|
||||||
lint-staged: ^15.5.0
|
lint-staged: ^15.5.1
|
||||||
npm-run-all2: ^7.0.2
|
npm-run-all2: ^7.0.2
|
||||||
only-allow: ^1.2.1
|
only-allow: ^1.2.1
|
||||||
p-map: ^7.0.3
|
p-map: ^7.0.3
|
||||||
|
@ -24,7 +24,7 @@ catalog:
|
||||||
tsup: ^8.4.0
|
tsup: ^8.4.0
|
||||||
tsx: ^4.19.3
|
tsx: ^4.19.3
|
||||||
turbo: ^2.5.0
|
turbo: ^2.5.0
|
||||||
type-fest: ^4.39.1
|
type-fest: ^4.40.0
|
||||||
typescript: ^5.8.2
|
typescript: ^5.8.3
|
||||||
vitest: ^3.1.1
|
vitest: ^3.1.1
|
||||||
zod: ^3.24.2
|
zod: ^3.24.3
|
||||||
|
|
Ładowanie…
Reference in New Issue