335 lines
10 KiB
JavaScript
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]';
|
|
}
|