diff --git a/examples/getstarted/config/plugins.js b/examples/getstarted/config/plugins.js index a6bb88b407..48ef1edb79 100644 --- a/examples/getstarted/config/plugins.js +++ b/examples/getstarted/config/plugins.js @@ -17,7 +17,7 @@ module.exports = () => ({ documentation: { config: { info: { - version: '2.0.0', + version: '1.0.0', }, }, }, diff --git a/packages/plugins/documentation/server/config/default-plugin-config.js b/packages/plugins/documentation/server/config/default-plugin-config.js index 2df51cde28..08c185a496 100644 --- a/packages/plugins/documentation/server/config/default-plugin-config.js +++ b/packages/plugins/documentation/server/config/default-plugin-config.js @@ -16,12 +16,11 @@ module.exports = { name: 'Apache 2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0.html', }, - 'x-strapi-generation-date': new Date().toISOString(), }, 'x-strapi-config': { path: '/documentation', plugins: null, - customizer: null, + mutateDocumentation: null, }, servers: [], externalDocs: { diff --git a/packages/plugins/documentation/server/services/__tests__/documentation.test.js b/packages/plugins/documentation/server/services/__tests__/documentation.test.js index aef4d0842b..cb318eaf31 100644 --- a/packages/plugins/documentation/server/services/__tests__/documentation.test.js +++ b/packages/plugins/documentation/server/services/__tests__/documentation.test.js @@ -154,13 +154,15 @@ describe('Documentation service', () => { }, ], }; + global.strapi.config.get = () => ({ ...userConfig }); const docService = documentation({ strapi: global.strapi }); await docService.generateFullDoc(); const lastMockCall = fse.writeJson.mock.calls[fse.writeJson.mock.calls.length - 1]; const mockFinalDoc = lastMockCall[1]; - - expect(mockFinalDoc.info).toEqual(userConfig.info); + // The generation data is dynamically added, it cannot be modified by the user + const { 'x-generation-date': generationConfig, ...mockFinalDocInfo } = mockFinalDoc.info; + expect(mockFinalDocInfo).toEqual(userConfig.info); expect(mockFinalDoc['x-strapi-config']).toEqual(userConfig['x-strapi-config']); expect(mockFinalDoc.externalDocs).toEqual(userConfig.externalDocs); expect(mockFinalDoc.security).toEqual(userConfig.security); @@ -319,6 +321,66 @@ describe('Documentation service', () => { 'test-new-component' ); }); + it('overrides only the specified version', async () => { + const overrideService = override({ strapi: global.strapi }); + // Simulate override from upload plugin + overrideService.registerOverride( + { + // Only override for version 1.0.0 + info: { version: '1.0.0' }, + components: { + schemas: { + // This component schema exists after generating with mock data, replace it + ShouldNotBeAdded: {}, + }, + }, + }, + { pluginOrigin: 'upload' } + ); + // Simulate override from upload plugin + overrideService.registerOverride( + { + // Only override for version 2.0.0 + info: { version: '2.0.0' }, + components: { + schemas: { + // This component schema exists after generating with mock data, replace it + ShouldBeAdded: {}, + }, + }, + }, + { pluginOrigin: 'upload' } + ); + // Simulate override from upload plugin + overrideService.registerOverride( + { + components: { + schemas: { + // This component schema exists after generating with mock data, replace it + ShouldAlsoBeAdded: {}, + }, + }, + }, + { pluginOrigin: 'upload' } + ); + global.strapi.plugins.documentation = { + service: jest.fn((name) => { + const mockServices = { + override: overrideService, + }; + + return mockServices[name]; + }), + }; + const docService = documentation({ strapi: global.strapi }); + await docService.generateFullDoc('2.0.0'); + const lastMockCall = fse.writeJson.mock.calls[fse.writeJson.mock.calls.length - 1]; + const mockFinalDoc = lastMockCall[1]; + + expect(mockFinalDoc.components.schemas.ShouldNotBeAdded).toBeUndefined(); + expect(mockFinalDoc.components.schemas.ShouldBeAdded).toBeDefined(); + expect(mockFinalDoc.components.schemas.ShouldAlsoBeAdded).toBeDefined(); + }); it('excludes apis and plugins from generation', async () => { const overrideService = override({ strapi: global.strapi }); @@ -346,12 +408,12 @@ describe('Documentation service', () => { Object.keys(mockFinalDoc.components.schemas).find((compo) => compo.includes('Kitchensink')) ).toBeUndefined(); }); - it("applies a user's customizer function", async () => { + it("applies a user's mutateDocumentation function", async () => { global.strapi.config.get = () => ({ ...defaultConfig, 'x-strapi-config': { ...defaultConfig['x-strapi-config'], - customizer(draft) { + mutateDocumentation(draft) { draft.paths['/kitchensinks'] = { get: { responses: { 200: { description: 'test' } } } }; }, }, diff --git a/packages/plugins/documentation/server/services/documentation.js b/packages/plugins/documentation/server/services/documentation.js index 784d116b16..dea7236c2d 100755 --- a/packages/plugins/documentation/server/services/documentation.js +++ b/packages/plugins/documentation/server/services/documentation.js @@ -176,7 +176,6 @@ module.exports = ({ strapi }) => { 'full_documentation.json' ); - // Set config defaults const serverUrl = getAbsoluteServerUrl(strapi.config); const apiPath = strapi.config.get('api.rest.prefix'); const generatedDocumentation = produce(config, (draft) => { @@ -189,42 +188,49 @@ module.exports = ({ strapi }) => { }, ]; } - + // Set the generated date + draft.info['x-generation-date'] = new Date().toISOString(); + // Set the plugins that need documentation draft['x-strapi-config'].plugins = pluginsThatNeedDocumentation; // Delete it from the config so it doesn't end up in the spec - delete draft['x-strapi-config'].customizer; - + delete draft['x-strapi-config'].mutateDocumentation; // Set the generated paths draft.paths = paths; // Merge the generated component schemas with the defaults draft.components = _.merge(defaultOpenApiComponents, { schemas }); + // Check for overrides and then add them + if (overrideService.registeredOverrides.length > 0) { + overrideService.registeredOverrides.forEach((doc) => { + // Only run the overrrides when no override version is provided, + // or when the generated documentation version matches the override version + if (!doc?.info?.version || doc.info.version === version) { + if (doc.tags) { + // Merge override tags with the generated tags + draft.tags = draft.tags || []; + draft.tags.push(...doc.tags); + } - overrideService.registeredOverrides.forEach((doc) => { - if (doc.tags) { - // Merge override tags with the generated tags - draft.tags = draft.tags || []; - draft.tags.push(...doc.tags); - } + if (doc.paths) { + // Merge override paths with the generated paths + // The override will add a new path or replace the value of an existing path + draft.paths = { ...draft.paths, ...doc.paths }; + } - if (doc.paths) { - // Merge override paths with the generated paths - // The override will add a new path or replace the value of an existing path - draft.paths = { ...draft.paths, ...doc.paths }; - } - - if (doc.components) { - Object.entries(doc.components).forEach(([key, val]) => { - draft.components[key] = draft.components[key] || {}; - // Merge override components with the generated components, - // The override will add a new component or replace the value of an existing component - draft.components[key] = { ...draft.components[key], ...val }; - }); - } - }); + if (doc.components) { + Object.entries(doc.components).forEach(([key, val]) => { + draft.components[key] = draft.components[key] || {}; + // Merge override components with the generated components, + // The override will add a new component or replace the value of an existing component + draft.components[key] = { ...draft.components[key], ...val }; + }); + } + } + }); + } }); - // Get the documentation customizer - const documentationCustomizer = config['x-strapi-config'].customizer; - // Escape hatch, allow the user to provide a customizer function that can manipulate + // Get the documentation mutateDocumentation + const documentationCustomizer = config['x-strapi-config'].mutateDocumentation; + // Escape hatch, allow the user to provide a mutateDocumentation function that can alter any part of // the generated documentation before it is written to the file system const finalDocumentation = documentationCustomizer ? produce(generatedDocumentation, documentationCustomizer) diff --git a/packages/plugins/documentation/server/services/override.js b/packages/plugins/documentation/server/services/override.js index 79405b1c8f..7d6fd666ca 100644 --- a/packages/plugins/documentation/server/services/override.js +++ b/packages/plugins/documentation/server/services/override.js @@ -5,53 +5,48 @@ const { getPluginsThatNeedDocumentation } = require('./utils/get-plugins-that-ne module.exports = ({ strapi }) => { const registeredOverrides = []; const excludedFromGeneration = []; - /** - * - * @param {string | string[]} api - The name of the api or and array of apis to exclude from generation - */ - const excludeFromGeneration = (api) => { - if (Array.isArray(api)) { - excludedFromGeneration.push(...api); - - return; - } - - excludedFromGeneration.push(api); - }; - /** - * @TODO pluginOrigin should be required in next major release - * @param {object} doc - The openapi specifcation to override - * @param {object} options - The options to override the documentation - * @param {string} options.pluginOrigin - The name of the plugin that is overriding the documentation - * @param {string[]} options.excludeFromGeneration - The name of the plugin that is overriding the documentation - */ - const registerOverride = (override, { pluginOrigin, excludeFromGeneration = [] }) => { - const pluginsThatNeedDocumentation = getPluginsThatNeedDocumentation( - strapi.config.get('plugin.documentation') - ); - // Don't apply the override if the plugin is not in the list of plugins that need documentation - if (pluginOrigin && !pluginsThatNeedDocumentation.includes(pluginOrigin)) return; - - if (excludeFromGeneration.length) { - strapi - .plugin('documentation') - .service('override') - .excludeFromGeneration(excludeFromGeneration); - } - - let overrideToRegister = override; - // Parse yaml if we receive a string - if (typeof override === 'string') { - overrideToRegister = require('yaml').parse(overrideToRegister); - } - // receive an object we can register it directly - registeredOverrides.push(overrideToRegister); - }; return { registeredOverrides, - registerOverride, - excludeFromGeneration, excludedFromGeneration, + /** + * + * @param {string | string[]} api - The name of the api or and array of apis to exclude from generation + */ + excludeFromGeneration(api) { + if (Array.isArray(api)) { + excludedFromGeneration.push(...api); + + return; + } + + excludedFromGeneration.push(api); + }, + /** + * @TODO pluginOrigin should be required in next major release + * @param {object} doc - The openapi specifcation to override + * @param {object} options - The options to override the documentation + * @param {string} options.pluginOrigin - The name of the plugin that is overriding the documentation + * @param {string[]} options.excludeFromGeneration - The name of the plugin that is overriding the documentation + */ + registerOverride(override, { pluginOrigin, excludeFromGeneration = [] }) { + const pluginsThatNeedDocumentation = getPluginsThatNeedDocumentation( + strapi.config.get('plugin.documentation') + ); + // Don't apply the override if the plugin is not in the list of plugins that need documentation + if (pluginOrigin && !pluginsThatNeedDocumentation.includes(pluginOrigin)) return; + + if (excludeFromGeneration.length) { + this.excludeFromGeneration(excludeFromGeneration); + } + + let overrideToRegister = override; + // Parse yaml if we receive a string + if (typeof override === 'string') { + overrideToRegister = require('yaml').parse(overrideToRegister); + } + // receive an object we can register it directly + registeredOverrides.push(overrideToRegister); + }, }; };