Compare commits
2 Commits
c40532c757
...
a067d167e9
| Author | SHA1 | Date | |
|---|---|---|---|
| a067d167e9 | |||
| d22fdc9618 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ dist/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
|
||||
.env
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Koa from 'koa';
|
||||
import { initMiddlewares } from './middlewares/index.ts';
|
||||
import { log } from './libs/logger.ts';
|
||||
import { ExecutionQueue } from './libs/execution-queue.ts';
|
||||
import { log } from './libs/logger.ts';
|
||||
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
||||
import { initMiddlewares } from './middlewares/index.ts';
|
||||
|
||||
// 初始化应用
|
||||
async function initializeApp() {
|
||||
@@ -26,7 +26,7 @@ async function initializeApp() {
|
||||
}
|
||||
|
||||
// 启动应用
|
||||
initializeApp().catch(error => {
|
||||
initializeApp().catch((error) => {
|
||||
console.error('Failed to start application:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Context } from 'koa';
|
||||
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 { log } from '../../libs/logger.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { loginSchema } from './dto.ts';
|
||||
|
||||
@Controller('/auth')
|
||||
|
||||
@@ -12,8 +12,7 @@ export const createDeploymentSchema = z.object({
|
||||
branch: z.string().min(1, { message: '分支不能为空' }),
|
||||
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
||||
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
||||
env: z.string().optional(),
|
||||
sparseCheckoutPaths: z.string().optional(), // 添加稀疏检出路径字段
|
||||
envVars: z.record(z.string(), z.string()).optional(), // 环境变量 key-value 对象
|
||||
});
|
||||
|
||||
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 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 { prisma } from '../../libs/prisma.ts';
|
||||
import { createDeploymentSchema, listDeploymentsQuerySchema } from './dto.ts';
|
||||
|
||||
@Controller('/deployments')
|
||||
export class DeploymentController {
|
||||
@Get('')
|
||||
async list(ctx: Context) {
|
||||
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(ctx.query);
|
||||
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(
|
||||
ctx.query,
|
||||
);
|
||||
const where: Prisma.DeploymentWhereInput = {
|
||||
valid: 1,
|
||||
};
|
||||
@@ -50,8 +52,7 @@ export class DeploymentController {
|
||||
connect: { id: body.projectId },
|
||||
},
|
||||
pipelineId: body.pipelineId,
|
||||
env: body.env || 'dev',
|
||||
sparseCheckoutPaths: body.sparseCheckoutPaths || '', // 添加稀疏检出路径
|
||||
envVars: body.envVars ? JSON.stringify(body.envVars) : null,
|
||||
buildLog: '',
|
||||
createdBy: 'system', // TODO: get from user
|
||||
updatedBy: 'system',
|
||||
@@ -73,7 +74,7 @@ export class DeploymentController {
|
||||
|
||||
// 获取原始部署记录
|
||||
const originalDeployment = await prisma.deployment.findUnique({
|
||||
where: { id: Number(id) }
|
||||
where: { id: Number(id) },
|
||||
});
|
||||
|
||||
if (!originalDeployment) {
|
||||
@@ -82,7 +83,7 @@ export class DeploymentController {
|
||||
code: 404,
|
||||
message: '部署记录不存在',
|
||||
data: null,
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -96,8 +97,7 @@ export class DeploymentController {
|
||||
status: 'pending',
|
||||
projectId: originalDeployment.projectId,
|
||||
pipelineId: originalDeployment.pipelineId,
|
||||
env: originalDeployment.env,
|
||||
sparseCheckoutPaths: originalDeployment.sparseCheckoutPaths,
|
||||
envVars: originalDeployment.envVars,
|
||||
buildLog: '',
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
@@ -113,7 +113,7 @@ export class DeploymentController {
|
||||
code: 0,
|
||||
message: '重新执行任务已创建',
|
||||
data: newDeployment,
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
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>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Context } from 'koa';
|
||||
import { Controller, Get } from '../../decorators/route.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { gitea } from '../../libs/gitea.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { getCommitsQuerySchema, getBranchesQuerySchema } from './dto.ts';
|
||||
import { getBranchesQuerySchema, getCommitsQuerySchema } from './dto.ts';
|
||||
|
||||
@Controller('/git')
|
||||
export class GitController {
|
||||
@@ -33,7 +33,11 @@ export class GitController {
|
||||
console.log('Access token present:', !!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 {
|
||||
@@ -65,7 +69,11 @@ export class GitController {
|
||||
const accessToken = ctx.session?.gitea?.access_token;
|
||||
|
||||
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 {
|
||||
@@ -85,7 +93,7 @@ export class GitController {
|
||||
|
||||
// Handle SCP-like syntax: git@host:owner/repo.git
|
||||
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
|
||||
const scpMatch = cleanUrl.match(/:([^\/]+)\/([^\/]+?)(\.git)?$/);
|
||||
const scpMatch = cleanUrl.match(/:([^/]+)\/([^/]+?)(\.git)?$/);
|
||||
if (scpMatch) {
|
||||
return { owner: scpMatch[1], repo: scpMatch[2] };
|
||||
}
|
||||
@@ -96,13 +104,15 @@ export class GitController {
|
||||
const urlObj = new URL(cleanUrl);
|
||||
const parts = urlObj.pathname.split('/').filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
const repo = parts.pop()!.replace(/\.git$/, '');
|
||||
const owner = parts.pop()!;
|
||||
return { owner, repo };
|
||||
const repo = parts.pop()?.replace(/\.git$/, '');
|
||||
const owner = parts.pop();
|
||||
if (repo && owner) {
|
||||
return { owner, repo };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Fallback to simple regex
|
||||
const match = cleanUrl.match(/([^\/]+)\/([^\/]+?)(\.git)?$/);
|
||||
const match = cleanUrl.match(/([^/]+)\/([^/]+?)(\.git)?$/);
|
||||
if (match) {
|
||||
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 { DeploymentController } from './deployment/index.ts';
|
||||
export { PipelineController } from './pipeline/index.ts';
|
||||
export { StepController } from './step/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({
|
||||
name: z.string({
|
||||
message: '流水线名称必须是字符串',
|
||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }),
|
||||
name: z
|
||||
.string({
|
||||
message: '流水线名称必须是字符串',
|
||||
})
|
||||
.min(1, { message: '流水线名称不能为空' })
|
||||
.max(100, { message: '流水线名称不能超过100个字符' }),
|
||||
|
||||
description: z.string({
|
||||
message: '流水线描述必须是字符串',
|
||||
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
||||
description: z
|
||||
.string({
|
||||
message: '流水线描述必须是字符串',
|
||||
})
|
||||
.max(500, { message: '流水线描述不能超过500个字符' })
|
||||
.optional(),
|
||||
|
||||
projectId: z.number({
|
||||
message: '项目ID必须是数字',
|
||||
}).int().positive({ message: '项目ID必须是正整数' }).optional(),
|
||||
projectId: z
|
||||
.number({
|
||||
message: '项目ID必须是数字',
|
||||
})
|
||||
.int()
|
||||
.positive({ message: '项目ID必须是正整数' })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const updatePipelineSchema = z.object({
|
||||
name: z.string({
|
||||
message: '流水线名称必须是字符串',
|
||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }).optional(),
|
||||
name: z
|
||||
.string({
|
||||
message: '流水线名称必须是字符串',
|
||||
})
|
||||
.min(1, { message: '流水线名称不能为空' })
|
||||
.max(100, { message: '流水线名称不能超过100个字符' })
|
||||
.optional(),
|
||||
|
||||
description: z.string({
|
||||
message: '流水线描述必须是字符串',
|
||||
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
||||
description: z
|
||||
.string({
|
||||
message: '流水线描述必须是字符串',
|
||||
})
|
||||
.max(500, { message: '流水线描述不能超过500个字符' })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const pipelineIdSchema = z.object({
|
||||
id: z.coerce.number().int().positive({ message: '流水线 ID 必须是正整数' }),
|
||||
});
|
||||
|
||||
export const listPipelinesQuerySchema = z.object({
|
||||
projectId: z.coerce.number().int().positive({ message: '项目ID必须是正整数' }).optional(),
|
||||
}).optional();
|
||||
export const listPipelinesQuerySchema = z
|
||||
.object({
|
||||
projectId: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive({ message: '项目ID必须是正整数' })
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
// 类型
|
||||
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { Context } from 'koa';
|
||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.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 { getAvailableTemplates, createPipelineFromTemplate } from '../../libs/pipeline-template.ts';
|
||||
import {
|
||||
createPipelineSchema,
|
||||
updatePipelineSchema,
|
||||
pipelineIdSchema,
|
||||
listPipelinesQuerySchema,
|
||||
pipelineIdSchema,
|
||||
updatePipelineSchema,
|
||||
} from './dto.ts';
|
||||
|
||||
@Controller('/pipelines')
|
||||
@@ -46,7 +49,7 @@ export class PipelineController {
|
||||
|
||||
// GET /api/pipelines/templates - 获取可用的流水线模板
|
||||
@Get('/templates')
|
||||
async getTemplates(ctx: Context) {
|
||||
async getTemplates(_ctx: Context) {
|
||||
try {
|
||||
const templates = await getAvailableTemplates();
|
||||
return templates;
|
||||
@@ -126,7 +129,7 @@ export class PipelineController {
|
||||
templateId,
|
||||
projectId,
|
||||
name,
|
||||
description || ''
|
||||
description || '',
|
||||
);
|
||||
|
||||
// 返回新创建的流水线
|
||||
|
||||
@@ -5,46 +5,83 @@ import { projectDirSchema } from '../../libs/path-validator.js';
|
||||
* 创建项目验证架构
|
||||
*/
|
||||
export const createProjectSchema = z.object({
|
||||
name: z.string({
|
||||
message: '项目名称必须是字符串',
|
||||
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }),
|
||||
name: z
|
||||
.string({
|
||||
message: '项目名称必须是字符串',
|
||||
})
|
||||
.min(2, { message: '项目名称至少2个字符' })
|
||||
.max(50, { message: '项目名称不能超过50个字符' }),
|
||||
|
||||
description: z.string({
|
||||
message: '项目描述必须是字符串',
|
||||
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
||||
description: z
|
||||
.string({
|
||||
message: '项目描述必须是字符串',
|
||||
})
|
||||
.max(200, { message: '项目描述不能超过200个字符' })
|
||||
.optional(),
|
||||
|
||||
repository: z.string({
|
||||
message: '仓库地址必须是字符串',
|
||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }),
|
||||
repository: z
|
||||
.string({
|
||||
message: '仓库地址必须是字符串',
|
||||
})
|
||||
.url({ message: '请输入有效的仓库地址' })
|
||||
.min(1, { message: '仓库地址不能为空' }),
|
||||
|
||||
projectDir: projectDirSchema,
|
||||
|
||||
envPresets: z.string().optional(), // JSON 字符串格式
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新项目验证架构
|
||||
*/
|
||||
export const updateProjectSchema = z.object({
|
||||
name: z.string({
|
||||
message: '项目名称必须是字符串',
|
||||
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }).optional(),
|
||||
name: z
|
||||
.string({
|
||||
message: '项目名称必须是字符串',
|
||||
})
|
||||
.min(2, { message: '项目名称至少2个字符' })
|
||||
.max(50, { message: '项目名称不能超过50个字符' })
|
||||
.optional(),
|
||||
|
||||
description: z.string({
|
||||
message: '项目描述必须是字符串',
|
||||
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
||||
description: z
|
||||
.string({
|
||||
message: '项目描述必须是字符串',
|
||||
})
|
||||
.max(200, { message: '项目描述不能超过200个字符' })
|
||||
.optional(),
|
||||
|
||||
repository: z.string({
|
||||
message: '仓库地址必须是字符串',
|
||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }).optional(),
|
||||
repository: z
|
||||
.string({
|
||||
message: '仓库地址必须是字符串',
|
||||
})
|
||||
.url({ message: '请输入有效的仓库地址' })
|
||||
.min(1, { message: '仓库地址不能为空' })
|
||||
.optional(),
|
||||
|
||||
envPresets: z.string().optional(), // JSON 字符串格式
|
||||
});
|
||||
|
||||
/**
|
||||
* 项目列表查询参数验证架构
|
||||
*/
|
||||
export const listProjectQuerySchema = z.object({
|
||||
page: z.coerce.number().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();
|
||||
export const listProjectQuerySchema = z
|
||||
.object({
|
||||
page: z.coerce
|
||||
.number()
|
||||
.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验证架构
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Context } from 'koa';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { log } from '../../libs/logger.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.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 {
|
||||
createProjectSchema,
|
||||
updateProjectSchema,
|
||||
listProjectQuerySchema,
|
||||
projectIdSchema,
|
||||
updateProjectSchema,
|
||||
} from './dto.ts';
|
||||
|
||||
@Controller('/projects')
|
||||
@@ -135,6 +135,7 @@ export class ProjectController {
|
||||
description: validatedData.description || '',
|
||||
repository: validatedData.repository,
|
||||
projectDir: validatedData.projectDir,
|
||||
envPresets: validatedData.envPresets,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
@@ -182,6 +183,9 @@ export class ProjectController {
|
||||
if (validatedData.repository !== undefined) {
|
||||
updateData.repository = validatedData.repository;
|
||||
}
|
||||
if (validatedData.envPresets !== undefined) {
|
||||
updateData.envPresets = validatedData.envPresets;
|
||||
}
|
||||
|
||||
const project = await prisma.project.update({
|
||||
where: { id },
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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 { prisma } from '../../libs/prisma.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import {
|
||||
createStepSchema,
|
||||
updateStepSchema,
|
||||
stepIdSchema,
|
||||
listStepsQuerySchema,
|
||||
stepIdSchema,
|
||||
updateStepSchema,
|
||||
} from './dto.ts';
|
||||
|
||||
@Controller('/steps')
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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 {
|
||||
userIdSchema,
|
||||
createUserSchema,
|
||||
updateUserSchema,
|
||||
searchUserQuerySchema,
|
||||
updateUserSchema,
|
||||
userIdSchema,
|
||||
} from './dto.ts';
|
||||
|
||||
/**
|
||||
@@ -13,14 +13,18 @@ import {
|
||||
*/
|
||||
@Controller('/user')
|
||||
export class UserController {
|
||||
|
||||
@Get('/list')
|
||||
async list(ctx: Context) {
|
||||
async list(_ctx: Context) {
|
||||
// 模拟用户列表数据
|
||||
const users = [
|
||||
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
||||
{ 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;
|
||||
@@ -33,10 +37,10 @@ export class UserController {
|
||||
// 模拟根据ID查找用户
|
||||
const user = {
|
||||
id,
|
||||
name: 'User ' + id,
|
||||
name: `User ${id}`,
|
||||
email: `user${id}@example.com`,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString()
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (id > 100) {
|
||||
@@ -55,7 +59,7 @@ export class UserController {
|
||||
id: Date.now(),
|
||||
...body,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: body.status
|
||||
status: body.status,
|
||||
};
|
||||
|
||||
return newUser;
|
||||
@@ -70,7 +74,7 @@ export class UserController {
|
||||
const updatedUser = {
|
||||
id,
|
||||
...body,
|
||||
updatedAt: new Date().toISOString()
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return updatedUser;
|
||||
@@ -88,7 +92,7 @@ export class UserController {
|
||||
return {
|
||||
success: true,
|
||||
message: `用户 ${id} 已删除`,
|
||||
deletedAt: new Date().toISOString()
|
||||
deletedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,25 +103,26 @@ export class UserController {
|
||||
// 模拟搜索逻辑
|
||||
let results = [
|
||||
{ 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) {
|
||||
results = results.filter(user =>
|
||||
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(keyword.toLowerCase())
|
||||
results = results.filter(
|
||||
(user) =>
|
||||
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(keyword.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
results = results.filter(user => user.status === status);
|
||||
results = results.filter((user) => user.status === status);
|
||||
}
|
||||
|
||||
return {
|
||||
keyword,
|
||||
status,
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -43,24 +50,28 @@ function getMetadata<T = any>(key: string | symbol, target: any): T | undefined
|
||||
* 创建HTTP方法装饰器的工厂函数(TC39标准)
|
||||
*/
|
||||
function createMethodDecorator(method: HttpMethod) {
|
||||
return function (path: string = '') {
|
||||
return function <This, Args extends any[], Return>(
|
||||
return (path: string = '') =>
|
||||
<This, Args extends any[], 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 () {
|
||||
// 使用 this.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 = {
|
||||
method,
|
||||
path,
|
||||
propertyKey: String(context.name)
|
||||
propertyKey: String(context.name),
|
||||
};
|
||||
|
||||
existingRoutes.push(newRoute);
|
||||
@@ -71,7 +82,6 @@ function createMethodDecorator(method: HttpMethod) {
|
||||
|
||||
return target;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,10 +119,10 @@ export const Patch = createMethodDecorator('PATCH');
|
||||
* @param prefix 路由前缀
|
||||
*/
|
||||
export function Controller(prefix: string = '') {
|
||||
return function <T extends abstract new (...args: any) => any>(
|
||||
return <T extends abstract new (...args: any) => any>(
|
||||
target: T,
|
||||
context: ClassDecoratorContext<T>
|
||||
) {
|
||||
context: ClassDecoratorContext<T>,
|
||||
) => {
|
||||
// 在类初始化时保存控制器前缀
|
||||
context.addInitializer(function () {
|
||||
setMetadata('prefix', prefix, this);
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* Use it to get access to models, enums, and input types.
|
||||
*
|
||||
*
|
||||
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||
* See `client.ts` for the standard, server-side entry point.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.ts'
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts';
|
||||
export { Prisma };
|
||||
export * as $Enums from './enums.ts';
|
||||
export * from './enums.ts';
|
||||
/**
|
||||
* Model Project
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Project = Prisma.ProjectModel
|
||||
export type Project = Prisma.ProjectModel;
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
export type User = Prisma.UserModel;
|
||||
/**
|
||||
* Model Pipeline
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Pipeline = Prisma.PipelineModel
|
||||
export type Pipeline = Prisma.PipelineModel;
|
||||
/**
|
||||
* Model Step
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Step = Prisma.StepModel
|
||||
export type Step = Prisma.StepModel;
|
||||
/**
|
||||
* Model Deployment
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Deployment = Prisma.DeploymentModel
|
||||
export type Deployment = Prisma.DeploymentModel;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||
@@ -10,21 +9,22 @@
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as process from 'node:process'
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
import * as path from 'node:path';
|
||||
import * as process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import * as $Class from "./internal/class.ts"
|
||||
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from "./enums.ts"
|
||||
import * as runtime from '@prisma/client/runtime/client';
|
||||
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
|
||||
*
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
@@ -32,35 +32,41 @@ export * from "./enums.ts"
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||
*/
|
||||
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 { Prisma }
|
||||
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 { Prisma };
|
||||
|
||||
/**
|
||||
* Model Project
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Project = Prisma.ProjectModel
|
||||
export type Project = Prisma.ProjectModel;
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
export type User = Prisma.UserModel;
|
||||
/**
|
||||
* Model Pipeline
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Pipeline = Prisma.PipelineModel
|
||||
export type Pipeline = Prisma.PipelineModel;
|
||||
/**
|
||||
* Model Step
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Step = Prisma.StepModel
|
||||
export type Step = Prisma.StepModel;
|
||||
/**
|
||||
* Model Deployment
|
||||
*
|
||||
*
|
||||
*/
|
||||
export type Deployment = Prisma.DeploymentModel
|
||||
export type Deployment = Prisma.DeploymentModel;
|
||||
|
||||
@@ -1,402 +1,426 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import type * as Prisma from "./internal/prismaNamespace.ts"
|
||||
|
||||
import type * as runtime from '@prisma/client/runtime/client';
|
||||
import * as $Enums from './enums.ts';
|
||||
import type * as Prisma from './internal/prismaNamespace.ts';
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||
};
|
||||
|
||||
export type StringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||
};
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | null;
|
||||
notIn?: string[] | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||
};
|
||||
|
||||
export type DateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[];
|
||||
notIn?: Date[] | string[];
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||
};
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
sort: Prisma.SortOrder;
|
||||
nulls?: Prisma.NullsOrder;
|
||||
};
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | null;
|
||||
notIn?: string[] | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
||||
| string
|
||||
| null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[];
|
||||
notIn?: Date[] | string[];
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type BoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
}
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||
};
|
||||
|
||||
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
}
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type IntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | null;
|
||||
notIn?: number[] | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||
};
|
||||
|
||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | null;
|
||||
notIn?: number[] | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
||||
| number
|
||||
| null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | null;
|
||||
notIn?: Date[] | string[] | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
| Date
|
||||
| string
|
||||
| null;
|
||||
};
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | null;
|
||||
notIn?: Date[] | string[] | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
||||
| Date
|
||||
| string
|
||||
| null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||
};
|
||||
|
||||
export type NestedStringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||
};
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | null;
|
||||
notIn?: string[] | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||
};
|
||||
|
||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[];
|
||||
notIn?: Date[] | string[];
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||
};
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number;
|
||||
};
|
||||
|
||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||
in?: string[] | null;
|
||||
notIn?: string[] | null;
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
||||
| string
|
||||
| null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | null;
|
||||
notIn?: number[] | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||
};
|
||||
|
||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
in?: Date[] | string[];
|
||||
notIn?: Date[] | string[];
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
}
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||
};
|
||||
|
||||
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
}
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | null;
|
||||
notIn?: number[] | null;
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
||||
| number
|
||||
| null;
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||
};
|
||||
|
||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null;
|
||||
in?: number[] | null;
|
||||
notIn?: number[] | null;
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null;
|
||||
};
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | null;
|
||||
notIn?: Date[] | string[] | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
| Date
|
||||
| string
|
||||
| null;
|
||||
};
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||
in?: Date[] | string[] | null;
|
||||
notIn?: Date[] | string[] | null;
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||
not?:
|
||||
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
||||
| Date
|
||||
| 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. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
// This file is empty because there are no enums in the schema.
|
||||
export {}
|
||||
export {};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
@@ -11,49 +10,58 @@
|
||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "./prismaNamespace.ts"
|
||||
|
||||
import * as runtime from '@prisma/client/runtime/client';
|
||||
import type * as Prisma from './prismaNamespace.ts';
|
||||
|
||||
const config: runtime.GetPrismaClientConfig = {
|
||||
"previewFeatures": [],
|
||||
"clientVersion": "7.0.0",
|
||||
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513",
|
||||
"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",
|
||||
"runtimeDataModel": {
|
||||
"models": {},
|
||||
"enums": {},
|
||||
"types": {}
|
||||
}
|
||||
}
|
||||
previewFeatures: [],
|
||||
clientVersion: '7.0.0',
|
||||
engineVersion: '0c19ccc313cf9911a90d99d2ac2eb0280c76c513',
|
||||
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 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',
|
||||
runtimeDataModel: {
|
||||
models: {},
|
||||
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> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||
return new WebAssembly.Module(wasmArray)
|
||||
async function decodeBase64AsWasm(
|
||||
wasmBase64: string,
|
||||
): Promise<WebAssembly.Module> {
|
||||
const { Buffer } = await import('node:buffer');
|
||||
const wasmArray = Buffer.from(wasmBase64, 'base64');
|
||||
return new WebAssembly.Module(wasmArray);
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs")
|
||||
return await decodeBase64AsWasm(wasm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const { wasm } = await import(
|
||||
'@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs'
|
||||
);
|
||||
return await decodeBase64AsWasm(wasm);
|
||||
},
|
||||
};
|
||||
|
||||
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 {
|
||||
/**
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
@@ -61,21 +69,28 @@ export interface PrismaClientConstructor {
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||
*/
|
||||
|
||||
new <
|
||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends {
|
||||
omit: infer U;
|
||||
}
|
||||
? U
|
||||
: Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends
|
||||
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||
>(
|
||||
options: Prisma.Subset<Options, Prisma.PrismaClientOptions>,
|
||||
): PrismaClient<LogOpts, OmitOpts, ExtArgs>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
@@ -83,18 +98,24 @@ export interface PrismaClientConstructor {
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||
*/
|
||||
|
||||
export interface PrismaClient<
|
||||
in LogOpts extends Prisma.LogLevel = never,
|
||||
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
|
||||
@@ -106,7 +127,7 @@ export interface PrismaClient<
|
||||
*/
|
||||
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Executes a prepared raw query and returns the number of affected rows.
|
||||
* @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).
|
||||
*/
|
||||
$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.
|
||||
@@ -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).
|
||||
*/
|
||||
$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.
|
||||
@@ -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).
|
||||
*/
|
||||
$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.
|
||||
@@ -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).
|
||||
*/
|
||||
$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.
|
||||
@@ -163,68 +195,88 @@ export interface PrismaClient<
|
||||
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* 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>, {
|
||||
extArgs: ExtArgs
|
||||
}>>
|
||||
$extends: runtime.Types.Extensions.ExtendsHook<
|
||||
'extends',
|
||||
Prisma.TypeMapCb<OmitOpts>,
|
||||
ExtArgs,
|
||||
runtime.Types.Utils.Call<
|
||||
Prisma.TypeMapCb<OmitOpts>,
|
||||
{
|
||||
extArgs: ExtArgs;
|
||||
}
|
||||
>
|
||||
>;
|
||||
|
||||
/**
|
||||
/**
|
||||
* `prisma.project`: Exposes CRUD operations for the **Project** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*/
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*/
|
||||
get project(): Prisma.ProjectDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.user`: Exposes CRUD operations for the **User** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*/
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*/
|
||||
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.pipeline`: Exposes CRUD operations for the **Pipeline** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Pipelines
|
||||
* const pipelines = await prisma.pipeline.findMany()
|
||||
* ```
|
||||
*/
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Pipelines
|
||||
* const pipelines = await prisma.pipeline.findMany()
|
||||
* ```
|
||||
*/
|
||||
get pipeline(): Prisma.PipelineDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.step`: Exposes CRUD operations for the **Step** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Steps
|
||||
* const steps = await prisma.step.findMany()
|
||||
* ```
|
||||
*/
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Steps
|
||||
* const steps = await prisma.step.findMany()
|
||||
* ```
|
||||
*/
|
||||
get step(): Prisma.StepDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.deployment`: Exposes CRUD operations for the **Deployment** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Deployments
|
||||
* const deployments = await prisma.deployment.findMany()
|
||||
* ```
|
||||
*/
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Deployments
|
||||
* const deployments = await prisma.deployment.findMany()
|
||||
* ```
|
||||
*/
|
||||
get deployment(): Prisma.DeploymentDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
}
|
||||
|
||||
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,8 +1,7 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
@@ -15,61 +14,65 @@
|
||||
* 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 './prismaNamespace.ts'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
export type * from '../models.ts';
|
||||
export type * from './prismaNamespace.ts';
|
||||
|
||||
export const Decimal = runtime.Decimal;
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => 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),
|
||||
}
|
||||
DbNull: runtime.NullTypes.DbNull as new (
|
||||
secret: never,
|
||||
) => 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)
|
||||
*
|
||||
* @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)
|
||||
*
|
||||
* @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`
|
||||
*
|
||||
* @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 = {
|
||||
Project: 'Project',
|
||||
User: 'User',
|
||||
Pipeline: 'Pipeline',
|
||||
Step: 'Step',
|
||||
Deployment: 'Deployment'
|
||||
} as const
|
||||
Deployment: 'Deployment',
|
||||
} as const;
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName];
|
||||
|
||||
/*
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = {
|
||||
Serializable: 'Serializable'
|
||||
} as const
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
Serializable: 'Serializable',
|
||||
} as const;
|
||||
|
||||
export type TransactionIsolationLevel =
|
||||
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel];
|
||||
|
||||
export const ProjectScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -77,15 +80,16 @@ export const ProjectScalarFieldEnum = {
|
||||
description: 'description',
|
||||
repository: 'repository',
|
||||
projectDir: 'projectDir',
|
||||
envPresets: 'envPresets',
|
||||
valid: 'valid',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy'
|
||||
} as const
|
||||
|
||||
export type ProjectScalarFieldEnum = (typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum]
|
||||
updatedBy: 'updatedBy',
|
||||
} as const;
|
||||
|
||||
export type ProjectScalarFieldEnum =
|
||||
(typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum];
|
||||
|
||||
export const UserScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -98,11 +102,11 @@ export const UserScalarFieldEnum = {
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy'
|
||||
} as const
|
||||
|
||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||
updatedBy: 'updatedBy',
|
||||
} as const;
|
||||
|
||||
export type UserScalarFieldEnum =
|
||||
(typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum];
|
||||
|
||||
export const PipelineScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -113,11 +117,11 @@ export const PipelineScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy',
|
||||
projectId: 'projectId'
|
||||
} as const
|
||||
|
||||
export type PipelineScalarFieldEnum = (typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum]
|
||||
projectId: 'projectId',
|
||||
} as const;
|
||||
|
||||
export type PipelineScalarFieldEnum =
|
||||
(typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum];
|
||||
|
||||
export const StepScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -129,16 +133,16 @@ export const StepScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy',
|
||||
pipelineId: 'pipelineId'
|
||||
} as const
|
||||
|
||||
export type StepScalarFieldEnum = (typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum]
|
||||
pipelineId: 'pipelineId',
|
||||
} as const;
|
||||
|
||||
export type StepScalarFieldEnum =
|
||||
(typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum];
|
||||
|
||||
export const DeploymentScalarFieldEnum = {
|
||||
id: 'id',
|
||||
branch: 'branch',
|
||||
env: 'env',
|
||||
envVars: 'envVars',
|
||||
status: 'status',
|
||||
commitHash: 'commitHash',
|
||||
commitMessage: 'commitMessage',
|
||||
@@ -152,24 +156,22 @@ export const DeploymentScalarFieldEnum = {
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy',
|
||||
projectId: 'projectId',
|
||||
pipelineId: 'pipelineId'
|
||||
} as const
|
||||
|
||||
export type DeploymentScalarFieldEnum = (typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum]
|
||||
pipelineId: 'pipelineId',
|
||||
} as const;
|
||||
|
||||
export type DeploymentScalarFieldEnum =
|
||||
(typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum];
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
desc: 'desc',
|
||||
} as const;
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder];
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
last: 'last',
|
||||
} as const;
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder];
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
|
||||
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. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This is a barrel export file for all models and their related types.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/Project.ts'
|
||||
export type * from './models/User.ts'
|
||||
export type * from './models/Pipeline.ts'
|
||||
export type * from './models/Step.ts'
|
||||
export type * from './models/Deployment.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
export type * from './models/Project.ts';
|
||||
export type * from './models/Step.ts';
|
||||
export type * from './models/User.ts';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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 操作:克隆、更新、分支切换等
|
||||
*/
|
||||
|
||||
import { $ } from 'zx';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { $ } from 'zx';
|
||||
import { log } from './logger';
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,26 +38,23 @@ class Gitea {
|
||||
clientId: process.env.GITEA_CLIENT_ID!,
|
||||
clientSecret: process.env.GITEA_CLIENT_SECRET!,
|
||||
redirectUri: process.env.GITEA_REDIRECT_URI!,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
const { giteaUrl, clientId, clientSecret, redirectUri } = this.config;
|
||||
console.log('this.config', this.config);
|
||||
const response = await fetch(
|
||||
`${giteaUrl}/login/oauth/access_token`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const response = await fetch(`${giteaUrl}/login/oauth/access_token`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.log(await response.json());
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
@@ -108,19 +105,23 @@ class Gitea {
|
||||
* @param accessToken 访问令牌
|
||||
* @param sha 分支名称或提交SHA
|
||||
*/
|
||||
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) {
|
||||
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
||||
async getCommits(
|
||||
owner: string,
|
||||
repo: string,
|
||||
accessToken: string,
|
||||
sha?: string,
|
||||
) {
|
||||
const url = new URL(
|
||||
`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`,
|
||||
);
|
||||
if (sha) {
|
||||
url.searchParams.append('sha', sha);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
url.toString(),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken),
|
||||
},
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
}
|
||||
@@ -133,7 +134,7 @@ class Gitea {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (accessToken) {
|
||||
headers['Authorization'] = `token ${accessToken}`;
|
||||
headers.Authorization = `token ${accessToken}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,6 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
||||
name: 'Git Clone Pipeline',
|
||||
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
||||
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',
|
||||
order: 1,
|
||||
@@ -36,51 +31,21 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
||||
name: 'Build Project',
|
||||
order: 3,
|
||||
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',
|
||||
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
||||
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',
|
||||
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({
|
||||
where: {
|
||||
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',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
projectId: null // 模板不属于任何特定项目
|
||||
}
|
||||
projectId: null, // 模板不属于任何特定项目
|
||||
},
|
||||
});
|
||||
|
||||
// 创建模板步骤
|
||||
@@ -127,8 +92,8 @@ export async function initializePipelineTemplates(): Promise<void> {
|
||||
pipelineId: pipeline.id,
|
||||
createdBy: '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 {
|
||||
const templates = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
projectId: null, // 模板流水线没有关联的项目
|
||||
valid: 1
|
||||
valid: 1,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true
|
||||
}
|
||||
description: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 处理可能为null的description字段
|
||||
return templates.map(template => ({
|
||||
return templates.map((template) => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description || ''
|
||||
description: template.description || '',
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to get pipeline templates:', error);
|
||||
@@ -185,7 +152,7 @@ export async function createPipelineFromTemplate(
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
pipelineName: string,
|
||||
pipelineDescription: string
|
||||
pipelineDescription: string,
|
||||
): Promise<number> {
|
||||
try {
|
||||
// 获取模板流水线及其步骤
|
||||
@@ -193,18 +160,18 @@ export async function createPipelineFromTemplate(
|
||||
where: {
|
||||
id: templateId,
|
||||
projectId: null, // 确保是模板流水线
|
||||
valid: 1
|
||||
valid: 1,
|
||||
},
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1
|
||||
valid: 1,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc'
|
||||
}
|
||||
}
|
||||
}
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!templatePipeline) {
|
||||
@@ -219,8 +186,8 @@ export async function createPipelineFromTemplate(
|
||||
projectId: projectId,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 复制模板步骤到新流水线
|
||||
@@ -233,12 +200,14 @@ export async function createPipelineFromTemplate(
|
||||
pipelineId: newPipeline.id,
|
||||
createdBy: '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;
|
||||
} catch (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 { 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';
|
||||
|
||||
/**
|
||||
@@ -33,7 +37,7 @@ export class RouteScanner {
|
||||
* 注册多个控制器类
|
||||
*/
|
||||
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);
|
||||
|
||||
// 注册每个路由
|
||||
routes.forEach(route => {
|
||||
routes.forEach((route) => {
|
||||
const fullPath = this.buildFullPath(controllerPrefix, route.path);
|
||||
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey);
|
||||
const handler = this.wrapControllerMethod(
|
||||
controllerInstance,
|
||||
route.propertyKey,
|
||||
);
|
||||
|
||||
// 根据HTTP方法注册路由
|
||||
switch (route.method) {
|
||||
@@ -87,10 +94,10 @@ export class RouteScanner {
|
||||
|
||||
let fullPath = '';
|
||||
if (cleanControllerPrefix) {
|
||||
fullPath += '/' + cleanControllerPrefix;
|
||||
fullPath += `/${cleanControllerPrefix}`;
|
||||
}
|
||||
if (cleanRoutePath) {
|
||||
fullPath += '/' + cleanRoutePath;
|
||||
fullPath += `/${cleanRoutePath}`;
|
||||
}
|
||||
|
||||
// 如果路径为空,返回根路径
|
||||
@@ -105,11 +112,11 @@ export class RouteScanner {
|
||||
// 调用控制器方法
|
||||
const method = instance[methodName];
|
||||
if (typeof method !== 'function') {
|
||||
ctx.throw(401, 'Not Found')
|
||||
ctx.throw(401, 'Not Found');
|
||||
}
|
||||
|
||||
// 绑定this并调用方法
|
||||
const result = await method.call(instance, ctx, next) ?? null;
|
||||
const result = (await method.call(instance, ctx, next)) ?? null;
|
||||
|
||||
ctx.body = createSuccessResponse(result);
|
||||
};
|
||||
@@ -133,19 +140,29 @@ export class RouteScanner {
|
||||
/**
|
||||
* 获取已注册的路由信息(用于调试)
|
||||
*/
|
||||
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> {
|
||||
const routes: Array<{ method: string; path: string; controller: string; action: string }> = [];
|
||||
getRegisteredRoutes(): Array<{
|
||||
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 routeMetadata = getRouteMetadata(ControllerClass);
|
||||
|
||||
routeMetadata.forEach(route => {
|
||||
routeMetadata.forEach((route) => {
|
||||
routes.push({
|
||||
method: route.method,
|
||||
path: this.buildFullPath(controllerPrefix, route.path),
|
||||
controller: ControllerClass.name,
|
||||
action: route.propertyKey
|
||||
action: route.propertyKey,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import type Koa from 'koa';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import type { Middleware } from './types.ts';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type Koa from 'koa';
|
||||
import { z } from 'zod';
|
||||
import type { Middleware } from './types.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 fieldPath = firstError?.path?.join('.') || 'unknown';
|
||||
|
||||
log.info('Exception', 'Zod validation failed: %s at %s', errorMessage, fieldPath);
|
||||
this.sendResponse(ctx, 1003, errorMessage, {
|
||||
field: fieldPath,
|
||||
validationErrors: error.issues.map(issue => ({
|
||||
field: issue.path.join('.'),
|
||||
message: issue.message,
|
||||
code: issue.code,
|
||||
}))
|
||||
}, 400);
|
||||
log.info(
|
||||
'Exception',
|
||||
'Zod validation failed: %s at %s',
|
||||
errorMessage,
|
||||
fieldPath,
|
||||
);
|
||||
this.sendResponse(
|
||||
ctx,
|
||||
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) {
|
||||
// 业务异常
|
||||
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 { 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 type { Middleware } from './types.ts';
|
||||
|
||||
@@ -8,7 +9,7 @@ export class HttpLogger implements Middleware {
|
||||
const start = Date.now();
|
||||
await next();
|
||||
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 type Koa from 'koa';
|
||||
import type { Middleware } from './types.ts';
|
||||
import { RouteScanner } from '../libs/route-scanner.ts';
|
||||
import {
|
||||
ProjectController,
|
||||
UserController,
|
||||
AuthController,
|
||||
DeploymentController,
|
||||
GitController,
|
||||
PipelineController,
|
||||
ProjectController,
|
||||
StepController,
|
||||
GitController
|
||||
UserController,
|
||||
} from '../controllers/index.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 {
|
||||
private router: KoaRouter;
|
||||
@@ -45,7 +45,7 @@ export class Router implements Middleware {
|
||||
DeploymentController,
|
||||
PipelineController,
|
||||
StepController,
|
||||
GitController
|
||||
GitController,
|
||||
]);
|
||||
|
||||
// 输出注册的路由信息
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import session from 'koa-session';
|
||||
import type Koa from 'koa';
|
||||
import session from 'koa-session';
|
||||
import type { Middleware } from './types.ts';
|
||||
|
||||
export class Session implements Middleware {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type Koa from 'koa';
|
||||
import type Koa from 'koa';
|
||||
|
||||
export abstract class Middleware {
|
||||
abstract apply(app: Koa, options?: unknown): void;
|
||||
|
||||
Binary file not shown.
@@ -16,6 +16,7 @@ model Project {
|
||||
description String?
|
||||
repository String
|
||||
projectDir String @unique // 项目工作目录路径(必填)
|
||||
envPresets String? // 环境预设配置(JSON格式)
|
||||
// Relations
|
||||
deployments Deployment[]
|
||||
pipelines Pipeline[]
|
||||
@@ -75,7 +76,7 @@ model Step {
|
||||
model Deployment {
|
||||
id Int @id @default(autoincrement())
|
||||
branch String
|
||||
env String?
|
||||
envVars String? // 环境变量(JSON格式),统一存储所有配置
|
||||
status String // pending, running, success, failed, cancelled
|
||||
commitHash String?
|
||||
commitMessage String?
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { $ } from 'zx';
|
||||
import { prisma } from '../libs/prisma.ts';
|
||||
import type { Step } from '../generated/client.ts';
|
||||
import { GitManager, WorkspaceDirStatus } from '../libs/git-manager.ts';
|
||||
import { log } from '../libs/logger.ts';
|
||||
import { prisma } from '../libs/prisma.ts';
|
||||
|
||||
export class PipelineRunner {
|
||||
private readonly TAG = 'PipelineRunner';
|
||||
@@ -81,7 +81,7 @@ export class PipelineRunner {
|
||||
|
||||
// 执行步骤
|
||||
const stepLog = await this.executeStep(step, envVars);
|
||||
logs += stepLog + '\n';
|
||||
logs += `${stepLog}\n`;
|
||||
|
||||
// 记录步骤执行完成的日志
|
||||
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
|
||||
@@ -215,12 +215,18 @@ export class PipelineRunner {
|
||||
envVars.BRANCH_NAME = deployment.branch || '';
|
||||
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.PROJECT_DIR = this.projectDir;
|
||||
|
||||
return envVars;
|
||||
}
|
||||
@@ -248,13 +254,11 @@ export class PipelineRunner {
|
||||
private addTimestampToLines(content: string, isError = false): string {
|
||||
if (!content) return '';
|
||||
|
||||
return (
|
||||
content
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() !== '')
|
||||
.map((line) => this.addTimestamp(line, isError))
|
||||
.join('\n') + '\n'
|
||||
);
|
||||
return `${content
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() !== '')
|
||||
.map((line) => this.addTimestamp(line, isError))
|
||||
.join('\n')}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,7 +274,7 @@ export class PipelineRunner {
|
||||
|
||||
try {
|
||||
// 添加步骤开始执行的时间戳
|
||||
logs += this.addTimestamp(`执行脚本: ${step.script}`) + '\n';
|
||||
logs += `${this.addTimestamp(`执行脚本: ${step.script}`)}\n`;
|
||||
|
||||
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
||||
const script = step.script;
|
||||
@@ -291,10 +295,10 @@ export class PipelineRunner {
|
||||
logs += this.addTimestampToLines(result.stderr, true);
|
||||
}
|
||||
|
||||
logs += this.addTimestamp(`步骤执行完成`) + '\n';
|
||||
logs += `${this.addTimestamp(`步骤执行完成`)}\n`;
|
||||
} catch (error) {
|
||||
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);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
export function useAsyncEffect(
|
||||
effect: () => Promise<void | (() => void)>,
|
||||
effect: () => Promise<undefined | (() => void)>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const callback = useCallback(effect, [...deps]);
|
||||
@@ -11,7 +11,7 @@ export function useAsyncEffect(
|
||||
const cleanupPromise = callback();
|
||||
return () => {
|
||||
if (cleanupPromise instanceof Promise) {
|
||||
cleanupPromise.then(cleanup => cleanup && cleanup());
|
||||
cleanupPromise.then((cleanup) => cleanup?.());
|
||||
}
|
||||
};
|
||||
}, [callback]);
|
||||
|
||||
@@ -2,7 +2,7 @@ import App from '@pages/App';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
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');
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import {
|
||||
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 { Form, Input, Message, Modal, Select } from '@arco-design/web-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';
|
||||
|
||||
interface EnvPreset {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'select' | 'multiselect' | 'input';
|
||||
required?: boolean;
|
||||
options?: Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
interface DeployModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: () => void;
|
||||
pipelines: Pipeline[];
|
||||
projectId: number;
|
||||
project?: Project | null;
|
||||
}
|
||||
|
||||
function DeployModal({
|
||||
@@ -26,12 +27,29 @@ function DeployModal({
|
||||
onOk,
|
||||
pipelines,
|
||||
projectId,
|
||||
project,
|
||||
}: DeployModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [commits, setCommits] = useState<Commit[]>([]);
|
||||
const [loading, setLoading] = 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(
|
||||
async (branch: string) => {
|
||||
@@ -91,16 +109,27 @@ function DeployModal({
|
||||
try {
|
||||
const values = await form.validate();
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化环境变量
|
||||
const env = values.envVars
|
||||
?.map((item: { key: string; value: string }) => `${item.key}=${item.value}`)
|
||||
.join('\n');
|
||||
// 收集所有环境变量(从预设项中提取)
|
||||
const envVars: Record<string, string> = {};
|
||||
for (const preset of envPresets) {
|
||||
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({
|
||||
projectId,
|
||||
@@ -108,8 +137,7 @@ function DeployModal({
|
||||
branch: values.branch,
|
||||
commitHash: selectedCommit.sha,
|
||||
commitMessage: selectedCommit.commit.message,
|
||||
env: env,
|
||||
sparseCheckoutPaths: values.sparseCheckoutPaths,
|
||||
envVars, // 提交所有环境变量
|
||||
});
|
||||
|
||||
Message.success('部署任务已创建');
|
||||
@@ -128,126 +156,162 @@ function DeployModal({
|
||||
onCancel={onCancel}
|
||||
autoFocus={false}
|
||||
focusLock={true}
|
||||
style={{ width: 650 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="选择流水线"
|
||||
field="pipelineId"
|
||||
rules={[{ required: true, message: '请选择流水线' }]}
|
||||
>
|
||||
<Select placeholder="请选择流水线">
|
||||
{pipelines.map((pipeline) => (
|
||||
<Select.Option key={pipeline.id} value={pipeline.id}>
|
||||
{pipeline.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{/* 基本参数 */}
|
||||
<div className="mb-4 pb-4 border-b border-gray-200">
|
||||
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||
基本参数
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="选择分支"
|
||||
field="branch"
|
||||
rules={[{ required: true, message: '请选择分支' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择分支"
|
||||
loading={branchLoading}
|
||||
onChange={handleBranchChange}
|
||||
<Form.Item
|
||||
label="选择流水线"
|
||||
field="pipelineId"
|
||||
rules={[{ required: true, message: '请选择流水线' }]}
|
||||
>
|
||||
{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>
|
||||
|
||||
<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>
|
||||
<Select placeholder="请选择流水线">
|
||||
{pipelines.map((pipeline) => (
|
||||
<Select.Option key={pipeline.id} value={pipeline.id}>
|
||||
{pipeline.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
long
|
||||
onClick={() => add()}
|
||||
icon={<IconPlus />}
|
||||
>
|
||||
添加环境变量
|
||||
</Button>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择分支"
|
||||
field="branch"
|
||||
rules={[{ required: true, message: '请选择分支' }]}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</Form.List>
|
||||
{envPresets.map((preset) => {
|
||||
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>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -25,17 +25,6 @@ function DeployRecordItem({
|
||||
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 (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
@@ -68,9 +57,6 @@ function DeployRecordItem({
|
||||
分支:{' '}
|
||||
<span className="font-medium text-gray-700">{item.branch}</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
环境: {getEnvTag(item.env || 'unknown')}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
状态: {getStatusTag(item.status)}
|
||||
</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';
|
||||
import {
|
||||
IconCode,
|
||||
IconCommand,
|
||||
IconCopy,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
@@ -49,9 +50,12 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
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 DeployRecordItem from './components/DeployRecordItem';
|
||||
import EnvPresetsEditor, {
|
||||
type EnvPreset,
|
||||
} from './components/EnvPresetsEditor';
|
||||
import PipelineStepItem from './components/PipelineStepItem';
|
||||
import { detailService } from './service';
|
||||
|
||||
@@ -84,7 +88,8 @@ function ProjectDetailPage() {
|
||||
null,
|
||||
);
|
||||
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
||||
const [editingPipeline, setEditingPipeline] = useState<PipelineWithEnabled | null>(null);
|
||||
const [editingPipeline, setEditingPipeline] =
|
||||
useState<PipelineWithEnabled | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [pipelineForm] = Form.useForm();
|
||||
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
||||
@@ -92,12 +97,18 @@ function ProjectDetailPage() {
|
||||
|
||||
// 流水线模板相关状态
|
||||
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
||||
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(
|
||||
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 [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
|
||||
const [envPresetsLoading, setEnvPresetsLoading] = useState(false);
|
||||
|
||||
const { id } = useParams();
|
||||
|
||||
@@ -172,8 +183,14 @@ function ProjectDetailPage() {
|
||||
setDeployRecords(records);
|
||||
|
||||
// 如果当前选中的记录正在运行,则更新选中记录
|
||||
const selectedRecord = records.find((r: Deployment) => r.id === selectedRecordId);
|
||||
if (selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending')) {
|
||||
const selectedRecord = records.find(
|
||||
(r: Deployment) => r.id === selectedRecordId,
|
||||
);
|
||||
if (
|
||||
selectedRecord &&
|
||||
(selectedRecord.status === 'running' ||
|
||||
selectedRecord.status === 'pending')
|
||||
) {
|
||||
// 保持当前选中状态,但更新数据
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -354,14 +371,15 @@ function ProjectDetailPage() {
|
||||
selectedTemplateId,
|
||||
Number(id),
|
||||
values.name,
|
||||
values.description || ''
|
||||
values.description || '',
|
||||
);
|
||||
|
||||
// 更新本地状态 - 需要转换步骤数据结构
|
||||
const transformedSteps = newPipeline.steps?.map(step => ({
|
||||
...step,
|
||||
enabled: step.valid === 1
|
||||
})) || [];
|
||||
const transformedSteps =
|
||||
newPipeline.steps?.map((step) => ({
|
||||
...step,
|
||||
enabled: step.valid === 1,
|
||||
})) || [];
|
||||
|
||||
const pipelineWithDefaults = {
|
||||
...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 = () => {
|
||||
if (detail) {
|
||||
@@ -600,16 +633,21 @@ function ProjectDetailPage() {
|
||||
description: detail.description,
|
||||
repository: detail.repository,
|
||||
});
|
||||
setProjectEditModalVisible(true);
|
||||
setIsEditingProject(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProjectEditSuccess = async () => {
|
||||
const handleCancelEditProject = () => {
|
||||
setIsEditingProject(false);
|
||||
projectForm.resetFields();
|
||||
};
|
||||
|
||||
const handleSaveProject = async () => {
|
||||
try {
|
||||
const values = await projectForm.validate();
|
||||
await detailService.updateProject(Number(id), values);
|
||||
Message.success('项目更新成功');
|
||||
setProjectEditModalVisible(false);
|
||||
setIsEditingProject(false);
|
||||
|
||||
// 刷新项目详情
|
||||
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 = () => {
|
||||
Modal.confirm({
|
||||
title: '删除项目',
|
||||
@@ -671,7 +730,7 @@ function ProjectDetailPage() {
|
||||
);
|
||||
|
||||
// 获取选中的流水线
|
||||
const selectedPipeline = pipelines.find(
|
||||
const _selectedPipeline = pipelines.find(
|
||||
(pipeline) => pipeline.id === selectedPipelineId,
|
||||
);
|
||||
|
||||
@@ -681,11 +740,13 @@ function ProjectDetailPage() {
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
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 }> = {
|
||||
not_created: { text: '未创建', color: 'gray' },
|
||||
empty: { text: '空目录', color: 'orange' },
|
||||
@@ -703,7 +764,15 @@ function ProjectDetailPage() {
|
||||
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
||||
|
||||
return (
|
||||
<Card className="mb-6" title={<Space><IconFolder />工作目录状态</Space>}>
|
||||
<Card
|
||||
className="mb-6"
|
||||
title={
|
||||
<Space>
|
||||
<IconFolder />
|
||||
工作目录状态
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Descriptions
|
||||
column={2}
|
||||
data={[
|
||||
@@ -717,7 +786,9 @@ function ProjectDetailPage() {
|
||||
},
|
||||
{
|
||||
label: '目录大小',
|
||||
value: workspaceStatus.size ? formatSize(workspaceStatus.size) : '-',
|
||||
value: workspaceStatus.size
|
||||
? formatSize(workspaceStatus.size)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
label: '当前分支',
|
||||
@@ -727,16 +798,24 @@ function ProjectDetailPage() {
|
||||
label: '最后提交',
|
||||
value: workspaceStatus.gitInfo?.lastCommit ? (
|
||||
<Space direction="vertical" size="mini">
|
||||
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text>
|
||||
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text>
|
||||
<Typography.Text code>
|
||||
{workspaceStatus.gitInfo.lastCommit}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{workspaceStatus.gitInfo.lastCommitMessage}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
) : '-',
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{workspaceStatus.error && (
|
||||
<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>
|
||||
)}
|
||||
</Card>
|
||||
@@ -763,7 +842,15 @@ function ProjectDetailPage() {
|
||||
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"
|
||||
>
|
||||
<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="col-span-2 space-y-4 h-full flex flex-col">
|
||||
@@ -801,7 +888,7 @@ function ProjectDetailPage() {
|
||||
</Typography.Title>
|
||||
{selectedRecord && (
|
||||
<Typography.Text type="secondary" className="text-sm">
|
||||
{selectedRecord.branch} · {selectedRecord.env} ·{' '}
|
||||
{selectedRecord.branch}
|
||||
{formatDateTime(selectedRecord.createdAt)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
@@ -813,7 +900,9 @@ function ProjectDetailPage() {
|
||||
type="primary"
|
||||
icon={<IconRefresh />}
|
||||
size="small"
|
||||
onClick={() => handleRetryDeployment(selectedRecord.id)}
|
||||
onClick={() =>
|
||||
handleRetryDeployment(selectedRecord.id)
|
||||
}
|
||||
>
|
||||
重新执行
|
||||
</Button>
|
||||
@@ -838,7 +927,15 @@ function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
</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="col-span-2 space-y-4">
|
||||
@@ -951,9 +1048,7 @@ function ProjectDetailPage() {
|
||||
{pipeline.description}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||
<span>
|
||||
{pipeline.steps?.length || 0} 个步骤
|
||||
</span>
|
||||
<span>{pipeline.steps?.length || 0} 个步骤</span>
|
||||
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1005,7 +1100,11 @@ function ProjectDetailPage() {
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={selectedPipeline.steps?.map(step => step.id) || []}
|
||||
items={
|
||||
selectedPipeline.steps?.map(
|
||||
(step) => step.id,
|
||||
) || []
|
||||
}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
@@ -1046,48 +1145,140 @@ function ProjectDetailPage() {
|
||||
</Tabs.TabPane>
|
||||
|
||||
{/* 项目设置标签页 */}
|
||||
<Tabs.TabPane key="settings" title={<Space><IconSettings />项目设置</Space>}>
|
||||
<Tabs.TabPane
|
||||
key="settings"
|
||||
title={
|
||||
<Space>
|
||||
<IconSettings />
|
||||
项目设置
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="p-6">
|
||||
<Card title="项目信息" className="mb-4">
|
||||
<Descriptions
|
||||
column={1}
|
||||
data={[
|
||||
{
|
||||
label: '项目名称',
|
||||
value: detail?.name,
|
||||
},
|
||||
{
|
||||
label: '项目描述',
|
||||
value: detail?.description || '-',
|
||||
},
|
||||
{
|
||||
label: 'Git 仓库',
|
||||
value: detail?.repository,
|
||||
},
|
||||
{
|
||||
label: '工作目录',
|
||||
value: detail?.projectDir || '-',
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
value: formatDateTime(detail?.createdAt),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button type="primary" onClick={handleEditProject}>
|
||||
编辑项目
|
||||
</Button>
|
||||
<Button status="danger" onClick={handleDeleteProject}>
|
||||
删除项目
|
||||
</Button>
|
||||
</div>
|
||||
{!isEditingProject ? (
|
||||
<>
|
||||
<Descriptions
|
||||
column={1}
|
||||
data={[
|
||||
{
|
||||
label: '项目名称',
|
||||
value: detail?.name,
|
||||
},
|
||||
{
|
||||
label: '项目描述',
|
||||
value: detail?.description || '-',
|
||||
},
|
||||
{
|
||||
label: 'Git 仓库',
|
||||
value: detail?.repository,
|
||||
},
|
||||
{
|
||||
label: '工作目录',
|
||||
value: detail?.projectDir || '-',
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
value: formatDateTime(detail?.createdAt),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button type="primary" onClick={handleEditProject}>
|
||||
编辑项目
|
||||
</Button>
|
||||
<Button status="danger" onClick={handleDeleteProject}>
|
||||
删除项目
|
||||
</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>
|
||||
|
||||
{/* 工作目录状态 */}
|
||||
{renderWorkspaceStatus()}
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -1139,7 +1330,9 @@ function ProjectDetailPage() {
|
||||
<Select.Option key={template.id} value={template.id}>
|
||||
<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>
|
||||
</Select.Option>
|
||||
))}
|
||||
@@ -1155,10 +1348,7 @@ function ProjectDetailPage() {
|
||||
>
|
||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
>
|
||||
<Form.Item field="description" label="流水线描述">
|
||||
<Input.TextArea
|
||||
placeholder="描述这个流水线的用途和特点..."
|
||||
rows={3}
|
||||
@@ -1176,10 +1366,7 @@ function ProjectDetailPage() {
|
||||
>
|
||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
>
|
||||
<Form.Item field="description" label="流水线描述">
|
||||
<Input.TextArea
|
||||
placeholder="描述这个流水线的用途和特点..."
|
||||
rows={3}
|
||||
@@ -1228,47 +1415,6 @@ function ProjectDetailPage() {
|
||||
</Form>
|
||||
</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
|
||||
visible={deployModalVisible}
|
||||
onCancel={() => setDeployModalVisible(false)}
|
||||
@@ -1286,6 +1432,7 @@ function ProjectDetailPage() {
|
||||
}}
|
||||
pipelines={pipelines}
|
||||
projectId={Number(id)}
|
||||
project={detail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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 {
|
||||
async getProject(id: string) {
|
||||
@@ -19,7 +27,9 @@ class DetailService {
|
||||
|
||||
// 获取可用的流水线模板
|
||||
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',
|
||||
});
|
||||
return data;
|
||||
@@ -59,7 +69,7 @@ class DetailService {
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
name: string,
|
||||
description?: string
|
||||
description?: string,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
url: '/api/pipelines/from-template',
|
||||
@@ -68,7 +78,7 @@ class DetailService {
|
||||
templateId,
|
||||
projectId,
|
||||
name,
|
||||
description
|
||||
description,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
@@ -204,7 +214,7 @@ class DetailService {
|
||||
// 更新项目
|
||||
async updateProject(
|
||||
id: number,
|
||||
project: Partial<{ name: string; description: string; repository: string }>,
|
||||
project: Partial<Project>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Project>>({
|
||||
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 { projectService } from '../service';
|
||||
import EnvPresetsEditor from '../../detail/components/EnvPresetsEditor';
|
||||
import type { Project } from '../../types';
|
||||
import { projectService } from '../service';
|
||||
|
||||
interface CreateProjectModalProps {
|
||||
visible: boolean;
|
||||
@@ -22,7 +30,15 @@ function CreateProjectModal({
|
||||
const values = await form.validate();
|
||||
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('项目创建成功');
|
||||
onSuccess(newProject);
|
||||
@@ -114,7 +130,9 @@ function CreateProjectModal({
|
||||
if (value.includes('..') || value.includes('~')) {
|
||||
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||
}
|
||||
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
||||
// 检查非法字符(控制字符 0x00-0x1F)
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: 需要检测路径中的控制字符
|
||||
if (/[<>:"|?*\u0000-\u001f]/.test(value)) {
|
||||
return cb('路径包含非法字符');
|
||||
}
|
||||
cb();
|
||||
@@ -124,6 +142,14 @@ function CreateProjectModal({
|
||||
>
|
||||
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
|
||||
</Form.Item>
|
||||
|
||||
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
|
||||
<Collapse.Item header="环境变量预设配置(可选)" name="envPresets">
|
||||
<Form.Item field="envPresets" noStyle>
|
||||
<EnvPresetsEditor />
|
||||
</Form.Item>
|
||||
</Collapse.Item>
|
||||
</Collapse>
|
||||
</Form>
|
||||
</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 EnvPresetsEditor, {
|
||||
type EnvPreset,
|
||||
} from '../../detail/components/EnvPresetsEditor';
|
||||
import type { Project } from '../../types';
|
||||
import { projectService } from '../service';
|
||||
import type { Project } from '../types';
|
||||
|
||||
interface EditProjectModalProps {
|
||||
visible: boolean;
|
||||
@@ -22,10 +32,20 @@ function EditProjectModal({
|
||||
// 当项目信息变化时,更新表单数据
|
||||
React.useEffect(() => {
|
||||
if (project && visible) {
|
||||
let envPresets: EnvPreset[] = [];
|
||||
try {
|
||||
if (project.envPresets) {
|
||||
envPresets = JSON.parse(project.envPresets);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析环境预设失败:', error);
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
repository: project.repository,
|
||||
envPresets,
|
||||
});
|
||||
}
|
||||
}, [project, visible, form]);
|
||||
@@ -37,7 +57,18 @@ function EditProjectModal({
|
||||
|
||||
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('项目更新成功');
|
||||
onSuccess(updatedProject);
|
||||
@@ -111,6 +142,14 @@ function EditProjectModal({
|
||||
>
|
||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||
</Form.Item>
|
||||
|
||||
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
|
||||
<Collapse.Item header="环境变量预设配置" name="envPresets">
|
||||
<Form.Item field="envPresets" noStyle>
|
||||
<EnvPresetsEditor />
|
||||
</Form.Item>
|
||||
</Collapse.Item>
|
||||
</Collapse>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Card,
|
||||
Space,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
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 { useState } from 'react';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface Project {
|
||||
description: string;
|
||||
repository: string;
|
||||
projectDir: string; // 项目工作目录路径(必填)
|
||||
envPresets?: string; // 环境预设配置(JSON格式)
|
||||
valid: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -77,12 +78,11 @@ export interface Pipeline {
|
||||
export interface Deployment {
|
||||
id: number;
|
||||
branch: string;
|
||||
env?: string;
|
||||
envVars?: string; // JSON 字符串
|
||||
status: string;
|
||||
commitHash?: string;
|
||||
commitMessage?: string;
|
||||
buildLog?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
valid: number;
|
||||
@@ -127,6 +127,5 @@ export interface CreateDeploymentRequest {
|
||||
branch: string;
|
||||
commitHash: string;
|
||||
commitMessage: string;
|
||||
env?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
envVars?: Record<string, string>; // 环境变量 key-value 对象
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ class Net {
|
||||
(error) => {
|
||||
console.log('error', error);
|
||||
// 对于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({
|
||||
...error.response,
|
||||
|
||||
7
docs/.meta/OWNERS.md
Normal file
7
docs/.meta/OWNERS.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 文档拥有者
|
||||
|
||||
- backend: backend-team@example.com
|
||||
- ops: ops-team@example.com
|
||||
- product: product-team@example.com
|
||||
|
||||
每个文档请在 front-matter 中声明 `owners` 字段。
|
||||
116
docs/.meta/templates/design-template.md
Normal file
116
docs/.meta/templates/design-template.md
Normal file
@@ -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 等
|
||||
22
docs/.meta/templates/runbook-template.md
Normal file
22
docs/.meta/templates/runbook-template.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Runbook 模板
|
||||
owners:
|
||||
- ops: ops-team
|
||||
status: draft
|
||||
---
|
||||
|
||||
# Runbook 标题
|
||||
|
||||
## 触发条件
|
||||
|
||||
## 负责人
|
||||
|
||||
## 联系方式
|
||||
|
||||
## 暂时性缓解
|
||||
|
||||
## 恢复步骤
|
||||
|
||||
## 验证
|
||||
|
||||
## 回滚(如果适用)
|
||||
14
docs/api/README.md
Normal file
14
docs/api/README.md
Normal file
@@ -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`
|
||||
78
docs/api/endpoints.md
Normal file
78
docs/api/endpoints.md
Normal file
@@ -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` 自动生成示例请求/响应片段。是否需要我继续生成?
|
||||
28
docs/api/openapi.yaml
Normal file
28
docs/api/openapi.yaml
Normal file
@@ -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
|
||||
26
docs/architecture/adr-0001-service-design.md
Normal file
26
docs/architecture/adr-0001-service-design.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: ADR 0001 - 服务设计决策
|
||||
date: 2026-01-03
|
||||
authors:
|
||||
- backend-team
|
||||
status: accepted
|
||||
---
|
||||
|
||||
# ADR 0001: 服务设计与部署模型
|
||||
|
||||
## 背景
|
||||
|
||||
需要选择微服务还是单体部署以便平衡开发速度与运维复杂度。
|
||||
|
||||
## 决策
|
||||
|
||||
采用模块化单体(modular monolith)作为初始阶段部署方式,关键模块解耦、接口明确,后续按需拆分服务。
|
||||
|
||||
## 影响
|
||||
|
||||
- 优点:降低初期运维成本,便于本地调试与 CI 集成。
|
||||
- 缺点:需要在代码边界设计中预留拆分点。
|
||||
|
||||
## 备注
|
||||
|
||||
在拆分时优先考虑数据库边界和独立部署能力。
|
||||
175
docs/architecture/design-0001-product-prototype.md
Normal file
175
docs/architecture/design-0001-product-prototype.md
Normal file
@@ -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`
|
||||
166
docs/architecture/design-0004-execution-queue.md
Normal file
166
docs/architecture/design-0004-execution-queue.md
Normal file
@@ -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`
|
||||
127
docs/architecture/design-0005-refactor-deply.md
Normal file
127
docs/architecture/design-0005-refactor-deply.md
Normal file
@@ -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 字段 → **已决定**:保留字段(兼容历史),但新建部署不再写入
|
||||
|
||||
9
docs/changelogs/CHANGELOG.md
Normal file
9
docs/changelogs/CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
所有 notable 更改应在此记录。遵循 Keep a Changelog 格式。
|
||||
|
||||
## [Unreleased]
|
||||
- 初始文档目录建立。
|
||||
|
||||
## [1.0.0] - 2026-01-03
|
||||
- 初始发布
|
||||
88
docs/development/setup.md
Normal file
88
docs/development/setup.md
Normal file
@@ -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` 并更新迁移。
|
||||
79
docs/getting-started.md
Normal file
79
docs/getting-started.md
Normal file
@@ -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`。
|
||||
22
docs/index.md
Normal file
22
docs/index.md
Normal file
@@ -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 提交。
|
||||
13
docs/onboarding/new-hire.md
Normal file
13
docs/onboarding/new-hire.md
Normal file
@@ -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
|
||||
28
docs/runbooks/incident-response.md
Normal file
28
docs/runbooks/incident-response.md
Normal file
@@ -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 小时内发布
|
||||
@@ -8,7 +8,11 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6"
|
||||
},
|
||||
"keywords": ["ci", "ark", "ark-ci"],
|
||||
"keywords": [
|
||||
"ci",
|
||||
"ark",
|
||||
"ark-ci"
|
||||
],
|
||||
"author": "hurole",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
|
||||
|
||||
Reference in New Issue
Block a user