diff --git a/.changeset/techdocs-healthy-kiwis-mix.md b/.changeset/techdocs-healthy-kiwis-mix.md new file mode 100644 index 0000000000..9415b43bca --- /dev/null +++ b/.changeset/techdocs-healthy-kiwis-mix.md @@ -0,0 +1,9 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Create type for TechDocsMetadata (#3716) + +This change introduces a new type (TechDocsMetadata) in packages/techdocs-common. This type is then introduced in the endpoint response in techdocs-backend and in the api interface in techdocs (frontend). diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 387c75241c..7f39bc6907 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -50,6 +50,7 @@ "fs-extra": "^9.0.1", "git-url-parse": "^11.4.3", "js-yaml": "^4.0.0", + "json5": "^2.1.3", "mime-types": "^2.1.27", "mock-fs": "^4.13.0", "recursive-readdir": "^2.2.2", diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index c157a7c205..3902aab21a 100755 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -18,7 +18,7 @@ import path from 'path'; import * as winston from 'winston'; import { ConfigReader } from '@backstage/config'; import { AwsS3Publish } from './awsS3'; -import { PublisherBase } from './types'; +import { PublisherBase, TechDocsMetadata } from './types'; import type { Entity, EntityName } from '@backstage/catalog-model'; const createMockEntity = (annotations = {}): Entity => { @@ -159,13 +159,39 @@ describe('AwsS3Publish', () => { mockFs({ [entityRootDir]: { - 'techdocs_metadata.json': 'file-content', + 'techdocs_metadata.json': + '{"site_name": "backstage", "site_description": "site_content"}', }, }); - expect(await publisher.fetchTechDocsMetadata(entityNameMock)).toBe( - 'file-content', - ); + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return tech docs metadata when json encoded with single quotes', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content'}`, + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); mockFs.restore(); }); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 3f7a0e3d81..b9e9fe12e6 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -20,9 +20,10 @@ import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; -import { PublisherBase, PublishRequest } from './types'; +import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; import fs from 'fs-extra'; import { Readable } from 'stream'; +import JSON5 from 'json5'; const streamToBuffer = (stream: Readable): Promise => { return new Promise((resolve, reject) => { @@ -32,7 +33,7 @@ const streamToBuffer = (stream: Readable): Promise => { stream.on('error', reject); stream.on('end', () => resolve(Buffer.concat(chunks))); } catch (e) { - throw new Error(`Unable to parse the response data, ${e.message}`); + throw new Error(`Unable to parse the response data ${e.message}`); } }); }; @@ -162,9 +163,11 @@ export class AwsS3Publish implements PublisherBase { } } - async fetchTechDocsMetadata(entityName: EntityName): Promise { + async fetchTechDocsMetadata( + entityName: EntityName, + ): Promise { try { - return await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; this.storageClient @@ -182,8 +185,11 @@ export class AwsS3Publish implements PublisherBase { `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, ); } + const techdocsMetadata = JSON5.parse( + techdocsMetadataJson.toString('utf-8'), + ); - resolve(techdocsMetadataJson.toString('utf-8')); + resolve(techdocsMetadata); }) .catch(err => { this.logger.error(err.message); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index e6b2c26943..f75a4b3bfd 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -24,7 +24,8 @@ import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; -import { PublisherBase, PublishRequest } from './types'; +import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; +import JSON5 from 'json5'; export class GoogleGCSPublish implements PublisherBase { static async fromConfig( @@ -132,7 +133,7 @@ export class GoogleGCSPublish implements PublisherBase { }); } - fetchTechDocsMetadata(entityName: EntityName): Promise { + fetchTechDocsMetadata(entityName: EntityName): Promise { return new Promise((resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; @@ -152,7 +153,7 @@ export class GoogleGCSPublish implements PublisherBase { const techdocsMetadataJson = Buffer.concat( fileStreamChunks, ).toString(); - resolve(techdocsMetadataJson); + resolve(JSON5.parse(techdocsMetadataJson)); }); }); } diff --git a/packages/techdocs-common/src/stages/publish/index.ts b/packages/techdocs-common/src/stages/publish/index.ts index 494efe5ac1..dbf4ff732a 100644 --- a/packages/techdocs-common/src/stages/publish/index.ts +++ b/packages/techdocs-common/src/stages/publish/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ export { Publisher } from './publish'; -export type { PublisherBase, PublisherType } from './types'; +export type { PublisherBase, PublisherType, TechDocsMetadata } from './types'; diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index de670cfba5..d07472e17d 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -25,7 +25,12 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import { PublisherBase, PublishRequest, PublishResponse } from './types'; +import { + PublisherBase, + PublishRequest, + PublishResponse, + TechDocsMetadata, +} from './types'; // TODO: Use a more persistent storage than node_modules or /tmp directory. // Make it configurable with techdocs.publisher.local.publishDirectory @@ -102,7 +107,7 @@ export class LocalPublish implements PublisherBase { }); } - fetchTechDocsMetadata(entityName: EntityName): Promise { + fetchTechDocsMetadata(entityName: EntityName): Promise { return new Promise((resolve, reject) => { this.discovery.getBaseUrl('techdocs').then(techdocsApiUrl => { const storageUrl = new URL( @@ -116,7 +121,7 @@ export class LocalPublish implements PublisherBase { .then(response => response .json() - .then(techdocsMetadataJson => resolve(techdocsMetadataJson)) + .then(techdocsMetadata => resolve(techdocsMetadata)) .catch(err => { reject( `Unable to parse metadata JSON for ${entityRootDir}. Error: ${err}`, diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index 24a9d05adf..db6a075d43 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -32,6 +32,14 @@ export type PublishResponse = { remoteUrl?: string; } | void; +/** + * Type to hold metadata found in techdocs_metadata.json and associated with each site + */ +export type TechDocsMetadata = { + site_name: string; + site_description: string; +}; + /** * Base class for a TechDocs publisher (e.g. Local, Google GCS Bucket, AWS S3, etc.) * The publisher handles publishing of the generated static files after the prepare and generate steps of TechDocs. @@ -50,7 +58,7 @@ export interface PublisherBase { * Retrieve TechDocs Metadata about a site e.g. name, contributors, last updated, etc. * This API uses the techdocs_metadata.json file that co-exists along with the generated docs. */ - fetchTechDocsMetadata(entityName: EntityName): Promise; + fetchTechDocsMetadata(entityName: EntityName): Promise; /** * Route middleware to serve static documentation files for an entity. diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index c2ecaab61b..5ad23cde84 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -58,14 +58,22 @@ export async function createRouter({ const { '0': path } = req.params; const entityName = getEntityNameFromUrlPath(path); - publisher - .fetchTechDocsMetadata(entityName) - .then(techdocsMetadataJson => { - res.send(techdocsMetadataJson); - }) - .catch(reason => { - res.status(500).send(`Unable to get Metadata. Reason: ${reason}`); - }); + try { + const techdocsMetadata = await publisher.fetchTechDocsMetadata( + entityName, + ); + + res.send(techdocsMetadata); + } catch (err) { + logger.error( + `Unable to get metadata for ${entityName.namespace}/${entityName.name} with error ${err}`, + ); + res + .status(500) + .send( + `Unable to get metadata for $${entityName.namespace}/${entityName.name}, reason: ${err}`, + ); + } }); router.get('/metadata/entity/:namespace/:kind/:name', async (req, res) => { diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index c8581ec5d3..3a950aeeb3 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -16,6 +16,7 @@ import { createApiRef } from '@backstage/core'; import { EntityName } from '@backstage/catalog-model'; +import { TechDocsMetadata } from './types'; export const techdocsStorageApiRef = createApiRef({ id: 'plugin.techdocs.storageservice', @@ -33,7 +34,7 @@ export interface TechDocsStorage { } export interface TechDocs { - getTechDocsMetadata(entityId: EntityName): Promise; + getTechDocsMetadata(entityId: EntityName): Promise; getEntityMetadata(entityId: EntityName): Promise; } diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx index ac9d21d319..c994b52422 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx @@ -19,12 +19,13 @@ import { AsyncState } from 'react-use/lib/useAsync'; import CodeIcon from '@material-ui/icons/Code'; import { EntityName } from '@backstage/catalog-model'; import { Header, HeaderLabel, Link } from '@backstage/core'; +import { TechDocsMetadata } from '../../types'; type TechDocsPageHeaderProps = { entityId: EntityName; metadataRequest: { entity: AsyncState; - techdocs: AsyncState; + techdocs: AsyncState; }; }; diff --git a/plugins/techdocs/src/types.ts b/plugins/techdocs/src/types.ts new file mode 100644 index 0000000000..36cc4e187b --- /dev/null +++ b/plugins/techdocs/src/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type TechDocsMetadata = { + site_name: string; + site_description: string; +}; diff --git a/yarn.lock b/yarn.lock index 7fdc2c3a62..9db2ecd70a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16734,7 +16734,7 @@ json3@^3.3.2: resolved "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== -json5@2.x, json5@^2.1.1, json5@^2.1.2: +json5@2.x, json5@^2.1.1, json5@^2.1.2, json5@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==