Merge branch 'master' of https://github.com/backstage/backstage into pr-draft
This commit is contained in:
@@ -15,29 +15,34 @@
|
||||
*/
|
||||
|
||||
import { ApiEntity, Entity } from '@backstage/catalog-model';
|
||||
import { Content, Header, Page } from '@backstage/core-components';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { CatalogEntityPage } from '@backstage/plugin-catalog';
|
||||
import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import {
|
||||
apiDocsConfigRef,
|
||||
apiDocsPlugin,
|
||||
ApiExplorerPage,
|
||||
defaultDefinitionWidgets,
|
||||
EntityApiDefinitionCard,
|
||||
} from '../src';
|
||||
import asyncapiApiEntity from './asyncapi-example-api.yaml';
|
||||
import graphqlApiEntity from './graphql-example-api.yaml';
|
||||
import invalidLanguageApiEntity from './invalid-language-example-api.yaml';
|
||||
import openapiApiEntity from './openapi-example-api.yaml';
|
||||
import otherApiEntity from './other-example-api.yaml';
|
||||
import { Content, Header, Page } from '@backstage/core-components';
|
||||
|
||||
const mockEntities = [
|
||||
openapiApiEntity,
|
||||
asyncapiApiEntity,
|
||||
graphqlApiEntity,
|
||||
invalidLanguageApiEntity,
|
||||
otherApiEntity,
|
||||
] as unknown as Entity[];
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(apiDocsPlugin)
|
||||
.registerApi({
|
||||
api: catalogApiRef,
|
||||
deps: {},
|
||||
@@ -65,6 +70,7 @@ createDevApp()
|
||||
};
|
||||
},
|
||||
})
|
||||
.addPage({ element: <CatalogEntityPage /> })
|
||||
.addPage({ title: 'API Explorer', element: <ApiExplorerPage /> })
|
||||
.addPage({
|
||||
title: 'OpenAPI',
|
||||
@@ -105,6 +111,19 @@ createDevApp()
|
||||
</Page>
|
||||
),
|
||||
})
|
||||
.addPage({
|
||||
title: 'Invalid Language',
|
||||
element: (
|
||||
<Page themeId="home">
|
||||
<Header title="Invalid Language" />
|
||||
<Content>
|
||||
<EntityProvider entity={invalidLanguageApiEntity as any as Entity}>
|
||||
<EntityApiDefinitionCard />
|
||||
</EntityProvider>
|
||||
</Content>
|
||||
</Page>
|
||||
),
|
||||
})
|
||||
.addPage({
|
||||
title: 'Other',
|
||||
element: (
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: API
|
||||
metadata:
|
||||
name: hello-world
|
||||
description: Hello World example for gRPC
|
||||
spec:
|
||||
type: my-invalid-language
|
||||
lifecycle: deprecated
|
||||
owner: team-c
|
||||
definition: |
|
||||
// Copyright 2015 gRPC 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.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.grpc.examples.helloworld";
|
||||
option java_outer_classname = "HelloWorldProto";
|
||||
option objc_class_prefix = "HLW";
|
||||
|
||||
package helloworld;
|
||||
|
||||
// The greeting service definition.
|
||||
service Greeter {
|
||||
// Sends a greeting
|
||||
rpc SayHello (HelloRequest) returns (HelloReply) {}
|
||||
}
|
||||
|
||||
// The request message containing the user's name.
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
// The response message containing the greetings
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
||||
@@ -37,7 +37,6 @@
|
||||
"@backstage/plugin-catalog": "^0.7.0",
|
||||
"@backstage/plugin-catalog-react": "^0.5.2",
|
||||
"@backstage/theme": "^0.2.10",
|
||||
"@material-icons/font": "^1.0.2",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
@@ -50,7 +49,7 @@
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^17.2.4",
|
||||
"swagger-ui-react": "^3.37.2"
|
||||
"swagger-ui-react": "^4.0.0-rc.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.7.15",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
@@ -32,7 +34,6 @@ export type AuthProviderFactoryOptions = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
catalogApi: CatalogApi;
|
||||
identityResolver?: ExperimentalIdentityResolver;
|
||||
};
|
||||
|
||||
// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
|
||||
@@ -74,6 +75,16 @@ export type AuthResponse<ProviderInfo> = {
|
||||
backstageIdentity?: BackstageIdentity;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type AwsAlbProviderOptions = {
|
||||
authHandler?: AuthHandler<AwsAlbResult>;
|
||||
signIn: {
|
||||
resolver: SignInResolver<AwsAlbResult>;
|
||||
};
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "BackstageIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -135,6 +146,13 @@ export const bitbucketUserIdSignInResolver: SignInResolver<BitbucketOAuthResult>
|
||||
// @public (undocumented)
|
||||
export const bitbucketUsernameSignInResolver: SignInResolver<BitbucketOAuthResult>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const createAwsAlbProvider: (
|
||||
options?: AwsAlbProviderOptions | undefined,
|
||||
) => AuthProviderFactory;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -526,9 +544,9 @@ export type WebMessageResponse =
|
||||
//
|
||||
// src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
|
||||
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/bitbucket/provider.d.ts:61:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/bitbucket/provider.d.ts:69:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
|
||||
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/aws-alb/provider.d.ts:85:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:99:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/providers/types.d.ts:121:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
|
||||
```
|
||||
|
||||
Vendored
+2
@@ -50,6 +50,8 @@ export interface Config {
|
||||
issuer: string;
|
||||
cert: string;
|
||||
privateKey?: string;
|
||||
authnContext?: string[];
|
||||
identifierFormat?: string;
|
||||
decryptionPvk?: string;
|
||||
signatureAlgorithm?: 'sha256' | 'sha512';
|
||||
digestAlgorithm?: string;
|
||||
|
||||
@@ -17,8 +17,14 @@ import { getVoidLogger } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import { JWT } from 'jose';
|
||||
|
||||
import { AwsAlbAuthProvider } from './provider';
|
||||
import { AuthResponse } from '../types';
|
||||
import {
|
||||
ALB_ACCESSTOKEN_HEADER,
|
||||
ALB_JWT_HEADER,
|
||||
AwsAlbAuthProvider,
|
||||
} from './provider';
|
||||
import { TokenIssuer } from '../../identity/types';
|
||||
import { CatalogIdentityClient } from '../../lib/catalog';
|
||||
import { makeProfileInfo } from '../../lib/passport';
|
||||
|
||||
const jwtMock = JWT as jest.Mocked<any>;
|
||||
|
||||
@@ -29,9 +35,21 @@ yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
|
||||
-----END PUBLIC KEY-----
|
||||
`;
|
||||
};
|
||||
const mockJwt =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94';
|
||||
const mockAccessToken = 'ACCESS_TOKEN';
|
||||
const mockClaims = {
|
||||
sub: '1234567890',
|
||||
name: 'User Name',
|
||||
family_name: 'Name',
|
||||
given_name: 'User',
|
||||
picture: 'PICTURE_URL',
|
||||
email: 'user.name@email.test',
|
||||
exp: 1632833763,
|
||||
iss: 'ISSUER_URL',
|
||||
};
|
||||
|
||||
jest.mock('jose');
|
||||
|
||||
jest.mock('cross-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: async () => {
|
||||
@@ -43,53 +61,48 @@ jest.mock('cross-fetch', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const identityResolutionCallbackMock = async (): Promise<AuthResponse<any>> => {
|
||||
return {
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
idToken: '',
|
||||
},
|
||||
profile: {
|
||||
displayName: 'Foo Bar',
|
||||
},
|
||||
providerInfo: {},
|
||||
};
|
||||
};
|
||||
|
||||
const identityResolutionCallbackRejectedMock = async (): Promise<
|
||||
AuthResponse<any>
|
||||
> => {
|
||||
throw new Error('failed');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('AwsALBAuthProvider', () => {
|
||||
const catalogApi = {
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
getOriginLocationByEntity: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
describe('AwsAlbAuthProvider', () => {
|
||||
const tokenIssuer: TokenIssuer = {
|
||||
listPublicKeys: jest.fn(),
|
||||
async issueToken(params) {
|
||||
return `token-for-${params.claims.sub}`;
|
||||
},
|
||||
};
|
||||
const catalogIdentityClient: CatalogIdentityClient = {
|
||||
findUser: jest.fn(),
|
||||
} as unknown as CatalogIdentityClient;
|
||||
|
||||
const mockRequest = {
|
||||
header: jest.fn(() => {
|
||||
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
const mockRequestWithoutJwt = {
|
||||
header: jest.fn(() => {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_JWT_HEADER) {
|
||||
return mockJwt;
|
||||
} else if (name === ALB_ACCESSTOKEN_HEADER) {
|
||||
return mockAccessToken;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
const mockRequestWithoutJwt = {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_ACCESSTOKEN_HEADER) {
|
||||
return mockAccessToken;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
const mockRequestWithoutAccessToken = {
|
||||
header: jest.fn(name => {
|
||||
if (name === ALB_JWT_HEADER) {
|
||||
return mockJwt;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
end: jest.fn(),
|
||||
header: () => jest.fn(),
|
||||
@@ -97,38 +110,78 @@ describe('AwsALBAuthProvider', () => {
|
||||
status: jest.fn(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
describe('should transform to type OAuthResponse', () => {
|
||||
describe('should transform to type AwsAlbResponse', () => {
|
||||
it('when JWT is valid and identity is resolved successfully', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foo',
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.verify.mockImplementationOnce(() => ({
|
||||
sub: 'foo',
|
||||
}));
|
||||
jwtMock.verify.mockReturnValueOnce(mockClaims);
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
idToken: '',
|
||||
id: 'user.name',
|
||||
token: 'TOKEN',
|
||||
},
|
||||
profile: {
|
||||
displayName: 'Foo Bar',
|
||||
displayName: 'User Name',
|
||||
email: 'user.name@email.test',
|
||||
picture: 'PICTURE_URL',
|
||||
},
|
||||
providerInfo: {
|
||||
accessToken: mockAccessToken,
|
||||
expiresInSeconds: mockClaims.exp,
|
||||
},
|
||||
providerInfo: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should fail when', () => {
|
||||
it('Access token is missing', async () => {
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequestWithoutAccessToken, mockResponse);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('JWT is missing', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foo',
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequestWithoutJwt, mockResponse);
|
||||
@@ -137,10 +190,18 @@ describe('AwsALBAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('JWT is invalid', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foo',
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.verify.mockImplementationOnce(() => {
|
||||
@@ -152,11 +213,19 @@ describe('AwsALBAuthProvider', () => {
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('issuer is invalid', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackMock,
|
||||
issuer: 'foobar',
|
||||
it('issuer is missing', async () => {
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.verify.mockReturnValueOnce({});
|
||||
@@ -165,14 +234,68 @@ describe('AwsALBAuthProvider', () => {
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('identity resolution callback rejects', async () => {
|
||||
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
|
||||
region: 'us-west-2',
|
||||
identityResolutionCallback: identityResolutionCallbackRejectedMock,
|
||||
issuer: 'foo',
|
||||
it('issuer is invalid', async () => {
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.verify.mockReturnValueOnce({});
|
||||
jwtMock.verify.mockReturnValueOnce({
|
||||
iss: 'INVALID_ISSUE_URL',
|
||||
});
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('SignInResolver rejects', async () => {
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
}),
|
||||
signInResolver: async () => {
|
||||
throw new Error();
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.verify.mockReturnValueOnce(mockClaims);
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
expect(mockResponse.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('AuthHandler rejects', async () => {
|
||||
const provider = new AwsAlbAuthProvider({
|
||||
region: 'eu-west-1',
|
||||
issuer: 'ISSUER_URL',
|
||||
logger: getVoidLogger(),
|
||||
catalogIdentityClient,
|
||||
tokenIssuer,
|
||||
authHandler: async () => {
|
||||
throw new Error();
|
||||
},
|
||||
signInResolver: async () => {
|
||||
return { id: 'user.name', token: 'TOKEN' };
|
||||
},
|
||||
});
|
||||
|
||||
jwtMock.verify.mockReturnValueOnce(mockClaims);
|
||||
|
||||
await provider.refresh(mockRequest, mockResponse);
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
AuthProviderFactoryOptions,
|
||||
AuthHandler,
|
||||
AuthProviderFactory,
|
||||
AuthProviderRouteHandlers,
|
||||
ExperimentalIdentityResolver,
|
||||
AuthResponse,
|
||||
SignInResolver,
|
||||
} from '../types';
|
||||
import express from 'express';
|
||||
import fetch from 'cross-fetch';
|
||||
@@ -25,66 +27,101 @@ import { KeyObject } from 'crypto';
|
||||
import { Logger } from 'winston';
|
||||
import NodeCache from 'node-cache';
|
||||
import { JWT } from 'jose';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { TokenIssuer } from '../../identity/types';
|
||||
import { CatalogIdentityClient } from '../../lib/catalog';
|
||||
import { Profile as PassportProfile } from 'passport';
|
||||
import { makeProfileInfo } from '../../lib/passport';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
|
||||
const ALB_JWT_HEADER = 'x-amzn-oidc-data';
|
||||
/**
|
||||
* A callback function that receives a verified JWT and returns a UserEntity
|
||||
* @param {payload} The verified JWT payload
|
||||
*/
|
||||
type AwsAlbAuthProviderOptions = {
|
||||
export const ALB_JWT_HEADER = 'x-amzn-oidc-data';
|
||||
export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken';
|
||||
|
||||
type Options = {
|
||||
region: string;
|
||||
issuer?: string;
|
||||
identityResolutionCallback: ExperimentalIdentityResolver;
|
||||
logger: Logger;
|
||||
authHandler: AuthHandler<AwsAlbResult>;
|
||||
signInResolver: SignInResolver<AwsAlbResult>;
|
||||
tokenIssuer: TokenIssuer;
|
||||
catalogIdentityClient: CatalogIdentityClient;
|
||||
};
|
||||
export const getJWTHeaders = (input: string) => {
|
||||
|
||||
export const getJWTHeaders = (input: string): AwsAlbHeaders => {
|
||||
const encoded = input.split('.')[0];
|
||||
return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
||||
};
|
||||
|
||||
export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
private logger: Logger;
|
||||
private readonly catalogClient: CatalogApi;
|
||||
private options: AwsAlbAuthProviderOptions;
|
||||
private readonly keyCache: NodeCache;
|
||||
export type AwsAlbHeaders = {
|
||||
alg: string;
|
||||
kid: string;
|
||||
signer: string;
|
||||
iss: string;
|
||||
client: string;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
constructor(
|
||||
logger: Logger,
|
||||
catalogClient: CatalogApi,
|
||||
options: AwsAlbAuthProviderOptions,
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.catalogClient = catalogClient;
|
||||
this.options = options;
|
||||
export type AwsAlbClaims = {
|
||||
sub: string;
|
||||
name: string;
|
||||
family_name: string;
|
||||
given_name: string;
|
||||
picture: string;
|
||||
email: string;
|
||||
exp: number;
|
||||
iss: string;
|
||||
};
|
||||
|
||||
export type AwsAlbResult = {
|
||||
fullProfile: PassportProfile;
|
||||
expiresInSeconds?: number;
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
export type AwsAlbProviderInfo = {
|
||||
/**
|
||||
* An access token issued for the signed in user.
|
||||
*/
|
||||
accessToken: string;
|
||||
/**
|
||||
* Expiry of the access token in seconds.
|
||||
*/
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
|
||||
export type AwsAlbResponse = AuthResponse<AwsAlbProviderInfo>;
|
||||
|
||||
export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly region: string;
|
||||
private readonly issuer?: string;
|
||||
private readonly tokenIssuer: TokenIssuer;
|
||||
private readonly catalogIdentityClient: CatalogIdentityClient;
|
||||
private readonly logger: Logger;
|
||||
private readonly keyCache: NodeCache;
|
||||
private readonly authHandler: AuthHandler<AwsAlbResult>;
|
||||
private readonly signInResolver: SignInResolver<AwsAlbResult>;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.region = options.region;
|
||||
this.issuer = options.issuer;
|
||||
this.authHandler = options.authHandler;
|
||||
this.signInResolver = options.signInResolver;
|
||||
this.tokenIssuer = options.tokenIssuer;
|
||||
this.catalogIdentityClient = options.catalogIdentityClient;
|
||||
this.logger = options.logger;
|
||||
this.keyCache = new NodeCache({ stdTTL: 3600 });
|
||||
}
|
||||
|
||||
frameHandler(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async refresh(req: express.Request, res: express.Response): Promise<void> {
|
||||
const jwt = req.header(ALB_JWT_HEADER);
|
||||
if (jwt !== undefined) {
|
||||
try {
|
||||
const headers = getJWTHeaders(jwt);
|
||||
const key = await this.getKey(headers.kid);
|
||||
const payload = JWT.verify(jwt, key);
|
||||
|
||||
if (this.options.issuer && headers.iss !== this.options.issuer) {
|
||||
throw new Error('issuer mismatch on JWT');
|
||||
}
|
||||
|
||||
const resolvedEntity = await this.options.identityResolutionCallback(
|
||||
payload,
|
||||
this.catalogClient,
|
||||
);
|
||||
res.json(resolvedEntity);
|
||||
} catch (e) {
|
||||
this.logger.error('exception occurred during JWT processing', e);
|
||||
res.status(401);
|
||||
res.end();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const result = await this.getResult(req);
|
||||
const response = await this.handleResult(result);
|
||||
res.json(response);
|
||||
} catch (e) {
|
||||
this.logger.error('Exception occurred during AWS ALB token refresh', e);
|
||||
res.status(401);
|
||||
res.end();
|
||||
}
|
||||
@@ -94,13 +131,85 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private async getResult(req: express.Request): Promise<AwsAlbResult> {
|
||||
const jwt = req.header(ALB_JWT_HEADER);
|
||||
const accessToken = req.header(ALB_ACCESSTOKEN_HEADER);
|
||||
|
||||
if (jwt === undefined) {
|
||||
throw new AuthenticationError(
|
||||
`Missing ALB OIDC header: ${ALB_JWT_HEADER}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (accessToken === undefined) {
|
||||
throw new AuthenticationError(
|
||||
`Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = getJWTHeaders(jwt);
|
||||
const key = await this.getKey(headers.kid);
|
||||
const claims = JWT.verify(jwt, key) as AwsAlbClaims;
|
||||
|
||||
if (this.issuer && claims.iss !== this.issuer) {
|
||||
throw new AuthenticationError('Issuer mismatch on JWT token');
|
||||
}
|
||||
|
||||
const fullProfile: PassportProfile = {
|
||||
provider: 'unknown',
|
||||
id: claims.sub,
|
||||
displayName: claims.name,
|
||||
username: claims.email.split('@')[0].toLowerCase(),
|
||||
name: {
|
||||
familyName: claims.family_name,
|
||||
givenName: claims.given_name,
|
||||
},
|
||||
emails: [{ value: claims.email.toLowerCase() }],
|
||||
photos: [{ value: claims.picture }],
|
||||
};
|
||||
|
||||
return {
|
||||
fullProfile,
|
||||
expiresInSeconds: claims.exp,
|
||||
accessToken,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(`Exception occurred during JWT processing: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleResult(result: AwsAlbResult): Promise<AwsAlbResponse> {
|
||||
const { profile } = await this.authHandler(result);
|
||||
const backstageIdentity = await this.signInResolver(
|
||||
{
|
||||
result,
|
||||
profile,
|
||||
},
|
||||
{
|
||||
tokenIssuer: this.tokenIssuer,
|
||||
catalogIdentityClient: this.catalogIdentityClient,
|
||||
logger: this.logger,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
providerInfo: {
|
||||
accessToken: result.accessToken,
|
||||
expiresInSeconds: result.expiresInSeconds,
|
||||
},
|
||||
backstageIdentity,
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
async getKey(keyId: string): Promise<KeyObject> {
|
||||
const optionalCacheKey = this.keyCache.get<KeyObject>(keyId);
|
||||
if (optionalCacheKey) {
|
||||
return crypto.createPublicKey(optionalCacheKey);
|
||||
}
|
||||
const keyText: string = await fetch(
|
||||
`https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`,
|
||||
`https://public-keys.auth.elb.${this.region}.amazonaws.com/${keyId}`,
|
||||
).then(response => response.text());
|
||||
const keyValue = crypto.createPublicKey(keyText);
|
||||
this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' }));
|
||||
@@ -108,26 +217,58 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export type AwsAlbProviderOptions = {};
|
||||
export type AwsAlbProviderOptions = {
|
||||
/**
|
||||
* The profile transformation function used to verify and convert the auth response
|
||||
* into the profile that will be presented to the user.
|
||||
*/
|
||||
authHandler?: AuthHandler<AwsAlbResult>;
|
||||
|
||||
export const createAwsAlbProvider = (_options?: AwsAlbProviderOptions) => {
|
||||
return ({
|
||||
logger,
|
||||
catalogApi,
|
||||
config,
|
||||
identityResolver,
|
||||
}: AuthProviderFactoryOptions) => {
|
||||
const region = config.getString('region');
|
||||
const issuer = config.getOptionalString('iss');
|
||||
if (identityResolver !== undefined) {
|
||||
return new AwsAlbAuthProvider(logger, catalogApi, {
|
||||
region,
|
||||
issuer,
|
||||
identityResolutionCallback: identityResolver,
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
'Identity resolver is required to use this authentication provider',
|
||||
);
|
||||
/**
|
||||
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
||||
*/
|
||||
signIn: {
|
||||
/**
|
||||
* Maps an auth result to a Backstage identity for the user.
|
||||
*/
|
||||
resolver: SignInResolver<AwsAlbResult>;
|
||||
};
|
||||
};
|
||||
|
||||
export const createAwsAlbProvider = (
|
||||
options?: AwsAlbProviderOptions,
|
||||
): AuthProviderFactory => {
|
||||
return ({ config, tokenIssuer, catalogApi, logger }) => {
|
||||
const region = config.getString('region');
|
||||
const issuer = config.getOptionalString('iss');
|
||||
|
||||
if (options?.signIn.resolver === undefined) {
|
||||
throw new Error(
|
||||
'SignInResolver is required to use this authentication provider',
|
||||
);
|
||||
}
|
||||
|
||||
const catalogIdentityClient = new CatalogIdentityClient({
|
||||
catalogApi,
|
||||
tokenIssuer,
|
||||
});
|
||||
|
||||
const authHandler: AuthHandler<AwsAlbResult> = options?.authHandler
|
||||
? options.authHandler
|
||||
: async ({ fullProfile }) => ({
|
||||
profile: makeProfileInfo(fullProfile),
|
||||
});
|
||||
|
||||
const signInResolver = options?.signIn.resolver;
|
||||
|
||||
return new AwsAlbAuthProvider({
|
||||
region,
|
||||
issuer,
|
||||
signInResolver,
|
||||
authHandler,
|
||||
tokenIssuer,
|
||||
catalogIdentityClient,
|
||||
logger,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export * from './microsoft';
|
||||
export * from './oauth2';
|
||||
export * from './okta';
|
||||
export * from './bitbucket';
|
||||
export * from './aws-alb';
|
||||
|
||||
export { factories as defaultAuthProviderFactories } from './factories';
|
||||
|
||||
|
||||
@@ -127,6 +127,8 @@ export const createSamlProvider = (
|
||||
issuer: config.getString('issuer'),
|
||||
cert: config.getString('cert'),
|
||||
privateCert: config.getOptionalString('privateKey'),
|
||||
authnContext: config.getOptionalStringArray('authnContext'),
|
||||
identifierFormat: config.getOptionalString('identifierFormat'),
|
||||
decryptionPvk: config.getOptionalString('decryptionPvk'),
|
||||
signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as
|
||||
| SignatureAlgorithm
|
||||
|
||||
@@ -119,19 +119,6 @@ export interface AuthProviderRouteHandlers {
|
||||
logout?(req: express.Request, res: express.Response): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - this will almost certainly break in a future release.
|
||||
*
|
||||
* Used to resolve an identity from auth information in some auth providers.
|
||||
*/
|
||||
export type ExperimentalIdentityResolver = (
|
||||
/**
|
||||
* An object containing information specific to the auth provider.
|
||||
*/
|
||||
payload: object,
|
||||
catalogApi: CatalogApi,
|
||||
) => Promise<AuthResponse<any>>;
|
||||
|
||||
export type AuthProviderFactoryOptions = {
|
||||
providerId: string;
|
||||
globalConfig: AuthProviderConfig;
|
||||
@@ -140,7 +127,6 @@ export type AuthProviderFactoryOptions = {
|
||||
tokenIssuer: TokenIssuer;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
catalogApi: CatalogApi;
|
||||
identityResolver?: ExperimentalIdentityResolver;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
|
||||
@@ -47,7 +47,7 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
export type RepoBuild = {
|
||||
id?: number;
|
||||
title: string;
|
||||
link: string;
|
||||
link?: string;
|
||||
status?: BuildStatus;
|
||||
result?: BuildResult;
|
||||
queueTime?: Date;
|
||||
|
||||
@@ -94,4 +94,100 @@ describe('AzureDevOpsApi', () => {
|
||||
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
|
||||
});
|
||||
});
|
||||
|
||||
describe('repoBuildFromBuild with undefined status', () => {
|
||||
it('should return BuildStatus of None for status', () => {
|
||||
const inputLinks: any = {
|
||||
web: {
|
||||
href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
|
||||
},
|
||||
};
|
||||
|
||||
const inputBuild: Build = {
|
||||
id: 1,
|
||||
buildNumber: 'Build-1',
|
||||
status: undefined,
|
||||
result: BuildResult.Succeeded,
|
||||
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
|
||||
sourceBranch: 'refs/heads/develop',
|
||||
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
|
||||
definition: undefined,
|
||||
_links: inputLinks,
|
||||
};
|
||||
|
||||
const outputRepoBuild: RepoBuild = {
|
||||
id: 1,
|
||||
title: 'Build-1',
|
||||
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
|
||||
status: BuildStatus.None,
|
||||
result: BuildResult.Succeeded,
|
||||
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
|
||||
source: 'refs/heads/develop (f4f78b31)',
|
||||
};
|
||||
|
||||
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
|
||||
});
|
||||
});
|
||||
|
||||
describe('repoBuildFromBuild with undefined result', () => {
|
||||
it('should return BuildResult of None for result', () => {
|
||||
const inputLinks: any = {
|
||||
web: {
|
||||
href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
|
||||
},
|
||||
};
|
||||
|
||||
const inputBuild: Build = {
|
||||
id: 1,
|
||||
buildNumber: 'Build-1',
|
||||
status: BuildStatus.InProgress,
|
||||
result: undefined,
|
||||
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
|
||||
sourceBranch: 'refs/heads/develop',
|
||||
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
|
||||
definition: undefined,
|
||||
_links: inputLinks,
|
||||
};
|
||||
|
||||
const outputRepoBuild: RepoBuild = {
|
||||
id: 1,
|
||||
title: 'Build-1',
|
||||
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
|
||||
status: BuildStatus.InProgress,
|
||||
result: BuildResult.None,
|
||||
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
|
||||
source: 'refs/heads/develop (f4f78b31)',
|
||||
};
|
||||
|
||||
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
|
||||
});
|
||||
});
|
||||
|
||||
describe('repoBuildFromBuild with undefined link', () => {
|
||||
it('should return empty string for link', () => {
|
||||
const inputBuild: Build = {
|
||||
id: 1,
|
||||
buildNumber: 'Build-1',
|
||||
status: BuildStatus.InProgress,
|
||||
result: undefined,
|
||||
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
|
||||
sourceBranch: 'refs/heads/develop',
|
||||
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
|
||||
definition: undefined,
|
||||
_links: undefined,
|
||||
};
|
||||
|
||||
const outputRepoBuild: RepoBuild = {
|
||||
id: 1,
|
||||
title: 'Build-1',
|
||||
link: '',
|
||||
status: BuildStatus.InProgress,
|
||||
result: BuildResult.None,
|
||||
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
|
||||
source: 'refs/heads/develop (f4f78b31)',
|
||||
};
|
||||
|
||||
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
import { Logger } from 'winston';
|
||||
import { WebApi } from 'azure-devops-node-api';
|
||||
import { RepoBuild } from './types';
|
||||
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
|
||||
import {
|
||||
Build,
|
||||
BuildResult,
|
||||
BuildStatus,
|
||||
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
|
||||
|
||||
export class AzureDevOpsApi {
|
||||
constructor(
|
||||
@@ -97,9 +101,9 @@ export function repoBuildFromBuild(build: Build) {
|
||||
title: [build.definition?.name, build.buildNumber]
|
||||
.filter(Boolean)
|
||||
.join(' - '),
|
||||
link: build._links?.web.href,
|
||||
status: build.status,
|
||||
result: build.result,
|
||||
link: build._links?.web.href ? build._links?.web.href : '',
|
||||
status: build.status ? build.status : BuildStatus.None,
|
||||
result: build.result ? build.result : BuildResult.None,
|
||||
queueTime: build.queueTime,
|
||||
source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`,
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
export type RepoBuild = {
|
||||
id?: number;
|
||||
title: string;
|
||||
link: string;
|
||||
link?: string;
|
||||
status?: BuildStatus;
|
||||
result?: BuildResult;
|
||||
queueTime?: Date;
|
||||
|
||||
@@ -435,7 +435,7 @@ export class CodeOwnersProcessor implements CatalogProcessor {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CommonDatabase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export class CommonDatabase implements Database {
|
||||
constructor(database: Knex, logger: Logger_2);
|
||||
// (undocumented)
|
||||
@@ -499,7 +499,7 @@ export class CommonDatabase implements Database {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CreateDatabaseOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type CreateDatabaseOptions = {
|
||||
logger: Logger_2;
|
||||
};
|
||||
@@ -528,7 +528,7 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Database" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type Database = {
|
||||
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
||||
addEntities(
|
||||
@@ -596,7 +596,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DatabaseLocationsCatalog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export class DatabaseLocationsCatalog implements LocationsCatalog {
|
||||
constructor(database: Database);
|
||||
// (undocumented)
|
||||
@@ -624,7 +624,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DatabaseLocationUpdateLogEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DatabaseLocationUpdateLogEvent = {
|
||||
id: string;
|
||||
status: DatabaseLocationUpdateLogStatus;
|
||||
@@ -646,7 +646,7 @@ export enum DatabaseLocationUpdateLogStatus {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DatabaseManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export class DatabaseManager {
|
||||
// (undocumented)
|
||||
static createDatabase(
|
||||
@@ -665,7 +665,7 @@ export class DatabaseManager {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DbEntitiesRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DbEntitiesRequest = {
|
||||
filter?: EntityFilter;
|
||||
pagination?: EntityPagination;
|
||||
@@ -673,7 +673,7 @@ export type DbEntitiesRequest = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DbEntitiesResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DbEntitiesResponse = {
|
||||
entities: DbEntityResponse[];
|
||||
pageInfo: DbPageInfo;
|
||||
@@ -681,7 +681,7 @@ export type DbEntitiesResponse = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DbEntityRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DbEntityRequest = {
|
||||
locationId?: string;
|
||||
entity: Entity;
|
||||
@@ -690,7 +690,7 @@ export type DbEntityRequest = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DbEntityResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DbEntityResponse = {
|
||||
locationId?: string;
|
||||
entity: Entity;
|
||||
@@ -698,7 +698,7 @@ export type DbEntityResponse = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DbLocationsRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DbLocationsRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -707,7 +707,7 @@ export type DbLocationsRow = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DbLocationsRowWithStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DbLocationsRowWithStatus = DbLocationsRow & {
|
||||
status: string | null;
|
||||
timestamp: string | null;
|
||||
@@ -716,7 +716,7 @@ export type DbLocationsRowWithStatus = DbLocationsRow & {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DbPageInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type DbPageInfo =
|
||||
| {
|
||||
hasNextPage: false;
|
||||
@@ -812,7 +812,7 @@ export function durationText(startTimestamp: [number, number]): string;
|
||||
export type EntitiesCatalog = {
|
||||
entities(request?: EntitiesRequest): Promise<EntitiesResponse>;
|
||||
removeEntityByUid(uid: string): Promise<void>;
|
||||
batchAddOrUpdateEntities(
|
||||
batchAddOrUpdateEntities?(
|
||||
requests: EntityUpsertRequest[],
|
||||
options?: {
|
||||
locationId?: string;
|
||||
@@ -943,7 +943,7 @@ export type EntityProviderMutation =
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityUpsertRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type EntityUpsertRequest = {
|
||||
entity: Entity;
|
||||
relations: EntityRelationSpec[];
|
||||
@@ -951,7 +951,7 @@ export type EntityUpsertRequest = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityUpsertResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type EntityUpsertResponse = {
|
||||
entityId: string;
|
||||
entity?: Entity;
|
||||
@@ -1157,7 +1157,7 @@ export class LocationReaders implements LocationReader {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LocationResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type LocationResponse = {
|
||||
data: Location_2;
|
||||
currentStatus: LocationUpdateStatus;
|
||||
@@ -1165,7 +1165,7 @@ export type LocationResponse = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LocationsCatalog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type LocationsCatalog = {
|
||||
addLocation(location: Location_2): Promise<Location_2>;
|
||||
removeLocation(id: string): Promise<void>;
|
||||
@@ -1220,7 +1220,7 @@ export interface LocationStore {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LocationUpdateLogEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type LocationUpdateLogEvent = {
|
||||
id: string;
|
||||
status: 'fail' | 'success';
|
||||
@@ -1232,7 +1232,7 @@ export type LocationUpdateLogEvent = {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LocationUpdateStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export type LocationUpdateStatus = {
|
||||
timestamp: string | null;
|
||||
status: string | null;
|
||||
@@ -1487,7 +1487,7 @@ export class StaticLocationProcessor implements StaticLocationProcessor {
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Transaction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type Transaction = {
|
||||
rollback(): Promise<unknown>;
|
||||
};
|
||||
@@ -1512,20 +1512,20 @@ export class UrlReaderProcessor implements CatalogProcessor {
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/catalog/types.d.ts:52:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
|
||||
// src/catalog/types.d.ts:53:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
|
||||
// src/catalog/types.d.ts:54:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
|
||||
// src/database/types.d.ts:125:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:131:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:132:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:146:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:147:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:148:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:150:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:163:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:164:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/database/types.d.ts:165:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/catalog/types.d.ts:97:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
|
||||
// src/catalog/types.d.ts:98:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
|
||||
// src/catalog/types.d.ts:99:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
|
||||
// src/ingestion/processors/GithubMultiOrgReaderProcessor.d.ts:23:9 - (ae-forgotten-export) The symbol "GithubMultiOrgConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/ingestion/types.d.ts:8:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:98:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:104:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:105:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:119:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:120:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:121:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:123:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:136:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:137:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/database/types.d.ts:138:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// src/legacy/ingestion/types.d.ts:19:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
```
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog';
|
||||
export type {
|
||||
EntitiesCatalog,
|
||||
EntitiesRequest,
|
||||
@@ -22,9 +21,8 @@ export type {
|
||||
EntityAncestryResponse,
|
||||
EntityUpsertRequest,
|
||||
EntityUpsertResponse,
|
||||
LocationResponse,
|
||||
LocationsCatalog,
|
||||
LocationUpdateLogEvent,
|
||||
LocationUpdateStatus,
|
||||
PageInfo,
|
||||
EntitiesSearchFilter,
|
||||
EntityFilter,
|
||||
EntityPagination,
|
||||
} from './types';
|
||||
|
||||
@@ -14,12 +14,52 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityRelationSpec, Location } from '@backstage/catalog-model';
|
||||
import { EntityFilter, EntityPagination } from '../database/types';
|
||||
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
|
||||
|
||||
//
|
||||
// Entities
|
||||
//
|
||||
/**
|
||||
* A filter expression for entities.
|
||||
*
|
||||
* Any (at least one) of the outer sets must match, within which all of the
|
||||
* individual filters must match.
|
||||
*/
|
||||
export type EntityFilter = {
|
||||
anyOf: { allOf: EntitiesSearchFilter[] }[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A pagination rule for entities.
|
||||
*/
|
||||
export type EntityPagination = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
after?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches rows in the entities_search table.
|
||||
*/
|
||||
export type EntitiesSearchFilter = {
|
||||
/**
|
||||
* The key to match on.
|
||||
*
|
||||
* Matches are always case insensitive.
|
||||
*/
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* Match on plain equality of values.
|
||||
*
|
||||
* If undefined, this factor is not taken into account. Otherwise, match on
|
||||
* values that are equal to any of the given array items. Matches are always
|
||||
* case insensitive.
|
||||
*/
|
||||
matchValueIn?: string[];
|
||||
|
||||
/**
|
||||
* Match on existence of key.
|
||||
*/
|
||||
matchValueExists?: boolean;
|
||||
};
|
||||
|
||||
export type PageInfo =
|
||||
| {
|
||||
@@ -41,11 +81,13 @@ export type EntitiesResponse = {
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type EntityUpsertRequest = {
|
||||
entity: Entity;
|
||||
relations: EntityRelationSpec[];
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type EntityUpsertResponse = {
|
||||
entityId: string;
|
||||
entity?: Entity;
|
||||
@@ -79,12 +121,14 @@ export type EntitiesCatalog = {
|
||||
/**
|
||||
* Writes a number of entities efficiently to storage.
|
||||
*
|
||||
* @deprecated This method was part of the legacy catalog engine an will be removed.
|
||||
*
|
||||
* @param requests - The entities and their relations
|
||||
* @param options.locationId - The location that they all belong to (default none)
|
||||
* @param options.dryRun - Whether to throw away the results (default false)
|
||||
* @param options.outputEntities - Whether to return the resulting entities (default false)
|
||||
*/
|
||||
batchAddOrUpdateEntities(
|
||||
batchAddOrUpdateEntities?(
|
||||
requests: EntityUpsertRequest[],
|
||||
options?: {
|
||||
locationId?: string;
|
||||
@@ -100,44 +144,3 @@ export type EntitiesCatalog = {
|
||||
*/
|
||||
entityAncestry(entityRef: string): Promise<EntityAncestryResponse>;
|
||||
};
|
||||
|
||||
//
|
||||
// Locations
|
||||
//
|
||||
|
||||
export type LocationUpdateStatus = {
|
||||
timestamp: string | null;
|
||||
status: string | null;
|
||||
message: string | null;
|
||||
};
|
||||
|
||||
export type LocationUpdateLogEvent = {
|
||||
id: string;
|
||||
status: 'fail' | 'success';
|
||||
location_id: string;
|
||||
entity_name: string;
|
||||
created_at?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type LocationResponse = {
|
||||
data: Location;
|
||||
currentStatus: LocationUpdateStatus;
|
||||
};
|
||||
|
||||
export type LocationsCatalog = {
|
||||
addLocation(location: Location): Promise<Location>;
|
||||
removeLocation(id: string): Promise<void>;
|
||||
locations(): Promise<LocationResponse[]>;
|
||||
location(id: string): Promise<LocationResponse>;
|
||||
locationHistory(id: string): Promise<LocationUpdateLogEvent[]>;
|
||||
logUpdateSuccess(
|
||||
locationId: string,
|
||||
entityName?: string | string[],
|
||||
): Promise<void>;
|
||||
logUpdateFailure(
|
||||
locationId: string,
|
||||
error?: Error,
|
||||
entityName?: string,
|
||||
): Promise<void>;
|
||||
};
|
||||
|
||||
+3
-3
@@ -21,14 +21,14 @@ import { Knex } from 'knex';
|
||||
import * as uuid from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { DateTime } from 'luxon';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
import { applyDatabaseMigrations } from './migrations';
|
||||
import { DefaultProcessingDatabase } from './DefaultProcessingDatabase';
|
||||
import {
|
||||
DbRefreshStateReferencesRow,
|
||||
DbRefreshStateRow,
|
||||
DbRelationsRow,
|
||||
} from './tables';
|
||||
import { createRandomRefreshInterval } from '../refresh';
|
||||
import { createRandomRefreshInterval } from '../processing/refresh';
|
||||
import { timestampToDateTime } from './conversion';
|
||||
import { generateStableHash } from './util';
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('Default Processing Database', () => {
|
||||
logger: Logger = defaultLogger,
|
||||
) {
|
||||
const knex = await databases.init(databaseId);
|
||||
await DatabaseManager.createDatabase(knex);
|
||||
await applyDatabaseMigrations(knex);
|
||||
return {
|
||||
knex,
|
||||
db: new DefaultProcessingDatabase({
|
||||
+11
-10
@@ -20,17 +20,8 @@ import { Knex } from 'knex';
|
||||
import lodash from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { Logger } from 'winston';
|
||||
import { Transaction } from '../../database';
|
||||
import { DeferredEntity } from '../processing/types';
|
||||
import { RefreshIntervalFunction } from '../refresh';
|
||||
import { rethrowError, timestampToDateTime } from './conversion';
|
||||
import { initDatabaseMetrics } from './metrics';
|
||||
import {
|
||||
DbRefreshStateReferencesRow,
|
||||
DbRefreshStateRow,
|
||||
DbRelationsRow,
|
||||
} from './tables';
|
||||
import {
|
||||
Transaction,
|
||||
GetProcessableEntitiesResult,
|
||||
ProcessingDatabase,
|
||||
RefreshStateItem,
|
||||
@@ -41,6 +32,16 @@ import {
|
||||
ListAncestorsResult,
|
||||
UpdateEntityCacheOptions,
|
||||
} from './types';
|
||||
import { DeferredEntity } from '../processing/types';
|
||||
import { RefreshIntervalFunction } from '../processing/refresh';
|
||||
import { rethrowError, timestampToDateTime } from './conversion';
|
||||
import { initDatabaseMetrics } from './metrics';
|
||||
import {
|
||||
DbRefreshStateReferencesRow,
|
||||
DbRefreshStateRow,
|
||||
DbRelationsRow,
|
||||
} from './tables';
|
||||
|
||||
import { generateStableHash } from './util';
|
||||
|
||||
// The number of items that are sent per batch to the database layer, when
|
||||
+2
-3
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import { DbLocationsRow } from '../../database/types';
|
||||
import { createGaugeMetric } from '../metrics';
|
||||
import { DbRefreshStateRow, DbRelationsRow } from './tables';
|
||||
import { createGaugeMetric } from '../util/metrics';
|
||||
import { DbRefreshStateRow, DbRelationsRow, DbLocationsRow } from './tables';
|
||||
|
||||
export function initDatabaseMetrics(knex: Knex) {
|
||||
const seen = new Set<string>();
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2020 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 { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function applyDatabaseMigrations(knex: Knex): Promise<void> {
|
||||
const migrationsDir = resolvePackagePath(
|
||||
'@backstage/plugin-catalog-backend',
|
||||
'migrations',
|
||||
);
|
||||
|
||||
await knex.migrate.latest({
|
||||
directory: migrationsDir,
|
||||
});
|
||||
}
|
||||
+9
@@ -14,6 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type DbPageInfo =
|
||||
| {
|
||||
hasNextPage: false;
|
||||
}
|
||||
| {
|
||||
hasNextPage: true;
|
||||
endCursor: string;
|
||||
};
|
||||
|
||||
export type DbLocationsRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,135 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Entity,
|
||||
EntityName,
|
||||
EntityRelationSpec,
|
||||
Location,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
export type DbEntitiesRow = {
|
||||
id: string;
|
||||
location_id: string | null;
|
||||
etag: string;
|
||||
generation: number;
|
||||
full_name: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type DbEntityRequest = {
|
||||
locationId?: string;
|
||||
entity: Entity;
|
||||
relations: EntityRelationSpec[];
|
||||
};
|
||||
|
||||
export type DbEntitiesRequest = {
|
||||
filter?: EntityFilter;
|
||||
pagination?: EntityPagination;
|
||||
};
|
||||
|
||||
export type DbEntitiesResponse = {
|
||||
entities: DbEntityResponse[];
|
||||
pageInfo: DbPageInfo;
|
||||
};
|
||||
|
||||
export type DbPageInfo =
|
||||
| {
|
||||
hasNextPage: false;
|
||||
}
|
||||
| {
|
||||
hasNextPage: true;
|
||||
endCursor: string;
|
||||
};
|
||||
|
||||
export type DbEntityResponse = {
|
||||
locationId?: string;
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
export type DbEntitiesRelationsRow = {
|
||||
originating_entity_id: string;
|
||||
source_full_name: string;
|
||||
type: string;
|
||||
target_full_name: string;
|
||||
};
|
||||
|
||||
export type DbEntitiesSearchRow = {
|
||||
entity_id: string;
|
||||
key: string;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export type DbLocationsRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type DbLocationsRowWithStatus = DbLocationsRow & {
|
||||
status: string | null;
|
||||
timestamp: string | null;
|
||||
message: string | null;
|
||||
};
|
||||
|
||||
export enum DatabaseLocationUpdateLogStatus {
|
||||
FAIL = 'fail',
|
||||
SUCCESS = 'success',
|
||||
}
|
||||
|
||||
export type DatabaseLocationUpdateLogEvent = {
|
||||
id: string;
|
||||
status: DatabaseLocationUpdateLogStatus;
|
||||
location_id: string;
|
||||
entity_name: string;
|
||||
created_at?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches rows in the entities_search table.
|
||||
*/
|
||||
export type EntitiesSearchFilter = {
|
||||
/**
|
||||
* The key to match on.
|
||||
*
|
||||
* Matches are always case insensitive.
|
||||
*/
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* Match on plain equality of values.
|
||||
*
|
||||
* If undefined, this factor is not taken into account. Otherwise, match on
|
||||
* values that are equal to any of the given array items. Matches are always
|
||||
* case insensitive.
|
||||
*/
|
||||
matchValueIn?: string[];
|
||||
|
||||
/**
|
||||
* Match on existence of key.
|
||||
*/
|
||||
matchValueExists?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A filter expression for entities.
|
||||
*
|
||||
* Any (at least one) of the outer sets must match, within which all of the
|
||||
* individual filters must match.
|
||||
*/
|
||||
export type EntityFilter = {
|
||||
anyOf: { allOf: EntitiesSearchFilter[] }[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A pagination rule for entities.
|
||||
*/
|
||||
export type EntityPagination = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
after?: string;
|
||||
};
|
||||
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { DateTime } from 'luxon';
|
||||
import { DeferredEntity } from '../processing/types';
|
||||
|
||||
/**
|
||||
* An abstraction for transactions of the underlying database technology.
|
||||
@@ -151,103 +26,126 @@ export type Transaction = {
|
||||
rollback(): Promise<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* An abstraction on top of the underlying database, wrapping the basic CRUD
|
||||
* needs.
|
||||
*/
|
||||
export type Database = {
|
||||
/**
|
||||
* Runs a transaction.
|
||||
*
|
||||
* The callback is expected to make calls back into this class. When it
|
||||
* completes, the transaction is closed.
|
||||
*
|
||||
* @param fn The callback that implements the transaction
|
||||
*/
|
||||
export type AddUnprocessedEntitiesResult = {};
|
||||
|
||||
export type UpdateProcessedEntityOptions = {
|
||||
id: string;
|
||||
processedEntity: Entity;
|
||||
resultHash: string;
|
||||
errors?: string;
|
||||
relations: EntityRelationSpec[];
|
||||
deferredEntities: DeferredEntity[];
|
||||
locationKey?: string;
|
||||
};
|
||||
|
||||
export type UpdateEntityCacheOptions = {
|
||||
id: string;
|
||||
state?: JsonObject;
|
||||
};
|
||||
|
||||
export type UpdateProcessedEntityErrorsOptions = {
|
||||
id: string;
|
||||
errors?: string;
|
||||
resultHash: string;
|
||||
};
|
||||
|
||||
export type RefreshStateItem = {
|
||||
id: string;
|
||||
entityRef: string;
|
||||
unprocessedEntity: Entity;
|
||||
processedEntity?: Entity;
|
||||
resultHash: string;
|
||||
nextUpdateAt: DateTime;
|
||||
lastDiscoveryAt: DateTime; // remove?
|
||||
state?: JsonObject;
|
||||
errors?: string;
|
||||
locationKey?: string;
|
||||
};
|
||||
|
||||
export type GetProcessableEntitiesResult = {
|
||||
items: RefreshStateItem[];
|
||||
};
|
||||
|
||||
export type ReplaceUnprocessedEntitiesOptions =
|
||||
| {
|
||||
sourceKey: string;
|
||||
items: DeferredEntity[];
|
||||
type: 'full';
|
||||
}
|
||||
| {
|
||||
sourceKey: string;
|
||||
added: DeferredEntity[];
|
||||
removed: DeferredEntity[];
|
||||
type: 'delta';
|
||||
};
|
||||
|
||||
export type RefreshOptions = {
|
||||
entityRef: string;
|
||||
};
|
||||
|
||||
export type ListAncestorsOptions = {
|
||||
entityRef: string;
|
||||
};
|
||||
|
||||
export type ListAncestorsResult = {
|
||||
entityRefs: string[];
|
||||
};
|
||||
|
||||
export interface ProcessingDatabase {
|
||||
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
||||
|
||||
/**
|
||||
* Adds a set of new entities to the catalog.
|
||||
*
|
||||
* @param tx An ongoing transaction
|
||||
* @param request The entities being added
|
||||
* Add unprocessed entities to the front of the processing queue using a mutation.
|
||||
*/
|
||||
addEntities(
|
||||
tx: Transaction,
|
||||
request: DbEntityRequest[],
|
||||
): Promise<DbEntityResponse[]>;
|
||||
|
||||
/**
|
||||
* Updates an existing entity in the catalog.
|
||||
*
|
||||
* The given entity must contain an uid to identify an already stored entity
|
||||
* in the catalog. If it is missing or if no matching entity is found, the
|
||||
* operation fails.
|
||||
*
|
||||
* If matchingEtag or matchingGeneration are given, they are taken into
|
||||
* account. Attempts to update a matching entity, but where the etag and/or
|
||||
* generation are not equal to the passed values, will fail.
|
||||
*
|
||||
* @param tx An ongoing transaction
|
||||
* @param request The entity being updated
|
||||
* @param matchingEtag If specified, reject with ConflictError if not
|
||||
* matching the entry in the database
|
||||
* @param matchingGeneration If specified, reject with ConflictError if not
|
||||
* matching the entry in the database
|
||||
* @returns The updated entity
|
||||
*/
|
||||
updateEntity(
|
||||
tx: Transaction,
|
||||
request: DbEntityRequest,
|
||||
matchingEtag?: string,
|
||||
matchingGeneration?: number,
|
||||
): Promise<DbEntityResponse>;
|
||||
|
||||
entities(
|
||||
tx: Transaction,
|
||||
request?: DbEntitiesRequest,
|
||||
): Promise<DbEntitiesResponse>;
|
||||
|
||||
entityByName(
|
||||
tx: Transaction,
|
||||
name: EntityName,
|
||||
): Promise<DbEntityResponse | undefined>;
|
||||
|
||||
entityByUid(
|
||||
tx: Transaction,
|
||||
uid: string,
|
||||
): Promise<DbEntityResponse | undefined>;
|
||||
|
||||
removeEntityByUid(tx: Transaction, uid: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove current relations for the entity and replace them with the new
|
||||
* relations array.
|
||||
*
|
||||
* @param tx An ongoing transaction
|
||||
* @param entityUid The entity uid
|
||||
* @param relations The relationships to be set
|
||||
*/
|
||||
setRelations(
|
||||
tx: Transaction,
|
||||
entityUid: string,
|
||||
relations: EntityRelationSpec[],
|
||||
replaceUnprocessedEntities(
|
||||
txOpaque: Transaction,
|
||||
options: ReplaceUnprocessedEntitiesOptions,
|
||||
): Promise<void>;
|
||||
|
||||
addLocation(tx: Transaction, location: Location): Promise<DbLocationsRow>;
|
||||
getProcessableEntities(
|
||||
txOpaque: Transaction,
|
||||
request: { processBatchSize: number },
|
||||
): Promise<GetProcessableEntitiesResult>;
|
||||
|
||||
removeLocation(tx: Transaction, id: string): Promise<void>;
|
||||
|
||||
location(id: string): Promise<DbLocationsRowWithStatus>;
|
||||
|
||||
locations(): Promise<DbLocationsRowWithStatus[]>;
|
||||
|
||||
locationHistory(id: string): Promise<DatabaseLocationUpdateLogEvent[]>;
|
||||
|
||||
addLocationUpdateLogEvent(
|
||||
locationId: string,
|
||||
status: DatabaseLocationUpdateLogStatus,
|
||||
entityName?: string | string[],
|
||||
message?: string,
|
||||
/**
|
||||
* Updates a processed entity.
|
||||
*
|
||||
* Any deferred entities are added at the front of the processing queue for
|
||||
* immediate processing, meaning this should only be called when the entity has changes.
|
||||
*/
|
||||
updateProcessedEntity(
|
||||
txOpaque: Transaction,
|
||||
options: UpdateProcessedEntityOptions,
|
||||
): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the cache associated with an entity.
|
||||
*/
|
||||
updateEntityCache(
|
||||
txOpaque: Transaction,
|
||||
options: UpdateEntityCacheOptions,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates only the errors of a processed entity
|
||||
*/
|
||||
updateProcessedEntityErrors(
|
||||
txOpaque: Transaction,
|
||||
options: UpdateProcessedEntityErrorsOptions,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Schedules a refresh of a given entityRef.
|
||||
*/
|
||||
refresh(txOpaque: Transaction, options: RefreshOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Lists all ancestors of a given entityRef.
|
||||
*
|
||||
* The returned list is ordered from the most immediate ancestor to the most distant one.
|
||||
*/
|
||||
listAncestors(
|
||||
txOpaque: Transaction,
|
||||
options: ListAncestorsOptions,
|
||||
): Promise<ListAncestorsResult>;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
*/
|
||||
|
||||
export * from './catalog';
|
||||
export * from './database';
|
||||
export * from './ingestion';
|
||||
export * from './legacy';
|
||||
export * from './search';
|
||||
export * from './util';
|
||||
export * from './next';
|
||||
export * from './processing';
|
||||
export * from './providers';
|
||||
export * from './service';
|
||||
|
||||
@@ -37,13 +37,8 @@ AWSMock.mock(
|
||||
Buffer.from(
|
||||
require('fs').readFileSync(
|
||||
path.resolve(
|
||||
'src',
|
||||
'ingestion',
|
||||
'processors',
|
||||
'__fixtures__',
|
||||
'fileReaderProcessor',
|
||||
'awsS3',
|
||||
'awsS3-mock-object.txt',
|
||||
__dirname,
|
||||
'__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -23,13 +23,7 @@ import {
|
||||
import path from 'path';
|
||||
|
||||
describe('FileReaderProcessor', () => {
|
||||
const fixturesRoot = path.join(
|
||||
'src',
|
||||
'ingestion',
|
||||
'processors',
|
||||
'__fixtures__',
|
||||
'fileReaderProcessor',
|
||||
);
|
||||
const fixturesRoot = path.join(__dirname, '__fixtures__/fileReaderProcessor');
|
||||
|
||||
it('should load from file', async () => {
|
||||
const processor = new FileReaderProcessor();
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model';
|
||||
import { Database, DatabaseManager, Transaction } from '../../database';
|
||||
import { Database, DatabaseManager, Transaction } from '../database';
|
||||
import { basicEntityFilter } from '../../service/request';
|
||||
import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog';
|
||||
import { EntityUpsertRequest } from '../../catalog/types';
|
||||
|
||||
@@ -26,8 +26,8 @@ import { ConflictError } from '@backstage/errors';
|
||||
import { chunk, groupBy } from 'lodash';
|
||||
import limiterFactory from 'p-limit';
|
||||
import { Logger } from 'winston';
|
||||
import type { Database, DbEntityResponse, Transaction } from '../../database';
|
||||
import { DbEntitiesRequest } from '../../database/types';
|
||||
import type { Database, DbEntityResponse, Transaction } from '../database';
|
||||
import { DbEntitiesRequest } from '../database/types';
|
||||
import { basicEntityFilter } from '../../service/request';
|
||||
import { durationText } from '../../util/timing';
|
||||
import type {
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ import {
|
||||
} from '../database/types';
|
||||
import { LocationResponse, LocationsCatalog } from './types';
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export class DatabaseLocationsCatalog implements LocationsCatalog {
|
||||
constructor(private readonly database: Database) {}
|
||||
|
||||
@@ -15,3 +15,10 @@
|
||||
*/
|
||||
|
||||
export { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog';
|
||||
export { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog';
|
||||
export type {
|
||||
LocationResponse,
|
||||
LocationsCatalog,
|
||||
LocationUpdateLogEvent,
|
||||
LocationUpdateStatus,
|
||||
} from './types';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 { Location } from '@backstage/catalog-model';
|
||||
|
||||
//
|
||||
// Locations
|
||||
//
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type LocationUpdateStatus = {
|
||||
timestamp: string | null;
|
||||
status: string | null;
|
||||
message: string | null;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type LocationUpdateLogEvent = {
|
||||
id: string;
|
||||
status: 'fail' | 'success';
|
||||
location_id: string;
|
||||
entity_name: string;
|
||||
created_at?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type LocationResponse = {
|
||||
data: Location;
|
||||
currentStatus: LocationUpdateStatus;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type LocationsCatalog = {
|
||||
addLocation(location: Location): Promise<Location>;
|
||||
removeLocation(id: string): Promise<void>;
|
||||
locations(): Promise<LocationResponse[]>;
|
||||
location(id: string): Promise<LocationResponse>;
|
||||
locationHistory(id: string): Promise<LocationUpdateLogEvent[]>;
|
||||
logUpdateSuccess(
|
||||
locationId: string,
|
||||
entityName?: string | string[],
|
||||
): Promise<void>;
|
||||
logUpdateFailure(
|
||||
locationId: string,
|
||||
error?: Error,
|
||||
entityName?: string,
|
||||
): Promise<void>;
|
||||
};
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import { Entity, Location, parseEntityRef } from '@backstage/catalog-model';
|
||||
import { ConflictError } from '@backstage/errors';
|
||||
import { basicEntityFilter } from '../service/request';
|
||||
import { basicEntityFilter } from '../../service/request';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
import type {
|
||||
DbEntityRequest,
|
||||
+3
-2
@@ -44,9 +44,9 @@ import {
|
||||
DbLocationsRow,
|
||||
DbLocationsRowWithStatus,
|
||||
DbPageInfo,
|
||||
EntityPagination,
|
||||
Transaction,
|
||||
} from './types';
|
||||
import { EntityPagination } from '../../catalog/types';
|
||||
|
||||
// The number of items that are sent per batch to the database layer, when
|
||||
// doing .batchInsert calls to knex. This needs to be low enough to not cause
|
||||
@@ -55,7 +55,8 @@ import {
|
||||
const BATCH_SIZE = 50;
|
||||
|
||||
/**
|
||||
* The core database implementation.
|
||||
* The core database implementation..
|
||||
* @deprecated This was part of the legacy catalog engin
|
||||
*/
|
||||
export class CommonDatabase implements Database {
|
||||
constructor(
|
||||
+2
@@ -26,6 +26,7 @@ const migrationsDir = resolvePackagePath(
|
||||
'migrations',
|
||||
);
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type CreateDatabaseOptions = {
|
||||
logger: Logger;
|
||||
};
|
||||
@@ -34,6 +35,7 @@ const defaultOptions: CreateDatabaseOptions = {
|
||||
logger: getVoidLogger(),
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export class DatabaseManager {
|
||||
public static async createDatabase(
|
||||
knex: Knex,
|
||||
-3
@@ -21,9 +21,6 @@ export type {
|
||||
Database,
|
||||
DbEntityRequest,
|
||||
DbEntityResponse,
|
||||
EntitiesSearchFilter,
|
||||
EntityFilter,
|
||||
EntityPagination,
|
||||
Transaction,
|
||||
DbEntitiesRequest,
|
||||
DbEntitiesResponse,
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2020 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 type {
|
||||
Entity,
|
||||
EntityName,
|
||||
EntityRelationSpec,
|
||||
Location,
|
||||
} from '@backstage/catalog-model';
|
||||
import { EntityFilter, EntityPagination } from '../../catalog/types';
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbEntitiesRow = {
|
||||
id: string;
|
||||
location_id: string | null;
|
||||
etag: string;
|
||||
generation: number;
|
||||
full_name: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbEntityRequest = {
|
||||
locationId?: string;
|
||||
entity: Entity;
|
||||
relations: EntityRelationSpec[];
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbEntitiesRequest = {
|
||||
filter?: EntityFilter;
|
||||
pagination?: EntityPagination;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbEntitiesResponse = {
|
||||
entities: DbEntityResponse[];
|
||||
pageInfo: DbPageInfo;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbPageInfo =
|
||||
| {
|
||||
hasNextPage: false;
|
||||
}
|
||||
| {
|
||||
hasNextPage: true;
|
||||
endCursor: string;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbEntityResponse = {
|
||||
locationId?: string;
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbEntitiesRelationsRow = {
|
||||
originating_entity_id: string;
|
||||
source_full_name: string;
|
||||
type: string;
|
||||
target_full_name: string;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbEntitiesSearchRow = {
|
||||
entity_id: string;
|
||||
key: string;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbLocationsRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DbLocationsRowWithStatus = DbLocationsRow & {
|
||||
status: string | null;
|
||||
timestamp: string | null;
|
||||
message: string | null;
|
||||
};
|
||||
|
||||
export enum DatabaseLocationUpdateLogStatus {
|
||||
FAIL = 'fail',
|
||||
SUCCESS = 'success',
|
||||
}
|
||||
|
||||
/** @deprecated This was part of the legacy catalog engine */
|
||||
export type DatabaseLocationUpdateLogEvent = {
|
||||
id: string;
|
||||
status: DatabaseLocationUpdateLogStatus;
|
||||
location_id: string;
|
||||
entity_name: string;
|
||||
created_at?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An abstraction for transactions of the underlying database technology.
|
||||
*
|
||||
* @deprecated This was part of the legacy catalog engine
|
||||
*/
|
||||
export type Transaction = {
|
||||
rollback(): Promise<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* An abstraction on top of the underlying database, wrapping the basic CRUD
|
||||
* needs.
|
||||
* @deprecated This was part of the legacy catalog engine
|
||||
*/
|
||||
export type Database = {
|
||||
/**
|
||||
* Runs a transaction.
|
||||
*
|
||||
* The callback is expected to make calls back into this class. When it
|
||||
* completes, the transaction is closed.
|
||||
*
|
||||
* @param fn The callback that implements the transaction
|
||||
*/
|
||||
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
||||
|
||||
/**
|
||||
* Adds a set of new entities to the catalog.
|
||||
*
|
||||
* @param tx An ongoing transaction
|
||||
* @param request The entities being added
|
||||
*/
|
||||
addEntities(
|
||||
tx: Transaction,
|
||||
request: DbEntityRequest[],
|
||||
): Promise<DbEntityResponse[]>;
|
||||
|
||||
/**
|
||||
* Updates an existing entity in the catalog.
|
||||
*
|
||||
* The given entity must contain an uid to identify an already stored entity
|
||||
* in the catalog. If it is missing or if no matching entity is found, the
|
||||
* operation fails.
|
||||
*
|
||||
* If matchingEtag or matchingGeneration are given, they are taken into
|
||||
* account. Attempts to update a matching entity, but where the etag and/or
|
||||
* generation are not equal to the passed values, will fail.
|
||||
*
|
||||
* @param tx An ongoing transaction
|
||||
* @param request The entity being updated
|
||||
* @param matchingEtag If specified, reject with ConflictError if not
|
||||
* matching the entry in the database
|
||||
* @param matchingGeneration If specified, reject with ConflictError if not
|
||||
* matching the entry in the database
|
||||
* @returns The updated entity
|
||||
*/
|
||||
updateEntity(
|
||||
tx: Transaction,
|
||||
request: DbEntityRequest,
|
||||
matchingEtag?: string,
|
||||
matchingGeneration?: number,
|
||||
): Promise<DbEntityResponse>;
|
||||
|
||||
entities(
|
||||
tx: Transaction,
|
||||
request?: DbEntitiesRequest,
|
||||
): Promise<DbEntitiesResponse>;
|
||||
|
||||
entityByName(
|
||||
tx: Transaction,
|
||||
name: EntityName,
|
||||
): Promise<DbEntityResponse | undefined>;
|
||||
|
||||
entityByUid(
|
||||
tx: Transaction,
|
||||
uid: string,
|
||||
): Promise<DbEntityResponse | undefined>;
|
||||
|
||||
removeEntityByUid(tx: Transaction, uid: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove current relations for the entity and replace them with the new
|
||||
* relations array.
|
||||
*
|
||||
* @param tx An ongoing transaction
|
||||
* @param entityUid The entity uid
|
||||
* @param relations The relationships to be set
|
||||
*/
|
||||
setRelations(
|
||||
tx: Transaction,
|
||||
entityUid: string,
|
||||
relations: EntityRelationSpec[],
|
||||
): Promise<void>;
|
||||
|
||||
addLocation(tx: Transaction, location: Location): Promise<DbLocationsRow>;
|
||||
|
||||
removeLocation(tx: Transaction, id: string): Promise<void>;
|
||||
|
||||
location(id: string): Promise<DbLocationsRowWithStatus>;
|
||||
|
||||
locations(): Promise<DbLocationsRowWithStatus[]>;
|
||||
|
||||
locationHistory(id: string): Promise<DatabaseLocationUpdateLogEvent[]>;
|
||||
|
||||
addLocationUpdateLogEvent(
|
||||
locationId: string,
|
||||
status: DatabaseLocationUpdateLogStatus,
|
||||
entityName?: string | string[],
|
||||
message?: string,
|
||||
): Promise<void>;
|
||||
};
|
||||
@@ -17,3 +17,4 @@
|
||||
export * from './catalog';
|
||||
export * from './ingestion';
|
||||
export * from './service';
|
||||
export * from './database';
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { Entity, Location, LocationSpec } from '@backstage/catalog-model';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
|
||||
import { LocationUpdateStatus } from '../../catalog/types';
|
||||
import { DatabaseLocationUpdateLogStatus } from '../../database/types';
|
||||
import { EntitiesCatalog } from '../../catalog';
|
||||
import { LocationsCatalog } from '../catalog';
|
||||
import { LocationUpdateStatus } from '../catalog/types';
|
||||
import { DatabaseLocationUpdateLogStatus } from '../database/types';
|
||||
import { HigherOrderOperations } from './HigherOrderOperations';
|
||||
import { LocationReader } from './types';
|
||||
|
||||
describe('HigherOrderOperations', () => {
|
||||
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
|
||||
let entitiesCatalog: jest.Mocked<Required<EntitiesCatalog>>;
|
||||
let locationsCatalog: jest.Mocked<LocationsCatalog>;
|
||||
let locationReader: jest.Mocked<LocationReader>;
|
||||
let higherOrderOperation: HigherOrderOperations;
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
|
||||
import { EntitiesCatalog } from '../../catalog';
|
||||
import { LocationsCatalog } from '../catalog';
|
||||
import { durationText } from '../../util';
|
||||
import {
|
||||
AddLocationResult,
|
||||
@@ -95,14 +96,12 @@ export class HigherOrderOperations implements HigherOrderOperation {
|
||||
return { location, entities: [] };
|
||||
}
|
||||
|
||||
const writtenEntities = await this.entitiesCatalog.batchAddOrUpdateEntities(
|
||||
readerOutput.entities,
|
||||
{
|
||||
locationId: dryRun ? undefined : location.id,
|
||||
dryRun,
|
||||
outputEntities: true,
|
||||
},
|
||||
);
|
||||
const writtenEntities = await this.entitiesCatalog
|
||||
.batchAddOrUpdateEntities!(readerOutput.entities, {
|
||||
locationId: dryRun ? undefined : location.id,
|
||||
dryRun,
|
||||
outputEntities: true,
|
||||
});
|
||||
|
||||
const entities = writtenEntities.map(e => e.entity!);
|
||||
|
||||
@@ -186,7 +185,7 @@ export class HigherOrderOperations implements HigherOrderOperation {
|
||||
startTimestamp = process.hrtime();
|
||||
|
||||
try {
|
||||
await this.entitiesCatalog.batchAddOrUpdateEntities(
|
||||
await this.entitiesCatalog.batchAddOrUpdateEntities!(
|
||||
readerOutput.entities,
|
||||
{ locationId: location.id },
|
||||
);
|
||||
|
||||
@@ -19,11 +19,11 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Knex } from 'knex';
|
||||
import yaml from 'yaml';
|
||||
import { DatabaseManager } from '../../database';
|
||||
import { DatabaseManager } from '../database';
|
||||
import { CatalogProcessorParser } from '../../ingestion';
|
||||
import * as result from '../../ingestion/processors/results';
|
||||
import { CatalogBuilder } from './CatalogBuilder';
|
||||
import { CatalogEnvironment } from '../../next';
|
||||
import { CatalogEnvironment } from '../../service';
|
||||
|
||||
const dummyEntity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
|
||||
@@ -26,13 +26,13 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import lodash from 'lodash';
|
||||
import { EntitiesCatalog } from '../../catalog';
|
||||
import {
|
||||
DatabaseEntitiesCatalog,
|
||||
DatabaseLocationsCatalog,
|
||||
EntitiesCatalog,
|
||||
LocationsCatalog,
|
||||
} from '../../catalog';
|
||||
import { DatabaseEntitiesCatalog } from '../catalog';
|
||||
import { DatabaseManager } from '../../database';
|
||||
} from '../catalog';
|
||||
import { DatabaseManager } from '../database';
|
||||
import {
|
||||
AnnotateLocationEntityProcessor,
|
||||
BitbucketDiscoveryProcessor,
|
||||
@@ -64,7 +64,7 @@ import {
|
||||
} from '../../ingestion/processors/PlaceholderProcessor';
|
||||
import { defaultEntityDataParser } from '../../ingestion/processors/util/parse';
|
||||
import { LocationAnalyzer } from '../../ingestion/types';
|
||||
import { CatalogEnvironment, NextCatalogBuilder } from '../../next';
|
||||
import { CatalogEnvironment, NextCatalogBuilder } from '../../service';
|
||||
|
||||
/**
|
||||
* A builder that helps wire up all of the component parts of the catalog.
|
||||
|
||||
@@ -20,15 +20,15 @@ import { NotFoundError } from '@backstage/errors';
|
||||
import type { Entity, LocationSpec } from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
|
||||
import { LocationResponse } from '../../catalog/types';
|
||||
import { EntitiesCatalog } from '../../catalog';
|
||||
import { LocationResponse, LocationsCatalog } from '../catalog/types';
|
||||
import { HigherOrderOperation } from '../ingestion/types';
|
||||
import { createRouter } from './router';
|
||||
import { basicEntityFilter } from '../../service/request';
|
||||
import { RefreshService } from '../../next';
|
||||
import { RefreshService } from '../../service';
|
||||
|
||||
describe('createRouter readonly disabled', () => {
|
||||
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
|
||||
let entitiesCatalog: jest.Mocked<Required<EntitiesCatalog>>;
|
||||
let locationsCatalog: jest.Mocked<LocationsCatalog>;
|
||||
let higherOrderOperation: jest.Mocked<HigherOrderOperation>;
|
||||
let app: express.Express;
|
||||
|
||||
@@ -26,14 +26,15 @@ import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import yn from 'yn';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
|
||||
import { EntitiesCatalog } from '../../catalog';
|
||||
import { LocationsCatalog } from '../catalog';
|
||||
import { LocationAnalyzer } from '../../ingestion/types';
|
||||
import { HigherOrderOperation } from '../ingestion/types';
|
||||
import {
|
||||
RefreshService,
|
||||
LocationService,
|
||||
RefreshOptions,
|
||||
} from '../../next/types';
|
||||
} from '../../service/types';
|
||||
import {
|
||||
basicEntityFilter,
|
||||
parseEntityFilterParams,
|
||||
@@ -124,7 +125,7 @@ export async function createRouter(
|
||||
disallowReadonlyMode(readonlyEnabled);
|
||||
|
||||
const body = await requireRequestBody(req);
|
||||
const [result] = await entitiesCatalog.batchAddOrUpdateEntities([
|
||||
const [result] = await entitiesCatalog.batchAddOrUpdateEntities!([
|
||||
{ entity: body as Entity, relations: [] },
|
||||
]);
|
||||
const response = await entitiesCatalog.entities({
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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 { Context, ContextKey } from './types';
|
||||
|
||||
/**
|
||||
* A Context implementation that holds a single value, optionally extending an existing context.
|
||||
*/
|
||||
export class ContextWithValue implements Context {
|
||||
static create(parent: Context, key: ContextKey<unknown>, value: unknown) {
|
||||
return new ContextWithValue(parent, key, value);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly parent: Context,
|
||||
private readonly key: ContextKey<unknown>,
|
||||
private readonly value: unknown,
|
||||
) {}
|
||||
|
||||
getContextValue<T>(key: ContextKey<T>): T {
|
||||
if (this.key === key) {
|
||||
return this.value as T;
|
||||
}
|
||||
return this.parent.getContextValue(key);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* 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 { TransactionValue } from './TransactionValue';
|
||||
import { Knex } from 'knex';
|
||||
import { BackgroundContext } from './BackgroundContext';
|
||||
|
||||
describe('TransactionValue Context', () => {
|
||||
it('should be able to store tx values and retrieve them from a context', () => {
|
||||
const tx = {} as Knex.Transaction;
|
||||
const ctx = new BackgroundContext();
|
||||
|
||||
const nextCtx = TransactionValue.in(ctx, tx);
|
||||
|
||||
expect(TransactionValue.from(nextCtx)).toBe(tx);
|
||||
});
|
||||
|
||||
it('should throw when there is no tx value in the context', () => {
|
||||
const ctx = new BackgroundContext();
|
||||
|
||||
expect(() => TransactionValue.from(ctx)).toThrow(
|
||||
/No transaction available in context/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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 { Context, ContextKey } from './types';
|
||||
import { Knex } from 'knex';
|
||||
import { ContextWithValue } from './ContextWithValue';
|
||||
|
||||
const transactionContextKey = new ContextKey<Knex.Transaction | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* TransactionValue handles the wrapping of a knex transaction in a Context.
|
||||
*/
|
||||
export class TransactionValue {
|
||||
static in(parent: Context, tx: Knex.Transaction) {
|
||||
return ContextWithValue.create(parent, transactionContextKey, tx);
|
||||
}
|
||||
|
||||
static from(context: Context): Knex.Transaction {
|
||||
const transaction = context.getContextValue(transactionContextKey);
|
||||
if (!transaction) {
|
||||
throw new Error(`No transaction available in context`);
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { getVoidLogger, resolvePackagePath } from '@backstage/backend-common';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { CommonDatabase } from '../../database/CommonDatabase';
|
||||
import { Database } from '../../database/types';
|
||||
|
||||
export type CreateDatabaseOptions = {
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
const defaultOptions: CreateDatabaseOptions = {
|
||||
logger: getVoidLogger(),
|
||||
};
|
||||
|
||||
export class DatabaseManager {
|
||||
public static async createDatabase(
|
||||
knex: Knex,
|
||||
options: Partial<CreateDatabaseOptions> = {},
|
||||
): Promise<Database> {
|
||||
const migrationsDir = resolvePackagePath(
|
||||
'@backstage/plugin-catalog-backend',
|
||||
'migrations',
|
||||
);
|
||||
|
||||
await knex.migrate.latest({
|
||||
directory: migrationsDir,
|
||||
});
|
||||
const { logger } = { ...defaultOptions, ...options };
|
||||
return new CommonDatabase(knex, logger);
|
||||
}
|
||||
|
||||
public static async createInMemoryDatabase(): Promise<Database> {
|
||||
const knex = await this.createInMemoryDatabaseConnection();
|
||||
return await this.createDatabase(knex);
|
||||
}
|
||||
|
||||
public static async createInMemoryDatabaseConnection(): Promise<Knex> {
|
||||
const knex = knexFactory({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
|
||||
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
|
||||
return knex;
|
||||
}
|
||||
|
||||
public static async createTestDatabase(): Promise<Database> {
|
||||
const knex = await this.createTestDatabaseConnection();
|
||||
return await this.createDatabase(knex);
|
||||
}
|
||||
|
||||
public static async createTestDatabaseConnection(): Promise<Knex> {
|
||||
const config: Knex.Config<any> = {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
};
|
||||
|
||||
let knex = knexFactory(config);
|
||||
if (typeof config.connection !== 'string') {
|
||||
const tempDbName = `d${uuid().replace(/-/g, '')}`;
|
||||
await knex.raw(`CREATE DATABASE ${tempDbName};`);
|
||||
knex = knexFactory({
|
||||
...config,
|
||||
connection: {
|
||||
...config.connection,
|
||||
database: tempDbName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
|
||||
return knex;
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* 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 { Entity, EntityRelationSpec } from '@backstage/catalog-model';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Transaction } from '../../database/types';
|
||||
import { DeferredEntity } from '../processing/types';
|
||||
|
||||
export type AddUnprocessedEntitiesResult = {};
|
||||
|
||||
export type UpdateProcessedEntityOptions = {
|
||||
id: string;
|
||||
processedEntity: Entity;
|
||||
resultHash: string;
|
||||
errors?: string;
|
||||
relations: EntityRelationSpec[];
|
||||
deferredEntities: DeferredEntity[];
|
||||
locationKey?: string;
|
||||
};
|
||||
|
||||
export type UpdateEntityCacheOptions = {
|
||||
id: string;
|
||||
state?: JsonObject;
|
||||
};
|
||||
|
||||
export type UpdateProcessedEntityErrorsOptions = {
|
||||
id: string;
|
||||
errors?: string;
|
||||
resultHash: string;
|
||||
};
|
||||
|
||||
export type RefreshStateItem = {
|
||||
id: string;
|
||||
entityRef: string;
|
||||
unprocessedEntity: Entity;
|
||||
processedEntity?: Entity;
|
||||
resultHash: string;
|
||||
nextUpdateAt: DateTime;
|
||||
lastDiscoveryAt: DateTime; // remove?
|
||||
state?: JsonObject;
|
||||
errors?: string;
|
||||
locationKey?: string;
|
||||
};
|
||||
|
||||
export type GetProcessableEntitiesResult = {
|
||||
items: RefreshStateItem[];
|
||||
};
|
||||
|
||||
export type ReplaceUnprocessedEntitiesOptions =
|
||||
| {
|
||||
sourceKey: string;
|
||||
items: DeferredEntity[];
|
||||
type: 'full';
|
||||
}
|
||||
| {
|
||||
sourceKey: string;
|
||||
added: DeferredEntity[];
|
||||
removed: DeferredEntity[];
|
||||
type: 'delta';
|
||||
};
|
||||
|
||||
export type RefreshOptions = {
|
||||
entityRef: string;
|
||||
};
|
||||
|
||||
export type ListAncestorsOptions = {
|
||||
entityRef: string;
|
||||
};
|
||||
|
||||
export type ListAncestorsResult = {
|
||||
entityRefs: string[];
|
||||
};
|
||||
|
||||
export interface ProcessingDatabase {
|
||||
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
||||
|
||||
/**
|
||||
* Add unprocessed entities to the front of the processing queue using a mutation.
|
||||
*/
|
||||
replaceUnprocessedEntities(
|
||||
txOpaque: Transaction,
|
||||
options: ReplaceUnprocessedEntitiesOptions,
|
||||
): Promise<void>;
|
||||
|
||||
getProcessableEntities(
|
||||
txOpaque: Transaction,
|
||||
request: { processBatchSize: number },
|
||||
): Promise<GetProcessableEntitiesResult>;
|
||||
|
||||
/**
|
||||
* Updates a processed entity.
|
||||
*
|
||||
* Any deferred entities are added at the front of the processing queue for
|
||||
* immediate processing, meaning this should only be called when the entity has changes.
|
||||
*/
|
||||
updateProcessedEntity(
|
||||
txOpaque: Transaction,
|
||||
options: UpdateProcessedEntityOptions,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates the cache associated with an entity.
|
||||
*/
|
||||
updateEntityCache(
|
||||
txOpaque: Transaction,
|
||||
options: UpdateEntityCacheOptions,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates only the errors of a processed entity
|
||||
*/
|
||||
updateProcessedEntityErrors(
|
||||
txOpaque: Transaction,
|
||||
options: UpdateProcessedEntityErrorsOptions,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Schedules a refresh of a given entityRef.
|
||||
*/
|
||||
refresh(txOpaque: Transaction, options: RefreshOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Lists all ancestors of a given entityRef.
|
||||
*
|
||||
* The returned list is ordered from the most immediate ancestor to the most distant one.
|
||||
*/
|
||||
listAncestors(
|
||||
txOpaque: Transaction,
|
||||
options: ListAncestorsOptions,
|
||||
): Promise<ListAncestorsResult>;
|
||||
}
|
||||
+3
-3
@@ -18,10 +18,10 @@ import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { Hash } from 'crypto';
|
||||
import { DateTime } from 'luxon';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
|
||||
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
|
||||
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
|
||||
import { CatalogProcessingOrchestrator } from './processing/types';
|
||||
import { Stitcher } from './stitching/Stitcher';
|
||||
import { CatalogProcessingOrchestrator } from './types';
|
||||
import { Stitcher } from '../stitching/Stitcher';
|
||||
|
||||
describe('DefaultCatalogProcessingEngine', () => {
|
||||
const db = {
|
||||
+9
-9
@@ -23,20 +23,20 @@ import { serializeError } from '@backstage/errors';
|
||||
import { Hash } from 'crypto';
|
||||
import stableStringify from 'fast-json-stable-stringify';
|
||||
import { Logger } from 'winston';
|
||||
import { ProcessingDatabase, RefreshStateItem } from './database/types';
|
||||
import { createCounterMetric, createSummaryMetric } from './metrics';
|
||||
import {
|
||||
CatalogProcessingOrchestrator,
|
||||
EntityProcessingResult,
|
||||
} from './processing/types';
|
||||
import { Stitcher } from './stitching/Stitcher';
|
||||
import { startTaskPipeline } from './TaskPipeline';
|
||||
import { ProcessingDatabase, RefreshStateItem } from '../database/types';
|
||||
import { createCounterMetric, createSummaryMetric } from '../util/metrics';
|
||||
import {
|
||||
CatalogProcessingEngine,
|
||||
CatalogProcessingOrchestrator,
|
||||
EntityProcessingResult,
|
||||
} from '../processing/types';
|
||||
import { Stitcher } from '../stitching/Stitcher';
|
||||
import { startTaskPipeline } from './TaskPipeline';
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
EntityProviderMutation,
|
||||
} from './types';
|
||||
} from '../providers/types';
|
||||
|
||||
const CACHE_TTL = 5;
|
||||
|
||||
+3
-3
@@ -30,10 +30,10 @@ import {
|
||||
CatalogProcessorEmit,
|
||||
CatalogProcessorParser,
|
||||
results,
|
||||
} from '../../ingestion';
|
||||
import { CatalogRulesEnforcer } from '../../ingestion/CatalogRules';
|
||||
} from '../ingestion';
|
||||
import { CatalogRulesEnforcer } from '../ingestion/CatalogRules';
|
||||
import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator';
|
||||
import { defaultEntityDataParser } from '../../ingestion/processors/util/parse';
|
||||
import { defaultEntityDataParser } from '../ingestion/processors/util/parse';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
class FooBarProcessor implements CatalogProcessor {
|
||||
+3
-3
@@ -31,8 +31,8 @@ import { Logger } from 'winston';
|
||||
import {
|
||||
CatalogProcessor,
|
||||
CatalogProcessorParser,
|
||||
} from '../../ingestion/processors';
|
||||
import * as results from '../../ingestion/processors/results';
|
||||
} from '../ingestion/processors';
|
||||
import * as results from '../ingestion/processors/results';
|
||||
import {
|
||||
CatalogProcessingOrchestrator,
|
||||
EntityProcessingRequest,
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
validateEntityEnvelope,
|
||||
isObject,
|
||||
} from './util';
|
||||
import { CatalogRulesEnforcer } from '../../ingestion/CatalogRules';
|
||||
import { CatalogRulesEnforcer } from '../ingestion/CatalogRules';
|
||||
import { ProcessorCacheManager } from './ProcessorCacheManager';
|
||||
|
||||
type Context = {
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogProcessor } from '../../ingestion/processors';
|
||||
import { CatalogProcessor } from '../ingestion/processors';
|
||||
import { ProcessorCacheManager } from './ProcessorCacheManager';
|
||||
|
||||
class MyProcessor implements CatalogProcessor {
|
||||
+2
-2
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import { JsonObject, JsonValue } from '@backstage/config';
|
||||
import { CatalogProcessor } from '../../ingestion/processors';
|
||||
import { CatalogProcessorCache } from '../../ingestion/processors/types';
|
||||
import { CatalogProcessor } from '../ingestion/processors';
|
||||
import { CatalogProcessorCache } from '../ingestion/processors/types';
|
||||
import { isObject } from './util';
|
||||
|
||||
class SingleProcessorSubCache implements CatalogProcessorCache {
|
||||
+2
-2
@@ -22,8 +22,8 @@ import {
|
||||
stringifyLocationReference,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Logger } from 'winston';
|
||||
import { CatalogProcessorResult } from '../../ingestion';
|
||||
import { locationSpecToLocationEntity } from '../util';
|
||||
import { CatalogProcessorResult } from '../ingestion';
|
||||
import { locationSpecToLocationEntity } from '../util/conversion';
|
||||
import { DeferredEntity } from './types';
|
||||
import {
|
||||
getEntityLocationRef,
|
||||
+4
@@ -16,8 +16,12 @@
|
||||
|
||||
export type {
|
||||
CatalogProcessingOrchestrator,
|
||||
CatalogProcessingEngine,
|
||||
EntityProcessingRequest,
|
||||
EntityProcessingResult,
|
||||
DeferredEntity,
|
||||
} from './types';
|
||||
export { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator';
|
||||
|
||||
export { createRandomRefreshInterval } from './refresh';
|
||||
export type { RefreshIntervalFunction } from './refresh';
|
||||
+5
@@ -44,3 +44,8 @@ export type DeferredEntity = {
|
||||
entity: Entity;
|
||||
locationKey?: string;
|
||||
};
|
||||
|
||||
export interface CatalogProcessingEngine {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
+1
-5
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import path from 'path';
|
||||
import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider';
|
||||
@@ -44,10 +43,7 @@ describe('ConfigLocationEntityProvider', () => {
|
||||
{
|
||||
entity: expect.objectContaining({
|
||||
spec: {
|
||||
target: path.join(
|
||||
resolvePackagePath('@backstage/plugin-catalog-backend'),
|
||||
'./lols.yaml',
|
||||
),
|
||||
target: path.join(process.cwd(), 'lols.yaml'),
|
||||
type: 'file',
|
||||
},
|
||||
}),
|
||||
+2
-2
@@ -16,9 +16,9 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import path from 'path';
|
||||
import { getEntityLocationRef } from './processing/util';
|
||||
import { getEntityLocationRef } from '../processing/util';
|
||||
import { EntityProvider, EntityProviderConnection } from './types';
|
||||
import { locationSpecToLocationEntity } from './util';
|
||||
import { locationSpecToLocationEntity } from '../util/conversion';
|
||||
|
||||
export class ConfigLocationEntityProvider implements EntityProvider {
|
||||
constructor(private readonly config: Config) {}
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DatabaseManager } from './database/DatabaseManager';
|
||||
import { applyDatabaseMigrations } from '../database/migrations';
|
||||
import { DefaultLocationStore } from './DefaultLocationStore';
|
||||
|
||||
describe('DefaultLocationStore', () => {
|
||||
@@ -25,7 +25,7 @@ describe('DefaultLocationStore', () => {
|
||||
|
||||
async function createLocationStore(databaseId: TestDatabaseId) {
|
||||
const knex = await databases.init(databaseId);
|
||||
await DatabaseManager.createDatabase(knex);
|
||||
await applyDatabaseMigrations(knex);
|
||||
const connection = { applyMutation: jest.fn() };
|
||||
const store = new DefaultLocationStore(knex);
|
||||
await store.connect(connection);
|
||||
+5
-8
@@ -18,14 +18,11 @@ import { Location, LocationSpec } from '@backstage/catalog-model';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DbLocationsRow } from './database/tables';
|
||||
import { getEntityLocationRef } from './processing/util';
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
LocationStore,
|
||||
} from './types';
|
||||
import { locationSpecToLocationEntity } from './util';
|
||||
import { DbLocationsRow } from '../database/tables';
|
||||
import { getEntityLocationRef } from '../processing/util';
|
||||
import { EntityProvider, EntityProviderConnection } from './types';
|
||||
import { locationSpecToLocationEntity } from '../util/conversion';
|
||||
import { LocationStore } from '../service';
|
||||
|
||||
export class DefaultLocationStore implements LocationStore, EntityProvider {
|
||||
private _connection: EntityProviderConnection | undefined;
|
||||
+5
-7
@@ -14,10 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export class ContextKey<T> {
|
||||
constructor(readonly defaultValue: T) {}
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
getContextValue<T>(key: ContextKey<T>): T;
|
||||
}
|
||||
export type {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
EntityProviderMutation,
|
||||
} from './types';
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { DeferredEntity } from '../processing';
|
||||
|
||||
export type EntityProviderMutation =
|
||||
| { type: 'full'; entities: DeferredEntity[] }
|
||||
| { type: 'delta'; added: DeferredEntity[]; removed: DeferredEntity[] };
|
||||
|
||||
export interface EntityProviderConnection {
|
||||
applyMutation(mutation: EntityProviderMutation): Promise<void>;
|
||||
}
|
||||
|
||||
export interface EntityProvider {
|
||||
getProviderName(): string;
|
||||
connect(connection: EntityProviderConnection): Promise<void>;
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { DefaultLocationService } from './DefaultLocationService';
|
||||
import { CatalogProcessingOrchestrator } from './processing/types';
|
||||
import { CatalogProcessingOrchestrator } from '../processing/types';
|
||||
import { LocationStore } from './types';
|
||||
|
||||
describe('DefaultLocationServiceTest', () => {
|
||||
+2
-2
@@ -23,9 +23,9 @@ import {
|
||||
import {
|
||||
CatalogProcessingOrchestrator,
|
||||
DeferredEntity,
|
||||
} from './processing/types';
|
||||
} from '../processing/types';
|
||||
import { LocationService, LocationStore } from './types';
|
||||
import { locationSpecToMetadataName } from './util';
|
||||
import { locationSpecToMetadataName } from '../util/conversion';
|
||||
|
||||
export class DefaultLocationService implements LocationService {
|
||||
constructor(
|
||||
+8
-8
@@ -19,16 +19,16 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { createHash } from 'crypto';
|
||||
import { Knex } from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { DatabaseManager } from './database/DatabaseManager';
|
||||
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
|
||||
import { applyDatabaseMigrations } from '../database/migrations';
|
||||
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
|
||||
import {
|
||||
DbRefreshStateReferencesRow,
|
||||
DbRefreshStateRow,
|
||||
} from './database/tables';
|
||||
import { ProcessingDatabase } from './database/types';
|
||||
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
|
||||
import { EntityProcessingRequest } from './processing/types';
|
||||
import { Stitcher } from './stitching/Stitcher';
|
||||
} from '../database/tables';
|
||||
import { ProcessingDatabase } from '../database/types';
|
||||
import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine';
|
||||
import { EntityProcessingRequest } from '../processing/types';
|
||||
import { Stitcher } from '../stitching/Stitcher';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DefaultRefreshService } from './DefaultRefreshService';
|
||||
@@ -44,7 +44,7 @@ describe('Refresh integration', () => {
|
||||
logger: Logger = defaultLogger,
|
||||
) {
|
||||
const knex = await databases.init(databaseId);
|
||||
await DatabaseManager.createDatabase(knex);
|
||||
await applyDatabaseMigrations(knex);
|
||||
return {
|
||||
knex,
|
||||
db: new DefaultProcessingDatabase({
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
|
||||
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
|
||||
import { RefreshOptions, RefreshService } from './types';
|
||||
|
||||
export class DefaultRefreshService implements RefreshService {
|
||||
+18
-26
@@ -14,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
PluginDatabaseManager,
|
||||
resolvePackagePath,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common';
|
||||
import {
|
||||
DefaultNamespaceEntityPolicy,
|
||||
EntityPolicies,
|
||||
@@ -33,12 +29,13 @@ import { ScmIntegrations } from '@backstage/integration';
|
||||
import { createHash } from 'crypto';
|
||||
import { Router } from 'express';
|
||||
import lodash from 'lodash';
|
||||
import { EntitiesCatalog } from '../catalog';
|
||||
import {
|
||||
DatabaseLocationsCatalog,
|
||||
EntitiesCatalog,
|
||||
LocationsCatalog,
|
||||
} from '../catalog';
|
||||
import { CommonDatabase } from '../database/CommonDatabase';
|
||||
CommonDatabase,
|
||||
} from '../legacy';
|
||||
|
||||
import {
|
||||
AnnotateLocationEntityProcessor,
|
||||
BitbucketDiscoveryProcessor,
|
||||
@@ -62,28 +59,27 @@ import {
|
||||
} from '../ingestion/processors/PlaceholderProcessor';
|
||||
import { defaultEntityDataParser } from '../ingestion/processors/util/parse';
|
||||
import { LocationAnalyzer } from '../ingestion/types';
|
||||
import {
|
||||
CatalogProcessingEngine,
|
||||
EntityProvider,
|
||||
LocationService,
|
||||
} from '../next/types';
|
||||
import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider';
|
||||
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
|
||||
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
|
||||
import { EntityProvider } from '../providers/types';
|
||||
import { CatalogProcessingEngine } from '../processing/types';
|
||||
import { ConfigLocationEntityProvider } from '../providers/ConfigLocationEntityProvider';
|
||||
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
|
||||
import { applyDatabaseMigrations } from '../database/migrations';
|
||||
import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine';
|
||||
import { DefaultLocationService } from './DefaultLocationService';
|
||||
import { DefaultLocationStore } from './DefaultLocationStore';
|
||||
import { DefaultLocationStore } from '../providers/DefaultLocationStore';
|
||||
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
|
||||
import { DefaultCatalogProcessingOrchestrator } from './processing/DefaultCatalogProcessingOrchestrator';
|
||||
import { Stitcher } from './stitching/Stitcher';
|
||||
import { DefaultCatalogProcessingOrchestrator } from '../processing/DefaultCatalogProcessingOrchestrator';
|
||||
import { Stitcher } from '../stitching/Stitcher';
|
||||
import {
|
||||
createRandomRefreshInterval,
|
||||
RefreshIntervalFunction,
|
||||
} from './refresh';
|
||||
} from '../processing/refresh';
|
||||
import { createNextRouter } from './NextRouter';
|
||||
import { DefaultRefreshService } from './DefaultRefreshService';
|
||||
import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
import { LocationService } from './types';
|
||||
|
||||
export type CatalogEnvironment = {
|
||||
logger: Logger;
|
||||
@@ -289,6 +285,7 @@ export class NextCatalogBuilder {
|
||||
*/
|
||||
async build(): Promise<{
|
||||
entitiesCatalog: EntitiesCatalog;
|
||||
/** @deprecated This will be removed */
|
||||
locationsCatalog: LocationsCatalog;
|
||||
locationAnalyzer: LocationAnalyzer;
|
||||
processingEngine: CatalogProcessingEngine;
|
||||
@@ -302,12 +299,7 @@ export class NextCatalogBuilder {
|
||||
const parser = this.parser || defaultEntityDataParser;
|
||||
|
||||
const dbClient = await database.getClient();
|
||||
await dbClient.migrate.latest({
|
||||
directory: resolvePackagePath(
|
||||
'@backstage/plugin-catalog-backend',
|
||||
'migrations',
|
||||
),
|
||||
});
|
||||
await applyDatabaseMigrations(dbClient);
|
||||
|
||||
const db = new CommonDatabase(dbClient, logger);
|
||||
|
||||
+3
-3
@@ -18,12 +18,12 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DatabaseManager } from './database/DatabaseManager';
|
||||
import { applyDatabaseMigrations } from '../database/migrations';
|
||||
import {
|
||||
DbFinalEntitiesRow,
|
||||
DbRefreshStateReferencesRow,
|
||||
DbRefreshStateRow,
|
||||
} from './database/tables';
|
||||
} from '../database/tables';
|
||||
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
|
||||
|
||||
describe('NextEntitiesCatalog', () => {
|
||||
@@ -33,7 +33,7 @@ describe('NextEntitiesCatalog', () => {
|
||||
|
||||
async function createDatabase(databaseId: TestDatabaseId) {
|
||||
const knex = await databases.init(databaseId);
|
||||
await DatabaseManager.createDatabase(knex);
|
||||
await applyDatabaseMigrations(knex);
|
||||
return { knex };
|
||||
}
|
||||
|
||||
+3
-2
@@ -22,14 +22,15 @@ import {
|
||||
EntitiesRequest,
|
||||
EntitiesResponse,
|
||||
EntityAncestryResponse,
|
||||
EntityPagination,
|
||||
} from '../catalog/types';
|
||||
import { DbPageInfo, EntityPagination } from '../database/types';
|
||||
import {
|
||||
DbFinalEntitiesRow,
|
||||
DbRefreshStateReferencesRow,
|
||||
DbRefreshStateRow,
|
||||
DbSearchRow,
|
||||
} from './database/tables';
|
||||
DbPageInfo,
|
||||
} from '../database/tables';
|
||||
|
||||
function parsePagination(input?: EntityPagination): {
|
||||
limit?: number;
|
||||
+1
-1
@@ -22,7 +22,7 @@ import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { EntitiesCatalog } from '../catalog';
|
||||
import { LocationService, RefreshService } from './types';
|
||||
import { basicEntityFilter } from '../service/request';
|
||||
import { basicEntityFilter } from './request';
|
||||
import { createNextRouter } from './NextRouter';
|
||||
|
||||
describe('createNextRouter readonly disabled', () => {
|
||||
+8
-16
@@ -14,21 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { CatalogEnvironment } from './NextCatalogBuilder';
|
||||
export { NextCatalogBuilder } from './NextCatalogBuilder';
|
||||
export type {
|
||||
LocationService,
|
||||
RefreshService,
|
||||
RefreshOptions,
|
||||
LocationStore,
|
||||
} from './types';
|
||||
export { createNextRouter } from './NextRouter';
|
||||
export type { NextRouterOptions } from './NextRouter';
|
||||
export * from './processing';
|
||||
export { createRandomRefreshInterval } from './refresh';
|
||||
export type { RefreshIntervalFunction } from './refresh';
|
||||
export * from './stitching';
|
||||
export type {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
EntityProviderMutation,
|
||||
CatalogProcessingEngine,
|
||||
LocationService,
|
||||
LocationStore,
|
||||
RefreshOptions,
|
||||
RefreshService,
|
||||
} from './types';
|
||||
export type { CatalogEnvironment } from './NextCatalogBuilder';
|
||||
export { NextCatalogBuilder } from './NextCatalogBuilder';
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntitiesSearchFilter, EntityFilter } from '../../database';
|
||||
import { EntitiesSearchFilter, EntityFilter } from '../../catalog';
|
||||
|
||||
/**
|
||||
* Forms a full EntityFilter based on a single key-value(s) object.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { EntitiesSearchFilter, EntityFilter } from '../../database';
|
||||
import { EntitiesSearchFilter, EntityFilter } from '../../catalog';
|
||||
import { parseStringsParam } from './common';
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { EntityPagination } from '../../database';
|
||||
import { EntityPagination } from '../../catalog';
|
||||
import { parseIntegerParam, parseStringParam } from './common';
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { DatabaseManager } from '../database';
|
||||
import { DatabaseManager } from '../legacy/database';
|
||||
import { CatalogBuilder } from '../legacy/service/CatalogBuilder';
|
||||
import { createRouter } from '../legacy/service';
|
||||
|
||||
|
||||
+6
-25
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, Location, LocationSpec } from '@backstage/catalog-model';
|
||||
import { DeferredEntity } from './processing/types';
|
||||
import { Entity, LocationSpec, Location } from '@backstage/catalog-model';
|
||||
|
||||
export interface LocationService {
|
||||
createLocation(
|
||||
@@ -27,18 +26,6 @@ export interface LocationService {
|
||||
deleteLocation(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface LocationStore {
|
||||
createLocation(spec: LocationSpec): Promise<Location>;
|
||||
listLocations(): Promise<Location[]>;
|
||||
getLocation(id: string): Promise<Location>;
|
||||
deleteLocation(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface CatalogProcessingEngine {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for requesting a refresh of entities in the catalog.
|
||||
*
|
||||
@@ -61,15 +48,9 @@ export interface RefreshService {
|
||||
refresh(options: RefreshOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export type EntityProviderMutation =
|
||||
| { type: 'full'; entities: DeferredEntity[] }
|
||||
| { type: 'delta'; added: DeferredEntity[]; removed: DeferredEntity[] };
|
||||
|
||||
export interface EntityProviderConnection {
|
||||
applyMutation(mutation: EntityProviderMutation): Promise<void>;
|
||||
}
|
||||
|
||||
export interface EntityProvider {
|
||||
getProviderName(): string;
|
||||
connect(connection: EntityProviderConnection): Promise<void>;
|
||||
export interface LocationStore {
|
||||
createLocation(spec: LocationSpec): Promise<Location>;
|
||||
listLocations(): Promise<Location[]>;
|
||||
getLocation(id: string): Promise<Location>;
|
||||
deleteLocation(id: string): Promise<void>;
|
||||
}
|
||||
+2
-2
@@ -17,7 +17,7 @@
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { DatabaseManager } from '../database/DatabaseManager';
|
||||
import { applyDatabaseMigrations } from '../database/migrations';
|
||||
import {
|
||||
DbFinalEntitiesRow,
|
||||
DbRefreshStateReferencesRow,
|
||||
@@ -37,7 +37,7 @@ describe('Stitcher', () => {
|
||||
'runs the happy path for %p',
|
||||
async databaseId => {
|
||||
const db = await databases.init(databaseId);
|
||||
await DatabaseManager.createDatabase(db);
|
||||
await applyDatabaseMigrations(db);
|
||||
|
||||
const stitcher = new Stitcher(db, logger);
|
||||
let entities: DbFinalEntitiesRow[];
|
||||
@@ -93,13 +93,23 @@ export const EntityCatalogGraphCard: ({
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type EntityEdge = DependencyGraphTypes.DependencyEdge<{
|
||||
export type EntityEdge = DependencyGraphTypes.DependencyEdge<EntityEdgeData>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityEdgeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type EntityEdgeData = {
|
||||
relations: string[];
|
||||
label: 'visible';
|
||||
}>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type EntityNode = DependencyGraphTypes.DependencyNode<{
|
||||
export type EntityNode = DependencyGraphTypes.DependencyNode<EntityNodeData>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityNodeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export type EntityNodeData = {
|
||||
name: string;
|
||||
kind?: string;
|
||||
title?: string;
|
||||
@@ -107,7 +117,7 @@ export type EntityNode = DependencyGraphTypes.DependencyNode<{
|
||||
focused?: boolean;
|
||||
color?: 'primary' | 'secondary' | 'default';
|
||||
onClick?: MouseEventHandler<unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const EntityRelationsGraph: ({
|
||||
|
||||
@@ -31,12 +31,6 @@ describe('<CustomLabel />', () => {
|
||||
relations: [RELATION_PARENT_OF],
|
||||
from: 'from-id',
|
||||
to: 'to-id',
|
||||
id: 'id',
|
||||
x: 111,
|
||||
y: 222,
|
||||
width: 100,
|
||||
height: 25,
|
||||
points: [],
|
||||
}}
|
||||
/>
|
||||
</svg>,
|
||||
@@ -54,12 +48,6 @@ describe('<CustomLabel />', () => {
|
||||
relations: [RELATION_PARENT_OF, RELATION_CHILD_OF],
|
||||
from: 'from-id',
|
||||
to: 'to-id',
|
||||
id: 'id',
|
||||
x: 111,
|
||||
y: 222,
|
||||
width: 100,
|
||||
height: 25,
|
||||
points: [],
|
||||
}}
|
||||
/>
|
||||
</svg>,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { DependencyGraphTypes } from '@backstage/core-components';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import makeStyles from '@material-ui/core/styles/makeStyles';
|
||||
import React from 'react';
|
||||
import { GraphEdge } from './types';
|
||||
import { EntityEdgeData } from './types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const useStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
@@ -31,7 +31,7 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
|
||||
export function CustomLabel({
|
||||
edge: { relations },
|
||||
}: DependencyGraphTypes.RenderLabelProps<GraphEdge>) {
|
||||
}: DependencyGraphTypes.RenderLabelProps<EntityEdgeData>) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<text className={classes.text} textAnchor="middle">
|
||||
|
||||
@@ -36,10 +36,6 @@ describe('<CustomNode />', () => {
|
||||
name: 'name',
|
||||
namespace: 'namespace',
|
||||
id: 'kind:namespace/name',
|
||||
x: 111,
|
||||
y: 222,
|
||||
width: 100,
|
||||
height: 25,
|
||||
color: 'primary',
|
||||
}}
|
||||
/>
|
||||
@@ -59,10 +55,6 @@ describe('<CustomNode />', () => {
|
||||
name: 'name',
|
||||
namespace: 'default',
|
||||
id: 'kind:default/name',
|
||||
x: 111,
|
||||
y: 222,
|
||||
width: 100,
|
||||
height: 25,
|
||||
}}
|
||||
/>
|
||||
</svg>,
|
||||
@@ -83,10 +75,6 @@ describe('<CustomNode />', () => {
|
||||
namespace: 'namespace',
|
||||
onClick,
|
||||
id: 'kind:namespace/name',
|
||||
x: 111,
|
||||
y: 222,
|
||||
width: 100,
|
||||
height: 25,
|
||||
}}
|
||||
/>
|
||||
</svg>,
|
||||
@@ -108,10 +96,6 @@ describe('<CustomNode />', () => {
|
||||
namespace: 'namespace',
|
||||
title: 'Custom Title',
|
||||
id: 'kind:namespace/name',
|
||||
x: 111,
|
||||
y: 222,
|
||||
width: 100,
|
||||
height: 25,
|
||||
}}
|
||||
/>
|
||||
</svg>,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user