Introduce techdocs metadata in techdocs-common

* Add TechdocsMetadata type in backend plugin endpoint
 * Introduce TechDocsMetadata type in frontend
 * Add changeset
 * Remove old thennable metadata resp
 * Address PR feedback
  - Remove explicit type annotation on TechDocsMetadata
  - Reintroduce res.send instead of throwing an error
  - Change logger info to logger error
 * Add TechDocsMetadata type in frontend plugin
 * Commit yarn.lock
 * Introduce JSON5 and remove parsing in local pub
Signed-off-by: Matei David <matei.david.35@gmail.com>
This commit is contained in:
Matei David
2021-01-08 11:21:50 +00:00
parent 57460cda02
commit a5e27d5c1d
13 changed files with 115 additions and 29 deletions
+9
View File
@@ -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).
+1
View File
@@ -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",
@@ -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();
});
@@ -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<Buffer> => {
return new Promise((resolve, reject) => {
@@ -32,7 +33,7 @@ const streamToBuffer = (stream: Readable): Promise<Buffer> => {
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<string> {
async fetchTechDocsMetadata(
entityName: EntityName,
): Promise<TechDocsMetadata> {
try {
return await new Promise<string>((resolve, reject) => {
return await new Promise<TechDocsMetadata>((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);
@@ -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<string> {
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata> {
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));
});
});
}
@@ -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';
@@ -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<string> {
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata> {
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}`,
@@ -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<string>;
fetchTechDocsMetadata(entityName: EntityName): Promise<TechDocsMetadata>;
/**
* Route middleware to serve static documentation files for an entity.
+16 -8
View File
@@ -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) => {
+2 -1
View File
@@ -16,6 +16,7 @@
import { createApiRef } from '@backstage/core';
import { EntityName } from '@backstage/catalog-model';
import { TechDocsMetadata } from './types';
export const techdocsStorageApiRef = createApiRef<TechDocsStorageApi>({
id: 'plugin.techdocs.storageservice',
@@ -33,7 +34,7 @@ export interface TechDocsStorage {
}
export interface TechDocs {
getTechDocsMetadata(entityId: EntityName): Promise<string>;
getTechDocsMetadata(entityId: EntityName): Promise<TechDocsMetadata>;
getEntityMetadata(entityId: EntityName): Promise<string>;
}
@@ -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<any>;
techdocs: AsyncState<any>;
techdocs: AsyncState<TechDocsMetadata>;
};
};
+20
View File
@@ -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;
};
+1 -1
View File
@@ -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==