kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add deployments api routes
rodzic
4e946fccd5
commit
9c0975f170
|
@ -54,6 +54,7 @@
|
|||
"p-all": "^5.0.0",
|
||||
"postgres": "^3.4.5",
|
||||
"restore-cursor": "catalog:",
|
||||
"semver": "^7.7.2",
|
||||
"stripe": "^18.1.0",
|
||||
"type-fest": "catalog:",
|
||||
"zod": "catalog:",
|
||||
|
@ -61,6 +62,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/semver": "^7.7.0",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"drizzle-orm": "^0.43.1"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { consumerIdParamsSchema } from './schemas'
|
|||
|
||||
const route = createRoute({
|
||||
description:
|
||||
"Updates a consumer's subscription to a different deployment or pricing plan.",
|
||||
"Updates a consumer's subscription to a different deployment or pricing plan. Set `plan` to undefined to cancel the subscription.",
|
||||
tags: ['consumers'],
|
||||
operationId: 'updateConsumer',
|
||||
method: 'post',
|
||||
|
@ -28,7 +28,7 @@ const route = createRoute({
|
|||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.consumerInsertSchema
|
||||
schema: schema.consumerUpdateSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
import { validators } from '@agentic/validators'
|
||||
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||
import semver from 'semver'
|
||||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { db, eq, schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
openapiErrorResponse409,
|
||||
openapiErrorResponse410,
|
||||
openapiErrorResponses
|
||||
} from '@/lib/openapi-utils'
|
||||
import { assert, parseZodSchema, sha256 } from '@/lib/utils'
|
||||
|
||||
const route = createRoute({
|
||||
description: 'Creates a new deployment within a project.',
|
||||
tags: ['deployments'],
|
||||
operationId: 'createDeployment',
|
||||
method: 'post',
|
||||
path: 'deployments',
|
||||
security: openapiAuthenticatedSecuritySchemas,
|
||||
request: {
|
||||
body: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.deploymentInsertSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'A deployment object',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.deploymentSelectSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
...openapiErrorResponses,
|
||||
...openapiErrorResponse404,
|
||||
...openapiErrorResponse409,
|
||||
...openapiErrorResponse410
|
||||
}
|
||||
})
|
||||
|
||||
export function registerV1DeploymentsCreateDeployment(
|
||||
app: OpenAPIHono<AuthenticatedEnv>
|
||||
) {
|
||||
return app.openapi(route, async (c) => {
|
||||
const user = await ensureAuthUser(c)
|
||||
const body = c.req.valid('json')
|
||||
const teamMember = c.get('teamMember')
|
||||
const { projectId } = body
|
||||
|
||||
// validatePricingPlans(ctx, pricingPlans)
|
||||
|
||||
// TODO: OpenAPI support
|
||||
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: eq(schema.projects.id, projectId),
|
||||
with: {
|
||||
lastPublishedDeployment: true
|
||||
}
|
||||
})
|
||||
assert(project, 404, `Project not found "${projectId}"`)
|
||||
await acl(c, project, { label: 'Project' })
|
||||
|
||||
// TODO: investigate better short hash generation
|
||||
const hash = sha256().slice(0, 8)
|
||||
const deploymentId = `${project.id}@${hash}`
|
||||
assert(
|
||||
validators.deploymentId(deploymentId),
|
||||
400,
|
||||
`Invalid deployment id "${deploymentId}"`
|
||||
)
|
||||
|
||||
let { version } = body
|
||||
|
||||
if (version) {
|
||||
version = semver.clean(version) ?? undefined
|
||||
assert(
|
||||
version && semver.valid(version),
|
||||
400,
|
||||
`Invalid semver version "${version}"`
|
||||
)
|
||||
|
||||
const lastPublishedVersion =
|
||||
project.lastPublishedDeployment?.version ?? '0.0.0'
|
||||
|
||||
assert(
|
||||
semver.gt(version, lastPublishedVersion),
|
||||
400,
|
||||
`Semver version "${version}" must be greater than last published version "${lastPublishedVersion}"`
|
||||
)
|
||||
}
|
||||
|
||||
const [[deployment]] = await db.transaction(async (tx) => {
|
||||
return Promise.all([
|
||||
// Create the deployment
|
||||
tx
|
||||
.insert(schema.deployments)
|
||||
.values({
|
||||
...body,
|
||||
id: deploymentId,
|
||||
hash,
|
||||
userId: user.id,
|
||||
teamId: teamMember?.teamId,
|
||||
projectId,
|
||||
version
|
||||
})
|
||||
.returning(),
|
||||
|
||||
// Update the project
|
||||
tx
|
||||
.update(schema.projects)
|
||||
.set({
|
||||
lastDeploymentId: deploymentId
|
||||
})
|
||||
.where(eq(schema.projects.id, projectId))
|
||||
])
|
||||
})
|
||||
assert(deployment, 500, `Failed to create deployment "${deploymentId}"`)
|
||||
|
||||
return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment))
|
||||
})
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
openapiErrorResponses
|
||||
} from '@/lib/openapi-utils'
|
||||
import { tryGetDeployment } from '@/lib/try-get-deployment'
|
||||
import { assert, parseZodSchema } from '@/lib/utils'
|
||||
|
||||
import { deploymentIdParamsSchema, populateDeploymentSchema } from './schemas'
|
||||
|
||||
const route = createRoute({
|
||||
description: 'Gets a deployment',
|
||||
tags: ['deployments'],
|
||||
operationId: 'getdeployment',
|
||||
method: 'get',
|
||||
path: 'deployments/{deploymentId}',
|
||||
security: openapiAuthenticatedSecuritySchemas,
|
||||
request: {
|
||||
params: deploymentIdParamsSchema,
|
||||
query: populateDeploymentSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'A deployment object',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.deploymentSelectSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
...openapiErrorResponses,
|
||||
...openapiErrorResponse404
|
||||
}
|
||||
})
|
||||
|
||||
export function registerV1DeploymentsGetDeployment(
|
||||
app: OpenAPIHono<AuthenticatedEnv>
|
||||
) {
|
||||
return app.openapi(route, async (c) => {
|
||||
const { deploymentId } = c.req.valid('param')
|
||||
const { populate = [] } = c.req.valid('query')
|
||||
|
||||
const deployment = await tryGetDeployment(c, deploymentId, {
|
||||
with: {
|
||||
...Object.fromEntries(populate.map((field) => [field, true]))
|
||||
}
|
||||
})
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
await acl(c, deployment, { label: 'Deployment' })
|
||||
|
||||
return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment))
|
||||
})
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { and, db, eq, schema } from '@/db'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponses
|
||||
} from '@/lib/openapi-utils'
|
||||
import { parseZodSchema } from '@/lib/utils'
|
||||
|
||||
import { paginationAndPopulateAndFilterDeploymentSchema } from './schemas'
|
||||
|
||||
const route = createRoute({
|
||||
description: 'Lists deployments the user or team has access to.',
|
||||
tags: ['deployments'],
|
||||
operationId: 'listDeployments',
|
||||
method: 'get',
|
||||
path: 'deployments',
|
||||
security: openapiAuthenticatedSecuritySchemas,
|
||||
request: {
|
||||
query: paginationAndPopulateAndFilterDeploymentSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'A list of deployments',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.array(schema.deploymentSelectSchema)
|
||||
}
|
||||
}
|
||||
},
|
||||
...openapiErrorResponses
|
||||
}
|
||||
})
|
||||
|
||||
export function registerV1DeploymentsListDeployments(
|
||||
app: OpenAPIHono<AuthenticatedEnv>
|
||||
) {
|
||||
return app.openapi(route, async (c) => {
|
||||
const {
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort = 'desc',
|
||||
sortBy = 'createdAt',
|
||||
populate = [],
|
||||
projectId
|
||||
} = c.req.valid('query')
|
||||
|
||||
const userId = c.get('userId')
|
||||
const teamMember = c.get('teamMember')
|
||||
|
||||
const deployments = await db.query.deployments.findMany({
|
||||
where: and(
|
||||
teamMember
|
||||
? eq(schema.deployments.teamId, teamMember.teamId)
|
||||
: eq(schema.deployments.userId, userId),
|
||||
projectId ? eq(schema.deployments.projectId, projectId) : undefined
|
||||
),
|
||||
with: {
|
||||
...Object.fromEntries(populate.map((field) => [field, true]))
|
||||
},
|
||||
orderBy: (deployments, { asc, desc }) => [
|
||||
sort === 'desc' ? desc(deployments[sortBy]) : asc(deployments[sortBy])
|
||||
],
|
||||
offset,
|
||||
limit
|
||||
})
|
||||
|
||||
return c.json(
|
||||
parseZodSchema(z.array(schema.deploymentSelectSchema), deployments)
|
||||
)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||
import semver from 'semver'
|
||||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { db, eq, schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
openapiErrorResponses
|
||||
} from '@/lib/openapi-utils'
|
||||
import { tryGetDeployment } from '@/lib/try-get-deployment'
|
||||
import { assert, parseZodSchema } from '@/lib/utils'
|
||||
|
||||
import { deploymentIdParamsSchema } from './schemas'
|
||||
|
||||
const route = createRoute({
|
||||
description: 'Publishes a deployment.',
|
||||
tags: ['deployments'],
|
||||
operationId: 'publishDeployment',
|
||||
method: 'post',
|
||||
path: 'deployments/{deploymentId}/publish',
|
||||
security: openapiAuthenticatedSecuritySchemas,
|
||||
request: {
|
||||
params: deploymentIdParamsSchema,
|
||||
body: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.deploymentPublishSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'A deployment object',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.deploymentSelectSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
...openapiErrorResponses,
|
||||
...openapiErrorResponse404
|
||||
}
|
||||
})
|
||||
|
||||
export function registerV1DeploymentsPublishDeployment(
|
||||
app: OpenAPIHono<AuthenticatedEnv>
|
||||
) {
|
||||
return app.openapi(route, async (c) => {
|
||||
const { deploymentId } = c.req.valid('param')
|
||||
const body = c.req.valid('json')
|
||||
const version = semver.clean(body.version)
|
||||
assert(version, 400, `Invalid semver version "${body.version}"`)
|
||||
|
||||
// First ensure the deployment exists and the user has access to it
|
||||
let deployment = await tryGetDeployment(c, deploymentId)
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
await acl(c, deployment, { label: 'Deployment' })
|
||||
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: eq(schema.projects.id, deployment.projectId),
|
||||
with: {
|
||||
lastPublishedDeployment: true
|
||||
}
|
||||
})
|
||||
assert(project, 404, `Project not found "${deployment.projectId}"`)
|
||||
await acl(c, project, { label: 'Project' })
|
||||
|
||||
const lastPublishedVersion =
|
||||
project.lastPublishedDeployment?.version || '0.0.0'
|
||||
|
||||
assert(
|
||||
semver.valid(version),
|
||||
400,
|
||||
`Invalid semver version "${version}" for deployment "${deployment.id}"`
|
||||
)
|
||||
assert(
|
||||
semver.gt(version, lastPublishedVersion),
|
||||
400,
|
||||
`Invalid semver version: "${version}" must be greater than current published version "${lastPublishedVersion}" for deployment "${deployment.id}"`
|
||||
)
|
||||
|
||||
// TODO: enforce certain semver constraints
|
||||
// - pricing changes require major version update
|
||||
// - deployment shouldn't already be published?
|
||||
// - any others?
|
||||
|
||||
// Update the deployment and project together in a transaction
|
||||
;[[deployment]] = await db.transaction(async (tx) => {
|
||||
return Promise.all([
|
||||
// Update the deployment
|
||||
tx
|
||||
.update(schema.deployments)
|
||||
.set({
|
||||
published: true,
|
||||
version
|
||||
})
|
||||
.where(eq(schema.deployments.id, deploymentId))
|
||||
.returning(),
|
||||
|
||||
tx
|
||||
.update(schema.projects)
|
||||
.set({
|
||||
lastPublishedDeploymentId: deploymentId
|
||||
})
|
||||
.where(eq(schema.projects.id, project.id))
|
||||
|
||||
// TODO: add publishDeploymentLogEntry
|
||||
])
|
||||
})
|
||||
assert(deployment, 500, `Failed to update deployment "${deploymentId}"`)
|
||||
|
||||
return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment))
|
||||
})
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
import { deploymentIdSchema, paginationSchema, projectIdSchema } from '@/db'
|
||||
import { deploymentRelationsSchema } from '@/db/schema'
|
||||
|
||||
export const deploymentIdParamsSchema = z.object({
|
||||
deploymentId: deploymentIdSchema.openapi({
|
||||
param: {
|
||||
description: 'deployment ID',
|
||||
name: 'deploymentId',
|
||||
in: 'path'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const filterDeploymentSchema = z.object({
|
||||
projectId: projectIdSchema.optional()
|
||||
})
|
||||
|
||||
export const populateDeploymentSchema = z.object({
|
||||
populate: z.array(deploymentRelationsSchema).default([]).optional()
|
||||
})
|
||||
|
||||
export const paginationAndPopulateAndFilterDeploymentSchema = z.object({
|
||||
...paginationSchema.shape,
|
||||
...populateDeploymentSchema.shape,
|
||||
...filterDeploymentSchema.shape
|
||||
})
|
|
@ -0,0 +1,70 @@
|
|||
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { db, eq, schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
openapiErrorResponses
|
||||
} from '@/lib/openapi-utils'
|
||||
import { tryGetDeployment } from '@/lib/try-get-deployment'
|
||||
import { assert, parseZodSchema } from '@/lib/utils'
|
||||
|
||||
import { deploymentIdParamsSchema } from './schemas'
|
||||
|
||||
const route = createRoute({
|
||||
description: 'Updates a deployment.',
|
||||
tags: ['deployments'],
|
||||
operationId: 'updateDeployment',
|
||||
method: 'post',
|
||||
path: 'deployments/{deploymentId}',
|
||||
security: openapiAuthenticatedSecuritySchemas,
|
||||
request: {
|
||||
params: deploymentIdParamsSchema,
|
||||
body: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.deploymentUpdateSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'A deployment object',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schema.deploymentSelectSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
...openapiErrorResponses,
|
||||
...openapiErrorResponse404
|
||||
}
|
||||
})
|
||||
|
||||
export function registerV1DeploymentsUpdateDeployment(
|
||||
app: OpenAPIHono<AuthenticatedEnv>
|
||||
) {
|
||||
return app.openapi(route, async (c) => {
|
||||
const { deploymentId } = c.req.valid('param')
|
||||
const body = c.req.valid('json')
|
||||
|
||||
// First ensure the deployment exists and the user has access to it
|
||||
let deployment = await tryGetDeployment(c, deploymentId)
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
await acl(c, deployment, { label: 'Deployment' })
|
||||
|
||||
// Update the deployment
|
||||
;[deployment] = await db
|
||||
.update(schema.deployments)
|
||||
.set(body)
|
||||
.where(eq(schema.deployments.id, deploymentId))
|
||||
.returning()
|
||||
assert(deployment, 500, `Failed to update deployment "${deploymentId}"`)
|
||||
|
||||
return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment))
|
||||
})
|
||||
}
|
|
@ -10,6 +10,11 @@ import { registerV1ConsumersGetConsumer } from './consumers/get-consumer'
|
|||
import { registerV1ProjectsListConsumers } from './consumers/list-consumers'
|
||||
import { registerV1ConsumersRefreshConsumerToken } from './consumers/refresh-consumer-token'
|
||||
import { registerV1ConsumersUpdateConsumer } from './consumers/update-consumer'
|
||||
import { registerV1DeploymentsCreateDeployment } from './deployments/create-deployment'
|
||||
import { registerV1DeploymentsGetDeployment } from './deployments/get-deployment'
|
||||
import { registerV1DeploymentsListDeployments } from './deployments/list-deployments'
|
||||
import { registerV1DeploymentsPublishDeployment } from './deployments/publish-deployment'
|
||||
import { registerV1DeploymentsUpdateDeployment } from './deployments/update-deployment'
|
||||
import { registerHealthCheck } from './health-check'
|
||||
import { registerV1ProjectsCreateProject } from './projects/create-project'
|
||||
import { registerV1ProjectsGetProject } from './projects/get-project'
|
||||
|
@ -76,17 +81,6 @@ registerV1ProjectsListProjects(privateRouter)
|
|||
registerV1ProjectsGetProject(privateRouter)
|
||||
registerV1ProjectsUpdateProject(privateRouter)
|
||||
|
||||
// TODO
|
||||
// pub.get('/projects/alias/:alias(.+)', require('./projects').readByAlias)
|
||||
// pri.get('/projects/provider/:project(.+)', require('./provider').read)
|
||||
// pri.put('/projects/provider/:project(.+)', require('./provider').update)
|
||||
// pri.put('/projects/connect/:project(.+)', require('./projects').connect)
|
||||
// pub.get(
|
||||
// '/projects/:project(.+)',
|
||||
// middleware.authenticate({ passthrough: true }),
|
||||
// require('./projects').read
|
||||
// )
|
||||
|
||||
// Consumers
|
||||
registerV1ConsumersGetConsumer(privateRouter)
|
||||
registerV1ConsumersCreateConsumer(privateRouter)
|
||||
|
@ -94,6 +88,13 @@ registerV1ConsumersUpdateConsumer(privateRouter)
|
|||
registerV1ConsumersRefreshConsumerToken(privateRouter)
|
||||
registerV1ProjectsListConsumers(privateRouter)
|
||||
|
||||
// Deployments
|
||||
registerV1DeploymentsGetDeployment(privateRouter)
|
||||
registerV1DeploymentsCreateDeployment(privateRouter)
|
||||
registerV1DeploymentsUpdateDeployment(privateRouter)
|
||||
registerV1DeploymentsListDeployments(privateRouter)
|
||||
registerV1DeploymentsPublishDeployment(privateRouter)
|
||||
|
||||
// Webhook event handlers
|
||||
registerV1StripeWebhook(publicRouter)
|
||||
|
||||
|
@ -135,3 +136,9 @@ export type ApiRoutes =
|
|||
| ReturnType<typeof registerV1ConsumersUpdateConsumer>
|
||||
| ReturnType<typeof registerV1ConsumersRefreshConsumerToken>
|
||||
| ReturnType<typeof registerV1ProjectsListConsumers>
|
||||
// Deployments
|
||||
| ReturnType<typeof registerV1DeploymentsGetDeployment>
|
||||
| ReturnType<typeof registerV1DeploymentsCreateDeployment>
|
||||
| ReturnType<typeof registerV1DeploymentsUpdateDeployment>
|
||||
| ReturnType<typeof registerV1DeploymentsListDeployments>
|
||||
| ReturnType<typeof registerV1DeploymentsPublishDeployment>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
|||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { db, schema } from '@/db'
|
||||
import { aclTeamMember } from '@/lib/acl-team-member'
|
||||
import { createProviderToken } from '@/lib/auth/create-provider-token'
|
||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
import {
|
||||
|
@ -48,9 +47,9 @@ export function registerV1ProjectsCreateProject(
|
|||
const body = c.req.valid('json')
|
||||
const user = await ensureAuthUser(c)
|
||||
|
||||
if (body.teamId) {
|
||||
await aclTeamMember(c, { teamId: body.teamId })
|
||||
}
|
||||
// if (body.teamId) {
|
||||
// await aclTeamMember(c, { teamId: body.teamId })
|
||||
// }
|
||||
|
||||
const teamMember = c.get('teamMember')
|
||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||
|
|
|
@ -62,7 +62,7 @@ export const deployments = pgTable(
|
|||
// TODO: third-party auth provider config?
|
||||
|
||||
// Backend API URL
|
||||
_url: text().notNull(),
|
||||
originUrl: text().notNull(),
|
||||
|
||||
// Array<PricingPlan>
|
||||
pricingPlans: jsonb().$type<PricingPlanList>().notNull()
|
||||
|
@ -96,6 +96,13 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
|||
})
|
||||
}))
|
||||
|
||||
export type DeploymentRelationFields = keyof ReturnType<
|
||||
(typeof deploymentsRelations)['config']
|
||||
>
|
||||
|
||||
export const deploymentRelationsSchema: z.ZodType<DeploymentRelationFields> =
|
||||
z.enum(['user', 'team', 'project'])
|
||||
|
||||
// TODO: virtual hasFreeTier
|
||||
// TODO: virtual url
|
||||
// TODO: virtual openApiUrl
|
||||
|
@ -106,28 +113,6 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
|||
export const deploymentSelectSchema = createSelectSchema(deployments, {
|
||||
// build: z.object({}),
|
||||
// env: z.object({}),
|
||||
|
||||
pricingPlans: pricingPlanListSchema
|
||||
// coupons: z.array(couponSchema)
|
||||
})
|
||||
.omit({
|
||||
_url: true
|
||||
})
|
||||
.extend({
|
||||
user: z
|
||||
.lazy(() => userSelectSchema)
|
||||
.optional()
|
||||
.openapi('User', { type: 'object' }),
|
||||
|
||||
team: z
|
||||
.lazy(() => teamSelectSchema)
|
||||
.optional()
|
||||
.openapi('Team', { type: 'object' })
|
||||
})
|
||||
.strip()
|
||||
.openapi('Deployment')
|
||||
|
||||
export const deploymentInsertSchema = createInsertSchema(deployments, {
|
||||
id: (schema) =>
|
||||
schema.refine((id) => validators.deploymentId(id), {
|
||||
message: 'Invalid deployment id'
|
||||
|
@ -138,28 +123,63 @@ export const deploymentInsertSchema = createInsertSchema(deployments, {
|
|||
message: 'Invalid deployment hash'
|
||||
}),
|
||||
|
||||
pricingPlans: pricingPlanListSchema
|
||||
// coupons: z.array(couponSchema)
|
||||
})
|
||||
.omit({
|
||||
originUrl: true
|
||||
})
|
||||
.extend({
|
||||
user: z
|
||||
.lazy(() => userSelectSchema)
|
||||
.optional()
|
||||
.openapi('User', { 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' })
|
||||
})
|
||||
.strip()
|
||||
.openapi('Deployment')
|
||||
|
||||
export const deploymentInsertSchema = createInsertSchema(deployments, {
|
||||
projectId: (schema) =>
|
||||
schema.refine((id) => validators.projectId(id), {
|
||||
message: 'Invalid project id'
|
||||
}),
|
||||
|
||||
_url: (schema) => schema.url(),
|
||||
originUrl: (schema) => schema.url(),
|
||||
|
||||
pricingPlans: pricingPlanListSchema
|
||||
|
||||
// TODOp
|
||||
// coupons: z.array(couponSchema).optional()
|
||||
})
|
||||
.omit({ id: true, createdAt: true, updatedAt: true })
|
||||
.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
hash: true,
|
||||
userId: true,
|
||||
teamId: true
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const deploymentUpdateSchema = createUpdateSchema(deployments)
|
||||
.pick({
|
||||
enabled: true,
|
||||
published: true,
|
||||
version: true,
|
||||
description: true
|
||||
})
|
||||
.strict()
|
||||
|
||||
// TODO: add admin select schema which includes all fields?
|
||||
export const deploymentPublishSchema = createUpdateSchema(deployments, {
|
||||
version: z.string().nonempty()
|
||||
})
|
||||
.pick({
|
||||
version: true
|
||||
})
|
||||
.strict()
|
||||
|
|
|
@ -18,8 +18,7 @@ import {
|
|||
type StripePriceIdMap,
|
||||
stripePriceIdMapSchema,
|
||||
type StripeProductIdMap,
|
||||
stripeProductIdMapSchema,
|
||||
type Webhook
|
||||
stripeProductIdMapSchema
|
||||
} from './schemas'
|
||||
import { teams, teamSelectSchema } from './team'
|
||||
import { users, userSelectSchema } from './user'
|
||||
|
@ -77,13 +76,11 @@ export const projects = pgTable(
|
|||
// All deployments share the same underlying proxy secret
|
||||
_secret: text().notNull(),
|
||||
|
||||
// Auth token used to access the saasify API on behalf of this project
|
||||
// Auth token used to access the platform API on behalf of this project
|
||||
_providerToken: text().notNull(),
|
||||
|
||||
// TODO: Full-text search
|
||||
_text: text().default('').notNull(),
|
||||
|
||||
_webhooks: jsonb().$type<Webhook[]>().default([]).notNull(),
|
||||
// _text: text().default('').notNull(),
|
||||
|
||||
// Stripe coupons associated with this project, mapping from unique coupon
|
||||
// object hash to stripe coupon id.
|
||||
|
@ -184,8 +181,7 @@ export const projectSelectSchema = createSelectSchema(projects, {
|
|||
.omit({
|
||||
_secret: true,
|
||||
_providerToken: true,
|
||||
_text: true,
|
||||
_webhooks: true,
|
||||
// _text: true,
|
||||
_stripeProductIdMap: true,
|
||||
_stripePriceIdMap: true,
|
||||
_stripeMeterIdMap: true,
|
||||
|
@ -227,8 +223,7 @@ export const projectInsertSchema = createInsertSchema(projects, {
|
|||
})
|
||||
})
|
||||
.pick({
|
||||
name: true,
|
||||
teamId: true
|
||||
name: true
|
||||
})
|
||||
.strict()
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export async function upsertConsumer(
|
|||
deploymentId,
|
||||
consumerId
|
||||
}: {
|
||||
plan: string
|
||||
plan?: string
|
||||
deploymentId?: string
|
||||
consumerId?: string
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export const team = createMiddleware<AuthenticatedEnv>(
|
|||
eq(schema.teamMembers.userId, user.id)
|
||||
)
|
||||
})
|
||||
assert(teamMember, 401, 'Unauthorized')
|
||||
assert(teamMember, 403, 'Unauthorized')
|
||||
|
||||
await aclTeamMember(ctx, { teamMember })
|
||||
|
||||
|
|
|
@ -8,8 +8,6 @@ import { assert } from './utils'
|
|||
|
||||
/**
|
||||
* Attempts to find the Deployment matching the given identifier.
|
||||
*
|
||||
* If the Deployment is not found, throw an HttpError.
|
||||
*/
|
||||
export async function tryGetDeployment(
|
||||
ctx: AuthenticatedContext,
|
||||
|
@ -21,7 +19,7 @@ export async function tryGetDeployment(
|
|||
project?: true
|
||||
}
|
||||
} = {}
|
||||
): Promise<RawDeployment> {
|
||||
): Promise<RawDeployment | undefined> {
|
||||
const user = await ensureAuthUser(ctx)
|
||||
|
||||
const teamMember = ctx.get('teamMember')
|
||||
|
|
|
@ -179,6 +179,9 @@ importers:
|
|||
restore-cursor:
|
||||
specifier: 'catalog:'
|
||||
version: 5.1.0
|
||||
semver:
|
||||
specifier: ^7.7.2
|
||||
version: 7.7.2
|
||||
stripe:
|
||||
specifier: ^18.1.0
|
||||
version: 18.1.0(@types/node@22.15.18)
|
||||
|
@ -195,6 +198,9 @@ importers:
|
|||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.9
|
||||
version: 9.0.9
|
||||
'@types/semver':
|
||||
specifier: ^7.7.0
|
||||
version: 7.7.0
|
||||
drizzle-kit:
|
||||
specifier: ^0.31.1
|
||||
version: 0.31.1
|
||||
|
@ -1133,6 +1139,9 @@ packages:
|
|||
'@types/pg@8.6.1':
|
||||
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
|
||||
|
||||
'@types/semver@7.7.0':
|
||||
resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
|
||||
|
||||
'@types/shimmer@1.2.0':
|
||||
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
||||
|
||||
|
@ -4278,6 +4287,8 @@ snapshots:
|
|||
pg-protocol: 1.10.0
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
'@types/shimmer@1.2.0': {}
|
||||
|
||||
'@types/tedious@4.0.14':
|
||||
|
|
Ładowanie…
Reference in New Issue