functions/readonly-in-response-schema.js (96 lines of code) (raw):

// Flag any properties that are readonly in the response schema. // Scan an OpenAPI document to determine if a schema is a response-only schema, // which means it is not referenced by any request schemas. // Any schema that is referenced by a request is considered a request schema. // Any schema referenced by a request schema is also considered a request schema. // Any schema that "allOf"'s a request schema with a discriminator is also a request schema. // Any schema that is not a request schema is considered a response-only schema. // requestSchemas is a set of schema names that we have determined are request schemas let requestSchemas; function getRequestSchemas(oasDoc) { /* eslint-disable object-curly-newline,object-curly-spacing */ const getOps = ({put, post, patch}) => [put, post, patch]; const topLevelRequestSchemas = Object.values(oasDoc.paths || {}) .flatMap(getOps).filter(Boolean) .flatMap(({parameters}) => parameters?.filter(({in: location}) => location === 'body') || []) .flatMap(({schema}) => (schema ? [schema] : [])) .filter(({$ref}) => $ref && $ref.match(/^#\/definitions\//)) .map(({$ref}) => $ref.replace(/^#\/definitions\//, '')); /* eslint-enable object-curly-newline,object-curly-spacing */ requestSchemas = new Set(); // Now that we have the top-level response schemas, we need to find all the // schemas that are referenced by those schemas. We do this by iterating // over the schemas until we find no new schemas to add to the set. const schemasToProcess = [...topLevelRequestSchemas]; while (schemasToProcess.length > 0) { const schemaName = schemasToProcess.pop(); requestSchemas.add(schemaName); const schema = oasDoc.definitions[schemaName]; if (schema) { if (schema.properties) { // eslint-disable-next-line no-restricted-syntax for (const property of Object.values(schema.properties)) { if (property.$ref && property.$ref.match(/^#\/definitions\//)) { const ref = property.$ref.replace(/^#\/definitions\//, ''); if (!requestSchemas.has(ref) && !schemasToProcess.includes(ref)) { schemasToProcess.push(ref); } } if (property.items && property.items.$ref && property.items.$ref.match(/^#\/definitions\//)) { const ref = property.items.$ref.replace(/^#\/definitions\//, ''); if (!requestSchemas.has(ref) && !schemasToProcess.includes(ref)) { schemasToProcess.push(ref); } } if (property.additionalProperties && property.additionalProperties.$ref && property.additionalProperties.$ref.match(/^#\/definitions\//)) { const ref = property.additionalProperties.$ref.replace(/^#\/definitions\//, ''); if (!requestSchemas.has(ref) && !schemasToProcess.includes(ref)) { schemasToProcess.push(ref); } } } } if (schema.allOf) { // eslint-disable-next-line no-restricted-syntax for (const element of schema.allOf) { if (element.$ref && element.$ref.match(/^#\/definitions\//)) { const ref = element.$ref.replace(/^#\/definitions\//, ''); if (!requestSchemas.has(ref) && !schemasToProcess.includes(ref)) { schemasToProcess.push(ref); } } } } if (schema.discriminator) { // Check all the schemas in the document and add any that "allOf" this schema // into schemasToProcess const schemaRef = `#/definitions/${schemaName}`; // eslint-disable-next-line no-restricted-syntax for (const [key, value] of Object.entries(oasDoc.definitions)) { if (value.allOf?.some((elem) => elem.$ref === schemaRef)) { schemasToProcess.push(key); } } } } } } // compute a hash for a string function hashCode(str) { let hash = 0; for (let i = 0; i < str.length; i += 1) { /* eslint-disable no-bitwise */ hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; // Convert to 32bit integer /* eslint-enable no-bitwise */ } return hash; } let docHash; function responseOnlySchema(schemaName, oasDoc) { const thisDocHash = hashCode(JSON.stringify(oasDoc)); if (!requestSchemas || docHash !== thisDocHash) { getRequestSchemas(oasDoc); docHash = thisDocHash; } if (requestSchemas.has(schemaName)) { return false; } return true; } // `schema` is a (resolved) parameter entry at the path or operation level module.exports = (schema, _opts, context) => { const schemaName = context.path[context.path.length - 1]; const oasDoc = context.document.data; if (!responseOnlySchema(schemaName, oasDoc)) { return []; } // Flag any properties that are readonly in the response schema. const errors = []; // eslint-disable-next-line no-restricted-syntax for (const [propertyName, property] of Object.entries(schema.properties || {})) { if (property.readOnly) { errors.push({ message: 'Property of response-only schema should not be marked readOnly', path: [...context.path, 'properties', propertyName, 'readOnly'], }); } } return errors; };