import { cloneDeep } from 'lodash-es';
|
|
export const fileActions = ['VIEW', 'DOWNLOAD', 'EDIT', 'REVIEW', 'ADD', 'REPLACE', 'REMOVE'] as const;
|
export type FileAction = (typeof fileActions)[number];
|
export type FileEffect = 'ALLOW' | 'DENY' | 'INHERIT';
|
export type FileActions = Partial<Record<FileAction, FileEffect>>;
|
|
export interface FilePermissionRule {
|
id: string;
|
formId: string;
|
field: string;
|
subject: 'CREATOR' | 'CURRENT_HANDLERS' | 'NODE_PARTICIPANTS' | 'NON_CURRENT_HANDLERS' | 'PARTICIPANTS';
|
subjectNodeId?: string;
|
trigger: 'ACTIVE' | 'CANCELLED' | 'COMPLETED' | 'ENTERED' | 'TERMINATED';
|
triggerNodeId?: string;
|
onReturn?: 'KEEP' | 'RELEASE';
|
actions: FileActions;
|
}
|
|
export interface FilePermissionPolicy {
|
version: 1;
|
rules: FilePermissionRule[];
|
}
|
|
/** 仅同一表单同一附件字段保留;不要把旧授权复制到另一个表单的同名字段。 */
|
export function retainFilePermissions(previous: any, field: any, isSameForm: boolean): { filePermissions?: FileActions } {
|
if (!isSameForm || field.__config__?.jnpfKey !== 'uploadFile' || previous?.jnpfKey !== 'uploadFile' || !Object.hasOwn(previous, 'filePermissions')) return {};
|
// 保留未知值交由校验报告,不能静默删除损坏的 DENY 配置。
|
return { filePermissions: cloneDeep(previous.filePermissions) };
|
}
|
|
/** 校验对象来自该流程版本的表单字段和节点,不能以显示名称匹配。 */
|
export function validateFilePermissionPolicy(policy: FilePermissionPolicy, fieldsByForm: Record<string, Set<string>>, nodeIds: Set<string>): string[] {
|
if (!policy || policy.version !== 1 || !Array.isArray(policy.rules)) return ['文件权限配置版本或结构无效'];
|
const errors: string[] = [];
|
const ids = new Set<string>();
|
for (const rule of policy.rules) {
|
if (!rule || typeof rule.id !== 'string' || !rule.id.trim()) {
|
errors.push('文件规则缺少 ID');
|
continue;
|
}
|
if (ids.has(rule.id)) errors.push(`文件规则 ID 重复:${rule.id}`);
|
ids.add(rule.id);
|
if (!Object.hasOwn(fieldsByForm, rule.formId) || !fieldsByForm[rule.formId]?.has(rule.field)) errors.push(`附件字段不存在:${rule.id}`);
|
if (!['CREATOR', 'CURRENT_HANDLERS', 'NODE_PARTICIPANTS', 'NON_CURRENT_HANDLERS', 'PARTICIPANTS'].includes(rule.subject))
|
errors.push(`作用人员无效:${rule.id}`);
|
if (!['ACTIVE', 'CANCELLED', 'COMPLETED', 'ENTERED', 'TERMINATED'].includes(rule.trigger)) errors.push(`生效条件无效:${rule.id}`);
|
if (rule.subject === 'NODE_PARTICIPANTS' && !nodeIds.has(rule.subjectNodeId || '')) errors.push(`参与人节点不存在:${rule.id}`);
|
if (['ACTIVE', 'ENTERED'].includes(rule.trigger) && !nodeIds.has(rule.triggerNodeId || '')) errors.push(`触发节点不存在:${rule.id}`);
|
if (rule.trigger === 'ENTERED' && !['KEEP', 'RELEASE'].includes(rule.onReturn || '')) errors.push(`请选择退回后保留或解除规则:${rule.id}`);
|
if (!rule.actions || Array.isArray(rule.actions) || typeof rule.actions !== 'object' || !Object.keys(rule.actions).length) {
|
errors.push(`文件动作为空或无效:${rule.id}`);
|
} else {
|
for (const [action, effect] of Object.entries(rule.actions)) {
|
if (!fileActions.includes(action as FileAction) || !['ALLOW', 'DENY', 'INHERIT'].includes(effect)) errors.push(`文件动作无效:${rule.id}`);
|
}
|
}
|
}
|
return errors;
|
}
|
|
export interface FilePermissionDesignerContext {
|
attachments: { field: string; formId: string; label: string; value: string }[];
|
nodes: { label: string; value: string }[];
|
fieldsByForm: Record<string, Set<string>>;
|
nodeIds: Set<string>;
|
}
|
|
/** 只使用当前流程实际存在的人工节点与原生表单字段缓存,不采纳已删除节点/其他表单的缓存。 */
|
export function filePermissionDesignerContext(data: Record<string, any>, liveNodeIds: Set<string>): FilePermissionDesignerContext {
|
const nodes = Object.entries(data).filter(([id, node]) => liveNodeIds.has(id) && ['approver', 'processing', 'start'].includes(node?.type));
|
const startFormId = nodes.find(([, node]) => node.type === 'start')?.[1]?.formId;
|
const formNames = new Map<string, string>();
|
for (const [, node] of nodes) {
|
const formId = node.formId || startFormId;
|
if (formId) formNames.set(formId, node.formName || formNames.get(formId) || formId);
|
}
|
const attachments: FilePermissionDesignerContext['attachments'] = [];
|
const fieldsByForm: Record<string, Set<string>> = Object.create(null);
|
for (const [formId, name] of formNames) {
|
fieldsByForm[formId] = new Set();
|
const fields = data.global?.allFormMap?.[`form_${formId}`];
|
if (!Array.isArray(fields)) continue;
|
for (const field of fields) {
|
if (field?.__config__?.jnpfKey !== 'uploadFile' || typeof field.id !== 'string' || !field.id || fieldsByForm[formId]!.has(field.id)) continue;
|
fieldsByForm[formId]!.add(field.id);
|
attachments.push({ value: JSON.stringify([formId, field.id]), formId, field: field.id, label: `${name} / ${field.fullName || field.id}` });
|
}
|
}
|
return {
|
attachments,
|
fieldsByForm,
|
nodes: nodes.map(([id, node]) => ({ value: id, label: node.nodeName || id })),
|
nodeIds: new Set(nodes.map(([id]) => id)),
|
};
|
}
|
|
/** 编辑器可展示的结构;未知或损坏配置保留原值,不能打开后自动覆盖为无规则。 */
|
export function isEditableFilePermissionPolicy(value: unknown): value is FilePermissionPolicy {
|
const policy = value as FilePermissionPolicy;
|
return (
|
!!policy &&
|
policy.version === 1 &&
|
Array.isArray(policy.rules) &&
|
policy.rules.every(
|
(rule) =>
|
!!rule &&
|
typeof rule === 'object' &&
|
!Array.isArray(rule) &&
|
typeof rule.id === 'string' &&
|
!!rule.actions &&
|
typeof rule.actions === 'object' &&
|
!Array.isArray(rule.actions),
|
)
|
);
|
}
|
|
export function validateDesignerFilePermissions(data: Record<string, any>, liveNodeIds: Set<string>, flowType: number): string[] {
|
const policies = Object.entries(data).filter(([id, node]) => (id === 'global' || liveNodeIds.has(id)) && node?.filePermissionPolicy != null);
|
if (!policies.length) return [];
|
if (![0, 1].includes(flowType)) return ['当前流程类型暂不支持文件权限规则'];
|
const context = filePermissionDesignerContext(data, liveNodeIds);
|
const ids = new Set<string>();
|
const errors: string[] = [];
|
for (const [, node] of policies) {
|
const policy = node.filePermissionPolicy;
|
errors.push(...validateFilePermissionPolicy(policy, context.fieldsByForm, context.nodeIds));
|
if (!isEditableFilePermissionPolicy(policy)) continue;
|
for (const rule of policy.rules) {
|
if (rule.id.startsWith('@field/')) errors.push('文件规则使用了保留标识');
|
if (ids.has(rule.id)) errors.push(`文件规则 ID 重复:${rule.id}`);
|
ids.add(rule.id);
|
}
|
}
|
return errors;
|
}
|