diff --git a/.changeset/chilled-queens-wonder.md b/.changeset/chilled-queens-wonder.md new file mode 100644 index 0000000000..a10212792d --- /dev/null +++ b/.changeset/chilled-queens-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Ensure that entity cache state is only written to the database when actually changed diff --git a/.changeset/fifty-grapes-explode.md b/.changeset/fifty-grapes-explode.md new file mode 100644 index 0000000000..999a105c4f --- /dev/null +++ b/.changeset/fifty-grapes-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests. diff --git a/.changeset/moody-shrimps-train.md b/.changeset/moody-shrimps-train.md new file mode 100644 index 0000000000..397500e2cb --- /dev/null +++ b/.changeset/moody-shrimps-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': minor +--- + +Add a new scaffolder action for gitlab to ensure a group exists diff --git a/.changeset/quiet-bikes-smash.md b/.changeset/quiet-bikes-smash.md new file mode 100644 index 0000000000..ca751adaf7 --- /dev/null +++ b/.changeset/quiet-bikes-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': patch +--- + +Corrected resolution of parameter nested schema to use central schemas. diff --git a/.changeset/thin-ways-exist.md b/.changeset/thin-ways-exist.md new file mode 100644 index 0000000000..fd1ecb62a4 --- /dev/null +++ b/.changeset/thin-ways-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Fix text-overflow UI issue for Lifecycle spans in SearchFilter checkbox labels. diff --git a/.changeset/twelve-zebras-repair.md b/.changeset/twelve-zebras-repair.md new file mode 100644 index 0000000000..8d74080c38 --- /dev/null +++ b/.changeset/twelve-zebras-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +Added an OpenAPI 3.0 spec and enforced schema-first model on the router. diff --git a/packages/backend-openapi-utils/api-report.md b/packages/backend-openapi-utils/api-report.md index 180a7846ed..a4db8ac7bd 100644 --- a/packages/backend-openapi-utils/api-report.md +++ b/packages/backend-openapi-utils/api-report.md @@ -333,7 +333,6 @@ declare namespace internal { ImmutableSchemaObject, DocParameter, DocParameters, - ResolveDocParameterSchema, ParameterSchema, MapToSchema, ParametersSchema, @@ -412,7 +411,7 @@ type OptionalMap< type ParameterSchema< Doc extends RequiredDoc, Schema extends ImmutableParameterObject['schema'], -> = ResolveDocParameterSchema extends infer R +> = SchemaRef extends infer R ? R extends ImmutableSchemaObject ? R extends JSONSchema7 ? FromSchema @@ -549,16 +548,6 @@ type RequiredMap< [P in Exclude, undefined>]: NonNullable; }; -// @public (undocumented) -type ResolveDocParameterSchema< - Doc extends RequiredDoc, - Schema extends ImmutableParameterObject['schema'], -> = Schema extends ImmutableReferenceObject - ? 'parameters' extends ComponentTypes - ? ComponentRef - : never - : Schema; - // @public (undocumented) type Response_2< Doc extends RequiredDoc, diff --git a/packages/backend-openapi-utils/src/types/params.ts b/packages/backend-openapi-utils/src/types/params.ts index dce743e6e8..b737450db7 100644 --- a/packages/backend-openapi-utils/src/types/params.ts +++ b/packages/backend-openapi-utils/src/types/params.ts @@ -34,6 +34,7 @@ import { MapDiscriminatedUnion, PathTemplate, RequiredDoc, + SchemaRef, } from './common'; import { FromSchema, JSONSchema7 } from 'json-schema-to-ts'; @@ -76,25 +77,13 @@ export type DocParameters< } : never; -/** - * @public - */ -export type ResolveDocParameterSchema< - Doc extends RequiredDoc, - Schema extends ImmutableParameterObject['schema'], -> = Schema extends ImmutableReferenceObject - ? 'parameters' extends ComponentTypes - ? ComponentRef - : never - : Schema; - /** * @public */ export type ParameterSchema< Doc extends RequiredDoc, Schema extends ImmutableParameterObject['schema'], -> = ResolveDocParameterSchema extends infer R +> = SchemaRef extends infer R ? R extends ImmutableSchemaObject ? R extends JSONSchema7 ? FromSchema diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index ab1e0b96c9..e89d53f79f 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -150,7 +150,10 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { track.markProcessorsCompleted(result); if (result.ok) { - if (stableStringify(state) !== stableStringify(result.state)) { + const { ttl: _, ...stateWithoutTtl } = state ?? {}; + if ( + stableStringify(stateWithoutTtl) !== stableStringify(result.state) + ) { await this.processingDatabase.transaction(async tx => { await this.processingDatabase.updateEntityCache(tx, { id, diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 08ae295834..93a37fb04a 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -1418,6 +1418,87 @@ describe('DefaultEntitiesCatalog', () => { }, ); + it.each(databases.eachSupportedId())( + 'should exclude filtered entities when paginating, %p', + async databaseId => { + await createDatabase(databaseId); + + await Promise.all([ + addEntityToSearch(entityFrom('AA', { uid: '1', kind: 'included' })), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace2', + kind: 'included', + uid: '2', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'ns', + kind: 'excluded', + uid: '3', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace3', + uid: '4', + kind: 'included', + }), + ), + addEntityToSearch( + entityFrom('AA', { + namespace: 'namespace4', + uid: '5', + kind: 'included', + }), + ), + addEntityToSearch(entityFrom('CC', { uid: '6', kind: 'included' })), + addEntityToSearch(entityFrom('DD', { uid: '7', kind: 'included' })), + ]); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: getVoidLogger(), + stitcher, + }); + + const limit = 2; + + // initial request + const request1: QueryEntitiesInitialRequest = { + limit, + filter: { + key: 'kind', + values: ['included'], + }, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + }; + const response1 = await catalog.queryEntities(request1); + expect(response1.items).toMatchObject([ + entityFrom('AA', { uid: '1', kind: 'included' }), + entityFrom('AA', { uid: '2', kind: 'included' }), + ]); + expect(response1.pageInfo.nextCursor).toBeDefined(); + expect(response1.pageInfo.prevCursor).toBeUndefined(); + expect(response1.totalItems).toBe(6); + + // second request (forward) + const request2: QueryEntitiesCursorRequest = { + cursor: response1.pageInfo.nextCursor!, + limit, + }; + const response2 = await catalog.queryEntities(request2); + expect(response2.items).toMatchObject([ + entityFrom('AA', { uid: '4', kind: 'included' }), + entityFrom('AA', { uid: '5', kind: 'included' }), + ]); + expect(response2.pageInfo.nextCursor).toBeDefined(); + expect(response2.pageInfo.prevCursor).toBeDefined(); + expect(response2.totalItems).toBe(6); + }, + ); + it.each(databases.eachSupportedId())( 'should paginate results without sort fields, %p', async databaseId => { @@ -1754,11 +1835,12 @@ function entityFrom( uid, namespace, title, - }: { uid?: string; namespace?: string; title?: string } = {}, + kind = 'k', + }: { uid?: string; namespace?: string; title?: string; kind?: string } = {}, ) { return { apiVersion: 'a', - kind: 'k', + kind, metadata: { name, ...(!!namespace && { namespace }), diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index ba165f96af..5fca950cac 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -413,17 +413,18 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { const isOrderingDescending = sortField.order === 'desc'; if (prevItemOrderFieldValue) { - dbQuery.andWhere( - 'value', - isFetchingBackwards !== isOrderingDescending ? '<' : '>', - prevItemOrderFieldValue, - ); - dbQuery.orWhere(function nested() { - this.where('value', '=', prevItemOrderFieldValue).andWhere( - 'search.entity_id', + dbQuery.andWhere(function nested() { + this.where( + 'value', isFetchingBackwards !== isOrderingDescending ? '<' : '>', - prevItemUid, - ); + prevItemOrderFieldValue, + ) + .orWhere('value', '=', prevItemOrderFieldValue) + .andWhere( + 'search.entity_id', + isFetchingBackwards !== isOrderingDescending ? '<' : '>', + prevItemUid, + ); }); } diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md index 35353cf9c1..d071ca0cf6 100644 --- a/plugins/scaffolder-backend-module-gitlab/README.md +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -45,6 +45,9 @@ const actions = [ createGitlabProjectDeployTokenAction({ integrations: integrations, }), + createGitlabGroupEnsureExistsAction({ + integrations: integrations, + }), ]; // Create Scaffolder Router @@ -104,13 +107,22 @@ spec: url: https://github.com/TEMPLATE values: name: ${{ parameters.name }} + - id: createGitlabGroup + name: Ensure Gitlab group exists + action: gitlab:group:ensureExists + input: + repoUrl: ${{ parameters.repoUrl }} + path: + - path + - to + - group - id: publish name: Publish action: publish:gitlab input: description: This is ${{ parameters.name }} - repoUrl: ${{ parameters.repoUrl }} + repoUrl: ${{ parameters.repoUrl }}?owner=${{ steps.createGitlabGroup.output.groupId }} sourcePath: pimcore defaultBranch: main diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index 2ec05e74de..96ec3445dc 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -7,19 +7,37 @@ import { JsonObject } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +// @public +export const createGitlabGroupEnsureExistsAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction< + { + repoUrl: string; + token?: string | undefined; + } & { + path: string[]; + }, + { + groupId?: number | undefined; + } +>; + // @public export const createGitlabProjectAccessTokenAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< { repoUrl: string; - projectId: string | number; - name: string; - accessLevel: number; - scopes: string[]; token?: string | undefined; + } & { + projectId: string | number; + name?: string | undefined; + accessLevel?: number | undefined; + scopes?: string[] | undefined; }, - JsonObject + { + access_token: string; + } >; // @public @@ -28,13 +46,17 @@ export const createGitlabProjectDeployTokenAction: (options: { }) => TemplateAction< { repoUrl: string; - projectId: string | number; - name: string; - username: string; - scopes: string[]; token?: string | undefined; + } & { + name: string; + projectId: string | number; + username?: string | undefined; + scopes?: string[] | undefined; }, - JsonObject + { + user: string; + deploy_token: string; + } >; // @public @@ -43,15 +65,16 @@ export const createGitlabProjectVariableAction: (options: { }) => TemplateAction< { repoUrl: string; - projectId: string | number; + token?: string | undefined; + } & { key: string; value: string; + projectId: string | number; variableType: string; - variableProtected: boolean; - masked: boolean; - raw: boolean; - environmentScope: string; - token?: string | undefined; + variableProtected?: boolean | undefined; + masked?: boolean | undefined; + raw?: boolean | undefined; + environmentScope?: string | undefined; }, JsonObject >; diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index ee10ecaf65..e338c21784 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -35,7 +35,8 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "@gitbeaker/node": "^35.8.0" + "@gitbeaker/node": "^35.8.0", + "zod": "^3.21.4" }, "devDependencies": { "@backstage/backend-common": "workspace:^", diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts new file mode 100644 index 0000000000..6fb119413f --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { createGitlabGroupEnsureExistsAction } from './createGitlabGroupEnsureExistsAction'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; + +const mockGitlabClient = { + Groups: { + search: jest.fn(), + create: jest.fn(), + }, +}; +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:group:ensureExists', () => { + const mockContext = { + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should create a new group if it does not exists', async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'repos/bar', + }, + { + id: 2, + full_path: 'repos/foo', + }, + ]); + + mockGitlabClient.Groups.create.mockResolvedValue({ + id: 3, + full_path: 'repos/foo/bar', + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupEnsureExistsAction({ integrations }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com', + path: ['foo', 'bar'], + }, + }); + + expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith('bar', 'bar', { + parent_id: 2, + }); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); + }); + + it('should return existing group if it does exists', async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'repos/bar', + }, + { + id: 2, + full_path: 'repos/foo', + }, + { + id: 42, + full_path: 'repos/foo/bar', + }, + ]); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupEnsureExistsAction({ integrations }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com', + path: ['foo', 'bar'], + }, + }); + + expect(mockGitlabClient.Groups.create).not.toHaveBeenCalled(); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 42); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts new file mode 100644 index 0000000000..698d402697 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabGroupEnsureExistsAction.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { Gitlab } from '@gitbeaker/node'; +import { GroupSchema } from '@gitbeaker/core/dist/types/resources/Groups'; +import commonGitlabConfig from '../commonGitlabConfig'; +import { getToken } from '../util'; +import { z } from 'zod'; + +/** + * Creates an `gitlab:group:ensureExists` Scaffolder action. + * + * @public + */ +export const createGitlabGroupEnsureExistsAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + + return createTemplateAction({ + id: 'gitlab:group:ensureExists', + description: 'Ensures a Gitlab group exists', + schema: { + input: commonGitlabConfig.and( + z.object({ + path: z + .array(z.string(), { + description: 'A path of group names that is ensured to exist', + }) + .min(1), + }), + ), + output: z.object({ + groupId: z + .number({ description: 'The id of the innermost sub-group' }) + .optional(), + }), + }, + async handler(ctx) { + const { path } = ctx.input; + const { token, integrationConfig } = getToken(ctx.input, integrations); + + const api = new Gitlab({ + host: integrationConfig.config.baseUrl, + token: token, + }); + + let currentPath: string = 'repos'; + let parent: GroupSchema | null = null; + for (const pathElement of path) { + const fullPath = `${currentPath}/${pathElement}`; + const result = (await api.Groups.search( + fullPath, + )) as any as Array; // recast since the return type for search is wrong in the gitbeaker typings + const subGroup = result.find( + searchPathElem => searchPathElem.full_path === fullPath, + ); + if (!subGroup) { + ctx.logger.info(`creating missing group ${fullPath}`); + parent = await api.Groups.create( + pathElement, + pathElement, + parent + ? { + parent_id: parent.id, + } + : {}, + ); + } else { + parent = subGroup; + } + currentPath = fullPath; + } + if (parent !== null) { + ctx.output('groupId', parent?.id); + } + }, + }); +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts index 0f39e3bc33..832eb4cac7 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts @@ -16,10 +16,12 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import commonGitlabConfig from '../commonGitlabConfig'; import { getToken } from '../util'; +import { z } from 'zod'; /** - * Creates a `gitlab:create-project-access-token` Scaffolder action. + * Creates a `gitlab:projectAccessToken:create` Scaffolder action. * * @param options - Templating configuration. * @public @@ -28,65 +30,29 @@ export const createGitlabProjectAccessTokenAction = (options: { integrations: ScmIntegrationRegistry; }) => { const { integrations } = options; - return createTemplateAction<{ - repoUrl: string; - projectId: string | number; - name: string; - accessLevel: number; - scopes: string[]; - token?: string; - }>({ + return createTemplateAction({ id: 'gitlab:projectAccessToken:create', schema: { - input: { - required: ['projectId', 'repoUrl'], - type: 'object', - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - projectId: { - title: 'Project ID', - type: ['string', 'number'], - }, - name: { - title: 'Deploy Token Name', - type: 'string', - }, - accessLevel: { - title: 'Access Level of the Token', - type: 'number', - }, - scopes: { - title: 'Scopes', - type: 'array', - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitLab', - }, - }, - }, - output: { - type: 'object', - properties: { - access_token: { - title: 'Access Token', - type: 'string', - }, - }, - }, + input: commonGitlabConfig.and( + z.object({ + projectId: z.union([z.number(), z.string()], { + description: 'Project ID', + }), + name: z.string({ description: 'Deploy Token Name' }).optional(), + accessLevel: z + .number({ description: 'Access Level of the Token' }) + .optional(), + scopes: z.array(z.string(), { description: 'Scopes' }).optional(), + }), + ), + output: z.object({ + access_token: z.string({ description: 'Access Token' }), + }), }, async handler(ctx) { ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`); - const { repoUrl, projectId, name, accessLevel, scopes } = ctx.input; - const { token, integrationConfig } = getToken( - repoUrl, - ctx.input.token, - integrations, - ); + const { projectId, name, accessLevel, scopes } = ctx.input; + const { token, integrationConfig } = getToken(ctx.input, integrations); const response = await fetch( `${integrationConfig.config.baseUrl}/api/v4/projects/${projectId}/access_tokens`, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts index 35a53c87e2..3010bd33d2 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts @@ -18,11 +18,13 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Gitlab } from '@gitbeaker/node'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { DeployTokenScope } from '@gitbeaker/core/dist/types/templates/ResourceDeployTokens'; +import commonGitlabConfig from '../commonGitlabConfig'; import { getToken } from '../util'; import { InputError } from '@backstage/errors'; +import { z } from 'zod'; /** - * Creates a `gitlab:create-project-deploy-token` Scaffolder action. + * Creates a `gitlab:projectDeployToken:create` Scaffolder action. * * @param options - Templating configuration. * @public @@ -31,69 +33,30 @@ export const createGitlabProjectDeployTokenAction = (options: { integrations: ScmIntegrationRegistry; }) => { const { integrations } = options; - return createTemplateAction<{ - repoUrl: string; - projectId: string | number; - name: string; - username: string; - scopes: string[]; - token?: string; - }>({ + return createTemplateAction({ id: 'gitlab:projectDeployToken:create', schema: { - input: { - required: ['projectId', 'repoUrl'], - type: 'object', - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - projectId: { - title: 'Project ID', - type: ['string', 'number'], - }, - name: { - title: 'Deploy Token Name', - type: 'string', - }, - username: { - title: 'Deploy Token Username', - type: 'string', - }, - scopes: { - title: 'Scopes', - type: 'array', - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitLab', - }, - }, - }, - output: { - type: 'object', - properties: { - deploy_token: { - title: 'Deploy Token', - type: 'string', - }, - user: { - title: 'User', - type: 'string', - }, - }, - }, + input: commonGitlabConfig.and( + z.object({ + projectId: z.union([z.number(), z.string()], { + description: 'Project ID', + }), + name: z.string({ description: 'Deploy Token Name' }), + username: z + .string({ description: 'Deploy Token Username' }) + .optional(), + scopes: z.array(z.string(), { description: 'Scopes' }).optional(), + }), + ), + output: z.object({ + deploy_token: z.string({ description: 'Deploy Token' }), + user: z.string({ description: 'User' }), + }), }, async handler(ctx) { ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`); - const { repoUrl, projectId, name, username, scopes } = ctx.input; - const { token, integrationConfig } = getToken( - repoUrl, - ctx.input.token, - integrations, - ); + const { projectId, name, username, scopes } = ctx.input; + const { token, integrationConfig } = getToken(ctx.input, integrations); const api = new Gitlab({ host: integrationConfig.config.baseUrl, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts index 847bbc2057..a6043bea01 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts @@ -16,11 +16,13 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { getToken } from '../util'; import { Gitlab } from '@gitbeaker/node'; +import { getToken } from '../util'; +import commonGitlabConfig from '../commonGitlabConfig'; +import { z } from 'zod'; /** - * Creates a `gitlab:create-project-variable` Scaffolder action. + * Creates a `gitlab:projectVariable:create` Scaffolder action. * * @param options - Templating configuration. * @public @@ -29,96 +31,55 @@ export const createGitlabProjectVariableAction = (options: { integrations: ScmIntegrationRegistry; }) => { const { integrations } = options; - return createTemplateAction<{ - repoUrl: string; - projectId: string | number; - key: string; - value: string; - variableType: string; - variableProtected: boolean; - masked: boolean; - raw: boolean; - environmentScope: string; - token?: string; - }>({ + return createTemplateAction({ id: 'gitlab:projectVariable:create', schema: { - input: { - required: [ - 'repoUrl', - 'projectId', - 'key', - 'value', - 'variableType', - 'variableProtected', - 'masked', - 'raw', - 'environmentScope', - ], - type: 'object', - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - projectId: { - title: 'Project ID', - type: ['string', 'number'], - }, - key: { - title: - 'The key of a variable; must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed', - type: 'string', - }, - value: { - title: 'The value of a variable', - type: 'string', - }, - variableType: { - title: 'Variable Type (env_var or file)', - type: 'string', - }, - variableProtected: { - title: 'Whether the variable is protected. Default: false', - type: 'boolean', - }, - masked: { - title: 'Whether the variable is masked. Default: false', - type: 'boolean', - }, - raw: { - title: 'Whether the variable is expandable. Default: false', - type: 'boolean', - }, - environmentScope: { - title: 'The environment_scope of the variable. Default: *', - type: 'string', - }, - token: { - title: 'Authentication Token', - type: 'string', - description: 'The token to use for authorization to GitLab', - }, - }, - }, + input: commonGitlabConfig.and( + z.object({ + projectId: z.union([z.number(), z.string()], { + description: 'Project ID', + }), + key: z + .string({ + description: + 'The key of a variable; must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed', + }) + .regex(/^[A-Za-z0-9_]{1,255}$/), + value: z.string({ description: 'The value of a variable' }), + variableType: z.string({ + description: 'Variable Type (env_var or file)', + }), + variableProtected: z + .boolean({ description: 'Whether the variable is protected' }) + .default(false) + .optional(), + masked: z + .boolean({ description: 'Whether the variable is masked' }) + .default(false) + .optional(), + raw: z + .boolean({ description: 'Whether the variable is expandable' }) + .default(false) + .optional(), + environmentScope: z + .string({ description: 'The environment_scope of the variable' }) + .default('*') + .optional(), + }), + ), }, async handler(ctx) { const { - repoUrl, projectId, key, value, variableType, - variableProtected, - masked, - raw, - environmentScope, + variableProtected = false, + masked = false, + raw = false, + environmentScope = '*', } = ctx.input; - const { token, integrationConfig } = getToken( - repoUrl, - ctx.input.token, - integrations, - ); + const { token, integrationConfig } = getToken(ctx.input, integrations); const api = new Gitlab({ host: integrationConfig.config.baseUrl, diff --git a/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts new file mode 100644 index 0000000000..02724a3674 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/commonGitlabConfig.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; + +const commonGitlabConfig = z.object({ + repoUrl: z.string({ description: 'Repository Location' }), + token: z + .string({ description: 'The token to use for authorization to GitLab' }) + .optional(), +}); + +export default commonGitlabConfig; diff --git a/plugins/scaffolder-backend-module-gitlab/src/index.ts b/plugins/scaffolder-backend-module-gitlab/src/index.ts index 11209e19bd..4fd4dee35b 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/index.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/index.ts @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** - * A module for the scaffolder backend that lets you create gitlab project access tokens or deploy tokens + * A module for the scaffolder backend that lets you interact with gitlab * * @packageDocumentation */ - +export * from './actions/createGitlabGroupEnsureExistsAction'; export * from './actions/createGitlabProjectDeployTokenAction'; export * from './actions/createGitlabProjectAccessTokenAction'; export * from './actions/createGitlabProjectVariableAction'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index fc6667d6d3..13c95e48e8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -19,6 +19,8 @@ import { GitLabIntegration, ScmIntegrationRegistry, } from '@backstage/integration'; +import { z } from 'zod'; +import commonGitlabConfig from './commonGitlabConfig'; export const parseRepoHost = (repoUrl: string): string => { let parsed; @@ -33,11 +35,10 @@ export const parseRepoHost = (repoUrl: string): string => { }; export const getToken = ( - repoUrl: string, - inputToken: string | null | undefined, + config: z.infer, integrations: ScmIntegrationRegistry, ): { token: string; integrationConfig: GitLabIntegration } => { - const host = parseRepoHost(repoUrl); + const host = parseRepoHost(config.repoUrl); const integrationConfig = integrations.gitlab.byHost(host); if (!integrationConfig) { @@ -46,8 +47,8 @@ export const getToken = ( ); } - const token = inputToken || integrationConfig.config.token!; - const tokenType = inputToken ? 'oauthToken' : 'token'; + const token = config.token || integrationConfig.config.token!; + const tokenType = config.token ? 'oauthToken' : 'token'; if (tokenType === 'oauthToken') { throw new InputError(`OAuth Token is currently not supported`); diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 5fa59077db..d3648c33bf 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -57,6 +57,7 @@ "zod": "^3.21.4" }, "devDependencies": { + "@backstage/backend-openapi-utils": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", diff --git a/plugins/search-backend/src/schema/openapi.generated.ts b/plugins/search-backend/src/schema/openapi.generated.ts new file mode 100644 index 0000000000..b8f5f5b35e --- /dev/null +++ b/plugins/search-backend/src/schema/openapi.generated.ts @@ -0,0 +1,211 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +export default { + openapi: '3.0.3', + info: { + title: '@backstage/plugin-search-backend', + version: '1', + description: + 'The Backstage backend plugin that provides search functionality.', + license: { + name: 'Apache-2.0', + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', + }, + contact: {}, + }, + servers: [ + { + url: '/', + }, + ], + components: { + examples: {}, + headers: {}, + parameters: {}, + requestBodies: {}, + responses: {}, + schemas: { + JsonObject: { + type: 'object', + properties: {}, + additionalProperties: {}, + }, + }, + securitySchemes: { + JWT: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + paths: { + '/query': { + get: { + operationId: 'Query', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + results: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + description: 'The "type" of the given document.', + }, + document: { + type: 'object', + description: + 'The raw value of the document, as indexed.', + properties: { + title: { + type: 'string', + description: + 'The primary name of the document (e.g. name, title, identifier, etc).', + }, + text: { + type: 'string', + description: + 'Free-form text of the document (e.g. description, content, etc).', + }, + location: { + type: 'string', + description: + 'The relative or absolute URL of the document (target when a search result is clicked).', + }, + }, + }, + highlight: { + type: 'object', + description: + 'Optional result highlight. Useful for improving the search result\ndisplay/experience.', + }, + rank: { + type: 'integer', + description: + 'Optional result rank, where 1 is the first/top result returned. \nUseful for understanding search effectiveness in analytics.', + }, + }, + required: ['type', 'document'], + additionalProperties: false, + }, + }, + nextPageCursor: { + type: 'string', + }, + previousPageCursor: { + type: 'string', + }, + numberOfResults: { + type: 'integer', + }, + }, + required: ['results'], + }, + }, + }, + }, + '400': { + description: 'Bad request', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + message: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + name: 'term', + in: 'query', + required: false, + schema: { + type: 'string', + default: '', + }, + }, + { + name: 'filters', + in: 'query', + required: false, + style: 'deepObject', + explode: true, + schema: { + $ref: '#/components/schemas/JsonObject', + }, + }, + { + name: 'types', + in: 'query', + required: false, + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + { + name: 'pageCursor', + in: 'query', + required: false, + schema: { + type: 'string', + }, + }, + { + name: 'pageLimit', + in: 'query', + required: false, + schema: { + type: 'integer', + }, + }, + ], + }, + }, + }, +} as const; diff --git a/plugins/search-backend/src/schema/openapi.yaml b/plugins/search-backend/src/schema/openapi.yaml new file mode 100644 index 0000000000..e7f44c9f11 --- /dev/null +++ b/plugins/search-backend/src/schema/openapi.yaml @@ -0,0 +1,135 @@ +openapi: 3.0.3 + +info: + title: '@backstage/plugin-search-backend' + version: '1' + description: The Backstage backend plugin that provides search functionality. + license: + name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + contact: {} + +servers: + - url: / + +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + JsonObject: + type: object + properties: {} + # Free form object. + additionalProperties: {} + securitySchemes: + JWT: + type: http + scheme: bearer + bearerFormat: JWT +paths: + /query: + get: + operationId: Query + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + type: + type: string + description: The "type" of the given document. + document: + type: object + description: The raw value of the document, as indexed. + properties: + title: + type: string + description: The primary name of the document (e.g. name, title, identifier, etc). + text: + type: string + description: Free-form text of the document (e.g. description, content, etc). + location: + type: string + description: The relative or absolute URL of the document (target when a search result is clicked). + highlight: + type: object + description: |- + Optional result highlight. Useful for improving the search result + display/experience. + rank: + type: integer + description: |- + Optional result rank, where 1 is the first/top result returned. + Useful for understanding search effectiveness in analytics. + required: + - type + - document + additionalProperties: false + nextPageCursor: + type: string + previousPageCursor: + type: string + numberOfResults: + type: integer + required: + - results + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + message: + type: string + security: + - {} + - JWT: [] + parameters: + - name: term + in: query + required: false + schema: + type: string + default: '' + - name: filters + in: query + required: false + style: deepObject + explode: true + schema: + # JsonObject is used here instead of the full ZOD schema definition as + # resolution of recursive schemas isn't possible with the library we're using + # and it causes a performance hit when _trying_ to resolve them. + $ref: '#/components/schemas/JsonObject' + - name: types + in: query + required: false + schema: + type: array + items: + type: string + - name: pageCursor + in: query + required: false + schema: + type: string + - name: pageLimit + in: query + required: false + schema: + type: integer diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 10f47d180f..3643eec38a 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -19,7 +19,7 @@ import Router from 'express-promise-router'; import { Logger } from 'winston'; import { z } from 'zod'; import { errorHandler } from '@backstage/backend-common'; -import { ErrorResponseBody, InputError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; @@ -35,6 +35,8 @@ import { } from '@backstage/plugin-search-common'; import { SearchEngine } from '@backstage/plugin-search-common'; import { AuthorizedSearchEngine } from './AuthorizedSearchEngine'; +import type { ApiRouter } from '@backstage/backend-openapi-utils'; +import spec from '../schema/openapi.generated'; const jsonObjectSchema: z.ZodSchema = z.lazy(() => { const jsonValueSchema: z.ZodSchema = z.lazy(() => @@ -146,49 +148,43 @@ export async function createRouter( })), }); - const router = Router(); - router.get( - '/query', - async ( - req: express.Request, - res: express.Response, - ) => { - const parseResult = requestSchema.passthrough().safeParse(req.query); + const router = Router() as ApiRouter; + router.get('/query', async (req, res) => { + const parseResult = requestSchema.passthrough().safeParse(req.query); - if (!parseResult.success) { - throw new InputError(`Invalid query string: ${parseResult.error}`); + if (!parseResult.success) { + throw new InputError(`Invalid query string: ${parseResult.error}`); + } + + const query = parseResult.data; + + logger.info( + `Search request received: term="${query.term}", filters=${JSON.stringify( + query.filters, + )}, types=${query.types ? query.types.join(',') : ''}, pageCursor=${ + query.pageCursor ?? '' + }`, + ); + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + try { + const resultSet = await engine?.query(query, { token }); + + res.json(filterResultSet(toSearchResults(resultSet))); + } catch (error) { + if (error.name === 'MissingIndexError') { + // re-throw and let the default error handler middleware captures it and serializes it with the right response code on the standard form + throw error; } - const query = parseResult.data; - - logger.info( - `Search request received: term="${ - query.term - }", filters=${JSON.stringify(query.filters)}, types=${ - query.types ? query.types.join(',') : '' - }, pageCursor=${query.pageCursor ?? ''}`, + throw new Error( + `There was a problem performing the search query: ${error.message}`, ); - - const token = getBearerTokenFromAuthorizationHeader( - req.header('authorization'), - ); - - try { - const resultSet = await engine?.query(query, { token }); - - res.json(filterResultSet(toSearchResults(resultSet))); - } catch (error) { - if (error.name === 'MissingIndexError') { - // re-throw and let the default error handler middleware captures it and serializes it with the right response code on the standard form - throw error; - } - - throw new Error( - `There was a problem performing the search query: ${error.message}`, - ); - } - }, - ); + } + }); router.use(errorHandler()); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx index 78ef981ebc..6194ab8387 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx @@ -24,6 +24,7 @@ import { Select, MenuItem, FormLabel, + Typography, } from '@material-ui/core'; import { useSearch } from '../../context'; @@ -37,6 +38,16 @@ const useStyles = makeStyles({ label: { textTransform: 'capitalize', }, + checkboxWrapper: { + display: 'flex', + alignItems: 'center', + width: '100%', + }, + textWrapper: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, }); /** @@ -119,6 +130,11 @@ export const CheckboxFilter = (props: SearchFilterComponentProps) => { {values.map((value: string) => ( { checked={((filters[name] as string[]) ?? []).includes(value)} /> } - label={value} /> ))} @@ -197,7 +212,9 @@ export const SelectFilter = (props: SearchFilterComponentProps) => { {values.map((value: string) => ( - {value} + + {value} + ))} diff --git a/yarn.lock b/yarn.lock index 27ee93b8ca..fbc069a7e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -182,8 +182,8 @@ __metadata: linkType: hard "@apollo/server@npm:^4.0.0": - version: 4.7.0 - resolution: "@apollo/server@npm:4.7.0" + version: 4.7.1 + resolution: "@apollo/server@npm:4.7.1" dependencies: "@apollo/cache-control-types": ^1.0.2 "@apollo/server-gateway-interface": ^1.1.0 @@ -213,7 +213,7 @@ __metadata: whatwg-mimetype: ^3.0.0 peerDependencies: graphql: ^16.6.0 - checksum: 68f94a3859b99b931d8c6ae36f8684ec85eb7524a74e2a814b36ae3ae87773ff73562657021eaa2c2d382ec32bb6b1d9f9f7975fdc0a4509f24cddd71d33953e + checksum: 6c8bdd240a4c688641bfec0520375db0eb78d2495286f30f962d7b6e97d7db70f105c09364e56103e1b93543e5760621666655eaa8ae7218312ac54ca7200d4b languageName: node linkType: hard @@ -8314,6 +8314,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@gitbeaker/node": ^35.8.0 + zod: ^3.21.4 languageName: unknown linkType: soft @@ -8723,6 +8724,7 @@ __metadata: resolution: "@backstage/plugin-search-backend@workspace:plugins/search-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -14049,8 +14051,8 @@ __metadata: linkType: hard "@roadiehq/backstage-plugin-github-pull-requests@npm:^2.2.7": - version: 2.5.9 - resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.5.9" + version: 2.5.10 + resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.5.10" dependencies: "@backstage/catalog-model": ^1.3.0 "@backstage/core-components": ^0.13.0 @@ -14073,7 +14075,7 @@ __metadata: react: ^16.13.1 || ^17.0.0 react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 - checksum: 5a60f2c67c095a0fbf081b4097e0dfd4d6c22f59514e66800eba8460ef32ee43cf1eb7105a36a56f7b63d8ac44f5343f7e4ec66279ba2009945b828e6698e00e + checksum: 0f273ede63024635f850fc9df662941589f5f8848307507e3f6e376ea0bd9b7e17026e1182808d5c193be90ee9a2725c83a0850475547b364cd3d7cf35bf7b74 languageName: node linkType: hard @@ -31170,6 +31172,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + "minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -31456,12 +31465,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.1.23, nanoid@npm:^3.3.4": - version: 3.3.4 - resolution: "nanoid@npm:3.3.4" +"nanoid@npm:^3.1.23, nanoid@npm:^3.3.4, nanoid@npm:^3.3.6": + version: 3.3.6 + resolution: "nanoid@npm:3.3.6" bin: nanoid: bin/nanoid.cjs - checksum: 2fddd6dee994b7676f008d3ffa4ab16035a754f4bb586c61df5a22cf8c8c94017aadd360368f47d653829e0569a92b129979152ff97af23a558331e47e37cd9c + checksum: 7d0eda657002738aa5206107bd0580aead6c95c460ef1bdd0b1a87a9c7ae6277ac2e9b945306aaa5b32c6dcb7feaf462d0f552e7f8b5718abfc6ead5c94a71b3 languageName: node linkType: hard @@ -33910,13 +33919,13 @@ __metadata: linkType: hard "postcss@npm:^8.1.0, postcss@npm:^8.4.19": - version: 8.4.21 - resolution: "postcss@npm:8.4.21" + version: 8.4.23 + resolution: "postcss@npm:8.4.23" dependencies: - nanoid: ^3.3.4 + nanoid: ^3.3.6 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679 + checksum: 8bb9d1b2ea6e694f8987d4f18c94617971b2b8d141602725fedcc2222fdc413b776a6e1b969a25d627d7b2681ca5aabb56f59e727ef94072e1b6ac8412105a2f languageName: node linkType: hard @@ -38339,16 +38348,16 @@ __metadata: linkType: hard "tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": - version: 6.1.13 - resolution: "tar@npm:6.1.13" + version: 6.1.14 + resolution: "tar@npm:6.1.14" dependencies: chownr: ^2.0.0 fs-minipass: ^2.0.0 - minipass: ^4.0.0 + minipass: ^5.0.0 minizlib: ^2.1.1 mkdirp: ^1.0.3 yallist: ^4.0.0 - checksum: 8a278bed123aa9f53549b256a36b719e317c8b96fe86a63406f3c62887f78267cea9b22dc6f7007009738509800d4a4dccc444abd71d762287c90f35b002eb1c + checksum: a1be0815a9bdc97dfca7c6c2d71d1b836f8ba9314684e2c412832f0f59cc226d4c13da303d6bc30925e82f634cc793f40da79ae72f3e96fb87c23d0f4efd5207 languageName: node linkType: hard