This commit is contained in:
ch197511161
2025-12-11 02:09:07 +08:00
parent 54d6acbce3
commit aaaf08e8f3
84 changed files with 4131 additions and 0 deletions

337
server/lib/crud-handler.ts Normal file
View File

@@ -0,0 +1,337 @@
/**
* 通用 CRUD 处理器基类
* Generic CRUD Handler Base Class
*
* 提供标准的 HTTP 请求处理和响应格式化
* Provides standard HTTP request handling and response formatting
*/
import type { H3Event } from 'h3'
import { getQuery, getRouterParams, readBody, createError } from 'h3'
import { validateQuery, validateBody, validateParams } from '@nuxt4crud/shared'
import type {
BaseEntity,
CreateInput,
UpdateInput,
QueryParams,
ApiResponse,
PaginatedResponse,
} from '@nuxt4crud/shared'
/**
* CRUD 处理器基类
* CRUD Handler Base Class
*/
export abstract class BaseCrudHandler<
T extends BaseEntity,
CreateInputType extends CreateInput,
UpdateInputType extends UpdateInput,
QueryParamsType extends QueryParams = QueryParams,
> {
constructor(
protected service: any, // 实际的服务实例
protected schemas: {
query?: any
create?: any
update?: any
params?: any
} = {}
) {}
/**
* 处理列表查询请求
* Handle list query request
*/
async handleList(event: H3Event): Promise<ApiResponse<PaginatedResponse<T>>> {
try {
const query = this.schemas.query
? validateQuery(this.schemas.query, getQuery(event))
: getQuery(event)
const result = await this.service.findMany(query)
return {
success: true,
data: result,
}
} catch (error: any) {
return this.handleError(error)
}
}
/**
* 处理单个记录查询请求
* Handle single record query request
*/
async handleGet(event: H3Event): Promise<ApiResponse<T>> {
try {
const params = this.schemas.params
? validateParams(this.schemas.params, getRouterParams(event))
: getRouterParams(event)
const id = Number(params.id)
const result = await this.service.findById(id)
if (!result) {
throw createError({
statusCode: 404,
statusMessage: '记录不存在',
})
}
return {
success: true,
data: result,
}
} catch (error: any) {
return this.handleError(error)
}
}
/**
* 兼容单个或多个ID的查询请求支持 GET 与 POST
* Flexible handler to fetch by a single ID or multiple IDs (supports GET and POST)
*
* - GET: 路径参数 `id`,支持逗号分隔(如 `/api/.../1,2,3`
* - POST: 请求体包含 `id: number` 或 `ids: number[] | string`
*/
async handleGetFlexible(event: H3Event): Promise<ApiResponse<T | T[]>> {
try {
const method = event?.node?.req?.method || 'GET'
// POST 支持通过请求体传输多个ID
if (method === 'POST') {
const body = await readBody(event)
if (!body || (body.id === undefined && body.ids === undefined)) {
throw createError({ statusCode: 400, statusMessage: '请求体必须包含 id 或 ids' })
}
// 归一化为ID数组
const ids: number[] = Array.isArray(body.ids)
? body.ids.map((v: any) => Number(v)).filter((v: number) => Number.isFinite(v) && v > 0)
: typeof body.ids === 'string'
? body.ids
.split(',')
.map(s => Number(s.trim()))
.filter(n => Number.isFinite(n) && n > 0)
: body.id !== undefined
? [Number(body.id)].filter(n => Number.isFinite(n) && n > 0)
: []
if (ids.length === 0) {
throw createError({ statusCode: 400, statusMessage: '无效的ID或ID数组' })
}
// 单个ID与多个ID统一处理
if (ids.length === 1) {
const one = await this.service.findById(ids[0])
if (!one) {
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
}
return { success: true, data: one }
} else {
const list = await this.service.findByIds(ids)
if (!list || list.length === 0) {
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
}
const partial = list.length < ids.length
return {
success: true,
data: list,
message: partial ? '部分ID未找到' : undefined,
}
}
}
// GET 支持 `/api/.../[id]` 单个ID或逗号分隔的多个ID
const params = getRouterParams(event)
if (!params || params.id === undefined) {
throw createError({ statusCode: 400, statusMessage: '缺少路径参数 id' })
}
const rawId = String(params.id)
const ids = rawId
.split(',')
.map(s => Number(s.trim()))
.filter(n => Number.isFinite(n) && n > 0)
if (ids.length === 0) {
throw createError({ statusCode: 400, statusMessage: '无效的路径参数 id' })
}
if (ids.length === 1) {
const one = await this.service.findById(ids[0])
if (!one) {
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
}
return { success: true, data: one }
} else {
const list = await this.service.findByIds(ids)
if (!list || list.length === 0) {
throw createError({ statusCode: 404, statusMessage: '记录不存在' })
}
const partial = list.length < ids.length
return {
success: true,
data: list,
message: partial ? '部分ID未找到' : undefined,
}
}
} catch (error: any) {
return this.handleError(error)
}
}
/**
* 处理创建请求
* Handle create request
*/
async handleCreate(event: H3Event): Promise<ApiResponse<T>> {
try {
const body = await readBody(event)
if (!body) {
throw createError({
statusCode: 400,
statusMessage: '请求体不能为空',
})
}
const data = this.schemas.create ? validateBody(this.schemas.create, body) : body
const result = await this.service.create(data)
return {
success: true,
data: result,
message: '创建成功',
}
} catch (error: any) {
return this.handleError(error)
}
}
/**
* 处理更新请求
* Handle update request
*/
async handleUpdate(event: H3Event): Promise<ApiResponse<T>> {
try {
const params = this.schemas.params
? validateParams(this.schemas.params, getRouterParams(event))
: getRouterParams(event)
const id = Number(params.id)
const body = await readBody(event)
if (!body) {
throw createError({
statusCode: 400,
statusMessage: '请求体不能为空',
})
}
const data = this.schemas.update ? validateBody(this.schemas.update, body) : body
const result = await this.service.update(id, data)
return {
success: true,
data: result,
message: '更新成功',
}
} catch (error: any) {
return this.handleError(error)
}
}
/**
* 处理删除请求
* Handle delete request
*/
async handleDelete(event: H3Event): Promise<ApiResponse<void>> {
try {
const params = this.schemas.params
? validateParams(this.schemas.params, getRouterParams(event))
: getRouterParams(event)
const id = Number(params.id)
await this.service.delete(id)
return {
success: true,
message: '删除成功',
}
} catch (error: any) {
return this.handleError(error)
}
}
/**
* 错误处理
* Error handling
*/
protected handleError(error: any): ApiResponse {
console.error('CRUD Handler Error:', error)
if (error.statusCode === 404) {
return {
success: false,
message: error.message || '记录不存在',
}
}
if (error.statusCode === 400) {
return {
success: false,
message: error.message || '请求参数错误',
validationErrors: error.validationErrors,
}
}
if (error.statusCode === 409) {
return {
success: false,
message: error.message || '数据冲突',
}
}
return {
success: false,
message: error.message || '服务器内部错误',
}
}
}
/**
* 创建 CRUD 处理器工厂函数
* Create CRUD handler factory function
*/
export function createCrudHandlers<
T extends BaseEntity,
CreateInputType extends CreateInput,
UpdateInputType extends UpdateInput,
QueryParamsType extends QueryParams = QueryParams,
>(
service: any,
schemas: {
query?: any
create?: any
update?: any
params?: any
} = {}
) {
const handler = new BaseCrudHandler<T, CreateInputType, UpdateInputType, QueryParamsType>(
service,
schemas
)
return {
list: (event: H3Event) => handler.handleList(event),
get: (event: H3Event) => handler.handleGet(event),
create: (event: H3Event) => handler.handleCreate(event),
update: (event: H3Event) => handler.handleUpdate(event),
delete: (event: H3Event) => handler.handleDelete(event),
}
}

252
server/lib/crud-service.ts Normal file
View File

@@ -0,0 +1,252 @@
/**
* 通用 CRUD 服务基类
* Generic CRUD Service Base Class
*
* 提供标准的 CRUD 操作,支持分页、搜索、排序等功能
* Provides standard CRUD operations with pagination, search, and sorting
*/
import { PrismaClient } from '@prisma/client'
import type { BaseEntity, PaginatedResponse, CrudOptions } from '@nuxt4crud/shared'
/**
* CRUD 服务基类
* CRUD Service Base Class
*/
export abstract class BaseCrudService<
T extends BaseEntity,
CreateInput,
UpdateInput,
QueryParams = any,
> {
protected model: any
protected options: CrudOptions
constructor(
protected prisma: PrismaClient,
protected modelName: string,
options: Partial<CrudOptions> = {}
) {
this.model = (prisma as any)[modelName]
this.options = {
softDelete: false,
defaultOrderBy: 'createdAt',
defaultOrderDirection: 'desc',
searchableFields: [],
uniqueFields: [],
...options,
}
}
/**
* 获取列表(支持分页、搜索、排序)
* Get list with pagination, search, and sorting
*/
async findMany(
params: QueryParams & {
page?: number
limit?: number
search?: string
orderBy?: string
orderDirection?: 'asc' | 'desc'
}
): Promise<PaginatedResponse<T>> {
const {
page = 1,
limit = 10,
search = '',
orderBy = this.options.defaultOrderBy,
orderDirection = this.options.defaultOrderDirection,
...filters
} = params
const where = this.buildWhereClause(search, filters)
const skip = (page - 1) * limit
const take = limit
const [data, total] = await Promise.all([
this.model.findMany({
where,
skip,
take,
orderBy: { [orderBy]: orderDirection },
}),
this.model.count({ where }),
])
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
}
}
/**
* 根据ID查找
* Find by ID
*/
async findById(id: number): Promise<T | null> {
return await this.model.findUnique({
where: { id },
})
}
/**
* 根据多个ID查找记录
* Find records by multiple IDs
*
* @param ids 要查询的ID数组
* @returns 匹配的记录列表
*/
async findByIds(ids: number[]): Promise<T[]> {
const normalizedIds = Array.from(
new Set(
(ids || [])
.map(id => Number(id))
.filter(id => Number.isFinite(id) && id > 0)
)
)
if (normalizedIds.length === 0) {
return []
}
return await this.model.findMany({
where: { id: { in: normalizedIds } },
})
}
/**
* 创建记录
* Create record
*/
async create(data: CreateInput): Promise<T> {
await this.validateCreate(data)
const transformedData = await this.transformCreateData(data)
return await this.model.create({
data: transformedData,
})
}
/**
* 更新记录
* Update record
*/
async update(id: number, data: UpdateInput): Promise<T> {
const existing = await this.findById(id)
if (!existing) {
const error = new Error('记录不存在')
;(error as any).statusCode = 404
throw error
}
await this.validateUpdate(id, data, existing)
const transformedData = await this.transformUpdateData(data, existing)
return await this.model.update({
where: { id },
data: transformedData,
})
}
/**
* 删除记录
* Delete record
*/
async delete(id: number): Promise<void> {
const existing = await this.findById(id)
if (!existing) {
const error = new Error('记录不存在')
;(error as any).statusCode = 404
throw error
}
await this.validateDelete(id, existing)
if (this.options.softDelete) {
await this.model.update({
where: { id },
data: { deletedAt: new Date() },
})
} else {
await this.model.delete({
where: { id },
})
}
}
/**
* 构建查询条件
* Build where clause
*/
protected buildWhereClause(search: string, filters: any): any {
const where: any = {}
// 搜索功能
if (search && this.options.searchableFields?.length) {
where.OR = this.options.searchableFields.map(field => ({
[field]: { contains: search },
}))
}
// 过滤条件
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && key !== 'search') {
where[key] = value
}
})
// 软删除过滤
if (this.options.softDelete) {
where.deletedAt = null
}
return where
}
/**
* 创建前验证钩子
* Pre-create validation hook
*/
protected async validateCreate(data: CreateInput): Promise<void> {
// 子类可以重写此方法
}
/**
* 更新前验证钩子
* Pre-update validation hook
*/
protected async validateUpdate(id: number, data: UpdateInput, existing: T): Promise<void> {
// 子类可以重写此方法
}
/**
* 删除前验证钩子
* Pre-delete validation hook
*/
protected async validateDelete(id: number, existing: T): Promise<void> {
// 子类可以重写此方法
}
/**
* 创建数据转换钩子
* Create data transformation hook
*/
protected async transformCreateData(data: CreateInput): Promise<any> {
return data
}
/**
* 更新数据转换钩子
* Update data transformation hook
*/
protected async transformUpdateData(data: UpdateInput, existing: T): Promise<any> {
return data
}
}

22
server/lib/prisma.ts Normal file
View File

@@ -0,0 +1,22 @@
/**
* Prisma 客户端实例
* Prisma Client Instance
*/
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const { PrismaClient } = require('@prisma/client')
/**
* 全局Prisma客户端实例
* Global Prisma Client instance
*/
export const prisma = new PrismaClient()
/**
* 在应用关闭时正确断开Prisma连接
* Disconnect Prisma connection properly on application shutdown
*/
export async function disconnectPrisma(): Promise<void> {
await prisma.$disconnect()
}

View File

@@ -0,0 +1,739 @@
/**
* SQL查询构建与执行工具
* SQL Query Builder & Executor
*
* 提供两类能力:
* 1) 基础的SQL字符串构建用于预览
* 2) 基于 Prisma 的查询执行与表字段获取
*/
import type { PrismaClient } from '@prisma/client'
import { Prisma } from '@prisma/client'
import type { AdvancedQueryOptions } from '@nuxt4crud/shared/types'
export interface QueryBuilderOptions {
table: string
select?: string[]
where?: Record<string, any>
orderBy?: string
orderDirection?: 'asc' | 'desc'
limit?: number
offset?: number
}
/**
* SQL查询构建器类
* SQL Query Builder Class
*/
// 仅用于生成基础 SQL 预览字符串的构建器(不直接执行)
export class SqlQueryBuilder {
private options: QueryBuilderOptions
constructor(options: QueryBuilderOptions) {
this.options = options
}
/**
* 构建SELECT查询
* Build SELECT query
*/
buildSelectQuery(): { sql: string; params: any[] } {
const params: any[] = []
let sql = 'SELECT '
// SELECT 子句
if (this.options.select && this.options.select.length > 0) {
sql += this.options.select.join(', ')
} else {
sql += '*'
}
// FROM 子句
sql += ` FROM ${this.escapeIdentifier(this.options.table)}`
// WHERE 子句
const whereClause = this.buildWhereClause(params)
if (whereClause) {
sql += ` WHERE ${whereClause}`
}
// ORDER BY 子句
if (this.options.orderBy) {
sql += ` ORDER BY ${this.escapeIdentifier(this.options.orderBy)} ${this.options.orderDirection || 'ASC'}`
}
// LIMIT 子句
if (this.options.limit) {
sql += ` LIMIT ${this.options.limit}`
}
// OFFSET 子句
if (this.options.offset) {
sql += ` OFFSET ${this.options.offset}`
}
return { sql, params }
}
/**
* 构建COUNT查询
* Build COUNT query
*/
buildCountQuery(): { sql: string; params: any[] } {
const params: any[] = []
let sql = `SELECT COUNT(*) as count FROM ${this.escapeIdentifier(this.options.table)}`
// WHERE 子句
const whereClause = this.buildWhereClause(params)
if (whereClause) {
sql += ` WHERE ${whereClause}`
}
return { sql, params }
}
/**
* 构建WHERE子句
* Build WHERE clause
*/
private buildWhereClause(params: any[]): string {
if (!this.options.where || Object.keys(this.options.where).length === 0) {
return ''
}
const conditions: string[] = []
Object.entries(this.options.where).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
conditions.push(`${this.escapeIdentifier(key)} = ?`)
params.push(value)
}
})
return conditions.join(' AND ')
}
/**
* 转义标识符防止SQL注入
* Escape identifier (prevent SQL injection)
*/
private escapeIdentifier(identifier: string): string {
// 简单的标识符转义,移除特殊字符
return identifier.replace(/[^a-zA-Z0-9_]/g, '')
}
/**
* 获取分页查询
* Get paginated query
*/
buildPaginatedQueries(): {
selectQuery: { sql: string; params: any[] }
countQuery: { sql: string; params: any[] }
} {
return {
selectQuery: this.buildSelectQuery(),
countQuery: this.buildCountQuery(),
}
}
}
/**
* Prisma 驱动的查询构建器
* Prisma-driven query builder and executor
*/
export class PrismaSqlQueryBuilder {
constructor(private prisma: PrismaClient) {}
/**
* 获取当前数据库方言postgres/mysql/sqlite
* Get current database dialect from DATABASE_URL
*/
private getDialect(): 'postgres' | 'mysql' | 'sqlite' {
const url = process.env.DATABASE_URL || 'file:./dev.db'
if (url.startsWith('postgres') || url.includes('postgresql')) return 'postgres'
if (url.startsWith('mysql')) return 'mysql'
// Prisma sqlite 通常是 file:./xxx.db
return 'sqlite'
}
/**
* 获取指定表的字段列表(通过数据库系统表)
* Get table column names using database system views
*/
async getTableFields(tableName: string): Promise<string[]> {
const dialect = this.getDialect()
try {
if (dialect === 'postgres') {
const rows: Array<{ column_name: string }> = await (this.prisma.$queryRawUnsafe as any)(
`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${tableName}' ORDER BY ordinal_position;`
)
return rows.map(r => r.column_name)
}
if (dialect === 'mysql') {
const rows: Array<{ Field: string }> = await (this.prisma.$queryRawUnsafe as any)(
`SHOW COLUMNS FROM \`${tableName}\`;`
)
return rows.map(r => r.Field)
}
// sqlite
const rows: Array<{ name: string }> = await (this.prisma.$queryRawUnsafe as any)(
`PRAGMA table_info('${tableName}');`
)
return rows.map(r => r.name)
} catch (e) {
console.warn('getTableFields failed, falling back to defaults:', e)
// 尝试返回常见字段集合作为降级方案
if (tableName === 'users') {
return ['id', 'name', 'email', 'age', 'createdAt', 'updatedAt']
}
return ['id', 'createdAt', 'updatedAt']
}
}
/**
* 验证高级查询选项的合法性
* Validate AdvancedQueryOptions against available fields
*/
validateQueryOptions(options: AdvancedQueryOptions, availableFields: string[]): string[] {
const errors: string[] = []
const fieldSet = new Set(availableFields)
// where 条件校验
if (Array.isArray(options.where)) {
for (const cond of options.where) {
if (!cond || !cond.field) {
errors.push('存在缺少字段的条件')
continue
}
if (!fieldSet.has(cond.field)) {
errors.push(`字段不存在: ${cond.field}`)
}
if (!cond.operator) {
errors.push(`字段 ${cond.field} 缺少操作符`)
}
const nullOps = new Set(['is_null', 'is_not_null'])
const multiOps = new Set(['in', 'not_in', 'between', 'date_between'])
if (!nullOps.has(cond.operator)) {
if (multiOps.has(cond.operator)) {
if (!Array.isArray(cond.value) || cond.value.length === 0) {
errors.push(`字段 ${cond.field} 的操作符 ${cond.operator} 需要数组值`)
}
} else if (cond.value === undefined || cond.value === null || cond.value === '') {
errors.push(`字段 ${cond.field} 的操作符 ${cond.operator} 需要提供值`)
}
}
}
}
// 排序校验
if (Array.isArray(options.orderBy)) {
for (const s of options.orderBy) {
if (!s || !s.field) {
errors.push('存在缺少字段的排序项')
continue
}
if (!fieldSet.has(s.field)) errors.push(`排序字段不存在: ${s.field}`)
const dir = String(s.direction || '').toUpperCase()
if (dir !== 'ASC' && dir !== 'DESC') errors.push(`排序方向非法: ${s.direction}`)
}
}
// 分页校验
const page = Number(options.pagination?.page || 1)
const limit = Number(options.pagination?.limit || 20)
if (!Number.isFinite(page) || page < 1) errors.push('分页页码必须为正整数')
if (!Number.isFinite(limit) || limit < 1 || limit > 1000)
errors.push('分页数量必须在1-1000之间')
return errors
}
/**
* 将条件映射为 Prisma where 片段
* Map one condition to Prisma where fragment
*/
private mapConditionToPrisma(cond: any): any {
const { field, operator, value } = cond || {}
// 如果值为null、undefined或空字符串除了isNull/isNotNull操作符返回空对象
if (
(value === null || value === undefined || value === '') &&
operator !== 'is_null' &&
operator !== 'is_not_null'
) {
return {}
}
switch (operator) {
case 'equals':
case 'eq': // 支持简写形式
return { [field]: value }
case 'not_equals':
case 'ne': // 支持简写形式
return { [field]: { not: value } }
case 'contains':
case 'like': // 支持简写形式
// 确保值不为空字符串,否则会导致不匹配任何记录
if (String(value) === '') return {}
return { [field]: { contains: String(value) } }
case 'not_contains':
case 'notLike': // 支持简写形式
// 确保值不为空字符串,否则会导致不匹配任何记录
if (String(value) === '') return {}
return { [field]: { not: { contains: String(value) } } }
case 'starts_with':
// 确保值不为空字符串,否则会导致不匹配任何记录
if (String(value) === '') return {}
return { [field]: { startsWith: String(value) } }
case 'ends_with':
// 确保值不为空字符串,否则会导致不匹配任何记录
if (String(value) === '') return {}
return { [field]: { endsWith: String(value) } }
case 'gt':
case 'greater_than':
return { [field]: { gt: value } }
case 'gte':
case 'greater_than_or_equal':
return { [field]: { gte: value } }
case 'lt':
case 'less_than':
return { [field]: { lt: value } }
case 'lte':
case 'less_than_or_equal':
return { [field]: { lte: value } }
case 'in': {
const arr = Array.isArray(value)
? value
: String(value)
.split(',')
.map(v => v.trim())
// 确保数组不为空
if (arr.length === 0 || (arr.length === 1 && arr[0] === '')) return {}
return { [field]: { in: arr } }
}
case 'not_in': {
const arr = Array.isArray(value)
? value
: String(value)
.split(',')
.map(v => v.trim())
// 确保数组不为空
if (arr.length === 0 || (arr.length === 1 && arr[0] === '')) return {}
return { [field]: { notIn: arr } }
}
case 'is_null':
return { [field]: null }
case 'is_not_null':
return { [field]: { not: null } }
case 'between': {
const [from, to] = Array.isArray(value) ? value : []
// 确保from和to都有效
if (from === undefined || to === undefined || from === '' || to === '') return {}
return { [field]: { gte: from, lte: to } }
}
case 'date_between': {
const [from, to] = Array.isArray(value) ? value : []
// 确保from和to都有效
if (from === undefined || to === undefined || from === '' || to === '') return {}
return { [field]: { gte: new Date(from), lte: new Date(to) } }
}
default:
return {}
}
}
/**
* 根据查询选项构建 Prisma where 条件及 SQL 预览
* Build Prisma where clause and a basic SQL preview from options
*/
private buildWhereFromOptions(
tableName: string,
options: AdvancedQueryOptions,
searchFields: string[]
): { where: any; sqlPreview: string } {
const where: any = {}
const andConditions: any[] = []
const orConditions: any[] = []
const sqlPreviewParts: string[] = []
// 搜索条件
const search = options.search?.trim()
if (search && searchFields.length > 0) {
const searchOr = searchFields.map(f => ({ [f]: { contains: search } }))
// 默认与其它条件 AND 组合
andConditions.push({ OR: searchOr })
sqlPreviewParts.push(`(${searchFields.map(f => `${f} LIKE '%${search}%'`).join(' OR ')})`)
}
// where 条件
if (Array.isArray(options.where)) {
for (const cond of options.where) {
const { field, operator, value } = cond
// 特殊处理posts表的title字段因为在Prisma模型中title是必填字段String
if (tableName === 'posts' && field === 'title') {
if (operator === 'isNull' || operator === 'is_null') {
// posts表的title字段不可能为null所以如果有这个条件直接返回空结果
return { where: { id: -1 }, sqlPreview: 'WHERE title IS NULL' }
} else if (operator === 'isNotNull' || operator === 'is_not_null') {
// posts表的title字段总是不为null所以这个条件可以忽略
continue
}
}
const frag = this.mapConditionToPrisma(cond)
if (!frag || Object.keys(frag).length === 0) continue
// 普通条件处理
if (String(cond.logicalOperator || 'AND').toUpperCase() === 'OR') {
orConditions.push(frag)
} else {
andConditions.push(frag)
}
// 生成SQL预览
let sqlPart = ''
switch (operator) {
case 'equals':
sqlPart = `${field} = '${value}'`
break
case 'not_equals':
sqlPart = `${field} <> '${value}'`
break
case 'contains':
sqlPart = `${field} LIKE '%${value}%'`
break
case 'not_contains':
sqlPart = `${field} NOT LIKE '%${value}%'`
break
case 'starts_with':
sqlPart = `${field} LIKE '${value}%'`
break
case 'ends_with':
sqlPart = `${field} LIKE '%${value}'`
break
case 'gt':
sqlPart = `${field} > ${value}`
break
case 'gte':
sqlPart = `${field} >= ${value}`
break
case 'lt':
sqlPart = `${field} < ${value}`
break
case 'lte':
sqlPart = `${field} <= ${value}`
break
case 'in':
sqlPart = `${field} IN (${Array.isArray(value) ? value.map(v => `'${v}'`).join(', ') : `'${value}'`})`
break
case 'not_in':
sqlPart = `${field} NOT IN (${Array.isArray(value) ? value.map(v => `'${v}'`).join(', ') : `'${value}'`})`
break
case 'is_null':
sqlPart = `${field} IS NULL`
break
case 'is_not_null':
sqlPart = `${field} IS NOT NULL`
break
case 'between':
const [from, to] = Array.isArray(value) ? value : []
sqlPart = `${field} BETWEEN ${from} AND ${to}`
break
}
if (sqlPart) {
if (sqlPreviewParts.length > 0) {
sqlPreviewParts.push(String(cond.logicalOperator || 'AND').toUpperCase())
}
sqlPreviewParts.push(sqlPart)
}
}
}
let finalConditions: any[] = []
// 处理搜索条件
if (search && searchFields.length > 0) {
const searchOr = searchFields.map(f => ({ [f]: { contains: search } }))
finalConditions.push({ OR: searchOr })
}
// 正确处理逻辑操作符序列
// 前端发送的条件数组中每个条件除了第一个都有一个logicalOperator
// 表示该条件与前一个条件的连接方式
if (Array.isArray(options.where) && options.where.length > 0) {
// 第一个条件
let currentCondition = this.mapConditionToPrisma(options.where[0])
// 如果只有一个条件,直接使用它
if (options.where.length === 1) {
if (currentCondition && Object.keys(currentCondition).length > 0) {
finalConditions.push(currentCondition)
}
} else {
// 处理多个条件
for (let i = 1; i < options.where.length; i++) {
const nextCondition = this.mapConditionToPrisma(options.where[i])
const logicalOperator = String(options.where[i].logicalOperator || 'AND').toUpperCase()
if (!currentCondition || Object.keys(currentCondition).length === 0) {
currentCondition = nextCondition
continue
}
if (!nextCondition || Object.keys(nextCondition).length === 0) {
continue
}
// 根据逻辑操作符构建新的条件结构
if (logicalOperator === 'OR') {
// 如果当前条件已经是OR结构添加新条件
if (currentCondition.OR) {
currentCondition.OR.push(nextCondition)
} else {
// 否则创建新的OR结构
currentCondition = {
OR: [currentCondition, nextCondition],
}
}
} else {
// AND
// 如果当前条件已经是AND结构添加新条件
if (currentCondition.AND) {
currentCondition.AND.push(nextCondition)
} else {
// 否则创建新的AND结构
currentCondition = {
AND: [currentCondition, nextCondition],
}
}
}
}
if (currentCondition && Object.keys(currentCondition).length > 0) {
finalConditions.push(currentCondition)
}
}
}
// 组合所有条件
if (finalConditions.length > 1) {
where.AND = finalConditions
} else if (finalConditions.length === 1) {
Object.assign(where, finalConditions[0])
}
// 构建非常基础的 SQL 预览
const whereParts: string[] = []
if (search && searchFields.length > 0) {
const like = search.replace(/'/g, "''")
whereParts.push('(' + searchFields.map(f => `${f} ILIKE '%${like}%'`).join(' OR ') + ')')
}
if (Array.isArray(options.where)) {
for (const cond of options.where) {
const op = String(cond.operator || '').toLowerCase()
const f = cond.field
const v = cond.value
const logic = String(cond.logicalOperator || 'AND').toUpperCase()
let exp = ''
switch (op) {
case 'equals':
exp = `${f} = '${v}'`
break
case 'not_equals':
exp = `${f} != '${v}'`
break
case 'contains':
exp = `${f} ILIKE '%${String(v)}%'`
break
case 'not_contains':
exp = `${f} NOT ILIKE '%${String(v)}%'`
break
case 'starts_with':
exp = `${f} ILIKE '${String(v)}%'`
break
case 'ends_with':
exp = `${f} ILIKE '%${String(v)}'`
break
case 'gt':
case 'greater_than':
exp = `${f} > ${v}`
break
case 'gte':
case 'greater_than_or_equal':
exp = `${f} >= ${v}`
break
case 'lt':
case 'less_than':
exp = `${f} < ${v}`
break
case 'lte':
case 'less_than_or_equal':
exp = `${f} <= ${v}`
break
case 'in': {
const arr = Array.isArray(v)
? v
: String(v)
.split(',')
.map(s => s.trim())
exp = `${f} IN (${arr.map(x => `'${x}'`).join(', ')})`
break
}
case 'not_in': {
const arr = Array.isArray(v)
? v
: String(v)
.split(',')
.map(s => s.trim())
exp = `${f} NOT IN (${arr.map(x => `'${x}'`).join(', ')})`
break
}
case 'is_null':
exp = `${f} IS NULL`
break
case 'is_not_null':
exp = `${f} IS NOT NULL`
break
case 'between': {
const [from, to] = Array.isArray(v) ? v : []
exp = `${f} BETWEEN '${from}' AND '${to}'`
break
}
case 'date_between': {
const [from, to] = Array.isArray(v) ? v : []
exp = `${f} BETWEEN '${from}' AND '${to}'`
break
}
}
if (exp) whereParts.push((whereParts.length > 0 ? logic + ' ' : '') + exp)
}
}
const whereSql = whereParts.length > 0 ? ' WHERE ' + whereParts.join(' ') : ''
const baseSql = `SELECT * FROM ${tableName}${whereSql}`
return { where, sqlPreview: baseSql }
}
/**
* 执行普通查询返回分页数据以及SQL预览
* Execute normal query with pagination and a SQL preview
*/
async executeQuery(
tableName: string,
options: AdvancedQueryOptions,
searchFields: string[]
): Promise<{
data: any[]
total: number
pagination: { page: number; limit: number; totalPages: number }
executedAt: string
table: string
query: { sql: string }
}> {
const page = Number(options.pagination?.page || 1)
const limit = Number(options.pagination?.limit || 20)
const skip = (page - 1) * limit
const { where, sqlPreview } = this.buildWhereFromOptions(tableName, options, searchFields)
// 如果where条件包含id: -1说明是posts表的title IS NULL查询直接返回空结果
if (where.id === -1) {
return {
data: [],
total: 0,
pagination: {
page: Number(options.pagination?.page || 1),
limit: Number(options.pagination?.limit || 20),
totalPages: 0,
},
executedAt: new Date().toISOString(),
table: tableName,
query: { sql: sqlPreview },
}
}
// 映射到 Prisma 模型名称(简单复数到单数映射)
// 将表名转换为Prisma模型名通常是单数形式
const modelName = tableName === 'users' ? 'user' : tableName === 'posts' ? 'post' : tableName
const model = (this.prisma as any)[modelName]
// 检查模型是否存在
if (!model) {
const err: any = new Error(`未找到 Prisma 模型: ${modelName}`)
err.statusCode = 400
throw err
}
// 排序处理
const orderBy =
Array.isArray(options.orderBy) && options.orderBy.length > 0
? options.orderBy.map(s => ({
[s.field]: String(s.direction || 'ASC').toLowerCase(),
}))
: undefined
// 执行查询
const [data, total] = await Promise.all([
model.findMany({ where, skip, take: limit, orderBy }),
model.count({ where }),
])
return {
data,
total,
pagination: { page, limit, totalPages: Math.max(Math.ceil(total / limit), 1) },
executedAt: new Date().toISOString(),
table: tableName,
query: { sql: sqlPreview + ` ORDER BY ... LIMIT ${limit} OFFSET ${skip}` },
}
}
/**
* 执行聚合查询count/sum/avg/min/max
* Execute aggregate query (count/sum/avg/min/max)
*/
async executeAggregateQuery(tableName: string, options: AdvancedQueryOptions): Promise<any> {
const { where } = this.buildWhereFromOptions(tableName, options, [])
const modelName = tableName === 'users' ? 'user' : tableName
const model = (this.prisma as any)[modelName]
if (!model) {
const err: any = new Error(`未找到 Prisma 模型: ${modelName}`)
err.statusCode = 400
throw err
}
const agg = options.aggregate || {}
const params: any = { where }
if (agg.count) params._count = { _all: true }
const toFieldObj = (fields?: string[]) => {
if (!fields || fields.length === 0) return undefined
return Object.fromEntries(fields.map(f => [f, true]))
}
params._sum = toFieldObj(agg.sum)
params._avg = toFieldObj(agg.avg)
params._min = toFieldObj(agg.min)
params._max = toFieldObj(agg.max)
const result = await model.aggregate(params)
return {
executedAt: new Date().toISOString(),
result,
}
}
}
/**
* 创建基于 Prisma 的查询构建器实例
* Create Prisma-based SQL query builder instance
*/
export function createSqlQueryBuilder(prisma: PrismaClient): PrismaSqlQueryBuilder {
return new PrismaSqlQueryBuilder(prisma)
}