ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

661 lines
25 KiB

'use strict';
const { TEMP_POSITION_BASE } = require('./constants');
const { hasOwn, toInt, parsePagination, ensureCreatedBy, normalizeParentId, normalizeNullableInt, ensureObjectPayload } = require('./helpers');
const {
ensureInstance,
ensureTemplate,
ensureTemplateChapter,
ensureTemplateBlock,
ensureDataSource,
resolveSchemaVersion,
getTemplateChapterTree,
getInstanceChapterTree,
cloneInstanceToTemplate,
getTemplateBlockBatchBinding,
normalizeTemplateBlockConfigOverrides,
cloneTemplateToInstance,
} = require('./repository');
const {
ensureNoTemplateChapterCycle,
reorderTemplateChapterSiblingsWithInsert,
reorderTemplateChapterSiblings,
reorderTemplateBlockSiblingsWithInsert,
reorderTemplateBlockSiblings,
} = require('./ordering');
const {
getDataSourceTableColumns,
normalizeTemplateBlockConfigForStorage,
} = require('./dataSourceQuery');
const {
sanitizeQueryBlockConfig,
normalizeChartContentSnapshot,
} = require('./chartSnapshot');
const {
resolveBlockPayload,
finalizeResolvedBlockPayload,
} = require('./blockPayloadResolver');
const extractBlockQueryConfig = config => {
const safeConfig = ensureObjectPayload(config || {}, 'config');
if (safeConfig.query && typeof safeConfig.query === 'object' && !Array.isArray(safeConfig.query)) {
return safeConfig.query;
}
return safeConfig;
};
const extractBlockQueryTable = config => {
const safeConfig = ensureObjectPayload(config || {}, 'config');
const queryConfig = extractBlockQueryConfig(safeConfig);
return String(
queryConfig.table
|| queryConfig.tableId
|| queryConfig.tableName
|| safeConfig.table
|| safeConfig.tableId
|| safeConfig.tableName
|| safeConfig.previewQuery?.table
|| ''
).trim();
};
const getFieldSetForTable = async (ctx, dataSourceId, tableId, transaction = null) => {
const payload = await getDataSourceTableColumns(ctx, dataSourceId, tableId, transaction);
return new Set((payload.rows || []).map(item => String(item.name || '').trim()).filter(Boolean));
};
const validateTemplateBlockBindingConsistency = async (
ctx,
template,
templateBlockId,
config,
transaction = null
) => {
const binding = await getTemplateBlockBatchBinding(ctx, template.id, templateBlockId, transaction);
if (!binding) return;
const currentTable = extractBlockQueryTable(config);
if (!currentTable) {
throw '当前模板块已配置批量绑定,请先补齐查询表/数据集或删除绑定';
}
if (binding.targetTable && binding.targetTable !== currentTable) {
throw '当前模板块已配置批量绑定,查询表/数据集与绑定规则不一致,请先修改或删除绑定';
}
const fieldSet = await getFieldSetForTable(ctx, template.dataSourceId, currentTable, transaction);
if (!fieldSet.has(String(binding.targetField || '').trim())) {
throw '当前模板块已配置批量绑定,目标字段与块配置不一致,请先修改或删除绑定';
}
};
module.exports.getTemplates = async (ctx, next) => {
try {
const { models, ORM: { Op } } = ctx.app.fs.dc;
const { offset, limit } = parsePagination(ctx.request.query);
const where = {};
const name = String(ctx.request.query.name || '').trim();
const type = String(ctx.request.query.type || '').trim();
const createdBy = String(ctx.request.query.createdBy || '').trim();
const departmentId = normalizeNullableInt(ctx.request.query.departmentId, 'departmentId');
if (name) where.name = { [Op.like]: `%${name}%` };
if (type) where.type = type;
if (createdBy) where.createdBy = createdBy;
if (departmentId) where.departmentId = departmentId;
if (hasOwn(ctx.request.query, 'isActive')) {
where.isActive = String(ctx.request.query.isActive) === 'true';
}
const list = await models.ReportTemplate.findAndCountAll({
where,
include: [{ model: models.ReportSchemaVersion, required: false }],
order: [['id', 'DESC']],
offset,
limit,
});
ctx.body = { count: list.count, rows: list.rows };
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '获取模板列表失败' };
}
};
module.exports.getTemplateDetail = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
if (!templateId) throw '缺少参数: templateId';
const template = await models.ReportTemplate.findByPk(templateId, {
include: [{ model: models.ReportSchemaVersion, required: false }],
});
if (!template) throw '模板不存在';
const chapters = await getTemplateChapterTree(ctx, templateId);
ctx.body = {
...template.toJSON(),
chapters,
};
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '获取模板详情失败' };
}
};
module.exports.getTemplateInstances = async (ctx, next) => {
try {
const { models, ORM: { Op } } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
if (!templateId) throw '缺少参数: templateId';
await ensureTemplate(ctx, templateId);
const { offset, limit } = parsePagination(ctx.request.query);
const where = { templateId };
const name = String(ctx.request.query.name || '').trim();
const type = String(ctx.request.query.type || '').trim();
const createdBy = String(ctx.request.query.createdBy || '').trim();
const departmentId = normalizeNullableInt(ctx.request.query.departmentId, 'departmentId');
if (name) where.name = { [Op.like]: `%${name}%` };
if (type) where.type = type;
if (createdBy) where.createdBy = createdBy;
if (departmentId) where.departmentId = departmentId;
const list = await models.ReportInstance.findAndCountAll({
where,
order: [['id', 'DESC']],
include: [
{ model: models.ReportTemplate, required: false },
{ model: models.ReportDataSource, required: false },
{ model: models.ReportSchemaVersion, required: false },
],
offset,
limit,
});
ctx.body = { count: list.count, rows: list.rows };
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '获取模板下报表实例列表失败' };
}
};
module.exports.updateTemplate = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
if (!templateId) throw '缺少参数: templateId';
await ensureTemplate(ctx, templateId);
const body = ctx.request.body || {};
const updateData = {};
if (hasOwn(body, 'name')) updateData.name = body.name ? String(body.name).trim() : '';
if (hasOwn(body, 'description')) updateData.description = body.description || null;
if (hasOwn(body, 'version')) updateData.version = body.version ? String(body.version).trim() : '1.0.0';
if (hasOwn(body, 'type')) updateData.type = body.type ? String(body.type).trim() : 'general';
if (hasOwn(body, 'isActive')) updateData.isActive = Boolean(body.isActive);
if (hasOwn(body, 'departmentId')) updateData.departmentId = normalizeNullableInt(body.departmentId, 'departmentId');
if (hasOwn(body, 'schemaVersionId')) {
const schema = await resolveSchemaVersion(ctx, body.schemaVersionId);
updateData.schemaVersionId = schema.id;
}
await models.ReportTemplate.update(updateData, { where: { id: templateId } });
ctx.status = 204;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '更新模板失败' };
}
};
module.exports.getTemplateChapters = async (ctx, next) => {
try {
const templateId = toInt(ctx.params.templateId);
if (!templateId) throw '缺少参数: templateId';
await ensureTemplate(ctx, templateId);
ctx.body = await getTemplateChapterTree(ctx, templateId);
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '获取模板章节列表失败' };
}
};
module.exports.createTemplateChapter = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
if (!templateId) throw '缺少参数: templateId';
await ensureTemplate(ctx, templateId, transaction);
const { title, position } = ctx.request.body || {};
if (!String(title || '').trim()) throw '缺少参数: title';
const parentId = normalizeParentId(ctx.request.body?.parentId);
await ensureNoTemplateChapterCycle(ctx, templateId, null, parentId, transaction);
const created = await models.ReportTemplateChapter.create({
templateId,
parentId,
title: String(title).trim(),
position: TEMP_POSITION_BASE - 1,
}, {
transaction,
returning: true,
});
await reorderTemplateChapterSiblingsWithInsert(
ctx,
templateId,
parentId,
created.id,
position,
transaction
);
const fresh = await models.ReportTemplateChapter.findByPk(created.id, { transaction });
await transaction.commit();
ctx.body = fresh;
ctx.status = 200;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '创建模板章节失败' };
}
};
module.exports.updateTemplateChapter = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
const templateChapterId = toInt(ctx.params.templateChapterId);
if (!templateId || !templateChapterId) throw '缺少参数';
const chapter = await ensureTemplateChapter(ctx, templateId, templateChapterId, transaction);
const body = ctx.request.body || {};
const updateData = {};
if (hasOwn(body, 'title')) updateData.title = body.title ? String(body.title).trim() : '';
const oldParentId = chapter.parentId;
const nextParentId = hasOwn(body, 'parentId') ? normalizeParentId(body.parentId) : chapter.parentId;
await ensureNoTemplateChapterCycle(ctx, templateId, templateChapterId, nextParentId, transaction);
const needMove = hasOwn(body, 'parentId') || hasOwn(body, 'position');
if (!needMove) {
await models.ReportTemplateChapter.update(updateData, {
where: { id: templateChapterId },
transaction,
});
await transaction.commit();
ctx.status = 204;
return;
}
updateData.parentId = nextParentId;
updateData.position = TEMP_POSITION_BASE - 1;
await models.ReportTemplateChapter.update(updateData, {
where: { id: templateChapterId },
transaction,
});
if (oldParentId !== nextParentId) {
await reorderTemplateChapterSiblings(ctx, templateId, oldParentId, transaction);
}
const targetPosition = hasOwn(body, 'position')
? body.position
: (oldParentId === nextParentId ? chapter.position : undefined);
await reorderTemplateChapterSiblingsWithInsert(
ctx,
templateId,
nextParentId,
templateChapterId,
targetPosition,
transaction
);
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '更新模板章节失败' };
}
};
module.exports.deleteTemplateChapter = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
const templateChapterId = toInt(ctx.params.templateChapterId);
if (!templateId || !templateChapterId) throw '缺少参数';
const chapter = await ensureTemplateChapter(ctx, templateId, templateChapterId, transaction);
await models.ReportTemplateChapter.destroy({
where: { id: templateChapterId, templateId },
transaction,
});
await reorderTemplateChapterSiblings(ctx, templateId, chapter.parentId, transaction);
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '删除模板章节失败' };
}
};
module.exports.createTemplateBlock = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
const templateChapterId = toInt(ctx.params.templateChapterId);
if (!templateId || !templateChapterId) throw '缺少参数';
await ensureTemplate(ctx, templateId, transaction);
await ensureTemplateChapter(ctx, templateId, templateChapterId, transaction);
const { blockType, position, config, contentSnapshot } = ctx.request.body || {};
const normalizedBlockType = String(blockType || 'text');
const normalizedConfig = await normalizeTemplateBlockConfigForStorage(
ctx,
templateId,
normalizedBlockType,
config || {},
transaction
);
const created = await models.ReportTemplateBlock.create({
templateId,
chapterId: templateChapterId,
blockType: normalizedBlockType,
position: TEMP_POSITION_BASE - 1,
defaultConfig: sanitizeQueryBlockConfig(normalizedConfig),
contentSnapshot: String(normalizedBlockType).trim().toLowerCase() === 'chart'
? (normalizeChartContentSnapshot(contentSnapshot)
|| normalizeChartContentSnapshot(normalizedConfig))
: (contentSnapshot || null),
}, {
transaction,
returning: true,
});
await reorderTemplateBlockSiblingsWithInsert(
ctx,
templateChapterId,
created.id,
position,
transaction
);
const fresh = await models.ReportTemplateBlock.findByPk(created.id, { transaction });
await transaction.commit();
ctx.body = fresh;
ctx.status = 200;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '创建模板内容块失败' };
}
};
module.exports.updateTemplateBlock = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
const templateBlockId = toInt(ctx.params.templateBlockId);
if (!templateId || !templateBlockId) throw '缺少参数';
const template = await ensureTemplate(ctx, templateId, transaction);
const block = await ensureTemplateBlock(ctx, templateId, templateBlockId, transaction);
const body = ctx.request.body || {};
const updateData = {};
const hasConfigMutation = hasOwn(body, 'config') || hasOwn(body, 'blockType');
if (hasOwn(body, 'blockType')) updateData.blockType = String(body.blockType || 'text');
if (hasConfigMutation) {
const finalBlockType = hasOwn(body, 'blockType') ? updateData.blockType : block.blockType;
const finalConfig = hasOwn(body, 'config') ? (body.config || {}) : (block.defaultConfig || {});
updateData.defaultConfig = await normalizeTemplateBlockConfigForStorage(
ctx,
templateId,
finalBlockType,
finalConfig,
transaction
);
updateData.defaultConfig = sanitizeQueryBlockConfig(updateData.defaultConfig);
await validateTemplateBlockBindingConsistency(
ctx,
template,
templateBlockId,
updateData.defaultConfig,
transaction
);
const finalContentSnapshot = hasOwn(body, 'contentSnapshot')
? body.contentSnapshot
: block.contentSnapshot;
updateData.contentSnapshot = String(finalBlockType || '').trim().toLowerCase() === 'chart'
? (normalizeChartContentSnapshot(finalContentSnapshot)
|| normalizeChartContentSnapshot(finalConfig))
: (finalContentSnapshot || null);
} else if (hasOwn(body, 'contentSnapshot')) {
updateData.contentSnapshot = body.contentSnapshot || null;
}
const oldChapterId = block.chapterId;
const nextChapterId = hasOwn(body, 'chapterId') ? toInt(body.chapterId) : block.chapterId;
if (!nextChapterId) throw '参数错误: chapterId';
await ensureTemplateChapter(ctx, templateId, nextChapterId, transaction);
const needMove = hasOwn(body, 'chapterId') || hasOwn(body, 'position');
if (!needMove) {
await models.ReportTemplateBlock.update(updateData, {
where: { id: templateBlockId },
transaction,
});
await transaction.commit();
ctx.status = 204;
return;
}
updateData.chapterId = nextChapterId;
updateData.position = TEMP_POSITION_BASE - 1;
await models.ReportTemplateBlock.update(updateData, {
where: { id: templateBlockId },
transaction,
});
if (oldChapterId !== nextChapterId) {
await reorderTemplateBlockSiblings(ctx, oldChapterId, transaction);
}
const targetPosition = hasOwn(body, 'position')
? body.position
: (oldChapterId === nextChapterId ? block.position : undefined);
await reorderTemplateBlockSiblingsWithInsert(
ctx,
nextChapterId,
templateBlockId,
targetPosition,
transaction
);
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '更新模板内容块失败' };
}
};
module.exports.deleteTemplateBlock = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
const templateBlockId = toInt(ctx.params.templateBlockId);
if (!templateId || !templateBlockId) throw '缺少参数';
const block = await ensureTemplateBlock(ctx, templateId, templateBlockId, transaction);
await models.ReportTemplateBlock.destroy({
where: { id: templateBlockId, templateId },
transaction,
});
await reorderTemplateBlockSiblings(ctx, block.chapterId, transaction);
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '删除模板内容块失败' };
}
};
module.exports.deleteTemplate = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
if (!templateId) throw '缺少参数: templateId';
await ensureTemplate(ctx, templateId, transaction);
await models.ReportTemplateBlock.destroy({
where: { templateId },
transaction,
});
await models.ReportTemplateChapter.destroy({
where: { templateId },
transaction,
});
await models.ReportTemplate.destroy({
where: { id: templateId },
transaction,
});
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '删除模板失败' };
}
};
module.exports.saveInstanceAsTemplate = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const instanceId = toInt(ctx.params.instanceId);
if (!instanceId) throw '缺少参数: instanceId';
const instance = await ensureInstance(ctx, instanceId, transaction);
const body = ctx.request.body || {};
const createdBy = ensureCreatedBy(body);
const name = String(body.name || '').trim();
const departmentId = hasOwn(body, 'departmentId')
? normalizeNullableInt(body.departmentId, 'departmentId')
: normalizeNullableInt(instance.departmentId, 'departmentId');
if (!name) throw '缺少参数: name';
const schemaVersion = await resolveSchemaVersion(ctx, instance.schemaVersionId, transaction);
const createdTemplate = await models.ReportTemplate.create({
name,
description: body.description || null,
version: String(body.version || '1.0.0'),
schemaVersionId: schemaVersion.id,
dataSourceId: toInt(instance.dataSourceId),
type: String(body.type || instance.type || 'general'),
createdBy,
departmentId,
isActive: hasOwn(body, 'isActive') ? Boolean(body.isActive) : true,
}, {
transaction,
returning: true,
});
await cloneInstanceToTemplate(ctx, instanceId, createdTemplate.id, transaction);
const chapters = await getTemplateChapterTree(ctx, createdTemplate.id, transaction);
await transaction.commit();
ctx.body = {
...createdTemplate.toJSON(),
chapters,
};
ctx.status = 200;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '报表另存模板失败' };
}
};
module.exports.createInstanceFromTemplate = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const templateId = toInt(ctx.params.templateId);
if (!templateId) throw '缺少参数: templateId';
const template = await ensureTemplate(ctx, templateId, transaction);
const body = ctx.request.body || {};
const createdBy = ensureCreatedBy(body);
const name = String(body.name || '').trim();
const departmentId = hasOwn(body, 'departmentId')
? normalizeNullableInt(body.departmentId, 'departmentId')
: normalizeNullableInt(template.departmentId, 'departmentId');
if (!name) throw '缺少参数: name';
const templateDataSourceId = toInt(template.dataSourceId);
if (!templateDataSourceId) throw '模板未绑定数据源';
await ensureDataSource(ctx, templateDataSourceId, transaction);
const blockConfigOverrideMap = hasOwn(body, 'blockConfigOverrides')
? await normalizeTemplateBlockConfigOverrides(
ctx,
template.id,
body.blockConfigOverrides,
transaction
)
: new Map();
const schemaVersion = await resolveSchemaVersion(ctx, body.schemaVersionId || template.schemaVersionId, transaction);
const createdInstance = await models.ReportInstance.create({
templateId: template.id,
schemaVersionId: schemaVersion.id,
name,
type: String(body.type || template.type || 'general'),
dataSourceId: templateDataSourceId,
createdBy,
departmentId,
}, {
transaction,
returning: true,
});
await cloneTemplateToInstance(
ctx,
template.id,
createdInstance.id,
blockConfigOverrideMap,
transaction,
{
resolveBlockPayload: async (templateBlock, payload) => {
return resolveBlockPayload(ctx, {
blockType: templateBlock?.blockType,
dataSourceId: templateDataSourceId,
config: payload?.config || {},
contentSnapshot: payload?.contentSnapshot || null,
transaction,
});
},
finalizeCreatedBlock: async (templateBlock, createdPayload, helpers) => {
return finalizeResolvedBlockPayload(ctx, {
blockType: templateBlock?.blockType,
config: createdPayload?.config || {},
contentSnapshot: createdPayload?.contentSnapshot || null,
blockIdMap: helpers?.blockIdMap,
getCreatedBlockStateByInstanceId: helpers?.getCreatedStateByInstanceId,
});
},
}
);
const chapters = await getInstanceChapterTree(ctx, createdInstance.id, transaction);
await transaction.commit();
ctx.body = {
...createdInstance.toJSON(),
chapters,
};
ctx.status = 200;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: typeof error === 'string' ? error : '模板创建实例失败' };
}
};