init6
This commit is contained in:
337
server/lib/crud-handler.ts
Normal file
337
server/lib/crud-handler.ts
Normal 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
252
server/lib/crud-service.ts
Normal 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
22
server/lib/prisma.ts
Normal 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()
|
||||
}
|
||||
739
server/lib/sql-query-builder.ts
Normal file
739
server/lib/sql-query-builder.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user