kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
2484e7efdb
commit
264a5455a3
|
@ -75,11 +75,11 @@ export function registerV1DeploymentsCreateDeployment(
|
|||
|
||||
// TODO: investigate better short hash generation
|
||||
const hash = sha256().slice(0, 8)
|
||||
const deploymentId = `${project.id}@${hash}`
|
||||
const deploymentIdentifier = `${project.identifier}@${hash}`
|
||||
assert(
|
||||
validators.deploymentId(deploymentId),
|
||||
validators.deploymentIdentifier(deploymentIdentifier),
|
||||
400,
|
||||
`Invalid deployment id "${deploymentId}"`
|
||||
`Invalid deployment identifier "${deploymentIdentifier}"`
|
||||
)
|
||||
|
||||
let { version } = body
|
||||
|
@ -87,13 +87,13 @@ export function registerV1DeploymentsCreateDeployment(
|
|||
assert(
|
||||
version,
|
||||
400,
|
||||
`Deployment "version" field is required to publish deployment "${deploymentId}"`
|
||||
`Deployment "version" field is required to publish deployment "${deploymentIdentifier}"`
|
||||
)
|
||||
}
|
||||
|
||||
if (version) {
|
||||
version = normalizeDeploymentVersion({
|
||||
deploymentId,
|
||||
deploymentIdentifier,
|
||||
project,
|
||||
version
|
||||
})
|
||||
|
@ -102,36 +102,36 @@ export function registerV1DeploymentsCreateDeployment(
|
|||
// Validate OpenAPI originUrl and originAdapter
|
||||
await validateDeploymentOriginAdapter({
|
||||
...pick(body, 'originUrl', 'originAdapter'),
|
||||
deploymentId,
|
||||
deploymentIdentifier,
|
||||
logger
|
||||
})
|
||||
|
||||
let [[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(),
|
||||
// Create the deployment
|
||||
let [deployment] = await db
|
||||
.insert(schema.deployments)
|
||||
.values({
|
||||
...body,
|
||||
identifier: deploymentIdentifier,
|
||||
hash,
|
||||
userId: user.id,
|
||||
teamId: teamMember?.teamId,
|
||||
projectId,
|
||||
version
|
||||
})
|
||||
.returning()
|
||||
assert(
|
||||
deployment,
|
||||
500,
|
||||
`Failed to create deployment "${deploymentIdentifier}"`
|
||||
)
|
||||
|
||||
// Update the project
|
||||
tx
|
||||
.update(schema.projects)
|
||||
.set({
|
||||
lastDeploymentId: deploymentId
|
||||
})
|
||||
.where(eq(schema.projects.id, projectId))
|
||||
])
|
||||
})
|
||||
assert(deployment, 500, `Failed to create deployment "${deploymentId}"`)
|
||||
// Update the project
|
||||
await db
|
||||
.update(schema.projects)
|
||||
.set({
|
||||
lastDeploymentId: deployment.id
|
||||
})
|
||||
.where(eq(schema.projects.id, projectId))
|
||||
|
||||
if (publish) {
|
||||
deployment = await publishDeployment(c, {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
|||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import { tryGetDeployment } from '@/lib/deployments/try-get-deployment'
|
||||
import { getDeploymentById } from '@/lib/deployments/get-deployment-by-id'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
|
@ -45,7 +45,8 @@ export function registerV1DeploymentsGetDeployment(
|
|||
const { deploymentId } = c.req.valid('param')
|
||||
const { populate = [] } = c.req.valid('query')
|
||||
|
||||
const deployment = await tryGetDeployment(c, deploymentId, {
|
||||
const deployment = await getDeploymentById({
|
||||
deploymentId,
|
||||
with: {
|
||||
...Object.fromEntries(populate.map((field) => [field, true]))
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
|||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import { getDeploymentById } from '@/lib/deployments/get-deployment-by-id'
|
||||
import { publishDeployment } from '@/lib/deployments/publish-deployment'
|
||||
import { tryGetDeployment } from '@/lib/deployments/try-get-deployment'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
|
@ -54,7 +54,7 @@ export function registerV1DeploymentsPublishDeployment(
|
|||
const { version } = c.req.valid('json')
|
||||
|
||||
// First ensure the deployment exists and the user has access to it
|
||||
const deployment = await tryGetDeployment(c, deploymentId)
|
||||
const deployment = await getDeploymentById({ deploymentId })
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
await acl(c, deployment, { label: 'Deployment' })
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ 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 { tryGetDeployment } from '@/lib/deployments/try-get-deployment'
|
||||
import { getDeploymentById } from '@/lib/deployments/get-deployment-by-id'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
|
@ -53,7 +53,7 @@ export function registerV1DeploymentsUpdateDeployment(
|
|||
const body = c.req.valid('json')
|
||||
|
||||
// First ensure the deployment exists and the user has access to it
|
||||
let deployment = await tryGetDeployment(c, deploymentId)
|
||||
let deployment = await getDeploymentById({ deploymentId })
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
await acl(c, deployment, { label: 'Deployment' })
|
||||
|
||||
|
|
|
@ -53,17 +53,17 @@ export function registerV1ProjectsCreateProject(
|
|||
|
||||
const teamMember = c.get('teamMember')
|
||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||
const id = `${namespace}/${body.name}`
|
||||
const identifier = `${namespace}/${body.name}`
|
||||
|
||||
const [project] = await db
|
||||
.insert(schema.projects)
|
||||
.values({
|
||||
...body,
|
||||
id,
|
||||
identifier,
|
||||
teamId: teamMember?.teamId,
|
||||
userId: user.id,
|
||||
_secret: sha256(),
|
||||
_providerToken: createProviderToken({ id })
|
||||
_providerToken: createProviderToken({ identifier })
|
||||
})
|
||||
.returning()
|
||||
assert(project, 500, `Failed to create project "${body.name}"`)
|
||||
|
|
|
@ -2,8 +2,8 @@ import jwt from 'jsonwebtoken'
|
|||
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
export function createProviderToken(project: { id: string }) {
|
||||
export function createProviderToken(project: { identifier: string }) {
|
||||
// TODO: Possibly in the future store stripe account ID as well and require
|
||||
// provider tokens to refresh after account changes?
|
||||
return jwt.sign({ projectId: project.id }, env.JWT_SECRET)
|
||||
return jwt.sign({ projectIdentifier: project.identifier }, env.JWT_SECRET)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { assert } from '@agentic/platform-core'
|
||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
import type { AuthenticatedContext } from '@/lib/types'
|
||||
import { and, db, eq, schema } from '@/db'
|
||||
import { and, db, eq, type RawDeployment, type RawProject, schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import { upsertStripeConnectCustomer } from '@/lib/billing/upsert-stripe-connect-customer'
|
||||
import { upsertStripeCustomer } from '@/lib/billing/upsert-stripe-customer'
|
||||
|
@ -25,12 +24,42 @@ export async function upsertConsumer(
|
|||
assert(consumerId || deploymentId, 400, 'Missing required "deploymentId"')
|
||||
const logger = c.get('logger')
|
||||
const userId = c.get('userId')
|
||||
let deployment: RawDeployment | undefined
|
||||
let project: RawProject | undefined
|
||||
let projectId: string | undefined
|
||||
|
||||
async function initDeploymentAndProject() {
|
||||
assert(deploymentId, 400, 'Missing required "deploymentId"')
|
||||
if (deployment && project) {
|
||||
// Already initialized
|
||||
return
|
||||
}
|
||||
|
||||
deployment = await db.query.deployments.findFirst({
|
||||
where: eq(schema.deployments.id, deploymentId),
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
})
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
assert(
|
||||
!deployment.deletedAt,
|
||||
410,
|
||||
`Deployment has been deleted by its owner "${deployment.id}"`
|
||||
)
|
||||
await acl(c, deployment, { label: 'Deployment' })
|
||||
|
||||
project = deployment.project!
|
||||
assert(
|
||||
project,
|
||||
404,
|
||||
`Project not found "${projectId}" for deployment "${deploymentId}"`
|
||||
)
|
||||
await acl(c, project, { label: 'Project' })
|
||||
}
|
||||
|
||||
if (deploymentId) {
|
||||
const parsedIds = parseFaasIdentifier(deploymentId)
|
||||
assert(parsedIds, 400, 'Invalid "deploymentId"')
|
||||
projectId = parsedIds.projectId
|
||||
await initDeploymentAndProject()
|
||||
}
|
||||
|
||||
if (!consumerId) {
|
||||
|
@ -83,28 +112,15 @@ export async function upsertConsumer(
|
|||
existingConsumer.plan !== plan ||
|
||||
existingConsumer.deploymentId !== deploymentId,
|
||||
409,
|
||||
`User "${user.email}" already has an active subscription to plan "${plan}" for project "${projectId}"`
|
||||
|
||||
plan
|
||||
? `User "${user.email}" already has an active subscription to plan "${plan}" for project "${projectId}"`
|
||||
: `User "${user.email}" already has cancelled their subscription for project "${projectId}"`
|
||||
)
|
||||
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
where: eq(schema.deployments.id, deploymentId),
|
||||
with: {
|
||||
project: true
|
||||
}
|
||||
})
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
|
||||
const { project } = deployment
|
||||
assert(
|
||||
project,
|
||||
404,
|
||||
`Project not found "${projectId}" for deployment "${deploymentId}"`
|
||||
)
|
||||
assert(
|
||||
!deployment.deletedAt,
|
||||
410,
|
||||
`Deployment has been deleted by its owner "${deployment.id}"`
|
||||
)
|
||||
await initDeploymentAndProject()
|
||||
assert(deployment, 500, `Error getting deployment "${deploymentId}"`)
|
||||
assert(project, 500, `Error getting project "${projectId}"`)
|
||||
|
||||
if (plan) {
|
||||
const pricingPlan = deployment.pricingPlans.find((p) => p.slug === plan)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { db, eq, type RawDeployment, schema } from '@/db'
|
||||
|
||||
/**
|
||||
* Finds the Deployment with the given id.
|
||||
*
|
||||
* Does not take care of ACLs.
|
||||
*
|
||||
* Returns `undefined` if not found.
|
||||
*/
|
||||
export async function getDeploymentById({
|
||||
deploymentId,
|
||||
...dbQueryOpts
|
||||
}: {
|
||||
deploymentId: string
|
||||
with?: {
|
||||
user?: true
|
||||
team?: true
|
||||
project?: true
|
||||
}
|
||||
}): Promise<RawDeployment | undefined> {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.deployments.id, deploymentId)
|
||||
})
|
||||
|
||||
return deployment
|
||||
}
|
|
@ -4,11 +4,11 @@ import semver from 'semver'
|
|||
import type { RawProject } from '@/db'
|
||||
|
||||
export function normalizeDeploymentVersion({
|
||||
deploymentId,
|
||||
deploymentIdentifier,
|
||||
version: rawVersion,
|
||||
project
|
||||
}: {
|
||||
deploymentId: string
|
||||
deploymentIdentifier: string
|
||||
version: string
|
||||
project: RawProject
|
||||
}): string | undefined {
|
||||
|
@ -18,14 +18,14 @@ export function normalizeDeploymentVersion({
|
|||
assert(
|
||||
semver.valid(version),
|
||||
400,
|
||||
`Invalid semver version "${version}" for deployment "${deploymentId}"`
|
||||
`Invalid semver version "${version}" for deployment "${deploymentIdentifier}"`
|
||||
)
|
||||
|
||||
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}"`
|
||||
`Semver version "${version}" must be greater than the current published version "${lastPublishedVersion}" for deployment "${deploymentIdentifier}"`
|
||||
)
|
||||
|
||||
return version
|
||||
|
|
|
@ -26,7 +26,7 @@ export async function publishDeployment(
|
|||
await acl(ctx, project, { label: 'Project' })
|
||||
|
||||
const version = normalizeDeploymentVersion({
|
||||
deploymentId: deployment.id,
|
||||
deploymentIdentifier: deployment.identifier,
|
||||
project,
|
||||
version: rawVersion
|
||||
})
|
||||
|
|
|
@ -6,46 +6,57 @@ import { db, eq, type RawDeployment, schema } from '@/db'
|
|||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
|
||||
/**
|
||||
* Attempts to find the Deployment matching the given identifier.
|
||||
* Attempts to find the Deployment matching the given deployment identifier.
|
||||
*
|
||||
* Throws a HTTP 404 error if not found.
|
||||
*
|
||||
* Does not take care of ACLs.
|
||||
*/
|
||||
export async function tryGetDeployment(
|
||||
export async function tryGetDeploymentByIdentifier(
|
||||
ctx: AuthenticatedContext,
|
||||
identifier: string,
|
||||
dbQueryOpts: {
|
||||
{
|
||||
deploymentIdentifier,
|
||||
...dbQueryOpts
|
||||
}: {
|
||||
deploymentIdentifier: string
|
||||
with?: {
|
||||
user?: true
|
||||
team?: true
|
||||
project?: true
|
||||
}
|
||||
} = {}
|
||||
): Promise<RawDeployment | undefined> {
|
||||
}
|
||||
): Promise<RawDeployment> {
|
||||
const user = await ensureAuthUser(ctx)
|
||||
|
||||
const teamMember = ctx.get('teamMember')
|
||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||
const parsedFaas = parseFaasIdentifier(identifier, {
|
||||
const parsedFaas = parseFaasIdentifier(deploymentIdentifier, {
|
||||
namespace
|
||||
})
|
||||
assert(parsedFaas, 400, `Invalid deployment identifier "${identifier}"`)
|
||||
assert(
|
||||
parsedFaas,
|
||||
400,
|
||||
`Invalid deployment identifier "${deploymentIdentifier}"`
|
||||
)
|
||||
|
||||
const { projectId, deploymentHash, version } = parsedFaas
|
||||
const { projectIdentifier, deploymentHash, version } = parsedFaas
|
||||
|
||||
if (deploymentHash) {
|
||||
const deploymentId = `${projectId}@${deploymentHash}`
|
||||
const deploymentIdentifier = `${projectIdentifier}@${deploymentHash}`
|
||||
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.deployments.id, deploymentId)
|
||||
where: eq(schema.deployments.identifier, deploymentIdentifier)
|
||||
})
|
||||
assert(deployment, 404, `Deployment not found "${deploymentId}"`)
|
||||
assert(deployment, 404, `Deployment not found "${deploymentIdentifier}"`)
|
||||
|
||||
return deployment
|
||||
} else if (version === 'latest') {
|
||||
const project = await db.query.projects.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.projects.id, projectId)
|
||||
where: eq(schema.projects.identifier, projectIdentifier)
|
||||
})
|
||||
assert(project, 404, `Project not found "${projectId}"`)
|
||||
assert(project, 404, `Project not found "${projectIdentifier}"`)
|
||||
assert(
|
||||
project.lastPublishedDeploymentId,
|
||||
404,
|
||||
|
@ -66,9 +77,9 @@ export async function tryGetDeployment(
|
|||
} else if (version === 'dev') {
|
||||
const project = await db.query.projects.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.projects.id, projectId)
|
||||
where: eq(schema.projects.id, projectIdentifier)
|
||||
})
|
||||
assert(project, 404, `Project not found "${projectId}"`)
|
||||
assert(project, 404, `Project not found "${projectIdentifier}"`)
|
||||
assert(
|
||||
project.lastDeploymentId,
|
||||
404,
|
||||
|
@ -88,5 +99,5 @@ export async function tryGetDeployment(
|
|||
return deployment
|
||||
}
|
||||
|
||||
assert(false, 400, `Invalid Deployment identifier "${identifier}"`)
|
||||
assert(false, 400, `Invalid Deployment identifier "${deploymentIdentifier}"`)
|
||||
}
|
|
@ -10,12 +10,12 @@ import { validateOpenAPISpec } from '@/lib/validate-openapi-spec'
|
|||
* NOTE: This method may mutate `originAdapter.spec`.
|
||||
*/
|
||||
export async function validateDeploymentOriginAdapter({
|
||||
deploymentId,
|
||||
deploymentIdentifier,
|
||||
originUrl,
|
||||
originAdapter,
|
||||
logger
|
||||
}: {
|
||||
deploymentId: string
|
||||
deploymentIdentifier: string
|
||||
originUrl: string
|
||||
originAdapter: DeploymentOriginAdapter
|
||||
logger: Logger
|
||||
|
@ -23,14 +23,14 @@ export async function validateDeploymentOriginAdapter({
|
|||
assert(
|
||||
originUrl,
|
||||
400,
|
||||
`Origin URL is required for deployment "${deploymentId}"`
|
||||
`Origin URL is required for deployment "${deploymentIdentifier}"`
|
||||
)
|
||||
|
||||
if (originAdapter.type === 'openapi') {
|
||||
assert(
|
||||
originAdapter.spec,
|
||||
400,
|
||||
`OpenAPI spec is required for deployment "${deploymentId}" with origin adapter type set to "openapi"`
|
||||
`OpenAPI spec is required for deployment "${deploymentIdentifier}" with origin adapter type set to "openapi"`
|
||||
)
|
||||
|
||||
// Validate and normalize the OpenAPI spec
|
||||
|
@ -54,7 +54,7 @@ export async function validateDeploymentOriginAdapter({
|
|||
assert(
|
||||
originAdapter.type === 'raw',
|
||||
400,
|
||||
`Invalid origin adapter type "${originAdapter.type}" for deployment "${deploymentId}"`
|
||||
`Invalid origin adapter type "${originAdapter.type}" for deployment "${deploymentIdentifier}"`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ import type { AuthenticatedEnv } from '@/lib/types'
|
|||
import { and, db, eq, schema } from '@/db'
|
||||
import { aclTeamMember } from '@/lib/acl-team-member'
|
||||
|
||||
// TODO: Instead of accepting `teamId` query param, change the authenticate
|
||||
// middleware to accept a different JWT payload and then use that to
|
||||
// determine the intended user and/or team.
|
||||
|
||||
export const team = createMiddleware<AuthenticatedEnv>(
|
||||
async function teamMiddleware(ctx, next) {
|
||||
const teamId = ctx.req.query('teamId')
|
||||
|
|
|
@ -33,7 +33,10 @@ export function stripeId<U extends string, T extends Readonly<[U, ...U[]]>>(
|
|||
/**
|
||||
* `namespace/projectName`
|
||||
*/
|
||||
export function projectId<U extends string, T extends Readonly<[U, ...U[]]>>(
|
||||
export function projectIdentifier<
|
||||
U extends string,
|
||||
T extends Readonly<[U, ...U[]]>
|
||||
>(
|
||||
config?: PgVarcharConfig<T | Writable<T>, never>
|
||||
): PgVarcharBuilderInitial<'', Writable<T>, 130> {
|
||||
return varchar({ length: 130, ...config })
|
||||
|
@ -42,7 +45,10 @@ export function projectId<U extends string, T extends Readonly<[U, ...U[]]>>(
|
|||
/**
|
||||
* `namespace/projectName@hash`
|
||||
*/
|
||||
export function deploymentId<U extends string, T extends Readonly<[U, ...U[]]>>(
|
||||
export function deploymentIdentifier<
|
||||
U extends string,
|
||||
T extends Readonly<[U, ...U[]]>
|
||||
>(
|
||||
config?: PgVarcharConfig<T | Writable<T>, never>
|
||||
): PgVarcharBuilderInitial<'', Writable<T>, 160> {
|
||||
return varchar({ length: 160, ...config })
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { validators } from '@agentic/platform-validators'
|
||||
import { relations } from '@fisch0920/drizzle-orm'
|
||||
import {
|
||||
boolean,
|
||||
|
@ -14,9 +13,7 @@ import {
|
|||
createSelectSchema,
|
||||
createUpdateSchema,
|
||||
cuid,
|
||||
deploymentId,
|
||||
id,
|
||||
projectId,
|
||||
stripeId,
|
||||
timestamps
|
||||
} from './common'
|
||||
|
@ -70,7 +67,7 @@ export const consumers = pgTable(
|
|||
.references(() => users.id),
|
||||
|
||||
// The project this user is subscribed to
|
||||
projectId: projectId()
|
||||
projectId: cuid()
|
||||
.notNull()
|
||||
.references(() => projects.id, {
|
||||
onDelete: 'cascade'
|
||||
|
@ -78,7 +75,7 @@ export const consumers = pgTable(
|
|||
|
||||
// The specific deployment this user is subscribed to, since pricing can
|
||||
// change across deployment versions)
|
||||
deploymentId: deploymentId()
|
||||
deploymentId: cuid()
|
||||
.notNull()
|
||||
.references(() => deployments.id, {
|
||||
onDelete: 'cascade'
|
||||
|
@ -134,17 +131,7 @@ export const consumersRelations = relations(consumers, ({ one }) => ({
|
|||
}))
|
||||
|
||||
export const consumerSelectSchema = createSelectSchema(consumers, {
|
||||
_stripeSubscriptionItemIdMap: stripeSubscriptionItemIdMapSchema,
|
||||
|
||||
deploymentId: (schema) =>
|
||||
schema.refine((id) => validators.deploymentId(id), {
|
||||
message: 'Invalid deployment id'
|
||||
}),
|
||||
|
||||
projectId: (schema) =>
|
||||
schema.refine((id) => validators.projectId(id), {
|
||||
message: 'Invalid project id'
|
||||
})
|
||||
_stripeSubscriptionItemIdMap: stripeSubscriptionItemIdMapSchema
|
||||
})
|
||||
.omit({
|
||||
_stripeSubscriptionId: true,
|
||||
|
@ -171,10 +158,7 @@ export const consumerSelectSchema = createSelectSchema(consumers, {
|
|||
.openapi('Consumer')
|
||||
|
||||
export const consumerInsertSchema = createInsertSchema(consumers, {
|
||||
deploymentId: (schema) =>
|
||||
schema.refine((id) => validators.deploymentId(id), {
|
||||
message: 'Invalid deployment id'
|
||||
}),
|
||||
deploymentId: (schema) => schema.cuid2().optional(),
|
||||
|
||||
plan: z.string().nonempty()
|
||||
})
|
||||
|
@ -186,12 +170,7 @@ export const consumerInsertSchema = createInsertSchema(consumers, {
|
|||
.strict()
|
||||
|
||||
export const consumerUpdateSchema = createUpdateSchema(consumers, {
|
||||
deploymentId: (schema) =>
|
||||
schema
|
||||
.refine((id) => validators.deploymentId(id), {
|
||||
message: 'Invalid deployment id'
|
||||
})
|
||||
.optional()
|
||||
deploymentId: (schema) => schema.cuid2().optional()
|
||||
})
|
||||
.pick({
|
||||
plan: true,
|
||||
|
|
|
@ -5,17 +5,19 @@ import {
|
|||
index,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text
|
||||
text,
|
||||
uniqueIndex
|
||||
} from '@fisch0920/drizzle-orm/pg-core'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
import { deploymentIdentifierSchema, projectIdSchema } from '../schemas'
|
||||
import {
|
||||
createInsertSchema,
|
||||
createSelectSchema,
|
||||
createUpdateSchema,
|
||||
cuid,
|
||||
deploymentId,
|
||||
projectId,
|
||||
deploymentIdentifier,
|
||||
id,
|
||||
timestamps
|
||||
} from './common'
|
||||
import { projects } from './project'
|
||||
|
@ -31,10 +33,10 @@ import { users } from './user'
|
|||
export const deployments = pgTable(
|
||||
'deployments',
|
||||
{
|
||||
// namespace/projectName@hash
|
||||
id: deploymentId().primaryKey(),
|
||||
id,
|
||||
...timestamps,
|
||||
|
||||
identifier: deploymentIdentifier().unique().notNull(),
|
||||
hash: text().notNull(),
|
||||
version: text(),
|
||||
|
||||
|
@ -48,7 +50,7 @@ export const deployments = pgTable(
|
|||
.notNull()
|
||||
.references(() => users.id),
|
||||
teamId: cuid().references(() => teams.id),
|
||||
projectId: projectId()
|
||||
projectId: cuid()
|
||||
.notNull()
|
||||
.references(() => projects.id, {
|
||||
onDelete: 'cascade'
|
||||
|
@ -78,6 +80,7 @@ export const deployments = pgTable(
|
|||
// coupons: jsonb().$type<Coupon[]>().default([]).notNull()
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('deployment_identifier_idx').on(table.identifier),
|
||||
index('deployment_userId_idx').on(table.userId),
|
||||
index('deployment_teamId_idx').on(table.teamId),
|
||||
index('deployment_projectId_idx').on(table.projectId),
|
||||
|
@ -112,12 +115,7 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
|||
// TODO: virtual openapi spec? (hide openapi.servers)
|
||||
|
||||
export const deploymentSelectSchema = createSelectSchema(deployments, {
|
||||
// build: z.object({}),
|
||||
// env: z.object({}),
|
||||
id: (schema) =>
|
||||
schema.refine((id) => validators.deploymentId(id), {
|
||||
message: 'Invalid deployment id'
|
||||
}),
|
||||
identifier: deploymentIdentifierSchema,
|
||||
|
||||
hash: (schema) =>
|
||||
schema.refine((hash) => validators.deploymentHash(hash), {
|
||||
|
@ -148,10 +146,7 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
|
|||
.openapi('Deployment')
|
||||
|
||||
export const deploymentInsertSchema = createInsertSchema(deployments, {
|
||||
projectId: (schema) =>
|
||||
schema.refine((id) => validators.projectId(id), {
|
||||
message: 'Invalid project id'
|
||||
}),
|
||||
projectId: projectIdSchema,
|
||||
|
||||
iconUrl: (schema) =>
|
||||
schema
|
||||
|
|
|
@ -5,11 +5,9 @@ import {
|
|||
createInsertSchema,
|
||||
createSelectSchema,
|
||||
cuid,
|
||||
deploymentId,
|
||||
id,
|
||||
logEntryLevelEnum,
|
||||
logEntryTypeEnum,
|
||||
projectId,
|
||||
timestamps
|
||||
} from './common'
|
||||
import { consumers } from './consumer'
|
||||
|
@ -39,8 +37,8 @@ export const logEntries = pgTable(
|
|||
|
||||
// relations (optional)
|
||||
userId: cuid(),
|
||||
projectId: projectId(),
|
||||
deploymentId: deploymentId(),
|
||||
projectId: cuid(),
|
||||
deploymentId: cuid(),
|
||||
consumerId: cuid(),
|
||||
|
||||
// misc metadata (optional)
|
||||
|
|
|
@ -6,19 +6,21 @@ import {
|
|||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text
|
||||
text,
|
||||
uniqueIndex
|
||||
} from '@fisch0920/drizzle-orm/pg-core'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
import { projectIdentifierSchema } from '../schemas'
|
||||
import {
|
||||
createInsertSchema,
|
||||
createSelectSchema,
|
||||
createUpdateSchema,
|
||||
cuid,
|
||||
deploymentId,
|
||||
id,
|
||||
pricingCurrencyEnum,
|
||||
pricingIntervalEnum,
|
||||
projectId,
|
||||
projectIdentifier,
|
||||
stripeId,
|
||||
timestamps
|
||||
} from './common'
|
||||
|
@ -38,10 +40,10 @@ import { users } from './user'
|
|||
export const projects = pgTable(
|
||||
'projects',
|
||||
{
|
||||
// namespace/projectName
|
||||
id: projectId().primaryKey(),
|
||||
id,
|
||||
...timestamps,
|
||||
|
||||
identifier: projectIdentifier().unique().notNull(),
|
||||
name: text().notNull(),
|
||||
alias: text(),
|
||||
|
||||
|
@ -51,10 +53,10 @@ export const projects = pgTable(
|
|||
teamId: cuid(),
|
||||
|
||||
// Most recently published Deployment if one exists
|
||||
lastPublishedDeploymentId: deploymentId(),
|
||||
lastPublishedDeploymentId: cuid(),
|
||||
|
||||
// Most recent Deployment if one exists
|
||||
lastDeploymentId: deploymentId(),
|
||||
lastDeploymentId: cuid(),
|
||||
|
||||
applicationFeePercent: integer().default(20).notNull(),
|
||||
|
||||
|
@ -122,6 +124,7 @@ export const projects = pgTable(
|
|||
_stripeAccountId: stripeId()
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('project_identifier_idx').on(table.identifier),
|
||||
index('project_userId_idx').on(table.userId),
|
||||
index('project_teamId_idx').on(table.teamId),
|
||||
index('project_alias_idx').on(table.alias),
|
||||
|
@ -159,6 +162,8 @@ export const projectsRelations = relations(projects, ({ one }) => ({
|
|||
}))
|
||||
|
||||
export const projectSelectSchema = createSelectSchema(projects, {
|
||||
identifier: projectIdentifierSchema,
|
||||
|
||||
applicationFeePercent: (schema) => schema.nonnegative(),
|
||||
|
||||
_stripeProductIdMap: stripeProductIdMapSchema,
|
||||
|
@ -202,10 +207,7 @@ export const projectSelectSchema = createSelectSchema(projects, {
|
|||
.openapi('Project')
|
||||
|
||||
export const projectInsertSchema = createInsertSchema(projects, {
|
||||
id: (schema) =>
|
||||
schema.refine((id) => validators.projectId(id), {
|
||||
message: 'Invalid project id'
|
||||
}),
|
||||
identifier: projectIdentifierSchema,
|
||||
|
||||
name: (schema) =>
|
||||
schema.refine((name) => validators.projectName(name), {
|
||||
|
|
|
@ -13,19 +13,25 @@ function getCuidSchema(idLabel: string) {
|
|||
|
||||
export const cuidSchema = getCuidSchema('id')
|
||||
export const userIdSchema = getCuidSchema('user id')
|
||||
export const teamIdSchema = getCuidSchema('team id')
|
||||
export const consumerIdSchema = getCuidSchema('consumer id')
|
||||
export const projectIdSchema = getCuidSchema('project id')
|
||||
export const deploymentIdSchema = getCuidSchema('deployment id')
|
||||
export const logEntryIdSchema = getCuidSchema('log entry id')
|
||||
|
||||
export const projectIdSchema = z
|
||||
export const projectIdentifierSchema = z
|
||||
.string()
|
||||
.refine((id) => validators.projectId(id), {
|
||||
message: 'Invalid project id'
|
||||
.refine((id) => validators.projectIdentifier(id), {
|
||||
message: 'Invalid project identifier'
|
||||
})
|
||||
.openapi('ProjectIdentifier')
|
||||
|
||||
export const deploymentIdSchema = z
|
||||
export const deploymentIdentifierSchema = z
|
||||
.string()
|
||||
.refine((id) => validators.deploymentId(id), {
|
||||
message: 'Invalid deployment id'
|
||||
.refine((id) => validators.deploymentIdentifier(id), {
|
||||
message: 'Invalid deployment identifier'
|
||||
})
|
||||
.openapi('DeploymentIdentifier')
|
||||
|
||||
export const usernameSchema = z
|
||||
.string()
|
||||
|
|
|
@ -6,14 +6,16 @@ import * as validators from './validators'
|
|||
function success(...args: Parameters<typeof parseFaasIdentifier>) {
|
||||
const result = parseFaasIdentifier(...args)
|
||||
expect(result).toBeTruthy()
|
||||
expect(result!.projectId).toBeTruthy()
|
||||
expect(result!.projectIdentifier).toBeTruthy()
|
||||
expect(result!.version || result!.deploymentHash).toBeTruthy()
|
||||
expect(validators.projectId(result!.projectId)).toBe(true)
|
||||
expect(validators.projectIdentifier(result!.projectIdentifier)).toBe(true)
|
||||
expect(validators.servicePath(result!.servicePath)).toBe(true)
|
||||
|
||||
if (result!.deploymentHash) {
|
||||
expect(validators.deploymentHash(result!.deploymentHash)).toBe(true)
|
||||
expect(validators.deploymentId(result!.deploymentId!)).toBe(true)
|
||||
expect(validators.deploymentIdentifier(result!.deploymentIdentifier!)).toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
expect(result).toMatchSnapshot()
|
||||
|
|
|
@ -5,7 +5,7 @@ import { parseFaasUri } from './parse-faas-uri'
|
|||
function success(value: string) {
|
||||
const result = parseFaasUri(value)
|
||||
expect(result).toBeTruthy()
|
||||
expect(result?.projectId).toBeTruthy()
|
||||
expect(result?.projectIdentifier).toBeTruthy()
|
||||
expect(result?.version || result?.deploymentHash).toBeTruthy()
|
||||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
|
|
|
@ -22,15 +22,15 @@ export function parseFaasUri(uri: string): ParsedFaasIdentifier | undefined {
|
|||
const pdsMatch = uri.match(projectDeploymentServiceRe)
|
||||
|
||||
if (pdsMatch) {
|
||||
const projectId = pdsMatch[1]!
|
||||
const projectIdentifier = pdsMatch[1]!
|
||||
const deploymentHash = pdsMatch[2]!
|
||||
const servicePath = pdsMatch[3] || '/'
|
||||
|
||||
return {
|
||||
projectId,
|
||||
projectIdentifier,
|
||||
deploymentHash,
|
||||
servicePath,
|
||||
deploymentId: `${projectId}@${deploymentHash}`
|
||||
deploymentIdentifier: `${projectIdentifier}@${deploymentHash}`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ export function parseFaasUri(uri: string): ParsedFaasIdentifier | undefined {
|
|||
|
||||
if (pvsMatch) {
|
||||
return {
|
||||
projectId: pvsMatch[1]!,
|
||||
projectIdentifier: pvsMatch[1]!,
|
||||
version: pvsMatch[2]!,
|
||||
servicePath: pvsMatch[3] || '/'
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export function parseFaasUri(uri: string): ParsedFaasIdentifier | undefined {
|
|||
|
||||
if (psMatch) {
|
||||
return {
|
||||
projectId: psMatch[1]!,
|
||||
projectIdentifier: psMatch[1]!,
|
||||
servicePath: psMatch[2] || '/',
|
||||
version: 'latest'
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
export type ParsedFaasIdentifier = {
|
||||
projectId: string
|
||||
projectIdentifier: string
|
||||
servicePath: string
|
||||
deploymentHash?: string
|
||||
deploymentId?: string
|
||||
deploymentIdentifier?: string
|
||||
version?: string
|
||||
} & (
|
||||
| {
|
||||
deploymentHash: string
|
||||
deploymentId: string
|
||||
deploymentIdentifier: string
|
||||
}
|
||||
| {
|
||||
version: string
|
||||
|
|
|
@ -72,40 +72,50 @@ test('deploymentHash failure', () => {
|
|||
expect(validators.deploymentHash('012345678')).toBe(false)
|
||||
})
|
||||
|
||||
test('projectId success', () => {
|
||||
expect(validators.projectId('username/project-name')).toBe(true)
|
||||
expect(validators.projectId('a/123')).toBe(true)
|
||||
test('projectIdentifier success', () => {
|
||||
expect(validators.projectIdentifier('username/project-name')).toBe(true)
|
||||
expect(validators.projectIdentifier('a/123')).toBe(true)
|
||||
})
|
||||
|
||||
test('projectId failure', () => {
|
||||
expect(validators.projectId('aaa//0123')).toBe(false)
|
||||
expect(validators.projectId('foo@bar')).toBe(false)
|
||||
expect(validators.projectId('abc/1.23')).toBe(false)
|
||||
expect(validators.projectId('012345678/123@latest')).toBe(false)
|
||||
expect(validators.projectId('foo@dev')).toBe(false)
|
||||
expect(validators.projectId('username/Project-Name')).toBe(false)
|
||||
expect(validators.projectId('_/___')).toBe(false)
|
||||
test('projectIdentifier failure', () => {
|
||||
expect(validators.projectIdentifier('aaa//0123')).toBe(false)
|
||||
expect(validators.projectIdentifier('foo@bar')).toBe(false)
|
||||
expect(validators.projectIdentifier('abc/1.23')).toBe(false)
|
||||
expect(validators.projectIdentifier('012345678/123@latest')).toBe(false)
|
||||
expect(validators.projectIdentifier('foo@dev')).toBe(false)
|
||||
expect(validators.projectIdentifier('username/Project-Name')).toBe(false)
|
||||
expect(validators.projectIdentifier('_/___')).toBe(false)
|
||||
})
|
||||
|
||||
test('deploymentId success', () => {
|
||||
expect(validators.deploymentId('username/project-name@01234567')).toBe(true)
|
||||
expect(validators.deploymentId('a/123@01234567')).toBe(true)
|
||||
test('deploymentIdentifier success', () => {
|
||||
expect(
|
||||
validators.deploymentIdentifier('username/project-name@01234567')
|
||||
).toBe(true)
|
||||
expect(validators.deploymentIdentifier('a/123@01234567')).toBe(true)
|
||||
})
|
||||
|
||||
test('deploymentId failure', () => {
|
||||
expect(validators.deploymentId('username/project-name@012345678')).toBe(false)
|
||||
expect(validators.deploymentId('username/project-name@latest')).toBe(false)
|
||||
expect(validators.deploymentId('username/project-name@dev')).toBe(false)
|
||||
expect(validators.deploymentId('username/Project-Name@01234567')).toBe(false)
|
||||
expect(validators.deploymentId('a/123@0123A567')).toBe(false)
|
||||
expect(validators.deploymentId('_/___@012.4567')).toBe(false)
|
||||
expect(validators.deploymentId('_/___@01234567')).toBe(false)
|
||||
expect(validators.deploymentId('aaa//0123@01234567')).toBe(false)
|
||||
expect(validators.deploymentId('foo@bar@01234567')).toBe(false)
|
||||
expect(validators.deploymentId('abc/1.23@01234567')).toBe(false)
|
||||
expect(validators.deploymentId('012345678/123@latest')).toBe(false)
|
||||
expect(validators.deploymentId('012345678/123@dev')).toBe(false)
|
||||
expect(validators.deploymentId('012345678/123@1.0.1')).toBe(false)
|
||||
test('deploymentIdentifier failure', () => {
|
||||
expect(
|
||||
validators.deploymentIdentifier('username/project-name@012345678')
|
||||
).toBe(false)
|
||||
expect(validators.deploymentIdentifier('username/project-name@latest')).toBe(
|
||||
false
|
||||
)
|
||||
expect(validators.deploymentIdentifier('username/project-name@dev')).toBe(
|
||||
false
|
||||
)
|
||||
expect(
|
||||
validators.deploymentIdentifier('username/Project-Name@01234567')
|
||||
).toBe(false)
|
||||
expect(validators.deploymentIdentifier('a/123@0123A567')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('_/___@012.4567')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('_/___@01234567')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('aaa//0123@01234567')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('foo@bar@01234567')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('abc/1.23@01234567')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('012345678/123@latest')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('012345678/123@dev')).toBe(false)
|
||||
expect(validators.deploymentIdentifier('012345678/123@1.0.1')).toBe(false)
|
||||
})
|
||||
|
||||
test('serviceName success', () => {
|
||||
|
|
|
@ -40,11 +40,11 @@ export function deploymentHash(value: string): boolean {
|
|||
return !!value && deploymentHashRe.test(value)
|
||||
}
|
||||
|
||||
export function projectId(value: string): boolean {
|
||||
export function projectIdentifier(value: string): boolean {
|
||||
return !!value && projectRe.test(value)
|
||||
}
|
||||
|
||||
export function deploymentId(value: string): boolean {
|
||||
export function deploymentIdentifier(value: string): boolean {
|
||||
return !!value && deploymentRe.test(value)
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue