'use strict'; /** * 构建 CRUD 执行动作所需的指令集合,保存于 msg.crudFlow。 * 约定 operationId 采用 dms→oas 转换后默认的命名: * lists / create / delete * 允许使用 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]'; }