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",
|
"p-all": "^5.0.0",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"restore-cursor": "catalog:",
|
"restore-cursor": "catalog:",
|
||||||
|
"semver": "^7.7.2",
|
||||||
"stripe": "^18.1.0",
|
"stripe": "^18.1.0",
|
||||||
"type-fest": "catalog:",
|
"type-fest": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
|
@ -61,6 +62,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"@types/semver": "^7.7.0",
|
||||||
"drizzle-kit": "^0.31.1",
|
"drizzle-kit": "^0.31.1",
|
||||||
"drizzle-orm": "^0.43.1"
|
"drizzle-orm": "^0.43.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { consumerIdParamsSchema } from './schemas'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
description:
|
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'],
|
tags: ['consumers'],
|
||||||
operationId: 'updateConsumer',
|
operationId: 'updateConsumer',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
@ -28,7 +28,7 @@ const route = createRoute({
|
||||||
required: true,
|
required: true,
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'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 { registerV1ProjectsListConsumers } from './consumers/list-consumers'
|
||||||
import { registerV1ConsumersRefreshConsumerToken } from './consumers/refresh-consumer-token'
|
import { registerV1ConsumersRefreshConsumerToken } from './consumers/refresh-consumer-token'
|
||||||
import { registerV1ConsumersUpdateConsumer } from './consumers/update-consumer'
|
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 { registerHealthCheck } from './health-check'
|
||||||
import { registerV1ProjectsCreateProject } from './projects/create-project'
|
import { registerV1ProjectsCreateProject } from './projects/create-project'
|
||||||
import { registerV1ProjectsGetProject } from './projects/get-project'
|
import { registerV1ProjectsGetProject } from './projects/get-project'
|
||||||
|
@ -76,17 +81,6 @@ registerV1ProjectsListProjects(privateRouter)
|
||||||
registerV1ProjectsGetProject(privateRouter)
|
registerV1ProjectsGetProject(privateRouter)
|
||||||
registerV1ProjectsUpdateProject(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
|
// Consumers
|
||||||
registerV1ConsumersGetConsumer(privateRouter)
|
registerV1ConsumersGetConsumer(privateRouter)
|
||||||
registerV1ConsumersCreateConsumer(privateRouter)
|
registerV1ConsumersCreateConsumer(privateRouter)
|
||||||
|
@ -94,6 +88,13 @@ registerV1ConsumersUpdateConsumer(privateRouter)
|
||||||
registerV1ConsumersRefreshConsumerToken(privateRouter)
|
registerV1ConsumersRefreshConsumerToken(privateRouter)
|
||||||
registerV1ProjectsListConsumers(privateRouter)
|
registerV1ProjectsListConsumers(privateRouter)
|
||||||
|
|
||||||
|
// Deployments
|
||||||
|
registerV1DeploymentsGetDeployment(privateRouter)
|
||||||
|
registerV1DeploymentsCreateDeployment(privateRouter)
|
||||||
|
registerV1DeploymentsUpdateDeployment(privateRouter)
|
||||||
|
registerV1DeploymentsListDeployments(privateRouter)
|
||||||
|
registerV1DeploymentsPublishDeployment(privateRouter)
|
||||||
|
|
||||||
// Webhook event handlers
|
// Webhook event handlers
|
||||||
registerV1StripeWebhook(publicRouter)
|
registerV1StripeWebhook(publicRouter)
|
||||||
|
|
||||||
|
@ -135,3 +136,9 @@ export type ApiRoutes =
|
||||||
| ReturnType<typeof registerV1ConsumersUpdateConsumer>
|
| ReturnType<typeof registerV1ConsumersUpdateConsumer>
|
||||||
| ReturnType<typeof registerV1ConsumersRefreshConsumerToken>
|
| ReturnType<typeof registerV1ConsumersRefreshConsumerToken>
|
||||||
| ReturnType<typeof registerV1ProjectsListConsumers>
|
| 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 type { AuthenticatedEnv } from '@/lib/types'
|
||||||
import { db, schema } from '@/db'
|
import { db, schema } from '@/db'
|
||||||
import { aclTeamMember } from '@/lib/acl-team-member'
|
|
||||||
import { createProviderToken } from '@/lib/auth/create-provider-token'
|
import { createProviderToken } from '@/lib/auth/create-provider-token'
|
||||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||||
import {
|
import {
|
||||||
|
@ -48,9 +47,9 @@ export function registerV1ProjectsCreateProject(
|
||||||
const body = c.req.valid('json')
|
const body = c.req.valid('json')
|
||||||
const user = await ensureAuthUser(c)
|
const user = await ensureAuthUser(c)
|
||||||
|
|
||||||
if (body.teamId) {
|
// if (body.teamId) {
|
||||||
await aclTeamMember(c, { teamId: body.teamId })
|
// await aclTeamMember(c, { teamId: body.teamId })
|
||||||
}
|
// }
|
||||||
|
|
||||||
const teamMember = c.get('teamMember')
|
const teamMember = c.get('teamMember')
|
||||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const deployments = pgTable(
|
||||||
// TODO: third-party auth provider config?
|
// TODO: third-party auth provider config?
|
||||||
|
|
||||||
// Backend API URL
|
// Backend API URL
|
||||||
_url: text().notNull(),
|
originUrl: text().notNull(),
|
||||||
|
|
||||||
// Array<PricingPlan>
|
// Array<PricingPlan>
|
||||||
pricingPlans: jsonb().$type<PricingPlanList>().notNull()
|
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 hasFreeTier
|
||||||
// TODO: virtual url
|
// TODO: virtual url
|
||||||
// TODO: virtual openApiUrl
|
// TODO: virtual openApiUrl
|
||||||
|
@ -106,28 +113,6 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||||
export const deploymentSelectSchema = createSelectSchema(deployments, {
|
export const deploymentSelectSchema = createSelectSchema(deployments, {
|
||||||
// build: z.object({}),
|
// build: z.object({}),
|
||||||
// env: 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) =>
|
id: (schema) =>
|
||||||
schema.refine((id) => validators.deploymentId(id), {
|
schema.refine((id) => validators.deploymentId(id), {
|
||||||
message: 'Invalid deployment id'
|
message: 'Invalid deployment id'
|
||||||
|
@ -138,28 +123,63 @@ export const deploymentInsertSchema = createInsertSchema(deployments, {
|
||||||
message: 'Invalid deployment hash'
|
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) =>
|
projectId: (schema) =>
|
||||||
schema.refine((id) => validators.projectId(id), {
|
schema.refine((id) => validators.projectId(id), {
|
||||||
message: 'Invalid project id'
|
message: 'Invalid project id'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
_url: (schema) => schema.url(),
|
originUrl: (schema) => schema.url(),
|
||||||
|
|
||||||
pricingPlans: pricingPlanListSchema
|
pricingPlans: pricingPlanListSchema
|
||||||
|
|
||||||
// TODOp
|
// TODOp
|
||||||
// coupons: z.array(couponSchema).optional()
|
// 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()
|
.strict()
|
||||||
|
|
||||||
export const deploymentUpdateSchema = createUpdateSchema(deployments)
|
export const deploymentUpdateSchema = createUpdateSchema(deployments)
|
||||||
.pick({
|
.pick({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
published: true,
|
|
||||||
version: true,
|
|
||||||
description: true
|
description: true
|
||||||
})
|
})
|
||||||
.strict()
|
.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,
|
type StripePriceIdMap,
|
||||||
stripePriceIdMapSchema,
|
stripePriceIdMapSchema,
|
||||||
type StripeProductIdMap,
|
type StripeProductIdMap,
|
||||||
stripeProductIdMapSchema,
|
stripeProductIdMapSchema
|
||||||
type Webhook
|
|
||||||
} from './schemas'
|
} from './schemas'
|
||||||
import { teams, teamSelectSchema } from './team'
|
import { teams, teamSelectSchema } from './team'
|
||||||
import { users, userSelectSchema } from './user'
|
import { users, userSelectSchema } from './user'
|
||||||
|
@ -77,13 +76,11 @@ export const projects = pgTable(
|
||||||
// All deployments share the same underlying proxy secret
|
// All deployments share the same underlying proxy secret
|
||||||
_secret: text().notNull(),
|
_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(),
|
_providerToken: text().notNull(),
|
||||||
|
|
||||||
// TODO: Full-text search
|
// TODO: Full-text search
|
||||||
_text: text().default('').notNull(),
|
// _text: text().default('').notNull(),
|
||||||
|
|
||||||
_webhooks: jsonb().$type<Webhook[]>().default([]).notNull(),
|
|
||||||
|
|
||||||
// Stripe coupons associated with this project, mapping from unique coupon
|
// Stripe coupons associated with this project, mapping from unique coupon
|
||||||
// object hash to stripe coupon id.
|
// object hash to stripe coupon id.
|
||||||
|
@ -184,8 +181,7 @@ export const projectSelectSchema = createSelectSchema(projects, {
|
||||||
.omit({
|
.omit({
|
||||||
_secret: true,
|
_secret: true,
|
||||||
_providerToken: true,
|
_providerToken: true,
|
||||||
_text: true,
|
// _text: true,
|
||||||
_webhooks: true,
|
|
||||||
_stripeProductIdMap: true,
|
_stripeProductIdMap: true,
|
||||||
_stripePriceIdMap: true,
|
_stripePriceIdMap: true,
|
||||||
_stripeMeterIdMap: true,
|
_stripeMeterIdMap: true,
|
||||||
|
@ -227,8 +223,7 @@ export const projectInsertSchema = createInsertSchema(projects, {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true
|
||||||
teamId: true
|
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ export async function upsertConsumer(
|
||||||
deploymentId,
|
deploymentId,
|
||||||
consumerId
|
consumerId
|
||||||
}: {
|
}: {
|
||||||
plan: string
|
plan?: string
|
||||||
deploymentId?: string
|
deploymentId?: string
|
||||||
consumerId?: string
|
consumerId?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const team = createMiddleware<AuthenticatedEnv>(
|
||||||
eq(schema.teamMembers.userId, user.id)
|
eq(schema.teamMembers.userId, user.id)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
assert(teamMember, 401, 'Unauthorized')
|
assert(teamMember, 403, 'Unauthorized')
|
||||||
|
|
||||||
await aclTeamMember(ctx, { teamMember })
|
await aclTeamMember(ctx, { teamMember })
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,6 @@ import { assert } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to find the Deployment matching the given identifier.
|
* Attempts to find the Deployment matching the given identifier.
|
||||||
*
|
|
||||||
* If the Deployment is not found, throw an HttpError.
|
|
||||||
*/
|
*/
|
||||||
export async function tryGetDeployment(
|
export async function tryGetDeployment(
|
||||||
ctx: AuthenticatedContext,
|
ctx: AuthenticatedContext,
|
||||||
|
@ -21,7 +19,7 @@ export async function tryGetDeployment(
|
||||||
project?: true
|
project?: true
|
||||||
}
|
}
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<RawDeployment> {
|
): Promise<RawDeployment | undefined> {
|
||||||
const user = await ensureAuthUser(ctx)
|
const user = await ensureAuthUser(ctx)
|
||||||
|
|
||||||
const teamMember = ctx.get('teamMember')
|
const teamMember = ctx.get('teamMember')
|
||||||
|
|
|
@ -179,6 +179,9 @@ importers:
|
||||||
restore-cursor:
|
restore-cursor:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
|
semver:
|
||||||
|
specifier: ^7.7.2
|
||||||
|
version: 7.7.2
|
||||||
stripe:
|
stripe:
|
||||||
specifier: ^18.1.0
|
specifier: ^18.1.0
|
||||||
version: 18.1.0(@types/node@22.15.18)
|
version: 18.1.0(@types/node@22.15.18)
|
||||||
|
@ -195,6 +198,9 @@ importers:
|
||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: ^9.0.9
|
specifier: ^9.0.9
|
||||||
version: 9.0.9
|
version: 9.0.9
|
||||||
|
'@types/semver':
|
||||||
|
specifier: ^7.7.0
|
||||||
|
version: 7.7.0
|
||||||
drizzle-kit:
|
drizzle-kit:
|
||||||
specifier: ^0.31.1
|
specifier: ^0.31.1
|
||||||
version: 0.31.1
|
version: 0.31.1
|
||||||
|
@ -1133,6 +1139,9 @@ packages:
|
||||||
'@types/pg@8.6.1':
|
'@types/pg@8.6.1':
|
||||||
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
|
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
|
||||||
|
|
||||||
|
'@types/semver@7.7.0':
|
||||||
|
resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
|
||||||
|
|
||||||
'@types/shimmer@1.2.0':
|
'@types/shimmer@1.2.0':
|
||||||
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
||||||
|
|
||||||
|
@ -4278,6 +4287,8 @@ snapshots:
|
||||||
pg-protocol: 1.10.0
|
pg-protocol: 1.10.0
|
||||||
pg-types: 2.2.0
|
pg-types: 2.2.0
|
||||||
|
|
||||||
|
'@types/semver@7.7.0': {}
|
||||||
|
|
||||||
'@types/shimmer@1.2.0': {}
|
'@types/shimmer@1.2.0': {}
|
||||||
|
|
||||||
'@types/tedious@4.0.14':
|
'@types/tedious@4.0.14':
|
||||||
|
|
Ładowanie…
Reference in New Issue