node-red/compiliance-js/flow2-build-crud-plan.js
ruoyunbai d9b08c89ee js
2025-11-17 10:55:25 +08:00

335 lines
10 KiB
JavaScript

'use strict';
/**
* 构建 CRUD 执行动作所需的指令集合,保存于 msg.crudFlow。
* 约定 operationId 采用 dms→oas 转换后默认的命名:
* list<ResourceName>s / create<ResourceName> / delete<ResourceName>
* 允许使用 msg.crudConfig.* 进行覆盖。
*/
const FALLBACK_BASE_URL = 'https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1';
const DEFAULT_LIST_PAYLOAD = {
version: '1.0.0',
data: [],
pageNo: 1,
pageSize: 20,
isSearchCount: true,
};
const DEFAULT_CREATE_SAMPLE = {
version: '1.0.0',
act: -1,
data: [
{
dsid: 'testid2',
wellId: 'WELL-zzlhTEST-002',
wellCommonName: 'zzlh测试用井名',
wellLegalName: 'zzlh-test-01',
wellPurpose: '开发井',
wellType: '直井',
dataRegion: 'ZZLH',
projectId: 'PROJ-ZZLH-001',
projectName: 'zzlh测试地质单元',
orgId: 'ORG-ZZLH-01',
orgName: 'zzlh采油厂',
bsflag: 1,
wellState: '生产中',
spudDate: '2024-01-15',
completionDate: '2024-05-20',
prodDate: '2024-06-01',
egl: 145.5,
kbd: 5.2,
kb: 150.7,
actualXAxis: 550123.45,
actualYAxis: 4998765.32,
coordinateSystemName: 'zzlh测试坐标系',
geoDescription: '位于zzlh测试区域',
remarks: '这是一口用于系统测试的生产井。',
createUserId: 'testuser001',
createDate: '2025-09-12T10:00:00Z',
updateUserId: 'testuser001',
updateDate: '2025-09-12T10:00:00Z',
},
],
};
const DEFAULT_DELETE_TEMPLATE = {
version: '1.0.0',
data: ['{{primaryKey}}'],
};
return buildCrudPlan(msg, node);
function buildCrudPlan(message, node) {
const requestBody = getRequestBody(message);
const crudConfig = mergeDeep({}, requestBody.crudConfig || {}, message.crudConfig || {});
const apiDoc = normaliseOpenApi(message, crudConfig, requestBody, node);
const operations = collectOperations(apiDoc);
const listOp = pickOperation('list', operations, crudConfig);
const createOp = pickOperation('create', operations, crudConfig);
const deleteOp = pickOperation('delete', operations, crudConfig);
if (!listOp || !createOp || !deleteOp) {
const missing = [
!listOp ? 'list' : null,
!createOp ? 'create' : null,
!deleteOp ? 'delete' : null,
].filter(Boolean).join(', ');
const errMsg = `未能在 OpenAPI 文档中找到必要的 CRUD 操作:${missing}`;
node.error(errMsg, message);
message.error = errMsg;
return null;
}
const resourceName = determineResourceName([createOp, listOp, deleteOp]) || 'Resource';
const identityField = crudConfig.identityField ||
requestBody.identityField ||
selectIdentityField(apiDoc, crudConfig) ||
'dsid';
const baseUrl = trimTrailingSlash(
crudConfig.baseUrl ||
requestBody.baseUrl ||
message.baseUrl ||
FALLBACK_BASE_URL
);
const headers = mergeDeep({}, requestBody.headers || {}, crudConfig.headers || {});
const listConfig = mergeDeep(
{},
{ payload: clone(DEFAULT_LIST_PAYLOAD) },
requestBody.list || {},
crudConfig.list || {}
);
const createConfig = mergeDeep(
{},
{ payload: clone(DEFAULT_CREATE_SAMPLE) },
requestBody.create || {},
crudConfig.create || {}
);
const deleteConfig = mergeDeep(
{},
{ payloadTemplate: clone(DEFAULT_DELETE_TEMPLATE) },
requestBody.delete || {},
crudConfig.delete || {}
);
if (requestBody.dataRegion) {
headers.Dataregion = requestBody.dataRegion;
}
message.crudFlow = {
resourceName,
identityField,
baseUrl,
headers,
list: Object.assign({
operationId: listOp.operationId,
method: listOp.method,
path: listOp.path,
}, listConfig),
create: Object.assign({
operationId: createOp.operationId,
method: createOp.method,
path: createOp.path,
samplePayload: clone(DEFAULT_CREATE_SAMPLE),
}, createConfig),
delete: Object.assign({
operationId: deleteOp.operationId,
method: deleteOp.method,
path: deleteOp.path,
}, deleteConfig),
openapi: apiDoc,
};
delete message.crudConfig;
if (message.baseUrl) {
delete message.baseUrl;
}
if (message.headers && !Object.keys(message.headers).length) {
delete message.headers;
}
message.oas_def = apiDoc;
delete message.error;
return message;
}
function normaliseOpenApi(message, crudConfig, requestBody, node) {
let candidate =
crudConfig.openapi ||
requestBody.openapi ||
message.oas_def ||
message.swagger ||
message.payload;
if (typeof candidate === 'string') {
try {
candidate = JSON.parse(candidate);
} catch (err) {
throw new Error(`OpenAPI JSON 解析失败:${err.message}`);
}
}
if (!candidate || typeof candidate !== 'object') {
throw new Error('未提供合法的 OpenAPI 文档');
}
if (!candidate.paths || typeof candidate.paths !== 'object' || Object.keys(candidate.paths).length === 0) {
throw new Error('OpenAPI 文档缺少 paths 定义');
}
return candidate;
}
function collectOperations(apiDoc) {
const operations = [];
const allowed = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
for (const path of Object.keys(apiDoc.paths)) {
const pathItem = apiDoc.paths[path];
if (!pathItem || typeof pathItem !== 'object') {
continue;
}
for (const method of Object.keys(pathItem)) {
if (!allowed.includes(method.toLowerCase())) {
continue;
}
const operation = pathItem[method];
if (!operation || typeof operation !== 'object') {
continue;
}
operations.push({
operationId: operation.operationId || '',
summary: operation.summary || '',
description: operation.description || '',
method: method.toUpperCase(),
path,
operation,
});
}
}
return operations;
}
function pickOperation(kind, operations, crudConfig) {
const overrideId =
(crudConfig[kind] && crudConfig[kind].operationId) ||
crudConfig[`${kind}OperationId`];
if (overrideId) {
return operations.find(op => op.operationId === overrideId);
}
const matcher = getDefaultMatcher(kind);
let matched = operations.find(op => matcher.test(op.operationId));
if (matched) {
return matched;
}
// 兜底策略
switch (kind) {
case 'list':
return operations.find(op => op.method === 'GET') || null;
case 'create':
return operations.find(op => op.method === 'POST') || null;
case 'delete':
return operations.find(op => op.method === 'DELETE') ||
operations.find(op => op.method === 'POST' && /delete/i.test(op.operationId)) ||
null;
default:
return null;
}
}
function getDefaultMatcher(kind) {
switch (kind) {
case 'list':
return /^list[A-Z].*s$/;
case 'create':
return /^create[A-Z].*/;
case 'delete':
return /^delete[A-Z].*/;
default:
return /^$/;
}
}
function determineResourceName(candidates) {
for (const item of candidates) {
const opId = item && item.operationId ? item.operationId : '';
let match;
if ((match = opId.match(/^list([A-Z].*)s$/))) {
return match[1];
}
if ((match = opId.match(/^create([A-Z].*)$/))) {
return match[1];
}
if ((match = opId.match(/^delete([A-Z].*)$/))) {
return match[1];
}
}
return null;
}
function selectIdentityField(apiDoc, crudConfig) {
if (crudConfig.identityField) {
return crudConfig.identityField;
}
if (apiDoc.components && apiDoc.components.schemas) {
for (const schemaName of Object.keys(apiDoc.components.schemas)) {
const schema = apiDoc.components.schemas[schemaName];
if (!schema || typeof schema !== 'object') {
continue;
}
const identity = Array.isArray(schema['x-dms-identityId']) ? schema['x-dms-identityId'][0] : null;
if (identity) {
return identity;
}
}
}
if (apiDoc.components && apiDoc.components.schemas) {
for (const schemaName of Object.keys(apiDoc.components.schemas)) {
const schema = apiDoc.components.schemas[schemaName];
if (schema && schema.properties && Object.prototype.hasOwnProperty.call(schema.properties, 'dsid')) {
return 'dsid';
}
}
}
return 'dsid';
}
function trimTrailingSlash(url) {
if (!url) {
return '';
}
return url.replace(/\/+$/, '');
}
function getRequestBody(message) {
if (message && message.req && message.req.body && typeof message.req.body === 'object') {
return message.req.body;
}
return {};
}
function clone(value) {
return value == null ? value : JSON.parse(JSON.stringify(value));
}
function mergeDeep(target, ...sources) {
for (const source of sources) {
if (!isPlainObject(source)) {
continue;
}
for (const key of Object.keys(source)) {
const value = source[key];
if (value === undefined) {
continue;
}
if (isPlainObject(value)) {
const base = isPlainObject(target[key]) ? target[key] : {};
target[key] = mergeDeep({}, base, value);
} else {
target[key] = clone(value);
}
}
}
return target;
}
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}