kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
c85d2e449a
commit
09198abd29
|
@ -3,10 +3,12 @@ import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
|||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { and, db, eq, schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponses
|
||||
} from '@/lib/openapi-utils'
|
||||
import { tryGetProjectByIdentifier } from '@/lib/projects/try-get-project-by-identifier'
|
||||
|
||||
import { paginationAndPopulateAndFilterDeploymentSchema } from './schemas'
|
||||
|
||||
|
@ -43,18 +45,31 @@ export function registerV1DeploymentsListDeployments(
|
|||
sort = 'desc',
|
||||
sortBy = 'createdAt',
|
||||
populate = [],
|
||||
projectId
|
||||
projectIdentifier,
|
||||
deploymentIdentifier
|
||||
} = c.req.valid('query')
|
||||
|
||||
const userId = c.get('userId')
|
||||
const teamMember = c.get('teamMember')
|
||||
let projectId: string | undefined
|
||||
|
||||
if (projectIdentifier) {
|
||||
const project = await tryGetProjectByIdentifier(c, {
|
||||
projectIdentifier
|
||||
})
|
||||
await acl(c, project, { label: 'Project' })
|
||||
projectId = project.id
|
||||
}
|
||||
|
||||
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
|
||||
projectId ? eq(schema.deployments.projectId, projectId) : undefined,
|
||||
deploymentIdentifier
|
||||
? eq(schema.deployments.identifier, deploymentIdentifier)
|
||||
: undefined
|
||||
),
|
||||
with: {
|
||||
...Object.fromEntries(populate.map((field) => [field, true]))
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
deploymentIdSchema,
|
||||
deploymentRelationsSchema,
|
||||
paginationSchema,
|
||||
projectIdSchema
|
||||
projectIdentifierSchema
|
||||
} from '@/db'
|
||||
|
||||
export const deploymentIdParamsSchema = z.object({
|
||||
|
@ -23,7 +23,8 @@ export const createDeploymentQuerySchema = z.object({
|
|||
})
|
||||
|
||||
export const filterDeploymentSchema = z.object({
|
||||
projectId: projectIdSchema.optional()
|
||||
projectIdentifier: projectIdentifierSchema.optional(),
|
||||
deploymentIdentifier: deploymentIdentifierSchema.optional()
|
||||
})
|
||||
|
||||
export const populateDeploymentSchema = z.object({
|
||||
|
|
|
@ -2,7 +2,7 @@ import { assert } from '@agentic/platform-core'
|
|||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
import type { AuthenticatedContext } from '@/lib/types'
|
||||
import { db, eq, type RawDeployment, schema } from '@/db'
|
||||
import { db, deploymentIdSchema, eq, type RawDeployment, schema } from '@/db'
|
||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
|
||||
/**
|
||||
|
@ -28,6 +28,16 @@ export async function tryGetDeploymentByIdentifier(
|
|||
): Promise<RawDeployment> {
|
||||
const user = await ensureAuthUser(ctx)
|
||||
|
||||
// First check if the identifier is a deployment ID
|
||||
if (deploymentIdSchema.safeParse(deploymentIdentifier).success) {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.deployments.id, deploymentIdentifier)
|
||||
})
|
||||
assert(deployment, 404, `Deployment not found "${deploymentIdentifier}"`)
|
||||
return deployment
|
||||
}
|
||||
|
||||
const teamMember = ctx.get('teamMember')
|
||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||
const parsedFaas = parseFaasIdentifier(deploymentIdentifier, {
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { assert } from '@agentic/platform-core'
|
||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
import type { AuthenticatedContext } from '@/lib/types'
|
||||
import { db, eq, projectIdSchema, type RawProject, schema } from '@/db'
|
||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
|
||||
/**
|
||||
* Attempts to find the Project matching the given identifier.
|
||||
*
|
||||
* Throws a HTTP 404 error if not found.
|
||||
*
|
||||
* Does not take care of ACLs.
|
||||
*/
|
||||
export async function tryGetProjectByIdentifier(
|
||||
ctx: AuthenticatedContext,
|
||||
{
|
||||
projectIdentifier,
|
||||
...dbQueryOpts
|
||||
}: {
|
||||
projectIdentifier: string
|
||||
with?: {
|
||||
user?: true
|
||||
team?: true
|
||||
lastPublishedproject?: true
|
||||
lastproject?: true
|
||||
}
|
||||
}
|
||||
): Promise<RawProject> {
|
||||
const user = await ensureAuthUser(ctx)
|
||||
|
||||
// First check if the identifier is a project ID
|
||||
if (projectIdSchema.safeParse(projectIdentifier).success) {
|
||||
const project = await db.query.projects.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.projects.id, projectIdentifier)
|
||||
})
|
||||
assert(project, 404, `project not found "${projectIdentifier}"`)
|
||||
return project
|
||||
}
|
||||
|
||||
const teamMember = ctx.get('teamMember')
|
||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
|
||||
namespace
|
||||
})
|
||||
assert(
|
||||
parsedFaas?.projectIdentifier,
|
||||
400,
|
||||
`Invalid project identifier "${projectIdentifier}"`
|
||||
)
|
||||
|
||||
const project = await db.query.projects.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.projects.identifier, parsedFaas.projectIdentifier)
|
||||
})
|
||||
assert(project, 404, `Project not found "${projectIdentifier}"`)
|
||||
|
||||
return project
|
||||
}
|
|
@ -1379,8 +1379,10 @@ export interface operations {
|
|||
sort?: "asc" | "desc";
|
||||
sortBy?: "createdAt" | "updatedAt";
|
||||
populate?: ("user" | "team" | "project")[];
|
||||
/** @description Project id (e.g. "proj_tz4a98xxat96iws9zmbrgj3a") */
|
||||
projectId?: string;
|
||||
/** @description Public project identifier (e.g. "namespace/project-name") */
|
||||
projectIdentifier?: components["schemas"]["ProjectIdentifier"];
|
||||
/** @description Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}") */
|
||||
deploymentIdentifier?: components["schemas"]["DeploymentIdentifier"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@agentic/platform-api-client": "workspace:*",
|
||||
"@agentic/platform-core": "workspace:*",
|
||||
"@agentic/platform-schemas": "workspace:*",
|
||||
"@agentic/platform-validators": "workspace:*",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@hono/node-server": "^1.14.1",
|
||||
"commander": "^14.0.0",
|
||||
|
|
|
@ -5,6 +5,8 @@ import { Command } from 'commander'
|
|||
import restoreCursor from 'restore-cursor'
|
||||
|
||||
import { registerDeployCommand } from './commands/deploy'
|
||||
import { registerGetDeploymentCommand } from './commands/get'
|
||||
import { registerListDeploymentsCommand } from './commands/list'
|
||||
import { registerPublishCommand } from './commands/publish'
|
||||
import { registerSigninCommand } from './commands/signin'
|
||||
import { registerSignoutCommand } from './commands/signout'
|
||||
|
@ -75,6 +77,8 @@ async function main() {
|
|||
registerSignoutCommand(ctx)
|
||||
registerDeployCommand(ctx)
|
||||
registerPublishCommand(ctx)
|
||||
registerGetDeploymentCommand(ctx)
|
||||
registerListDeploymentsCommand(ctx)
|
||||
|
||||
program.parse()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { Command } from 'commander'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
import type { Context } from '../types'
|
||||
import { AuthStore } from '../lib/auth-store'
|
||||
|
||||
export function registerGetDeploymentCommand({
|
||||
client,
|
||||
program,
|
||||
logger
|
||||
}: Context) {
|
||||
const command = new Command('get')
|
||||
.description('Gets details for a specific deployment')
|
||||
.argument('<deploymentIdentifier>', 'Deployment ID or identifier')
|
||||
.action(async (deploymentIdentifier) => {
|
||||
AuthStore.requireAuth()
|
||||
|
||||
const deployment = await oraPromise(
|
||||
client.getDeploymentByIdentifier({
|
||||
deploymentIdentifier
|
||||
}),
|
||||
{
|
||||
text: 'Resolving deployment...',
|
||||
successText: 'Resolved deployment',
|
||||
failText: 'Failed to resolve deployment'
|
||||
}
|
||||
)
|
||||
|
||||
logger.log(deployment)
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import type { Deployment } from '@agentic/platform-api-client'
|
||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
import { Command } from 'commander'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
import type { Context } from '../types'
|
||||
import { AuthStore } from '../lib/auth-store'
|
||||
import { pruneDeployment } from '../lib/utils'
|
||||
|
||||
export function registerListDeploymentsCommand({
|
||||
client,
|
||||
program,
|
||||
logger
|
||||
}: Context) {
|
||||
const command = new Command('list')
|
||||
.alias('ls')
|
||||
.description('Lists deployments')
|
||||
.argument('[projectIdentifier]', 'Optional project identifier')
|
||||
.option('-v, --verbose', 'Display full deployments', false)
|
||||
.action(async (projectIdentifier, opts) => {
|
||||
AuthStore.requireAuth()
|
||||
|
||||
const query: Parameters<typeof client.listDeployments>[0] = {}
|
||||
let label = 'Fetching all projects and deployments'
|
||||
|
||||
if (projectIdentifier) {
|
||||
const auth = AuthStore.getAuth()
|
||||
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
|
||||
// TODO: use team slug if available
|
||||
namespace: auth.user.username
|
||||
})
|
||||
|
||||
if (!parsedFaas) {
|
||||
throw new Error(`Invalid project identifier "${projectIdentifier}"`)
|
||||
}
|
||||
|
||||
if (parsedFaas.deploymentIdentifier) {
|
||||
query.deploymentIdentifier = parsedFaas.deploymentIdentifier
|
||||
label = `Fetching deployment "${query.deploymentIdentifier}"`
|
||||
} else {
|
||||
query.projectIdentifier = parsedFaas.projectIdentifier
|
||||
label = `Fetching deployments for project "${query.projectIdentifier}"`
|
||||
}
|
||||
}
|
||||
|
||||
const deployments = await oraPromise(client.listDeployments(query), label)
|
||||
|
||||
const projectIdToDeploymentsMap: Record<string, Deployment[]> = {}
|
||||
const sortedProjects: {
|
||||
projectId: string
|
||||
deployments: Deployment[]
|
||||
}[] = []
|
||||
|
||||
// Aggregate deployments by project
|
||||
for (const deployment of deployments) {
|
||||
const prunedDeployment = pruneDeployment(deployment, opts)
|
||||
|
||||
const { projectId } = deployment
|
||||
if (!projectIdToDeploymentsMap[projectId]) {
|
||||
projectIdToDeploymentsMap[projectId] = []
|
||||
}
|
||||
|
||||
projectIdToDeploymentsMap[projectId].push(prunedDeployment)
|
||||
}
|
||||
|
||||
// Sort deployments within each project with recently created first
|
||||
for (const projectId of Object.keys(projectIdToDeploymentsMap)) {
|
||||
const deployments = projectIdToDeploymentsMap[projectId]!
|
||||
deployments.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
|
||||
sortedProjects.push({
|
||||
projectId,
|
||||
deployments
|
||||
})
|
||||
}
|
||||
|
||||
// Sort projects with most recently created first
|
||||
sortedProjects.sort(
|
||||
(a, b) =>
|
||||
new Date(b.deployments[0]!.createdAt).getTime() -
|
||||
new Date(a.deployments[0]!.createdAt).getTime()
|
||||
)
|
||||
|
||||
// TODO: better output formatting
|
||||
logger.log(sortedProjects)
|
||||
})
|
||||
|
||||
program.addCommand(command)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import type { Deployment } from '@agentic/platform-api-client'
|
||||
|
||||
export function pruneDeployment(
|
||||
deployment: Deployment,
|
||||
{ verbose = false }: { verbose?: boolean }
|
||||
): Deployment {
|
||||
if (!verbose) {
|
||||
const d = structuredClone(deployment)
|
||||
|
||||
if (d.readme) {
|
||||
d.readme = '<omitted>'
|
||||
}
|
||||
|
||||
if (d.originAdapter?.type === 'openapi') {
|
||||
d.originAdapter.spec = '<omitted>'
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
return deployment
|
||||
}
|
|
@ -6,17 +6,17 @@ import type { ParsedFaasIdentifier } from './types'
|
|||
// namespace/project-name@deploymentHash/servicePath
|
||||
// project@deploymentHash/servicePath
|
||||
const projectDeploymentServiceRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{3,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
// namespace/project-name@version/servicePath
|
||||
// project@version/servicePath
|
||||
const projectVersionServiceRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{3,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
// namespace/project-name/servicePath
|
||||
// project/servicePath (latest version)
|
||||
const projectServiceRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{3,64})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
export function parseFaasUri(uri: string): ParsedFaasIdentifier | undefined {
|
||||
const pdsMatch = uri.match(projectDeploymentServiceRe)
|
||||
|
|
|
@ -241,6 +241,9 @@ importers:
|
|||
'@agentic/platform-schemas':
|
||||
specifier: workspace:*
|
||||
version: link:../schemas
|
||||
'@agentic/platform-validators':
|
||||
specifier: workspace:*
|
||||
version: link:../validators
|
||||
'@clack/prompts':
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0
|
||||
|
|
Ładowanie…
Reference in New Issue