pull/715/head
Travis Fischer 2025-05-20 01:46:53 +07:00
rodzic 4ec31b176c
commit 095c513b52
13 zmienionych plików z 229 dodań i 140 usunięć

Wyświetl plik

@ -3,6 +3,7 @@ import { fromError } from 'zod-validation-error'
import type { AuthenticatedEnv } from '@/lib/types'
import * as middleware from '@/lib/middleware'
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
import { registerV1AdminConsumersGetConsumerByToken } from './consumers/admin-get-consumer-by-token'
import { registerV1ConsumersCreateConsumer } from './consumers/create-consumer'
@ -51,6 +52,8 @@ apiV1.openAPIRegistry.registerComponent('securitySchemes', 'Bearer', {
bearerFormat: 'JWT'
})
registerOpenAPIErrorResponses(apiV1)
// Public routes
const publicRouter = new OpenAPIHono()

Wyświetl plik

@ -3,6 +3,8 @@ import 'dotenv/config'
import { parseZodSchema } from '@agentic/platform-core'
import { z } from 'zod'
import { logLevelsSchema } from './logger'
export const envSchema = z.object({
NODE_ENV: z
.enum(['development', 'test', 'production'])
@ -10,13 +12,14 @@ export const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string(),
PORT: z.number().default(3000),
JWT_SECRET: z.string().nonempty(),
SENTRY_DSN: z.string().url(),
PORT: z.number().default(3000),
LOG_LEVEL: logLevelsSchema.default('info'),
STRIPE_SECRET_KEY: z.string(),
STRIPE_PUBLISHABLE_KEY: z.string(),
STRIPE_WEBHOOK_SECRET: z.string()
STRIPE_SECRET_KEY: z.string().nonempty(),
STRIPE_PUBLISHABLE_KEY: z.string().nonempty(),
STRIPE_WEBHOOK_SECRET: z.string().nonempty()
})
export type Env = z.infer<typeof envSchema>

Wyświetl plik

@ -1,4 +1,6 @@
import { assert } from '@agentic/platform-core'
import * as Sentry from '@sentry/node'
import { z } from 'zod'
import type { Environment, Service } from '@/lib/types'
import { env } from '@/lib/env'
@ -13,7 +15,17 @@ export interface Logger {
error(message?: any, ...detail: any[]): void
}
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error'
const rawLogLevels = ['trace', 'debug', 'info', 'warn', 'error'] as const
export const logLevelsSchema = z.enum(rawLogLevels)
export type LogLevel = z.infer<typeof logLevelsSchema>
export const logLevelsMap = rawLogLevels.reduce(
(acc, level, index) => {
acc[level] = index
return acc
},
{} as Record<LogLevel, number>
)
export class ConsoleLogger implements Logger {
protected readonly environment: Environment
@ -21,44 +33,70 @@ export class ConsoleLogger implements Logger {
protected readonly requestId: string
protected readonly metadata: Record<string, unknown>
protected readonly console: Console
protected readonly logLevel: LogLevel
constructor({
requestId,
service,
environment = env.NODE_ENV,
metadata = {},
console = globalThis.console
console = globalThis.console,
logLevel = env.LOG_LEVEL
}: {
requestId: string
service: Service
environment?: Environment
metadata?: Record<string, unknown>
console?: Console
logLevel?: LogLevel
}) {
assert(console, 500, '`console` is required for Logger')
this.requestId = requestId
this.service = service
this.environment = environment
this.metadata = metadata
this.console = console
this.logLevel = logLevel
}
trace(message?: any, ...detail: any[]) {
if (logLevelsMap[this.logLevel] > logLevelsMap.trace) {
return
}
this.console.trace(this._marshal('trace', message, ...detail))
}
debug(message?: any, ...detail: any[]) {
if (logLevelsMap[this.logLevel] > logLevelsMap.debug) {
return
}
this.console.debug(this._marshal('debug', message, ...detail))
}
info(message?: any, ...detail: any[]) {
if (logLevelsMap[this.logLevel] > logLevelsMap.info) {
return
}
this.console.info(this._marshal('info', message, ...detail))
}
warn(message?: any, ...detail: any[]) {
if (logLevelsMap[this.logLevel] > logLevelsMap.warn) {
return
}
this.console.warn(this._marshal('warn', message, ...detail))
}
error(message?: any, ...detail: any[]) {
if (logLevelsMap[this.logLevel] > logLevelsMap.error) {
return
}
this.console.error(this._marshal('error', message, ...detail))
}

Wyświetl plik

@ -8,7 +8,7 @@ import { unless } from './unless'
export const accessLogger = unless(
createMiddleware<DefaultEnv>(async (ctx, next) => {
const logger = ctx.get('logger')
await honoLogger(logger.trace)(ctx, next)
await honoLogger(logger.trace.bind(logger))(ctx, next)
}),
'/v1/health'
)

Wyświetl plik

@ -1,46 +1,32 @@
import { z } from '@hono/zod-openapi'
const openapiErrorContent = {
'application/json': {
schema: z.object({
error: z.string()
})
}
} as const
import type { OpenAPIHono } from '@hono/zod-openapi'
export const openapiErrorResponses = {
400: {
description: 'Bad Request',
content: openapiErrorContent
$ref: '#/components/responses/400'
},
401: {
description: 'Unauthorized',
content: openapiErrorContent
$ref: '#/components/responses/401'
},
403: {
description: 'Forbidden',
content: openapiErrorContent
$ref: '#/components/responses/403'
}
} as const
export const openapiErrorResponse404 = {
404: {
description: 'Not Found',
content: openapiErrorContent
$ref: '#/components/responses/404'
}
} as const
export const openapiErrorResponse409 = {
409: {
description: 'Conflict',
content: openapiErrorContent
$ref: '#/components/responses/409'
}
} as const
export const openapiErrorResponse410 = {
410: {
description: 'Gone',
content: openapiErrorContent
$ref: '#/components/responses/410'
}
} as const
@ -50,3 +36,49 @@ export const openapiAuthenticatedSecuritySchemas = [
Bearer: []
}
]
const openapiErrorContent = {
'application/json': {
schema: {
type: 'object' as const,
properties: {
error: {
type: 'string' as const
}
},
required: ['error' as const]
}
}
}
export function registerOpenAPIErrorResponses(app: OpenAPIHono) {
app.openAPIRegistry.registerComponent('responses', '400', {
description: 'Bad Request',
content: openapiErrorContent
})
app.openAPIRegistry.registerComponent('responses', '401', {
description: 'Unauthorized',
content: openapiErrorContent
})
app.openAPIRegistry.registerComponent('responses', '403', {
description: 'Forbidden',
content: openapiErrorContent
})
app.openAPIRegistry.registerComponent('responses', '404', {
description: 'Not Found',
content: openapiErrorContent
})
app.openAPIRegistry.registerComponent('responses', '409', {
description: 'Conflict',
content: openapiErrorContent
})
app.openAPIRegistry.registerComponent('responses', '410', {
description: 'Gone',
content: openapiErrorContent
})
}

Wyświetl plik

@ -33,7 +33,6 @@
"zod": "catalog:"
},
"devDependencies": {
"@agentic/platform-api": "workspace:*",
"@agentic/platform-db": "workspace:*",
"@commander-js/extra-typings": "^14.0.0"
},

Wyświetl plik

@ -20,13 +20,13 @@ import {
stripeId,
timestamps
} from './common'
import { deployments, deploymentSelectSchema } from './deployment'
import { projects, projectSelectSchema } from './project'
import { deployments } from './deployment'
import { projects } from './project'
import {
type StripeSubscriptionItemIdMap,
stripeSubscriptionItemIdMapSchema
} from './schemas'
import { users, userSelectSchema } from './user'
import { users } from './user'
// TODO: Consumers should be valid for any enabled project like in RapidAPI and GCP.
// This may require a separate model to aggregate User Applications.
@ -146,22 +146,22 @@ export const consumerSelectSchema = createSelectSchema(consumers, {
_stripeSubscriptionItemIdMap: true,
_stripeCustomerId: true
})
.extend({
user: z
.lazy(() => userSelectSchema)
.optional()
.openapi('User', { type: 'object' }),
// .extend({
// user: z
// .lazy(() => userSelectSchema)
// .optional()
// .openapi('User', { type: 'object' }),
project: z
.lazy(() => projectSelectSchema)
.optional()
.openapi('Project', { type: 'object' }),
// project: z
// .lazy(() => projectSelectSchema)
// .optional()
// .openapi('Project', { type: 'object' }),
deployment: z
.lazy(() => deploymentSelectSchema)
.optional()
.openapi('Deployment', { type: 'object' })
})
// deployment: z
// .lazy(() => deploymentSelectSchema)
// .optional()
// .openapi('Deployment', { type: 'object' })
// })
.strip()
.openapi('Consumer')

Wyświetl plik

@ -25,8 +25,8 @@ import {
type PricingPlanList,
pricingPlanListSchema
} from './schemas'
import { teams, teamSelectSchema } from './team'
import { users, userSelectSchema } from './user'
import { teams } from './team'
import { users } from './user'
export const deployments = pgTable(
'deployments',
@ -131,20 +131,20 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
.omit({
originUrl: true
})
.extend({
user: z
.lazy(() => userSelectSchema)
.optional()
.openapi('User', { type: 'object' }),
// .extend({
// user: z
// .lazy(() => userSelectSchema)
// .optional()
// .openapi('User', { type: 'object' }),
team: z
.lazy(() => teamSelectSchema)
.optional()
.openapi('Team', { type: 'object' }),
// team: z
// .lazy(() => teamSelectSchema)
// .optional()
// .openapi('Team', { type: 'object' }),
// TODO: Circular references make this schema less than ideal
project: z.object({}).optional().openapi('Project', { type: 'object' })
})
// // TODO: Circular references make this schema less than ideal
// project: z.object({}).optional().openapi('Project', { type: 'object' })
// })
.strip()
.openapi('Deployment')

Wyświetl plik

@ -1,6 +1,5 @@
import { relations } from '@fisch0920/drizzle-orm'
import { index, jsonb, pgTable, text } from '@fisch0920/drizzle-orm/pg-core'
import { z } from '@hono/zod-openapi'
import {
createInsertSchema,
@ -13,10 +12,10 @@ import {
projectId,
timestamps
} from './common'
import { consumers, consumerSelectSchema } from './consumer'
import { deployments, deploymentSelectSchema } from './deployment'
import { projects, projectSelectSchema } from './project'
import { users, userSelectSchema } from './user'
import { consumers } from './consumer'
import { deployments } from './deployment'
import { projects } from './project'
import { users } from './user'
/**
* A `LogEntry` is an internal audit log entry.
@ -83,27 +82,27 @@ export const logEntriesRelations = relations(logEntries, ({ one }) => ({
}))
export const logEntrySelectSchema = createSelectSchema(logEntries)
.extend({
user: z
.lazy(() => userSelectSchema)
.optional()
.openapi('User', { type: 'object' }),
// .extend({
// user: z
// .lazy(() => userSelectSchema)
// .optional()
// .openapi('User', { type: 'object' }),
project: z
.lazy(() => projectSelectSchema)
.optional()
.openapi('Project', { type: 'object' }),
// project: z
// .lazy(() => projectSelectSchema)
// .optional()
// .openapi('Project', { type: 'object' }),
deployment: z
.lazy(() => deploymentSelectSchema)
.optional()
.openapi('Deployment', { type: 'object' }),
// deployment: z
// .lazy(() => deploymentSelectSchema)
// .optional()
// .openapi('Deployment', { type: 'object' }),
consumer: z
.lazy(() => consumerSelectSchema)
.optional()
.openapi('Consumer', { type: 'object' })
})
// consumer: z
// .lazy(() => consumerSelectSchema)
// .optional()
// .openapi('Consumer', { type: 'object' })
// })
.strip()
.openapi('LogEntry')

Wyświetl plik

@ -22,7 +22,7 @@ import {
stripeId,
timestamps
} from './common'
import { deployments, deploymentSelectSchema } from './deployment'
import { deployments } from './deployment'
import {
pricingIntervalSchema,
type StripeMeterIdMap,
@ -32,8 +32,8 @@ import {
type StripeProductIdMap,
stripeProductIdMapSchema
} from './schemas'
import { teams, teamSelectSchema } from './team'
import { users, userSelectSchema } from './user'
import { teams } from './team'
import { users } from './user'
export const projects = pgTable(
'projects',
@ -176,27 +176,27 @@ export const projectSelectSchema = createSelectSchema(projects, {
_stripeMeterIdMap: true,
_stripeAccountId: true
})
.extend({
user: z
.lazy(() => userSelectSchema)
.optional()
.openapi('User', { type: 'object' }),
// .extend({
// user: z
// .lazy(() => userSelectSchema)
// .optional()
// .openapi('User', { type: 'object' }),
team: z
.lazy(() => teamSelectSchema)
.optional()
.openapi('Team', { type: 'object' }),
// team: z
// .lazy(() => teamSelectSchema)
// .optional()
// .openapi('Team', { type: 'object' }),
lastPublishedDeployment: z
.lazy(() => deploymentSelectSchema)
.optional()
.openapi('Deployment', { type: 'object' }),
// lastPublishedDeployment: z
// .lazy(() => deploymentSelectSchema)
// .optional()
// .openapi('Deployment', { type: 'object' }),
lastDeployment: z
.lazy(() => deploymentSelectSchema)
.optional()
.openapi('Deployment', { type: 'object' })
})
// lastDeployment: z
// .lazy(() => deploymentSelectSchema)
// .optional()
// .openapi('Deployment', { type: 'object' })
// })
.strip()
.openapi('Project')

Wyświetl plik

@ -2,7 +2,14 @@ import { z } from '@hono/zod-openapi'
import parseJson from 'parse-json'
export const authProviderTypeSchema = z
.enum(['github', 'google', 'spotify', 'twitter', 'linkedin', 'stripe'])
.union([
z.literal('github'),
z.literal('google'),
z.literal('spotify'),
z.literal('twitter'),
z.literal('linkedin'),
z.literal('stripe')
])
.openapi('AuthProviderType')
export type AuthProviderType = z.infer<typeof authProviderTypeSchema>
@ -83,7 +90,12 @@ export const pricingPlanTierSchema = z
export type PricingPlanTier = z.infer<typeof pricingPlanTierSchema>
export const pricingIntervalSchema = z
.enum(['day', 'week', 'month', 'year'])
.union([
z.literal('day'),
z.literal('week'),
z.literal('month'),
z.literal('year')
])
.describe('The frequency at which a subscription is billed.')
.openapi('PricingInterval')
export type PricingInterval = z.infer<typeof pricingIntervalSchema>
@ -178,13 +190,15 @@ export const pricingPlanLineItemSchema = z
* tiering strategy as defined using the `tiers` and `tiersMode`
* attributes.
*/
billingScheme: z.enum(['per_unit', 'tiered']),
billingScheme: z.union([z.literal('per_unit'), z.literal('tiered')]),
// Only applicable for `per_unit` billing schemes
unitAmount: z.number().nonnegative().optional(),
// Only applicable for `tiered` billing schemes
tiersMode: z.enum(['graduated', 'volume']).optional(),
tiersMode: z
.union([z.literal('graduated'), z.literal('volume')])
.optional(),
tiers: z.array(pricingPlanTierSchema).optional(),
// TODO: add support for tiered rate limits?
@ -204,7 +218,9 @@ export const pricingPlanLineItemSchema = z
*
* Defaults to `sum`.
*/
formula: z.enum(['sum', 'count', 'last']).default('sum')
formula: z
.union([z.literal('sum'), z.literal('count'), z.literal('last')])
.default('sum')
})
.optional(),
@ -223,7 +239,7 @@ export const pricingPlanLineItemSchema = z
/**
* After division, either round the result `up` or `down`.
*/
round: z.enum(['down', 'up'])
round: z.union([z.literal('down'), z.literal('up')])
})
.optional()
})
@ -428,18 +444,19 @@ export type StripeSubscriptionItemIdMap = z.infer<
// .openapi('Coupon')
// export type Coupon = z.infer<typeof couponSchema>
export const deploymentOriginAdapterLocationSchema = z.enum([
'external'
// 'internal'
])
export const deploymentOriginAdapterLocationSchema = z.literal('external')
// z.union([
// z.literal('external'),
// z.literal('internal')
// ])
export type DeploymentOriginAdapterLocation = z.infer<
typeof deploymentOriginAdapterLocationSchema
>
// export const deploymentOriginAdapterInternalTypeSchema = z.enum([
// // 'docker',
// // 'mcp'
// // 'python-fastapi'
// export const deploymentOriginAdapterInternalTypeSchema = z.union([
// z.literal('docker'),
// z.literal('mcp'),
// z.literal('python-fastapi'),
// // etc
// ])
// export type DeploymentOriginAdapterInternalType = z.infer<

Wyświetl plik

@ -5,7 +5,6 @@ import {
pgTable,
primaryKey
} from '@fisch0920/drizzle-orm/pg-core'
import { z } from '@hono/zod-openapi'
import {
createInsertSchema,
@ -17,8 +16,8 @@ import {
timestamp,
timestamps
} from './common'
import { teams, teamSelectSchema } from './team'
import { users, userSelectSchema } from './user'
import { teams } from './team'
import { users } from './user'
export const teamMembers = pgTable(
'team_members',
@ -61,17 +60,17 @@ export const teamMembersRelations = relations(teamMembers, ({ one }) => ({
}))
export const teamMemberSelectSchema = createSelectSchema(teamMembers)
.extend({
user: z
.lazy(() => userSelectSchema)
.optional()
.openapi('User', { type: 'object' }),
// .extend({
// user: z
// .lazy(() => userSelectSchema)
// .optional()
// .openapi('User', { type: 'object' }),
team: z
.lazy(() => teamSelectSchema)
.optional()
.openapi('Team', { type: 'object' })
})
// team: z
// .lazy(() => teamSelectSchema)
// .optional()
// .openapi('Team', { type: 'object' })
// })
.strip()
.openapi('TeamMember')

Wyświetl plik

@ -6,7 +6,6 @@ import {
text,
uniqueIndex
} from '@fisch0920/drizzle-orm/pg-core'
import { z } from '@hono/zod-openapi'
import {
createInsertSchema,
@ -18,7 +17,7 @@ import {
timestamps
} from './common'
import { teamMembers } from './team-member'
import { users, userSelectSchema } from './user'
import { users } from './user'
export const teams = pgTable(
'teams',
@ -47,12 +46,12 @@ export const teamsRelations = relations(teams, ({ one, many }) => ({
}))
export const teamSelectSchema = createSelectSchema(teams)
.extend({
owner: z
.lazy(() => userSelectSchema)
.optional()
.openapi('User', { type: 'object' })
})
// .extend({
// owner: z
// .lazy(() => userSelectSchema)
// .optional()
// .openapi('User', { type: 'object' })
// })
.strip()
.openapi('Team')