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
+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.