diff --git a/package-lock.json b/package-lock.json index 1426bb7bae..f9076f2673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "dotenv": "^17.0.1", "ember-inflector": "^6.0.0", "eslint-plugin-jest": "^29.0.1", + "inflection": "^3.0.2", "markdown-table": "^3.0.4", "openapi-to-postmanv2": "5.0.0", "parquet-wasm": "^0.6.1" @@ -8359,6 +8360,15 @@ "node": ">=0.8.19" } }, + "node_modules/inflection": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz", + "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/package.json b/package.json index 9ee0e05c86..1644b26578 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,6 @@ "precommit": "husky install" }, "jest": { - "transformIgnorePatterns": [ - "/node_modules/(?!ember-inflector/)" - ], "transform": { "^.+\\.[t|j]sx?$": "babel-jest" }, @@ -36,6 +33,7 @@ "dotenv": "^17.0.1", "ember-inflector": "^6.0.0", "eslint-plugin-jest": "^29.0.1", + "inflection": "^3.0.2", "markdown-table": "^3.0.4", "openapi-to-postmanv2": "5.0.0", "parquet-wasm": "^0.6.1" @@ -49,14 +47,14 @@ "aws-sdk-client-mock": "^4.1.0", "babel-jest": "^30.0.2", "babel-plugin-transform-import-meta": "^2.3.3", + "brace-expansion": "4.0.1", "eslint": "^9.30.1", "eslint-plugin-require-extensions": "^0.1.3", "globals": "^16.3.0", "husky": "^9.1.7", "jest": "^30.0.4", "lint-staged": "^16.1.2", - "prettier": "3.6.2", - "brace-expansion": "4.0.1" + "prettier": "3.6.2" }, "engineStrict": false, "engines": { diff --git a/tools/spectral/ipa/__tests__/IPA106ValidOperationID.test.js b/tools/spectral/ipa/__tests__/IPA106ValidOperationID.test.js new file mode 100644 index 0000000000..4d03acc02b --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA106ValidOperationID.test.js @@ -0,0 +1,70 @@ +import testRule from './__helpers__/testRule'; + +// TODO: add tests for xgen-custom-method extension - CLOUDP-306294 +// TOOD: enable tests for invalid methods (after rules are upgraded to warning) - CLOUDP-329722 + +testRule('xgen-IPA-106-valid-operation-id', [ + { + name: 'valid methods', + document: { + paths: { + '/groups/{groupId}/clusters': { + post: { + operationId: 'createGroupCluster', + }, + }, + }, + }, + errors: [], + }, + // This test will be enable when the xgen-IPA-106-valid-operation-id is set to warning severity - CLOUDP-329722 + /* { + name: 'invalid methods', + document: { + paths: { + '/api/atlas/v2/groups/{groupId}/access': { + post: { + operationId: 'addUserToProject', + }, + }, + '/api/atlas/v2/groups/{groupId}/invites': { + post: { + operationId: 'createProjectInvitation', + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-106-valid-operation-id', + message: + 'Invalid OperationID. The Operation ID must start with the verb “create” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. ', + path: ['paths', '/api/atlas/v2/groups/{groupId}/access', 'post'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-106-valid-operation-id', + message: + 'Invalid OperationID. The Operation ID must start with the verb “create” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. ', + path: ['paths', '/api/atlas/v2/groups/{groupId}/invites', 'post'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, */ + { + name: 'invalid methods with exceptions', + document: { + paths: { + '/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/index ': { + post: { + operationId: 'createRollingIndex', + 'x-xgen-IPA-exception': { + 'xgen-IPA-106-valid-operation-id': 'Reason', + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/__tests__/IPA107ValidOperationID.test.js b/tools/spectral/ipa/__tests__/IPA107ValidOperationID.test.js new file mode 100644 index 0000000000..a157366ba2 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA107ValidOperationID.test.js @@ -0,0 +1,75 @@ +import testRule from './__helpers__/testRule'; + +// TODO: add tests for xgen-custom-method extension - CLOUDP-306294 +// TOOD: enable tests for invalid methods (after rules are upgraded to warning) - CLOUDP-329722 + +testRule('xgen-IPA-107-valid-operation-id', [ + { + name: 'valid methods', + document: { + paths: { + 'groups/{groupId}/clusters/{clusterName}': { + put: { + operationId: 'updateGroupCluster', + }, + }, + '/groups/{groupId}/settings': { + put: { + operationId: 'updateGroupSettings', + }, + }, + }, + }, + errors: [], + }, + // This test will be enable when the xgen-IPA-107-valid-operation-id is set to warning severity - CLOUDP-329722 + /* { + name: 'invalid methods', + document: { + paths: { + '/api/atlas/v2/groups/{groupId}/limits/{limitName}': { + patch: { + operationId: 'setProjectLimit', + }, + }, + '/api/atlas/v2/groups/{groupId}/settings': { + put: { + operationId: 'updateProjectSettings', + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-107-valid-operation-id', + message: + 'Invalid OperationID. The Operation ID must start with the verb “update” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. For singleton resources - the last noun may be in plural form.', + path: ['paths', '/api/atlas/v2/groups/{groupId}/limits/{limitName}', 'patch'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-valid-operation-id', + message: + 'Invalid OperationID. The Operation ID must start with the verb “update” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. For singleton resources - the last noun may be in plural form.', + path: ['paths', '/api/atlas/v2/groups/{groupId}/settings', 'put'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, */ + { + name: 'invalid methods with exceptions', + document: { + paths: { + '/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/index ': { + post: { + operationId: 'updateRollingIndex', + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-valid-operation-id': 'Reason', + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/__tests__/IPA108ValidOperationID.test.js b/tools/spectral/ipa/__tests__/IPA108ValidOperationID.test.js new file mode 100644 index 0000000000..7a311e0f65 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA108ValidOperationID.test.js @@ -0,0 +1,70 @@ +import testRule from './__helpers__/testRule'; + +// TODO: add tests for xgen-custom-method extension - CLOUDP-306294 +// TOOD: enable tests for invalid methods (after rules are upgraded to warning) - CLOUDP-329722 + +testRule('xgen-IPA-108-valid-operation-id', [ + { + name: 'valid methods', + document: { + paths: { + '/groups/{groupId}/clusters/{clusterName}': { + delete: { + operationId: 'deleteGroupCluster', + }, + }, + }, + }, + errors: [], + }, + // This test will be enable when the xgen-IPA-108-valid-operation-id is set to warning severity - CLOUDP-329722 + /* { + name: 'invalid methods', + document: { + paths: { + '/api/atlas/v2/groups/{groupId}/apiKeys/{apiUserId}': { + delete: { + operationId: 'removeProjectApiKey', + }, + }, + '/api/atlas/v2/groups/{groupId}': { + delete: { + operationId: 'deleteProject', + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-108-valid-operation-id', + message: + 'Invalid OperationID. The Operation ID must start with the verb “delete” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. ', + path: ['paths', '/api/atlas/v2/groups/{groupId}/apiKeys/{apiUserId}', 'delete'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-108-valid-operation-id', + message: + 'Invalid OperationID. The Operation ID must start with the verb “delete” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. ', + path: ['paths', '/api/atlas/v2/groups/{groupId}', 'delete'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, */ + { + name: 'invalid methods with exceptions', + document: { + paths: { + '/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/index ': { + post: { + operationId: 'deleteRollingIndex', + 'x-xgen-IPA-exception': { + 'xgen-IPA-108-valid-operation-id': 'Reason', + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-106.yaml b/tools/spectral/ipa/rulesets/IPA-106.yaml index 4d0be2b13b..e65406b618 100644 --- a/tools/spectral/ipa/rulesets/IPA-106.yaml +++ b/tools/spectral/ipa/rulesets/IPA-106.yaml @@ -8,6 +8,7 @@ functions: - IPA106CreateMethodRequestHasNoReadonlyFields - IPA106CreateMethodResponseCodeIs201Created - IPA106CreateMethodResponseIsGetMethodResponse + - IPA106ValidOperationID aliases: CreateOperationObject: @@ -112,3 +113,20 @@ rules: then: field: '@key' function: 'IPA106CreateMethodResponseIsGetMethodResponse' + xgen-IPA-106-valid-operation-id: + description: | + The Operation ID must start with the verb “create” and should be followed by a noun or compound noun. + The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. + + ##### Implementation details + Rule checks for the following conditions: + - Applies only to POST methods on resource collection paths + - Generates the expected OperationId given the resource identifier + - Confirms that the existing operationId is compliant with generated IPA Compliant OperationId + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-106-valid-operation-id' + severity: off + given: '#CreateOperationObject' + then: + function: 'IPA106ValidOperationID' + functionOptions: + methodName: 'create' diff --git a/tools/spectral/ipa/rulesets/IPA-107.yaml b/tools/spectral/ipa/rulesets/IPA-107.yaml index eae2d30d91..1901f6acc8 100644 --- a/tools/spectral/ipa/rulesets/IPA-107.yaml +++ b/tools/spectral/ipa/rulesets/IPA-107.yaml @@ -8,6 +8,7 @@ functions: - IPA107UpdateMethodRequestHasNoReadonlyFields - IPA107UpdateMethodRequestBodyIsGetResponse - IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject + - IPA107ValidOperationID aliases: UpdateOperationObject: @@ -112,3 +113,21 @@ rules: then: field: '@key' function: 'IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject' + xgen-IPA-107-valid-operation-id: + description: | + The Operation ID must start with the verb “update” and should be followed by a noun or compound noun. + The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. + If the resource is a singleton resource, the last noun may be the plural form of the collection identifier. + + ##### Implementation details + Rule checks for the following conditions: + - Validation checks the PATCH/PUT methods for single resource paths and [singleton resources](https://go/ipa/113). + - Generates the expected OperationId given the resource identifier + - Confirms that the existing operationId is compliant with generated IPA Compliant OperationId + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-107-valid-operation-id' + severity: off + given: '#UpdateOperationObject' + then: + function: 'IPA107ValidOperationID' + functionOptions: + methodName: 'update' diff --git a/tools/spectral/ipa/rulesets/IPA-108.yaml b/tools/spectral/ipa/rulesets/IPA-108.yaml index b0efe908a6..7918c85a22 100644 --- a/tools/spectral/ipa/rulesets/IPA-108.yaml +++ b/tools/spectral/ipa/rulesets/IPA-108.yaml @@ -55,8 +55,26 @@ rules: given: '#DeleteOperationObject' then: function: IPA108DeleteMethodNoRequestBody + xgen-IPA-108-valid-operation-id: + description: | + The Operation ID must start with the verb “delete” and should be followed by a noun or compound noun. + The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. + + ##### Implementation details + Rule checks for the following conditions: + - Applies to all DELETE methods for single resource endpoints (with path parameters) + - Generates the expected OperationId given the resource identifier + - Confirms that the existing operationId is compliant with generated IPA Compliant OperationId + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-108-valid-operation-id' + severity: off + given: '#DeleteOperationObject' + then: + function: 'IPA108ValidOperationID' + functionOptions: + methodName: 'delete' functions: - IPA108DeleteMethodResponseShouldNotHaveSchema - IPA108DeleteMethod204Response - IPA108DeleteMethodNoRequestBody + - IPA108ValidOperationID diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 3112cde4dc..7d452db1eb 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -273,6 +273,18 @@ Rule checks for the following conditions: - Ignores resources without a Get method - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation +#### xgen-IPA-106-valid-operation-id + + `off` +The Operation ID must start with the verb “create” and should be followed by a noun or compound noun. +The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. + +##### Implementation details +Rule checks for the following conditions: + - Applies only to POST methods on resource collection paths + - Generates the expected OperationId given the resource identifier + - Confirms that the existing operationId is compliant with generated IPA Compliant OperationId + ### IPA-107 @@ -344,6 +356,19 @@ Rule checks for the following conditions: - Validation only applies to schema references to a predefined schema (not inline) - Confirms the referenced schema name ends with "Request" suffix +#### xgen-IPA-107-valid-operation-id + + `off` +The Operation ID must start with the verb “update” and should be followed by a noun or compound noun. +The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. +If the resource is a singleton resource, the last noun may be the plural form of the collection identifier. + +##### Implementation details +Rule checks for the following conditions: + - Validation checks the PATCH/PUT methods for single resource paths and [singleton resources](https://go/ipa/113). + - Generates the expected OperationId given the resource identifier + - Confirms that the existing operationId is compliant with generated IPA Compliant OperationId + ### IPA-108 @@ -388,6 +413,18 @@ Rule checks for the following conditions: - Fails if any requestBody is defined for the DELETE method - Skips validation for collection endpoints (without path parameters) +#### xgen-IPA-108-valid-operation-id + + `off` +The Operation ID must start with the verb “delete” and should be followed by a noun or compound noun. +The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. + +##### Implementation details +Rule checks for the following conditions: + - Applies to all DELETE methods for single resource endpoints (with path parameters) + - Generates the expected OperationId given the resource identifier + - Confirms that the existing operationId is compliant with generated IPA Compliant OperationId + ### IPA-109 diff --git a/tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js new file mode 100644 index 0000000000..c50a5c6cdd --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js @@ -0,0 +1,42 @@ +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectException, collectAndReturnViolation } from './utils/collectionUtils.js'; +import { + isCustomMethodIdentifier, + isResourceCollectionIdentifier, + isSingletonResource, + getResourcePathItems, +} from './utils/resourceEvaluation.js'; +import { generateOperationID } from './utils/operationIdGeneration.js'; + +const RULE_NAME = 'xgen-IPA-106-valid-operation-id'; +const ERROR_MESSAGE = + 'Invalid OperationID. The Operation ID must start with the verb “create” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form.'; + +export default (input, { methodName }, { path, documentInventory }) => { + const resourcePath = path[1]; + const oas = documentInventory.resolved; + const resourcePaths = getResourcePathItems(resourcePath, oas.paths); + + const isResourceCollection = isResourceCollectionIdentifier(resourcePath) && !isSingletonResource(resourcePaths); + if (isCustomMethodIdentifier(resourcePath) || !isResourceCollection) { + return; + } + + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + const expectedOperationID = generateOperationID(methodName, resourcePath); + if (expectedOperationID !== input.operationId) { + const errors = [ + { + path, + message: `${ERROR_MESSAGE} Found ${input.operationId}, expected ${expectedOperationID}.`, + }, + ]; + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; diff --git a/tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js new file mode 100644 index 0000000000..526b2a4660 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js @@ -0,0 +1,49 @@ +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectException, collectAndReturnViolation } from './utils/collectionUtils.js'; +import { + isSingleResourceIdentifier, + isResourceCollectionIdentifier, + isSingletonResource, + getResourcePathItems, + isCustomMethodIdentifier, +} from './utils/resourceEvaluation.js'; +import { generateOperationID } from './utils/operationIdGeneration.js'; + +const RULE_NAME = 'xgen-IPA-107-valid-operation-id'; +const ERROR_MESSAGE = + 'Invalid OperationID. The Operation ID must start with the verb “update” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form. For singleton resources - the last noun may be in plural form.'; + +export default (input, { methodName }, { path, documentInventory }) => { + const resourcePath = path[1]; + const oas = documentInventory.resolved; + const resourcePaths = getResourcePathItems(resourcePath, oas.paths); + + if ( + isCustomMethodIdentifier(resourcePath) || + (!isSingleResourceIdentifier(resourcePath) && + !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths))) + ) { + return; + } + + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + const expectedOperationID = generateOperationID(methodName, resourcePath); + if (expectedOperationID !== input.operationId) { + console.log( + `${input.operationId}, ${expectedOperationID}, ${resourcePath}, ${input.deprecated ? 'TRUE' : 'FALSE'}, ${(resourcePath, input['x-xgen-owner-team'])}` + ); + const errors = [ + { + path, + message: `${ERROR_MESSAGE} Found ${input.operationId}, expected ${expectedOperationID}.`, + }, + ]; + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; diff --git a/tools/spectral/ipa/rulesets/functions/IPA108ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA108ValidOperationID.js new file mode 100644 index 0000000000..03aa2eab2c --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA108ValidOperationID.js @@ -0,0 +1,34 @@ +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectException, collectAndReturnViolation } from './utils/collectionUtils.js'; +import { isCustomMethodIdentifier, isSingleResourceIdentifier } from './utils/resourceEvaluation.js'; +import { generateOperationID } from './utils/operationIdGeneration.js'; + +const RULE_NAME = 'xgen-IPA-108-valid-operation-id'; +const ERROR_MESSAGE = + 'Invalid OperationID. The Operation ID must start with the verb “delete” and should be followed by a noun or compound noun. The noun(s) in the Operation ID should be the collection identifiers from the resource identifier in singular form.'; + +export default (input, { methodName }, { path }) => { + const resourcePath = path[1]; + + if (isCustomMethodIdentifier(resourcePath) || !isSingleResourceIdentifier(resourcePath)) { + return; + } + + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + const expectedOperationID = generateOperationID(methodName, resourcePath); + if (expectedOperationID !== input.operationId) { + const errors = [ + { + path, + message: `${ERROR_MESSAGE} Found ${input.operationId}, expected ${expectedOperationID}.`, + }, + ]; + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; diff --git a/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js b/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js index b2ded2a7bc..12ee3cfe98 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js +++ b/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js @@ -1,4 +1,4 @@ -import { singularize } from 'ember-inflector'; +const inflection = require('inflection'); import { isPathParam, removePrefix, isSingleResourceIdentifier } from './resourceEvaluation.js'; const CAMEL_CASE = /[A-Z]?[a-z]+/g; @@ -29,12 +29,12 @@ export function generateOperationID(method, path) { let opID = verb; for (let i = 0; i < nouns.length - 1; i++) { - opID += singularize(nouns[i]); + opID += inflection.singularize(nouns[i]); } // singularize final noun, dependent on resource identifier if (isSingleResourceIdentifier(resourceIdentifier) || verb === 'create') { - nouns[nouns.length - 1] = singularize(nouns[nouns.length - 1]); + nouns[nouns.length - 1] = inflection.singularize(nouns[nouns.length - 1]); } opID += nouns.pop(); diff --git a/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js b/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js index e872646ee1..9d6888b8fd 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js +++ b/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js @@ -57,6 +57,10 @@ export function getCustomMethodName(path) { return path.split(':')[1]; } +export function stripCustomMethodName(path) { + return path.substring(0, path.indexOf(':')); +} + export function isPathParam(string) { return string.startsWith('{') && string.endsWith('}'); }