Compare commits
2 Commits
c40532c757
...
a067d167e9
| Author | SHA1 | Date | |
|---|---|---|---|
| a067d167e9 | |||
| d22fdc9618 |
@@ -14,3 +14,5 @@ dist/
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
.env
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import { initMiddlewares } from './middlewares/index.ts';
|
|
||||||
import { log } from './libs/logger.ts';
|
|
||||||
import { ExecutionQueue } from './libs/execution-queue.ts';
|
import { ExecutionQueue } from './libs/execution-queue.ts';
|
||||||
|
import { log } from './libs/logger.ts';
|
||||||
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
||||||
|
import { initMiddlewares } from './middlewares/index.ts';
|
||||||
|
|
||||||
// 初始化应用
|
// 初始化应用
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
@@ -26,7 +26,7 @@ async function initializeApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动应用
|
// 启动应用
|
||||||
initializeApp().catch(error => {
|
initializeApp().catch((error) => {
|
||||||
console.error('Failed to start application:', error);
|
console.error('Failed to start application:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get, Post } from '../../decorators/route.ts';
|
import { Controller, Get, Post } from '../../decorators/route.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { gitea } from '../../libs/gitea.ts';
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { loginSchema } from './dto.ts';
|
import { loginSchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/auth')
|
@Controller('/auth')
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ export const createDeploymentSchema = z.object({
|
|||||||
branch: z.string().min(1, { message: '分支不能为空' }),
|
branch: z.string().min(1, { message: '分支不能为空' }),
|
||||||
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
||||||
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
||||||
env: z.string().optional(),
|
envVars: z.record(z.string(), z.string()).optional(), // 环境变量 key-value 对象
|
||||||
sparseCheckoutPaths: z.string().optional(), // 添加稀疏检出路径字段
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get, Post } from '../../decorators/route.ts';
|
import { Controller, Get, Post } from '../../decorators/route.ts';
|
||||||
import type { Prisma } from '../../generated/client.ts';
|
import type { Prisma } from '../../generated/client.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
|
||||||
import type { Context } from 'koa';
|
|
||||||
import { listDeploymentsQuerySchema, createDeploymentSchema } from './dto.ts';
|
|
||||||
import { ExecutionQueue } from '../../libs/execution-queue.ts';
|
import { ExecutionQueue } from '../../libs/execution-queue.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { createDeploymentSchema, listDeploymentsQuerySchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/deployments')
|
@Controller('/deployments')
|
||||||
export class DeploymentController {
|
export class DeploymentController {
|
||||||
@Get('')
|
@Get('')
|
||||||
async list(ctx: Context) {
|
async list(ctx: Context) {
|
||||||
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(ctx.query);
|
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(
|
||||||
|
ctx.query,
|
||||||
|
);
|
||||||
const where: Prisma.DeploymentWhereInput = {
|
const where: Prisma.DeploymentWhereInput = {
|
||||||
valid: 1,
|
valid: 1,
|
||||||
};
|
};
|
||||||
@@ -50,8 +52,7 @@ export class DeploymentController {
|
|||||||
connect: { id: body.projectId },
|
connect: { id: body.projectId },
|
||||||
},
|
},
|
||||||
pipelineId: body.pipelineId,
|
pipelineId: body.pipelineId,
|
||||||
env: body.env || 'dev',
|
envVars: body.envVars ? JSON.stringify(body.envVars) : null,
|
||||||
sparseCheckoutPaths: body.sparseCheckoutPaths || '', // 添加稀疏检出路径
|
|
||||||
buildLog: '',
|
buildLog: '',
|
||||||
createdBy: 'system', // TODO: get from user
|
createdBy: 'system', // TODO: get from user
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
@@ -73,7 +74,7 @@ export class DeploymentController {
|
|||||||
|
|
||||||
// 获取原始部署记录
|
// 获取原始部署记录
|
||||||
const originalDeployment = await prisma.deployment.findUnique({
|
const originalDeployment = await prisma.deployment.findUnique({
|
||||||
where: { id: Number(id) }
|
where: { id: Number(id) },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!originalDeployment) {
|
if (!originalDeployment) {
|
||||||
@@ -82,7 +83,7 @@ export class DeploymentController {
|
|||||||
code: 404,
|
code: 404,
|
||||||
message: '部署记录不存在',
|
message: '部署记录不存在',
|
||||||
data: null,
|
data: null,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -96,8 +97,7 @@ export class DeploymentController {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
projectId: originalDeployment.projectId,
|
projectId: originalDeployment.projectId,
|
||||||
pipelineId: originalDeployment.pipelineId,
|
pipelineId: originalDeployment.pipelineId,
|
||||||
env: originalDeployment.env,
|
envVars: originalDeployment.envVars,
|
||||||
sparseCheckoutPaths: originalDeployment.sparseCheckoutPaths,
|
|
||||||
buildLog: '',
|
buildLog: '',
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
@@ -113,7 +113,7 @@ export class DeploymentController {
|
|||||||
code: 0,
|
code: 0,
|
||||||
message: '重新执行任务已创建',
|
message: '重新执行任务已创建',
|
||||||
data: newDeployment,
|
data: newDeployment,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const getCommitsQuerySchema = z.object({
|
export const getCommitsQuerySchema = z.object({
|
||||||
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
projectId: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive({ message: 'Project ID is required' }),
|
||||||
branch: z.string().optional(),
|
branch: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getBranchesQuerySchema = z.object({
|
export const getBranchesQuerySchema = z.object({
|
||||||
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
projectId: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive({ message: 'Project ID is required' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get } from '../../decorators/route.ts';
|
import { Controller, Get } from '../../decorators/route.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
|
||||||
import { gitea } from '../../libs/gitea.ts';
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { getCommitsQuerySchema, getBranchesQuerySchema } from './dto.ts';
|
import { getBranchesQuerySchema, getCommitsQuerySchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/git')
|
@Controller('/git')
|
||||||
export class GitController {
|
export class GitController {
|
||||||
@@ -33,7 +33,11 @@ export class GitController {
|
|||||||
console.log('Access token present:', !!accessToken);
|
console.log('Access token present:', !!accessToken);
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
throw new BusinessError(
|
||||||
|
'Gitea access token not found. Please login again.',
|
||||||
|
1004,
|
||||||
|
401,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -65,7 +69,11 @@ export class GitController {
|
|||||||
const accessToken = ctx.session?.gitea?.access_token;
|
const accessToken = ctx.session?.gitea?.access_token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
throw new BusinessError(
|
||||||
|
'Gitea access token not found. Please login again.',
|
||||||
|
1004,
|
||||||
|
401,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -85,7 +93,7 @@ export class GitController {
|
|||||||
|
|
||||||
// Handle SCP-like syntax: git@host:owner/repo.git
|
// Handle SCP-like syntax: git@host:owner/repo.git
|
||||||
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
|
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
|
||||||
const scpMatch = cleanUrl.match(/:([^\/]+)\/([^\/]+?)(\.git)?$/);
|
const scpMatch = cleanUrl.match(/:([^/]+)\/([^/]+?)(\.git)?$/);
|
||||||
if (scpMatch) {
|
if (scpMatch) {
|
||||||
return { owner: scpMatch[1], repo: scpMatch[2] };
|
return { owner: scpMatch[1], repo: scpMatch[2] };
|
||||||
}
|
}
|
||||||
@@ -96,13 +104,15 @@ export class GitController {
|
|||||||
const urlObj = new URL(cleanUrl);
|
const urlObj = new URL(cleanUrl);
|
||||||
const parts = urlObj.pathname.split('/').filter(Boolean);
|
const parts = urlObj.pathname.split('/').filter(Boolean);
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
const repo = parts.pop()!.replace(/\.git$/, '');
|
const repo = parts.pop()?.replace(/\.git$/, '');
|
||||||
const owner = parts.pop()!;
|
const owner = parts.pop();
|
||||||
return { owner, repo };
|
if (repo && owner) {
|
||||||
|
return { owner, repo };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
// Fallback to simple regex
|
// Fallback to simple regex
|
||||||
const match = cleanUrl.match(/([^\/]+)\/([^\/]+?)(\.git)?$/);
|
const match = cleanUrl.match(/([^/]+)\/([^/]+?)(\.git)?$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
return { owner: match[1], repo: match[2] };
|
return { owner: match[1], repo: match[2] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// 控制器统一导出
|
// 控制器统一导出
|
||||||
export { ProjectController } from './project/index.ts';
|
|
||||||
export { UserController } from './user/index.ts';
|
|
||||||
export { AuthController } from './auth/index.ts';
|
export { AuthController } from './auth/index.ts';
|
||||||
export { DeploymentController } from './deployment/index.ts';
|
export { DeploymentController } from './deployment/index.ts';
|
||||||
export { PipelineController } from './pipeline/index.ts';
|
|
||||||
export { StepController } from './step/index.ts'
|
|
||||||
export { GitController } from './git/index.ts';
|
export { GitController } from './git/index.ts';
|
||||||
|
export { PipelineController } from './pipeline/index.ts';
|
||||||
|
export { ProjectController } from './project/index.ts';
|
||||||
|
export { StepController } from './step/index.ts';
|
||||||
|
export { UserController } from './user/index.ts';
|
||||||
|
|||||||
@@ -2,36 +2,59 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
// 定义验证架构
|
// 定义验证架构
|
||||||
export const createPipelineSchema = z.object({
|
export const createPipelineSchema = z.object({
|
||||||
name: z.string({
|
name: z
|
||||||
message: '流水线名称必须是字符串',
|
.string({
|
||||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }),
|
message: '流水线名称必须是字符串',
|
||||||
|
})
|
||||||
|
.min(1, { message: '流水线名称不能为空' })
|
||||||
|
.max(100, { message: '流水线名称不能超过100个字符' }),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
message: '流水线描述必须是字符串',
|
.string({
|
||||||
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
message: '流水线描述必须是字符串',
|
||||||
|
})
|
||||||
|
.max(500, { message: '流水线描述不能超过500个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
projectId: z.number({
|
projectId: z
|
||||||
message: '项目ID必须是数字',
|
.number({
|
||||||
}).int().positive({ message: '项目ID必须是正整数' }).optional(),
|
message: '项目ID必须是数字',
|
||||||
|
})
|
||||||
|
.int()
|
||||||
|
.positive({ message: '项目ID必须是正整数' })
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updatePipelineSchema = z.object({
|
export const updatePipelineSchema = z.object({
|
||||||
name: z.string({
|
name: z
|
||||||
message: '流水线名称必须是字符串',
|
.string({
|
||||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }).optional(),
|
message: '流水线名称必须是字符串',
|
||||||
|
})
|
||||||
|
.min(1, { message: '流水线名称不能为空' })
|
||||||
|
.max(100, { message: '流水线名称不能超过100个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
message: '流水线描述必须是字符串',
|
.string({
|
||||||
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
message: '流水线描述必须是字符串',
|
||||||
|
})
|
||||||
|
.max(500, { message: '流水线描述不能超过500个字符' })
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pipelineIdSchema = z.object({
|
export const pipelineIdSchema = z.object({
|
||||||
id: z.coerce.number().int().positive({ message: '流水线 ID 必须是正整数' }),
|
id: z.coerce.number().int().positive({ message: '流水线 ID 必须是正整数' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listPipelinesQuerySchema = z.object({
|
export const listPipelinesQuerySchema = z
|
||||||
projectId: z.coerce.number().int().positive({ message: '项目ID必须是正整数' }).optional(),
|
.object({
|
||||||
}).optional();
|
projectId: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive({ message: '项目ID必须是正整数' })
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
// 类型
|
// 类型
|
||||||
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import {
|
||||||
|
createPipelineFromTemplate,
|
||||||
|
getAvailableTemplates,
|
||||||
|
} from '../../libs/pipeline-template.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { getAvailableTemplates, createPipelineFromTemplate } from '../../libs/pipeline-template.ts';
|
|
||||||
import {
|
import {
|
||||||
createPipelineSchema,
|
createPipelineSchema,
|
||||||
updatePipelineSchema,
|
|
||||||
pipelineIdSchema,
|
|
||||||
listPipelinesQuerySchema,
|
listPipelinesQuerySchema,
|
||||||
|
pipelineIdSchema,
|
||||||
|
updatePipelineSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/pipelines')
|
@Controller('/pipelines')
|
||||||
@@ -46,7 +49,7 @@ export class PipelineController {
|
|||||||
|
|
||||||
// GET /api/pipelines/templates - 获取可用的流水线模板
|
// GET /api/pipelines/templates - 获取可用的流水线模板
|
||||||
@Get('/templates')
|
@Get('/templates')
|
||||||
async getTemplates(ctx: Context) {
|
async getTemplates(_ctx: Context) {
|
||||||
try {
|
try {
|
||||||
const templates = await getAvailableTemplates();
|
const templates = await getAvailableTemplates();
|
||||||
return templates;
|
return templates;
|
||||||
@@ -126,7 +129,7 @@ export class PipelineController {
|
|||||||
templateId,
|
templateId,
|
||||||
projectId,
|
projectId,
|
||||||
name,
|
name,
|
||||||
description || ''
|
description || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 返回新创建的流水线
|
// 返回新创建的流水线
|
||||||
|
|||||||
@@ -5,46 +5,83 @@ import { projectDirSchema } from '../../libs/path-validator.js';
|
|||||||
* 创建项目验证架构
|
* 创建项目验证架构
|
||||||
*/
|
*/
|
||||||
export const createProjectSchema = z.object({
|
export const createProjectSchema = z.object({
|
||||||
name: z.string({
|
name: z
|
||||||
message: '项目名称必须是字符串',
|
.string({
|
||||||
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }),
|
message: '项目名称必须是字符串',
|
||||||
|
})
|
||||||
|
.min(2, { message: '项目名称至少2个字符' })
|
||||||
|
.max(50, { message: '项目名称不能超过50个字符' }),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
message: '项目描述必须是字符串',
|
.string({
|
||||||
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
message: '项目描述必须是字符串',
|
||||||
|
})
|
||||||
|
.max(200, { message: '项目描述不能超过200个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
repository: z.string({
|
repository: z
|
||||||
message: '仓库地址必须是字符串',
|
.string({
|
||||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }),
|
message: '仓库地址必须是字符串',
|
||||||
|
})
|
||||||
|
.url({ message: '请输入有效的仓库地址' })
|
||||||
|
.min(1, { message: '仓库地址不能为空' }),
|
||||||
|
|
||||||
projectDir: projectDirSchema,
|
projectDir: projectDirSchema,
|
||||||
|
|
||||||
|
envPresets: z.string().optional(), // JSON 字符串格式
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新项目验证架构
|
* 更新项目验证架构
|
||||||
*/
|
*/
|
||||||
export const updateProjectSchema = z.object({
|
export const updateProjectSchema = z.object({
|
||||||
name: z.string({
|
name: z
|
||||||
message: '项目名称必须是字符串',
|
.string({
|
||||||
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }).optional(),
|
message: '项目名称必须是字符串',
|
||||||
|
})
|
||||||
|
.min(2, { message: '项目名称至少2个字符' })
|
||||||
|
.max(50, { message: '项目名称不能超过50个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
message: '项目描述必须是字符串',
|
.string({
|
||||||
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
message: '项目描述必须是字符串',
|
||||||
|
})
|
||||||
|
.max(200, { message: '项目描述不能超过200个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
repository: z.string({
|
repository: z
|
||||||
message: '仓库地址必须是字符串',
|
.string({
|
||||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }).optional(),
|
message: '仓库地址必须是字符串',
|
||||||
|
})
|
||||||
|
.url({ message: '请输入有效的仓库地址' })
|
||||||
|
.min(1, { message: '仓库地址不能为空' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
envPresets: z.string().optional(), // JSON 字符串格式
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目列表查询参数验证架构
|
* 项目列表查询参数验证架构
|
||||||
*/
|
*/
|
||||||
export const listProjectQuerySchema = z.object({
|
export const listProjectQuerySchema = z
|
||||||
page: z.coerce.number().int().min(1, { message: '页码必须大于0' }).optional().default(1),
|
.object({
|
||||||
limit: z.coerce.number().int().min(1, { message: '每页数量必须大于0' }).max(100, { message: '每页数量不能超过100' }).optional().default(10),
|
page: z.coerce
|
||||||
name: z.string().optional(),
|
.number()
|
||||||
}).optional();
|
.int()
|
||||||
|
.min(1, { message: '页码必须大于0' })
|
||||||
|
.optional()
|
||||||
|
.default(1),
|
||||||
|
limit: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, { message: '每页数量必须大于0' })
|
||||||
|
.max(100, { message: '每页数量不能超过100' })
|
||||||
|
.optional()
|
||||||
|
.default(10),
|
||||||
|
name: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目ID验证架构
|
* 项目ID验证架构
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
|
||||||
import { GitManager } from '../../libs/git-manager.ts';
|
import { GitManager } from '../../libs/git-manager.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import {
|
import {
|
||||||
createProjectSchema,
|
createProjectSchema,
|
||||||
updateProjectSchema,
|
|
||||||
listProjectQuerySchema,
|
listProjectQuerySchema,
|
||||||
projectIdSchema,
|
projectIdSchema,
|
||||||
|
updateProjectSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/projects')
|
@Controller('/projects')
|
||||||
@@ -135,6 +135,7 @@ export class ProjectController {
|
|||||||
description: validatedData.description || '',
|
description: validatedData.description || '',
|
||||||
repository: validatedData.repository,
|
repository: validatedData.repository,
|
||||||
projectDir: validatedData.projectDir,
|
projectDir: validatedData.projectDir,
|
||||||
|
envPresets: validatedData.envPresets,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
@@ -182,6 +183,9 @@ export class ProjectController {
|
|||||||
if (validatedData.repository !== undefined) {
|
if (validatedData.repository !== undefined) {
|
||||||
updateData.repository = validatedData.repository;
|
updateData.repository = validatedData.repository;
|
||||||
}
|
}
|
||||||
|
if (validatedData.envPresets !== undefined) {
|
||||||
|
updateData.envPresets = validatedData.envPresets;
|
||||||
|
}
|
||||||
|
|
||||||
const project = await prisma.project.update({
|
const project = await prisma.project.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
|
||||||
import {
|
import {
|
||||||
createStepSchema,
|
createStepSchema,
|
||||||
updateStepSchema,
|
|
||||||
stepIdSchema,
|
|
||||||
listStepsQuerySchema,
|
listStepsQuerySchema,
|
||||||
|
stepIdSchema,
|
||||||
|
updateStepSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/steps')
|
@Controller('/steps')
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import {
|
import {
|
||||||
userIdSchema,
|
|
||||||
createUserSchema,
|
createUserSchema,
|
||||||
updateUserSchema,
|
|
||||||
searchUserQuerySchema,
|
searchUserQuerySchema,
|
||||||
|
updateUserSchema,
|
||||||
|
userIdSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,14 +13,18 @@ import {
|
|||||||
*/
|
*/
|
||||||
@Controller('/user')
|
@Controller('/user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
|
|
||||||
@Get('/list')
|
@Get('/list')
|
||||||
async list(ctx: Context) {
|
async list(_ctx: Context) {
|
||||||
// 模拟用户列表数据
|
// 模拟用户列表数据
|
||||||
const users = [
|
const users = [
|
||||||
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
||||||
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
|
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
|
||||||
{ id: 3, name: 'Charlie', email: 'charlie@example.com', status: 'active' }
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Charlie',
|
||||||
|
email: 'charlie@example.com',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
@@ -33,10 +37,10 @@ export class UserController {
|
|||||||
// 模拟根据ID查找用户
|
// 模拟根据ID查找用户
|
||||||
const user = {
|
const user = {
|
||||||
id,
|
id,
|
||||||
name: 'User ' + id,
|
name: `User ${id}`,
|
||||||
email: `user${id}@example.com`,
|
email: `user${id}@example.com`,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id > 100) {
|
if (id > 100) {
|
||||||
@@ -55,7 +59,7 @@ export class UserController {
|
|||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
...body,
|
...body,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
status: body.status
|
status: body.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
@@ -70,7 +74,7 @@ export class UserController {
|
|||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
id,
|
id,
|
||||||
...body,
|
...body,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
@@ -88,7 +92,7 @@ export class UserController {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `用户 ${id} 已删除`,
|
message: `用户 ${id} 已删除`,
|
||||||
deletedAt: new Date().toISOString()
|
deletedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,25 +103,26 @@ export class UserController {
|
|||||||
// 模拟搜索逻辑
|
// 模拟搜索逻辑
|
||||||
let results = [
|
let results = [
|
||||||
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
||||||
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' }
|
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
results = results.filter(user =>
|
results = results.filter(
|
||||||
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
(user) =>
|
||||||
user.email.toLowerCase().includes(keyword.toLowerCase())
|
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
results = results.filter(user => user.status === status);
|
results = results.filter((user) => user.status === status);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
keyword,
|
keyword,
|
||||||
status,
|
status,
|
||||||
total: results.length,
|
total: results.length,
|
||||||
results
|
results,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,24 @@ const metadataStore = new WeakMap<any, Map<string | symbol, any>>();
|
|||||||
/**
|
/**
|
||||||
* 设置元数据(降级方案)
|
* 设置元数据(降级方案)
|
||||||
*/
|
*/
|
||||||
function setMetadata<T = any>(key: string | symbol, value: T, target: any): void {
|
function setMetadata<T = any>(
|
||||||
|
key: string | symbol,
|
||||||
|
value: T,
|
||||||
|
target: any,
|
||||||
|
): void {
|
||||||
if (!metadataStore.has(target)) {
|
if (!metadataStore.has(target)) {
|
||||||
metadataStore.set(target, new Map());
|
metadataStore.set(target, new Map());
|
||||||
}
|
}
|
||||||
metadataStore.get(target)!.set(key, value);
|
metadataStore.get(target)?.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取元数据(降级方案)
|
* 获取元数据(降级方案)
|
||||||
*/
|
*/
|
||||||
function getMetadata<T = any>(key: string | symbol, target: any): T | undefined {
|
function getMetadata<T = any>(
|
||||||
|
key: string | symbol,
|
||||||
|
target: any,
|
||||||
|
): T | undefined {
|
||||||
return metadataStore.get(target)?.get(key);
|
return metadataStore.get(target)?.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,24 +50,28 @@ function getMetadata<T = any>(key: string | symbol, target: any): T | undefined
|
|||||||
* 创建HTTP方法装饰器的工厂函数(TC39标准)
|
* 创建HTTP方法装饰器的工厂函数(TC39标准)
|
||||||
*/
|
*/
|
||||||
function createMethodDecorator(method: HttpMethod) {
|
function createMethodDecorator(method: HttpMethod) {
|
||||||
return function (path: string = '') {
|
return (path: string = '') =>
|
||||||
return function <This, Args extends any[], Return>(
|
<This, Args extends any[], Return>(
|
||||||
target: (this: This, ...args: Args) => Return,
|
target: (this: This, ...args: Args) => Return,
|
||||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
|
context: ClassMethodDecoratorContext<
|
||||||
) {
|
This,
|
||||||
|
(this: This, ...args: Args) => Return
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
// 在类初始化时执行
|
// 在类初始化时执行
|
||||||
context.addInitializer(function () {
|
context.addInitializer(function () {
|
||||||
// 使用 this.constructor 时需要类型断言
|
// 使用 this.constructor 时需要类型断言
|
||||||
const ctor = (this as any).constructor;
|
const ctor = (this as any).constructor;
|
||||||
|
|
||||||
// 获取现有的路由元数据
|
// 获取现有的路由元数据
|
||||||
const existingRoutes: RouteMetadata[] = getMetadata(ROUTE_METADATA_KEY, ctor) || [];
|
const existingRoutes: RouteMetadata[] =
|
||||||
|
getMetadata(ROUTE_METADATA_KEY, ctor) || [];
|
||||||
|
|
||||||
// 添加新的路由元数据
|
// 添加新的路由元数据
|
||||||
const newRoute: RouteMetadata = {
|
const newRoute: RouteMetadata = {
|
||||||
method,
|
method,
|
||||||
path,
|
path,
|
||||||
propertyKey: String(context.name)
|
propertyKey: String(context.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
existingRoutes.push(newRoute);
|
existingRoutes.push(newRoute);
|
||||||
@@ -71,7 +82,6 @@ function createMethodDecorator(method: HttpMethod) {
|
|||||||
|
|
||||||
return target;
|
return target;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,10 +119,10 @@ export const Patch = createMethodDecorator('PATCH');
|
|||||||
* @param prefix 路由前缀
|
* @param prefix 路由前缀
|
||||||
*/
|
*/
|
||||||
export function Controller(prefix: string = '') {
|
export function Controller(prefix: string = '') {
|
||||||
return function <T extends abstract new (...args: any) => any>(
|
return <T extends abstract new (...args: any) => any>(
|
||||||
target: T,
|
target: T,
|
||||||
context: ClassDecoratorContext<T>
|
context: ClassDecoratorContext<T>,
|
||||||
) {
|
) => {
|
||||||
// 在类初始化时保存控制器前缀
|
// 在类初始化时保存控制器前缀
|
||||||
context.addInitializer(function () {
|
context.addInitializer(function () {
|
||||||
setMetadata('prefix', prefix, this);
|
setMetadata('prefix', prefix, this);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -13,32 +12,32 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
import * as Prisma from './internal/prismaNamespaceBrowser.ts';
|
||||||
export { Prisma }
|
export { Prisma };
|
||||||
export * as $Enums from './enums.ts'
|
export * as $Enums from './enums.ts';
|
||||||
export * from './enums.ts';
|
export * from './enums.ts';
|
||||||
/**
|
/**
|
||||||
* Model Project
|
* Model Project
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Project = Prisma.ProjectModel
|
export type Project = Prisma.ProjectModel;
|
||||||
/**
|
/**
|
||||||
* Model User
|
* Model User
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type User = Prisma.UserModel
|
export type User = Prisma.UserModel;
|
||||||
/**
|
/**
|
||||||
* Model Pipeline
|
* Model Pipeline
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Pipeline = Prisma.PipelineModel
|
export type Pipeline = Prisma.PipelineModel;
|
||||||
/**
|
/**
|
||||||
* Model Step
|
* Model Step
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Step = Prisma.StepModel
|
export type Step = Prisma.StepModel;
|
||||||
/**
|
/**
|
||||||
* Model Deployment
|
* Model Deployment
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Deployment = Prisma.DeploymentModel
|
export type Deployment = Prisma.DeploymentModel;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -10,18 +9,19 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as process from 'node:process'
|
import * as path from 'node:path';
|
||||||
import * as path from 'node:path'
|
import * as process from 'node:process';
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url';
|
||||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/client"
|
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url));
|
||||||
import * as $Enums from "./enums.ts"
|
|
||||||
import * as $Class from "./internal/class.ts"
|
|
||||||
import * as Prisma from "./internal/prismaNamespace.ts"
|
|
||||||
|
|
||||||
export * as $Enums from './enums.ts'
|
import * as runtime from '@prisma/client/runtime/client';
|
||||||
export * from "./enums.ts"
|
import * as $Enums from './enums.ts';
|
||||||
|
import * as $Class from './internal/class.ts';
|
||||||
|
import * as Prisma from './internal/prismaNamespace.ts';
|
||||||
|
|
||||||
|
export * as $Enums from './enums.ts';
|
||||||
|
export * from './enums.ts';
|
||||||
/**
|
/**
|
||||||
* ## Prisma Client
|
* ## Prisma Client
|
||||||
*
|
*
|
||||||
@@ -35,32 +35,38 @@ export * from "./enums.ts"
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||||
*/
|
*/
|
||||||
export const PrismaClient = $Class.getPrismaClientClass()
|
export const PrismaClient = $Class.getPrismaClientClass();
|
||||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
export type PrismaClient<
|
||||||
export { Prisma }
|
LogOpts extends Prisma.LogLevel = never,
|
||||||
|
OmitOpts extends
|
||||||
|
Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'],
|
||||||
|
ExtArgs extends
|
||||||
|
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||||
|
> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>;
|
||||||
|
export { Prisma };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model Project
|
* Model Project
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Project = Prisma.ProjectModel
|
export type Project = Prisma.ProjectModel;
|
||||||
/**
|
/**
|
||||||
* Model User
|
* Model User
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type User = Prisma.UserModel
|
export type User = Prisma.UserModel;
|
||||||
/**
|
/**
|
||||||
* Model Pipeline
|
* Model Pipeline
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Pipeline = Prisma.PipelineModel
|
export type Pipeline = Prisma.PipelineModel;
|
||||||
/**
|
/**
|
||||||
* Model Step
|
* Model Step
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Step = Prisma.StepModel
|
export type Step = Prisma.StepModel;
|
||||||
/**
|
/**
|
||||||
* Model Deployment
|
* Model Deployment
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Deployment = Prisma.DeploymentModel
|
export type Deployment = Prisma.DeploymentModel;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -9,394 +8,419 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as runtime from "@prisma/client/runtime/client"
|
import type * as runtime from '@prisma/client/runtime/client';
|
||||||
import * as $Enums from "./enums.ts"
|
import * as $Enums from './enums.ts';
|
||||||
import type * as Prisma from "./internal/prismaNamespace.ts"
|
import type * as Prisma from './internal/prismaNamespace.ts';
|
||||||
|
|
||||||
|
|
||||||
export type IntFilter<$PrismaModel = never> = {
|
export type IntFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringFilter<$PrismaModel = never> = {
|
export type StringFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringNullableFilter<$PrismaModel = never> = {
|
export type StringNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type DateTimeFilter<$PrismaModel = never> = {
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type SortOrderInput = {
|
export type SortOrderInput = {
|
||||||
sort: Prisma.SortOrder
|
sort: Prisma.SortOrder;
|
||||||
nulls?: Prisma.NullsOrder
|
nulls?: Prisma.NullsOrder;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| string
|
||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| null;
|
||||||
}
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type BoolFilter<$PrismaModel = never> = {
|
export type BoolFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IntNullableFilter<$PrismaModel = never> = {
|
export type IntNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
| number
|
||||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| null;
|
||||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||||
}
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
}
|
| Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
| Date
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
|
||||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| Date
|
||||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| string
|
||||||
}
|
| null;
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedIntFilter<$PrismaModel = never> = {
|
export type NestedIntFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringFilter<$PrismaModel = never> = {
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
not?: Prisma.NestedFloatFilter<$PrismaModel> | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| string
|
||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| null;
|
||||||
}
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedBoolFilter<$PrismaModel = never> = {
|
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
| number
|
||||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| null;
|
||||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||||
}
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
}
|
| Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
| Date
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| Date
|
||||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| string
|
||||||
}
|
| null;
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
/*
|
/*
|
||||||
* This file exports all enum related types from the schema.
|
* This file exports all enum related types from the schema.
|
||||||
*
|
*
|
||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// This file is empty because there are no enums in the schema.
|
// This file is empty because there are no enums in the schema.
|
||||||
export {}
|
export {};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -11,47 +10,56 @@
|
|||||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/client"
|
import * as runtime from '@prisma/client/runtime/client';
|
||||||
import type * as Prisma from "./prismaNamespace.ts"
|
import type * as Prisma from './prismaNamespace.ts';
|
||||||
|
|
||||||
|
|
||||||
const config: runtime.GetPrismaClientConfig = {
|
const config: runtime.GetPrismaClientConfig = {
|
||||||
"previewFeatures": [],
|
previewFeatures: [],
|
||||||
"clientVersion": "7.0.0",
|
clientVersion: '7.0.0',
|
||||||
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513",
|
engineVersion: '0c19ccc313cf9911a90d99d2ac2eb0280c76c513',
|
||||||
"activeProvider": "sqlite",
|
activeProvider: 'sqlite',
|
||||||
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n projectDir String @unique // 项目工作目录路径(必填)\n // Relations\n deployments Deployment[]\n pipelines Pipeline[]\n\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n username String\n login String\n email String\n avatar_url String?\n active Boolean @default(true)\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String @default(\"system\")\n updatedBy String @default(\"system\")\n}\n\nmodel Pipeline {\n id Int @id @default(autoincrement())\n name String\n description String?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n // Relations\n projectId Int?\n Project Project? @relation(fields: [projectId], references: [id])\n steps Step[]\n}\n\nmodel Step {\n id Int @id @default(autoincrement())\n name String\n order Int\n script String // 执行的脚本命令\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n pipelineId Int\n pipeline Pipeline @relation(fields: [pipelineId], references: [id])\n}\n\nmodel Deployment {\n id Int @id @default(autoincrement())\n branch String\n env String?\n status String // pending, running, success, failed, cancelled\n commitHash String?\n commitMessage String?\n buildLog String?\n sparseCheckoutPaths String? // 稀疏检出路径,用于monorepo项目\n startedAt DateTime @default(now())\n finishedAt DateTime?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n projectId Int\n Project Project? @relation(fields: [projectId], references: [id])\n pipelineId Int\n}\n",
|
inlineSchema:
|
||||||
"runtimeDataModel": {
|
'// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = "prisma-client"\n output = "../generated"\n}\n\ndatasource db {\n provider = "sqlite"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n projectDir String @unique // 项目工作目录路径(必填)\n envPresets String? // 环境预设配置(JSON格式)\n // Relations\n deployments Deployment[]\n pipelines Pipeline[]\n\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n username String\n login String\n email String\n avatar_url String?\n active Boolean @default(true)\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String @default("system")\n updatedBy String @default("system")\n}\n\nmodel Pipeline {\n id Int @id @default(autoincrement())\n name String\n description String?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n // Relations\n projectId Int?\n Project Project? @relation(fields: [projectId], references: [id])\n steps Step[]\n}\n\nmodel Step {\n id Int @id @default(autoincrement())\n name String\n order Int\n script String // 执行的脚本命令\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n pipelineId Int\n pipeline Pipeline @relation(fields: [pipelineId], references: [id])\n}\n\nmodel Deployment {\n id Int @id @default(autoincrement())\n branch String\n envVars String? // 环境变量(JSON格式),统一存储所有配置\n status String // pending, running, success, failed, cancelled\n commitHash String?\n commitMessage String?\n buildLog String?\n sparseCheckoutPaths String? // 稀疏检出路径,用于monorepo项目\n startedAt DateTime @default(now())\n finishedAt DateTime?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n projectId Int\n Project Project? @relation(fields: [projectId], references: [id])\n pipelineId Int\n}\n',
|
||||||
"models": {},
|
runtimeDataModel: {
|
||||||
"enums": {},
|
models: {},
|
||||||
"types": {}
|
enums: {},
|
||||||
}
|
types: {},
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|
||||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"Project\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"repository\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectDir\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deployments\",\"kind\":\"object\",\"type\":\"Deployment\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelines\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToProject\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"avatar_url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Pipeline\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"PipelineToProject\"},{\"name\":\"steps\",\"kind\":\"object\",\"type\":\"Step\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Step\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"order\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"script\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"pipeline\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Deployment\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"branch\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"env\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitMessage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"buildLog\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sparseCheckoutPaths\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"startedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"finishedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
config.runtimeDataModel = JSON.parse(
|
||||||
|
'{"models":{"Project":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"name","kind":"scalar","type":"String"},{"name":"description","kind":"scalar","type":"String"},{"name":"repository","kind":"scalar","type":"String"},{"name":"projectDir","kind":"scalar","type":"String"},{"name":"envPresets","kind":"scalar","type":"String"},{"name":"deployments","kind":"object","type":"Deployment","relationName":"DeploymentToProject"},{"name":"pipelines","kind":"object","type":"Pipeline","relationName":"PipelineToProject"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"}],"dbName":null},"User":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"username","kind":"scalar","type":"String"},{"name":"login","kind":"scalar","type":"String"},{"name":"email","kind":"scalar","type":"String"},{"name":"avatar_url","kind":"scalar","type":"String"},{"name":"active","kind":"scalar","type":"Boolean"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"}],"dbName":null},"Pipeline":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"name","kind":"scalar","type":"String"},{"name":"description","kind":"scalar","type":"String"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"},{"name":"projectId","kind":"scalar","type":"Int"},{"name":"Project","kind":"object","type":"Project","relationName":"PipelineToProject"},{"name":"steps","kind":"object","type":"Step","relationName":"PipelineToStep"}],"dbName":null},"Step":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"name","kind":"scalar","type":"String"},{"name":"order","kind":"scalar","type":"Int"},{"name":"script","kind":"scalar","type":"String"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"},{"name":"pipelineId","kind":"scalar","type":"Int"},{"name":"pipeline","kind":"object","type":"Pipeline","relationName":"PipelineToStep"}],"dbName":null},"Deployment":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"branch","kind":"scalar","type":"String"},{"name":"envVars","kind":"scalar","type":"String"},{"name":"status","kind":"scalar","type":"String"},{"name":"commitHash","kind":"scalar","type":"String"},{"name":"commitMessage","kind":"scalar","type":"String"},{"name":"buildLog","kind":"scalar","type":"String"},{"name":"sparseCheckoutPaths","kind":"scalar","type":"String"},{"name":"startedAt","kind":"scalar","type":"DateTime"},{"name":"finishedAt","kind":"scalar","type":"DateTime"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"},{"name":"projectId","kind":"scalar","type":"Int"},{"name":"Project","kind":"object","type":"Project","relationName":"DeploymentToProject"},{"name":"pipelineId","kind":"scalar","type":"Int"}],"dbName":null}},"enums":{},"types":{}}',
|
||||||
|
);
|
||||||
|
|
||||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
async function decodeBase64AsWasm(
|
||||||
const { Buffer } = await import('node:buffer')
|
wasmBase64: string,
|
||||||
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
): Promise<WebAssembly.Module> {
|
||||||
return new WebAssembly.Module(wasmArray)
|
const { Buffer } = await import('node:buffer');
|
||||||
|
const wasmArray = Buffer.from(wasmBase64, 'base64');
|
||||||
|
return new WebAssembly.Module(wasmArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.compilerWasm = {
|
config.compilerWasm = {
|
||||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.sqlite.mjs"),
|
getRuntime: async () =>
|
||||||
|
await import('@prisma/client/runtime/query_compiler_bg.sqlite.mjs'),
|
||||||
|
|
||||||
getQueryCompilerWasmModule: async () => {
|
getQueryCompilerWasmModule: async () => {
|
||||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs")
|
const { wasm } = await import(
|
||||||
return await decodeBase64AsWasm(wasm)
|
'@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs'
|
||||||
}
|
);
|
||||||
}
|
return await decodeBase64AsWasm(wasm);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||||
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
'log' extends keyof ClientOptions
|
||||||
|
? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
|
||||||
|
? Prisma.GetEvents<ClientOptions['log']>
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
export interface PrismaClientConstructor {
|
export interface PrismaClientConstructor {
|
||||||
/**
|
/**
|
||||||
* ## Prisma Client
|
* ## Prisma Client
|
||||||
*
|
*
|
||||||
* Type-safe database client for TypeScript
|
* Type-safe database client for TypeScript
|
||||||
@@ -68,9 +76,16 @@ export interface PrismaClientConstructor {
|
|||||||
new <
|
new <
|
||||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends {
|
||||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
omit: infer U;
|
||||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
}
|
||||||
|
? U
|
||||||
|
: Prisma.PrismaClientOptions['omit'],
|
||||||
|
ExtArgs extends
|
||||||
|
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||||
|
>(
|
||||||
|
options: Prisma.Subset<Options, Prisma.PrismaClientOptions>,
|
||||||
|
): PrismaClient<LogOpts, OmitOpts, ExtArgs>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,11 +105,17 @@ export interface PrismaClientConstructor {
|
|||||||
export interface PrismaClient<
|
export interface PrismaClient<
|
||||||
in LogOpts extends Prisma.LogLevel = never,
|
in LogOpts extends Prisma.LogLevel = never,
|
||||||
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
in out ExtArgs extends
|
||||||
|
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||||
> {
|
> {
|
||||||
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] };
|
||||||
|
|
||||||
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
$on<V extends LogOpts>(
|
||||||
|
eventType: V,
|
||||||
|
callback: (
|
||||||
|
event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent,
|
||||||
|
) => void,
|
||||||
|
): PrismaClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect with the database
|
* Connect with the database
|
||||||
@@ -106,7 +127,7 @@ export interface PrismaClient<
|
|||||||
*/
|
*/
|
||||||
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a prepared raw query and returns the number of affected rows.
|
* Executes a prepared raw query and returns the number of affected rows.
|
||||||
* @example
|
* @example
|
||||||
* ```
|
* ```
|
||||||
@@ -115,7 +136,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
$executeRaw<T = unknown>(
|
||||||
|
query: TemplateStringsArray | Prisma.Sql,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a raw query and returns the number of affected rows.
|
* Executes a raw query and returns the number of affected rows.
|
||||||
@@ -127,7 +151,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
$executeRawUnsafe<T = unknown>(
|
||||||
|
query: string,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a prepared raw query and returns the `SELECT` data.
|
* Performs a prepared raw query and returns the `SELECT` data.
|
||||||
@@ -138,7 +165,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
$queryRaw<T = unknown>(
|
||||||
|
query: TemplateStringsArray | Prisma.Sql,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a raw query and returns the `SELECT` data.
|
* Performs a raw query and returns the `SELECT` data.
|
||||||
@@ -150,8 +180,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
$queryRawUnsafe<T = unknown>(
|
||||||
|
query: string,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||||
@@ -166,65 +198,85 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||||
*/
|
*/
|
||||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
$transaction<P extends Prisma.PrismaPromise<any>[]>(
|
||||||
|
arg: [...P],
|
||||||
|
options?: { isolationLevel?: Prisma.TransactionIsolationLevel },
|
||||||
|
): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>;
|
||||||
|
|
||||||
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
$transaction<R>(
|
||||||
|
fn: (
|
||||||
|
prisma: Omit<PrismaClient, runtime.ITXClientDenyList>,
|
||||||
|
) => runtime.Types.Utils.JsPromise<R>,
|
||||||
|
options?: {
|
||||||
|
maxWait?: number;
|
||||||
|
timeout?: number;
|
||||||
|
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||||
|
},
|
||||||
|
): runtime.Types.Utils.JsPromise<R>;
|
||||||
|
|
||||||
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
$extends: runtime.Types.Extensions.ExtendsHook<
|
||||||
extArgs: ExtArgs
|
'extends',
|
||||||
}>>
|
Prisma.TypeMapCb<OmitOpts>,
|
||||||
|
ExtArgs,
|
||||||
|
runtime.Types.Utils.Call<
|
||||||
|
Prisma.TypeMapCb<OmitOpts>,
|
||||||
|
{
|
||||||
|
extArgs: ExtArgs;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `prisma.project`: Exposes CRUD operations for the **Project** model.
|
* `prisma.project`: Exposes CRUD operations for the **Project** model.
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Fetch zero or more Projects
|
* // Fetch zero or more Projects
|
||||||
* const projects = await prisma.project.findMany()
|
* const projects = await prisma.project.findMany()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
get project(): Prisma.ProjectDelegate<ExtArgs, { omit: OmitOpts }>;
|
get project(): Prisma.ProjectDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `prisma.user`: Exposes CRUD operations for the **User** model.
|
* `prisma.user`: Exposes CRUD operations for the **User** model.
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Fetch zero or more Users
|
* // Fetch zero or more Users
|
||||||
* const users = await prisma.user.findMany()
|
* const users = await prisma.user.findMany()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
|
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `prisma.pipeline`: Exposes CRUD operations for the **Pipeline** model.
|
* `prisma.pipeline`: Exposes CRUD operations for the **Pipeline** model.
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Fetch zero or more Pipelines
|
* // Fetch zero or more Pipelines
|
||||||
* const pipelines = await prisma.pipeline.findMany()
|
* const pipelines = await prisma.pipeline.findMany()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
get pipeline(): Prisma.PipelineDelegate<ExtArgs, { omit: OmitOpts }>;
|
get pipeline(): Prisma.PipelineDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `prisma.step`: Exposes CRUD operations for the **Step** model.
|
* `prisma.step`: Exposes CRUD operations for the **Step** model.
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Fetch zero or more Steps
|
* // Fetch zero or more Steps
|
||||||
* const steps = await prisma.step.findMany()
|
* const steps = await prisma.step.findMany()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
get step(): Prisma.StepDelegate<ExtArgs, { omit: OmitOpts }>;
|
get step(): Prisma.StepDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `prisma.deployment`: Exposes CRUD operations for the **Deployment** model.
|
* `prisma.deployment`: Exposes CRUD operations for the **Deployment** model.
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Fetch zero or more Deployments
|
* // Fetch zero or more Deployments
|
||||||
* const deployments = await prisma.deployment.findMany()
|
* const deployments = await prisma.deployment.findMany()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
get deployment(): Prisma.DeploymentDelegate<ExtArgs, { omit: OmitOpts }>;
|
get deployment(): Prisma.DeploymentDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPrismaClientClass(): PrismaClientConstructor {
|
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||||
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
|
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -15,61 +14,65 @@
|
|||||||
* model files in the `model` directory!
|
* model files in the `model` directory!
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
import * as runtime from '@prisma/client/runtime/index-browser';
|
||||||
|
|
||||||
export type * from '../models.ts'
|
export type * from '../models.ts';
|
||||||
export type * from './prismaNamespace.ts'
|
export type * from './prismaNamespace.ts';
|
||||||
|
|
||||||
export const Decimal = runtime.Decimal
|
|
||||||
|
|
||||||
|
export const Decimal = runtime.Decimal;
|
||||||
|
|
||||||
export const NullTypes = {
|
export const NullTypes = {
|
||||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
DbNull: runtime.NullTypes.DbNull as new (
|
||||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
secret: never,
|
||||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
) => typeof runtime.DbNull,
|
||||||
}
|
JsonNull: runtime.NullTypes.JsonNull as new (
|
||||||
|
secret: never,
|
||||||
|
) => typeof runtime.JsonNull,
|
||||||
|
AnyNull: runtime.NullTypes.AnyNull as new (
|
||||||
|
secret: never,
|
||||||
|
) => typeof runtime.AnyNull,
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const DbNull = runtime.DbNull
|
export const DbNull = runtime.DbNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const JsonNull = runtime.JsonNull
|
export const JsonNull = runtime.JsonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const AnyNull = runtime.AnyNull
|
export const AnyNull = runtime.AnyNull;
|
||||||
|
|
||||||
|
|
||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
Project: 'Project',
|
Project: 'Project',
|
||||||
User: 'User',
|
User: 'User',
|
||||||
Pipeline: 'Pipeline',
|
Pipeline: 'Pipeline',
|
||||||
Step: 'Step',
|
Step: 'Step',
|
||||||
Deployment: 'Deployment'
|
Deployment: 'Deployment',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enums
|
* Enums
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const TransactionIsolationLevel = {
|
export const TransactionIsolationLevel = {
|
||||||
Serializable: 'Serializable'
|
Serializable: 'Serializable',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel =
|
||||||
|
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel];
|
||||||
|
|
||||||
export const ProjectScalarFieldEnum = {
|
export const ProjectScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -77,15 +80,16 @@ export const ProjectScalarFieldEnum = {
|
|||||||
description: 'description',
|
description: 'description',
|
||||||
repository: 'repository',
|
repository: 'repository',
|
||||||
projectDir: 'projectDir',
|
projectDir: 'projectDir',
|
||||||
|
envPresets: 'envPresets',
|
||||||
valid: 'valid',
|
valid: 'valid',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy'
|
updatedBy: 'updatedBy',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type ProjectScalarFieldEnum = (typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type ProjectScalarFieldEnum =
|
||||||
|
(typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum];
|
||||||
|
|
||||||
export const UserScalarFieldEnum = {
|
export const UserScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -98,11 +102,11 @@ export const UserScalarFieldEnum = {
|
|||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy'
|
updatedBy: 'updatedBy',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type UserScalarFieldEnum =
|
||||||
|
(typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum];
|
||||||
|
|
||||||
export const PipelineScalarFieldEnum = {
|
export const PipelineScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -113,11 +117,11 @@ export const PipelineScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy',
|
updatedBy: 'updatedBy',
|
||||||
projectId: 'projectId'
|
projectId: 'projectId',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type PipelineScalarFieldEnum = (typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type PipelineScalarFieldEnum =
|
||||||
|
(typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum];
|
||||||
|
|
||||||
export const StepScalarFieldEnum = {
|
export const StepScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -129,16 +133,16 @@ export const StepScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy',
|
updatedBy: 'updatedBy',
|
||||||
pipelineId: 'pipelineId'
|
pipelineId: 'pipelineId',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type StepScalarFieldEnum = (typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type StepScalarFieldEnum =
|
||||||
|
(typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum];
|
||||||
|
|
||||||
export const DeploymentScalarFieldEnum = {
|
export const DeploymentScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
branch: 'branch',
|
branch: 'branch',
|
||||||
env: 'env',
|
envVars: 'envVars',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
commitHash: 'commitHash',
|
commitHash: 'commitHash',
|
||||||
commitMessage: 'commitMessage',
|
commitMessage: 'commitMessage',
|
||||||
@@ -152,24 +156,22 @@ export const DeploymentScalarFieldEnum = {
|
|||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy',
|
updatedBy: 'updatedBy',
|
||||||
projectId: 'projectId',
|
projectId: 'projectId',
|
||||||
pipelineId: 'pipelineId'
|
pipelineId: 'pipelineId',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type DeploymentScalarFieldEnum = (typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type DeploymentScalarFieldEnum =
|
||||||
|
(typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum];
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
|
||||||
|
|
||||||
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder];
|
||||||
|
|
||||||
export const NullsOrder = {
|
export const NullsOrder = {
|
||||||
first: 'first',
|
first: 'first',
|
||||||
last: 'last'
|
last: 'last',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
|
||||||
|
|
||||||
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder];
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
export type * from './commonInputTypes.ts';
|
||||||
|
export type * from './models/Deployment.ts';
|
||||||
|
export type * from './models/Pipeline.ts';
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -8,9 +10,6 @@
|
|||||||
*
|
*
|
||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
export type * from './models/Project.ts'
|
export type * from './models/Project.ts';
|
||||||
export type * from './models/User.ts'
|
export type * from './models/Step.ts';
|
||||||
export type * from './models/Pipeline.ts'
|
export type * from './models/User.ts';
|
||||||
export type * from './models/Step.ts'
|
|
||||||
export type * from './models/Deployment.ts'
|
|
||||||
export type * from './commonInputTypes.ts'
|
|
||||||
|
|||||||
+1523
-1006
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1240
-813
File diff suppressed because it is too large
Load Diff
+1113
-754
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,9 @@
|
|||||||
* 封装 Git 操作:克隆、更新、分支切换等
|
* 封装 Git 操作:克隆、更新、分支切换等
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { $ } from 'zx';
|
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { $ } from 'zx';
|
||||||
import { log } from './logger';
|
import { log } from './logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+26
-25
@@ -38,26 +38,23 @@ class Gitea {
|
|||||||
clientId: process.env.GITEA_CLIENT_ID!,
|
clientId: process.env.GITEA_CLIENT_ID!,
|
||||||
clientSecret: process.env.GITEA_CLIENT_SECRET!,
|
clientSecret: process.env.GITEA_CLIENT_SECRET!,
|
||||||
redirectUri: process.env.GITEA_REDIRECT_URI!,
|
redirectUri: process.env.GITEA_REDIRECT_URI!,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToken(code: string) {
|
async getToken(code: string) {
|
||||||
const { giteaUrl, clientId, clientSecret, redirectUri } = this.config;
|
const { giteaUrl, clientId, clientSecret, redirectUri } = this.config;
|
||||||
console.log('this.config', this.config);
|
console.log('this.config', this.config);
|
||||||
const response = await fetch(
|
const response = await fetch(`${giteaUrl}/login/oauth/access_token`, {
|
||||||
`${giteaUrl}/login/oauth/access_token`,
|
method: 'POST',
|
||||||
{
|
headers: this.getHeaders(),
|
||||||
method: 'POST',
|
body: JSON.stringify({
|
||||||
headers: this.getHeaders(),
|
client_id: clientId,
|
||||||
body: JSON.stringify({
|
client_secret: clientSecret,
|
||||||
client_id: clientId,
|
code,
|
||||||
client_secret: clientSecret,
|
grant_type: 'authorization_code',
|
||||||
code,
|
redirect_uri: redirectUri,
|
||||||
grant_type: 'authorization_code',
|
}),
|
||||||
redirect_uri: redirectUri,
|
});
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.log(await response.json());
|
console.log(await response.json());
|
||||||
throw new Error(`Fetch failed: ${response.status}`);
|
throw new Error(`Fetch failed: ${response.status}`);
|
||||||
@@ -108,19 +105,23 @@ class Gitea {
|
|||||||
* @param accessToken 访问令牌
|
* @param accessToken 访问令牌
|
||||||
* @param sha 分支名称或提交SHA
|
* @param sha 分支名称或提交SHA
|
||||||
*/
|
*/
|
||||||
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) {
|
async getCommits(
|
||||||
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
accessToken: string,
|
||||||
|
sha?: string,
|
||||||
|
) {
|
||||||
|
const url = new URL(
|
||||||
|
`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`,
|
||||||
|
);
|
||||||
if (sha) {
|
if (sha) {
|
||||||
url.searchParams.append('sha', sha);
|
url.searchParams.append('sha', sha);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(url.toString(), {
|
||||||
url.toString(),
|
method: 'GET',
|
||||||
{
|
headers: this.getHeaders(accessToken),
|
||||||
method: 'GET',
|
});
|
||||||
headers: this.getHeaders(accessToken),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Fetch failed: ${response.status}`);
|
throw new Error(`Fetch failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ class Gitea {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers['Authorization'] = `token ${accessToken}`;
|
headers.Authorization = `token ${accessToken}`;
|
||||||
}
|
}
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,6 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
|||||||
name: 'Git Clone Pipeline',
|
name: 'Git Clone Pipeline',
|
||||||
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
||||||
steps: [
|
steps: [
|
||||||
{
|
|
||||||
name: 'Clone Repository',
|
|
||||||
order: 0,
|
|
||||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Install Dependencies',
|
name: 'Install Dependencies',
|
||||||
order: 1,
|
order: 1,
|
||||||
@@ -36,51 +31,21 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
|||||||
name: 'Build Project',
|
name: 'Build Project',
|
||||||
order: 3,
|
order: 3,
|
||||||
script: '# 构建项目\nnpm run build',
|
script: '# 构建项目\nnpm run build',
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Sparse Checkout Pipeline',
|
|
||||||
description: '稀疏检出流水线,适用于monorepo项目,只获取指定目录的代码',
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
name: 'Sparse Checkout Repository',
|
|
||||||
order: 0,
|
|
||||||
script: '# 进行稀疏检出指定目录的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit config core.sparseCheckout true\necho "$SPARSE_CHECKOUT_PATHS" > .git/info/sparse-checkout\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
name: 'Install Dependencies',
|
|
||||||
order: 1,
|
|
||||||
script: '# 安装项目依赖\nnpm install',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Run Tests',
|
|
||||||
order: 2,
|
|
||||||
script: '# 运行测试\nnpm test',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Build Project',
|
|
||||||
order: 3,
|
|
||||||
script: '# 构建项目\nnpm run build',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Simple Deploy Pipeline',
|
name: 'Simple Deploy Pipeline',
|
||||||
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
||||||
steps: [
|
steps: [
|
||||||
{
|
|
||||||
name: 'Clone Repository',
|
|
||||||
order: 0,
|
|
||||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Build and Deploy',
|
name: 'Build and Deploy',
|
||||||
order: 1,
|
order: 1,
|
||||||
script: '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
script:
|
||||||
}
|
'# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
||||||
]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,10 +59,10 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
const existingTemplates = await prisma.pipeline.findMany({
|
const existingTemplates = await prisma.pipeline.findMany({
|
||||||
where: {
|
where: {
|
||||||
name: {
|
name: {
|
||||||
in: DEFAULT_PIPELINE_TEMPLATES.map(template => template.name)
|
in: DEFAULT_PIPELINE_TEMPLATES.map((template) => template.name),
|
||||||
},
|
},
|
||||||
valid: 1
|
valid: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果没有现有的模板,则创建默认模板
|
// 如果没有现有的模板,则创建默认模板
|
||||||
@@ -113,8 +78,8 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
projectId: null // 模板不属于任何特定项目
|
projectId: null, // 模板不属于任何特定项目
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建模板步骤
|
// 创建模板步骤
|
||||||
@@ -127,8 +92,8 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
pipelineId: pipeline.id,
|
pipelineId: pipeline.id,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1
|
valid: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,25 +113,27 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* 获取所有可用的流水线模板
|
* 获取所有可用的流水线模板
|
||||||
*/
|
*/
|
||||||
export async function getAvailableTemplates(): Promise<Array<{id: number, name: string, description: string}>> {
|
export async function getAvailableTemplates(): Promise<
|
||||||
|
Array<{ id: number; name: string; description: string }>
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const templates = await prisma.pipeline.findMany({
|
const templates = await prisma.pipeline.findMany({
|
||||||
where: {
|
where: {
|
||||||
projectId: null, // 模板流水线没有关联的项目
|
projectId: null, // 模板流水线没有关联的项目
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
description: true
|
description: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理可能为null的description字段
|
// 处理可能为null的description字段
|
||||||
return templates.map(template => ({
|
return templates.map((template) => ({
|
||||||
id: template.id,
|
id: template.id,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
description: template.description || ''
|
description: template.description || '',
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get pipeline templates:', error);
|
console.error('Failed to get pipeline templates:', error);
|
||||||
@@ -185,7 +152,7 @@ export async function createPipelineFromTemplate(
|
|||||||
templateId: number,
|
templateId: number,
|
||||||
projectId: number,
|
projectId: number,
|
||||||
pipelineName: string,
|
pipelineName: string,
|
||||||
pipelineDescription: string
|
pipelineDescription: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
// 获取模板流水线及其步骤
|
// 获取模板流水线及其步骤
|
||||||
@@ -193,18 +160,18 @@ export async function createPipelineFromTemplate(
|
|||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
projectId: null, // 确保是模板流水线
|
projectId: null, // 确保是模板流水线
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
steps: {
|
steps: {
|
||||||
where: {
|
where: {
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
order: 'asc'
|
order: 'asc',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!templatePipeline) {
|
if (!templatePipeline) {
|
||||||
@@ -219,8 +186,8 @@ export async function createPipelineFromTemplate(
|
|||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1
|
valid: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 复制模板步骤到新流水线
|
// 复制模板步骤到新流水线
|
||||||
@@ -233,12 +200,14 @@ export async function createPipelineFromTemplate(
|
|||||||
pipelineId: newPipeline.id,
|
pipelineId: newPipeline.id,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1
|
valid: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Created pipeline from template ${templateId}: ${newPipeline.name}`);
|
console.log(
|
||||||
|
`Created pipeline from template ${templateId}: ${newPipeline.name}`,
|
||||||
|
);
|
||||||
return newPipeline.id;
|
return newPipeline.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create pipeline from template:', error);
|
console.error('Failed to create pipeline from template:', error);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type Koa from 'koa';
|
|
||||||
import KoaRouter from '@koa/router';
|
import KoaRouter from '@koa/router';
|
||||||
import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts';
|
import type Koa from 'koa';
|
||||||
|
import {
|
||||||
|
getControllerPrefix,
|
||||||
|
getRouteMetadata,
|
||||||
|
type RouteMetadata,
|
||||||
|
} from '../decorators/route.ts';
|
||||||
import { createSuccessResponse } from '../middlewares/exception.ts';
|
import { createSuccessResponse } from '../middlewares/exception.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +37,7 @@ export class RouteScanner {
|
|||||||
* 注册多个控制器类
|
* 注册多个控制器类
|
||||||
*/
|
*/
|
||||||
registerControllers(controllers: ControllerClass[]): void {
|
registerControllers(controllers: ControllerClass[]): void {
|
||||||
controllers.forEach(controller => this.registerController(controller));
|
controllers.forEach((controller) => this.registerController(controller));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,9 +54,12 @@ export class RouteScanner {
|
|||||||
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
|
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
|
||||||
|
|
||||||
// 注册每个路由
|
// 注册每个路由
|
||||||
routes.forEach(route => {
|
routes.forEach((route) => {
|
||||||
const fullPath = this.buildFullPath(controllerPrefix, route.path);
|
const fullPath = this.buildFullPath(controllerPrefix, route.path);
|
||||||
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey);
|
const handler = this.wrapControllerMethod(
|
||||||
|
controllerInstance,
|
||||||
|
route.propertyKey,
|
||||||
|
);
|
||||||
|
|
||||||
// 根据HTTP方法注册路由
|
// 根据HTTP方法注册路由
|
||||||
switch (route.method) {
|
switch (route.method) {
|
||||||
@@ -87,10 +94,10 @@ export class RouteScanner {
|
|||||||
|
|
||||||
let fullPath = '';
|
let fullPath = '';
|
||||||
if (cleanControllerPrefix) {
|
if (cleanControllerPrefix) {
|
||||||
fullPath += '/' + cleanControllerPrefix;
|
fullPath += `/${cleanControllerPrefix}`;
|
||||||
}
|
}
|
||||||
if (cleanRoutePath) {
|
if (cleanRoutePath) {
|
||||||
fullPath += '/' + cleanRoutePath;
|
fullPath += `/${cleanRoutePath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果路径为空,返回根路径
|
// 如果路径为空,返回根路径
|
||||||
@@ -105,11 +112,11 @@ export class RouteScanner {
|
|||||||
// 调用控制器方法
|
// 调用控制器方法
|
||||||
const method = instance[methodName];
|
const method = instance[methodName];
|
||||||
if (typeof method !== 'function') {
|
if (typeof method !== 'function') {
|
||||||
ctx.throw(401, 'Not Found')
|
ctx.throw(401, 'Not Found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定this并调用方法
|
// 绑定this并调用方法
|
||||||
const result = await method.call(instance, ctx, next) ?? null;
|
const result = (await method.call(instance, ctx, next)) ?? null;
|
||||||
|
|
||||||
ctx.body = createSuccessResponse(result);
|
ctx.body = createSuccessResponse(result);
|
||||||
};
|
};
|
||||||
@@ -133,19 +140,29 @@ export class RouteScanner {
|
|||||||
/**
|
/**
|
||||||
* 获取已注册的路由信息(用于调试)
|
* 获取已注册的路由信息(用于调试)
|
||||||
*/
|
*/
|
||||||
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> {
|
getRegisteredRoutes(): Array<{
|
||||||
const routes: Array<{ method: string; path: string; controller: string; action: string }> = [];
|
method: string;
|
||||||
|
path: string;
|
||||||
|
controller: string;
|
||||||
|
action: string;
|
||||||
|
}> {
|
||||||
|
const routes: Array<{
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
controller: string;
|
||||||
|
action: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
this.controllers.forEach(ControllerClass => {
|
this.controllers.forEach((ControllerClass) => {
|
||||||
const controllerPrefix = getControllerPrefix(ControllerClass);
|
const controllerPrefix = getControllerPrefix(ControllerClass);
|
||||||
const routeMetadata = getRouteMetadata(ControllerClass);
|
const routeMetadata = getRouteMetadata(ControllerClass);
|
||||||
|
|
||||||
routeMetadata.forEach(route => {
|
routeMetadata.forEach((route) => {
|
||||||
routes.push({
|
routes.push({
|
||||||
method: route.method,
|
method: route.method,
|
||||||
path: this.buildFullPath(controllerPrefix, route.path),
|
path: this.buildFullPath(controllerPrefix, route.path),
|
||||||
controller: ControllerClass.name,
|
controller: ControllerClass.name,
|
||||||
action: route.propertyKey
|
action: route.propertyKey,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import bodyParser from 'koa-bodyparser';
|
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
|
import bodyParser from 'koa-bodyparser';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { Middleware } from './types.ts';
|
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一响应体结构
|
* 统一响应体结构
|
||||||
@@ -58,15 +58,26 @@ export class Exception implements Middleware {
|
|||||||
const errorMessage = firstError?.message || '参数验证失败';
|
const errorMessage = firstError?.message || '参数验证失败';
|
||||||
const fieldPath = firstError?.path?.join('.') || 'unknown';
|
const fieldPath = firstError?.path?.join('.') || 'unknown';
|
||||||
|
|
||||||
log.info('Exception', 'Zod validation failed: %s at %s', errorMessage, fieldPath);
|
log.info(
|
||||||
this.sendResponse(ctx, 1003, errorMessage, {
|
'Exception',
|
||||||
field: fieldPath,
|
'Zod validation failed: %s at %s',
|
||||||
validationErrors: error.issues.map(issue => ({
|
errorMessage,
|
||||||
field: issue.path.join('.'),
|
fieldPath,
|
||||||
message: issue.message,
|
);
|
||||||
code: issue.code,
|
this.sendResponse(
|
||||||
}))
|
ctx,
|
||||||
}, 400);
|
1003,
|
||||||
|
errorMessage,
|
||||||
|
{
|
||||||
|
field: fieldPath,
|
||||||
|
validationErrors: error.issues.map((issue) => ({
|
||||||
|
field: issue.path.join('.'),
|
||||||
|
message: issue.message,
|
||||||
|
code: issue.code,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
);
|
||||||
} else if (error instanceof BusinessError) {
|
} else if (error instanceof BusinessError) {
|
||||||
// 业务异常
|
// 业务异常
|
||||||
this.sendResponse(ctx, error.code, error.message, null, error.httpStatus);
|
this.sendResponse(ctx, error.code, error.message, null, error.httpStatus);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Router } from './router.ts';
|
|
||||||
import { Exception } from './exception.ts';
|
|
||||||
import { BodyParser } from './body-parser.ts';
|
|
||||||
import { Session } from './session.ts';
|
|
||||||
import { CORS } from './cors.ts';
|
|
||||||
import { HttpLogger } from './logger.ts';
|
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import { Authorization } from './authorization.ts';
|
import { Authorization } from './authorization.ts';
|
||||||
|
import { BodyParser } from './body-parser.ts';
|
||||||
|
import { CORS } from './cors.ts';
|
||||||
|
import { Exception } from './exception.ts';
|
||||||
|
import { HttpLogger } from './logger.ts';
|
||||||
|
import { Router } from './router.ts';
|
||||||
|
import { Session } from './session.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化中间件
|
* 初始化中间件
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Koa, { type Context } from 'koa';
|
import type Koa from 'koa';
|
||||||
|
import type { Context } from 'koa';
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ export class HttpLogger implements Middleware {
|
|||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
await next();
|
await next();
|
||||||
const ms = Date.now() - start;
|
const ms = Date.now() - start;
|
||||||
log.info('HTTP', `${ctx.method} ${ctx.url} - ${ms}ms`)
|
log.info('HTTP', `${ctx.method} ${ctx.url} - ${ms}ms`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import KoaRouter from '@koa/router';
|
import KoaRouter from '@koa/router';
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import type { Middleware } from './types.ts';
|
|
||||||
import { RouteScanner } from '../libs/route-scanner.ts';
|
|
||||||
import {
|
import {
|
||||||
ProjectController,
|
|
||||||
UserController,
|
|
||||||
AuthController,
|
AuthController,
|
||||||
DeploymentController,
|
DeploymentController,
|
||||||
|
GitController,
|
||||||
PipelineController,
|
PipelineController,
|
||||||
|
ProjectController,
|
||||||
StepController,
|
StepController,
|
||||||
GitController
|
UserController,
|
||||||
} from '../controllers/index.ts';
|
} from '../controllers/index.ts';
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
|
import { RouteScanner } from '../libs/route-scanner.ts';
|
||||||
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
export class Router implements Middleware {
|
export class Router implements Middleware {
|
||||||
private router: KoaRouter;
|
private router: KoaRouter;
|
||||||
@@ -45,7 +45,7 @@ export class Router implements Middleware {
|
|||||||
DeploymentController,
|
DeploymentController,
|
||||||
PipelineController,
|
PipelineController,
|
||||||
StepController,
|
StepController,
|
||||||
GitController
|
GitController,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 输出注册的路由信息
|
// 输出注册的路由信息
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import session from 'koa-session';
|
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
|
import session from 'koa-session';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
export class Session implements Middleware {
|
export class Session implements Middleware {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
|
|
||||||
export abstract class Middleware {
|
export abstract class Middleware {
|
||||||
abstract apply(app: Koa, options?: unknown): void;
|
abstract apply(app: Koa, options?: unknown): void;
|
||||||
|
|||||||
Binary file not shown.
@@ -16,6 +16,7 @@ model Project {
|
|||||||
description String?
|
description String?
|
||||||
repository String
|
repository String
|
||||||
projectDir String @unique // 项目工作目录路径(必填)
|
projectDir String @unique // 项目工作目录路径(必填)
|
||||||
|
envPresets String? // 环境预设配置(JSON格式)
|
||||||
// Relations
|
// Relations
|
||||||
deployments Deployment[]
|
deployments Deployment[]
|
||||||
pipelines Pipeline[]
|
pipelines Pipeline[]
|
||||||
@@ -75,7 +76,7 @@ model Step {
|
|||||||
model Deployment {
|
model Deployment {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
branch String
|
branch String
|
||||||
env String?
|
envVars String? // 环境变量(JSON格式),统一存储所有配置
|
||||||
status String // pending, running, success, failed, cancelled
|
status String // pending, running, success, failed, cancelled
|
||||||
commitHash String?
|
commitHash String?
|
||||||
commitMessage String?
|
commitMessage String?
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { $ } from 'zx';
|
import { $ } from 'zx';
|
||||||
import { prisma } from '../libs/prisma.ts';
|
|
||||||
import type { Step } from '../generated/client.ts';
|
import type { Step } from '../generated/client.ts';
|
||||||
import { GitManager, WorkspaceDirStatus } from '../libs/git-manager.ts';
|
import { GitManager, WorkspaceDirStatus } from '../libs/git-manager.ts';
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
|
import { prisma } from '../libs/prisma.ts';
|
||||||
|
|
||||||
export class PipelineRunner {
|
export class PipelineRunner {
|
||||||
private readonly TAG = 'PipelineRunner';
|
private readonly TAG = 'PipelineRunner';
|
||||||
@@ -81,7 +81,7 @@ export class PipelineRunner {
|
|||||||
|
|
||||||
// 执行步骤
|
// 执行步骤
|
||||||
const stepLog = await this.executeStep(step, envVars);
|
const stepLog = await this.executeStep(step, envVars);
|
||||||
logs += stepLog + '\n';
|
logs += `${stepLog}\n`;
|
||||||
|
|
||||||
// 记录步骤执行完成的日志
|
// 记录步骤执行完成的日志
|
||||||
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
|
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
|
||||||
@@ -215,12 +215,18 @@ export class PipelineRunner {
|
|||||||
envVars.BRANCH_NAME = deployment.branch || '';
|
envVars.BRANCH_NAME = deployment.branch || '';
|
||||||
envVars.COMMIT_HASH = deployment.commitHash || '';
|
envVars.COMMIT_HASH = deployment.commitHash || '';
|
||||||
|
|
||||||
// 稀疏检出路径(如果有配置的话)
|
// 注入用户配置的环境变量
|
||||||
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || '';
|
if (deployment.envVars) {
|
||||||
|
try {
|
||||||
|
const userEnvVars = JSON.parse(deployment.envVars);
|
||||||
|
Object.assign(envVars, userEnvVars);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析环境变量失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 工作空间路径(使用配置的项目目录)
|
// 工作空间路径(使用配置的项目目录)
|
||||||
envVars.WORKSPACE = this.projectDir;
|
envVars.WORKSPACE = this.projectDir;
|
||||||
envVars.PROJECT_DIR = this.projectDir;
|
|
||||||
|
|
||||||
return envVars;
|
return envVars;
|
||||||
}
|
}
|
||||||
@@ -248,13 +254,11 @@ export class PipelineRunner {
|
|||||||
private addTimestampToLines(content: string, isError = false): string {
|
private addTimestampToLines(content: string, isError = false): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
return (
|
return `${content
|
||||||
content
|
.split('\n')
|
||||||
.split('\n')
|
.filter((line) => line.trim() !== '')
|
||||||
.filter((line) => line.trim() !== '')
|
.map((line) => this.addTimestamp(line, isError))
|
||||||
.map((line) => this.addTimestamp(line, isError))
|
.join('\n')}\n`;
|
||||||
.join('\n') + '\n'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,7 +274,7 @@ export class PipelineRunner {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 添加步骤开始执行的时间戳
|
// 添加步骤开始执行的时间戳
|
||||||
logs += this.addTimestamp(`执行脚本: ${step.script}`) + '\n';
|
logs += `${this.addTimestamp(`执行脚本: ${step.script}`)}\n`;
|
||||||
|
|
||||||
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
||||||
const script = step.script;
|
const script = step.script;
|
||||||
@@ -291,10 +295,10 @@ export class PipelineRunner {
|
|||||||
logs += this.addTimestampToLines(result.stderr, true);
|
logs += this.addTimestampToLines(result.stderr, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
logs += this.addTimestamp(`步骤执行完成`) + '\n';
|
logs += `${this.addTimestamp(`步骤执行完成`)}\n`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = `Error executing step "${step.name}": ${(error as Error).message}`;
|
const errorMsg = `Error executing step "${step.name}": ${(error as Error).message}`;
|
||||||
logs += this.addTimestamp(errorMsg, true) + '\n';
|
logs += `${this.addTimestamp(errorMsg, true)}\n`;
|
||||||
log.error(this.TAG, errorMsg);
|
log.error(this.TAG, errorMsg);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useEffect, useCallback } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export function useAsyncEffect(
|
export function useAsyncEffect(
|
||||||
effect: () => Promise<void | (() => void)>,
|
effect: () => Promise<undefined | (() => void)>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
) {
|
) {
|
||||||
const callback = useCallback(effect, [...deps]);
|
const callback = useCallback(effect, [...deps]);
|
||||||
@@ -11,7 +11,7 @@ export function useAsyncEffect(
|
|||||||
const cleanupPromise = callback();
|
const cleanupPromise = callback();
|
||||||
return () => {
|
return () => {
|
||||||
if (cleanupPromise instanceof Promise) {
|
if (cleanupPromise instanceof Promise) {
|
||||||
cleanupPromise.then(cleanup => cleanup && cleanup());
|
cleanupPromise.then((cleanup) => cleanup?.());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [callback]);
|
}, [callback]);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import App from '@pages/App';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router';
|
import { BrowserRouter } from 'react-router';
|
||||||
import { useGlobalStore } from './stores/global';
|
import { useGlobalStore } from './stores/global';
|
||||||
import '@arco-design/web-react/es/_util/react-19-adapter'
|
import '@arco-design/web-react/es/_util/react-19-adapter';
|
||||||
|
|
||||||
const rootEl = document.getElementById('root');
|
const rootEl = document.getElementById('root');
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import {
|
import { Form, Input, Message, Modal, Select } from '@arco-design/web-react';
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Message,
|
|
||||||
Modal,
|
|
||||||
Select,
|
|
||||||
} from '@arco-design/web-react';
|
|
||||||
import { formatDateTime } from '../../../../utils/time';
|
|
||||||
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import type { Branch, Commit, Pipeline } from '../../types';
|
import { formatDateTime } from '../../../../utils/time';
|
||||||
|
import type { Branch, Commit, Pipeline, Project } from '../../types';
|
||||||
import { detailService } from '../service';
|
import { detailService } from '../service';
|
||||||
|
|
||||||
|
interface EnvPreset {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'select' | 'multiselect' | 'input';
|
||||||
|
required?: boolean;
|
||||||
|
options?: Array<{ label: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
interface DeployModalProps {
|
interface DeployModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onOk: () => void;
|
onOk: () => void;
|
||||||
pipelines: Pipeline[];
|
pipelines: Pipeline[];
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
project?: Project | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeployModal({
|
function DeployModal({
|
||||||
@@ -26,12 +27,29 @@ function DeployModal({
|
|||||||
onOk,
|
onOk,
|
||||||
pipelines,
|
pipelines,
|
||||||
projectId,
|
projectId,
|
||||||
|
project,
|
||||||
}: DeployModalProps) {
|
}: DeployModalProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [branches, setBranches] = useState<Branch[]>([]);
|
const [branches, setBranches] = useState<Branch[]>([]);
|
||||||
const [commits, setCommits] = useState<Commit[]>([]);
|
const [commits, setCommits] = useState<Commit[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [branchLoading, setBranchLoading] = useState(false);
|
const [branchLoading, setBranchLoading] = useState(false);
|
||||||
|
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
|
||||||
|
|
||||||
|
// 解析项目环境预设
|
||||||
|
useEffect(() => {
|
||||||
|
if (project?.envPresets) {
|
||||||
|
try {
|
||||||
|
const presets = JSON.parse(project.envPresets);
|
||||||
|
setEnvPresets(presets);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析环境预设失败:', error);
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
}, [project]);
|
||||||
|
|
||||||
const fetchCommits = useCallback(
|
const fetchCommits = useCallback(
|
||||||
async (branch: string) => {
|
async (branch: string) => {
|
||||||
@@ -91,16 +109,27 @@ function DeployModal({
|
|||||||
try {
|
try {
|
||||||
const values = await form.validate();
|
const values = await form.validate();
|
||||||
const selectedCommit = commits.find((c) => c.sha === values.commitHash);
|
const selectedCommit = commits.find((c) => c.sha === values.commitHash);
|
||||||
const selectedPipeline = pipelines.find((p) => p.id === values.pipelineId);
|
const selectedPipeline = pipelines.find(
|
||||||
|
(p) => p.id === values.pipelineId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!selectedCommit || !selectedPipeline) {
|
if (!selectedCommit || !selectedPipeline) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化环境变量
|
// 收集所有环境变量(从预设项中提取)
|
||||||
const env = values.envVars
|
const envVars: Record<string, string> = {};
|
||||||
?.map((item: { key: string; value: string }) => `${item.key}=${item.value}`)
|
for (const preset of envPresets) {
|
||||||
.join('\n');
|
const value = values[preset.key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
// 对于 multiselect,将数组转为逗号分隔的字符串
|
||||||
|
if (preset.type === 'multiselect' && Array.isArray(value)) {
|
||||||
|
envVars[preset.key] = value.join(',');
|
||||||
|
} else {
|
||||||
|
envVars[preset.key] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await detailService.createDeployment({
|
await detailService.createDeployment({
|
||||||
projectId,
|
projectId,
|
||||||
@@ -108,8 +137,7 @@ function DeployModal({
|
|||||||
branch: values.branch,
|
branch: values.branch,
|
||||||
commitHash: selectedCommit.sha,
|
commitHash: selectedCommit.sha,
|
||||||
commitMessage: selectedCommit.commit.message,
|
commitMessage: selectedCommit.commit.message,
|
||||||
env: env,
|
envVars, // 提交所有环境变量
|
||||||
sparseCheckoutPaths: values.sparseCheckoutPaths,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Message.success('部署任务已创建');
|
Message.success('部署任务已创建');
|
||||||
@@ -128,126 +156,162 @@ function DeployModal({
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
focusLock={true}
|
focusLock={true}
|
||||||
|
style={{ width: 650 }}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item
|
{/* 基本参数 */}
|
||||||
label="选择流水线"
|
<div className="mb-4 pb-4 border-b border-gray-200">
|
||||||
field="pipelineId"
|
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||||
rules={[{ required: true, message: '请选择流水线' }]}
|
基本参数
|
||||||
>
|
</div>
|
||||||
<Select placeholder="请选择流水线">
|
|
||||||
{pipelines.map((pipeline) => (
|
|
||||||
<Select.Option key={pipeline.id} value={pipeline.id}>
|
|
||||||
{pipeline.name}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="选择分支"
|
label="选择流水线"
|
||||||
field="branch"
|
field="pipelineId"
|
||||||
rules={[{ required: true, message: '请选择分支' }]}
|
rules={[{ required: true, message: '请选择流水线' }]}
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder="请选择分支"
|
|
||||||
loading={branchLoading}
|
|
||||||
onChange={handleBranchChange}
|
|
||||||
>
|
>
|
||||||
{branches.map((branch) => (
|
<Select placeholder="请选择流水线">
|
||||||
<Select.Option key={branch.name} value={branch.name}>
|
{pipelines.map((pipeline) => (
|
||||||
{branch.name}
|
<Select.Option key={pipeline.id} value={pipeline.id}>
|
||||||
</Select.Option>
|
{pipeline.name}
|
||||||
))}
|
</Select.Option>
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="选择提交"
|
|
||||||
field="commitHash"
|
|
||||||
rules={[{ required: true, message: '请选择提交记录' }]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder="请选择提交记录"
|
|
||||||
loading={loading}
|
|
||||||
renderFormat={(option) => {
|
|
||||||
const commit = commits.find((c) => c.sha === option?.value);
|
|
||||||
return commit ? commit.sha.substring(0, 7) : '';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{commits.map((commit) => (
|
|
||||||
<Select.Option key={commit.sha} value={commit.sha}>
|
|
||||||
<div className="flex flex-col py-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="font-mono font-medium">
|
|
||||||
{commit.sha.substring(0, 7)}
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-500 text-xs">
|
|
||||||
{formatDateTime(commit.commit.author.date)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-600 text-sm truncate">
|
|
||||||
{commit.commit.message}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-400 text-xs">
|
|
||||||
{commit.commit.author.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="稀疏检出路径(用于monorepo项目,每行一个路径)"
|
|
||||||
field="sparseCheckoutPaths"
|
|
||||||
tooltip="在monorepo项目中,指定需要检出的目录路径,每行一个路径。留空则检出整个仓库。"
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder={`例如:\n/packages/frontend\n/packages/backend`}
|
|
||||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<div className="mb-2 font-medium text-gray-700">环境变量</div>
|
|
||||||
<Form.List field="envVars">
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<div>
|
|
||||||
{fields.map((item, index) => (
|
|
||||||
<div key={item.key} className="flex items-center gap-2 mb-2">
|
|
||||||
<Form.Item
|
|
||||||
field={`${item.field}.key`}
|
|
||||||
noStyle
|
|
||||||
rules={[{ required: true, message: '请输入变量名' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="变量名" />
|
|
||||||
</Form.Item>
|
|
||||||
<span className="text-gray-400">=</span>
|
|
||||||
<Form.Item
|
|
||||||
field={`${item.field}.value`}
|
|
||||||
noStyle
|
|
||||||
rules={[{ required: true, message: '请输入变量值' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="变量值" />
|
|
||||||
</Form.Item>
|
|
||||||
<Button
|
|
||||||
icon={<IconDelete />}
|
|
||||||
status="danger"
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
<Button
|
</Select>
|
||||||
type="dashed"
|
</Form.Item>
|
||||||
long
|
|
||||||
onClick={() => add()}
|
<Form.Item
|
||||||
icon={<IconPlus />}
|
label="选择分支"
|
||||||
>
|
field="branch"
|
||||||
添加环境变量
|
rules={[{ required: true, message: '请选择分支' }]}
|
||||||
</Button>
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择分支"
|
||||||
|
loading={branchLoading}
|
||||||
|
onChange={handleBranchChange}
|
||||||
|
>
|
||||||
|
{branches.map((branch) => (
|
||||||
|
<Select.Option key={branch.name} value={branch.name}>
|
||||||
|
{branch.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="选择提交"
|
||||||
|
field="commitHash"
|
||||||
|
rules={[{ required: true, message: '请选择提交记录' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择提交记录"
|
||||||
|
loading={loading}
|
||||||
|
renderFormat={(option) => {
|
||||||
|
const commit = commits.find((c) => c.sha === option?.value);
|
||||||
|
return commit ? commit.sha.substring(0, 7) : '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{commits.map((commit) => (
|
||||||
|
<Select.Option key={commit.sha} value={commit.sha}>
|
||||||
|
<div className="flex flex-col py-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-mono font-medium">
|
||||||
|
{commit.sha.substring(0, 7)}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-500 text-xs">
|
||||||
|
{formatDateTime(commit.commit.author.date)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600 text-sm truncate">
|
||||||
|
{commit.commit.message}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-xs">
|
||||||
|
{commit.commit.author.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 环境变量预设 */}
|
||||||
|
{envPresets.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||||
|
环境变量
|
||||||
</div>
|
</div>
|
||||||
)}
|
{envPresets.map((preset) => {
|
||||||
</Form.List>
|
if (preset.type === 'select' && preset.options) {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={preset.key}
|
||||||
|
label={preset.label}
|
||||||
|
field={preset.key}
|
||||||
|
rules={
|
||||||
|
preset.required
|
||||||
|
? [{ required: true, message: `请选择${preset.label}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select placeholder={`请选择${preset.label}`}>
|
||||||
|
{preset.options.map((option) => (
|
||||||
|
<Select.Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.type === 'multiselect' && preset.options) {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={preset.key}
|
||||||
|
label={preset.label}
|
||||||
|
field={preset.key}
|
||||||
|
rules={
|
||||||
|
preset.required
|
||||||
|
? [{ required: true, message: `请选择${preset.label}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder={`请选择${preset.label}`}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
{preset.options.map((option) => (
|
||||||
|
<Select.Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.type === 'input') {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={preset.key}
|
||||||
|
label={preset.label}
|
||||||
|
field={preset.key}
|
||||||
|
rules={
|
||||||
|
preset.required
|
||||||
|
? [{ required: true, message: `请输入${preset.label}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input placeholder={`请输入${preset.label}`} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,17 +25,6 @@ function DeployRecordItem({
|
|||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 环境标签渲染函数
|
|
||||||
const getEnvTag = (env: string) => {
|
|
||||||
const envMap: Record<string, { color: string; text: string }> = {
|
|
||||||
production: { color: 'red', text: '生产环境' },
|
|
||||||
staging: { color: 'orange', text: '预发布环境' },
|
|
||||||
development: { color: 'blue', text: '开发环境' },
|
|
||||||
};
|
|
||||||
const config = envMap[env] || { color: 'gray', text: env };
|
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -68,9 +57,6 @@ function DeployRecordItem({
|
|||||||
分支:{' '}
|
分支:{' '}
|
||||||
<span className="font-medium text-gray-700">{item.branch}</span>
|
<span className="font-medium text-gray-700">{item.branch}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
环境: {getEnvTag(item.env || 'unknown')}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
状态: {getStatusTag(item.status)}
|
状态: {getStatusTag(item.status)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import { Button, Checkbox, Input, Select, Space } from '@arco-design/web-react';
|
||||||
|
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export interface EnvPreset {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'select' | 'multiselect' | 'input';
|
||||||
|
required?: boolean; // 是否必填
|
||||||
|
options?: Array<{ label: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnvPresetsEditorProps {
|
||||||
|
value?: EnvPreset[];
|
||||||
|
onChange?: (value: EnvPreset[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnvPresetsEditor({ value = [], onChange }: EnvPresetsEditorProps) {
|
||||||
|
const [presets, setPresets] = useState<EnvPreset[]>(value);
|
||||||
|
|
||||||
|
// 当外部 value 变化时同步到内部状态
|
||||||
|
useEffect(() => {
|
||||||
|
setPresets(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleAddPreset = () => {
|
||||||
|
const newPreset: EnvPreset = {
|
||||||
|
key: '',
|
||||||
|
label: '',
|
||||||
|
type: 'select',
|
||||||
|
options: [{ label: '', value: '' }],
|
||||||
|
};
|
||||||
|
const newPresets = [...presets, newPreset];
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemovePreset = (index: number) => {
|
||||||
|
const newPresets = presets.filter((_, i) => i !== index);
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePresetChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof EnvPreset,
|
||||||
|
val: string | boolean | EnvPreset['type'] | EnvPreset['options'],
|
||||||
|
) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
newPresets[index] = { ...newPresets[index], [field]: val };
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddOption = (presetIndex: number) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
if (!newPresets[presetIndex].options) {
|
||||||
|
newPresets[presetIndex].options = [];
|
||||||
|
}
|
||||||
|
newPresets[presetIndex].options?.push({ label: '', value: '' });
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveOption = (presetIndex: number, optionIndex: number) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
newPresets[presetIndex].options = newPresets[presetIndex].options?.filter(
|
||||||
|
(_, i) => i !== optionIndex,
|
||||||
|
);
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOptionChange = (
|
||||||
|
presetIndex: number,
|
||||||
|
optionIndex: number,
|
||||||
|
field: 'label' | 'value',
|
||||||
|
val: string,
|
||||||
|
) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
if (newPresets[presetIndex].options) {
|
||||||
|
newPresets[presetIndex].options![optionIndex][field] = val;
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{presets.map((preset, presetIndex) => (
|
||||||
|
<div
|
||||||
|
key={`preset-${preset.key || presetIndex}`}
|
||||||
|
className="border border-gray-200 rounded p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="font-medium text-gray-700">
|
||||||
|
预设项 #{presetIndex + 1}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
status="danger"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
onClick={() => handleRemovePreset(presetIndex)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="变量名 (key)"
|
||||||
|
value={preset.key}
|
||||||
|
onChange={(val) => handlePresetChange(presetIndex, 'key', val)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="显示名称 (label)"
|
||||||
|
value={preset.label}
|
||||||
|
onChange={(val) =>
|
||||||
|
handlePresetChange(presetIndex, 'label', val)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Select
|
||||||
|
placeholder="选择类型"
|
||||||
|
value={preset.type}
|
||||||
|
onChange={(val) => handlePresetChange(presetIndex, 'type', val)}
|
||||||
|
>
|
||||||
|
<Select.Option value="select">单选</Select.Option>
|
||||||
|
<Select.Option value="multiselect">多选</Select.Option>
|
||||||
|
<Select.Option value="input">输入框</Select.Option>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={preset.required || false}
|
||||||
|
onChange={(checked) =>
|
||||||
|
handlePresetChange(presetIndex, 'required', checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
必填项
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(preset.type === 'select' || preset.type === 'multiselect') && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="text-sm text-gray-600 mb-2">选项:</div>
|
||||||
|
{preset.options?.map((option, optionIndex) => (
|
||||||
|
<div
|
||||||
|
key={`option-${option.value || optionIndex}`}
|
||||||
|
className="flex items-center gap-2 mb-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder="显示文本"
|
||||||
|
value={option.label}
|
||||||
|
onChange={(val) =>
|
||||||
|
handleOptionChange(
|
||||||
|
presetIndex,
|
||||||
|
optionIndex,
|
||||||
|
'label',
|
||||||
|
val,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder="值"
|
||||||
|
value={option.value}
|
||||||
|
onChange={(val) =>
|
||||||
|
handleOptionChange(
|
||||||
|
presetIndex,
|
||||||
|
optionIndex,
|
||||||
|
'value',
|
||||||
|
val,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
status="danger"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
onClick={() =>
|
||||||
|
handleRemoveOption(presetIndex, optionIndex)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="dashed"
|
||||||
|
long
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => handleAddOption(presetIndex)}
|
||||||
|
>
|
||||||
|
添加选项
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button type="dashed" long icon={<IconPlus />} onClick={handleAddPreset}>
|
||||||
|
添加环境预设
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnvPresetsEditor;
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
IconCode,
|
IconCode,
|
||||||
|
IconCommand,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
@@ -49,9 +50,12 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
import { formatDateTime } from '../../../utils/time';
|
import { formatDateTime } from '../../../utils/time';
|
||||||
import type { Deployment, Pipeline, Project, Step, WorkspaceDirStatus, WorkspaceStatus } from '../types';
|
import type { Deployment, Pipeline, Project, Step } from '../types';
|
||||||
import DeployModal from './components/DeployModal';
|
import DeployModal from './components/DeployModal';
|
||||||
import DeployRecordItem from './components/DeployRecordItem';
|
import DeployRecordItem from './components/DeployRecordItem';
|
||||||
|
import EnvPresetsEditor, {
|
||||||
|
type EnvPreset,
|
||||||
|
} from './components/EnvPresetsEditor';
|
||||||
import PipelineStepItem from './components/PipelineStepItem';
|
import PipelineStepItem from './components/PipelineStepItem';
|
||||||
import { detailService } from './service';
|
import { detailService } from './service';
|
||||||
|
|
||||||
@@ -84,7 +88,8 @@ function ProjectDetailPage() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
||||||
const [editingPipeline, setEditingPipeline] = useState<PipelineWithEnabled | null>(null);
|
const [editingPipeline, setEditingPipeline] =
|
||||||
|
useState<PipelineWithEnabled | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [pipelineForm] = Form.useForm();
|
const [pipelineForm] = Form.useForm();
|
||||||
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
||||||
@@ -92,12 +97,18 @@ function ProjectDetailPage() {
|
|||||||
|
|
||||||
// 流水线模板相关状态
|
// 流水线模板相关状态
|
||||||
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(
|
||||||
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
null,
|
||||||
|
);
|
||||||
|
const [templates, setTemplates] = useState<
|
||||||
|
Array<{ id: number; name: string; description: string }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
// 项目设置相关状态
|
// 项目设置相关状态
|
||||||
const [projectEditModalVisible, setProjectEditModalVisible] = useState(false);
|
const [isEditingProject, setIsEditingProject] = useState(false);
|
||||||
const [projectForm] = Form.useForm();
|
const [projectForm] = Form.useForm();
|
||||||
|
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
|
||||||
|
const [envPresetsLoading, setEnvPresetsLoading] = useState(false);
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
@@ -172,8 +183,14 @@ function ProjectDetailPage() {
|
|||||||
setDeployRecords(records);
|
setDeployRecords(records);
|
||||||
|
|
||||||
// 如果当前选中的记录正在运行,则更新选中记录
|
// 如果当前选中的记录正在运行,则更新选中记录
|
||||||
const selectedRecord = records.find((r: Deployment) => r.id === selectedRecordId);
|
const selectedRecord = records.find(
|
||||||
if (selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending')) {
|
(r: Deployment) => r.id === selectedRecordId,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
selectedRecord &&
|
||||||
|
(selectedRecord.status === 'running' ||
|
||||||
|
selectedRecord.status === 'pending')
|
||||||
|
) {
|
||||||
// 保持当前选中状态,但更新数据
|
// 保持当前选中状态,但更新数据
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -354,14 +371,15 @@ function ProjectDetailPage() {
|
|||||||
selectedTemplateId,
|
selectedTemplateId,
|
||||||
Number(id),
|
Number(id),
|
||||||
values.name,
|
values.name,
|
||||||
values.description || ''
|
values.description || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新本地状态 - 需要转换步骤数据结构
|
// 更新本地状态 - 需要转换步骤数据结构
|
||||||
const transformedSteps = newPipeline.steps?.map(step => ({
|
const transformedSteps =
|
||||||
...step,
|
newPipeline.steps?.map((step) => ({
|
||||||
enabled: step.valid === 1
|
...step,
|
||||||
})) || [];
|
enabled: step.valid === 1,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
const pipelineWithDefaults = {
|
const pipelineWithDefaults = {
|
||||||
...newPipeline,
|
...newPipeline,
|
||||||
@@ -592,6 +610,21 @@ function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 解析环境变量预设
|
||||||
|
useEffect(() => {
|
||||||
|
if (detail?.envPresets) {
|
||||||
|
try {
|
||||||
|
const presets = JSON.parse(detail.envPresets);
|
||||||
|
setEnvPresets(presets);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析环境变量预设失败:', error);
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
}, [detail]);
|
||||||
|
|
||||||
// 项目设置相关函数
|
// 项目设置相关函数
|
||||||
const handleEditProject = () => {
|
const handleEditProject = () => {
|
||||||
if (detail) {
|
if (detail) {
|
||||||
@@ -600,16 +633,21 @@ function ProjectDetailPage() {
|
|||||||
description: detail.description,
|
description: detail.description,
|
||||||
repository: detail.repository,
|
repository: detail.repository,
|
||||||
});
|
});
|
||||||
setProjectEditModalVisible(true);
|
setIsEditingProject(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProjectEditSuccess = async () => {
|
const handleCancelEditProject = () => {
|
||||||
|
setIsEditingProject(false);
|
||||||
|
projectForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveProject = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await projectForm.validate();
|
const values = await projectForm.validate();
|
||||||
await detailService.updateProject(Number(id), values);
|
await detailService.updateProject(Number(id), values);
|
||||||
Message.success('项目更新成功');
|
Message.success('项目更新成功');
|
||||||
setProjectEditModalVisible(false);
|
setIsEditingProject(false);
|
||||||
|
|
||||||
// 刷新项目详情
|
// 刷新项目详情
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -622,6 +660,27 @@ function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveEnvPresets = async () => {
|
||||||
|
try {
|
||||||
|
setEnvPresetsLoading(true);
|
||||||
|
await detailService.updateProject(Number(id), {
|
||||||
|
envPresets: JSON.stringify(envPresets),
|
||||||
|
});
|
||||||
|
Message.success('环境变量预设保存成功');
|
||||||
|
|
||||||
|
// 刷新项目详情
|
||||||
|
if (id) {
|
||||||
|
const projectDetail = await detailService.getProjectDetail(Number(id));
|
||||||
|
setDetail(projectDetail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存环境变量预设失败:', error);
|
||||||
|
Message.error('保存环境变量预设失败');
|
||||||
|
} finally {
|
||||||
|
setEnvPresetsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteProject = () => {
|
const handleDeleteProject = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '删除项目',
|
title: '删除项目',
|
||||||
@@ -671,7 +730,7 @@ function ProjectDetailPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 获取选中的流水线
|
// 获取选中的流水线
|
||||||
const selectedPipeline = pipelines.find(
|
const _selectedPipeline = pipelines.find(
|
||||||
(pipeline) => pipeline.id === selectedPipelineId,
|
(pipeline) => pipeline.id === selectedPipelineId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -681,11 +740,13 @@ function ProjectDetailPage() {
|
|||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取工作目录状态标签
|
// 获取工作目录状态标签
|
||||||
const getWorkspaceStatusTag = (status: string): { text: string; color: string } => {
|
const getWorkspaceStatusTag = (
|
||||||
|
status: string,
|
||||||
|
): { text: string; color: string } => {
|
||||||
const statusMap: Record<string, { text: string; color: string }> = {
|
const statusMap: Record<string, { text: string; color: string }> = {
|
||||||
not_created: { text: '未创建', color: 'gray' },
|
not_created: { text: '未创建', color: 'gray' },
|
||||||
empty: { text: '空目录', color: 'orange' },
|
empty: { text: '空目录', color: 'orange' },
|
||||||
@@ -703,7 +764,15 @@ function ProjectDetailPage() {
|
|||||||
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mb-6" title={<Space><IconFolder />工作目录状态</Space>}>
|
<Card
|
||||||
|
className="mb-6"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconFolder />
|
||||||
|
工作目录状态
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Descriptions
|
<Descriptions
|
||||||
column={2}
|
column={2}
|
||||||
data={[
|
data={[
|
||||||
@@ -717,7 +786,9 @@ function ProjectDetailPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '目录大小',
|
label: '目录大小',
|
||||||
value: workspaceStatus.size ? formatSize(workspaceStatus.size) : '-',
|
value: workspaceStatus.size
|
||||||
|
? formatSize(workspaceStatus.size)
|
||||||
|
: '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '当前分支',
|
label: '当前分支',
|
||||||
@@ -727,16 +798,24 @@ function ProjectDetailPage() {
|
|||||||
label: '最后提交',
|
label: '最后提交',
|
||||||
value: workspaceStatus.gitInfo?.lastCommit ? (
|
value: workspaceStatus.gitInfo?.lastCommit ? (
|
||||||
<Space direction="vertical" size="mini">
|
<Space direction="vertical" size="mini">
|
||||||
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text>
|
<Typography.Text code>
|
||||||
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text>
|
{workspaceStatus.gitInfo.lastCommit}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{workspaceStatus.gitInfo.lastCommitMessage}
|
||||||
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
) : '-',
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{workspaceStatus.error && (
|
{workspaceStatus.error && (
|
||||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
||||||
<Typography.Text type="danger">{workspaceStatus.error}</Typography.Text>
|
<Typography.Text type="error">
|
||||||
|
{workspaceStatus.error}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -763,7 +842,15 @@ function ProjectDetailPage() {
|
|||||||
size="large"
|
size="large"
|
||||||
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
||||||
>
|
>
|
||||||
<Tabs.TabPane title={<Space><IconHistory />部署记录</Space>} key="deployRecords">
|
<Tabs.TabPane
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconHistory />
|
||||||
|
部署记录
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
key="deployRecords"
|
||||||
|
>
|
||||||
<div className="grid grid-cols-5 gap-6 h-full">
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
{/* 左侧部署记录列表 */}
|
{/* 左侧部署记录列表 */}
|
||||||
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
||||||
@@ -801,7 +888,7 @@ function ProjectDetailPage() {
|
|||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
{selectedRecord && (
|
{selectedRecord && (
|
||||||
<Typography.Text type="secondary" className="text-sm">
|
<Typography.Text type="secondary" className="text-sm">
|
||||||
{selectedRecord.branch} · {selectedRecord.env} ·{' '}
|
{selectedRecord.branch}
|
||||||
{formatDateTime(selectedRecord.createdAt)}
|
{formatDateTime(selectedRecord.createdAt)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
@@ -813,7 +900,9 @@ function ProjectDetailPage() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleRetryDeployment(selectedRecord.id)}
|
onClick={() =>
|
||||||
|
handleRetryDeployment(selectedRecord.id)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
重新执行
|
重新执行
|
||||||
</Button>
|
</Button>
|
||||||
@@ -838,7 +927,15 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane title={<Space><IconCode />流水线</Space>} key="pipeline">
|
<Tabs.TabPane
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconCode />
|
||||||
|
流水线
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
key="pipeline"
|
||||||
|
>
|
||||||
<div className="grid grid-cols-5 gap-6 h-full">
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
{/* 左侧流水线列表 */}
|
{/* 左侧流水线列表 */}
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
@@ -951,9 +1048,7 @@ function ProjectDetailPage() {
|
|||||||
{pipeline.description}
|
{pipeline.description}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||||
<span>
|
<span>{pipeline.steps?.length || 0} 个步骤</span>
|
||||||
{pipeline.steps?.length || 0} 个步骤
|
|
||||||
</span>
|
|
||||||
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1005,7 +1100,11 @@ function ProjectDetailPage() {
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={selectedPipeline.steps?.map(step => step.id) || []}
|
items={
|
||||||
|
selectedPipeline.steps?.map(
|
||||||
|
(step) => step.id,
|
||||||
|
) || []
|
||||||
|
}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||||
@@ -1046,48 +1145,140 @@ function ProjectDetailPage() {
|
|||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|
||||||
{/* 项目设置标签页 */}
|
{/* 项目设置标签页 */}
|
||||||
<Tabs.TabPane key="settings" title={<Space><IconSettings />项目设置</Space>}>
|
<Tabs.TabPane
|
||||||
|
key="settings"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconSettings />
|
||||||
|
项目设置
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<Card title="项目信息" className="mb-4">
|
<Card title="项目信息" className="mb-4">
|
||||||
<Descriptions
|
{!isEditingProject ? (
|
||||||
column={1}
|
<>
|
||||||
data={[
|
<Descriptions
|
||||||
{
|
column={1}
|
||||||
label: '项目名称',
|
data={[
|
||||||
value: detail?.name,
|
{
|
||||||
},
|
label: '项目名称',
|
||||||
{
|
value: detail?.name,
|
||||||
label: '项目描述',
|
},
|
||||||
value: detail?.description || '-',
|
{
|
||||||
},
|
label: '项目描述',
|
||||||
{
|
value: detail?.description || '-',
|
||||||
label: 'Git 仓库',
|
},
|
||||||
value: detail?.repository,
|
{
|
||||||
},
|
label: 'Git 仓库',
|
||||||
{
|
value: detail?.repository,
|
||||||
label: '工作目录',
|
},
|
||||||
value: detail?.projectDir || '-',
|
{
|
||||||
},
|
label: '工作目录',
|
||||||
{
|
value: detail?.projectDir || '-',
|
||||||
label: '创建时间',
|
},
|
||||||
value: formatDateTime(detail?.createdAt),
|
{
|
||||||
},
|
label: '创建时间',
|
||||||
]}
|
value: formatDateTime(detail?.createdAt),
|
||||||
/>
|
},
|
||||||
<div className="mt-4 flex gap-2">
|
]}
|
||||||
<Button type="primary" onClick={handleEditProject}>
|
/>
|
||||||
编辑项目
|
<div className="mt-4 flex gap-2">
|
||||||
</Button>
|
<Button type="primary" onClick={handleEditProject}>
|
||||||
<Button status="danger" onClick={handleDeleteProject}>
|
编辑项目
|
||||||
删除项目
|
</Button>
|
||||||
</Button>
|
<Button status="danger" onClick={handleDeleteProject}>
|
||||||
</div>
|
删除项目
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Form form={projectForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
field="name"
|
||||||
|
label="项目名称"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入项目名称' },
|
||||||
|
{ minLength: 2, message: '项目名称至少2个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:我的应用" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
field="description"
|
||||||
|
label="项目描述"
|
||||||
|
rules={[
|
||||||
|
{ maxLength: 200, message: '描述不能超过200个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="请输入项目描述"
|
||||||
|
rows={3}
|
||||||
|
maxLength={200}
|
||||||
|
showWordLimit
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
field="repository"
|
||||||
|
label="Git 仓库地址"
|
||||||
|
rules={[{ required: true, message: '请输入仓库地址' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:https://github.com/user/repo.git" />
|
||||||
|
</Form.Item>
|
||||||
|
<div className="text-sm text-gray-500 mb-4">
|
||||||
|
<strong>工作目录:</strong> {detail?.projectDir || '-'}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 mb-4">
|
||||||
|
<strong>创建时间:</strong>{' '}
|
||||||
|
{formatDateTime(detail?.createdAt)}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
<div className="mt-4 flex gap-2">
|
||||||
|
<Button type="primary" onClick={handleSaveProject}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCancelEditProject}>取消</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 工作目录状态 */}
|
{/* 工作目录状态 */}
|
||||||
{renderWorkspaceStatus()}
|
{renderWorkspaceStatus()}
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
{/* 环境变量预设标签页 */}
|
||||||
|
<Tabs.TabPane
|
||||||
|
key="envPresets"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconCommand />
|
||||||
|
环境变量
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<Card
|
||||||
|
title="环境变量预设"
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSaveEnvPresets}
|
||||||
|
loading={envPresetsLoading}
|
||||||
|
>
|
||||||
|
保存预设
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="text-sm text-gray-600 mb-4">
|
||||||
|
配置项目的环境变量预设,在部署时可以选择这些预设值。支持单选、多选和输入框类型。
|
||||||
|
</div>
|
||||||
|
<EnvPresetsEditor value={envPresets} onChange={setEnvPresets} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1139,7 +1330,9 @@ function ProjectDetailPage() {
|
|||||||
<Select.Option key={template.id} value={template.id}>
|
<Select.Option key={template.id} value={template.id}>
|
||||||
<div>
|
<div>
|
||||||
<div>{template.name}</div>
|
<div>{template.name}</div>
|
||||||
<div className="text-xs text-gray-500">{template.description}</div>
|
<div className="text-xs text-gray-500">
|
||||||
|
{template.description}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
@@ -1155,10 +1348,7 @@ function ProjectDetailPage() {
|
|||||||
>
|
>
|
||||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item field="description" label="流水线描述">
|
||||||
field="description"
|
|
||||||
label="流水线描述"
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="描述这个流水线的用途和特点..."
|
placeholder="描述这个流水线的用途和特点..."
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -1176,10 +1366,7 @@ function ProjectDetailPage() {
|
|||||||
>
|
>
|
||||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item field="description" label="流水线描述">
|
||||||
field="description"
|
|
||||||
label="流水线描述"
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="描述这个流水线的用途和特点..."
|
placeholder="描述这个流水线的用途和特点..."
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -1228,47 +1415,6 @@ function ProjectDetailPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 编辑项目模态框 */}
|
|
||||||
<Modal
|
|
||||||
title="编辑项目"
|
|
||||||
visible={projectEditModalVisible}
|
|
||||||
onOk={handleProjectEditSuccess}
|
|
||||||
onCancel={() => setProjectEditModalVisible(false)}
|
|
||||||
style={{ width: 500 }}
|
|
||||||
>
|
|
||||||
<Form form={projectForm} layout="vertical">
|
|
||||||
<Form.Item
|
|
||||||
field="name"
|
|
||||||
label="项目名称"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入项目名称' },
|
|
||||||
{ minLength: 2, message: '项目名称至少2个字符' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:我的应用" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
field="description"
|
|
||||||
label="项目描述"
|
|
||||||
rules={[{ maxLength: 200, message: '描述不能超过200个字符' }]}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder="请输入项目描述"
|
|
||||||
rows={3}
|
|
||||||
maxLength={200}
|
|
||||||
showWordLimit
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
field="repository"
|
|
||||||
label="Git 仓库地址"
|
|
||||||
rules={[{ required: true, message: '请输入仓库地址' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:https://github.com/user/repo.git" />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<DeployModal
|
<DeployModal
|
||||||
visible={deployModalVisible}
|
visible={deployModalVisible}
|
||||||
onCancel={() => setDeployModalVisible(false)}
|
onCancel={() => setDeployModalVisible(false)}
|
||||||
@@ -1286,6 +1432,7 @@ function ProjectDetailPage() {
|
|||||||
}}
|
}}
|
||||||
pipelines={pipelines}
|
pipelines={pipelines}
|
||||||
projectId={Number(id)}
|
projectId={Number(id)}
|
||||||
|
project={detail}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { type APIResponse, net } from '@shared';
|
import { type APIResponse, net } from '@shared';
|
||||||
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types';
|
import type {
|
||||||
|
Branch,
|
||||||
|
Commit,
|
||||||
|
CreateDeploymentRequest,
|
||||||
|
Deployment,
|
||||||
|
Pipeline,
|
||||||
|
Project,
|
||||||
|
Step,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
class DetailService {
|
class DetailService {
|
||||||
async getProject(id: string) {
|
async getProject(id: string) {
|
||||||
@@ -19,7 +27,9 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取可用的流水线模板
|
// 获取可用的流水线模板
|
||||||
async getPipelineTemplates() {
|
async getPipelineTemplates() {
|
||||||
const { data } = await net.request<APIResponse<{id: number, name: string, description: string}[]>>({
|
const { data } = await net.request<
|
||||||
|
APIResponse<{ id: number; name: string; description: string }[]>
|
||||||
|
>({
|
||||||
url: '/api/pipelines/templates',
|
url: '/api/pipelines/templates',
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -59,7 +69,7 @@ class DetailService {
|
|||||||
templateId: number,
|
templateId: number,
|
||||||
projectId: number,
|
projectId: number,
|
||||||
name: string,
|
name: string,
|
||||||
description?: string
|
description?: string,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||||
url: '/api/pipelines/from-template',
|
url: '/api/pipelines/from-template',
|
||||||
@@ -68,7 +78,7 @@ class DetailService {
|
|||||||
templateId,
|
templateId,
|
||||||
projectId,
|
projectId,
|
||||||
name,
|
name,
|
||||||
description
|
description,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -204,7 +214,7 @@ class DetailService {
|
|||||||
// 更新项目
|
// 更新项目
|
||||||
async updateProject(
|
async updateProject(
|
||||||
id: number,
|
id: number,
|
||||||
project: Partial<{ name: string; description: string; repository: string }>,
|
project: Partial<Project>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { projectService } from '../service';
|
import EnvPresetsEditor from '../../detail/components/EnvPresetsEditor';
|
||||||
import type { Project } from '../../types';
|
import type { Project } from '../../types';
|
||||||
|
import { projectService } from '../service';
|
||||||
|
|
||||||
interface CreateProjectModalProps {
|
interface CreateProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -22,7 +30,15 @@ function CreateProjectModal({
|
|||||||
const values = await form.validate();
|
const values = await form.validate();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const newProject = await projectService.create(values);
|
// 序列化环境预设
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
envPresets: values.envPresets
|
||||||
|
? JSON.stringify(values.envPresets)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newProject = await projectService.create(submitData);
|
||||||
|
|
||||||
Message.success('项目创建成功');
|
Message.success('项目创建成功');
|
||||||
onSuccess(newProject);
|
onSuccess(newProject);
|
||||||
@@ -114,7 +130,9 @@ function CreateProjectModal({
|
|||||||
if (value.includes('..') || value.includes('~')) {
|
if (value.includes('..') || value.includes('~')) {
|
||||||
return cb('不能包含路径遍历字符(.. 或 ~)');
|
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||||
}
|
}
|
||||||
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
// 检查非法字符(控制字符 0x00-0x1F)
|
||||||
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: 需要检测路径中的控制字符
|
||||||
|
if (/[<>:"|?*\u0000-\u001f]/.test(value)) {
|
||||||
return cb('路径包含非法字符');
|
return cb('路径包含非法字符');
|
||||||
}
|
}
|
||||||
cb();
|
cb();
|
||||||
@@ -124,6 +142,14 @@ function CreateProjectModal({
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
|
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
|
||||||
|
<Collapse.Item header="环境变量预设配置(可选)" name="envPresets">
|
||||||
|
<Form.Item field="envPresets" noStyle>
|
||||||
|
<EnvPresetsEditor />
|
||||||
|
</Form.Item>
|
||||||
|
</Collapse.Item>
|
||||||
|
</Collapse>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import EnvPresetsEditor, {
|
||||||
|
type EnvPreset,
|
||||||
|
} from '../../detail/components/EnvPresetsEditor';
|
||||||
|
import type { Project } from '../../types';
|
||||||
import { projectService } from '../service';
|
import { projectService } from '../service';
|
||||||
import type { Project } from '../types';
|
|
||||||
|
|
||||||
interface EditProjectModalProps {
|
interface EditProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -22,10 +32,20 @@ function EditProjectModal({
|
|||||||
// 当项目信息变化时,更新表单数据
|
// 当项目信息变化时,更新表单数据
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (project && visible) {
|
if (project && visible) {
|
||||||
|
let envPresets: EnvPreset[] = [];
|
||||||
|
try {
|
||||||
|
if (project.envPresets) {
|
||||||
|
envPresets = JSON.parse(project.envPresets);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析环境预设失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: project.name,
|
name: project.name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
repository: project.repository,
|
repository: project.repository,
|
||||||
|
envPresets,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [project, visible, form]);
|
}, [project, visible, form]);
|
||||||
@@ -37,7 +57,18 @@ function EditProjectModal({
|
|||||||
|
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
const updatedProject = await projectService.update(project.id, values);
|
// 序列化环境预设
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
envPresets: values.envPresets
|
||||||
|
? JSON.stringify(values.envPresets)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedProject = await projectService.update(
|
||||||
|
project.id,
|
||||||
|
submitData,
|
||||||
|
);
|
||||||
|
|
||||||
Message.success('项目更新成功');
|
Message.success('项目更新成功');
|
||||||
onSuccess(updatedProject);
|
onSuccess(updatedProject);
|
||||||
@@ -111,6 +142,14 @@ function EditProjectModal({
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
|
||||||
|
<Collapse.Item header="环境变量预设配置" name="envPresets">
|
||||||
|
<Form.Item field="envPresets" noStyle>
|
||||||
|
<EnvPresetsEditor />
|
||||||
|
</Form.Item>
|
||||||
|
</Collapse.Item>
|
||||||
|
</Collapse>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Space,
|
Space,
|
||||||
Tag,
|
Tag,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Grid, Message, Typography } from '@arco-design/web-react';
|
import { Button, Grid, Typography } from '@arco-design/web-react';
|
||||||
import { IconPlus } from '@arco-design/web-react/icon';
|
import { IconPlus } from '@arco-design/web-react/icon';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface Project {
|
|||||||
description: string;
|
description: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
projectDir: string; // 项目工作目录路径(必填)
|
projectDir: string; // 项目工作目录路径(必填)
|
||||||
|
envPresets?: string; // 环境预设配置(JSON格式)
|
||||||
valid: number;
|
valid: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -77,12 +78,11 @@ export interface Pipeline {
|
|||||||
export interface Deployment {
|
export interface Deployment {
|
||||||
id: number;
|
id: number;
|
||||||
branch: string;
|
branch: string;
|
||||||
env?: string;
|
envVars?: string; // JSON 字符串
|
||||||
status: string;
|
status: string;
|
||||||
commitHash?: string;
|
commitHash?: string;
|
||||||
commitMessage?: string;
|
commitMessage?: string;
|
||||||
buildLog?: string;
|
buildLog?: string;
|
||||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
finishedAt?: string;
|
finishedAt?: string;
|
||||||
valid: number;
|
valid: number;
|
||||||
@@ -127,6 +127,5 @@ export interface CreateDeploymentRequest {
|
|||||||
branch: string;
|
branch: string;
|
||||||
commitHash: string;
|
commitHash: string;
|
||||||
commitMessage: string;
|
commitMessage: string;
|
||||||
env?: string;
|
envVars?: Record<string, string>; // 环境变量 key-value 对象
|
||||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ class Net {
|
|||||||
(error) => {
|
(error) => {
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
// 对于DELETE请求返回204状态码的情况,视为成功
|
// 对于DELETE请求返回204状态码的情况,视为成功
|
||||||
if (error.response && error.response.status === 204 && error.config.method === 'delete') {
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.status === 204 &&
|
||||||
|
error.config.method === 'delete'
|
||||||
|
) {
|
||||||
// 创建一个模拟的成功响应
|
// 创建一个模拟的成功响应
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
...error.response,
|
...error.response,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 文档拥有者
|
||||||
|
|
||||||
|
- backend: backend-team@example.com
|
||||||
|
- ops: ops-team@example.com
|
||||||
|
- product: product-team@example.com
|
||||||
|
|
||||||
|
每个文档请在 front-matter 中声明 `owners` 字段。
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
title: 设计文档模板
|
||||||
|
summary: 记录一个功能/模块的设计方案、权衡与落地计划(建议配套 ADR)。
|
||||||
|
owners:
|
||||||
|
- team: <team>
|
||||||
|
reviewers:
|
||||||
|
- <name-or-team>
|
||||||
|
status: draft
|
||||||
|
date: 2026-01-03
|
||||||
|
version: 0.1.0
|
||||||
|
related:
|
||||||
|
- adr: docs/architecture/adr-xxxx-<slug>.md
|
||||||
|
- pr: <link>
|
||||||
|
- issue: <link>
|
||||||
|
---
|
||||||
|
|
||||||
|
# 设计文档:<标题>
|
||||||
|
|
||||||
|
## 1. 背景(Context)
|
||||||
|
|
||||||
|
- 当前问题是什么?为什么现在要做?
|
||||||
|
- 相关现状:已有模块/接口/数据模型(附链接)
|
||||||
|
- 约束:技术栈、部署方式、团队边界、时间/人力
|
||||||
|
|
||||||
|
## 2. 目标(Goals)
|
||||||
|
|
||||||
|
- [ ] 目标 1(可验证)
|
||||||
|
- [ ] 目标 2(可验证)
|
||||||
|
|
||||||
|
## 3. 非目标(Non-goals)
|
||||||
|
|
||||||
|
- 不做什么(防止范围膨胀)
|
||||||
|
|
||||||
|
## 4. 需求与范围(Requirements & Scope)
|
||||||
|
|
||||||
|
- 用户/角色:谁会用?(例如:管理员、开发者、CI runner)
|
||||||
|
- 功能需求:
|
||||||
|
- R1:
|
||||||
|
- R2:
|
||||||
|
- 非功能需求:性能、可用性、可维护性、可观测性
|
||||||
|
|
||||||
|
## 5. 方案概览(High-level Design)
|
||||||
|
|
||||||
|
- 用 5-10 行描述整体方案(模块、数据流、调用链)
|
||||||
|
- 关键选择:为什么选这个方案?
|
||||||
|
|
||||||
|
## 6. 详细设计(Detailed Design)
|
||||||
|
|
||||||
|
### 6.1 接口/API 设计
|
||||||
|
|
||||||
|
- 新增/变更端点(路径、方法、权限、请求/响应示例)
|
||||||
|
- 错误码与错误语义(与 `BusinessError` 对齐)
|
||||||
|
|
||||||
|
### 6.2 数据模型/数据库
|
||||||
|
|
||||||
|
- Prisma model 变更(字段、索引、迁移策略)
|
||||||
|
- 数据一致性与幂等策略(例如:重试/重复提交)
|
||||||
|
|
||||||
|
### 6.3 任务/队列/异步处理(如有)
|
||||||
|
|
||||||
|
- 队列模型:入队、出队、并发、重试、死信/失败处理
|
||||||
|
- 状态机:状态枚举与迁移
|
||||||
|
|
||||||
|
### 6.4 配置与环境变量
|
||||||
|
|
||||||
|
- 新增 env(默认值、是否敏感、是否需要重启)
|
||||||
|
|
||||||
|
### 6.5 可观测性
|
||||||
|
|
||||||
|
- 日志:关键日志点、traceId/requestId(如有)
|
||||||
|
- 指标:成功率、延迟、队列长度、失败原因分布
|
||||||
|
- 告警:P0/P1 触发条件
|
||||||
|
|
||||||
|
### 6.6 安全与权限
|
||||||
|
|
||||||
|
- 认证:是否要求登录(session)
|
||||||
|
- 授权:角色/资源权限(项目级、流水线级)
|
||||||
|
- 数据安全:敏感信息、token、日志脱敏
|
||||||
|
|
||||||
|
## 7. 影响与权衡(Trade-offs)
|
||||||
|
|
||||||
|
- 性能影响
|
||||||
|
- 运维影响
|
||||||
|
- 对现有接口/调用方的影响
|
||||||
|
- 技术债与后续演进
|
||||||
|
|
||||||
|
## 8. 兼容性与迁移(Compatibility & Migration)
|
||||||
|
|
||||||
|
- 是否 breaking change?
|
||||||
|
- 迁移步骤(DB、配置、数据回填)
|
||||||
|
- 回滚策略
|
||||||
|
|
||||||
|
## 9. 测试计划(Test Plan)
|
||||||
|
|
||||||
|
- 单测:覆盖哪些模块
|
||||||
|
- 集成测试:关键链路(如:创建部署 -> 入队 -> 执行 -> 状态更新)
|
||||||
|
- 手工验证:步骤清单
|
||||||
|
|
||||||
|
## 10. 发布计划(Rollout Plan)
|
||||||
|
|
||||||
|
- 分阶段:灰度/开关/逐步放量(如有)
|
||||||
|
- 监控指标与验收标准
|
||||||
|
|
||||||
|
## 11. 备选方案(Alternatives Considered)
|
||||||
|
|
||||||
|
- 方案 A:为什么不用
|
||||||
|
- 方案 B:为什么不用
|
||||||
|
|
||||||
|
## 12. 风险与开放问题(Risks & Open Questions)
|
||||||
|
|
||||||
|
- 风险 1
|
||||||
|
- 问题 1(需要谁来决定/何时决定)
|
||||||
|
|
||||||
|
## 13. 附录(Appendix)
|
||||||
|
|
||||||
|
- 相关链接:控制器、DTO、Prisma schema、PR 等
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Runbook 模板
|
||||||
|
owners:
|
||||||
|
- ops: ops-team
|
||||||
|
status: draft
|
||||||
|
---
|
||||||
|
|
||||||
|
# Runbook 标题
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
## 负责人
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
## 暂时性缓解
|
||||||
|
|
||||||
|
## 恢复步骤
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
## 回滚(如果适用)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: API 文档
|
||||||
|
summary: 本目录存放 OpenAPI 定义与 API 使用说明。
|
||||||
|
tags: [api]
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
status: stable
|
||||||
|
---
|
||||||
|
|
||||||
|
# API 文档
|
||||||
|
|
||||||
|
本目录包含 OpenAPI 规范与示例。可使用 Swagger UI 或 Redoc 渲染 `openapi.yaml`。
|
||||||
|
|
||||||
|
- OpenAPI: `openapi.yaml`
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
title: API 端点总览
|
||||||
|
summary: 基于 `apps/server` 控制器实现的主要 REST API 端点汇总。
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
status: stable
|
||||||
|
---
|
||||||
|
|
||||||
|
# API 端点总览
|
||||||
|
|
||||||
|
基础前缀:`/api`
|
||||||
|
|
||||||
|
下面列出当前实现的主要控制器与常用端点。
|
||||||
|
|
||||||
|
## Projects (`/api/projects`)
|
||||||
|
|
||||||
|
- GET `/api/projects` : 列表(支持分页与按 name 搜索)
|
||||||
|
- GET `/api/projects/:id` : 获取单个项目(包含 workspace 状态)
|
||||||
|
- POST `/api/projects` : 创建项目(body: `name`, `repository`, `projectDir` 等)
|
||||||
|
- PUT `/api/projects/:id` : 更新项目
|
||||||
|
- DELETE `/api/projects/:id` : 软删除(将 `valid` 置为 0)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/projects?page=1&limit=10
|
||||||
|
```
|
||||||
|
|
||||||
|
## User (`/api/user`)
|
||||||
|
|
||||||
|
- GET `/api/user/list` : 模拟用户列表
|
||||||
|
- GET `/api/user/detail/:id` : 用户详情
|
||||||
|
- POST `/api/user` : 创建用户
|
||||||
|
- PUT `/api/user/:id` : 更新用户
|
||||||
|
- DELETE `/api/user/:id` : 删除用户
|
||||||
|
- GET `/api/user/search` : 搜索用户
|
||||||
|
|
||||||
|
## Auth (`/api/auth`)
|
||||||
|
|
||||||
|
- GET `/api/auth/url` : 获取 Gitea OAuth 授权 URL
|
||||||
|
- POST `/api/auth/login` : 使用 OAuth code 登录(返回 session)
|
||||||
|
- GET `/api/auth/logout` : 登出
|
||||||
|
- GET `/api/auth/info` : 当前会话用户信息
|
||||||
|
|
||||||
|
注意:需要配置 `GITEA_URL`、`GITEA_CLIENT_ID`、`GITEA_REDIRECT_URI`。
|
||||||
|
|
||||||
|
## Deployments (`/api/deployments`)
|
||||||
|
|
||||||
|
- GET `/api/deployments` : 列表(支持 projectId 过滤)
|
||||||
|
- POST `/api/deployments` : 创建部署(会将任务加入执行队列)
|
||||||
|
- POST `/api/deployments/:id/retry` : 重新执行某次部署(复制记录并 requeue)
|
||||||
|
|
||||||
|
## Pipelines (`/api/pipelines`)
|
||||||
|
|
||||||
|
- GET `/api/pipelines` : 列表(含 steps)
|
||||||
|
- GET `/api/pipelines/templates` : 获取可用流水线模板
|
||||||
|
- GET `/api/pipelines/:id` : 单个流水线(含步骤)
|
||||||
|
- POST `/api/pipelines` : 创建流水线
|
||||||
|
- POST `/api/pipelines/from-template` : 基于模板创建流水线
|
||||||
|
- PUT `/api/pipelines/:id` : 更新流水线
|
||||||
|
- DELETE `/api/pipelines/:id` : 软删除
|
||||||
|
|
||||||
|
## Steps (`/api/steps`)
|
||||||
|
|
||||||
|
- GET `/api/steps` : 列表(支持 pipelineId 过滤)
|
||||||
|
- GET `/api/steps/:id` : 单个步骤
|
||||||
|
- POST `/api/steps` : 创建步骤(包含 `script` 字段)
|
||||||
|
- PUT `/api/steps/:id` : 更新步骤
|
||||||
|
- DELETE `/api/steps/:id` : 软删除
|
||||||
|
|
||||||
|
## Git (`/api/git`)
|
||||||
|
|
||||||
|
- GET `/api/git/commits?projectId=&branch=` : 获取指定项目的提交列表(调用 Gitea)
|
||||||
|
- GET `/api/git/branches?projectId=` : 获取分支列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
想要更详细的示例(请求 body、响应 schema),我可以为每个端点基于 `dto.ts` 自动生成示例请求/响应片段。是否需要我继续生成?
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
openapi: 3.0.1
|
||||||
|
info:
|
||||||
|
title: Foka-CI 示例 API
|
||||||
|
version: '1.0.0'
|
||||||
|
paths:
|
||||||
|
/health:
|
||||||
|
get:
|
||||||
|
summary: 健康检查
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
/projects:
|
||||||
|
get:
|
||||||
|
summary: 列出项目
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 项目列表
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: ADR 0001 - 服务设计决策
|
||||||
|
date: 2026-01-03
|
||||||
|
authors:
|
||||||
|
- backend-team
|
||||||
|
status: accepted
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR 0001: 服务设计与部署模型
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
需要选择微服务还是单体部署以便平衡开发速度与运维复杂度。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
采用模块化单体(modular monolith)作为初始阶段部署方式,关键模块解耦、接口明确,后续按需拆分服务。
|
||||||
|
|
||||||
|
## 影响
|
||||||
|
|
||||||
|
- 优点:降低初期运维成本,便于本地调试与 CI 集成。
|
||||||
|
- 缺点:需要在代码边界设计中预留拆分点。
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
在拆分时优先考虑数据库边界和独立部署能力。
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
title: 设计文档 0001 - 产品原型
|
||||||
|
summary: 产品的设计原型
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
reviewers:
|
||||||
|
- ops-team
|
||||||
|
status: draft
|
||||||
|
date: 2026-01-03
|
||||||
|
version: 0.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# 设计文档 0001:产品整体原型
|
||||||
|
|
||||||
|
## 1. 背景(Context)
|
||||||
|
|
||||||
|
企业内部代码发布是非常频繁的事情,无论是测试环境还是生产环境,发布代码常常面临着一些痛点:频繁发布浪费开发时;手动发布代码可能漏掉某些步骤、造成环境报错。所以需要引入 CI/CD 工具让发布流程自动化,市场上已经存在很多解决类似问题的产品,例如 Drone CI、jenkins 等,但是这些工具不够灵活、不能满足全部场景,所以便有了 Foka-CI 这款产品。
|
||||||
|
|
||||||
|
## 2. 拆解需求
|
||||||
|
|
||||||
|
### 登录认证页面
|
||||||
|
|
||||||
|
仅提供 Gitea 的 OAuth 认证登录,方便获取仓库信息,简化密码登录过程
|
||||||
|
|
||||||
|
### 项目列表页面
|
||||||
|
|
||||||
|
查看所有项目,项目的基本信息有:
|
||||||
|
|
||||||
|
- 项目名
|
||||||
|
- 项目描述(可选)
|
||||||
|
- 代码仓库地址
|
||||||
|
- 项目的工作目录,用于将代码克隆到该目录下,发布流水线脚本在该目录下执行
|
||||||
|
|
||||||
|
创建项目,需要填写基本信息
|
||||||
|
|
||||||
|
### 项目详情页
|
||||||
|
|
||||||
|
|
||||||
|
## 2. 目标(Goals)
|
||||||
|
|
||||||
|
- [ ] 明确“部署任务”从创建到执行的状态流转与约束
|
||||||
|
- [ ] 明确重试语义(retry 会创建新 deployment,而不是复用原记录)
|
||||||
|
- [ ] 降低重复入队/重复执行的风险(幂等与并发边界清晰)
|
||||||
|
- [ ] 让运维/排障更容易:关键日志点与可观测性清单
|
||||||
|
|
||||||
|
## 3. 非目标(Non-goals)
|
||||||
|
|
||||||
|
- 不引入外部消息队列(如 Redis/Kafka),仍以当前内存队列 + DB 恢复为基础
|
||||||
|
- 不在本文直接实现大规模调度/分布式 runner(后续 ADR/设计再做)
|
||||||
|
|
||||||
|
## 4. 需求与范围(Requirements & Scope)
|
||||||
|
|
||||||
|
### 功能需求
|
||||||
|
|
||||||
|
- 创建部署:写入一条 Deployment 记录,初始状态为 `pending`,并加入执行队列
|
||||||
|
- 重试部署:通过 `/deployments/:id/retry` 创建一条新的 Deployment 记录(复制必要字段),并加入执行队列
|
||||||
|
- 服务重启恢复:服务启动后能从 DB 找回仍为 `pending` 的任务并继续执行
|
||||||
|
|
||||||
|
### 非功能需求
|
||||||
|
|
||||||
|
- 可靠性:服务重启不会“丢任务”(pending 的任务能恢复)
|
||||||
|
- 幂等性:避免同一个 deployment 被重复执行
|
||||||
|
- 可观测性:能定位某次部署为何失败、何时开始/结束、队列长度
|
||||||
|
|
||||||
|
## 5. 方案概览(High-level Design)
|
||||||
|
|
||||||
|
当前方案核心链路如下:
|
||||||
|
|
||||||
|
1. API 创建 Deployment(status=pending)
|
||||||
|
2. `ExecutionQueue.addTask(deploymentId, pipelineId)` 入队
|
||||||
|
3. `ExecutionQueue.processQueue()` 串行消费 `pendingQueue`
|
||||||
|
4. `executePipeline()` 会读取 Deployment 与关联 Project,获取 `projectDir`,然后创建 `PipelineRunner` 执行
|
||||||
|
5. 定时轮询:每 30 秒扫描 DB 中的 pending 任务,若不在 `runningDeployments` 集合则补入队列
|
||||||
|
|
||||||
|
## 6. 详细设计(Detailed Design)
|
||||||
|
|
||||||
|
### 6.1 接口/API 设计
|
||||||
|
|
||||||
|
#### 创建部署
|
||||||
|
|
||||||
|
- POST `/api/deployments`
|
||||||
|
- 行为:创建 Deployment 后立即入队
|
||||||
|
|
||||||
|
#### 重试部署
|
||||||
|
|
||||||
|
- POST `/api/deployments/:id/retry`
|
||||||
|
- 行为:读取原 deployment -> 创建新 deployment(status=pending)-> 入队
|
||||||
|
- 语义:重试是“创建一个新的任务实例”,便于保留历史执行记录
|
||||||
|
|
||||||
|
### 6.2 数据模型/数据库
|
||||||
|
|
||||||
|
Deployment 当前关键字段(见 schema 注释):
|
||||||
|
|
||||||
|
- `status`: pending/running/success/failed/cancelled(目前入队依赖 pending)
|
||||||
|
- `buildLog`: 执行日志(当前创建时写空字符串)
|
||||||
|
- `startedAt`/`finishedAt`: 时间标记(目前 created 时 startedAt 默认 now)
|
||||||
|
|
||||||
|
建议补齐/明确(文档层面约束,代码后续落地):
|
||||||
|
|
||||||
|
- 状态迁移:
|
||||||
|
- `pending` -> `running`(开始执行前)
|
||||||
|
- `running` -> `success|failed|cancelled`(结束后)
|
||||||
|
- 幂等控制:
|
||||||
|
- 以 deploymentId 为“单次执行唯一标识”,同一个 deploymentId 不允许重复开始执行
|
||||||
|
|
||||||
|
### 6.3 队列/轮询/并发
|
||||||
|
|
||||||
|
现状:
|
||||||
|
|
||||||
|
- `runningDeployments` 同时承担“已入队/执行中”的去重集合
|
||||||
|
- `pendingQueue` 为内存 FIFO
|
||||||
|
- 单实例串行消费(`processQueue` while 循环)
|
||||||
|
- 轮询间隔常量 30 秒
|
||||||
|
|
||||||
|
风险点(需要在文档中明确约束/后续逐步修正):
|
||||||
|
|
||||||
|
- 多实例部署:如果将来启动多个 server 实例,每个实例都可能轮询到同一条 pending 记录并执行(需要 DB 锁/租约/状态原子更新)
|
||||||
|
- 状态更新缺口:当前 `ExecutionQueue` 代码中没有看到明确把 status 从 pending 改成 running/failed/success 的逻辑(可能在 `PipelineRunner` 内处理;若没有,需要补齐)
|
||||||
|
|
||||||
|
建议(不改变整体架构前提):
|
||||||
|
|
||||||
|
- 将轮询间隔改为可配置 env:`EXECUTION_POLL_INTERVAL_MS`(默认 30000)
|
||||||
|
- 在真正执行前做一次 DB 原子“抢占”:仅当 status=pending 时更新为 running(并记录开始时间);更新失败则放弃执行
|
||||||
|
|
||||||
|
### 6.4 可观测性
|
||||||
|
|
||||||
|
最低要求(建议后续落地到代码/日志规范):
|
||||||
|
|
||||||
|
- 日志字段:deploymentId、pipelineId、projectId、projectDir、status
|
||||||
|
- 队列指标:pendingQueue length、runningDeployments size
|
||||||
|
- 失败记录:捕获异常 message/stack(避免泄露敏感信息)
|
||||||
|
|
||||||
|
### 6.5 安全与权限
|
||||||
|
|
||||||
|
当前接口层面需要确认:
|
||||||
|
|
||||||
|
- `/api/deployments` 与 `/api/deployments/:id/retry` 是否需要登录/鉴权(取决于 middleware 配置)
|
||||||
|
- 若需要鉴权:建议限制为有项目权限的用户才能创建/重试部署
|
||||||
|
|
||||||
|
## 7. 影响与权衡(Trade-offs)
|
||||||
|
|
||||||
|
- 继续采用内存队列:实现简单,但天然不支持多实例并发安全
|
||||||
|
- DB 轮询恢复:可靠性提升,但会带来额外 DB 查询压力
|
||||||
|
|
||||||
|
## 8. 兼容性与迁移(Compatibility & Migration)
|
||||||
|
|
||||||
|
- 文档层面不破坏现有 API
|
||||||
|
- 若引入 status 原子抢占,需要确保旧数据/旧状态兼容(例如对历史 pending 记录仍可恢复)
|
||||||
|
|
||||||
|
## 9. 测试计划(Test Plan)
|
||||||
|
|
||||||
|
- 集成链路:创建 deployment -> 入队 -> 触发执行(可用假 runner)
|
||||||
|
- 重启恢复:插入 pending 记录 -> initialize() -> 任务被 addTask
|
||||||
|
- 重试接口:原记录存在/不存在的分支
|
||||||
|
|
||||||
|
## 10. 发布计划(Rollout Plan)
|
||||||
|
|
||||||
|
- 先补齐文档 + 最小日志规范
|
||||||
|
- 再逐步落地:status 原子抢占 + 轮询间隔 env
|
||||||
|
|
||||||
|
## 11. 备选方案(Alternatives Considered)
|
||||||
|
|
||||||
|
- 引入 Redis 队列(BullMQ 等):更可靠、支持多实例,但复杂度上升
|
||||||
|
- 使用 DB 作为队列(表 + 锁/租约):更可靠,但需要严格的并发控制
|
||||||
|
|
||||||
|
## 12. 风险与开放问题(Risks & Open Questions)
|
||||||
|
|
||||||
|
- Q1:`PipelineRunner` 是否负责更新 Deployment.status?如果没有,状态机应由谁维护?
|
||||||
|
- Q2:服务是否计划多实例部署?如果是,必须补齐“抢占执行”机制
|
||||||
|
|
||||||
|
## 13. 附录(Appendix)
|
||||||
|
|
||||||
|
- 代码:`apps/server/libs/execution-queue.ts`
|
||||||
|
- 控制器:`apps/server/controllers/deployment/index.ts`
|
||||||
|
- Schema:`apps/server/prisma/schema.prisma`
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
title: 设计文档 0001 - 部署执行队列与重试(基于当前实现)
|
||||||
|
summary: 记录当前 ExecutionQueue 的行为与下一步改进方向,便于团队对齐。
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
reviewers:
|
||||||
|
- ops-team
|
||||||
|
status: draft
|
||||||
|
date: 2026-01-03
|
||||||
|
version: 0.1.0
|
||||||
|
related:
|
||||||
|
- code: apps/server/libs/execution-queue.ts
|
||||||
|
- code: apps/server/controllers/deployment/index.ts
|
||||||
|
- schema: apps/server/prisma/schema.prisma
|
||||||
|
---
|
||||||
|
|
||||||
|
# 设计文档 0001:部署执行队列与重试
|
||||||
|
|
||||||
|
## 1. 背景(Context)
|
||||||
|
|
||||||
|
当前服务端在启动时会初始化执行队列(`ExecutionQueue.initialize()`),用于从数据库恢复 `status=pending` 的部署任务并按顺序执行流水线。
|
||||||
|
|
||||||
|
现有相关事实(来自当前代码):
|
||||||
|
|
||||||
|
- 服务启动入口:`apps/server/app.ts`
|
||||||
|
- 路由:`/api/deployments`(创建部署后入队)与 `/api/deployments/:id/retry`(复制记录后入队)
|
||||||
|
- 数据库:SQLite + Prisma,Deployment 存在 `status` 字段(注释标明:pending/running/success/failed/cancelled)
|
||||||
|
- 队列实现:内存队列 `pendingQueue` + `runningDeployments` Set,并有轮询机制(默认 30 秒)把 DB 中的 pending 任务补进队列
|
||||||
|
|
||||||
|
## 2. 目标(Goals)
|
||||||
|
|
||||||
|
- [ ] 明确“部署任务”从创建到执行的状态流转与约束
|
||||||
|
- [ ] 明确重试语义(retry 会创建新 deployment,而不是复用原记录)
|
||||||
|
- [ ] 降低重复入队/重复执行的风险(幂等与并发边界清晰)
|
||||||
|
- [ ] 让运维/排障更容易:关键日志点与可观测性清单
|
||||||
|
|
||||||
|
## 3. 非目标(Non-goals)
|
||||||
|
|
||||||
|
- 不引入外部消息队列(如 Redis/Kafka),仍以当前内存队列 + DB 恢复为基础
|
||||||
|
- 不在本文直接实现大规模调度/分布式 runner(后续 ADR/设计再做)
|
||||||
|
|
||||||
|
## 4. 需求与范围(Requirements & Scope)
|
||||||
|
|
||||||
|
### 功能需求
|
||||||
|
|
||||||
|
- 创建部署:写入一条 Deployment 记录,初始状态为 `pending`,并加入执行队列
|
||||||
|
- 重试部署:通过 `/deployments/:id/retry` 创建一条新的 Deployment 记录(复制必要字段),并加入执行队列
|
||||||
|
- 服务重启恢复:服务启动后能从 DB 找回仍为 `pending` 的任务并继续执行
|
||||||
|
|
||||||
|
### 非功能需求
|
||||||
|
|
||||||
|
- 可靠性:服务重启不会“丢任务”(pending 的任务能恢复)
|
||||||
|
- 幂等性:避免同一个 deployment 被重复执行
|
||||||
|
- 可观测性:能定位某次部署为何失败、何时开始/结束、队列长度
|
||||||
|
|
||||||
|
## 5. 方案概览(High-level Design)
|
||||||
|
|
||||||
|
当前方案核心链路如下:
|
||||||
|
|
||||||
|
1) API 创建 Deployment(status=pending)
|
||||||
|
2) `ExecutionQueue.addTask(deploymentId, pipelineId)` 入队
|
||||||
|
3) `ExecutionQueue.processQueue()` 串行消费 `pendingQueue`
|
||||||
|
4) `executePipeline()` 会读取 Deployment 与关联 Project,获取 `projectDir`,然后创建 `PipelineRunner` 执行
|
||||||
|
5) 定时轮询:每 30 秒扫描 DB 中的 pending 任务,若不在 `runningDeployments` 集合则补入队列
|
||||||
|
|
||||||
|
## 6. 详细设计(Detailed Design)
|
||||||
|
|
||||||
|
### 6.1 接口/API 设计
|
||||||
|
|
||||||
|
#### 创建部署
|
||||||
|
|
||||||
|
- POST `/api/deployments`
|
||||||
|
- 行为:创建 Deployment 后立即入队
|
||||||
|
|
||||||
|
#### 重试部署
|
||||||
|
|
||||||
|
- POST `/api/deployments/:id/retry`
|
||||||
|
- 行为:读取原 deployment -> 创建新 deployment(status=pending)-> 入队
|
||||||
|
- 语义:重试是“创建一个新的任务实例”,便于保留历史执行记录
|
||||||
|
|
||||||
|
### 6.2 数据模型/数据库
|
||||||
|
|
||||||
|
Deployment 当前关键字段(见 schema 注释):
|
||||||
|
|
||||||
|
- `status`: pending/running/success/failed/cancelled(目前入队依赖 pending)
|
||||||
|
- `buildLog`: 执行日志(当前创建时写空字符串)
|
||||||
|
- `startedAt`/`finishedAt`: 时间标记(目前 created 时 startedAt 默认 now)
|
||||||
|
|
||||||
|
建议补齐/明确(文档层面约束,代码后续落地):
|
||||||
|
|
||||||
|
- 状态迁移:
|
||||||
|
- `pending` -> `running`(开始执行前)
|
||||||
|
- `running` -> `success|failed|cancelled`(结束后)
|
||||||
|
- 幂等控制:
|
||||||
|
- 以 deploymentId 为“单次执行唯一标识”,同一个 deploymentId 不允许重复开始执行
|
||||||
|
|
||||||
|
### 6.3 队列/轮询/并发
|
||||||
|
|
||||||
|
现状:
|
||||||
|
|
||||||
|
- `runningDeployments` 同时承担“已入队/执行中”的去重集合
|
||||||
|
- `pendingQueue` 为内存 FIFO
|
||||||
|
- 单实例串行消费(`processQueue` while 循环)
|
||||||
|
- 轮询间隔常量 30 秒
|
||||||
|
|
||||||
|
风险点(需要在文档中明确约束/后续逐步修正):
|
||||||
|
|
||||||
|
- 多实例部署:如果将来启动多个 server 实例,每个实例都可能轮询到同一条 pending 记录并执行(需要 DB 锁/租约/状态原子更新)
|
||||||
|
- 状态更新缺口:当前 `ExecutionQueue` 代码中没有看到明确把 status 从 pending 改成 running/failed/success 的逻辑(可能在 `PipelineRunner` 内处理;若没有,需要补齐)
|
||||||
|
|
||||||
|
建议(不改变整体架构前提):
|
||||||
|
|
||||||
|
- 将轮询间隔改为可配置 env:`EXECUTION_POLL_INTERVAL_MS`(默认 30000)
|
||||||
|
- 在真正执行前做一次 DB 原子“抢占”:仅当 status=pending 时更新为 running(并记录开始时间);更新失败则放弃执行
|
||||||
|
|
||||||
|
### 6.4 可观测性
|
||||||
|
|
||||||
|
最低要求(建议后续落地到代码/日志规范):
|
||||||
|
|
||||||
|
- 日志字段:deploymentId、pipelineId、projectId、projectDir、status
|
||||||
|
- 队列指标:pendingQueue length、runningDeployments size
|
||||||
|
- 失败记录:捕获异常 message/stack(避免泄露敏感信息)
|
||||||
|
|
||||||
|
### 6.5 安全与权限
|
||||||
|
|
||||||
|
当前接口层面需要确认:
|
||||||
|
|
||||||
|
- `/api/deployments` 与 `/api/deployments/:id/retry` 是否需要登录/鉴权(取决于 middleware 配置)
|
||||||
|
- 若需要鉴权:建议限制为有项目权限的用户才能创建/重试部署
|
||||||
|
|
||||||
|
## 7. 影响与权衡(Trade-offs)
|
||||||
|
|
||||||
|
- 继续采用内存队列:实现简单,但天然不支持多实例并发安全
|
||||||
|
- DB 轮询恢复:可靠性提升,但会带来额外 DB 查询压力
|
||||||
|
|
||||||
|
## 8. 兼容性与迁移(Compatibility & Migration)
|
||||||
|
|
||||||
|
- 文档层面不破坏现有 API
|
||||||
|
- 若引入 status 原子抢占,需要确保旧数据/旧状态兼容(例如对历史 pending 记录仍可恢复)
|
||||||
|
|
||||||
|
## 9. 测试计划(Test Plan)
|
||||||
|
|
||||||
|
- 集成链路:创建 deployment -> 入队 -> 触发执行(可用假 runner)
|
||||||
|
- 重启恢复:插入 pending 记录 -> initialize() -> 任务被 addTask
|
||||||
|
- 重试接口:原记录存在/不存在的分支
|
||||||
|
|
||||||
|
## 10. 发布计划(Rollout Plan)
|
||||||
|
|
||||||
|
- 先补齐文档 + 最小日志规范
|
||||||
|
- 再逐步落地:status 原子抢占 + 轮询间隔 env
|
||||||
|
|
||||||
|
## 11. 备选方案(Alternatives Considered)
|
||||||
|
|
||||||
|
- 引入 Redis 队列(BullMQ 等):更可靠、支持多实例,但复杂度上升
|
||||||
|
- 使用 DB 作为队列(表 + 锁/租约):更可靠,但需要严格的并发控制
|
||||||
|
|
||||||
|
## 12. 风险与开放问题(Risks & Open Questions)
|
||||||
|
|
||||||
|
- Q1:`PipelineRunner` 是否负责更新 Deployment.status?如果没有,状态机应由谁维护?
|
||||||
|
- Q2:服务是否计划多实例部署?如果是,必须补齐“抢占执行”机制
|
||||||
|
|
||||||
|
## 13. 附录(Appendix)
|
||||||
|
|
||||||
|
- 代码:`apps/server/libs/execution-queue.ts`
|
||||||
|
- 控制器:`apps/server/controllers/deployment/index.ts`
|
||||||
|
- Schema:`apps/server/prisma/schema.prisma`
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: 设计文档 0005 - 部署流程重构(移除稀疏检出 & 环境预设)
|
||||||
|
summary: 调整部署相关能力:移除稀疏检出;将部署环境从创建时输入改为在项目设置中预设。
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
reviewers:
|
||||||
|
- team: frontend
|
||||||
|
status: draft
|
||||||
|
date: 2026-01-03
|
||||||
|
version: 0.1.0
|
||||||
|
related:
|
||||||
|
- docs: docs/api/endpoints.md
|
||||||
|
- schema: apps/server/prisma/schema.prisma
|
||||||
|
---
|
||||||
|
|
||||||
|
# 设计文档 0005:部署流程重构(移除稀疏检出 & 环境预设)
|
||||||
|
|
||||||
|
## 1. 背景(Context)
|
||||||
|
|
||||||
|
当前部署流程在“项目详情页发起部署”时包含“稀疏检出(sparse checkout)”表单项,并且流水线模板中也包含与稀疏检出相关的逻辑。
|
||||||
|
|
||||||
|
另外,部署时需要指定环境变量(例如 env),但目前是在“创建部署”时临时输入/选择。随着项目数量增加,这种方式容易造成不一致与误操作。
|
||||||
|
|
||||||
|
## 2. 目标(Goals)
|
||||||
|
|
||||||
|
- [ ] 移除项目详情页部署表单中的“稀疏检出”相关输入项
|
||||||
|
- [ ] 移除流水线模板中与稀疏检出相关的代码逻辑(后端模板/生成逻辑)
|
||||||
|
- [ ] 将“部署环境(env)”从创建部署时指定,调整为在“项目设置”中提前预设
|
||||||
|
- [ ] 创建部署时仍需要选择/指定环境,但选项来源于项目设置中的预设项
|
||||||
|
|
||||||
|
## 3. 非目标(Non-goals)
|
||||||
|
|
||||||
|
- 不新增多维度环境变量管理(仅覆盖本次提到的 env 单项预设)
|
||||||
|
- 不在本次引入复杂的环境权限、审批流
|
||||||
|
|
||||||
|
## 4. 需求与范围(Requirements & Scope)
|
||||||
|
|
||||||
|
### 4.1 移除稀疏检出
|
||||||
|
|
||||||
|
#### 用户侧
|
||||||
|
|
||||||
|
- 项目详情页发起部署时:不再展示/提交稀疏检出字段
|
||||||
|
|
||||||
|
#### 系统侧
|
||||||
|
|
||||||
|
- 流水线模板:移除任何基于稀疏检出路径的生成/执行逻辑
|
||||||
|
|
||||||
|
> 说明:当前 DB 中 Deployment 仍存在 `sparseCheckoutPaths` 字段(见 `schema.prisma`),本次需求仅明确“功能不再需要”。字段是否删除/迁移由本设计后续章节确定。
|
||||||
|
|
||||||
|
### 4.2 部署环境 env 改为项目设置预设
|
||||||
|
|
||||||
|
#### 核心约束
|
||||||
|
|
||||||
|
- 环境变量预设需要支持多选、单选、输入框这几种类型
|
||||||
|
- 在项目设置中新增可配置项(预设项):
|
||||||
|
例如:指定env 环境变量
|
||||||
|
- 类型:单选(single select)
|
||||||
|
- key:`env`,value 及时部署是选中的候选项的值
|
||||||
|
- options:`staging`(测试环境)、`production`(生产环境)
|
||||||
|
|
||||||
|
#### 行为
|
||||||
|
|
||||||
|
- 创建部署时仍需指定环境(env),但:
|
||||||
|
- 不再由用户自由输入
|
||||||
|
- 只允许从该项目预设的 options 中选择
|
||||||
|
|
||||||
|
## 5. 影响面(Impact)
|
||||||
|
|
||||||
|
### 5.1 前端
|
||||||
|
|
||||||
|
- 项目详情页部署表单:移除“稀疏检出”相关 UI 与字段提交
|
||||||
|
- 项目设置页:新增“环境预设(env)”配置入口(单选 + 选项 staging/production)
|
||||||
|
- 创建部署交互:环境选项从项目设置读取(不再硬编码/临时输入)
|
||||||
|
|
||||||
|
### 5.2 后端
|
||||||
|
|
||||||
|
- 部署创建接口:校验 env 必须来自项目预设(避免非法 env)
|
||||||
|
- 流水线模板:移除稀疏检出相关的模板字段/生成逻辑
|
||||||
|
|
||||||
|
### 5.3 数据库
|
||||||
|
|
||||||
|
- 需要新增“项目设置/项目配置”承载 env 预设(落库方案待定)
|
||||||
|
- 既有 Deployment 的 `sparseCheckoutPaths` 字段:后续决定是否保留(兼容历史)或迁移删除
|
||||||
|
|
||||||
|
## 6. 兼容性与迁移(Compatibility & Migration)
|
||||||
|
|
||||||
|
- 对历史部署记录:
|
||||||
|
- 若存在 `sparseCheckoutPaths`,不影响查询展示,但新建部署不再写入该字段
|
||||||
|
- 对创建部署:
|
||||||
|
- 若项目未配置 env 预设:创建部署应失败并提示先到项目设置配置(或提供默认值策略,待确认)
|
||||||
|
|
||||||
|
## 7. 测试要点(Test Plan)
|
||||||
|
|
||||||
|
- 前端:
|
||||||
|
- 项目详情页部署表单不再出现稀疏检出项
|
||||||
|
- 项目设置可保存 env 预设(单选)并在创建部署时正确展示
|
||||||
|
- 后端:
|
||||||
|
- 创建部署:env 不在项目预设 options 内时应拒绝
|
||||||
|
- 流水线模板:移除稀疏检出后仍能正常创建并执行
|
||||||
|
|
||||||
|
## 8. 实施状态(Implementation Status)
|
||||||
|
|
||||||
|
### 已完成(后端)
|
||||||
|
|
||||||
|
- ✅ Prisma Schema:在 Project 表添加 `envPresets` 字段(String? 类型,存储 JSON)
|
||||||
|
- ✅ 移除部署创建/重试接口中的 `sparseCheckoutPaths` 写入
|
||||||
|
- ✅ 在部署创建接口添加环境校验:验证 env 是否在项目 envPresets 的 options 中
|
||||||
|
- ✅ 更新 project DTO 和 controller 支持 envPresets 读写
|
||||||
|
- ✅ 移除 pipeline-runner 中的 `SPARSE_CHECKOUT_PATHS` 环境变量
|
||||||
|
- ✅ 生成 Prisma Client
|
||||||
|
|
||||||
|
### 已完成(前端)
|
||||||
|
|
||||||
|
- ✅ 创建 EnvPresetsEditor 组件(支持单选、多选、输入框类型)
|
||||||
|
- ✅ 在 CreateProjectModal 和 EditProjectModal 中集成环境预设编辑器
|
||||||
|
- ✅ 从 DeployModal 移除稀疏检出表单项
|
||||||
|
- ✅ 在 DeployModal 中从项目 envPresets 读取环境选项并展示
|
||||||
|
- ✅ 移除 DeployModal 中的动态环境变量列表(envVars Form.List)
|
||||||
|
- ✅ 从类型定义中移除 sparseCheckoutPaths 字段
|
||||||
|
- ✅ 在项目详情页项目设置 tab 中添加环境变量预设的查看和编辑功能
|
||||||
|
|
||||||
|
### 待定问题
|
||||||
|
|
||||||
|
- Q1:项目设置存储方式 → **已决定**:使用 Project.envPresets JSON 字段
|
||||||
|
- Q2:未配置 env 预设的默认行为 → **已实现**:若配置了预设则校验,否则允许任意值(向后兼容)
|
||||||
|
- Q3:Deployment.sparseCheckoutPaths 字段 → **已决定**:保留字段(兼容历史),但新建部署不再写入
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
所有 notable 更改应在此记录。遵循 Keep a Changelog 格式。
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
- 初始文档目录建立。
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-01-03
|
||||||
|
- 初始发布
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: 开发与本地环境搭建
|
||||||
|
summary: 针对本项目的本地开发、数据库与调试指南。
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
status: stable
|
||||||
|
---
|
||||||
|
|
||||||
|
# 开发与本地环境搭建
|
||||||
|
|
||||||
|
## 1. 安装依赖
|
||||||
|
|
||||||
|
建议使用 `pnpm` 管理工作区依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在仓库根
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
或者只在 server 子包安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 生成 Prisma Client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
如果需要执行迁移(开发场景):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev --name init
|
||||||
|
```
|
||||||
|
|
||||||
|
数据库:项目使用 SQLite(见 `apps/server/prisma/schema.prisma`),迁移会在本地创建 `.db` 文件。
|
||||||
|
|
||||||
|
## 3. 启动服务
|
||||||
|
|
||||||
|
并行启动 workspace 中所有 dev 脚本(推荐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
或单独启动 server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
服务默认端口:`3001`。如需修改:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=4000 pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 常见开发命令
|
||||||
|
|
||||||
|
- 运行测试脚本(仓库自带):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server
|
||||||
|
node test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
- TypeScript 类型检查(本地可使用 `tsc`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 环境变量与第三方集成
|
||||||
|
|
||||||
|
常见 env:`GITEA_URL`, `GITEA_CLIENT_ID`, `GITEA_REDIRECT_URI`, `PORT`, `NODE_ENV`。
|
||||||
|
|
||||||
|
登录采用 Gitea OAuth,未配置时某些 auth 接口会返回 401,需要先登录获取 session。
|
||||||
|
|
||||||
|
## 6. 运行与调试要点
|
||||||
|
|
||||||
|
- 代码通过装饰器注册路由(见 `apps/server/decorators/route.ts` 与 `apps/server/libs/route-scanner.ts`)。
|
||||||
|
- Prisma client 生成路径:`apps/server/generated`。
|
||||||
|
- 若变更 Prisma schema,请执行 `prisma generate` 并更新迁移。
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: 快速开始
|
||||||
|
summary: 本文档介绍如何在本地启动与运行项目的基础步骤。
|
||||||
|
tags: [getting-started]
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
status: stable
|
||||||
|
version: 1.0.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# 快速开始
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
- Node.js >= 18
|
||||||
|
- pnpm
|
||||||
|
- 克隆权限(或访问仓库)
|
||||||
|
|
||||||
|
## 克隆与依赖安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repo-url>
|
||||||
|
cd foka-ci
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:仓库使用 pnpm workspace,根目录脚本 `pnpm dev` 会并行启动工作区内的 `dev` 脚本。
|
||||||
|
|
||||||
|
## 运行服务(开发)
|
||||||
|
|
||||||
|
- 从仓库根(并行运行所有 dev 脚本):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- 单独运行 server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server
|
||||||
|
pnpm install
|
||||||
|
pnpm dev # 等同于: tsx watch ./app.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
服务器默认监听端口 `3001`(可通过 `PORT` 环境变量覆盖)。API 前缀为 `/api`。
|
||||||
|
|
||||||
|
## Prisma 与数据库
|
||||||
|
|
||||||
|
项目使用 SQLite(见 `apps/server/prisma/schema.prisma`)。如果需要生成 Prisma Client 或运行迁移:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server
|
||||||
|
npx prisma generate
|
||||||
|
npx prisma migrate dev --name init # 本地开发使用
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的 Prisma Client 位于 `apps/server/generated`(由 schema 中的 generator 指定)。
|
||||||
|
|
||||||
|
## 环境变量(常用)
|
||||||
|
|
||||||
|
- `GITEA_URL`、`GITEA_CLIENT_ID`、`GITEA_REDIRECT_URI`:用于 OAuth 登录(Gitea)。
|
||||||
|
- `PORT`:服务监听端口,默认 `3001`。
|
||||||
|
- `NODE_ENV`:环境(development/production)。
|
||||||
|
|
||||||
|
将敏感值放入 `.env`(不要将 `.env` 提交到仓库)。
|
||||||
|
|
||||||
|
## 运行脚本与测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/server
|
||||||
|
node test.js # 运行仓库自带的简单测试脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 其他说明
|
||||||
|
|
||||||
|
- 文档目录位于 `docs/`,设计模板在 `docs/.meta/templates/`。
|
||||||
|
- API 路由由装饰器注册,路由前缀为 `/api`,在 `apps/server/middlewares/router.ts` 中可查看。
|
||||||
|
|
||||||
|
更多开发细节请参见 `docs/development/setup.md` 与 `docs/api/endpoints.md`。
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: 项目文档总览
|
||||||
|
summary: 项目文档的导航页,指向快速开始、架构、API、运维等部分。
|
||||||
|
tags: [overview]
|
||||||
|
owners:
|
||||||
|
- team: backend
|
||||||
|
status: stable
|
||||||
|
version: 1.0.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# 文档总览
|
||||||
|
|
||||||
|
欢迎来到项目文档。下列是常用文档的入口:
|
||||||
|
|
||||||
|
- Getting Started: ./getting-started.md
|
||||||
|
- Architecture: ./architecture/adr-0001-service-design.md
|
||||||
|
- API: ./api/README.md
|
||||||
|
- Runbooks: ./runbooks/incident-response.md
|
||||||
|
- Onboarding: ./onboarding/new-hire.md
|
||||||
|
- Changelog: ./changelogs/CHANGELOG.md
|
||||||
|
|
||||||
|
维护说明:请在变更同时更新 `owners` 与 `version`,并通过 PR 提交。
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
title: 新成员入职指南
|
||||||
|
owners:
|
||||||
|
- people-team
|
||||||
|
status: draft
|
||||||
|
---
|
||||||
|
|
||||||
|
# 新成员入职快速清单
|
||||||
|
|
||||||
|
1. 获取公司邮箱与账号
|
||||||
|
2. 克隆仓库并运行 `pnpm install`
|
||||||
|
3. 阅读 `docs/getting-started.md` 与 `docs/architecture` 中关键 ADR
|
||||||
|
4. 约见导师并完成第一次 code walkthrough
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: 事故响应 Runbook
|
||||||
|
summary: 处理生产事故的步骤和联系方式。
|
||||||
|
owners:
|
||||||
|
- ops-team
|
||||||
|
status: stable
|
||||||
|
---
|
||||||
|
|
||||||
|
# 事故响应(Incident Response)
|
||||||
|
|
||||||
|
## 1. 识别与分级
|
||||||
|
|
||||||
|
- P0: 系统不可用或数据安全泄露
|
||||||
|
- P1: 主要功能严重受损
|
||||||
|
|
||||||
|
## 2. 通知
|
||||||
|
|
||||||
|
- 联系人:`on-call@company.example`,电话:+86-10-12345678
|
||||||
|
|
||||||
|
## 3. 暂时性缓解
|
||||||
|
|
||||||
|
- 回滚最近的部署
|
||||||
|
- 启用备用服务
|
||||||
|
|
||||||
|
## 4. 根因分析与恢复
|
||||||
|
|
||||||
|
- 记录时间线
|
||||||
|
- 生成 RCA 并在 72 小时内发布
|
||||||
+5
-1
@@ -8,7 +8,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.6"
|
"@biomejs/biome": "2.0.6"
|
||||||
},
|
},
|
||||||
"keywords": ["ci", "ark", "ark-ci"],
|
"keywords": [
|
||||||
|
"ci",
|
||||||
|
"ark",
|
||||||
|
"ark-ci"
|
||||||
|
],
|
||||||
"author": "hurole",
|
"author": "hurole",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
|
"packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
|
||||||
|
|||||||
Reference in New Issue
Block a user