mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 06:51:11 +00:00
189 lines
5.6 KiB
TypeScript
189 lines
5.6 KiB
TypeScript
import type { ZodType, z } from 'zod/v4';
|
|
import { ZodError } from 'zod/v4';
|
|
import { logger } from '../../logger/index.ts';
|
|
import { ExternalHostError } from '../../types/errors/external-host-error.ts';
|
|
import type { PackageCacheNamespace } from '../../util/cache/package/types.ts';
|
|
import type { CachedOptions } from '../../util/cache/package/with-cache.ts';
|
|
import { withCache } from '../../util/cache/package/with-cache.ts';
|
|
import type { HttpBase } from '../../util/http/http.ts';
|
|
import { Http, HttpError } from '../../util/http/index.ts';
|
|
import type { HttpOptions } from '../../util/http/types.ts';
|
|
import { coerceObject } from '../../util/object.ts';
|
|
import type {
|
|
DatasourceApi,
|
|
DigestConfig,
|
|
GetReleasesConfig,
|
|
PostprocessReleaseConfig,
|
|
PostprocessReleaseResult,
|
|
RegistryStrategy,
|
|
Release,
|
|
ReleaseResult,
|
|
SourceUrlSupport,
|
|
} from './types.ts';
|
|
|
|
/**
|
|
* The options accepted by the JSON methods of the http client `H`:
|
|
* `HttpOptions` for the plain client, and the client's own options type for a
|
|
* specialised one such as `GithubHttp`.
|
|
*/
|
|
type JsonOptions<H extends Http> =
|
|
H extends HttpBase<infer Opts, any> ? Opts : HttpOptions;
|
|
|
|
export abstract class Datasource<
|
|
H extends Http = Http,
|
|
> implements DatasourceApi {
|
|
public readonly id: string;
|
|
|
|
/**
|
|
* The package cache namespace used by {@link Datasource.cached}.
|
|
* Defaults to `datasource-<id>`, which must be registered in
|
|
* `packageCacheNamespaces`.
|
|
*/
|
|
protected readonly cacheNamespace: PackageCacheNamespace;
|
|
|
|
/**
|
|
* A subclass that narrows `H` to a specialised client (`GithubHttp`,
|
|
* `GitlabHttp`, ...) must construct that client itself and pass it as
|
|
* `http`, since this constructor can only build the plain `Http` default.
|
|
*/
|
|
protected constructor(id: string, http?: H) {
|
|
this.id = id;
|
|
this.http = http ?? (new Http(id) as H);
|
|
this.cacheNamespace = `datasource-${id}` as PackageCacheNamespace;
|
|
}
|
|
|
|
caching: boolean | undefined;
|
|
|
|
defaultConfig: Record<string, unknown> | undefined;
|
|
|
|
getDefaultRegistryUrls(_packageName: string): string[] | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
supportsCustomRegistry(_packageName: string): boolean {
|
|
return true;
|
|
}
|
|
|
|
defaultVersioning?: string | undefined;
|
|
|
|
registryStrategy: RegistryStrategy = 'first';
|
|
|
|
releaseTimestampSupport = false;
|
|
releaseTimestampNote?: string | undefined;
|
|
|
|
sourceUrlSupport: SourceUrlSupport = 'none';
|
|
sourceUrlNote?: string | undefined;
|
|
|
|
protected readonly http: H;
|
|
|
|
abstract getReleases(
|
|
getReleasesConfig: GetReleasesConfig,
|
|
): Promise<ReleaseResult | null>;
|
|
|
|
/**
|
|
* `newValue` may be `undefined`, for example when only the digest of the
|
|
* current value is being resolved. Implementations must handle that case
|
|
* explicitly, for example by resolving the digest of a default branch or
|
|
* by returning `null`.
|
|
*/
|
|
getDigest?(config: DigestConfig, newValue?: string): Promise<string | null>;
|
|
|
|
postprocessRelease?(
|
|
config: PostprocessReleaseConfig,
|
|
release: Release,
|
|
): Promise<PostprocessReleaseResult>;
|
|
|
|
handleHttpErrors(_err: HttpError): void {
|
|
// intentionally empty
|
|
}
|
|
|
|
/**
|
|
* Caches the result of `fn` in the datasource cache namespace.
|
|
*
|
|
* Same as {@link withCache}, except that `namespace` defaults to
|
|
* {@link Datasource.cacheNamespace}.
|
|
*/
|
|
protected cached<T>(
|
|
options: Omit<CachedOptions, 'namespace'> & {
|
|
namespace?: PackageCacheNamespace;
|
|
},
|
|
fn: () => T | Promise<T>,
|
|
): Promise<T> {
|
|
return withCache(
|
|
{ ...options, namespace: options.namespace ?? this.cacheNamespace },
|
|
fn,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Requests `url` and returns the response body validated by `schema`.
|
|
*
|
|
* Every error, a response that fails validation included, goes through
|
|
* {@link Datasource.handleGenericErrors} and therefore fails the lookup. Use
|
|
* it when a malformed response must be treated as an error; use
|
|
* {@link Datasource.fetchJsonOrNull} when it should count as "no data from
|
|
* this registry" instead.
|
|
*/
|
|
protected async fetchJson<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
schema: Schema,
|
|
options?: JsonOptions<H>,
|
|
): Promise<z.infer<Schema>> {
|
|
const httpOptions = coerceObject(options) as HttpOptions;
|
|
try {
|
|
const { body } = await this.http.getJson(url, httpOptions, schema);
|
|
return body;
|
|
} catch (err) {
|
|
this.handleGenericErrors(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Same as {@link Datasource.fetchJson}, except that a response which fails
|
|
* schema validation is logged and reported as `null`.
|
|
*
|
|
* Use it when a malformed response means "no data from this registry" rather
|
|
* than a failed lookup. Every other error still goes through
|
|
* {@link Datasource.handleGenericErrors}.
|
|
*/
|
|
protected async fetchJsonOrNull<Schema extends ZodType<any, any, any>>(
|
|
url: string,
|
|
schema: Schema,
|
|
options?: JsonOptions<H>,
|
|
): Promise<z.infer<Schema> | null> {
|
|
try {
|
|
return await this.fetchJson(url, schema, options);
|
|
} catch (err) {
|
|
if (err instanceof ZodError) {
|
|
logger.debug(
|
|
{ err, datasource: this.id, url },
|
|
'Ignoring response that failed schema validation',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
protected handleGenericErrors(err: Error): never {
|
|
if (err instanceof ExternalHostError) {
|
|
throw err;
|
|
}
|
|
|
|
if (err instanceof HttpError) {
|
|
this.handleHttpErrors(err);
|
|
|
|
const statusCode = err.response?.statusCode;
|
|
if (
|
|
statusCode &&
|
|
(statusCode === 429 || (statusCode >= 500 && statusCode < 600))
|
|
) {
|
|
throw new ExternalHostError(err);
|
|
}
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|