refactor(datasource/deno): Add schema validation (#21329)

This commit is contained in:
Sergei Zharinov 2023-04-11 17:25:34 +03:00 committed by GitHub
parent 3b53efe67c
commit 00c8f9055e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 89 deletions

View file

@ -1,4 +1,6 @@
import { ZodError } from 'zod';
import * as httpMock from '../../../../test/http-mock';
import { logger } from '../../../../test/util';
import { DenoDatasource } from '.';
describe('modules/datasource/deno/index', () => {
@ -10,7 +12,7 @@ describe('modules/datasource/deno/index', () => {
.scope(deno.defaultRegistryUrls[0])
.get('/v2/modules/std')
.reply(200, {
versions: ['0.163.0', '0.162.0'],
versions: ['0.163.0', '0.162.0', '0.161.0'],
tags: [{ value: 'top_5_percent', kind: 'popularity' }],
})
.get('/v2/modules/std/0.163.0')
@ -32,7 +34,9 @@ describe('modules/datasource/deno/index', () => {
type: 'github',
},
uploaded_at: '2022-10-20T12:10:21.592Z',
});
})
.get('/v2/modules/std/0.161.0')
.reply(200, { foo: 'bar' });
const result = await deno.getReleases({
packageName: 'https://deno.land/std',
@ -50,11 +54,21 @@ describe('modules/datasource/deno/index', () => {
sourceUrl: 'https://github.com/denoland/deno_std',
releaseTimestamp: '2022-10-20T12:10:21.592Z',
},
{
version: '0.161.0',
},
],
tags: {
popularity: 'top_5_percent',
},
});
expect(logger.logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(ZodError),
}),
`Deno: failed to get version details for 0.161.0`
);
});
it('throws error if module endpoint fails', async () => {

View file

@ -9,12 +9,7 @@ import * as semanticVersioning from '../../versioning/semver';
import { Datasource } from '../datasource';
import type { Release } from '../index';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import type {
DenoAPIModuleResponse,
DenoAPIModuleVersionResponse,
ReleaseMap,
} from './types';
import { createSourceURL, tagsToRecord } from './utils';
import { DenoAPIModuleResponse, DenoAPIModuleVersionResponse } from './schema';
export class DenoDatasource extends Datasource {
static readonly id = 'deno';
@ -45,7 +40,7 @@ export class DenoDatasource extends Datasource {
const massagedRegistryUrl = registryUrl!;
const extractResult = regEx(
'^(https://deno.land/)(?<rawPackageName>[^@\\s]+)'
/^(https:\/\/deno.land\/)(?<rawPackageName>[^@\s]+)/
).exec(packageName);
const rawPackageName = extractResult?.groups?.rawPackageName;
if (is.nullOrUndefined(rawPackageName)) {
@ -73,17 +68,17 @@ export class DenoDatasource extends Datasource {
key: (moduleAPIURL) => moduleAPIURL,
})
async getReleaseResult(moduleAPIURL: string): Promise<ReleaseResult> {
const { versions, tags } = (
await this.http.getJson<DenoAPIModuleResponse>(moduleAPIURL)
).body;
const releasesCache =
(await packageCache.get<ReleaseMap>(
const releasesCache: Record<string, Release> =
(await packageCache.get(
`datasource-${DenoDatasource.id}-details`,
moduleAPIURL
)) ?? {};
let cacheModified = false;
const {
body: { versions, tags },
} = await this.http.getJson(moduleAPIURL, DenoAPIModuleResponse);
// get details for the versions
const releases = await pMap(
versions,
@ -95,8 +90,16 @@ export class DenoDatasource extends Datasource {
}
// https://apiland.deno.dev/v2/modules/postgres/v0.17.0
const release = await this.getReleaseDetails(
joinUrlParts(moduleAPIURL, version)
const url = joinUrlParts(moduleAPIURL, version);
const { body: release } = await this.http.getJson(
url,
DenoAPIModuleVersionResponse.catch(({ error: err }) => {
logger.warn(
{ err },
`Deno: failed to get version details for ${version}`
);
return { version };
})
);
releasesCache[release.version] = release;
@ -117,21 +120,6 @@ export class DenoDatasource extends Datasource {
);
}
return {
releases,
tags: tagsToRecord(tags),
};
}
async getReleaseDetails(moduleAPIVersionURL: string): Promise<Release> {
const { version, uploaded_at, upload_options } = (
await this.http.getJson<DenoAPIModuleVersionResponse>(moduleAPIVersionURL)
).body;
return {
version,
gitRef: upload_options.ref,
releaseTimestamp: uploaded_at,
sourceUrl: createSourceURL(upload_options),
};
return { releases, tags };
}
}

View file

@ -0,0 +1,44 @@
import { z } from 'zod';
import { getSourceUrl as getGithubSourceUrl } from '../../../util/github/url';
import { looseArray } from '../../../util/schema-utils';
import type { Release } from '../types';
export const DenoApiTag = z.object({
kind: z.string(),
value: z.string(),
});
export const DenoAPIModuleResponse = z.object({
tags: looseArray(DenoApiTag).transform((tags) => {
const record: Record<string, string> = {};
for (const { kind, value } of tags) {
record[kind] = value;
}
return record;
}),
versions: z.array(z.string()),
});
export const DenoAPIUploadOptions = z.object({
ref: z.string(),
type: z.union([z.literal('github'), z.unknown()]),
repository: z.string(),
subdir: z.string().optional(),
});
export const DenoAPIModuleVersionResponse = z
.object({
upload_options: DenoAPIUploadOptions,
uploaded_at: z.string(),
version: z.string(),
})
.transform(
({ version, uploaded_at: releaseTimestamp, upload_options }): Release => {
let sourceUrl: string | undefined = undefined;
const { type, repository, ref: gitRef } = upload_options;
if (type === 'github') {
sourceUrl = getGithubSourceUrl(repository);
}
return { version, gitRef, releaseTimestamp, sourceUrl };
}
);

View file

@ -1,34 +0,0 @@
import type { Release } from '../types';
export interface DenoAPIModuleResponse {
name: string;
latest_version: string;
star_count: number;
popularity_score: number;
tags: DenoAPITags[];
versions: string[];
description: string;
}
export interface DenoAPIModuleVersionResponse {
upload_options: DenoAPIUploadOptions;
analysis_version: string;
description: string;
uploaded_at: string; // ISO date
name: string;
version: string;
}
export interface DenoAPIUploadOptions {
ref: string; // commit ref / tag
type: 'github' | unknown; // type of hosting. seen: ['github']
repository: string; // repo of hosting e.g. denodrivers/postgres
subdir?: string;
}
export interface DenoAPITags {
kind: string; // e.g. popularity
value: string; // e.g. top_5_percent
}
export type ReleaseMap = Record<string, Release>;

View file

@ -1,22 +0,0 @@
import { getSourceUrl as getGithubSourceUrl } from '../../../util/github/url';
import type { DenoAPITags, DenoAPIUploadOptions } from './types';
export function createSourceURL({
type,
repository,
}: DenoAPIUploadOptions): string | undefined {
switch (type) {
case 'github':
return getGithubSourceUrl(repository);
default:
return undefined;
}
}
export function tagsToRecord(tags: DenoAPITags[]): Record<string, string> {
const record: Record<string, string> = {};
for (const { kind, value } of tags) {
record[kind] = value;
}
return record;
}