From 8b4351ca963fabbace45fa124ec28a027ad0d240 Mon Sep 17 00:00:00 2001 From: ruoyunbai <19376215@buaa.edu.cn> Date: Mon, 17 Nov 2025 15:12:52 +0800 Subject: [PATCH] flow --- .node-red-data/projects/zsy/flows.json | 183 ++++++++++++++++--------- 1 file changed, 116 insertions(+), 67 deletions(-) diff --git a/.node-red-data/projects/zsy/flows.json b/.node-red-data/projects/zsy/flows.json index 6c3ae11..ed63970 100644 --- a/.node-red-data/projects/zsy/flows.json +++ b/.node-red-data/projects/zsy/flows.json @@ -2139,11 +2139,11 @@ "upload": false, "skipBodyParsing": false, "swaggerDoc": "", - "x": 120, - "y": 140, + "x": 60, + "y": 100, "wires": [ [ - "f414ed448fcf544b" + "211c041ab58c88f2" ] ] }, @@ -2154,7 +2154,7 @@ "name": "", "statusCode": "msg.testcase.results", "headers": {}, - "x": 700, + "x": 900, "y": 260, "wires": [] }, @@ -2163,21 +2163,21 @@ "type": "function", "z": "78d15f59dee4b6d8", "name": "dms转化为oas", - "func": "'use strict';\n\nconst DEFAULT_OPENAPI_VERSION = '3.0.1';\nconst DEFAULT_API_VERSION = '1.0.0';\nconst BASE_PREFIX = 'https://www.dev.ideas.cnpc/api/dms';\nconst DEFAULT_SERVICE_ID = 'well_kd_wellbore_ideas01';\nconst DEFAULT_API_SEGMENT = 'v1';\nconst FALLBACK_BASE_URL = `${BASE_PREFIX}/${DEFAULT_SERVICE_ID}/${DEFAULT_API_SEGMENT}`;\nconst FALLBACK_HEADERS = {\n 'Content-Type': 'application/json',\n 'Authorization': '1',\n 'Dataregion': 'ZZLH',\n};\nconst SERVICE_DOMAIN_CATALOG = {\n 'well_kd_wellbore_ideas01': {\n name: '井筒',\n id: 'well_kd_wellbore_ideas01',\n keywords: ['wb', 'wb_dr', 'wb_ml', 'wb_wl', 'wb_tp', 'wb_dh', 'wb_fr'],\n },\n 'geo_kd_res_ideas01': {\n name: '采油气',\n id: 'geo_kd_res_ideas01',\n keywords: ['pc', 'pc_op', 'pc_oe', 'pc_ge'],\n },\n 'kd_cr_ideas01': {\n name: '分析化验',\n id: 'kd_cr_ideas01',\n keywords: ['cr', 'cr_se'],\n },\n 'kd_rs_ideas01': {\n name: '油气藏',\n id: 'kd_rs_ideas01',\n keywords: ['rs', 'rs_rd', 'rs_rm', 'rs_gs', 'rs_in'],\n },\n};\n\nlet schema;\ntry {\n // 优先使用 HTTP In 提供的 req.body.schema,缺省时回退到 msg.payload。\n const schemaInput = extractSchemaInput(msg);\n schema = parseSchema(schemaInput);\n} catch (error) {\n node.error(`DMS -> Swagger 解析失败:${error.message}`, msg);\n msg.error = error.message;\n return msg;\n}\n\nconst resourceTitle = typeof schema.title === 'string' && schema.title.trim()\n ? schema.title.trim()\n : 'DMS Resource';\n\nconst resourceName = pascalCase(resourceTitle);\nconst collectionName = pluralize(kebabCase(resourceTitle));\nconst identityField = Array.isArray(schema.identityId) && schema.identityId.length > 0\n ? String(schema.identityId[0])\n : 'id';\n\nconst schemaComponent = buildComponentSchema(resourceName, schema);\nconst identitySchema = schemaComponent.properties && schemaComponent.properties[identityField]\n ? clone(schemaComponent.properties[identityField])\n : { type: 'string' };\n\nconst crudConfig = Object.assign({}, msg.crudConfig || {});\n\nconst dmsMeta = extractDmsMeta(msg);\nmsg.dms_meta = dmsMeta;\n\nconst serviceInfo = resolveServiceInfo(dmsMeta && dmsMeta.domain);\nconst apiVersionSegment = DEFAULT_API_SEGMENT;\nconst serviceId = serviceInfo.id;\n\nconst { resourceSegment, versionSegment } = deriveResourceSegments(dmsMeta, schema, collectionName);\nconst listPath = buildListPath(resourceSegment, versionSegment);\nconst singlePath = buildSinglePath(resourceSegment);\nconst detailPath = buildDetailPath(resourceSegment, versionSegment, identityField);\nconst createRequestSchema = buildCreateRequestSchema(resourceName);\nconst updateRequestSchema = buildCreateRequestSchema(resourceName);\nconst deleteRequestSchema = buildDeleteRequestSchema(identityField);\nconst listRequestSchema = buildListRequestSchema();\nconst listResponseSchema = buildListResponseSchema(resourceName);\n\nconst openApiDocument = {\n openapi: DEFAULT_OPENAPI_VERSION,\n info: {\n title: `${resourceTitle} API`,\n version: DEFAULT_API_VERSION,\n description: schema.description || `${schema.$id || ''}`.trim(),\n 'x-dms-sourceId': schema.$id || undefined,\n },\n paths: buildCrudPaths({\n resourceName,\n identityField,\n identitySchema,\n listPath,\n createPath: singlePath,\n detailPath,\n createRequestSchema,\n updateRequestSchema,\n deleteRequestSchema,\n listRequestSchema,\n listResponseSchema,\n }),\n components: {\n schemas: {\n [resourceName]: schemaComponent,\n },\n },\n};\n\nif (!crudConfig.baseUrl) {\n crudConfig.baseUrl = `${BASE_PREFIX}/${serviceId}/${apiVersionSegment}`;\n}\n\nconst headers = Object.assign({}, FALLBACK_HEADERS, crudConfig.headers || {});\nconst dataRegionValue = extractDataRegion(schema, headers.Dataregion);\n\nif (dataRegionValue) {\n headers.Dataregion = dataRegionValue;\n crudConfig.dataRegion = dataRegionValue;\n}\n\ncrudConfig.headers = headers;\ncrudConfig.identityField = crudConfig.identityField || identityField;\n\ncrudConfig.service = crudConfig.service || {\n id: serviceId,\n name: serviceInfo.name,\n};\n\ncrudConfig.list = crudConfig.list || {};\nif (!crudConfig.list.path) {\n crudConfig.list.path = listPath;\n}\ncrudConfig.list.method = crudConfig.list.method || 'POST';\n\ncrudConfig.create = crudConfig.create || {};\nif (!crudConfig.create.path) {\n crudConfig.create.path = singlePath;\n}\ncrudConfig.create.method = crudConfig.create.method || 'POST';\n\ncrudConfig.delete = crudConfig.delete || {};\nif (!crudConfig.delete.path) {\n crudConfig.delete.path = singlePath;\n}\ncrudConfig.delete.method = crudConfig.delete.method || 'POST';\n\ncrudConfig.detailPath = crudConfig.detailPath || detailPath;\ncrudConfig.version = crudConfig.version || versionSegment;\n\nmsg.crudConfig = crudConfig;\nmsg.headers = Object.assign({}, headers);\n\nmsg.oas_def = openApiDocument;\nmsg.payload = openApiDocument;\nreturn msg;\n\nfunction extractDataRegion(dmsSchema, fallback) {\n if (!dmsSchema || typeof dmsSchema !== 'object') {\n return fallback;\n }\n\n if (typeof dmsSchema.dataRegion === 'string' && dmsSchema.dataRegion.trim()) {\n return dmsSchema.dataRegion.trim();\n }\n\n if (dmsSchema.defaultShow && Array.isArray(dmsSchema.defaultShow)) {\n const candidate = dmsSchema.defaultShow.find(item => item && typeof item === 'string' && item.toLowerCase().includes('dataregion'));\n if (candidate) {\n return fallback;\n }\n }\n\n const props = dmsSchema.properties;\n if (props && typeof props === 'object' && props.dataRegion) {\n if (typeof props.dataRegion.default === 'string' && props.dataRegion.default.trim()) {\n return props.dataRegion.default.trim();\n }\n }\n\n return fallback;\n}\n\nfunction extractSchemaInput(message) {\n if (message && message.req && message.req.body && typeof message.req.body === 'object') {\n if (message.req.body.schema !== undefined) {\n return message.req.body.schema;\n }\n if (looksLikeDmsSchema(message.req.body)) {\n return message.req.body;\n }\n }\n\n if (message && message.payload !== undefined) {\n if (message.payload && typeof message.payload === 'object') {\n if (message.payload.schema !== undefined) {\n return message.payload.schema;\n }\n if (looksLikeDmsSchema(message.payload)) {\n return message.payload;\n }\n }\n return message.payload;\n }\n\n throw new Error('未找到schema,请在请求体的schema字段或msg.payload中提供');\n}\n\nfunction parseSchema(source) {\n if (typeof source === 'string') {\n try {\n return JSON.parse(source);\n } catch (error) {\n throw new Error(`JSON 解析失败:${error.message}`);\n }\n }\n\n if (!source || typeof source !== 'object') {\n throw new Error('schema 必须是 DMS 定义对象或 JSON 字符串');\n }\n\n return source;\n}\n\nfunction buildComponentSchema(resourceName, dmsSchema) {\n const { properties = {}, required = [], groupView, identityId, naturalKey, defaultShow } = dmsSchema;\n const openApiProps = {};\n\n for (const [propName, propSchema] of Object.entries(properties)) {\n openApiProps[propName] = mapProperty(propSchema);\n }\n\n return {\n type: 'object',\n required: Array.isArray(required) ? required.slice() : [],\n properties: openApiProps,\n description: dmsSchema.description || dmsSchema.title || resourceName,\n 'x-dms-groupView': groupView || undefined,\n 'x-dms-identityId': identityId || undefined,\n 'x-dms-naturalKey': naturalKey || undefined,\n 'x-dms-defaultShow': defaultShow || undefined,\n };\n}\n\nfunction mapProperty(propSchema) {\n if (!propSchema || typeof propSchema !== 'object') {\n return { type: 'string' };\n }\n\n const result = {};\n\n const type = normalizeType(propSchema.type);\n result.type = type.type;\n if (type.format) {\n result.format = type.format;\n }\n if (type.items) {\n result.items = type.items;\n }\n\n if (propSchema.description) {\n result.description = propSchema.description;\n } else if (propSchema.title) {\n result.description = propSchema.title;\n }\n\n if (propSchema.enum) {\n result.enum = propSchema.enum.slice();\n }\n\n if (propSchema.default !== undefined) {\n result.default = propSchema.default;\n }\n\n if (propSchema.mask) {\n result['x-dms-mask'] = propSchema.mask;\n }\n if (propSchema.geom) {\n result['x-dms-geom'] = propSchema.geom;\n }\n if (propSchema.title) {\n result['x-dms-title'] = propSchema.title;\n }\n if (propSchema.type) {\n result['x-dms-originalType'] = propSchema.type;\n }\n\n return result;\n}\n\nfunction normalizeType(typeValue) {\n const normalized = typeof typeValue === 'string' ? typeValue.toLowerCase() : undefined;\n\n switch (normalized) {\n case 'number':\n case 'integer':\n case 'long':\n case 'float':\n case 'double':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'array':\n return { type: 'array', items: { type: 'string' } };\n case 'date':\n return { type: 'string', format: 'date' };\n case 'date-time':\n return { type: 'string', format: 'date-time' };\n case 'object':\n return { type: 'object' };\n case 'string':\n default:\n return { type: 'string' };\n }\n}\n\nfunction buildCrudPaths({\n resourceName,\n identityField,\n identitySchema,\n listPath,\n createPath,\n detailPath,\n createRequestSchema,\n updateRequestSchema,\n deleteRequestSchema,\n listRequestSchema,\n listResponseSchema,\n}) {\n const ref = `#/components/schemas/${resourceName}`;\n const paths = {};\n\n const normalisedListPath = normalisePath(listPath);\n const normalisedCreatePath = normalisePath(createPath);\n const normalisedDetailPath = detailPath ? normalisePath(detailPath) : null;\n\n paths[normalisedListPath] = {\n post: {\n operationId: `list${resourceName}s`,\n summary: `List ${resourceName} resources`,\n requestBody: {\n required: false,\n content: {\n 'application/json': {\n schema: listRequestSchema,\n },\n },\n },\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: listResponseSchema || {\n type: 'array',\n items: { $ref: ref },\n },\n },\n },\n },\n },\n },\n };\n\n paths[normalisedCreatePath] = {\n post: {\n operationId: `create${resourceName}`,\n summary: `Create a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: createRequestSchema,\n },\n },\n },\n responses: {\n 201: {\n description: 'Created',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n put: {\n operationId: `update${resourceName}`,\n summary: `Update a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: updateRequestSchema,\n },\n },\n },\n responses: {\n 200: {\n description: 'Successful update',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n delete: {\n operationId: `delete${resourceName}`,\n summary: `Delete ${resourceName} resources`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: deleteRequestSchema,\n },\n },\n },\n responses: {\n 200: { description: 'Deleted' },\n },\n },\n };\n\n if (normalisedDetailPath) {\n paths[normalisedDetailPath] = {\n parameters: [\n {\n name: identityField,\n in: 'path',\n required: true,\n schema: identitySchema,\n },\n ],\n get: {\n operationId: `get${resourceName}`,\n summary: `Get a single ${resourceName}`,\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n 404: { description: `${resourceName} not found` },\n },\n },\n };\n }\n\n return paths;\n}\n\nfunction normalisePath(path) {\n if (!path) {\n return '/';\n }\n return path.startsWith('/') ? path : `/${path}`;\n}\n\nfunction pascalCase(input) {\n return input\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .split(' ')\n .filter(Boolean)\n .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join('') || 'Resource';\n}\n\nfunction kebabCase(input) {\n return input\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/[^a-zA-Z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .toLowerCase() || 'resource';\n}\n\nfunction pluralize(word) {\n if (word.endsWith('s')) {\n return word;\n }\n if (word.endsWith('y')) {\n return word.slice(0, -1) + 'ies';\n }\n return `${word}s`;\n}\n\nfunction clone(value) {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n\nfunction extractDmsMeta(message) {\n const candidateReq = message && message.req && message.req.body && message.req.body.dms_meta;\n const parsedReq = parseMetaCandidate(candidateReq);\n if (parsedReq) {\n return parsedReq;\n }\n\n const candidateMsg = message && message.dms_meta;\n const parsedMsg = parseMetaCandidate(candidateMsg);\n if (parsedMsg) {\n return parsedMsg;\n }\n\n return null;\n}\n\nfunction parseMetaCandidate(candidate) {\n if (!candidate) {\n return null;\n }\n if (typeof candidate === 'string') {\n try {\n const value = JSON.parse(candidate);\n return parseMetaCandidate(value);\n } catch (err) {\n return null;\n }\n }\n if (typeof candidate === 'object') {\n return clone(candidate);\n }\n return null;\n}\n\nfunction resolveServiceInfo(domain) {\n if (!domain || typeof domain !== 'string') {\n return SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n }\n const normalized = domain.toLowerCase();\n let best = null;\n for (const entry of Object.values(SERVICE_DOMAIN_CATALOG)) {\n const matched = entry.keywords.some(keyword => {\n if (typeof keyword !== 'string') return false;\n const normKeyword = keyword.toLowerCase();\n return normalized.includes(normKeyword) || normKeyword.includes(normalized);\n });\n if (matched) {\n best = entry;\n break;\n }\n }\n return best || SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n}\n\nfunction deriveResourceSegments(meta, schema, collectionName) {\n const defaultResource = (collectionName || '').replace(/^\\//, '');\n let resourceSegment = defaultResource;\n let versionSegment = null;\n\n if (meta && typeof meta === 'object') {\n if (typeof meta.id === 'string' && meta.id.trim()) {\n const parts = meta.id.trim().split('.');\n if (parts.length > 0 && parts[0]) {\n resourceSegment = parts[0];\n }\n if (parts.length > 1) {\n versionSegment = parts.slice(1).join('.');\n }\n } else if (typeof meta.name === 'string' && meta.name.trim()) {\n resourceSegment = meta.name.trim();\n }\n }\n\n if (!resourceSegment && schema && schema.title) {\n resourceSegment = kebabCase(schema.title);\n }\n\n return {\n resourceSegment: resourceSegment || defaultResource || 'resource',\n versionSegment: versionSegment,\n };\n}\n\nfunction buildListPath(resourceSegment, versionSegment) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}`;\n }\n return `/${resourceSegment}`;\n}\n\nfunction buildSinglePath(resourceSegment) {\n return `/${resourceSegment}`;\n}\n\nfunction buildDetailPath(resourceSegment, versionSegment, identityField) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}/{${identityField}}`;\n }\n return `/${resourceSegment}/{${identityField}}`;\n}\n\nfunction buildCreateRequestSchema(resourceName) {\n const ref = `#/components/schemas/${resourceName}`;\n return {\n type: 'object',\n required: ['version', 'act', 'data'],\n properties: {\n version: { type: 'string', default: '1.0.0' },\n act: { type: 'integer', default: -1 },\n data: {\n type: 'array',\n items: { $ref: ref },\n },\n },\n };\n}\n\nfunction buildDeleteRequestSchema(identityField) {\n return {\n type: 'object',\n required: ['version', 'data'],\n properties: {\n version: { type: 'string', default: '1.0.0' },\n data: {\n type: 'array',\n items: {\n type: 'string',\n description: `Value of ${identityField}`,\n },\n },\n },\n };\n}\n\nfunction buildListRequestSchema() {\n return {\n type: 'object',\n properties: {\n version: { type: 'string', default: '1.0.0' },\n data: {\n type: 'object',\n properties: {\n pageNo: { type: 'integer', default: 1 },\n pageSize: { type: 'integer', default: 20 },\n isSearchCount: { type: 'boolean', default: true },\n filters: {\n type: 'array',\n items: { type: 'object' },\n },\n },\n },\n },\n };\n}\n\nfunction buildListResponseSchema(resourceName) {\n const ref = `#/components/schemas/${resourceName}`;\n return {\n type: 'object',\n properties: {\n code: { type: 'integer' },\n message: { type: 'string' },\n data: {\n type: 'object',\n properties: {\n list: {\n type: 'array',\n items: { $ref: ref },\n },\n total: { type: 'integer' },\n },\n },\n },\n };\n}\n\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n", + "func": "'use strict';\n\nconst DEFAULT_OPENAPI_VERSION = '3.0.1';\nconst DEFAULT_API_VERSION = '1.0.0';\nconst BASE_PREFIX = 'https://www.dev.ideas.cnpc/api/dms';\nconst DEFAULT_SERVICE_ID = 'well_kd_wellbore_ideas01';\nconst DEFAULT_API_SEGMENT = 'v1';\nconst FALLBACK_BASE_URL = `${BASE_PREFIX}/${DEFAULT_SERVICE_ID}/${DEFAULT_API_SEGMENT}`;\nconst FALLBACK_HEADERS = {\n 'Content-Type': 'application/json',\n 'Authorization': '1',\n 'Dataregion': 'ZZLH',\n};\nconst SERVICE_DOMAIN_CATALOG = {\n 'well_kd_wellbore_ideas01': {\n name: '井筒',\n id: 'well_kd_wellbore_ideas01',\n keywords: ['wb', 'wb_dr', 'wb_ml', 'wb_wl', 'wb_tp', 'wb_dh', 'wb_fr'],\n },\n 'geo_kd_res_ideas01': {\n name: '采油气',\n id: 'geo_kd_res_ideas01',\n keywords: ['pc', 'pc_op', 'pc_oe', 'pc_ge'],\n },\n 'kd_cr_ideas01': {\n name: '分析化验',\n id: 'kd_cr_ideas01',\n keywords: ['cr', 'cr_se'],\n },\n 'kd_rs_ideas01': {\n name: '油气藏',\n id: 'kd_rs_ideas01',\n keywords: ['rs', 'rs_rd', 'rs_rm', 'rs_gs', 'rs_in'],\n },\n};\n\nlet schema;\ntry {\n // 优先使用 HTTP In 提供的 req.body.schema,缺省时回退到 msg.payload。\n const schemaInput = extractSchemaInput(msg);\n schema = parseSchema(schemaInput);\n} catch (error) {\n node.error(`DMS -> Swagger 解析失败:${error.message}`, msg);\n msg.error = error.message;\n return msg;\n}\n\nconst resourceTitle = typeof schema.title === 'string' && schema.title.trim()\n ? schema.title.trim()\n : 'DMS Resource';\n\nconst resourceName = pascalCase(resourceTitle);\nconst collectionName = pluralize(kebabCase(resourceTitle));\nconst identityField = Array.isArray(schema.identityId) && schema.identityId.length > 0\n ? String(schema.identityId[0])\n : 'id';\n\nconst schemaComponent = buildComponentSchema(resourceName, schema);\nconst identitySchema = schemaComponent.properties && schemaComponent.properties[identityField]\n ? clone(schemaComponent.properties[identityField])\n : { type: 'string' };\n\nconst crudConfig = Object.assign({}, msg.crudConfig || {});\n\nconst dmsMeta = extractDmsMeta(msg);\nmsg.dms_meta = dmsMeta;\n\nconst serviceInfo = resolveServiceInfo(dmsMeta && dmsMeta.domain);\nconst apiVersionSegment = DEFAULT_API_SEGMENT;\nconst serviceId = serviceInfo.id;\n\nconst { resourceSegment, versionSegment } = deriveResourceSegments(dmsMeta, schema, collectionName);\nconst listPath = buildListPath(resourceSegment, versionSegment);\nconst singlePath = buildSinglePath(resourceSegment);\nconst detailPath = buildDetailPath(resourceSegment, versionSegment, identityField);\nconst createRequestSchema = buildCreateRequestSchema(resourceName);\nconst updateRequestSchema = buildCreateRequestSchema(resourceName);\nconst deleteRequestSchema = buildDeleteRequestSchema(identityField);\nconst listRequestSchema = buildListRequestSchema();\nconst listResponseSchema = buildListResponseSchema(resourceName);\n\nconst openApiDocument = {\n openapi: DEFAULT_OPENAPI_VERSION,\n info: {\n title: `${resourceTitle} API`,\n version: DEFAULT_API_VERSION,\n description: schema.description || `${schema.$id || ''}`.trim(),\n 'x-dms-sourceId': schema.$id || undefined,\n },\n paths: buildCrudPaths({\n resourceName,\n identityField,\n identitySchema,\n listPath,\n createPath: singlePath,\n detailPath,\n createRequestSchema,\n updateRequestSchema,\n deleteRequestSchema,\n listRequestSchema,\n listResponseSchema,\n }),\n components: {\n schemas: {\n [resourceName]: schemaComponent,\n },\n },\n};\n\nif (!crudConfig.baseUrl) {\n crudConfig.baseUrl = `${BASE_PREFIX}/${serviceId}/${apiVersionSegment}`;\n}\n\nconst headers = Object.assign({}, FALLBACK_HEADERS, crudConfig.headers || {});\nconst dataRegionValue = extractDataRegion(schema, headers.Dataregion);\n\nif (dataRegionValue) {\n headers.Dataregion = dataRegionValue;\n crudConfig.dataRegion = dataRegionValue;\n}\n\ncrudConfig.headers = headers;\ncrudConfig.identityField = crudConfig.identityField || identityField;\n\ncrudConfig.service = crudConfig.service || {\n id: serviceId,\n name: serviceInfo.name,\n};\n\ncrudConfig.list = crudConfig.list || {};\nif (!crudConfig.list.path) {\n crudConfig.list.path = listPath;\n}\ncrudConfig.list.method = crudConfig.list.method || 'POST';\n\ncrudConfig.create = crudConfig.create || {};\nif (!crudConfig.create.path) {\n crudConfig.create.path = singlePath;\n}\ncrudConfig.create.method = crudConfig.create.method || 'POST';\n\ncrudConfig.delete = crudConfig.delete || {};\nif (!crudConfig.delete.path) {\n crudConfig.delete.path = singlePath;\n}\ncrudConfig.delete.method = crudConfig.delete.method || 'POST';\n\ncrudConfig.detailPath = crudConfig.detailPath || detailPath;\ncrudConfig.version = crudConfig.version || versionSegment;\n\nmsg.crudConfig = crudConfig;\nmsg.headers = Object.assign({}, headers);\n\nmsg.oas_def = openApiDocument;\nmsg.payload = openApiDocument;\nreturn msg;\n\nfunction extractDataRegion(dmsSchema, fallback) {\n if (!dmsSchema || typeof dmsSchema !== 'object') {\n return fallback;\n }\n\n if (typeof dmsSchema.dataRegion === 'string' && dmsSchema.dataRegion.trim()) {\n return dmsSchema.dataRegion.trim();\n }\n\n if (dmsSchema.defaultShow && Array.isArray(dmsSchema.defaultShow)) {\n const candidate = dmsSchema.defaultShow.find(item => item && typeof item === 'string' && item.toLowerCase().includes('dataregion'));\n if (candidate) {\n return fallback;\n }\n }\n\n const props = dmsSchema.properties;\n if (props && typeof props === 'object' && props.dataRegion) {\n if (typeof props.dataRegion.default === 'string' && props.dataRegion.default.trim()) {\n return props.dataRegion.default.trim();\n }\n }\n\n return fallback;\n}\n\nfunction extractSchemaInput(message) {\n if (message && message.req && message.req.body && typeof message.req.body === 'object') {\n if (message.req.body.dms_schema !== undefined) {\n return message.req.body.dms_schema;\n }\n if (looksLikeDmsSchema(message.req.body)) {\n return message.req.body;\n }\n }\n\n if (message && message.payload !== undefined) {\n if (message.payload && typeof message.payload === 'object') {\n if (message.payload.dms_schema !== undefined) {\n return message.payload.dms_schema;\n }\n if (looksLikeDmsSchema(message.payload)) {\n return message.payload;\n }\n }\n return message.payload;\n }\n\n throw new Error('未找到schema,请在请求体的schema字段或msg.payload中提供');\n}\n\nfunction parseSchema(source) {\n if (typeof source === 'string') {\n try {\n return JSON.parse(source);\n } catch (error) {\n throw new Error(`JSON 解析失败:${error.message}`);\n }\n }\n\n if (!source || typeof source !== 'object') {\n throw new Error('schema 必须是 DMS 定义对象或 JSON 字符串');\n }\n\n return source;\n}\n\nfunction buildComponentSchema(resourceName, dmsSchema) {\n const { properties = {}, required = [], groupView, identityId, naturalKey, defaultShow } = dmsSchema;\n const openApiProps = {};\n\n for (const [propName, propSchema] of Object.entries(properties)) {\n openApiProps[propName] = mapProperty(propSchema);\n }\n\n return {\n type: 'object',\n required: Array.isArray(required) ? required.slice() : [],\n properties: openApiProps,\n description: dmsSchema.description || dmsSchema.title || resourceName,\n 'x-dms-groupView': groupView || undefined,\n 'x-dms-identityId': identityId || undefined,\n 'x-dms-naturalKey': naturalKey || undefined,\n 'x-dms-defaultShow': defaultShow || undefined,\n };\n}\n\nfunction mapProperty(propSchema) {\n if (!propSchema || typeof propSchema !== 'object') {\n return { type: 'string' };\n }\n\n const result = {};\n\n const type = normalizeType(propSchema.type);\n result.type = type.type;\n if (type.format) {\n result.format = type.format;\n }\n if (type.items) {\n result.items = type.items;\n }\n\n if (propSchema.description) {\n result.description = propSchema.description;\n } else if (propSchema.title) {\n result.description = propSchema.title;\n }\n\n if (propSchema.enum) {\n result.enum = propSchema.enum.slice();\n }\n\n if (propSchema.default !== undefined) {\n result.default = propSchema.default;\n }\n\n if (propSchema.mask) {\n result['x-dms-mask'] = propSchema.mask;\n }\n if (propSchema.geom) {\n result['x-dms-geom'] = propSchema.geom;\n }\n if (propSchema.title) {\n result['x-dms-title'] = propSchema.title;\n }\n if (propSchema.type) {\n result['x-dms-originalType'] = propSchema.type;\n }\n\n return result;\n}\n\nfunction normalizeType(typeValue) {\n const normalized = typeof typeValue === 'string' ? typeValue.toLowerCase() : undefined;\n\n switch (normalized) {\n case 'number':\n case 'integer':\n case 'long':\n case 'float':\n case 'double':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'array':\n return { type: 'array', items: { type: 'string' } };\n case 'date':\n return { type: 'string', format: 'date' };\n case 'date-time':\n return { type: 'string', format: 'date-time' };\n case 'object':\n return { type: 'object' };\n case 'string':\n default:\n return { type: 'string' };\n }\n}\n\nfunction buildCrudPaths({\n resourceName,\n identityField,\n identitySchema,\n listPath,\n createPath,\n detailPath,\n createRequestSchema,\n updateRequestSchema,\n deleteRequestSchema,\n listRequestSchema,\n listResponseSchema,\n}) {\n const ref = `#/components/schemas/${resourceName}`;\n const paths = {};\n\n const normalisedListPath = normalisePath(listPath);\n const normalisedCreatePath = normalisePath(createPath);\n const normalisedDetailPath = detailPath ? normalisePath(detailPath) : null;\n\n paths[normalisedListPath] = {\n post: {\n operationId: `list${resourceName}s`,\n summary: `List ${resourceName} resources`,\n requestBody: {\n required: false,\n content: {\n 'application/json': {\n schema: listRequestSchema,\n },\n },\n },\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: listResponseSchema || {\n type: 'array',\n items: { $ref: ref },\n },\n },\n },\n },\n },\n },\n };\n\n paths[normalisedCreatePath] = {\n post: {\n operationId: `create${resourceName}`,\n summary: `Create a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: createRequestSchema,\n },\n },\n },\n responses: {\n 201: {\n description: 'Created',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n put: {\n operationId: `update${resourceName}`,\n summary: `Update a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: updateRequestSchema,\n },\n },\n },\n responses: {\n 200: {\n description: 'Successful update',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n delete: {\n operationId: `delete${resourceName}`,\n summary: `Delete ${resourceName} resources`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: deleteRequestSchema,\n },\n },\n },\n responses: {\n 200: { description: 'Deleted' },\n },\n },\n };\n\n if (normalisedDetailPath) {\n paths[normalisedDetailPath] = {\n parameters: [\n {\n name: identityField,\n in: 'path',\n required: true,\n schema: identitySchema,\n },\n ],\n get: {\n operationId: `get${resourceName}`,\n summary: `Get a single ${resourceName}`,\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n 404: { description: `${resourceName} not found` },\n },\n },\n };\n }\n\n return paths;\n}\n\nfunction normalisePath(path) {\n if (!path) {\n return '/';\n }\n return path.startsWith('/') ? path : `/${path}`;\n}\n\nfunction pascalCase(input) {\n return input\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .split(' ')\n .filter(Boolean)\n .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join('') || 'Resource';\n}\n\nfunction kebabCase(input) {\n return input\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/[^a-zA-Z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .toLowerCase() || 'resource';\n}\n\nfunction pluralize(word) {\n if (word.endsWith('s')) {\n return word;\n }\n if (word.endsWith('y')) {\n return word.slice(0, -1) + 'ies';\n }\n return `${word}s`;\n}\n\nfunction clone(value) {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n\nfunction extractDmsMeta(message) {\n const candidateReq = message && message.req && message.req.body && message.req.body.dms_meta;\n const parsedReq = parseMetaCandidate(candidateReq);\n if (parsedReq) {\n return parsedReq;\n }\n\n const candidateMsg = message && message.dms_meta;\n const parsedMsg = parseMetaCandidate(candidateMsg);\n if (parsedMsg) {\n return parsedMsg;\n }\n\n return null;\n}\n\nfunction parseMetaCandidate(candidate) {\n if (!candidate) {\n return null;\n }\n if (typeof candidate === 'string') {\n try {\n const value = JSON.parse(candidate);\n return parseMetaCandidate(value);\n } catch (err) {\n return null;\n }\n }\n if (typeof candidate === 'object') {\n return clone(candidate);\n }\n return null;\n}\n\nfunction resolveServiceInfo(domain) {\n if (!domain || typeof domain !== 'string') {\n return SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n }\n const normalized = domain.toLowerCase();\n let best = null;\n for (const entry of Object.values(SERVICE_DOMAIN_CATALOG)) {\n const matched = entry.keywords.some(keyword => {\n if (typeof keyword !== 'string') return false;\n const normKeyword = keyword.toLowerCase();\n return normalized.includes(normKeyword) || normKeyword.includes(normalized);\n });\n if (matched) {\n best = entry;\n break;\n }\n }\n return best || SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n}\n\nfunction deriveResourceSegments(meta, schema, collectionName) {\n const defaultResource = (collectionName || '').replace(/^\\//, '');\n let resourceSegment = defaultResource;\n let versionSegment = null;\n\n if (meta && typeof meta === 'object') {\n if (typeof meta.id === 'string' && meta.id.trim()) {\n const parts = meta.id.trim().split('.');\n if (parts.length > 0 && parts[0]) {\n resourceSegment = parts[0];\n }\n if (parts.length > 1) {\n versionSegment = parts.slice(1).join('.');\n }\n } else if (typeof meta.name === 'string' && meta.name.trim()) {\n resourceSegment = meta.name.trim();\n }\n }\n\n if (!resourceSegment && schema && schema.title) {\n resourceSegment = kebabCase(schema.title);\n }\n\n return {\n resourceSegment: resourceSegment || defaultResource || 'resource',\n versionSegment: versionSegment,\n };\n}\n\nfunction buildListPath(resourceSegment, versionSegment) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}`;\n }\n return `/${resourceSegment}`;\n}\n\nfunction buildSinglePath(resourceSegment) {\n return `/${resourceSegment}`;\n}\n\nfunction buildDetailPath(resourceSegment, versionSegment, identityField) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}/{${identityField}}`;\n }\n return `/${resourceSegment}/{${identityField}}`;\n}\n\nfunction buildCreateRequestSchema(resourceName) {\n const ref = `#/components/schemas/${resourceName}`;\n return {\n type: 'object',\n required: ['version', 'act', 'data'],\n properties: {\n version: { type: 'string', default: '1.0.0' },\n act: { type: 'integer', default: -1 },\n data: {\n type: 'array',\n items: { $ref: ref },\n },\n },\n };\n}\n\nfunction buildDeleteRequestSchema(identityField) {\n return {\n type: 'object',\n required: ['version', 'data'],\n properties: {\n version: { type: 'string', default: '1.0.0' },\n data: {\n type: 'array',\n items: {\n type: 'string',\n description: `Value of ${identityField}`,\n },\n },\n },\n };\n}\n\nfunction buildListRequestSchema() {\n return {\n type: 'object',\n properties: {\n version: { type: 'string', default: '1.0.0' },\n data: {\n type: 'object',\n properties: {\n pageNo: { type: 'integer', default: 1 },\n pageSize: { type: 'integer', default: 20 },\n isSearchCount: { type: 'boolean', default: true },\n filters: {\n type: 'array',\n items: { type: 'object' },\n },\n },\n },\n },\n };\n}\n\nfunction buildListResponseSchema(resourceName) {\n const ref = `#/components/schemas/${resourceName}`;\n return {\n type: 'object',\n properties: {\n code: { type: 'integer' },\n message: { type: 'string' },\n data: {\n type: 'object',\n properties: {\n list: {\n type: 'array',\n items: { $ref: ref },\n },\n total: { type: 'integer' },\n },\n },\n },\n };\n}\n\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 280, - "y": 200, + "x": 300, + "y": 80, "wires": [ [ "dfe6ed572461c4a5", - "5231ed8a796d6f17", "6ab9403df07fcefa", - "9c84414e55654e6a" + "9c84414e55654e6a", + "5231ed8a796d6f17" ] ] }, @@ -2185,7 +2185,7 @@ "id": "ab16f5ccd9266e28", "type": "inject", "z": "78d15f59dee4b6d8", - "name": "", + "name": "dms", "props": [ { "p": "req.body.schema", @@ -2207,11 +2207,11 @@ "once": false, "onceDelay": 0.1, "topic": "", - "x": 170, - "y": 280, + "x": 70, + "y": 340, "wires": [ [ - "f414ed448fcf544b" + "211c041ab58c88f2" ] ] }, @@ -2228,8 +2228,8 @@ "targetType": "jsonata", "statusVal": "", "statusType": "auto", - "x": 600, - "y": 140, + "x": 740, + "y": 20, "wires": [] }, { @@ -2264,27 +2264,6 @@ "y": 460, "wires": [] }, - { - "id": "5231ed8a796d6f17", - "type": "function", - "z": "78d15f59dee4b6d8", - "name": "分页参数检查", - "func": "'use strict';\n\nconst TEST_METADATA = {\n id: 'TC-DMS-PAGINATION-001',\n name: '分页参数检查',\n description: \"检查API请求参数中是否包含标准分页参数:pageNo、pageSize和isSearchCount。只有名称含有'查询'、'列表'等并且不含有'详情'一类的API才应用此验证。\",\n severity: 'MEDIUM',\n tags: ['pagination', 'params', 'backend-guide'],\n skip_execution: true,\n};\n\nconst globalApiSpec = msg && msg.oas_def ? msg.oas_def : null;\n\nif (!globalApiSpec || typeof globalApiSpec !== 'object' || !globalApiSpec.paths) {\n const errorMessage = '分页参数检查无法运行:msg.oas_def 缺失、不是对象或不包含 paths';\n node.error(errorMessage, msg);\n msg.error = errorMessage;\n msg.testcase = {\n metadata: TEST_METADATA,\n applies: false,\n skipped: true,\n results: [],\n };\n return msg;\n}\n\nconst allResults = [];\nlet testApplied = false;\n\n// 遍历所有路径和方法\nfor (const path in globalApiSpec.paths) {\n if (Object.prototype.hasOwnProperty.call(globalApiSpec.paths, path)) {\n const pathItem = globalApiSpec.paths[path];\n for (const method in pathItem) {\n if (Object.prototype.hasOwnProperty.call(pathItem, method) && ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method.toLowerCase())) {\n\n const operation = pathItem[method];\n if (!operation || typeof operation !== 'object') continue;\n\n // 构建 endpointSpec\n const endpointSpec = buildEndpointSpec(path, method, pathItem, operation);\n\n // 检查测试是否适用\n if (shouldApplyTest(endpointSpec)) {\n testApplied = true;\n const evaluation = runPaginationCheck(endpointSpec, globalApiSpec);\n allResults.push(...evaluation.results);\n }\n }\n }\n }\n}\n\nmsg.testcase = {\n metadata: TEST_METADATA,\n applies: testApplied,\n skipped: !testApplied,\n results: allResults,\n};\nmsg.payload = msg.testcase;\nreturn msg;\n\nfunction buildEndpointSpec(path, method, pathItem, operation) {\n const combinedParameters = [];\n if (Array.isArray(pathItem.parameters)) {\n combinedParameters.push(...pathItem.parameters);\n }\n if (Array.isArray(operation.parameters)) {\n combinedParameters.push(...operation.parameters);\n }\n\n const spec = deepClone(operation);\n spec.method = method;\n spec.path = path;\n if (combinedParameters.length > 0) {\n spec.parameters = mergeParameters(combinedParameters);\n }\n return spec;\n}\n\nfunction shouldApplyTest(spec) {\n const method = (spec.method || '').toLowerCase();\n if (method !== 'get' && method !== 'post') {\n return false;\n }\n\n const path = spec.path || '';\n const summary = spec.summary || '';\n const description = spec.description || '';\n const operationId = spec.operationId || '';\n\n const includeKeywords = ['查询', '列表', '分页', 'page', 'list', 'query', 'search', 'find'];\n const excludeKeywords = ['详情', '明细', 'detail', 'info', 'get by id', 'getbyid', '查看'];\n\n const apiDescriptionText = `${summary} ${description} ${operationId} ${path}`.toLowerCase();\n\n const containsInclude = includeKeywords.some(keyword => apiDescriptionText.includes(keyword.toLowerCase()));\n if (!containsInclude) {\n return false;\n }\n\n // 检查是否是获取单个资源的 'get'\n if (method === 'get' && path.includes('{') && path.includes('}')) {\n const getByIdHints = ['get', 'detail', 'info', '查看'];\n const isGetById = getByIdHints.some(hint => apiDescriptionText.includes(hint));\n if (isGetById) return false;\n }\n\n const containsExclude = excludeKeywords.some(keyword => apiDescriptionText.includes(keyword.toLowerCase()));\n return !containsExclude;\n}\n\nfunction extractEndpointSpec(message, apiSpec) {\n // This function is no longer needed as we iterate through all paths and methods.\n // Kept for potential future use if a single endpoint needs to be targeted.\n return null;\n}\n\nfunction resolveEndpointInfo(message) {\n // This function is no longer needed as we iterate through all paths and methods.\n return null;\n}\n\nfunction mergeParameters(parameters) {\n const merged = new Map();\n for (const param of parameters) {\n if (!param || typeof param !== 'object') {\n continue;\n }\n const name = param.name || '';\n const location = param.in || '';\n const key = `${location}:${name}`;\n if (!merged.has(key)) {\n merged.set(key, deepClone(param));\n }\n }\n return Array.from(merged.values());\n}\n\nfunction runPaginationCheck(spec, apiSpec) {\n const results = [];\n const path = spec.path || '';\n const method = (spec.method || '').toLowerCase();\n\n let foundPageNo = false;\n let foundPageSize = false;\n let foundIsSearchCount = false;\n\n if (Array.isArray(spec.parameters)) {\n for (const param of spec.parameters) {\n if (!param || typeof param !== 'object') {\n continue;\n }\n const location = (param.in || '').toLowerCase();\n if (location !== 'query') {\n continue;\n }\n\n if (param.name === 'pageNo') {\n foundPageNo = true;\n } else if (param.name === 'pageSize') {\n foundPageSize = true;\n } else if (param.name === 'isSearchCount') {\n foundIsSearchCount = true;\n }\n }\n }\n\n if (method === 'post' && spec.requestBody && typeof spec.requestBody === 'object') {\n const { content } = spec.requestBody;\n if (content && typeof content === 'object') {\n for (const mediaType of Object.keys(content)) {\n const mediaDefinition = content[mediaType];\n if (!mediaDefinition || typeof mediaDefinition !== 'object') {\n continue;\n }\n const resolvedSchema = resolveSchema(mediaDefinition.schema, apiSpec);\n const properties = resolvedSchema && resolvedSchema.properties;\n if (!properties || typeof properties !== 'object') {\n continue;\n }\n\n if (Object.prototype.hasOwnProperty.call(properties, 'pageNo')) {\n foundPageNo = true;\n }\n if (Object.prototype.hasOwnProperty.call(properties, 'pageSize')) {\n foundPageSize = true;\n }\n if (Object.prototype.hasOwnProperty.call(properties, 'isSearchCount')) {\n foundIsSearchCount = true;\n }\n }\n }\n }\n\n if (foundPageNo && foundPageSize && foundIsSearchCount) {\n results.push(makePassedResult('API请求包含所有标准分页参数:pageNo、pageSize和isSearchCount', {\n path,\n method: method.toUpperCase(),\n }));\n } else {\n const missingParams = [];\n if (!foundPageNo) missingParams.push('pageNo');\n if (!foundPageSize) missingParams.push('pageSize');\n if (!foundIsSearchCount) missingParams.push('isSearchCount');\n\n results.push(makeFailedResult(`API请求缺少标准分页参数:${missingParams.join(', ')}`, {\n path,\n method: method.toUpperCase(),\n missing_params: missingParams,\n found_params: {\n pageNo: foundPageNo,\n pageSize: foundPageSize,\n isSearchCount: foundIsSearchCount,\n },\n }));\n }\n\n return { results };\n}\n\nfunction deepClone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n\nfunction makePassedResult(message, details) {\n return {\n passed: true,\n message,\n details: details || {},\n };\n}\n\nfunction makeFailedResult(message, details) {\n return {\n passed: false,\n message,\n details: details || {},\n };\n}\n\nfunction resolveSchema(schemaOrRef, apiSpec) {\n if (!schemaOrRef || typeof schemaOrRef !== 'object') {\n return {};\n }\n\n if (!schemaOrRef.$ref) {\n return schemaOrRef;\n }\n\n if (!apiSpec || typeof apiSpec !== 'object') {\n return schemaOrRef;\n }\n\n const refPath = schemaOrRef.$ref;\n if (typeof refPath !== 'string' || !refPath.startsWith('#/')) {\n return schemaOrRef;\n }\n\n const tokens = refPath.slice(2).split('/').map(unescapeRefToken);\n let current = apiSpec;\n for (const token of tokens) {\n if (current && Object.prototype.hasOwnProperty.call(current, token)) {\n current = current[token];\n } else {\n return schemaOrRef;\n }\n }\n\n return current && typeof current === 'object' ? current : schemaOrRef;\n}\n\nfunction unescapeRefToken(token) {\n return token.replace(/~1/g, '/').replace(/~0/g, '~');\n}\n", - "outputs": 1, - "timeout": 0, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 500, - "y": 240, - "wires": [ - [ - "dfe6ed572461c4a5", - "d666851b16cf6482" - ] - ] - }, { "id": "9a020a2d438e0d5e", "type": "file", @@ -2302,35 +2281,6 @@ [] ] }, - { - "id": "ad6438b1a6e76365", - "type": "inject", - "z": "78d15f59dee4b6d8", - "name": "", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 130, - "y": 320, - "wires": [ - [ - "f414ed448fcf544b" - ] - ] - }, { "id": "6ab9403df07fcefa", "type": "function", @@ -2343,8 +2293,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 470, - "y": 400, + "x": 570, + "y": 320, "wires": [ [ "6e56a2ccd9fcaacc" @@ -2612,5 +2562,104 @@ "wires": [ [] ] + }, + { + "id": "5231ed8a796d6f17", + "type": "function", + "z": "78d15f59dee4b6d8", + "name": "分页参数检查", + "func": "'use strict';\n\nconst TEST_METADATA = {\n id: 'TC-DMS-PAGINATION-001',\n name: '分页参数检查',\n description: \"检查API请求参数中是否包含标准分页参数:pageNo、pageSize和isSearchCount。只有名称含有'查询'、'列表'等并且不含有'详情'一类的API才应用此验证。\",\n severity: 'MEDIUM',\n tags: ['pagination', 'params', 'backend-guide'],\n skip_execution: true,\n};\n\nconst globalApiSpec = msg && msg.oas_def ? msg.oas_def : null;\n\nif (!globalApiSpec || typeof globalApiSpec !== 'object' || !globalApiSpec.paths) {\n const errorMessage = '分页参数检查无法运行:msg.oas_def 缺失、不是对象或不包含 paths';\n node.error(errorMessage, msg);\n msg.error = errorMessage;\n msg.testcase = {\n metadata: TEST_METADATA,\n applies: false,\n skipped: true,\n results: [],\n };\n return msg;\n}\n\nconst allResults = [];\nlet testApplied = false;\n\n// 遍历所有路径和方法\nfor (const path in globalApiSpec.paths) {\n if (Object.prototype.hasOwnProperty.call(globalApiSpec.paths, path)) {\n const pathItem = globalApiSpec.paths[path];\n for (const method in pathItem) {\n if (Object.prototype.hasOwnProperty.call(pathItem, method) && ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method.toLowerCase())) {\n\n const operation = pathItem[method];\n if (!operation || typeof operation !== 'object') continue;\n\n // 构建 endpointSpec\n const endpointSpec = buildEndpointSpec(path, method, pathItem, operation);\n\n // 检查测试是否适用\n if (shouldApplyTest(endpointSpec)) {\n testApplied = true;\n const evaluation = runPaginationCheck(endpointSpec, globalApiSpec);\n allResults.push(...evaluation.results);\n }\n }\n }\n }\n}\n\nmsg.testcase = {\n metadata: TEST_METADATA,\n applies: testApplied,\n skipped: !testApplied,\n results: allResults,\n};\nmsg.payload = msg.testcase;\nreturn msg;\n\nfunction buildEndpointSpec(path, method, pathItem, operation) {\n const combinedParameters = [];\n if (Array.isArray(pathItem.parameters)) {\n combinedParameters.push(...pathItem.parameters);\n }\n if (Array.isArray(operation.parameters)) {\n combinedParameters.push(...operation.parameters);\n }\n\n const spec = deepClone(operation);\n spec.method = method;\n spec.path = path;\n if (combinedParameters.length > 0) {\n spec.parameters = mergeParameters(combinedParameters);\n }\n return spec;\n}\n\nfunction shouldApplyTest(spec) {\n const method = (spec.method || '').toLowerCase();\n if (method !== 'get' && method !== 'post') {\n return false;\n }\n\n const path = spec.path || '';\n const summary = spec.summary || '';\n const description = spec.description || '';\n const operationId = spec.operationId || '';\n\n const includeKeywords = ['查询', '列表', '分页', 'page', 'list', 'query', 'search', 'find'];\n const excludeKeywords = ['详情', '明细', 'detail', 'info', 'get by id', 'getbyid', '查看'];\n\n const apiDescriptionText = `${summary} ${description} ${operationId} ${path}`.toLowerCase();\n\n const containsInclude = includeKeywords.some(keyword => apiDescriptionText.includes(keyword.toLowerCase()));\n if (!containsInclude) {\n return false;\n }\n\n // 检查是否是获取单个资源的 'get'\n if (method === 'get' && path.includes('{') && path.includes('}')) {\n const getByIdHints = ['get', 'detail', 'info', '查看'];\n const isGetById = getByIdHints.some(hint => apiDescriptionText.includes(hint));\n if (isGetById) return false;\n }\n\n const containsExclude = excludeKeywords.some(keyword => apiDescriptionText.includes(keyword.toLowerCase()));\n return !containsExclude;\n}\n\nfunction extractEndpointSpec(message, apiSpec) {\n // This function is no longer needed as we iterate through all paths and methods.\n // Kept for potential future use if a single endpoint needs to be targeted.\n return null;\n}\n\nfunction resolveEndpointInfo(message) {\n // This function is no longer needed as we iterate through all paths and methods.\n return null;\n}\n\nfunction mergeParameters(parameters) {\n const merged = new Map();\n for (const param of parameters) {\n if (!param || typeof param !== 'object') {\n continue;\n }\n const name = param.name || '';\n const location = param.in || '';\n const key = `${location}:${name}`;\n if (!merged.has(key)) {\n merged.set(key, deepClone(param));\n }\n }\n return Array.from(merged.values());\n}\n\nfunction runPaginationCheck(spec, apiSpec) {\n const results = [];\n const path = spec.path || '';\n const method = (spec.method || '').toLowerCase();\n\n let foundPageNo = false;\n let foundPageSize = false;\n let foundIsSearchCount = false;\n\n if (Array.isArray(spec.parameters)) {\n for (const param of spec.parameters) {\n if (!param || typeof param !== 'object') {\n continue;\n }\n const location = (param.in || '').toLowerCase();\n if (location !== 'query') {\n continue;\n }\n\n if (param.name === 'pageNo') {\n foundPageNo = true;\n } else if (param.name === 'pageSize') {\n foundPageSize = true;\n } else if (param.name === 'isSearchCount') {\n foundIsSearchCount = true;\n }\n }\n }\n\n if (method === 'post' && spec.requestBody && typeof spec.requestBody === 'object') {\n const { content } = spec.requestBody;\n if (content && typeof content === 'object') {\n for (const mediaType of Object.keys(content)) {\n const mediaDefinition = content[mediaType];\n if (!mediaDefinition || typeof mediaDefinition !== 'object') {\n continue;\n }\n const resolvedSchema = resolveSchema(mediaDefinition.schema, apiSpec);\n const properties = resolvedSchema && resolvedSchema.properties;\n if (!properties || typeof properties !== 'object') {\n continue;\n }\n\n if (Object.prototype.hasOwnProperty.call(properties, 'pageNo')) {\n foundPageNo = true;\n }\n if (Object.prototype.hasOwnProperty.call(properties, 'pageSize')) {\n foundPageSize = true;\n }\n if (Object.prototype.hasOwnProperty.call(properties, 'isSearchCount')) {\n foundIsSearchCount = true;\n }\n }\n }\n }\n\n if (foundPageNo && foundPageSize && foundIsSearchCount) {\n results.push(makePassedResult('API请求包含所有标准分页参数:pageNo、pageSize和isSearchCount', {\n path,\n method: method.toUpperCase(),\n }));\n } else {\n const missingParams = [];\n if (!foundPageNo) missingParams.push('pageNo');\n if (!foundPageSize) missingParams.push('pageSize');\n if (!foundIsSearchCount) missingParams.push('isSearchCount');\n\n results.push(makeFailedResult(`API请求缺少标准分页参数:${missingParams.join(', ')}`, {\n path,\n method: method.toUpperCase(),\n missing_params: missingParams,\n found_params: {\n pageNo: foundPageNo,\n pageSize: foundPageSize,\n isSearchCount: foundIsSearchCount,\n },\n }));\n }\n\n return { results };\n}\n\nfunction deepClone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n\nfunction makePassedResult(message, details) {\n return {\n passed: true,\n message,\n details: details || {},\n };\n}\n\nfunction makeFailedResult(message, details) {\n return {\n passed: false,\n message,\n details: details || {},\n };\n}\n\nfunction resolveSchema(schemaOrRef, apiSpec) {\n if (!schemaOrRef || typeof schemaOrRef !== 'object') {\n return {};\n }\n\n if (!schemaOrRef.$ref) {\n return schemaOrRef;\n }\n\n if (!apiSpec || typeof apiSpec !== 'object') {\n return schemaOrRef;\n }\n\n const refPath = schemaOrRef.$ref;\n if (typeof refPath !== 'string' || !refPath.startsWith('#/')) {\n return schemaOrRef;\n }\n\n const tokens = refPath.slice(2).split('/').map(unescapeRefToken);\n let current = apiSpec;\n for (const token of tokens) {\n if (current && Object.prototype.hasOwnProperty.call(current, token)) {\n current = current[token];\n } else {\n return schemaOrRef;\n }\n }\n\n return current && typeof current === 'object' ? current : schemaOrRef;\n}\n\nfunction unescapeRefToken(token) {\n return token.replace(/~1/g, '/').replace(/~0/g, '~');\n}\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 500, + "y": 220, + "wires": [ + [ + "dfe6ed572461c4a5", + "d666851b16cf6482" + ] + ] + }, + { + "id": "ad6438b1a6e76365", + "type": "inject", + "z": "78d15f59dee4b6d8", + "name": "schema", + "props": [ + { + "p": "req.body.schema", + "v": "{\"openapi\":\"3.0.1\",\"info\":{\"title\":\"井 API\",\"version\":\"1.0.0\",\"description\":\"https://schema.ideas.cnpc/json/core-data/cd_well.1.0.0.json\",\"x-dms-sourceId\":\"https://schema.ideas.cnpc/json/core-data/cd_well.1.0.0.json\"},\"paths\":{\"/cd_well/1.0.0\":{\"post\":{\"operationId\":\"listResources\",\"summary\":\"List Resource resources\",\"requestBody\":{\"required\":false,\"content\":{\"application/json\":{\"schema\":{\"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\"}}}}}}}}},\"responses\":{\"200\":{\"description\":\"Successful response\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"integer\"},\"message\":{\"type\":\"string\"},\"data\":{\"type\":\"object\",\"properties\":{\"list\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/components/schemas/Resource\"}},\"total\":{\"type\":\"integer\"}}}}}}}}}}},\"/cd_well\":{\"post\":{\"operationId\":\"createResource\",\"summary\":\"Create a Resource\",\"requestBody\":{\"required\":true,\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"version\",\"act\",\"data\"],\"properties\":{\"version\":{\"type\":\"string\",\"default\":\"1.0.0\"},\"act\":{\"type\":\"integer\",\"default\":-1},\"data\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/components/schemas/Resource\"}}}}}}},\"responses\":{\"201\":{\"description\":\"Created\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Resource\"}}}}}},\"put\":{\"operationId\":\"updateResource\",\"summary\":\"Update a Resource\",\"requestBody\":{\"required\":true,\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"version\",\"act\",\"data\"],\"properties\":{\"version\":{\"type\":\"string\",\"default\":\"1.0.0\"},\"act\":{\"type\":\"integer\",\"default\":-1},\"data\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/components/schemas/Resource\"}}}}}}},\"responses\":{\"200\":{\"description\":\"Successful update\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Resource\"}}}}}},\"delete\":{\"operationId\":\"deleteResource\",\"summary\":\"Delete Resource resources\",\"requestBody\":{\"required\":true,\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"version\",\"data\"],\"properties\":{\"version\":{\"type\":\"string\",\"default\":\"1.0.0\"},\"data\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"description\":\"Value of dsid\"}}}}}}},\"responses\":{\"200\":{\"description\":\"Deleted\"}}}},\"/cd_well/1.0.0/{dsid}\":{\"parameters\":[{\"name\":\"dsid\",\"in\":\"path\",\"required\":true,\"schema\":{\"type\":\"string\",\"description\":\"主键ID\",\"x-dms-title\":\"DSID\",\"x-dms-originalType\":\"string\"}}],\"get\":{\"operationId\":\"getResource\",\"summary\":\"Get a single Resource\",\"responses\":{\"200\":{\"description\":\"Successful response\",\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/components/schemas/Resource\"}}}},\"404\":{\"description\":\"Resource not found\"}}}}},\"components\":{\"schemas\":{\"Resource\":{\"type\":\"object\",\"required\":[\"bsflag\",\"dataRegion\",\"createUserId\",\"orgId\",\"wellPurpose\",\"createDate\",\"wellLegalName\",\"dsid\",\"wellType\",\"updateUserId\",\"updateDate\",\"wellId\",\"wellCommonName\",\"projectId\"],\"properties\":{\"kb\":{\"type\":\"number\",\"description\":\"海拔(高程),补心海拔=地面海拔+补心高\",\"x-dms-title\":\"补心海拔\",\"x-dms-originalType\":\"number\"},\"egl\":{\"type\":\"number\",\"description\":\"地面海拔\",\"x-dms-title\":\"地面海拔\",\"x-dms-originalType\":\"number\"},\"kbd\":{\"type\":\"number\",\"description\":\"补心高度\",\"x-dms-title\":\"补心高度\",\"x-dms-originalType\":\"number\"},\"dsid\":{\"type\":\"string\",\"description\":\"主键ID\",\"x-dms-title\":\"DSID\",\"x-dms-originalType\":\"string\"},\"orgId\":{\"type\":\"string\",\"description\":\"单位唯一标识符,关联CD_ORGANIZATION的主键\",\"x-dms-title\":\"机构ID\",\"x-dms-originalType\":\"string\"},\"bsflag\":{\"type\":\"number\",\"description\":\"填写数据逻辑删除标识,1=在用,-5=废弃\",\"x-dms-title\":\"删除标识\",\"x-dms-originalType\":\"number\"},\"canton\":{\"type\":\"string\",\"description\":\"填写行政区代码对应的行政区名称\",\"x-dms-title\":\"行政区名称\",\"x-dms-originalType\":\"string\"},\"siteId\":{\"type\":\"string\",\"description\":\"物探工区ID\",\"x-dms-title\":\"物探工区ID\",\"x-dms-originalType\":\"string\"},\"wellId\":{\"type\":\"string\",\"description\":\"井标识符,非限定唯一。由EPDM系统自动产生维护,无需人工干预,是用于唯一标识EPDM系统的每一口井的内部机器码\",\"x-dms-title\":\"井ID\",\"x-dms-originalType\":\"string\"},\"orgName\":{\"type\":\"string\",\"description\":\"单位名称,必填\",\"x-dms-title\":\"机构名称\",\"x-dms-originalType\":\"string\"},\"remarks\":{\"type\":\"string\",\"description\":\"备注\",\"x-dms-title\":\"备注\",\"x-dms-originalType\":\"string\"},\"prodDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"投产日期\",\"x-dms-title\":\"投产日期\",\"x-dms-originalType\":\"date\"},\"siteName\":{\"type\":\"string\",\"description\":\"物探工区名称\",\"x-dms-title\":\"物探工区名称\",\"x-dms-originalType\":\"string\"},\"spudDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"主井筒的开钻日期\",\"x-dms-title\":\"开钻日期\",\"x-dms-originalType\":\"date\"},\"wellDesc\":{\"type\":\"string\",\"description\":\"填写这口井的曾用名\",\"x-dms-title\":\"曾用名\",\"x-dms-originalType\":\"string\"},\"wellType\":{\"type\":\"string\",\"description\":\"属性规范值字段,引用属性代码WELL_TYPE下的属性值\",\"x-dms-title\":\"井型\",\"x-dms-originalType\":\"string\"},\"checkDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"记录数据在本系统的审核时间,需精确到时分秒\",\"x-dms-title\":\"审核日期\",\"x-dms-originalType\":\"date\"},\"dataGroup\":{\"type\":\"string\",\"description\":\"数据分组\",\"x-dms-title\":\"数据分组\",\"x-dms-originalType\":\"string\"},\"projectId\":{\"type\":\"string\",\"description\":\"地质单元唯一标识符,根据井别不同,选择关联构造单元(探井)还是油气田单元(开发井),关联CD_GEO_UNIT表的主键\",\"x-dms-title\":\"地质单元ID\",\"x-dms-originalType\":\"string\"},\"stationId\":{\"type\":\"string\",\"description\":\"站库ID\",\"x-dms-title\":\"站库ID\",\"x-dms-originalType\":\"string\"},\"wellState\":{\"type\":\"string\",\"description\":\"井状态\",\"x-dms-title\":\"井状态\",\"x-dms-originalType\":\"string\"},\"activityId\":{\"type\":\"string\",\"description\":\"项目唯一标示符,关联CD_ACTIVITY表的主键\",\"x-dms-title\":\"项目ID\",\"x-dms-originalType\":\"string\"},\"cantonCode\":{\"type\":\"string\",\"description\":\"属性规范值字段,引用属性代码CANTON下的属性值\",\"x-dms-title\":\"行政区代码\",\"x-dms-originalType\":\"string\"},\"createDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"记录数据在本系统的创建时间,需精确到时分秒\",\"x-dms-title\":\"创建日期\",\"x-dms-originalType\":\"date\"},\"dataRegion\":{\"type\":\"string\",\"description\":\"油田标识\",\"x-dms-title\":\"油田标识\",\"x-dms-originalType\":\"string\"},\"dataSource\":{\"type\":\"string\",\"description\":\"填写数据来源的表CODE\",\"x-dms-title\":\"数据来源\",\"x-dms-originalType\":\"string\"},\"desgWellId\":{\"type\":\"string\",\"description\":\"设计井的唯一标识\",\"x-dms-title\":\"设计井ID\",\"x-dms-originalType\":\"string\"},\"energyType\":{\"type\":\"string\",\"description\":\"描述本井生产的油气资源类型,如煤层气、致密气、页岩气等\",\"x-dms-title\":\"能源类型\",\"x-dms-originalType\":\"string\"},\"platformId\":{\"type\":\"string\",\"description\":\"平台ID\",\"x-dms-title\":\"平台ID\",\"x-dms-originalType\":\"string\"},\"updateDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"记录数据在本系统最新的更新时间,需精确到时分秒,默认=创建时间\",\"x-dms-title\":\"更新日期\",\"x-dms-originalType\":\"date\"},\"wellTypeId\":{\"type\":\"string\",\"description\":\"属性规范值字段,引用属性代码WELL_TYPE下的属性值\",\"x-dms-title\":\"井型ID\",\"x-dms-originalType\":\"string\"},\"abandonDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"报废日期\",\"x-dms-title\":\"报废日期\",\"x-dms-originalType\":\"date\"},\"abondonType\":{\"type\":\"string\",\"description\":\"报废类型\",\"x-dms-title\":\"报废类型\",\"x-dms-originalType\":\"string\"},\"actualXAxis\":{\"type\":\"number\",\"description\":\"实际X坐标,实际X坐标\",\"x-dms-mask\":\"coordinate\",\"x-dms-geom\":\"Point.x.actual\",\"x-dms-title\":\"实际X坐标,实际X坐标\",\"x-dms-originalType\":\"number\"},\"actualYAxis\":{\"type\":\"number\",\"description\":\"实际Y坐标,实际Y坐标\",\"x-dms-mask\":\"coordinate\",\"x-dms-geom\":\"Point.y.actual\",\"x-dms-title\":\"实际Y坐标,实际Y坐标\",\"x-dms-originalType\":\"number\"},\"checkUserId\":{\"type\":\"string\",\"description\":\"记录数据在本系统的审核用户\",\"x-dms-title\":\"审核用户\",\"x-dms-originalType\":\"string\"},\"createAppId\":{\"type\":\"string\",\"description\":\"填写数据来源的系统名\",\"x-dms-title\":\"创建应用\",\"x-dms-originalType\":\"string\"},\"designXAxis\":{\"type\":\"number\",\"description\":\"设计X坐标,设计X坐标\",\"x-dms-mask\":\"coordinate\",\"x-dms-geom\":\"Point.x.design\",\"x-dms-title\":\"设计X坐标,设计X坐标\",\"x-dms-originalType\":\"number\"},\"designYAxis\":{\"type\":\"number\",\"description\":\"设计Y坐标,设计Y坐标\",\"x-dms-mask\":\"coordinate\",\"x-dms-geom\":\"Point.y.design\",\"x-dms-title\":\"设计Y坐标,设计Y坐标\",\"x-dms-originalType\":\"number\"},\"projectName\":{\"type\":\"string\",\"description\":\"地质单元名称,填写地质单元的中文名称,该名称在整个油田公司内不能重名,必填\",\"x-dms-title\":\"地质单元名称\",\"x-dms-originalType\":\"string\"},\"stationName\":{\"type\":\"string\",\"description\":\"站库名称,必填\",\"x-dms-title\":\"站库名称\",\"x-dms-originalType\":\"string\"},\"wellPurpose\":{\"type\":\"string\",\"description\":\"属性规范值字段,引用属性代码WELL_PURPOSE下的属性值\",\"x-dms-title\":\"井别\",\"x-dms-originalType\":\"string\"},\"activityName\":{\"type\":\"string\",\"description\":\"项目名称,必填\",\"x-dms-title\":\"项目名称\",\"x-dms-originalType\":\"string\"},\"completionMd\":{\"type\":\"number\",\"description\":\"完钻井深\",\"x-dms-title\":\"完钻井深\",\"x-dms-originalType\":\"number\"},\"createUserId\":{\"type\":\"string\",\"description\":\"记录数据在本系统的创建用户\",\"x-dms-title\":\"创建用户\",\"x-dms-originalType\":\"string\"},\"keyWellLevel\":{\"type\":\"string\",\"description\":\"重点井级别\",\"x-dms-title\":\"重点井级别\",\"x-dms-originalType\":\"string\"},\"platformName\":{\"type\":\"string\",\"description\":\"平台名称\",\"x-dms-title\":\"平台名称\",\"x-dms-originalType\":\"string\"},\"sourceDataId\":{\"type\":\"string\",\"description\":\"存储数据来源的主键信息\",\"x-dms-title\":\"源库ID标识\",\"x-dms-originalType\":\"string\"},\"structurePos\":{\"type\":\"string\",\"description\":\"构造位置的描述\",\"x-dms-title\":\"构造位置\",\"x-dms-originalType\":\"string\"},\"updateUserId\":{\"type\":\"string\",\"description\":\"记录数据在本系统最新的更新用户,默认=创建用户\",\"x-dms-title\":\"更新用户\",\"x-dms-originalType\":\"string\"},\"geoOffsetEast\":{\"type\":\"number\",\"description\":\"井口横坐标\",\"x-dms-mask\":\"coordinate\",\"x-dms-geom\":\"Point.x.wellhead\",\"x-dms-title\":\"井口横坐标\",\"x-dms-originalType\":\"number\"},\"seismicLineNo\":{\"type\":\"string\",\"description\":\"井旁地震测线号(勘探井)\",\"x-dms-title\":\"井旁地震测线号\",\"x-dms-originalType\":\"string\"},\"wellLegalName\":{\"type\":\"string\",\"description\":\"规范的井号名称,井号名称命名规范请参考《主数据库技术标准》\",\"x-dms-title\":\"拼音井号\",\"x-dms-originalType\":\"string\"},\"wellPurposeId\":{\"type\":\"string\",\"description\":\"属性规范值字段,引用属性代码WELL_PURPOSE下的属性值\",\"x-dms-title\":\"井别ID\",\"x-dms-originalType\":\"string\"},\"completionDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"完井日期\",\"x-dms-title\":\"完井日期\",\"x-dms-originalType\":\"date\"},\"geoDescription\":{\"type\":\"string\",\"description\":\"地理位置的描述\",\"x-dms-title\":\"地理位置\",\"x-dms-originalType\":\"string\"},\"geoOffsetNorth\":{\"type\":\"number\",\"description\":\"井口纵坐标\",\"x-dms-mask\":\"coordinate\",\"x-dms-geom\":\"Point.y.wellhead\",\"x-dms-title\":\"井口纵坐标\",\"x-dms-originalType\":\"number\"},\"wellCommonName\":{\"type\":\"string\",\"description\":\"通用井名,来源于钻井公报的汉字井名,必填\",\"x-dms-title\":\"井名\",\"x-dms-originalType\":\"string\"},\"endDrillingDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"最后一个井筒的完钻日期\",\"x-dms-title\":\"完钻日期\",\"x-dms-originalType\":\"date\"},\"targetFormation\":{\"type\":\"string\",\"description\":\"目的层\",\"x-dms-title\":\"目的层\",\"x-dms-originalType\":\"string\"},\"completionMethod\":{\"type\":\"string\",\"description\":\"完井方法\",\"x-dms-title\":\"完井方法\",\"x-dms-originalType\":\"string\"},\"registrationDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"井位通知单下达日期\",\"x-dms-title\":\"注册日期\",\"x-dms-originalType\":\"date\"},\"sourceCreateDate\":{\"type\":\"string\",\"format\":\"date\",\"description\":\"记录源头系统采集数据的时间\",\"x-dms-title\":\"源头数据采集时间\",\"x-dms-originalType\":\"date\"},\"coordinateSystemId\":{\"type\":\"string\",\"description\":\"坐标系统的唯一标示符\",\"x-dms-geom\":\"Point.srid.wellhead,Point.srid.design,Point.srid.actual\",\"x-dms-title\":\"坐标系统ID\",\"x-dms-originalType\":\"string\"},\"completionFormation\":{\"type\":\"string\",\"description\":\"钻井完钻层位\",\"x-dms-title\":\"钻井完钻层位\",\"x-dms-originalType\":\"string\"},\"coordinateSystemName\":{\"type\":\"string\",\"description\":\"坐标系名称\",\"x-dms-title\":\"坐标系名称\",\"x-dms-originalType\":\"string\"},\"wellbaseGeoOffsetEast\":{\"type\":\"number\",\"description\":\"井底横坐标\",\"x-dms-title\":\"井底横坐标\",\"x-dms-originalType\":\"number\"},\"wellbaseGeoOffsetNorth\":{\"type\":\"number\",\"description\":\"井底纵坐标\",\"x-dms-title\":\"井底纵坐标\",\"x-dms-originalType\":\"number\"}},\"description\":\"井\",\"x-dms-groupView\":[{\"name\":\"groupByWellCommonName\",\"group\":[\"dataRegion\",\"wellCommonName\",\"wellId\"]},{\"name\":\"groupByPlatformName\",\"group\":[\"dataRegion\",\"platformName\"]}],\"x-dms-identityId\":[\"dsid\"],\"x-dms-naturalKey\":[\"dataRegion\",\"wellCommonName\"],\"x-dms-defaultShow\":[\"wellCommonName\"]}}}}", + "vt": "json" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "x": 70, + "y": 220, + "wires": [ + [ + "211c041ab58c88f2" + ] + ] + }, + { + "id": "41818dbfe05be246", + "type": "function", + "z": "78d15f59dee4b6d8", + "name": "function 6", + "func": "'use strict';\n\ntry {\n const rawOas = extractOasInput(msg);\n const oasDef = typeof rawOas === 'string' ? JSON.parse(rawOas) : rawOas;\n\n validateOas(oasDef);\n\n const incomingCrudConfig = msg.req && msg.req.body && msg.req.body.crudConfig;\n if (isPlainObject(incomingCrudConfig)) {\n msg.crudConfig = mergeDeep({}, msg.crudConfig || {}, incomingCrudConfig);\n }\n\n if ((!msg.crudConfig || !msg.crudConfig.baseUrl) && Array.isArray(oasDef.servers) && oasDef.servers.length) {\n const validServer = oasDef.servers.find(server => server && typeof server.url === 'string' && server.url.trim());\n if (validServer) {\n msg.crudConfig = msg.crudConfig || {};\n msg.crudConfig.baseUrl = trimTrailingSlash(validServer.url.trim());\n }\n }\n\n msg.oas_def = oasDef;\n msg.payload = oasDef;\n delete msg.error;\n return msg;\n} catch (err) {\n node.error(`读取 OAS 定义失败: ${err.message}`, msg);\n msg.error = err.message;\n return msg;\n}\n\nfunction extractOasInput(message) {\n if (message && message.req && message.req.body && message.req.body.schema !== undefined) {\n return message.req.body.schema;\n }\n\n if (message && message.payload && message.payload.schema !== undefined) {\n return message.payload.schema;\n }\n\n throw new Error('未在 msg.req.body.schema 找到 OAS 定义');\n}\n\nfunction validateOas(oas) {\n if (!oas || typeof oas !== 'object') {\n throw new Error('OAS 文档必须是对象或 JSON 字符串');\n }\n\n if (!oas.openapi && !oas.swagger) {\n throw new Error('OpenAPI 文档缺少 openapi/swagger 字段');\n }\n\n if (!oas.paths || typeof oas.paths !== 'object' || Object.keys(oas.paths).length === 0) {\n throw new Error('OpenAPI 文档缺少 paths 定义');\n }\n}\n\nfunction mergeDeep(target, ...sources) {\n for (const source of sources) {\n if (!isPlainObject(source)) {\n continue;\n }\n\n for (const key of Object.keys(source)) {\n const value = source[key];\n if (value === undefined) {\n continue;\n }\n\n if (isPlainObject(value)) {\n const base = isPlainObject(target[key]) ? target[key] : {};\n target[key] = mergeDeep({}, base, value);\n } else {\n target[key] = clone(value);\n }\n }\n }\n\n return target;\n}\n\nfunction isPlainObject(candidate) {\n return Object.prototype.toString.call(candidate) === '[object Object]';\n}\n\nfunction clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n\nfunction trimTrailingSlash(url) {\n return !url ? '' : url.replace(/\\/+$/, '');\n}\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 260, + "y": 140, + "wires": [ + [ + "9c84414e55654e6a" + ] + ] + }, + { + "id": "211c041ab58c88f2", + "type": "switch", + "z": "78d15f59dee4b6d8", + "name": "", + "property": "req.body.schema", + "propertyType": "msg", + "rules": [ + { + "t": "empty" + }, + { + "t": "nnull" + } + ], + "checkall": "true", + "repair": false, + "outputs": 2, + "x": 150, + "y": 40, + "wires": [ + [ + "f414ed448fcf544b" + ], + [ + "41818dbfe05be246" + ] + ] } ] \ No newline at end of file