kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: drizzle-zod and hono openapi improvements
rodzic
f1437a47d1
commit
05d6b25781
|
@ -36,16 +36,17 @@
|
|||
"test:unit": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentic/faas-utils": "workspace:*",
|
||||
"@agentic/validators": "workspace:*",
|
||||
"@fisch0920/drizzle-zod": "^0.7.2",
|
||||
"@google-cloud/logging": "^11.2.0",
|
||||
"@hono/node-server": "^1.14.1",
|
||||
"@hono/sentry": "^1.2.1",
|
||||
"@hono/zod-openapi": "^0.19.5",
|
||||
"@hono/zod-validator": "^0.4.3",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@sentry/node": "^9.14.0",
|
||||
"@workos-inc/node": "^7.47.0",
|
||||
"drizzle-orm": "^0.43.0",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"eventid": "^2.0.1",
|
||||
"exit-hook": "catalog:",
|
||||
"hono": "^4.7.7",
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
import type { Context } from 'hono'
|
||||
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||
|
||||
export async function healthCheck(c: Context) {
|
||||
return c.json({ status: 'ok' })
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: 'health',
|
||||
responses: {
|
||||
200: {
|
||||
description: 'OK',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
status: z.string()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export function registerHealthCheck(app: OpenAPIHono) {
|
||||
return app.openapi(route, async (c) => {
|
||||
return c.json({ status: 'ok' })
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import { Hono } from 'hono'
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import * as middleware from '@/lib/middleware'
|
||||
|
||||
import { healthCheck } from './health-check'
|
||||
import { registerHealthCheck } from './health-check'
|
||||
import { registerV1UsersGetUser } from './users/get-user'
|
||||
|
||||
export const apiV1 = new Hono()
|
||||
export const apiV1 = new OpenAPIHono()
|
||||
|
||||
const pub = new Hono()
|
||||
const pri = new Hono<AuthenticatedEnv>()
|
||||
const pub = new OpenAPIHono()
|
||||
const pri = new OpenAPIHono<AuthenticatedEnv>()
|
||||
|
||||
pub.get('/health', healthCheck)
|
||||
registerHealthCheck(pub)
|
||||
|
||||
apiV1.route('', pub)
|
||||
// users
|
||||
registerV1UsersGetUser(pri)
|
||||
|
||||
apiV1.route('/', pub)
|
||||
apiV1.use(middleware.authenticate)
|
||||
apiV1.use(middleware.team)
|
||||
apiV1.use(middleware.me)
|
||||
apiV1.route('', pri)
|
||||
apiV1.route('/', pri)
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { db, eq, schema, userIdSchema } from '@/db'
|
||||
import { assert, parseZodSchema } from '@/lib/utils'
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
userId: userIdSchema.openapi({
|
||||
param: {
|
||||
name: 'userId',
|
||||
in: 'path'
|
||||
},
|
||||
example: 'pfh0haxfpzowht3oi213cqos'
|
||||
})
|
||||
})
|
||||
|
||||
const route = createRoute({
|
||||
tags: ['users'],
|
||||
operationId: 'getUser',
|
||||
method: 'get',
|
||||
path: 'users/{userId}',
|
||||
security: [{ bearerAuth: [] }],
|
||||
request: {
|
||||
params: ParamsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'A user object',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.userSelectSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
// ...openApiErrorResponses
|
||||
}
|
||||
})
|
||||
|
||||
export type Route = typeof route
|
||||
export type V1UsersGetUserResponse = z.infer<
|
||||
(typeof route.responses)[200]['content']['application/json']['schema']
|
||||
>
|
||||
|
||||
export const registerV1UsersGetUser = (app: OpenAPIHono<AuthenticatedEnv>) =>
|
||||
app.openapi(route, async (c) => {
|
||||
const { userId } = c.req.valid('param')
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(schema.users.id, userId)
|
||||
})
|
||||
assert(user, 404, `User not found: ${userId}`)
|
||||
|
||||
return c.json(parseZodSchema(schema.userSelectSchema, user))
|
||||
})
|
|
@ -12,4 +12,30 @@ const postgresClient =
|
|||
export const db = drizzle({ client: postgresClient, schema })
|
||||
|
||||
export * as schema from './schema'
|
||||
export * from './schemas'
|
||||
export type * from './types'
|
||||
export {
|
||||
and,
|
||||
arrayContained,
|
||||
arrayContains,
|
||||
between,
|
||||
eq,
|
||||
exists,
|
||||
gt,
|
||||
gte,
|
||||
ilike,
|
||||
inArray,
|
||||
isNotNull,
|
||||
isNull,
|
||||
like,
|
||||
lt,
|
||||
lte,
|
||||
ne,
|
||||
not,
|
||||
notBetween,
|
||||
notExists,
|
||||
notIlike,
|
||||
notInArray,
|
||||
notLike,
|
||||
or
|
||||
} from 'drizzle-orm'
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { validators } from '@agentic/faas-utils'
|
||||
import { validators } from '@agentic/validators'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
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 {
|
||||
type Coupon,
|
||||
couponSchema,
|
||||
type PricingPlan,
|
||||
pricingPlanSchema
|
||||
} from './types'
|
||||
import { users } from './user'
|
||||
import {
|
||||
createInsertSchema,
|
||||
|
@ -105,12 +111,24 @@ export const deploymentInsertSchema = createInsertSchema(deployments, {
|
|||
message: 'Invalid deployment hash'
|
||||
}),
|
||||
|
||||
_url: (schema) => schema.url()
|
||||
_url: (schema) => schema.url(),
|
||||
|
||||
build: z.object({}),
|
||||
env: z.object({}),
|
||||
pricingPlans: z.array(pricingPlanSchema),
|
||||
coupons: z.array(couponSchema).optional()
|
||||
})
|
||||
|
||||
export const deploymentSelectSchema = createSelectSchema(deployments).omit({
|
||||
_url: true
|
||||
export const deploymentSelectSchema = createSelectSchema(deployments, {
|
||||
build: z.object({}),
|
||||
env: z.object({}),
|
||||
pricingPlans: z.array(pricingPlanSchema),
|
||||
coupons: z.array(couponSchema)
|
||||
})
|
||||
.omit({
|
||||
_url: true
|
||||
})
|
||||
.openapi('Deployment')
|
||||
|
||||
export const deploymentUpdateSchema = createUpdateSchema(deployments).pick({
|
||||
enabled: true,
|
||||
|
|
|
@ -4,4 +4,3 @@ export * from './team'
|
|||
export * from './team-member'
|
||||
export type * from './types'
|
||||
export * from './user'
|
||||
export * from './utils'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { validators } from '@agentic/faas-utils'
|
||||
import { validators } from '@agentic/validators'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
boolean,
|
||||
|
@ -11,9 +12,9 @@ import {
|
|||
|
||||
import { getProviderToken } from '@/lib/auth/get-provider-token'
|
||||
|
||||
import type { Webhook } from './types'
|
||||
import { deployments } from './deployment'
|
||||
import { teams } from './team'
|
||||
import { type Webhook, webhookSchema } from './types'
|
||||
import { users } from './user'
|
||||
import {
|
||||
createInsertSchema,
|
||||
|
@ -149,15 +150,30 @@ export const projectInsertSchema = createInsertSchema(projects, {
|
|||
}
|
||||
})
|
||||
|
||||
export const projectSelectSchema = createSelectSchema(projects).omit({
|
||||
_secret: true,
|
||||
_providerToken: true,
|
||||
_text: true,
|
||||
_webhooks: true,
|
||||
_stripeCouponIds: true,
|
||||
_stripePlanIds: true,
|
||||
_stripeAccountId: true
|
||||
export const projectSelectSchema = createSelectSchema(projects, {
|
||||
_webhooks: z.array(webhookSchema),
|
||||
stripeMetricProductIds: z.record(z.string(), z.string()).optional(),
|
||||
_stripeCouponIds: z.record(z.string(), z.string()).optional(),
|
||||
_stripePlanIds: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
basePlanId: z.string(),
|
||||
requestPlanId: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
.omit({
|
||||
_secret: true,
|
||||
_providerToken: true,
|
||||
_text: true,
|
||||
_webhooks: true,
|
||||
_stripeCouponIds: true,
|
||||
_stripePlanIds: true,
|
||||
_stripeAccountId: true
|
||||
})
|
||||
.openapi('Project')
|
||||
|
||||
// TODO: narrow update schema
|
||||
export const projectUpdateSchema = createUpdateSchema(projects)
|
||||
|
|
|
@ -9,7 +9,12 @@ import {
|
|||
|
||||
import { teams } from './team'
|
||||
import { users } from './user'
|
||||
import { cuid, teamMemberRoleEnum, timestamps } from './utils'
|
||||
import {
|
||||
createSelectSchema,
|
||||
cuid,
|
||||
teamMemberRoleEnum,
|
||||
timestamps
|
||||
} from './utils'
|
||||
|
||||
export const teamMembers = pgTable(
|
||||
'team_members',
|
||||
|
@ -25,7 +30,7 @@ export const teamMembers = pgTable(
|
|||
role: teamMemberRoleEnum().default('user').notNull(),
|
||||
|
||||
confirmed: boolean().default(false),
|
||||
confirmedAt: timestamp()
|
||||
confirmedAt: timestamp({ mode: 'string' })
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.userId, table.teamId] }),
|
||||
|
@ -46,3 +51,6 @@ export const teamMembersRelations = relations(teamMembers, ({ one }) => ({
|
|||
references: [teams.id]
|
||||
})
|
||||
}))
|
||||
|
||||
export const teamMemberSelectSchema =
|
||||
createSelectSchema(teamMembers).openapi('TeamMember')
|
||||
|
|
|
@ -41,5 +41,5 @@ export const teamsRelations = relations(teams, ({ one, many }) => ({
|
|||
export const teamInsertSchema = createInsertSchema(teams, {
|
||||
slug: (schema) => schema.min(3).max(20) // TODO
|
||||
})
|
||||
export const teamSelectSchema = createSelectSchema(teams)
|
||||
export const teamSelectSchema = createSelectSchema(teams).openapi('Team')
|
||||
export const teamUpdateSchema = createUpdateSchema(teams).omit({ slug: true })
|
||||
|
|
|
@ -1,146 +1,161 @@
|
|||
export type AuthProviderType =
|
||||
| 'github'
|
||||
| 'google'
|
||||
| 'spotify'
|
||||
| 'twitter'
|
||||
| 'linkedin'
|
||||
| 'stripe'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
export type AuthProvider = {
|
||||
provider: AuthProviderType
|
||||
export const authProviderTypeSchema = z
|
||||
.enum(['github', 'google', 'spotify', 'twitter', 'linkedin', 'stripe'])
|
||||
.openapi('AuthProviderType')
|
||||
export type AuthProviderType = z.infer<typeof authProviderTypeSchema>
|
||||
|
||||
/** Provider-specific user id */
|
||||
id: string
|
||||
export const authProviderSchema = z
|
||||
.object({
|
||||
provider: authProviderTypeSchema,
|
||||
|
||||
/** Provider-specific username */
|
||||
username?: string
|
||||
/** Provider-specific user id */
|
||||
id: z.string(),
|
||||
|
||||
/** Standard oauth2 access token */
|
||||
accessToken?: string
|
||||
/** Provider-specific username */
|
||||
username: z.string().optional(),
|
||||
|
||||
/** Standard oauth2 refresh token */
|
||||
refreshToken?: string
|
||||
/** Standard oauth2 access token */
|
||||
accessToken: z.string().optional(),
|
||||
|
||||
/** Stripe public key */
|
||||
publicKey?: string
|
||||
/** Standard oauth2 refresh token */
|
||||
refreshToken: z.string().optional(),
|
||||
|
||||
/** OAuth scope(s) */
|
||||
scope?: string
|
||||
}
|
||||
/** Stripe public key */
|
||||
publicKey: z.string().optional(),
|
||||
|
||||
export type AuthProviders = {
|
||||
github?: AuthProvider
|
||||
google?: AuthProvider
|
||||
spotify?: AuthProvider
|
||||
twitter?: AuthProvider
|
||||
linkedin?: AuthProvider
|
||||
stripeTest?: AuthProvider
|
||||
stripeLive?: AuthProvider
|
||||
}
|
||||
/** OAuth scope(s) */
|
||||
scope: z.string().optional()
|
||||
})
|
||||
.openapi('AuthProvider')
|
||||
export type AuthProvider = z.infer<typeof authProviderSchema>
|
||||
|
||||
export type Webhook = {
|
||||
url: string
|
||||
events: string[]
|
||||
}
|
||||
export const authProvidersSchema = z
|
||||
.record(authProviderTypeSchema, authProviderSchema)
|
||||
.openapi('AuthProviders')
|
||||
export type AuthProviders = z.infer<typeof authProvidersSchema>
|
||||
|
||||
export type RateLimit = {
|
||||
enabled: boolean
|
||||
export const webhookSchema = z
|
||||
.object({
|
||||
url: z.string(),
|
||||
events: z.array(z.string())
|
||||
})
|
||||
.openapi('Webhook')
|
||||
export type Webhook = z.infer<typeof webhookSchema>
|
||||
|
||||
// informal description that overrides any other properties
|
||||
desc?: string
|
||||
export const rateLimitSchema = z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
|
||||
interval: number // seconds
|
||||
maxPerInterval: number // unitless
|
||||
}
|
||||
// informal description that overrides any other properties
|
||||
desc: z.string().optional(),
|
||||
|
||||
export type PricingPlanTier = {
|
||||
unitAmount?: number
|
||||
flatAmount?: number
|
||||
upTo: string
|
||||
} & (
|
||||
| {
|
||||
unitAmount: number
|
||||
interval: z.number(), // seconds
|
||||
maxPerInterval: z.number() // unitless
|
||||
})
|
||||
.openapi('RateLimit')
|
||||
export type RateLimit = z.infer<typeof rateLimitSchema>
|
||||
|
||||
export const pricingPlanTierSchema = z
|
||||
.object({
|
||||
unitAmount: z.number().optional(),
|
||||
flatAmount: z.number().optional(),
|
||||
upTo: z.string()
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.unitAmount !== undefined) !== (data.flatAmount !== undefined),
|
||||
{
|
||||
message: 'Either unitAmount or flatAmount must be provided, but not both'
|
||||
}
|
||||
| {
|
||||
flatAmount: number
|
||||
}
|
||||
)
|
||||
)
|
||||
.openapi('PricingPlanTier')
|
||||
export type PricingPlanTier = z.infer<typeof pricingPlanTierSchema>
|
||||
|
||||
export type PricingPlanMetric = {
|
||||
// slug acts as a primary key for metrics
|
||||
slug: string
|
||||
export const pricingPlanMetricSchema = z
|
||||
.object({
|
||||
// slug acts as a primary key for metrics
|
||||
slug: z.string(),
|
||||
|
||||
amount: number
|
||||
amount: z.number(),
|
||||
|
||||
label: string
|
||||
unitLabel: string
|
||||
label: z.string(),
|
||||
unitLabel: z.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'
|
||||
// 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: z.enum(['licensed', 'metered']),
|
||||
|
||||
billingScheme: 'per_unit' | 'tiered'
|
||||
billingScheme: z.enum(['per_unit', 'tiered']),
|
||||
|
||||
tiersMode: 'graduated' | 'volume'
|
||||
tiers: PricingPlanTier[]
|
||||
tiersMode: z.enum(['graduated', 'volume']),
|
||||
tiers: z.array(pricingPlanTierSchema),
|
||||
|
||||
// TODO (low priority): add aggregateUsage
|
||||
// TODO (low priority): add aggregateUsage
|
||||
|
||||
rateLimit?: RateLimit
|
||||
}
|
||||
rateLimit: rateLimitSchema.optional()
|
||||
})
|
||||
.openapi('PricingPlanMetric')
|
||||
export type PricingPlanMetric = z.infer<typeof pricingPlanMetricSchema>
|
||||
|
||||
export type PricingPlan = {
|
||||
name: string
|
||||
slug: string
|
||||
export const pricingPlanSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
|
||||
desc?: string
|
||||
features: string[]
|
||||
desc: z.string().optional(),
|
||||
features: z.array(z.string()),
|
||||
|
||||
auth: boolean
|
||||
amount: number
|
||||
trialPeriodDays?: number
|
||||
auth: z.boolean(),
|
||||
amount: z.number(),
|
||||
trialPeriodDays: z.number().optional(),
|
||||
|
||||
requests: PricingPlanMetric
|
||||
metrics: PricingPlanMetric[]
|
||||
requests: pricingPlanMetricSchema,
|
||||
metrics: z.array(pricingPlanMetricSchema),
|
||||
|
||||
rateLimit?: RateLimit
|
||||
rateLimit: rateLimitSchema.optional(),
|
||||
|
||||
// used to uniquely identify this plan across deployments
|
||||
baseId: string
|
||||
// used to uniquely identify this plan across deployments
|
||||
baseId: z.string(),
|
||||
|
||||
// used to uniquely identify this plan across deployments
|
||||
requestsId: string
|
||||
// used to uniquely identify this plan across deployments
|
||||
requestsId: z.string(),
|
||||
|
||||
// [metricSlug: string]: string
|
||||
metricIds: Record<string, string>
|
||||
// [metricSlug: string]: string
|
||||
metricIds: z.record(z.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
|
||||
// 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: z.string(),
|
||||
stripeRequestPlan: z.string(),
|
||||
|
||||
// [metricSlug: string]: string
|
||||
stripeMetricPlans: Record<string, string>
|
||||
}
|
||||
// [metricSlug: string]: string
|
||||
stripeMetricPlans: z.record(z.string())
|
||||
})
|
||||
.openapi('PricingPlan')
|
||||
export type PricingPlan = z.infer<typeof pricingPlanSchema>
|
||||
|
||||
export type Coupon = {
|
||||
// used to uniquely identify this coupon across deployments
|
||||
id: string
|
||||
export const couponSchema = z
|
||||
.object({
|
||||
// used to uniquely identify this coupon across deployments
|
||||
id: z.string(),
|
||||
|
||||
valid: boolean
|
||||
stripeCoupon: string
|
||||
valid: z.boolean(),
|
||||
stripeCoupon: z.string(),
|
||||
|
||||
name?: string
|
||||
name: z.string().optional(),
|
||||
|
||||
currency?: string
|
||||
amount_off?: number
|
||||
percent_off?: number
|
||||
currency: z.string().optional(),
|
||||
amount_off: z.number().optional(),
|
||||
percent_off: z.number().optional(),
|
||||
|
||||
duration: string
|
||||
duration_in_months?: number
|
||||
duration: z.string(),
|
||||
duration_in_months: z.number().optional(),
|
||||
|
||||
redeem_by?: Date
|
||||
max_redemptions?: number
|
||||
}
|
||||
redeem_by: z.date().optional(),
|
||||
max_redemptions: z.number().optional()
|
||||
})
|
||||
.openapi('Coupon')
|
||||
export type Coupon = z.infer<typeof couponSchema>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { validators } from '@agentic/faas-utils'
|
||||
import { validators } from '@agentic/validators'
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
boolean,
|
||||
|
@ -12,8 +12,8 @@ import {
|
|||
|
||||
import { sha256 } from '@/lib/utils'
|
||||
|
||||
import type { AuthProviders } from './types'
|
||||
import { teams } from './team'
|
||||
import { type AuthProviders, authProvidersSchema } from './types'
|
||||
import {
|
||||
createInsertSchema,
|
||||
createSelectSchema,
|
||||
|
@ -42,7 +42,7 @@ export const users = pgTable(
|
|||
image: text(),
|
||||
|
||||
emailConfirmed: boolean().default(false),
|
||||
emailConfirmedAt: timestamp(),
|
||||
emailConfirmedAt: timestamp({ mode: 'string' }),
|
||||
emailConfirmToken: text().unique().default(sha256()),
|
||||
passwordResetToken: text().unique(),
|
||||
|
||||
|
@ -85,7 +85,9 @@ export const userInsertSchema = createInsertSchema(users, {
|
|||
{
|
||||
message: 'Invalid email'
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
providers: authProvidersSchema.optional()
|
||||
}).pick({
|
||||
username: true,
|
||||
email: true,
|
||||
|
@ -95,5 +97,8 @@ export const userInsertSchema = createInsertSchema(users, {
|
|||
image: true
|
||||
})
|
||||
|
||||
export const userSelectSchema = createSelectSchema(users)
|
||||
export const userSelectSchema = createSelectSchema(users, {
|
||||
providers: authProvidersSchema.optional()
|
||||
}).openapi('User')
|
||||
|
||||
export const userUpdateSchema = createUpdateSchema(users)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { createSchemaFactory } from '@fisch0920/drizzle-zod'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { sql, type Writable } from 'drizzle-orm'
|
||||
import {
|
||||
|
@ -7,7 +9,6 @@ import {
|
|||
timestamp,
|
||||
varchar
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { createSchemaFactory } from 'drizzle-zod'
|
||||
|
||||
export function cuid<U extends string, T extends Readonly<[U, ...U[]]>>(
|
||||
config?: PgVarcharConfig<T | Writable<T>, never>
|
||||
|
@ -44,8 +45,8 @@ export const id = varchar('id', { length: 24 })
|
|||
.$defaultFn(createId)
|
||||
|
||||
export const timestamps = {
|
||||
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updatedAt')
|
||||
createdAt: timestamp('createdAt', { mode: 'string' }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updatedAt', { mode: 'string' })
|
||||
.notNull()
|
||||
.default(sql`now()`)
|
||||
}
|
||||
|
@ -53,8 +54,31 @@ export const timestamps = {
|
|||
export const userRoleEnum = pgEnum('UserRole', ['user', 'admin'])
|
||||
export const teamMemberRoleEnum = pgEnum('TeamMemberRole', ['user', 'admin'])
|
||||
|
||||
// TODO: Currently unused after forking drizzle-zod.
|
||||
// export function makeNullablePropsOptional<Schema extends z.AnyZodObject>(
|
||||
// schema: Schema
|
||||
// ): z.ZodObject<{
|
||||
// [key in keyof Schema['shape']]: Schema['shape'][key] extends z.ZodNullable<
|
||||
// infer T
|
||||
// >
|
||||
// ? z.ZodOptional<T>
|
||||
// : Schema['shape'][key]
|
||||
// }> {
|
||||
// const entries = Object.entries(schema.shape)
|
||||
// const newProps: any = {}
|
||||
|
||||
// for (const [key, value] of entries) {
|
||||
// newProps[key] =
|
||||
// value instanceof z.ZodNullable ? value.unwrap().optional() : value
|
||||
// return newProps
|
||||
// }
|
||||
|
||||
// return z.object(newProps) as any
|
||||
// }
|
||||
|
||||
export const { createInsertSchema, createSelectSchema, createUpdateSchema } =
|
||||
createSchemaFactory({
|
||||
zodInstance: z,
|
||||
coerce: {
|
||||
// Coerce dates / strings to timetamps
|
||||
date: true
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { validators } from '@agentic/validators'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
function getCuidSchema(idLabel: string) {
|
||||
return z.string().refine((id) => validators.cuid(id), {
|
||||
message: `Invalid ${idLabel}`
|
||||
})
|
||||
}
|
||||
|
||||
export const cuidSchema = getCuidSchema('id')
|
||||
export const userIdSchema = getCuidSchema('user id')
|
||||
|
||||
export const projectIdSchema = z
|
||||
.string()
|
||||
.refine((id) => validators.project(id), {
|
||||
message: 'Invalid project id'
|
||||
})
|
||||
|
||||
export const deploymentIdSchema = z
|
||||
.string()
|
||||
.refine((id) => validators.deployment(id), {
|
||||
message: 'Invalid deployment id'
|
||||
})
|
|
@ -1,19 +1,20 @@
|
|||
import type { z } from '@hono/zod-openapi'
|
||||
import type { BuildQueryResult, ExtractTablesWithRelations } from 'drizzle-orm'
|
||||
|
||||
import type * as schema from './schema'
|
||||
|
||||
export type Tables = ExtractTablesWithRelations<typeof schema>
|
||||
|
||||
export type User = typeof schema.users.$inferSelect
|
||||
export type User = z.infer<typeof schema.userSelectSchema>
|
||||
|
||||
export type Team = typeof schema.teams.$inferSelect
|
||||
export type Team = z.infer<typeof schema.teamSelectSchema>
|
||||
export type TeamWithMembers = BuildQueryResult<
|
||||
Tables,
|
||||
Tables['teams'],
|
||||
{ with: { members: true } }
|
||||
>
|
||||
|
||||
export type TeamMember = typeof schema.teamMembers.$inferSelect
|
||||
export type TeamMember = z.infer<typeof schema.teamMemberSelectSchema>
|
||||
export type TeamMemberWithTeam = BuildQueryResult<
|
||||
Tables,
|
||||
Tables['teamMembers'],
|
||||
|
|
|
@ -20,7 +20,11 @@ export function initExitHooks({
|
|||
// Gracefully shutdown the HTTP server
|
||||
asyncExitHook(
|
||||
async function shutdownServerExitHook() {
|
||||
await promisify(server.close)()
|
||||
try {
|
||||
await promisify(server.close)()
|
||||
} catch {
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
{
|
||||
wait: timeoutMs
|
||||
|
@ -30,9 +34,13 @@ export function initExitHooks({
|
|||
// Gracefully shutdown the postgres database connection
|
||||
asyncExitHook(
|
||||
async function shutdownDbExitHook() {
|
||||
await db.$client.end({
|
||||
timeout: timeoutMs
|
||||
})
|
||||
try {
|
||||
await db.$client.end({
|
||||
timeout: timeoutMs
|
||||
})
|
||||
} catch {
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
{
|
||||
wait: timeoutMs
|
||||
|
|
|
@ -2,7 +2,7 @@ import '@/lib/instrument'
|
|||
|
||||
import { serve } from '@hono/node-server'
|
||||
import { sentry } from '@hono/sentry'
|
||||
import { Hono } from 'hono'
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
import { compress } from 'hono/compress'
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
|
@ -12,20 +12,26 @@ import * as middleware from '@/lib/middleware'
|
|||
|
||||
import { initExitHooks } from './lib/exit-hooks'
|
||||
|
||||
export const app = new Hono()
|
||||
export const app = new OpenAPIHono()
|
||||
|
||||
app.use(sentry())
|
||||
app.use(compress())
|
||||
app.use(middleware.accessLogger)
|
||||
// app.use(middleware.accessLogger)
|
||||
app.use(middleware.responseTime)
|
||||
app.use(middleware.errorHandler)
|
||||
app.use(cors())
|
||||
|
||||
app.route('/v1', apiV1)
|
||||
|
||||
app.doc31('/docs', {
|
||||
openapi: '3.1.0',
|
||||
info: { title: 'Agentic', version: '1.0.0' }
|
||||
})
|
||||
|
||||
const server = serve({
|
||||
fetch: app.fetch,
|
||||
port: env.PORT
|
||||
})
|
||||
console.log(`Server running on port ${env.PORT}`)
|
||||
|
||||
initExitHooks({ server })
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"name": "@agentic/faas-utils",
|
||||
"name": "@agentic/validators",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Agentic platform FaaS utils.",
|
||||
"description": "Validation utils for the Agentic platform.",
|
||||
"author": "Travis Fischer <travis@transitivebullsh.it>",
|
||||
"license": "UNLICENSED",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/transitive-bullshit/agentic-platform.git",
|
||||
"directory": "packages/faas-utils"
|
||||
"directory": "packages/validators"
|
||||
},
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
|
@ -24,6 +24,7 @@
|
|||
"test:unit": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"email-validator": "^2.0.4",
|
||||
"is-relative-url": "^4.0.0"
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { isCuid } from '@paralleldrive/cuid2'
|
||||
import emailValidator from 'email-validator'
|
||||
import isRelativeUrl from 'is-relative-url'
|
||||
|
||||
|
@ -50,3 +51,7 @@ export function serviceName(value: string): boolean {
|
|||
export function servicePath(value: string): boolean {
|
||||
return !!value && servicePathRe.test(value) && isRelativeUrl(value)
|
||||
}
|
||||
|
||||
export function cuid(value: string): boolean {
|
||||
return !!value && isCuid(value)
|
||||
}
|
|
@ -125,9 +125,12 @@ importers:
|
|||
|
||||
apps/api:
|
||||
dependencies:
|
||||
'@agentic/faas-utils':
|
||||
'@agentic/validators':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/faas-utils
|
||||
version: link:../../packages/validators
|
||||
'@fisch0920/drizzle-zod':
|
||||
specifier: ^0.7.2
|
||||
version: 0.7.2(drizzle-orm@0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5))(zod@3.24.3)
|
||||
'@google-cloud/logging':
|
||||
specifier: ^11.2.0
|
||||
version: 11.2.0
|
||||
|
@ -137,6 +140,9 @@ importers:
|
|||
'@hono/sentry':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(hono@4.7.7)
|
||||
'@hono/zod-openapi':
|
||||
specifier: ^0.19.5
|
||||
version: 0.19.5(hono@4.7.7)(zod@3.24.3)
|
||||
'@hono/zod-validator':
|
||||
specifier: ^0.4.3
|
||||
version: 0.4.3(hono@4.7.7)(zod@3.24.3)
|
||||
|
@ -148,13 +154,10 @@ importers:
|
|||
version: 9.14.0
|
||||
'@workos-inc/node':
|
||||
specifier: ^7.47.0
|
||||
version: 7.47.0
|
||||
version: 7.48.0
|
||||
drizzle-orm:
|
||||
specifier: ^0.43.0
|
||||
version: 0.43.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5)
|
||||
drizzle-zod:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1(drizzle-orm@0.43.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5))(zod@3.24.3)
|
||||
version: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5)
|
||||
eventid:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
|
@ -196,8 +199,11 @@ importers:
|
|||
specifier: ^0.31.0
|
||||
version: 0.31.0
|
||||
|
||||
packages/faas-utils:
|
||||
packages/validators:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2':
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
email-validator:
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
|
@ -207,6 +213,11 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@asteasolutions/zod-to-openapi@7.3.0':
|
||||
resolution: {integrity: sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==}
|
||||
peerDependencies:
|
||||
zod: ^3.20.2
|
||||
|
||||
'@babel/code-frame@7.26.2':
|
||||
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -558,6 +569,12 @@ packages:
|
|||
prettier: '>= 3'
|
||||
typescript: '>= 5'
|
||||
|
||||
'@fisch0920/drizzle-zod@0.7.2':
|
||||
resolution: {integrity: sha512-f+ltSymKaD14RsZKiSBMoYsbxFrlKYHiybIDkDTPVUMrMQgpjdNleeYvjmyV2h47n73WxCQHF6K/4sTH8/GuTA==}
|
||||
peerDependencies:
|
||||
drizzle-orm: '>=0.36.0'
|
||||
zod: '>=3.0.0'
|
||||
|
||||
'@google-cloud/common@5.0.2':
|
||||
resolution: {integrity: sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
@ -598,6 +615,13 @@ packages:
|
|||
peerDependencies:
|
||||
hono: '>=3.*'
|
||||
|
||||
'@hono/zod-openapi@0.19.5':
|
||||
resolution: {integrity: sha512-n2RqdZL7XIaWPwBNygctG/1eySyRtSBnS7l+pIsP3f2JW5P2l7Smm6SLluscrGwB5l2C2fxbfvhWoC6Ig+SxXw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
peerDependencies:
|
||||
hono: '>=4.3.6'
|
||||
zod: 3.*
|
||||
|
||||
'@hono/zod-validator@0.4.3':
|
||||
resolution: {integrity: sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ==}
|
||||
peerDependencies:
|
||||
|
@ -1257,8 +1281,8 @@ packages:
|
|||
'@vitest/utils@3.1.2':
|
||||
resolution: {integrity: sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==}
|
||||
|
||||
'@workos-inc/node@7.47.0':
|
||||
resolution: {integrity: sha512-A7K6uIIGmD5qbrLxoXah2BcOzbxyxnrhps+crZ/CFImrkq4CdVAm6BV/wvRaTeAinRF+Ea9cIFFeEAvJ++RaSw==}
|
||||
'@workos-inc/node@7.48.0':
|
||||
resolution: {integrity: sha512-dS0wpf8MqezcPsYcEbpW4eLcXZ7wOh0X9Qne4VVumrDTnC60kuxpMWIUAH1iPjXlocd8LKg13aYeP7mHSLVeCw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
|
@ -1616,8 +1640,8 @@ packages:
|
|||
resolution: {integrity: sha512-pcKVT+GbfPA+bUovPIilgVOoq+onNBo/YQBG86sf3/GFHkN6lRJPm1l7dKN0IMAk57RQoIm4GUllRrasLlcaSg==}
|
||||
hasBin: true
|
||||
|
||||
drizzle-orm@0.43.0:
|
||||
resolution: {integrity: sha512-OF6ZOtpGJs3CNXHGwKLfP+mYXEzTnXNL/WRXgAGR+SrtPl6quIBbTPEQZNQ6HhVQchMmJeaezBIcpFBpJD3x+g==}
|
||||
drizzle-orm@0.43.1:
|
||||
resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==}
|
||||
peerDependencies:
|
||||
'@aws-sdk/client-rds-data': '>=3'
|
||||
'@cloudflare/workers-types': '>=4'
|
||||
|
@ -1705,12 +1729,6 @@ packages:
|
|||
sqlite3:
|
||||
optional: true
|
||||
|
||||
drizzle-zod@0.7.1:
|
||||
resolution: {integrity: sha512-nZzALOdz44/AL2U005UlmMqaQ1qe5JfanvLujiTHiiT8+vZJTBFhj3pY4Vk+L6UWyKFfNmLhk602Hn4kCTynKQ==}
|
||||
peerDependencies:
|
||||
drizzle-orm: '>=0.36.0'
|
||||
zod: '>=3.0.0'
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -2733,6 +2751,9 @@ packages:
|
|||
resolution: {integrity: sha512-M7CJbmv7UCopc0neRKdzfoGWaVZC+xC1925GitKH9EAqYFzX9//25Q7oX4+jw0tiCCj+t5l6VZh8UPH23NZkMA==}
|
||||
hasBin: true
|
||||
|
||||
openapi3-ts@4.4.0:
|
||||
resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
@ -2808,8 +2829,8 @@ packages:
|
|||
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg-pool@3.9.5:
|
||||
resolution: {integrity: sha512-DxyAlOgvUzRFpFAZjbCc8fUfG7BcETDHgepFPf724B0i08k9PAiZV1tkGGgQIL0jbMEuR9jW1YN7eX+WgXxCsQ==}
|
||||
pg-pool@3.9.6:
|
||||
resolution: {integrity: sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
|
@ -3703,6 +3724,11 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@asteasolutions/zod-to-openapi@7.3.0(zod@3.24.3)':
|
||||
dependencies:
|
||||
openapi3-ts: 4.4.0
|
||||
zod: 3.24.3
|
||||
|
||||
'@babel/code-frame@7.26.2':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
|
@ -3941,6 +3967,11 @@ snapshots:
|
|||
- supports-color
|
||||
- vitest
|
||||
|
||||
'@fisch0920/drizzle-zod@0.7.2(drizzle-orm@0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5))(zod@3.24.3)':
|
||||
dependencies:
|
||||
drizzle-orm: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5)
|
||||
zod: 3.24.3
|
||||
|
||||
'@google-cloud/common@5.0.2':
|
||||
dependencies:
|
||||
'@google-cloud/projectify': 4.0.0
|
||||
|
@ -4008,6 +4039,13 @@ snapshots:
|
|||
hono: 4.7.7
|
||||
toucan-js: 4.1.1
|
||||
|
||||
'@hono/zod-openapi@0.19.5(hono@4.7.7)(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@asteasolutions/zod-to-openapi': 7.3.0(zod@3.24.3)
|
||||
'@hono/zod-validator': 0.4.3(hono@4.7.7)(zod@3.24.3)
|
||||
hono: 4.7.7
|
||||
zod: 3.24.3
|
||||
|
||||
'@hono/zod-validator@0.4.3(hono@4.7.7)(zod@3.24.3)':
|
||||
dependencies:
|
||||
hono: 4.7.7
|
||||
|
@ -4769,7 +4807,7 @@ snapshots:
|
|||
loupe: 3.1.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@workos-inc/node@7.47.0':
|
||||
'@workos-inc/node@7.48.0':
|
||||
dependencies:
|
||||
iron-session: 6.3.1
|
||||
jose: 5.6.3
|
||||
|
@ -5140,18 +5178,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
drizzle-orm@0.43.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5):
|
||||
drizzle-orm@0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5):
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/pg': 8.11.13
|
||||
pg: 8.15.5
|
||||
postgres: 3.4.5
|
||||
|
||||
drizzle-zod@0.7.1(drizzle-orm@0.43.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5))(zod@3.24.3):
|
||||
dependencies:
|
||||
drizzle-orm: 0.43.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.13)(pg@8.15.5)(postgres@3.4.5)
|
||||
zod: 3.24.3
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
|
@ -6381,6 +6414,10 @@ snapshots:
|
|||
dependencies:
|
||||
which-pm-runs: 1.1.0
|
||||
|
||||
openapi3-ts@4.4.0:
|
||||
dependencies:
|
||||
yaml: 2.7.1
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
|
@ -6447,7 +6484,7 @@ snapshots:
|
|||
|
||||
pg-numeric@1.0.2: {}
|
||||
|
||||
pg-pool@3.9.5(pg@8.15.5):
|
||||
pg-pool@3.9.6(pg@8.15.5):
|
||||
dependencies:
|
||||
pg: 8.15.5
|
||||
optional: true
|
||||
|
@ -6475,7 +6512,7 @@ snapshots:
|
|||
pg@8.15.5:
|
||||
dependencies:
|
||||
pg-connection-string: 2.8.5
|
||||
pg-pool: 3.9.5(pg@8.15.5)
|
||||
pg-pool: 3.9.6(pg@8.15.5)
|
||||
pg-protocol: 1.9.5
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
|
|
Ładowanie…
Reference in New Issue