Merge branch 'backstage:master' into feature/upgrade-aws-sdk-elasticsearch
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Ensure that entity cache state is only written to the database when actually changed
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': minor
|
||||
---
|
||||
|
||||
Add a new scaffolder action for gitlab to ensure a group exists
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-openapi-utils': patch
|
||||
---
|
||||
|
||||
Corrected resolution of parameter nested schema to use central schemas.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': patch
|
||||
---
|
||||
|
||||
Fix text-overflow UI issue for Lifecycle spans in SearchFilter checkbox labels.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': patch
|
||||
---
|
||||
|
||||
Added an OpenAPI 3.0 spec and enforced schema-first model on the router.
|
||||
@@ -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<Doc, Schema> extends infer R
|
||||
> = SchemaRef<Doc, Schema> extends infer R
|
||||
? R extends ImmutableSchemaObject
|
||||
? R extends JSONSchema7
|
||||
? FromSchema<R>
|
||||
@@ -549,16 +548,6 @@ type RequiredMap<
|
||||
[P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
type ResolveDocParameterSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Schema extends ImmutableParameterObject['schema'],
|
||||
> = Schema extends ImmutableReferenceObject
|
||||
? 'parameters' extends ComponentTypes<Doc>
|
||||
? ComponentRef<Doc, 'parameters', Schema>
|
||||
: never
|
||||
: Schema;
|
||||
|
||||
// @public (undocumented)
|
||||
type Response_2<
|
||||
Doc extends RequiredDoc,
|
||||
|
||||
@@ -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<Doc>
|
||||
? ComponentRef<Doc, 'parameters', Schema>
|
||||
: never
|
||||
: Schema;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ParameterSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Schema extends ImmutableParameterObject['schema'],
|
||||
> = ResolveDocParameterSchema<Doc, Schema> extends infer R
|
||||
> = SchemaRef<Doc, Schema> extends infer R
|
||||
? R extends ImmutableSchemaObject
|
||||
? R extends JSONSchema7
|
||||
? FromSchema<R>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
+140
@@ -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);
|
||||
});
|
||||
});
|
||||
+94
@@ -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<GroupSchema>; // 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
+21
-55
@@ -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`,
|
||||
|
||||
+22
-59
@@ -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,
|
||||
|
||||
+43
-82
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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<typeof commonGitlabConfig>,
|
||||
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`);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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<JsonObject> = z.lazy(() => {
|
||||
const jsonValueSchema: z.ZodSchema<JsonValue> = z.lazy(() =>
|
||||
@@ -146,49 +148,43 @@ export async function createRouter(
|
||||
})),
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.get(
|
||||
'/query',
|
||||
async (
|
||||
req: express.Request,
|
||||
res: express.Response<SearchResultSet | ErrorResponseBody>,
|
||||
) => {
|
||||
const parseResult = requestSchema.passthrough().safeParse(req.query);
|
||||
const router = Router() as ApiRouter<typeof spec>;
|
||||
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());
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
<FormControlLabel
|
||||
key={value}
|
||||
classes={{
|
||||
root: classes.checkboxWrapper,
|
||||
label: classes.textWrapper,
|
||||
}}
|
||||
label={value}
|
||||
control={
|
||||
<Checkbox
|
||||
color="primary"
|
||||
@@ -130,7 +146,6 @@ export const CheckboxFilter = (props: SearchFilterComponentProps) => {
|
||||
checked={((filters[name] as string[]) ?? []).includes(value)}
|
||||
/>
|
||||
}
|
||||
label={value}
|
||||
/>
|
||||
))}
|
||||
</FormControl>
|
||||
@@ -197,7 +212,9 @@ export const SelectFilter = (props: SearchFilterComponentProps) => {
|
||||
</MenuItem>
|
||||
{values.map((value: string) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{value}
|
||||
<Typography variant="inherit" noWrap>
|
||||
{value}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user