'use strict'; const DEFAULT_OPENAPI_VERSION = '3.0.1'; const DEFAULT_API_VERSION = '1.0.0'; const BASE_PREFIX = 'https://www.dev.ideas.cnpc/api/dms'; const DEFAULT_SERVICE_ID = 'well_kd_wellbore_ideas01'; const DEFAULT_API_SEGMENT = 'v1'; const FALLBACK_BASE_URL = `${BASE_PREFIX}/${DEFAULT_SERVICE_ID}/${DEFAULT_API_SEGMENT}`; const FALLBACK_HEADERS = { 'Content-Type': 'application/json', 'Authorization': '1', 'Dataregion': 'ZZLH', }; const SERVICE_DOMAIN_CATALOG = { 'well_kd_wellbore_ideas01': { name: '井筒', id: 'well_kd_wellbore_ideas01', keywords: ['wb', 'wb_dr', 'wb_ml', 'wb_wl', 'wb_tp', 'wb_dh', 'wb_fr'], }, 'geo_kd_res_ideas01': { name: '采油气', id: 'geo_kd_res_ideas01', keywords: ['pc', 'pc_op', 'pc_oe', 'pc_ge'], }, 'kd_cr_ideas01': { name: '分析化验', id: 'kd_cr_ideas01', keywords: ['cr', 'cr_se'], }, 'kd_rs_ideas01': { name: '油气藏', id: 'kd_rs_ideas01', keywords: ['rs', 'rs_rd', 'rs_rm', 'rs_gs', 'rs_in'], }, }; let schema; try { // 优先使用 HTTP In 提供的 req.body.schema,缺省时回退到 msg.payload。 const schemaInput = extractSchemaInput(msg); schema = parseSchema(schemaInput); } catch (error) { node.error(`DMS -> Swagger 解析失败:${error.message}`, msg); msg.error = error.message; return msg; } const resourceTitle = typeof schema.title === 'string' && schema.title.trim() ? schema.title.trim() : 'DMS Resource'; const resourceName = pascalCase(resourceTitle); const collectionName = pluralize(kebabCase(resourceTitle)); const identityField = Array.isArray(schema.identityId) && schema.identityId.length > 0 ? String(schema.identityId[0]) : 'id'; const schemaComponent = buildComponentSchema(resourceName, schema); const identitySchema = schemaComponent.properties && schemaComponent.properties[identityField] ? clone(schemaComponent.properties[identityField]) : { type: 'string' }; const crudConfig = Object.assign({}, msg.crudConfig || {}); const dmsMeta = extractDmsMeta(msg); msg.dms_meta = dmsMeta; const serviceInfo = resolveServiceInfo(dmsMeta && dmsMeta.domain); const apiVersionSegment = DEFAULT_API_SEGMENT; const serviceId = serviceInfo.id; const { resourceSegment, versionSegment } = deriveResourceSegments(dmsMeta, schema, collectionName); const listPath = buildListPath(resourceSegment, versionSegment); const singlePath = buildSinglePath(resourceSegment); const detailPath = buildDetailPath(resourceSegment, versionSegment, identityField); const createRequestSchema = buildCreateRequestSchema(resourceName); const updateRequestSchema = buildCreateRequestSchema(resourceName); const deleteRequestSchema = buildDeleteRequestSchema(identityField); const listRequestSchema = buildListRequestSchema(); const listResponseSchema = buildListResponseSchema(resourceName); const openApiDocument = { openapi: DEFAULT_OPENAPI_VERSION, info: { title: `${resourceTitle} API`, version: DEFAULT_API_VERSION, description: schema.description || `${schema.$id || ''}`.trim(), 'x-dms-sourceId': schema.$id || undefined, }, paths: buildCrudPaths({ resourceName, identityField, identitySchema, listPath, createPath: singlePath, detailPath, createRequestSchema, updateRequestSchema, deleteRequestSchema, listRequestSchema, listResponseSchema, }), components: { schemas: { [resourceName]: schemaComponent, }, }, }; if (!crudConfig.baseUrl) { crudConfig.baseUrl = `${BASE_PREFIX}/${serviceId}/${apiVersionSegment}`; } const headers = Object.assign({}, FALLBACK_HEADERS, crudConfig.headers || {}); const dataRegionValue = extractDataRegion(schema, headers.Dataregion); if (dataRegionValue) { headers.Dataregion = dataRegionValue; crudConfig.dataRegion = dataRegionValue; } crudConfig.headers = headers; crudConfig.identityField = crudConfig.identityField || identityField; crudConfig.service = crudConfig.service || { id: serviceId, name: serviceInfo.name, }; crudConfig.list = crudConfig.list || {}; if (!crudConfig.list.path) { crudConfig.list.path = listPath; } crudConfig.list.method = crudConfig.list.method || 'POST'; crudConfig.create = crudConfig.create || {}; if (!crudConfig.create.path) { crudConfig.create.path = singlePath; } crudConfig.create.method = crudConfig.create.method || 'POST'; crudConfig.delete = crudConfig.delete || {}; if (!crudConfig.delete.path) { crudConfig.delete.path = singlePath; } crudConfig.delete.method = crudConfig.delete.method || 'POST'; crudConfig.detailPath = crudConfig.detailPath || detailPath; crudConfig.version = crudConfig.version || versionSegment; msg.crudConfig = crudConfig; msg.headers = Object.assign({}, headers); msg.oas_def = openApiDocument; msg.payload = openApiDocument; return msg; function extractDataRegion(dmsSchema, fallback) { if (!dmsSchema || typeof dmsSchema !== 'object') { return fallback; } if (typeof dmsSchema.dataRegion === 'string' && dmsSchema.dataRegion.trim()) { return dmsSchema.dataRegion.trim(); } if (dmsSchema.defaultShow && Array.isArray(dmsSchema.defaultShow)) { const candidate = dmsSchema.defaultShow.find(item => item && typeof item === 'string' && item.toLowerCase().includes('dataregion')); if (candidate) { return fallback; } } const props = dmsSchema.properties; if (props && typeof props === 'object' && props.dataRegion) { if (typeof props.dataRegion.default === 'string' && props.dataRegion.default.trim()) { return props.dataRegion.default.trim(); } } return fallback; } function extractSchemaInput(message) { if (message && message.req && message.req.body && typeof message.req.body === 'object') { if (message.req.body.dms_schema !== undefined) { return message.req.body.dms_schema; } if (looksLikeDmsSchema(message.req.body)) { return message.req.body; } } if (message && message.payload !== undefined) { if (message.payload && typeof message.payload === 'object') { if (message.payload.dms_schema !== undefined) { return message.payload.dms_schema; } if (looksLikeDmsSchema(message.payload)) { return message.payload; } } return message.payload; } throw new Error('未找到schema,请在请求体的schema字段或msg.payload中提供'); } function parseSchema(source) { if (typeof source === 'string') { try { return JSON.parse(source); } catch (error) { throw new Error(`JSON 解析失败:${error.message}`); } } if (!source || typeof source !== 'object') { throw new Error('schema 必须是 DMS 定义对象或 JSON 字符串'); } return source; } function buildComponentSchema(resourceName, dmsSchema) { const { properties = {}, required = [], groupView, identityId, naturalKey, defaultShow } = dmsSchema; const openApiProps = {}; for (const [propName, propSchema] of Object.entries(properties)) { openApiProps[propName] = mapProperty(propSchema); } return { type: 'object', required: Array.isArray(required) ? required.slice() : [], properties: openApiProps, description: dmsSchema.description || dmsSchema.title || resourceName, 'x-dms-groupView': groupView || undefined, 'x-dms-identityId': identityId || undefined, 'x-dms-naturalKey': naturalKey || undefined, 'x-dms-defaultShow': defaultShow || undefined, }; } function mapProperty(propSchema) { if (!propSchema || typeof propSchema !== 'object') { return { type: 'string' }; } const result = {}; const type = normalizeType(propSchema.type); result.type = type.type; if (type.format) { result.format = type.format; } if (type.items) { result.items = type.items; } if (propSchema.description) { result.description = propSchema.description; } else if (propSchema.title) { result.description = propSchema.title; } if (propSchema.enum) { result.enum = propSchema.enum.slice(); } if (propSchema.default !== undefined) { result.default = propSchema.default; } if (propSchema.mask) { result['x-dms-mask'] = propSchema.mask; } if (propSchema.geom) { result['x-dms-geom'] = propSchema.geom; } if (propSchema.title) { result['x-dms-title'] = propSchema.title; } if (propSchema.type) { result['x-dms-originalType'] = propSchema.type; } return result; } function normalizeType(typeValue) { const normalized = typeof typeValue === 'string' ? typeValue.toLowerCase() : undefined; switch (normalized) { case 'number': case 'integer': case 'long': case 'float': case 'double': return { type: 'number' }; case 'boolean': return { type: 'boolean' }; case 'array': return { type: 'array', items: { type: 'string' } }; case 'date': return { type: 'string', format: 'date' }; case 'date-time': return { type: 'string', format: 'date-time' }; case 'object': return { type: 'object' }; case 'string': default: return { type: 'string' }; } } function buildCrudPaths({ resourceName, identityField, identitySchema, listPath, createPath, detailPath, createRequestSchema, updateRequestSchema, deleteRequestSchema, listRequestSchema, listResponseSchema, }) { const ref = `#/components/schemas/${resourceName}`; const paths = {}; const normalisedListPath = normalisePath(listPath); const normalisedCreatePath = normalisePath(createPath); const normalisedDetailPath = detailPath ? normalisePath(detailPath) : null; paths[normalisedListPath] = { post: { operationId: `list${resourceName}s`, summary: `List ${resourceName} resources`, requestBody: { required: false, content: { 'application/json': { schema: listRequestSchema, }, }, }, responses: { 200: { description: 'Successful response', content: { 'application/json': { schema: listResponseSchema || { type: 'array', items: { $ref: ref }, }, }, }, }, }, }, }; paths[normalisedCreatePath] = { post: { operationId: `create${resourceName}`, summary: `Create a ${resourceName}`, requestBody: { required: true, content: { 'application/json': { schema: createRequestSchema, }, }, }, responses: { 201: { description: 'Created', content: { 'application/json': { schema: { $ref: ref }, }, }, }, }, }, put: { operationId: `update${resourceName}`, summary: `Update a ${resourceName}`, requestBody: { required: true, content: { 'application/json': { schema: updateRequestSchema, }, }, }, responses: { 200: { description: 'Successful update', content: { 'application/json': { schema: { $ref: ref }, }, }, }, }, }, delete: { operationId: `delete${resourceName}`, summary: `Delete ${resourceName} resources`, requestBody: { required: true, content: { 'application/json': { schema: deleteRequestSchema, }, }, }, responses: { 200: { description: 'Deleted' }, }, }, }; if (normalisedDetailPath) { paths[normalisedDetailPath] = { parameters: [ { name: identityField, in: 'path', required: true, schema: identitySchema, }, ], get: { operationId: `get${resourceName}`, summary: `Get a single ${resourceName}`, responses: { 200: { description: 'Successful response', content: { 'application/json': { schema: { $ref: ref }, }, }, }, 404: { description: `${resourceName} not found` }, }, }, }; } return paths; } function normalisePath(path) { if (!path) { return '/'; } return path.startsWith('/') ? path : `/${path}`; } function pascalCase(input) { return input .replace(/[^a-zA-Z0-9]+/g, ' ') .split(' ') .filter(Boolean) .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .join('') || 'Resource'; } function kebabCase(input) { return input .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[^a-zA-Z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .toLowerCase() || 'resource'; } function pluralize(word) { if (word.endsWith('s')) { return word; } if (word.endsWith('y')) { return word.slice(0, -1) + 'ies'; } return `${word}s`; } function clone(value) { return JSON.parse(JSON.stringify(value)); } function looksLikeDmsSchema(candidate) { if (!candidate || typeof candidate !== 'object') { return false; } if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) { return true; } if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') { return true; } return false; } function extractDmsMeta(message) { const candidateReq = message && message.req && message.req.body && message.req.body.dms_meta; const parsedReq = parseMetaCandidate(candidateReq); if (parsedReq) { return parsedReq; } const candidateMsg = message && message.dms_meta; const parsedMsg = parseMetaCandidate(candidateMsg); if (parsedMsg) { return parsedMsg; } return null; } function parseMetaCandidate(candidate) { if (!candidate) { return null; } if (typeof candidate === 'string') { try { const value = JSON.parse(candidate); return parseMetaCandidate(value); } catch (err) { return null; } } if (typeof candidate === 'object') { return clone(candidate); } return null; } function resolveServiceInfo(domain) { if (!domain || typeof domain !== 'string') { return SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID]; } const normalized = domain.toLowerCase(); let best = null; for (const entry of Object.values(SERVICE_DOMAIN_CATALOG)) { const matched = entry.keywords.some(keyword => { if (typeof keyword !== 'string') return false; const normKeyword = keyword.toLowerCase(); return normalized.includes(normKeyword) || normKeyword.includes(normalized); }); if (matched) { best = entry; break; } } return best || SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID]; } function deriveResourceSegments(meta, schema, collectionName) { const defaultResource = (collectionName || '').replace(/^\//, ''); let resourceSegment = defaultResource; let versionSegment = null; if (meta && typeof meta === 'object') { if (typeof meta.id === 'string' && meta.id.trim()) { const parts = meta.id.trim().split('.'); if (parts.length > 0 && parts[0]) { resourceSegment = parts[0]; } if (parts.length > 1) { versionSegment = parts.slice(1).join('.'); } } else if (typeof meta.name === 'string' && meta.name.trim()) { resourceSegment = meta.name.trim(); } } if (!resourceSegment && schema && schema.title) { resourceSegment = kebabCase(schema.title); } return { resourceSegment: resourceSegment || defaultResource || 'resource', versionSegment: versionSegment, }; } function buildListPath(resourceSegment, versionSegment) { if (versionSegment) { return `/${resourceSegment}/${versionSegment}`; } return `/${resourceSegment}`; } function buildSinglePath(resourceSegment) { return `/${resourceSegment}`; } function buildDetailPath(resourceSegment, versionSegment, identityField) { if (versionSegment) { return `/${resourceSegment}/${versionSegment}/{${identityField}}`; } return `/${resourceSegment}/{${identityField}}`; } function buildCreateRequestSchema(resourceName) { const ref = `#/components/schemas/${resourceName}`; return { type: 'object', required: ['version', 'act', 'data'], properties: { version: { type: 'string', default: '1.0.0' }, act: { type: 'integer', default: -1 }, data: { type: 'array', items: { $ref: ref }, }, }, }; } function buildDeleteRequestSchema(identityField) { return { type: 'object', required: ['version', 'data'], properties: { version: { type: 'string', default: '1.0.0' }, data: { type: 'array', items: { type: 'string', description: `Value of ${identityField}`, }, }, }, }; } function buildListRequestSchema() { return { type: 'object', properties: { version: { type: 'string', default: '1.0.0' }, data: { type: 'object', properties: { pageNo: { type: 'integer', default: 1 }, pageSize: { type: 'integer', default: 20 }, isSearchCount: { type: 'boolean', default: true }, filters: { type: 'array', items: { type: 'object' }, }, }, }, }, }; } function buildListResponseSchema(resourceName) { const ref = `#/components/schemas/${resourceName}`; return { type: 'object', properties: { code: { type: 'integer' }, message: { type: 'string' }, data: { type: 'object', properties: { list: { type: 'array', items: { $ref: ref }, }, total: { type: 'integer' }, }, }, }, }; } function looksLikeDmsSchema(candidate) { if (!candidate || typeof candidate !== 'object') { return false; } if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) { return true; } if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') { return true; } return false; }