697 lines
20 KiB
JavaScript
697 lines
20 KiB
JavaScript
'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;
|
||
}
|