Skip to content

Commit

Permalink
refactor(datasource): migrate to class based datasource
Browse files Browse the repository at this point in the history
    A small experiment into what OOP/class based datasources might look like. Picked Cdnjs as it's the smallest & simplest.

    With this approach we can share common logic, like error handling, rate limiting, etc. between different datasources, instead of having to reimplement it each time we write a new datasource. Currently there's nothing shared, as it's only 1 datasource, but the interesting stuff will come with the 2nd datasource
  • Loading branch information
JamieMagee committed May 6, 2021
1 parent e33df09 commit 032ac52
Show file tree
Hide file tree
Showing 16 changed files with 1,159 additions and 96 deletions.
8 changes: 4 additions & 4 deletions lib/datasource/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as bitbucketTags from './bitbucket-tags';
import * as cdnjs from './cdnjs';
import * as clojure from './clojure';
import { CdnJsDatasource } from './cdnjs';
import { ClojureDatasource } from './clojure';
import * as crate from './crate';
import * as dart from './dart';
import * as docker from './docker';
Expand Down Expand Up @@ -36,8 +36,8 @@ const api = new Map<string, DatasourceApi>();
export default api;

api.set('bitbucket-tags', bitbucketTags);
api.set('cdnjs', cdnjs);
api.set('clojure', clojure);
api.set('cdnjs', new CdnJsDatasource());
api.set('clojure', new ClojureDatasource());
api.set('crate', crate);
api.set('dart', dart);
api.set('docker', docker);
Expand Down
2 changes: 1 addition & 1 deletion lib/datasource/cdnjs/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Array [
]
`;

exports[`datasource/cdnjs/index getReleases returns null for unknown error 1`] = `
exports[`datasource/cdnjs/index getReleases throws for unknown error 1`] = `
Array [
Object {
"headers": Object {
Expand Down
45 changes: 33 additions & 12 deletions lib/datasource/cdnjs/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getPkgReleases } from '..';
import * as httpMock from '../../../test/http-mock';
import { getName, loadFixture } from '../../../test/util';
import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages';
import { id as datasource } from '.';
import { CdnJsDatasource } from '.';

const res1 = loadFixture('d3-force.json');
const res2 = loadFixture('bulma.json');
Expand All @@ -26,21 +26,30 @@ describe(getName(), () => {
it('throws for empty result', async () => {
httpMock.scope(baseUrl).get(pathFor('foo/bar')).reply(200, null);
await expect(
getPkgReleases({ datasource, depName: 'foo/bar' })
getPkgReleases({
datasource: CdnJsDatasource.id,
depName: 'foo/bar',
})
).rejects.toThrow(EXTERNAL_HOST_ERROR);
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('throws for error', async () => {
httpMock.scope(baseUrl).get(pathFor('foo/bar')).replyWithError('error');
await expect(
getPkgReleases({ datasource, depName: 'foo/bar' })
getPkgReleases({
datasource: CdnJsDatasource.id,
depName: 'foo/bar',
})
).rejects.toThrow(EXTERNAL_HOST_ERROR);
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('returns null for 404', async () => {
httpMock.scope(baseUrl).get(pathFor('foo/bar')).reply(404);
expect(
await getPkgReleases({ datasource, depName: 'foo/bar' })
await getPkgReleases({
datasource: CdnJsDatasource.id,
depName: 'foo/bar',
})
).toBeNull();
expect(httpMock.getTrace()).toMatchSnapshot();
});
Expand All @@ -51,7 +60,7 @@ describe(getName(), () => {
.reply(200, {});
expect(
await getPkgReleases({
datasource,
datasource: CdnJsDatasource.id,
depName: 'doesnotexist/doesnotexist',
})
).toBeNull();
Expand All @@ -60,28 +69,40 @@ describe(getName(), () => {
it('throws for 401', async () => {
httpMock.scope(baseUrl).get(pathFor('foo/bar')).reply(401);
await expect(
getPkgReleases({ datasource, depName: 'foo/bar' })
getPkgReleases({
datasource: CdnJsDatasource.id,
depName: 'foo/bar',
})
).rejects.toThrow(EXTERNAL_HOST_ERROR);
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('throws for 429', async () => {
httpMock.scope(baseUrl).get(pathFor('foo/bar')).reply(429);
await expect(
getPkgReleases({ datasource, depName: 'foo/bar' })
getPkgReleases({
datasource: CdnJsDatasource.id,
depName: 'foo/bar',
})
).rejects.toThrow(EXTERNAL_HOST_ERROR);
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('throws for 5xx', async () => {
httpMock.scope(baseUrl).get(pathFor('foo/bar')).reply(502);
await expect(
getPkgReleases({ datasource, depName: 'foo/bar' })
getPkgReleases({
datasource: CdnJsDatasource.id,
depName: 'foo/bar',
})
).rejects.toThrow(EXTERNAL_HOST_ERROR);
expect(httpMock.getTrace()).toMatchSnapshot();
});
it('returns null for unknown error', async () => {
it('throws for unknown error', async () => {
httpMock.scope(baseUrl).get(pathFor('foo/bar')).replyWithError('error');
await expect(
getPkgReleases({ datasource, depName: 'foo/bar' })
getPkgReleases({
datasource: CdnJsDatasource.id,
depName: 'foo/bar',
})
).rejects.toThrow(EXTERNAL_HOST_ERROR);
expect(httpMock.getTrace()).toMatchSnapshot();
});
Expand All @@ -91,7 +112,7 @@ describe(getName(), () => {
.get(pathFor('d3-force/d3-force.js'))
.reply(200, res1);
const res = await getPkgReleases({
datasource,
datasource: CdnJsDatasource.id,
depName: 'd3-force/d3-force.js',
});
expect(res).toMatchSnapshot();
Expand All @@ -103,7 +124,7 @@ describe(getName(), () => {
.get(pathFor('bulma/only/0.7.5/style.css'))
.reply(200, res2);
const res = await getPkgReleases({
datasource,
datasource: CdnJsDatasource.id,
depName: 'bulma/only/0.7.5/style.css',
});
expect(res).toMatchSnapshot();
Expand Down
93 changes: 44 additions & 49 deletions lib/datasource/cdnjs/index.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,56 @@
import { ExternalHostError } from '../../types/errors/external-host-error';
import { Http } from '../../util/http';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import type { CdnjsResponse } from './types';

export const id = 'cdnjs';
export const customRegistrySupport = false;
export const defaultRegistryUrls = ['https://api.cdnjs.com/'];
export const caching = true;
export class CdnJsDatasource extends Datasource {
static readonly id = 'cdnjs';

const http = new Http(id);
constructor() {
super(CdnJsDatasource.id);
}

interface CdnjsAsset {
version: string;
files: string[];
sri?: Record<string, string>;
}
customRegistrySupport = false;

interface CdnjsResponse {
homepage?: string;
repository?: {
type: 'git' | unknown;
url?: string;
};
assets?: CdnjsAsset[];
}
defaultRegistryUrls = ['https://api.cdnjs.com/'];

export async function getReleases({
lookupName,
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
// Each library contains multiple assets, so we cache at the library level instead of per-asset
const library = lookupName.split('/')[0];
const url = `${registryUrl}libraries/${library}?fields=homepage,repository,assets`;
try {
const { assets, homepage, repository } = (
await http.getJson<CdnjsResponse>(url)
).body;
if (!assets) {
return null;
}
const assetName = lookupName.replace(`${library}/`, '');
const releases = assets
.filter(({ files }) => files.includes(assetName))
.map(({ version, sri }) => ({ version, newDigest: sri[assetName] }));
caching = true;

const result: ReleaseResult = { releases };
// this.handleErrors will always throw
// eslint-disable-next-line consistent-return
async getReleases({
lookupName,
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
// Each library contains multiple assets, so we cache at the library level instead of per-asset
const library = lookupName.split('/')[0];
const url = `${registryUrl}libraries/${library}?fields=homepage,repository,assets`;
try {
const { assets, homepage, repository } = (
await this.http.getJson<CdnjsResponse>(url)
).body;
if (!assets) {
return null;
}
const assetName = lookupName.replace(`${library}/`, '');
const releases = assets
.filter(({ files }) => files.includes(assetName))
.map(({ version, sri }) => ({ version, newDigest: sri[assetName] }));

if (homepage) {
result.homepage = homepage;
}
if (repository?.url) {
result.sourceUrl = repository.url;
}
return result;
} catch (err) {
if (err.statusCode !== 404) {
throw new ExternalHostError(err);
const result: ReleaseResult = { releases };

if (homepage) {
result.homepage = homepage;
}
if (repository?.url) {
result.sourceUrl = repository.url;
}
return result;
} catch (err) {
if (err.statusCode !== 404) {
throw new ExternalHostError(err);
}
this.handleGenericErrors(err);
}
throw err;
}
}
14 changes: 14 additions & 0 deletions lib/datasource/cdnjs/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
interface CdnjsAsset {
version: string;
files: string[];
sri?: Record<string, string>;
}

export interface CdnjsResponse {
homepage?: string;
repository?: {
type: 'git' | unknown;
url?: string;
};
assets?: CdnjsAsset[];
}
Loading

0 comments on commit 032ac52

Please sign in to comment.