diff --git a/lib/modules/datasource/packagist/schema.ts b/lib/modules/datasource/packagist/schema.ts index 31c0ce0100a3ea8f5cfc77e952aeaf68f8750de3..e2929c33e1984be4152ae14c42b5d87b4daa946d 100644 --- a/lib/modules/datasource/packagist/schema.ts +++ b/lib/modules/datasource/packagist/schema.ts @@ -1,7 +1,11 @@ import is from '@sindresorhus/is'; import { z } from 'zod'; import { logger } from '../../../logger'; -import { looseArray, looseRecord, looseValue } from '../../../util/schema'; +import { + looseArray, + looseRecord, + looseValue, +} from '../../../util/schema-utils'; import type { Release, ReleaseResult } from '../types'; export const MinifiedArray = z.array(z.record(z.unknown())).transform((xs) => { diff --git a/lib/modules/platform/bitbucket-server/utils.ts b/lib/modules/platform/bitbucket-server/utils.ts index ea10aad605dc75bc9be9e52ceedaa92d5f268a97..a54e573b54663e4c52ae14275d51864ef108f5e6 100644 --- a/lib/modules/platform/bitbucket-server/utils.ts +++ b/lib/modules/platform/bitbucket-server/utils.ts @@ -61,7 +61,9 @@ function callApi<T>( case 'patch': return bitbucketServerHttp.patchJson<T>(apiUrl, options); case 'head': - return bitbucketServerHttp.headJson<T>(apiUrl, options); + return bitbucketServerHttp.headJson(apiUrl, options) as Promise< + HttpResponse<T> + >; case 'delete': return bitbucketServerHttp.deleteJson<T>(apiUrl, options); case 'get': diff --git a/lib/modules/platform/bitbucket/utils.ts b/lib/modules/platform/bitbucket/utils.ts index 7abfffb14ca633917a1eabdd5a1fdd766eb976a2..5466351d437db57cf36e1055ebc7a229cec0a8e5 100644 --- a/lib/modules/platform/bitbucket/utils.ts +++ b/lib/modules/platform/bitbucket/utils.ts @@ -86,7 +86,9 @@ function callApi<T>( case 'patch': return bitbucketHttp.patchJson<T>(apiUrl, options); case 'head': - return bitbucketHttp.headJson<T>(apiUrl, options); + return bitbucketHttp.headJson(apiUrl, options) as Promise< + HttpResponse<T> + >; case 'delete': return bitbucketHttp.deleteJson<T>(apiUrl, options); case 'get': diff --git a/lib/modules/platform/github/common.ts b/lib/modules/platform/github/common.ts index db8a22b3c84dcbaed2d02d8f3c0f2287a2aa6743..8fac76f9ee052ac8e461b78f2348f39b73d6c41c 100644 --- a/lib/modules/platform/github/common.ts +++ b/lib/modules/platform/github/common.ts @@ -1,7 +1,5 @@ import is from '@sindresorhus/is'; -import * as schema from '../../../util/schema'; import { getPrBodyStruct } from '../pr-body'; -import * as platformSchemas from '../schemas'; import type { GhPr, GhRestPr } from './types'; /** @@ -54,6 +52,5 @@ export function coerceRestPr(pr: GhRestPr): GhPr { result.targetBranch = pr.base.ref; } - schema.match(platformSchemas.Pr, result, 'warn'); return result; } diff --git a/lib/modules/platform/schemas.ts b/lib/modules/platform/schemas.ts deleted file mode 100644 index ac905936bff63f6ba529cdfbc5483a2d79953572..0000000000000000000000000000000000000000 --- a/lib/modules/platform/schemas.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from 'zod'; - -export const Pr = z.object( - { - sourceBranch: z.string().min(1), - number: z.number(), - state: z.string().min(1), - title: z.string().min(1), - }, - { - description: 'Pull Request', - } -); diff --git a/lib/util/cache/repository/impl/base.ts b/lib/util/cache/repository/impl/base.ts index c7a7e78da5a68ac22330256d2bf025a01572a20a..0114419b17dd0f259be95a438c32ec22bc1218bb 100644 --- a/lib/util/cache/repository/impl/base.ts +++ b/lib/util/cache/repository/impl/base.ts @@ -3,10 +3,9 @@ import hasha from 'hasha'; import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; import { compress, decompress } from '../../../compress'; -import * as schema from '../../../schema'; import { safeStringify } from '../../../stringify'; import { CACHE_REVISION } from '../common'; -import { RepoCacheRecord, RepoCacheV13 } from '../schemas'; +import { RepoCacheRecord, RepoCacheV13 } from '../schema'; import type { RepoCache, RepoCacheData } from '../types'; export abstract class RepoCacheBase implements RepoCache { @@ -44,8 +43,9 @@ export abstract class RepoCacheBase implements RepoCache { } const oldCache = JSON.parse(rawOldCache) as unknown; - if (schema.match(RepoCacheV13, oldCache)) { - await this.restore(oldCache); + const cacheV13 = RepoCacheV13.safeParse(oldCache); + if (cacheV13.success) { + await this.restore(cacheV13.data); logger.debug('Repository cache is restored from revision 13'); return; } diff --git a/lib/util/cache/repository/impl/local.spec.ts b/lib/util/cache/repository/impl/local.spec.ts index 80840ac124f9020fbe9ce7f4b1fff708d1558188..c6f8d50b07e426835b599812b0cf318b88f3a684 100644 --- a/lib/util/cache/repository/impl/local.spec.ts +++ b/lib/util/cache/repository/impl/local.spec.ts @@ -4,7 +4,7 @@ import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; import { compress } from '../../../compress'; import { CACHE_REVISION } from '../common'; -import type { RepoCacheRecord } from '../schemas'; +import type { RepoCacheRecord } from '../schema'; import type { RepoCacheData } from '../types'; import { CacheFactory } from './cache-factory'; import { RepoCacheLocal } from './local'; diff --git a/lib/util/cache/repository/impl/local.ts b/lib/util/cache/repository/impl/local.ts index ffed379566bf62765690feb383976bd9d115a130..c2d485b6803a29d6d69e9aa467ef8ac251432f79 100644 --- a/lib/util/cache/repository/impl/local.ts +++ b/lib/util/cache/repository/impl/local.ts @@ -2,7 +2,7 @@ import upath from 'upath'; import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; import { cachePathExists, outputCacheFile, readCacheFile } from '../../../fs'; -import type { RepoCacheRecord } from '../schemas'; +import type { RepoCacheRecord } from '../schema'; import { RepoCacheBase } from './base'; export class RepoCacheLocal extends RepoCacheBase { diff --git a/lib/util/cache/repository/impl/s3.spec.ts b/lib/util/cache/repository/impl/s3.spec.ts index fa10708369b50352c5f14fa2c66d684420d05562..a981dbef710c87dfc6de91bc5ff018840f0cacc4 100644 --- a/lib/util/cache/repository/impl/s3.spec.ts +++ b/lib/util/cache/repository/impl/s3.spec.ts @@ -12,7 +12,7 @@ import { partial } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; import { parseS3Url } from '../../../s3'; -import type { RepoCacheRecord } from '../schemas'; +import type { RepoCacheRecord } from '../schema'; import { CacheFactory } from './cache-factory'; import { RepoCacheS3 } from './s3'; diff --git a/lib/util/cache/repository/impl/s3.ts b/lib/util/cache/repository/impl/s3.ts index 87ff00ae82fdd730a9a84e58759bc18be68b2b79..f402409acab70740b7fa64e9d38e49a18889b260 100644 --- a/lib/util/cache/repository/impl/s3.ts +++ b/lib/util/cache/repository/impl/s3.ts @@ -8,7 +8,7 @@ import { import { logger } from '../../../../logger'; import { getS3Client, parseS3Url } from '../../../s3'; import { streamToString } from '../../../streams'; -import type { RepoCacheRecord } from '../schemas'; +import type { RepoCacheRecord } from '../schema'; import { RepoCacheBase } from './base'; export class RepoCacheS3 extends RepoCacheBase { diff --git a/lib/util/cache/repository/schemas.ts b/lib/util/cache/repository/schema.ts similarity index 100% rename from lib/util/cache/repository/schemas.ts rename to lib/util/cache/repository/schema.ts diff --git a/lib/util/http/index.spec.ts b/lib/util/http/index.spec.ts index 97226e30c161fa698ebd50050c66907afe92c283..811d3dfd7e6e6e7efe6f0a505569697196e67bee 100644 --- a/lib/util/http/index.spec.ts +++ b/lib/util/http/index.spec.ts @@ -1,4 +1,4 @@ -import * as z from 'zod'; +import { z } from 'zod'; import * as httpMock from '../../../test/http-mock'; import { logger } from '../../../test/util'; import { @@ -7,7 +7,6 @@ import { } from '../../constants/error-messages'; import * as memCache from '../cache/memory'; import * as hostRules from '../host-rules'; -import { reportErrors } from '../schema'; import * as queue from './queue'; import * as throttle from './throttle'; import type { HttpResponse } from './types'; @@ -316,8 +315,9 @@ describe('util/http/index', () => { }); describe('Schema support', () => { - const testSchema = z.object({ test: z.boolean() }); - type TestType = z.infer<typeof testSchema>; + const SomeSchema = z + .object({ x: z.number(), y: z.number() }) + .transform(({ x, y }) => `${x} + ${y} = ${x + y}`); beforeEach(() => { jest.resetAllMocks(); @@ -329,7 +329,7 @@ describe('util/http/index', () => { }); describe('getJson', () => { - it('infers body type', async () => { + it('uses schema for response body', async () => { httpMock .scope(baseUrl, { reqheaders: { @@ -337,21 +337,19 @@ describe('util/http/index', () => { }, }) .get('/') - .reply(200, JSON.stringify({ test: true })); + .reply(200, JSON.stringify({ x: 2, y: 2 })); - const { body }: HttpResponse<TestType> = await http.getJson( + const { body }: HttpResponse<string> = await http.getJson( 'http://renovate.com', - testSchema + { headers: { accept: 'application/json' } }, + SomeSchema ); - expect(body).toEqual({ test: true }); - - reportErrors(); - expect(logger.logger.warn).not.toHaveBeenCalled(); + expect(body).toBe('2 + 2 = 4'); + expect(logger.logger.once.info).not.toHaveBeenCalled(); }); - it('reports warnings', async () => { - memCache.init(); + it('returns original body if schema does not match', async () => { httpMock .scope(baseUrl, { reqheaders: { @@ -359,98 +357,41 @@ describe('util/http/index', () => { }, }) .get('/') - .reply(200, JSON.stringify({ test: 'foobar' })); - - const res = await http.getJson( - 'http://renovate.com', - { onSchemaError: 'warn' }, - testSchema - ); - - expect(res.body).toEqual({ test: 'foobar' }); + .reply(200, JSON.stringify({ foo: 'bar' })); - expect(logger.logger.warn).not.toHaveBeenCalled(); - reportErrors(); - expect(logger.logger.warn).toHaveBeenCalled(); - }); + const { body } = await http.getJson('http://renovate.com', SomeSchema); - it('throws', async () => { - httpMock - .scope(baseUrl, { - reqheaders: { - accept: 'application/json', - }, - }) - .get('/') - .reply(200, JSON.stringify({ test: 'foobar' })); - - await expect( - http.getJson( - 'http://renovate.com', - { onSchemaError: 'throw' }, - testSchema - ) - ).rejects.toThrow(); - - reportErrors(); - expect(logger.logger.warn).not.toHaveBeenCalled(); + expect(body).toEqual({ foo: 'bar' }); + expect(logger.logger.once.info).toHaveBeenCalled(); }); }); describe('postJson', () => { - it('infers body type', async () => { + it('uses schema for response body', async () => { httpMock .scope(baseUrl) .post('/') - .reply(200, JSON.stringify({ test: true })); + .reply(200, JSON.stringify({ x: 2, y: 2 })); - const { body }: HttpResponse<TestType> = await http.postJson( + const { body }: HttpResponse<string> = await http.postJson( 'http://renovate.com', - testSchema + SomeSchema ); - expect(body).toEqual({ test: true }); - - reportErrors(); - expect(logger.logger.warn).not.toHaveBeenCalled(); + expect(body).toBe('2 + 2 = 4'); + expect(logger.logger.once.info).not.toHaveBeenCalled(); }); - it('reports warnings', async () => { - memCache.init(); + it('returns original body if schema does not match', async () => { httpMock .scope(baseUrl) .post('/') - .reply(200, JSON.stringify({ test: 'foobar' })); - - const res = await http.postJson( - 'http://renovate.com', - { onSchemaError: 'warn' }, - testSchema - ); + .reply(200, JSON.stringify({ foo: 'bar' })); - expect(res.body).toEqual({ test: 'foobar' }); + const { body } = await http.postJson('http://renovate.com', SomeSchema); - expect(logger.logger.warn).not.toHaveBeenCalled(); - reportErrors(); - expect(logger.logger.warn).toHaveBeenCalled(); - }); - - it('throws', async () => { - httpMock - .scope(baseUrl) - .post('/') - .reply(200, JSON.stringify({ test: 'foobar' })); - - await expect( - http.postJson( - 'http://renovate.com', - { onSchemaError: 'throw' }, - testSchema - ) - ).rejects.toThrow(); - - reportErrors(); - expect(logger.logger.warn).not.toHaveBeenCalled(); + expect(body).toEqual({ foo: 'bar' }); + expect(logger.logger.once.info).toHaveBeenCalled(); }); }); }); diff --git a/lib/util/http/index.ts b/lib/util/http/index.ts index 43dc5237056945c66a3ec6a83c00759d9b9d50bd..31c3d886e43422476cf5c316fad6c65c81621a57 100644 --- a/lib/util/http/index.ts +++ b/lib/util/http/index.ts @@ -1,14 +1,13 @@ import merge from 'deepmerge'; import got, { Options, RequestError } from 'got'; import hasha from 'hasha'; -import { infer as Infer, ZodSchema } from 'zod'; +import { infer as Infer, ZodType } from 'zod'; import { HOST_DISABLED } from '../../constants/error-messages'; import { pkg } from '../../expose.cjs'; import { logger } from '../../logger'; import { ExternalHostError } from '../../types/errors/external-host-error'; import * as memCache from '../cache/memory'; import { clone } from '../clone'; -import { match } from '../schema'; import { resolveBaseUrl } from '../url'; import { applyAuthorization, removeAuthorization } from './auth'; import { hooks } from './hooks'; @@ -28,10 +27,14 @@ import './legacy'; export { RequestError as HttpError }; -type JsonArgs<T extends HttpOptions> = { +type JsonArgs< + Opts extends HttpOptions, + ResT = unknown, + Schema extends ZodType<ResT> = ZodType<ResT> +> = { url: string; - httpOptions?: T; - schema?: ZodSchema | undefined; + httpOptions?: Opts; + schema?: Schema; }; type Task<T> = () => Promise<HttpResponse<T>>; @@ -235,11 +238,11 @@ export class Http<Opts extends HttpOptions = HttpOptions> { return this.requestBuffer(url, options); } - private async requestJson<T = unknown>( + private async requestJson<ResT = unknown>( method: InternalHttpOptions['method'], - { url, httpOptions: requestOptions, schema }: JsonArgs<Opts> - ): Promise<HttpResponse<T>> { - const { body, onSchemaError, ...httpOptions } = { ...requestOptions }; + { url, httpOptions: requestOptions, schema }: JsonArgs<Opts, ResT> + ): Promise<HttpResponse<ResT>> { + const { body, ...httpOptions } = { ...requestOptions }; const opts: InternalHttpOptions = { ...httpOptions, method, @@ -253,23 +256,32 @@ export class Http<Opts extends HttpOptions = HttpOptions> { if (body) { opts.json = body; } - const res = await this.request<T>(url, opts); + const res = await this.request<ResT>(url, opts); + + if (!schema) { + return { ...res, body: res.body }; + } - if (schema) { - match(schema, res.body, onSchemaError); + const parsed = await schema.safeParseAsync(res.body); + if (!parsed.success) { + logger.once.info( + { err: parsed.error }, + `Response does not match schema: please report this to https://github.com/renovatebot/renovate/pull/21338` + ); + return { ...res, body: res.body }; } - return { ...res, body: res.body }; + return { ...res, body: parsed.data }; } - private resolveArgs( + private resolveArgs<ResT = unknown>( arg1: string, - arg2: Opts | ZodSchema | undefined, - arg3: ZodSchema | undefined - ): JsonArgs<Opts> { - const res: JsonArgs<Opts> = { url: arg1 }; + arg2: Opts | ZodType<ResT> | undefined, + arg3: ZodType<ResT> | undefined + ): JsonArgs<Opts, ResT> { + const res: JsonArgs<Opts, ResT> = { url: arg1 }; - if (arg2 instanceof ZodSchema) { + if (arg2 instanceof ZodType<ResT>) { res.schema = arg2; } else if (arg2) { res.httpOptions = arg2; @@ -282,115 +294,100 @@ export class Http<Opts extends HttpOptions = HttpOptions> { return res; } - getJson<T>(url: string, options?: Opts): Promise<HttpResponse<T>>; - getJson<T>( + getJson<ResT>(url: string, options?: Opts): Promise<HttpResponse<ResT>>; + getJson<ResT, Schema extends ZodType<ResT> = ZodType<ResT>>( url: string, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - getJson<T>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + getJson<ResT, Schema extends ZodType<ResT> = ZodType<ResT>>( url: string, options: Opts, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - getJson<T = unknown>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + getJson<ResT = unknown, Schema extends ZodType<ResT> = ZodType<ResT>>( arg1: string, - arg2?: Opts | ZodSchema, - arg3?: ZodSchema - ): Promise<HttpResponse<T>> { - const args = this.resolveArgs(arg1, arg2, arg3); - return this.requestJson<T>('get', args); + arg2?: Opts | Schema, + arg3?: Schema + ): Promise<HttpResponse<ResT>> { + const args = this.resolveArgs<ResT>(arg1, arg2, arg3); + return this.requestJson<ResT>('get', args); } - headJson<T>(url: string, options?: Opts): Promise<HttpResponse<T>>; - headJson<T>( - url: string, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - headJson<T>( - url: string, - options: Opts, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - headJson<T = unknown>( - arg1: string, - arg2?: Opts | ZodSchema, - arg3?: ZodSchema - ): Promise<HttpResponse<T>> { - const args = this.resolveArgs(arg1, arg2, arg3); - return this.requestJson<T>('head', args); + headJson(url: string, httpOptions?: Opts): Promise<HttpResponse<never>> { + return this.requestJson<never>('head', { url, httpOptions }); } postJson<T>(url: string, options?: Opts): Promise<HttpResponse<T>>; - postJson<T>( + postJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - postJson<T>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + postJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, options: Opts, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - postJson<T = unknown>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + postJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>( arg1: string, - arg2?: Opts | ZodSchema, - arg3?: ZodSchema + arg2?: Opts | Schema, + arg3?: Schema ): Promise<HttpResponse<T>> { const args = this.resolveArgs(arg1, arg2, arg3); return this.requestJson<T>('post', args); } putJson<T>(url: string, options?: Opts): Promise<HttpResponse<T>>; - putJson<T>( + putJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - putJson<T>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + putJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, options: Opts, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - putJson<T = unknown>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + putJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>( arg1: string, - arg2?: Opts | ZodSchema, - arg3?: ZodSchema + arg2?: Opts | Schema, + arg3?: ZodType ): Promise<HttpResponse<T>> { const args = this.resolveArgs(arg1, arg2, arg3); return this.requestJson<T>('put', args); } patchJson<T>(url: string, options?: Opts): Promise<HttpResponse<T>>; - patchJson<T>( + patchJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - patchJson<T>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + patchJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, options: Opts, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - patchJson<T = unknown>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + patchJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>( arg1: string, - arg2?: Opts | ZodSchema, - arg3?: ZodSchema + arg2?: Opts | Schema, + arg3?: Schema ): Promise<HttpResponse<T>> { const args = this.resolveArgs(arg1, arg2, arg3); return this.requestJson<T>('patch', args); } deleteJson<T>(url: string, options?: Opts): Promise<HttpResponse<T>>; - deleteJson<T>( + deleteJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - deleteJson<T>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + deleteJson<T, Schema extends ZodType<T> = ZodType<T>>( url: string, options: Opts, - schema: ZodSchema<T> - ): Promise<HttpResponse<Infer<typeof schema>>>; - deleteJson<T = unknown>( + schema: Schema + ): Promise<HttpResponse<Infer<Schema>>>; + deleteJson<T = unknown, Schema extends ZodType<T> = ZodType<T>>( arg1: string, - arg2?: Opts | ZodSchema, - arg3?: ZodSchema + arg2?: Opts | Schema, + arg3?: Schema ): Promise<HttpResponse<T>> { const args = this.resolveArgs(arg1, arg2, arg3); return this.requestJson<T>('delete', args); diff --git a/lib/util/http/types.ts b/lib/util/http/types.ts index 8616e9c121a0013cb1a97c04e34d4bf3ab8bfa68..4cf1774683eb422b73ad2b64482522910c5555b7 100644 --- a/lib/util/http/types.ts +++ b/lib/util/http/types.ts @@ -63,8 +63,6 @@ export interface HttpOptions { token?: string; useCache?: boolean; - - onSchemaError?: 'warn' | 'throw'; } export interface InternalHttpOptions extends HttpOptions { diff --git a/lib/util/schema.spec.ts b/lib/util/schema-utils.spec.ts similarity index 52% rename from lib/util/schema.spec.ts rename to lib/util/schema-utils.spec.ts index 3bcabb285c196a9c9bebea49f5c2a798d821a5a5..10af9db538eb20c4e0a2c1f546c2052b41068b82 100644 --- a/lib/util/schema.spec.ts +++ b/lib/util/schema-utils.spec.ts @@ -1,87 +1,7 @@ import { z } from 'zod'; -import { logger } from '../../test/util'; -import * as memCache from './cache/memory'; -import * as schema from './schema'; - -describe('util/schema', () => { - beforeEach(() => { - jest.resetAllMocks(); - memCache.init(); - }); - - it('validates data', () => { - const testSchema = z.object({ foo: z.string() }); - const validData = { foo: 'bar' }; - - const res = schema.match(testSchema, validData); - expect(res).toBeTrue(); - }); - - it('returns false for invalid data', () => { - const testSchema = z.object({ foo: z.string() }); - const invalidData = { foo: 123 }; - - const res = schema.match(testSchema, invalidData); - expect(res).toBeFalse(); - - schema.reportErrors(); - expect(logger.logger.warn).not.toHaveBeenCalled(); - }); - - describe('warn', () => { - it('reports nothing if there are no any reports', () => { - schema.reportErrors(); - expect(logger.logger.warn).not.toHaveBeenCalled(); - }); - - it('reports same warning one time', () => { - const testSchema = z.object( - { foo: z.string() }, - { description: 'Some test schema' } - ); - const invalidData = { foo: 42 }; - - schema.match(testSchema, invalidData, 'warn'); - schema.match(testSchema, invalidData, 'warn'); - schema.match(testSchema, invalidData, 'warn'); - schema.match(testSchema, invalidData, 'warn'); - schema.reportErrors(); - - expect(logger.logger.warn).toHaveBeenCalledOnce(); - expect(logger.logger.warn.mock.calls[0]).toMatchObject([ - { description: 'Some test schema' }, - 'Schema validation error', - ]); - }); - - it('reports unspecified schema', () => { - const testSchema = z.object({ foo: z.string() }); - const invalidData = { foo: 42 }; - - schema.match(testSchema, invalidData, 'warn'); - schema.reportErrors(); - - expect(logger.logger.warn).toHaveBeenCalledOnce(); - expect(logger.logger.warn.mock.calls[0]).toMatchObject([ - { description: 'Unspecified schema' }, - 'Schema validation error', - ]); - }); - }); - - describe('throw', () => { - it('throws for invalid data', () => { - const testSchema = z.object({ - foo: z.string({ invalid_type_error: 'foobar' }), - }); - const invalidData = { foo: 123 }; - - expect(() => schema.match(testSchema, invalidData, 'throw')).toThrow( - 'foobar' - ); - }); - }); +import * as schema from './schema-utils'; +describe('util/schema-utils', () => { describe('looseArray', () => { it('parses array', () => { const s = schema.looseArray(z.string()); diff --git a/lib/util/schema.ts b/lib/util/schema-utils.ts similarity index 52% rename from lib/util/schema.ts rename to lib/util/schema-utils.ts index 3d6f0254a8cc709fceeb4ef8f9d8fbc9f68ee812..781cca9d8ea98162870c1c44c4438ab40e44b596 100644 --- a/lib/util/schema.ts +++ b/lib/util/schema-utils.ts @@ -1,71 +1,4 @@ -import is from '@sindresorhus/is'; -import hasha from 'hasha'; import { z } from 'zod'; -import { logger } from '../logger'; -import * as memCache from './cache/memory'; -import { safeStringify } from './stringify'; - -type SchemaErrorsMap = Record<string, Record<string, z.ZodError>>; - -function getCacheKey(error: z.ZodError): string { - const content = safeStringify(error); - const key = hasha(content).slice(0, 32); - return `err_${key}`; -} - -function collectError<T extends z.ZodSchema>( - schema: T, - error: z.ZodError -): void { - const { description = 'Unspecified schema' } = schema; - const schemaErrorsMap = memCache.get<SchemaErrorsMap>('schema-errors') ?? {}; - const schemaErrors = schemaErrorsMap[description] ?? {}; - const key = getCacheKey(error); - const schemaError = schemaErrors[key]; - if (!schemaError) { - schemaErrors[key] = error; - schemaErrorsMap[description] = schemaErrors; - } - memCache.set('schema-errors', schemaErrorsMap); -} - -export function reportErrors(): void { - const schemaErrorsMap = memCache.get<SchemaErrorsMap>('schema-errors'); - if (!schemaErrorsMap) { - return; - } - - for (const [description, schemaErrors] of Object.entries(schemaErrorsMap)) { - const errors = Object.values(schemaErrors); - for (const err of errors) { - logger.warn({ description, err }, `Schema validation error`); - } - } - - memCache.set('schema-errors', null); -} - -export function match<T extends z.ZodSchema>( - schema: T, - input: unknown, - onError?: 'warn' | 'throw' -): input is z.infer<T> { - const res = schema.safeParse(input); - const { success } = res; - if (!success) { - if (onError === 'warn') { - collectError(schema, res.error); - } - - if (onError === 'throw') { - throw res.error; - } - - return false; - } - - return true; -} export function looseArray<T extends z.ZodTypeAny>( schema: T, @@ -96,7 +29,7 @@ export function looseArray<T extends z.ZodTypeAny>( : arrayOfNullables.catch([]); const filteredArray = arrayWithFallback.transform((xs) => - xs.filter((x): x is Elem => !is.null_(x)) + xs.filter((x): x is Elem => x !== null) ); return filteredArray; @@ -133,7 +66,7 @@ export function looseRecord<T extends z.ZodTypeAny>( const filteredRecord = recordWithFallback.transform( (rec): Record<string, Elem> => { for (const key of Object.keys(rec)) { - if (is.null_(rec[key])) { + if (rec[key] === null) { delete rec[key]; } } diff --git a/lib/workers/repository/index.ts b/lib/workers/repository/index.ts index d5a5474d41a6f5a5aaf9d533eb5dc3fcd90a088a..45103753917f34b5617adbd59ff9a14883c7e421 100644 --- a/lib/workers/repository/index.ts +++ b/lib/workers/repository/index.ts @@ -17,7 +17,6 @@ import { detectSemanticCommits } from '../../util/git/semantic'; import { clearDnsCache, printDnsStats } from '../../util/http/dns'; import * as queue from '../../util/http/queue'; import * as throttle from '../../util/http/throttle'; -import * as schemaUtil from '../../util/schema'; import { addSplit, getSplits, splitInit } from '../../util/split'; import { setBranchCache } from './cache'; import { ensureDependencyDashboard } from './dependency-dashboard'; @@ -126,7 +125,6 @@ export async function renovateRepository( printLookupStats(); printDnsStats(); clearDnsCache(); - schemaUtil.reportErrors(); const cloned = isCloned(); logger.info({ cloned, durationMs: splits.total }, 'Repository finished'); return repoResult;