From eb8a02c37ff04fe3eb3e6e7f60eeef2ed47ee6f4 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov <zharinov@users.noreply.github.com> Date: Fri, 24 Feb 2023 12:01:58 +0300 Subject: [PATCH] refactor(github): Use schema validation for GraphQL (#20519) --- .../github/graphql/datasource-fetcher.spec.ts | 14 ++++-- lib/util/github/graphql/datasource-fetcher.ts | 8 +++- lib/util/github/graphql/index.spec.ts | 14 +++--- .../releases-query-adapter.spec.ts | 6 ++- .../query-adapters/releases-query-adapter.ts | 36 ++++++++++++-- .../query-adapters/tags-query-adapter.ts | 47 +++++++++++++------ lib/util/github/graphql/types.ts | 42 ++--------------- 7 files changed, 96 insertions(+), 71 deletions(-) diff --git a/lib/util/github/graphql/datasource-fetcher.spec.ts b/lib/util/github/graphql/datasource-fetcher.spec.ts index 673a96c0ee..e948966477 100644 --- a/lib/util/github/graphql/datasource-fetcher.spec.ts +++ b/lib/util/github/graphql/datasource-fetcher.spec.ts @@ -44,11 +44,14 @@ const adapter: GithubGraphqlDatasourceAdapter< version, releaseTimestamp, foo, - }: TestAdapterInput): TestAdapterOutput => ({ - version, - releaseTimestamp, - bar: foo, - }), + }: TestAdapterInput): TestAdapterOutput | null => + version && releaseTimestamp && foo + ? { + version, + releaseTimestamp, + bar: foo, + } + : null, }; function resp( @@ -208,6 +211,7 @@ describe('util/github/graphql/datasource-fetcher', () => { resp(false, [ { version: v3, releaseTimestamp: t3, foo: '3' }, { version: v2, releaseTimestamp: t2, foo: '2' }, + {} as never, { version: v1, releaseTimestamp: t1, foo: '1' }, ]) ); diff --git a/lib/util/github/graphql/datasource-fetcher.ts b/lib/util/github/graphql/datasource-fetcher.ts index 6ab09ebb56..ca53fb6eff 100644 --- a/lib/util/github/graphql/datasource-fetcher.ts +++ b/lib/util/github/graphql/datasource-fetcher.ts @@ -239,8 +239,14 @@ export class GithubGraphqlDatasourceFetcher< const resultItems: ResultItem[] = []; for (const node of queryResult.nodes) { const item = this.datasourceAdapter.transform(node); - // istanbul ignore if: will be tested later if (!item) { + logger.once.info( + { + packageName: `${this.repoOwner}/${this.repoName}`, + baseUrl: this.baseUrl, + }, + `GitHub GraphQL datasource: skipping empty item` + ); continue; } resultItems.push(item); diff --git a/lib/util/github/graphql/index.spec.ts b/lib/util/github/graphql/index.spec.ts index 315ee6afc5..a234bac6dd 100644 --- a/lib/util/github/graphql/index.spec.ts +++ b/lib/util/github/graphql/index.spec.ts @@ -52,12 +52,14 @@ describe('util/github/graphql/index', () => { payload: { nodes: [ { - id: 123, - name: 'name', - description: 'description', version: '1.2.3', releaseTimestamp: '2024-09-24', + isDraft: false, + isPrerelease: false, url: 'https://example.com', + id: 123, + name: 'name', + description: 'description', }, ], }, @@ -69,12 +71,12 @@ describe('util/github/graphql/index', () => { expect(res).toEqual([ { - id: 123, - name: 'name', - description: 'description', version: '1.2.3', releaseTimestamp: '2024-09-24', url: 'https://example.com', + id: 123, + name: 'name', + description: 'description', }, ]); }); diff --git a/lib/util/github/graphql/query-adapters/releases-query-adapter.spec.ts b/lib/util/github/graphql/query-adapters/releases-query-adapter.spec.ts index 0bd88165e6..7e874c311e 100644 --- a/lib/util/github/graphql/query-adapters/releases-query-adapter.spec.ts +++ b/lib/util/github/graphql/query-adapters/releases-query-adapter.spec.ts @@ -1,5 +1,5 @@ -import type { GithubGraphqlRelease } from '../types'; import { adapter } from './releases-query-adapter'; +import type { GithubGraphqlRelease } from './releases-query-adapter'; const item: GithubGraphqlRelease = { version: '1.2.3', @@ -28,6 +28,10 @@ describe('util/github/graphql/query-adapters/releases-query-adapter', () => { expect(adapter.transform({ ...item, isDraft: true })).toBeNull(); }); + it('handles invalid items', () => { + expect(adapter.transform({} as never)).toBeNull(); + }); + it('marks prereleases as unstable', () => { expect(adapter.transform({ ...item, isPrerelease: true })).toMatchObject({ isStable: false, diff --git a/lib/util/github/graphql/query-adapters/releases-query-adapter.ts b/lib/util/github/graphql/query-adapters/releases-query-adapter.ts index e149a0b25a..65cf0cfb10 100644 --- a/lib/util/github/graphql/query-adapters/releases-query-adapter.ts +++ b/lib/util/github/graphql/query-adapters/releases-query-adapter.ts @@ -1,6 +1,6 @@ +import { z } from 'zod'; import type { GithubGraphqlDatasourceAdapter, - GithubGraphqlRelease, GithubReleaseItem, } from '../types'; import { prepareQuery } from '../util'; @@ -30,7 +30,24 @@ const query = prepareQuery(` } `); +const GithubGraphqlRelease = z.object({ + version: z.string(), + releaseTimestamp: z.string(), + isDraft: z.boolean(), + isPrerelease: z.boolean(), + url: z.string(), + id: z.number().nullable(), + name: z.string().nullable(), + description: z.string().nullable(), +}); +export type GithubGraphqlRelease = z.infer<typeof GithubGraphqlRelease>; + function transform(item: GithubGraphqlRelease): GithubReleaseItem | null { + const releaseItem = GithubGraphqlRelease.safeParse(item); + if (!releaseItem.success) { + return null; + } + const { version, releaseTimestamp, @@ -40,7 +57,7 @@ function transform(item: GithubGraphqlRelease): GithubReleaseItem | null { id, name, description, - } = item; + } = releaseItem.data; if (isDraft) { return null; @@ -50,11 +67,20 @@ function transform(item: GithubGraphqlRelease): GithubReleaseItem | null { version, releaseTimestamp, url, - id, - name, - description, }; + if (id) { + result.id = id; + } + + if (name) { + result.name = name; + } + + if (description) { + result.description = description; + } + if (isPrerelease) { result.isStable = false; } diff --git a/lib/util/github/graphql/query-adapters/tags-query-adapter.ts b/lib/util/github/graphql/query-adapters/tags-query-adapter.ts index 7d1e034bfa..0fbb2cf772 100644 --- a/lib/util/github/graphql/query-adapters/tags-query-adapter.ts +++ b/lib/util/github/graphql/query-adapters/tags-query-adapter.ts @@ -1,12 +1,30 @@ -import type { - GithubGraphqlDatasourceAdapter, - GithubGraphqlTag, - GithubTagItem, -} from '../types'; +import { z } from 'zod'; +import type { GithubGraphqlDatasourceAdapter, GithubTagItem } from '../types'; import { prepareQuery } from '../util'; const key = 'github-tags-datasource-v2'; +const GithubGraphqlTag = z.object({ + version: z.string(), + target: z.union([ + z.object({ + type: z.literal('Commit'), + oid: z.string(), + releaseTimestamp: z.string(), + }), + z.object({ + type: z.literal('Tag'), + target: z.object({ + oid: z.string(), + }), + tagger: z.object({ + releaseTimestamp: z.string(), + }), + }), + ]), +}); +export type GithubGraphqlTag = z.infer<typeof GithubGraphqlTag>; + const query = prepareQuery(` refs( first: $count @@ -41,16 +59,17 @@ const query = prepareQuery(` }`); function transform(item: GithubGraphqlTag): GithubTagItem | null { - const { version, target } = item; - if (target.type === 'Commit') { - const { oid: hash, releaseTimestamp } = target; - return { version, gitRef: version, hash, releaseTimestamp }; - } else if (target.type === 'Tag') { - const { oid: hash } = target.target; - const { releaseTimestamp } = target.tagger; - return { version, gitRef: version, hash, releaseTimestamp }; + const res = GithubGraphqlTag.safeParse(item); + if (!res.success) { + return null; } - return null; + const { version, target } = item; + const releaseTimestamp = + target.type === 'Commit' + ? target.releaseTimestamp + : target.tagger.releaseTimestamp; + const hash = target.type === 'Commit' ? target.oid : target.target.oid; + return { version, gitRef: version, hash, releaseTimestamp }; } export const adapter: GithubGraphqlDatasourceAdapter< diff --git a/lib/util/github/graphql/types.ts b/lib/util/github/graphql/types.ts index 7b20caeed7..7bcfa4ec4a 100644 --- a/lib/util/github/graphql/types.ts +++ b/lib/util/github/graphql/types.ts @@ -58,51 +58,15 @@ export interface GithubPackageConfig { registryUrl?: string; } -/** - * GraphQL shape for releases - */ -export interface GithubGraphqlRelease { - version: string; - releaseTimestamp: string; - isDraft: boolean; - isPrerelease: boolean; - url: string; - id: number; - name: string; - description: string; -} - /** * Result of GraphQL response transformation for releases (via adapter) */ export interface GithubReleaseItem extends GithubDatasourceItem { isStable?: boolean; url: string; - id: number; - name: string; - description: string; -} - -/** - * GraphQL shape for tags - */ -export interface GithubGraphqlTag { - version: string; - target: - | { - type: 'Commit'; - oid: string; - releaseTimestamp: string; - } - | { - type: 'Tag'; - target: { - oid: string; - }; - tagger: { - releaseTimestamp: string; - }; - }; + id?: number; + name?: string; + description?: string; } /** -- GitLab