kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
9c0975f170
commit
e2d2303275
|
@ -1,20 +1,22 @@
|
||||||
import { validators } from '@agentic/validators'
|
import { validators } from '@agentic/validators'
|
||||||
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
import semver from 'semver'
|
|
||||||
|
|
||||||
import type { AuthenticatedEnv } from '@/lib/types'
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
import { db, eq, schema } from '@/db'
|
import { db, eq, schema } from '@/db'
|
||||||
import { acl } from '@/lib/acl'
|
import { acl } from '@/lib/acl'
|
||||||
|
import { publishDeployment } from '@/lib/deployments/publish-deployment'
|
||||||
|
import { resolveDeploymentVersion } from '@/lib/deployments/resolve-deployment-version'
|
||||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||||
import {
|
import {
|
||||||
openapiAuthenticatedSecuritySchemas,
|
openapiAuthenticatedSecuritySchemas,
|
||||||
openapiErrorResponse404,
|
openapiErrorResponse404,
|
||||||
openapiErrorResponse409,
|
openapiErrorResponse409,
|
||||||
openapiErrorResponse410,
|
|
||||||
openapiErrorResponses
|
openapiErrorResponses
|
||||||
} from '@/lib/openapi-utils'
|
} from '@/lib/openapi-utils'
|
||||||
import { assert, parseZodSchema, sha256 } from '@/lib/utils'
|
import { assert, parseZodSchema, sha256 } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { createDeploymentQuerySchema } from './schemas'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
description: 'Creates a new deployment within a project.',
|
description: 'Creates a new deployment within a project.',
|
||||||
tags: ['deployments'],
|
tags: ['deployments'],
|
||||||
|
@ -23,6 +25,7 @@ const route = createRoute({
|
||||||
path: 'deployments',
|
path: 'deployments',
|
||||||
security: openapiAuthenticatedSecuritySchemas,
|
security: openapiAuthenticatedSecuritySchemas,
|
||||||
request: {
|
request: {
|
||||||
|
query: createDeploymentQuerySchema,
|
||||||
body: {
|
body: {
|
||||||
required: true,
|
required: true,
|
||||||
content: {
|
content: {
|
||||||
|
@ -43,8 +46,7 @@ const route = createRoute({
|
||||||
},
|
},
|
||||||
...openapiErrorResponses,
|
...openapiErrorResponses,
|
||||||
...openapiErrorResponse404,
|
...openapiErrorResponse404,
|
||||||
...openapiErrorResponse409,
|
...openapiErrorResponse409
|
||||||
...openapiErrorResponse410
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -53,13 +55,14 @@ export function registerV1DeploymentsCreateDeployment(
|
||||||
) {
|
) {
|
||||||
return app.openapi(route, async (c) => {
|
return app.openapi(route, async (c) => {
|
||||||
const user = await ensureAuthUser(c)
|
const user = await ensureAuthUser(c)
|
||||||
|
const { publish } = c.req.valid('query')
|
||||||
const body = c.req.valid('json')
|
const body = c.req.valid('json')
|
||||||
const teamMember = c.get('teamMember')
|
const teamMember = c.get('teamMember')
|
||||||
const { projectId } = body
|
const { projectId } = body
|
||||||
|
|
||||||
// validatePricingPlans(ctx, pricingPlans)
|
// validatePricingPlans(ctx, pricingPlans)
|
||||||
|
|
||||||
// TODO: OpenAPI support
|
// TODO: validate OpenAPI origin schema
|
||||||
|
|
||||||
const project = await db.query.projects.findFirst({
|
const project = await db.query.projects.findFirst({
|
||||||
where: eq(schema.projects.id, projectId),
|
where: eq(schema.projects.id, projectId),
|
||||||
|
@ -80,26 +83,23 @@ export function registerV1DeploymentsCreateDeployment(
|
||||||
)
|
)
|
||||||
|
|
||||||
let { version } = body
|
let { version } = body
|
||||||
|
if (publish) {
|
||||||
if (version) {
|
|
||||||
version = semver.clean(version) ?? undefined
|
|
||||||
assert(
|
assert(
|
||||||
version && semver.valid(version),
|
version,
|
||||||
400,
|
400,
|
||||||
`Invalid semver version "${version}"`
|
`Deployment version is required to publish deployment "${deploymentId}"`
|
||||||
)
|
|
||||||
|
|
||||||
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) => {
|
if (version) {
|
||||||
|
version = resolveDeploymentVersion({
|
||||||
|
deploymentId,
|
||||||
|
project,
|
||||||
|
version
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let [[deployment]] = await db.transaction(async (tx) => {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
// Create the deployment
|
// Create the deployment
|
||||||
tx
|
tx
|
||||||
|
@ -126,6 +126,15 @@ export function registerV1DeploymentsCreateDeployment(
|
||||||
})
|
})
|
||||||
assert(deployment, 500, `Failed to create deployment "${deploymentId}"`)
|
assert(deployment, 500, `Failed to create deployment "${deploymentId}"`)
|
||||||
|
|
||||||
|
if (publish) {
|
||||||
|
deployment = await publishDeployment(c, {
|
||||||
|
deployment,
|
||||||
|
version: deployment.version!
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// TODO: validate deployment originUrl, originAdapter, originSchema, and
|
||||||
|
// originSchemaVersion
|
||||||
|
|
||||||
return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment))
|
return c.json(parseZodSchema(schema.deploymentSelectSchema, deployment))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
import semver from 'semver'
|
|
||||||
|
|
||||||
import type { AuthenticatedEnv } from '@/lib/types'
|
import type { AuthenticatedEnv } from '@/lib/types'
|
||||||
import { db, eq, schema } from '@/db'
|
import { schema } from '@/db'
|
||||||
import { acl } from '@/lib/acl'
|
import { acl } from '@/lib/acl'
|
||||||
|
import { publishDeployment } from '@/lib/deployments/publish-deployment'
|
||||||
import {
|
import {
|
||||||
openapiAuthenticatedSecuritySchemas,
|
openapiAuthenticatedSecuritySchemas,
|
||||||
openapiErrorResponse404,
|
openapiErrorResponse404,
|
||||||
|
@ -51,68 +51,20 @@ export function registerV1DeploymentsPublishDeployment(
|
||||||
) {
|
) {
|
||||||
return app.openapi(route, async (c) => {
|
return app.openapi(route, async (c) => {
|
||||||
const { deploymentId } = c.req.valid('param')
|
const { deploymentId } = c.req.valid('param')
|
||||||
const body = c.req.valid('json')
|
const { version } = 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
|
// First ensure the deployment exists and the user has access to it
|
||||||
let deployment = await tryGetDeployment(c, deploymentId)
|
const deployment = await tryGetDeployment(c, deploymentId)
|
||||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||||
await acl(c, deployment, { label: 'Deployment' })
|
await acl(c, deployment, { label: 'Deployment' })
|
||||||
|
|
||||||
const project = await db.query.projects.findFirst({
|
const publishedDeployment = await publishDeployment(c, {
|
||||||
where: eq(schema.projects.id, deployment.projectId),
|
deployment,
|
||||||
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
|
version
|
||||||
})
|
})
|
||||||
.where(eq(schema.deployments.id, deploymentId))
|
|
||||||
.returning(),
|
|
||||||
|
|
||||||
tx
|
return c.json(
|
||||||
.update(schema.projects)
|
parseZodSchema(schema.deploymentSelectSchema, publishedDeployment)
|
||||||
.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))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,10 @@ export const deploymentIdParamsSchema = z.object({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const createDeploymentQuerySchema = z.object({
|
||||||
|
publish: z.boolean().default(false).optional()
|
||||||
|
})
|
||||||
|
|
||||||
export const filterDeploymentSchema = z.object({
|
export const filterDeploymentSchema = z.object({
|
||||||
projectId: projectIdSchema.optional()
|
projectId: projectIdSchema.optional()
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,10 +11,10 @@ import { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
import { projects } from './project'
|
import { projects } from './project'
|
||||||
import {
|
import {
|
||||||
|
type DeploymentOriginAdapter,
|
||||||
|
deploymentOriginAdapterSchema,
|
||||||
type PricingPlanList,
|
type PricingPlanList,
|
||||||
pricingPlanListSchema
|
pricingPlanListSchema
|
||||||
// type Coupon,
|
|
||||||
// couponSchema,
|
|
||||||
} from './schemas'
|
} from './schemas'
|
||||||
import { teams, teamSelectSchema } from './team'
|
import { teams, teamSelectSchema } from './team'
|
||||||
import { users, userSelectSchema } from './user'
|
import { users, userSelectSchema } from './user'
|
||||||
|
@ -43,6 +43,7 @@ export const deployments = pgTable(
|
||||||
|
|
||||||
description: text().default('').notNull(),
|
description: text().default('').notNull(),
|
||||||
readme: text().default('').notNull(),
|
readme: text().default('').notNull(),
|
||||||
|
iconUrl: text(),
|
||||||
|
|
||||||
userId: cuid()
|
userId: cuid()
|
||||||
.notNull()
|
.notNull()
|
||||||
|
@ -57,13 +58,21 @@ export const deployments = pgTable(
|
||||||
// TODO: Tool definitions or OpenAPI spec
|
// TODO: Tool definitions or OpenAPI spec
|
||||||
// services: jsonb().$type<Service[]>().default([]),
|
// services: jsonb().$type<Service[]>().default([]),
|
||||||
|
|
||||||
// TODO: metadata config (logo, keywords, etc)
|
// TODO: metadata config (logo, keywords, examples, etc)
|
||||||
|
// TODO: openapi spec or tool definitions or mcp adapter
|
||||||
// TODO: webhooks
|
// TODO: webhooks
|
||||||
// TODO: third-party auth provider config?
|
// TODO: third-party auth provider config
|
||||||
|
// NOTE: will need consumer.authProviders as well as user.authProviders for
|
||||||
|
// this because custom oauth credentials that are deployment-specific. will
|
||||||
|
// prolly also need to hash the individual AuthProviders in
|
||||||
|
// deployment.authProviders to compare across deployments.
|
||||||
|
|
||||||
// Backend API URL
|
// Origin API URL
|
||||||
originUrl: text().notNull(),
|
originUrl: text().notNull(),
|
||||||
|
|
||||||
|
// Origin API adapter config (openapi, mcp, hosted externally or internally, etc)
|
||||||
|
originAdapter: jsonb().$type<DeploymentOriginAdapter>().notNull(),
|
||||||
|
|
||||||
// Array<PricingPlan>
|
// Array<PricingPlan>
|
||||||
pricingPlans: jsonb().$type<PricingPlanList>().notNull()
|
pricingPlans: jsonb().$type<PricingPlanList>().notNull()
|
||||||
|
|
||||||
|
@ -123,8 +132,8 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
|
||||||
message: 'Invalid deployment hash'
|
message: 'Invalid deployment hash'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
pricingPlans: pricingPlanListSchema
|
pricingPlans: pricingPlanListSchema,
|
||||||
// coupons: z.array(couponSchema)
|
originAdapter: deploymentOriginAdapterSchema
|
||||||
})
|
})
|
||||||
.omit({
|
.omit({
|
||||||
originUrl: true
|
originUrl: true
|
||||||
|
@ -152,11 +161,24 @@ export const deploymentInsertSchema = createInsertSchema(deployments, {
|
||||||
message: 'Invalid project id'
|
message: 'Invalid project id'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
originUrl: (schema) => schema.url(),
|
iconUrl: (schema) =>
|
||||||
|
schema
|
||||||
|
.url()
|
||||||
|
.describe(
|
||||||
|
'Logo image URL to use for this delpoyment. Logos should have a square aspect ratio.'
|
||||||
|
),
|
||||||
|
|
||||||
pricingPlans: pricingPlanListSchema
|
originUrl: (schema) =>
|
||||||
|
schema.url().describe(`Base URL of the externally hosted origin API server.
|
||||||
|
|
||||||
// TODOp
|
NOTE: Agentic currently only supports \`external\` API servers. If you'd like to host your API or MCP server on Agentic's infrastructure, please reach out to support@agentic.so.`),
|
||||||
|
|
||||||
|
pricingPlans: pricingPlanListSchema.describe(
|
||||||
|
'List of PricingPlans should be available as subscriptions for this deployment.'
|
||||||
|
),
|
||||||
|
originAdapter: deploymentOriginAdapterSchema.optional()
|
||||||
|
|
||||||
|
// TODO
|
||||||
// coupons: z.array(couponSchema).optional()
|
// coupons: z.array(couponSchema).optional()
|
||||||
})
|
})
|
||||||
.omit({
|
.omit({
|
||||||
|
|
|
@ -426,3 +426,64 @@ export type StripeSubscriptionItemIdMap = z.infer<
|
||||||
// })
|
// })
|
||||||
// .openapi('Coupon')
|
// .openapi('Coupon')
|
||||||
// export type Coupon = z.infer<typeof couponSchema>
|
// export type Coupon = z.infer<typeof couponSchema>
|
||||||
|
|
||||||
|
export const deploymentOriginAdapterLocationSchema = z.enum([
|
||||||
|
'external'
|
||||||
|
// 'internal'
|
||||||
|
])
|
||||||
|
export type DeploymentOriginAdapterLocation = z.infer<
|
||||||
|
typeof deploymentOriginAdapterLocationSchema
|
||||||
|
>
|
||||||
|
|
||||||
|
// export const deploymentOriginAdapterInternalTypeSchema = z.enum([
|
||||||
|
// // 'docker',
|
||||||
|
// // 'mcp'
|
||||||
|
// // 'python-fastapi'
|
||||||
|
// // etc
|
||||||
|
// ])
|
||||||
|
// export type DeploymentOriginAdapterInternalType = z.infer<
|
||||||
|
// typeof deploymentOriginAdapterInternalTypeSchema
|
||||||
|
// >
|
||||||
|
|
||||||
|
export const commonDeploymentOriginAdapterSchema = z.object({
|
||||||
|
location: deploymentOriginAdapterLocationSchema
|
||||||
|
|
||||||
|
// TODO: Add support for `internal` hosted API servers
|
||||||
|
// internalType: deploymentOriginAdapterInternalTypeSchema.optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: add future support for:
|
||||||
|
// - external mcp
|
||||||
|
// - internal docker
|
||||||
|
// - internal mcp
|
||||||
|
// - internal http
|
||||||
|
export const deploymentOriginAdapterSchema = z
|
||||||
|
.discriminatedUnion('type', [
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal('openapi'),
|
||||||
|
version: z.enum(['3.0', '3.1']),
|
||||||
|
// TODO: Make sure origin API servers are hidden in this embedded OpenAPI spec
|
||||||
|
spec: z.any().describe('JSON OpenAPI spec for the origin API server.')
|
||||||
|
})
|
||||||
|
.merge(commonDeploymentOriginAdapterSchema),
|
||||||
|
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal('raw')
|
||||||
|
})
|
||||||
|
.merge(commonDeploymentOriginAdapterSchema)
|
||||||
|
])
|
||||||
|
.default({
|
||||||
|
location: 'external',
|
||||||
|
type: 'raw'
|
||||||
|
})
|
||||||
|
.describe(
|
||||||
|
`Deployment origin API adapter is used to configure the origin API server downstream from Agentic's API gateway. It specifies whether the origin API server denoted by \`originUrl\` is hosted externally or deployed internally to Agentic's infrastructure. It also specifies the format for how origin tools / services are defined: either as an OpenAPI spec, an MCP server, or as a raw HTTP REST API.
|
||||||
|
|
||||||
|
NOTE: Agentic currently only supports \`external\` API servers. If you'd like to host your API or MCP server on Agentic's infrastructure, please reach out to support@agentic.so.`
|
||||||
|
)
|
||||||
|
.openapi('DeploymentOriginAdapter')
|
||||||
|
export type DeploymentOriginAdapter = z.infer<
|
||||||
|
typeof deploymentOriginAdapterSchema
|
||||||
|
>
|
||||||
|
|
|
@ -51,7 +51,7 @@ export const users = pgTable(
|
||||||
isStripeConnectEnabledByDefault: boolean().default(true).notNull(),
|
isStripeConnectEnabledByDefault: boolean().default(true).notNull(),
|
||||||
|
|
||||||
// third-party auth providers
|
// third-party auth providers
|
||||||
providers: jsonb().$type<AuthProviders>().default({}).notNull(),
|
authProviders: jsonb().$type<AuthProviders>().default({}).notNull(),
|
||||||
|
|
||||||
stripeCustomerId: stripeId().unique()
|
stripeCustomerId: stripeId().unique()
|
||||||
},
|
},
|
||||||
|
@ -70,7 +70,7 @@ export const usersRelations = relations(users, ({ many }) => ({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const userSelectSchema = createSelectSchema(users, {
|
export const userSelectSchema = createSelectSchema(users, {
|
||||||
providers: publicAuthProvidersSchema
|
authProviders: publicAuthProvidersSchema
|
||||||
})
|
})
|
||||||
.omit({ password: true, emailConfirmToken: true, passwordResetToken: true })
|
.omit({ password: true, emailConfirmToken: true, passwordResetToken: true })
|
||||||
.strip()
|
.strip()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { LogLevel } from '@/lib/logger'
|
||||||
|
|
||||||
import type { LogEntry, RawLogEntry, RawUser, User } from './types'
|
import type { LogEntry, RawLogEntry, RawUser, User } from './types'
|
||||||
|
|
||||||
type UserKeys = Exclude<keyof User & keyof RawUser, 'providers'>
|
type UserKeys = Exclude<keyof User & keyof RawUser, 'authProviders'>
|
||||||
type LogEntryKeys = keyof RawLogEntry & keyof LogEntry
|
type LogEntryKeys = keyof RawLogEntry & keyof LogEntry
|
||||||
|
|
||||||
test('User types are compatible', () => {
|
test('User types are compatible', () => {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
InferSelectModel
|
InferSelectModel
|
||||||
} from '@fisch0920/drizzle-orm'
|
} from '@fisch0920/drizzle-orm'
|
||||||
import type { z } from '@hono/zod-openapi'
|
import type { z } from '@hono/zod-openapi'
|
||||||
|
import type { Simplify } from 'type-fest'
|
||||||
|
|
||||||
import type * as schema from './schema'
|
import type * as schema from './schema'
|
||||||
|
|
||||||
|
@ -35,7 +36,12 @@ export type ProjectWithLastPublishedDeployment = BuildQueryResult<
|
||||||
Tables['projects'],
|
Tables['projects'],
|
||||||
{ with: { lastPublishedDeployment: true } }
|
{ with: { lastPublishedDeployment: true } }
|
||||||
>
|
>
|
||||||
export type RawProject = InferSelectModel<typeof schema.projects>
|
export type RawProject = Simplify<
|
||||||
|
InferSelectModel<typeof schema.projects> & {
|
||||||
|
lastPublishedDeployment?: RawDeployment | null // TODO: remove null (requires drizzle-orm changes)
|
||||||
|
lastDeployment?: RawDeployment | null // TODO: remove null (requires drizzle-orm changes)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
export type Deployment = z.infer<typeof schema.deploymentSelectSchema>
|
export type Deployment = z.infer<typeof schema.deploymentSelectSchema>
|
||||||
export type DeploymentWithProject = BuildQueryResult<
|
export type DeploymentWithProject = BuildQueryResult<
|
||||||
|
@ -43,7 +49,11 @@ export type DeploymentWithProject = BuildQueryResult<
|
||||||
Tables['deployments'],
|
Tables['deployments'],
|
||||||
{ with: { project: true } }
|
{ with: { project: true } }
|
||||||
>
|
>
|
||||||
export type RawDeployment = InferSelectModel<typeof schema.deployments>
|
export type RawDeployment = Simplify<
|
||||||
|
InferSelectModel<typeof schema.deployments> & {
|
||||||
|
project?: RawProject | null // TODO: remove null (requires drizzle-orm changes)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
export type Consumer = z.infer<typeof schema.consumerSelectSchema>
|
export type Consumer = z.infer<typeof schema.consumerSelectSchema>
|
||||||
export type ConsumerWithProjectAndDeployment = BuildQueryResult<
|
export type ConsumerWithProjectAndDeployment = BuildQueryResult<
|
||||||
|
@ -51,7 +61,13 @@ export type ConsumerWithProjectAndDeployment = BuildQueryResult<
|
||||||
Tables['consumers'],
|
Tables['consumers'],
|
||||||
{ with: { project: true; deployment: true } }
|
{ with: { project: true; deployment: true } }
|
||||||
>
|
>
|
||||||
export type RawConsumer = InferSelectModel<typeof schema.consumers>
|
export type RawConsumer = Simplify<
|
||||||
|
InferSelectModel<typeof schema.consumers> & {
|
||||||
|
user?: RawUser | null // TODO: remove null (requires drizzle-orm changes)
|
||||||
|
project?: RawProject | null // TODO: remove null (requires drizzle-orm changes)
|
||||||
|
deployment?: RawDeployment | null // TODO: remove null (requires drizzle-orm changes)
|
||||||
|
}
|
||||||
|
>
|
||||||
export type ConsumerUpdate = Partial<
|
export type ConsumerUpdate = Partial<
|
||||||
Omit<
|
Omit<
|
||||||
InferInsertModel<typeof schema.consumers>,
|
InferInsertModel<typeof schema.consumers>,
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { db, eq, type RawDeployment, schema } from '@/db'
|
||||||
|
import { acl } from '@/lib/acl'
|
||||||
|
import { assert } from '@/lib/utils'
|
||||||
|
|
||||||
|
import type { AuthenticatedContext } from '../types'
|
||||||
|
import { resolveDeploymentVersion } from './resolve-deployment-version'
|
||||||
|
|
||||||
|
export async function publishDeployment(
|
||||||
|
ctx: AuthenticatedContext,
|
||||||
|
{
|
||||||
|
deployment,
|
||||||
|
version: rawVersion
|
||||||
|
}: {
|
||||||
|
deployment: RawDeployment
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
): Promise<RawDeployment> {
|
||||||
|
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(ctx, project, { label: 'Project' })
|
||||||
|
|
||||||
|
const version = resolveDeploymentVersion({
|
||||||
|
deploymentId: deployment.id,
|
||||||
|
project,
|
||||||
|
version: rawVersion
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const [[updatedDeployment]] = await db.transaction(async (tx) => {
|
||||||
|
return Promise.all([
|
||||||
|
// Update the deployment
|
||||||
|
tx
|
||||||
|
.update(schema.deployments)
|
||||||
|
.set({
|
||||||
|
published: true,
|
||||||
|
version
|
||||||
|
})
|
||||||
|
.where(eq(schema.deployments.id, deployment.id))
|
||||||
|
.returning(),
|
||||||
|
|
||||||
|
tx
|
||||||
|
.update(schema.projects)
|
||||||
|
.set({
|
||||||
|
lastPublishedDeploymentId: deployment.id
|
||||||
|
})
|
||||||
|
.where(eq(schema.projects.id, project.id))
|
||||||
|
|
||||||
|
// TODO: add publishDeploymentLogEntry
|
||||||
|
])
|
||||||
|
})
|
||||||
|
assert(
|
||||||
|
updatedDeployment,
|
||||||
|
500,
|
||||||
|
`Failed to update deployment "${deployment.id}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
return updatedDeployment
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import semver from 'semver'
|
||||||
|
|
||||||
|
import type { RawProject } from '@/db/types'
|
||||||
|
import { assert } from '@/lib/utils'
|
||||||
|
|
||||||
|
export function resolveDeploymentVersion({
|
||||||
|
deploymentId,
|
||||||
|
version: rawVersion,
|
||||||
|
project
|
||||||
|
}: {
|
||||||
|
deploymentId: string
|
||||||
|
version: string
|
||||||
|
project: RawProject
|
||||||
|
}): string | undefined {
|
||||||
|
const version = semver.clean(rawVersion)
|
||||||
|
assert(version, 400, `Invalid semver version "${rawVersion}"`)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
semver.valid(version),
|
||||||
|
400,
|
||||||
|
`Invalid semver version "${version}" for deployment "${deploymentId}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const lastPublishedVersion = project.lastPublishedDeployment?.version
|
||||||
|
assert(
|
||||||
|
!lastPublishedVersion || semver.gt(version, lastPublishedVersion),
|
||||||
|
400,
|
||||||
|
`Semver version "${version}" must be greater than the current published version "${lastPublishedVersion}" for deployment "${deploymentId}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
|
@ -24,6 +24,8 @@
|
||||||
- https://github.com/NangoHQ/nango
|
- https://github.com/NangoHQ/nango
|
||||||
- https://github.com/transitive-bullshit?submit=Search&q=oauth&tab=stars&type=&sort=&direction=&submit=Search
|
- https://github.com/transitive-bullshit?submit=Search&q=oauth&tab=stars&type=&sort=&direction=&submit=Search
|
||||||
- clerk / workos / auth0
|
- clerk / workos / auth0
|
||||||
|
- db
|
||||||
|
- replace `enabled` with soft `deletedAt` timestamp
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue