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 Oct 6, 2020
1 parent 5896d9c commit acc6cf5
Show file tree
Hide file tree
Showing 17 changed files with 1,027 additions and 141 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = {
'prefer-destructuring': 0,
'prefer-template': 0,
'no-underscore-dangle': 0,
'class-methods-use-this': 0,

'sort-imports': [
'error',
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 @@ -185,7 +185,7 @@ Array [
]
`;

exports[`datasource/cdnjs getReleases returns null for unknown error 1`] = `
exports[`datasource/cdnjs getReleases throws for unknown error 1`] = `
Array [
Object {
"headers": Object {
Expand Down
2 changes: 1 addition & 1 deletion lib/datasource/cdnjs/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('datasource/cdnjs', () => {
).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' })
Expand Down
82 changes: 46 additions & 36 deletions lib/datasource/cdnjs/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ExternalHostError } from '../../types/errors/external-host-error';
import { Http } from '../../util/http';
import { HttpError } from '../../util/http';
import { CachePromise, cacheAble } from '../cache';
import { GetReleasesConfig, ReleaseResult } from '../common';
import { Datasource } from '../datasource';

export const id = 'cdnjs';

const http = new Http(id);

interface CdnjsAsset {
version: string;
files: string[];
Expand All @@ -22,43 +21,54 @@ interface CdnjsResponse {
assets?: CdnjsAsset[];
}

async function downloadLibrary(library: string): CachePromise<CdnjsResponse> {
const url = `https://api.cdnjs.com/libraries/${library}?fields=homepage,repository,assets`;
return { data: (await http.getJson<CdnjsResponse>(url)).body };
}

export async function getReleases({
lookupName,
}: 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];
try {
const { assets, homepage, repository } = await cacheAble({
id,
lookup: library,
cb: downloadLibrary,
});
if (!assets) {
return null;
}
const assetName = lookupName.replace(`${library}/`, '');
const releases = assets
.filter(({ files }) => files.includes(assetName))
.map(({ version, sri }) => ({ version, newDigest: sri[assetName] }));
export class CdnJs extends Datasource {
readonly id = 'cdnjs';

const result: ReleaseResult = { releases };
private async downloadLibrary(library: string): CachePromise<CdnjsResponse> {
const url = `https://api.cdnjs.com/libraries/${library}?fields=homepage,repository,assets`;
return { data: (await this.http.getJson<CdnjsResponse>(url)).body };
}

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

// this.handleErrors will always throw
// eslint-disable-next-line consistent-return
async getReleases({
lookupName,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
const library = lookupName.split('/')[0];
try {
const { assets, homepage, repository } = await cacheAble({
id,
lookup: library,
cb: () => this.downloadLibrary(library),
});
if (!assets) {
return null;
}
const assetName = lookupName.replace(`${library}/`, '');
const releases = assets
.filter(({ files }) => files.includes(assetName))
.map(({ version, sri }) => ({ version, newDigest: sri[assetName] }));

const result: ReleaseResult = { releases };

if (homepage) {
result.homepage = homepage;
}
if (repository?.url) {
result.sourceUrl = repository.url;
}
return result;
} catch (err) {
if (err.statusCode !== undefined && err.statusCode !== 404) {
throw new ExternalHostError(err);
}
this.handleGenericErrors(err);
}
}
}
14 changes: 13 additions & 1 deletion lib/datasource/clojure/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { GetReleasesConfig, ReleaseResult } from '../common';
import { Datasource } from '../datasource';
import { getReleases } from '../maven';
import { MAVEN_REPO } from '../maven/common';

export const id = 'clojure';

export const defaultRegistryUrls = ['https://clojars.org/repo', MAVEN_REPO];
export const registryStrategy = 'merge';

export { getReleases } from '../maven';
export class Clojure extends Datasource {
readonly id = 'clojure';

getReleases({
lookupName,
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
return getReleases({ lookupName, registryUrl });
}
}
Loading

0 comments on commit acc6cf5

Please sign in to comment.