Merge branch 'master' into ndudnik/edit-yaml
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
name: CLI Test Windows
|
||||
|
||||
# Building on windows is really slow, so this workflow is separate from cli.yml and only builds on changes
|
||||
# to the cli itself. They're more likely to introduce issues on windows, compared to changes to core and yarn.lock.
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/cli-win.yml'
|
||||
- 'packages/cli/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest]
|
||||
node-version: [12.x]
|
||||
|
||||
env:
|
||||
CI: true
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: find location of global yarn cache
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: yarn tsc
|
||||
- run: yarn build
|
||||
- name: verify app and plugin creation
|
||||
working-directory: ${{ runner.temp }}
|
||||
run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: lint newly created app and plugin
|
||||
run: yarn lint:all
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: test newly created app and plugin
|
||||
run: yarn test:all
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: e2e test newly created app
|
||||
run: yarn test:e2e:ci
|
||||
working-directory: ${{ runner.temp }}/test-app/packages/app
|
||||
env:
|
||||
PORT: 3001
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
- '.github/workflows/cli.yml'
|
||||
- 'packages/cli/**'
|
||||
- 'packages/core/**'
|
||||
- 'packages/core-api/**'
|
||||
- 'yarn.lock'
|
||||
|
||||
jobs:
|
||||
@@ -14,7 +15,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
os: [ubuntu-latest]
|
||||
node-version: [12.x]
|
||||
|
||||
env:
|
||||
@@ -28,7 +29,7 @@ jobs:
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
@@ -42,15 +43,8 @@ jobs:
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: yarn tsc
|
||||
- run: yarn build
|
||||
- name: verify app and plugin creation on Windows
|
||||
- name: verify app and plugin creation
|
||||
working-directory: ${{ runner.temp }}
|
||||
if: runner.os == 'Windows'
|
||||
run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
|
||||
env:
|
||||
BACKSTAGE_E2E_CLI_TEST: true
|
||||
- name: verify app and plugin creation on Linux
|
||||
working-directory: ${{ runner.temp }}
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo sysctl fs.inotify.max_user_watches=524288
|
||||
node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
|
||||
|
||||
@@ -25,19 +25,19 @@ jobs:
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: cache node_modules
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
|
||||
- name: cache build cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .backstage-build-cache
|
||||
key: build-cache-${{ github.sha }}
|
||||
|
||||
@@ -24,19 +24,19 @@ jobs:
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: cache node_modules
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
|
||||
- name: cache build cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .backstage-build-cache
|
||||
key: build-cache-${{ github.sha }}
|
||||
|
||||
@@ -27,14 +27,14 @@ jobs:
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: cache node_modules
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
app:
|
||||
title: Backstage Example App
|
||||
baseUrl: http://localhost:3000
|
||||
|
||||
backend:
|
||||
baseUrl: http://localhost:7000
|
||||
|
||||
organization:
|
||||
name: Spotify
|
||||
@@ -0,0 +1,77 @@
|
||||
# ADR005: Catalog Core Entities
|
||||
|
||||
| Created | Status |
|
||||
| ---------- | ------ |
|
||||
| 2020-05-29 | Open |
|
||||
|
||||
## Context
|
||||
|
||||
We want to standardize on a few core entities that we are tracking in the Backstage catalog. This allows us to build specific plugins around them.
|
||||
|
||||
## Decision
|
||||
|
||||
Backstage should eventually support the following core entities:
|
||||
|
||||
* **Components** are individual pieces of software
|
||||
* **APIs** are the boundaries between different components
|
||||
* **Resources** are physical or virtual infrastructure needed to operate a component
|
||||
|
||||

|
||||
|
||||
For now, we'll start by only implementing support for the Component entity in the Backstage catalog. This can later be extended to APIs, Resources and other potentially useful entities.
|
||||
|
||||
### Component
|
||||
A component is a piece of software, for example a mobile application feature, web site, backend service or data pipeline (list not exhaustive). A component can be tracked in source control, or use some existing open source or commercial software. It can implement APIs for other components to consume. In turn it might depend on APIs implemented by other components, or resources that are attached to it at runtime.
|
||||
|
||||
Component entities are typically defined in YAML descriptor files next to the code of the component, and could look like this (actual schema will evolve):
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1beta1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: my-component-name
|
||||
spec:
|
||||
type: service
|
||||
```
|
||||
|
||||
### API
|
||||
APIs form an abstraction that allows large software ecosystems to scale. Thus, APIs are a first class citizen in the Backstage model and the primary way to discover existing functionality in the ecosystem.
|
||||
|
||||
APIs are implemented by components and make their boundaries explicit. They might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g. framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs exposed by components need to be in a known machine-readable format so we can build further tooling and analysis on top.
|
||||
|
||||
APIs are typically indexed from existing definitions in source control and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve):
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1beta1
|
||||
kind: API
|
||||
metadata:
|
||||
name: my-component-api
|
||||
spec:
|
||||
type: grpc
|
||||
definition: >
|
||||
service HelloService {
|
||||
rpc SayHello (HelloRequest) returns (HelloResponse);
|
||||
}
|
||||
message HelloRequest {
|
||||
string greeting = 1;
|
||||
}
|
||||
message HelloResponse {
|
||||
string reply = 1;
|
||||
}
|
||||
```
|
||||
|
||||
### Resource
|
||||
Resources are the infrastructure your software needs to operate at runtime like Bigtable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and APIs will allow us to visualize and create tooling around them in Backstage.
|
||||
|
||||
Resources are typically indexed from declarative definitions (e.g. Terraform, GCP Config Connector, AWS Cloud Formation) and/or inventories from cloud providers (e.g. GCP Asset Inventory) and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve):
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1beta1
|
||||
kind: Resource
|
||||
metadata:
|
||||
name: my-component-db
|
||||
spec:
|
||||
type: gcp-spanner
|
||||
url: spanner.googleapis.com/projects/prj/instances/my-component-db/databases/my-db
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
We will continue fleshing out support for the Component entity in the Backstage catalog.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,16 +1,28 @@
|
||||
# Development Environment
|
||||
|
||||
This section describes how to get set up for doing development on the Backstage repository.
|
||||
|
||||
## Cloning the Repository
|
||||
|
||||
After you have cloned the Backstage repository, you should run the following commands
|
||||
once to set things up for development:
|
||||
|
||||
```bash
|
||||
$ yarn install # fetch dependency packages - may take a while
|
||||
|
||||
$ yarn tsc # does a first run of type generation and checks
|
||||
```
|
||||
|
||||
## Serving the Example App
|
||||
|
||||
Open a terminal window and start the web app using the following commands from the project root:
|
||||
Open a terminal window and start the web app by using the following command from the project root.
|
||||
Make sure you have run the above mentioned commands first.
|
||||
|
||||
```bash
|
||||
$ yarn install # may take a while
|
||||
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.
|
||||
This should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.
|
||||
|
||||
By default, backstage will start on port 3000, however you can override this by setting an environment variable `PORT` on your local machine. e.g. `export PORT=8080` then running `yarn start`. Or `PORT=8080 yarn start`.
|
||||
|
||||
|
||||
@@ -24,18 +24,6 @@ import apis from './apis';
|
||||
const app = createApp({
|
||||
apis,
|
||||
plugins: Object.values(plugins),
|
||||
configLoader: async () => ({
|
||||
app: {
|
||||
title: 'Backstage Example App',
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:7000',
|
||||
},
|
||||
organization: {
|
||||
name: 'Spotify',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const AppProvider = app.getProvider();
|
||||
|
||||
@@ -32,7 +32,6 @@ export default async function createPlugin({
|
||||
logger,
|
||||
database,
|
||||
}: PluginEnvironment) {
|
||||
const policy = new EntityPolicies();
|
||||
const ingestionModel = new IngestionModels(
|
||||
new LocationReaders(),
|
||||
new DescriptorParsers(),
|
||||
@@ -40,19 +39,17 @@ export default async function createPlugin({
|
||||
);
|
||||
|
||||
const db = await DatabaseManager.createDatabase(database, logger);
|
||||
runPeriodically(
|
||||
() => DatabaseManager.refreshLocations(db, ingestionModel, policy, logger),
|
||||
10000,
|
||||
);
|
||||
|
||||
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
|
||||
const locationsCatalog = new DatabaseLocationsCatalog(db);
|
||||
const higherOrderOperation = new HigherOrderOperations(
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
ingestionModel,
|
||||
logger,
|
||||
);
|
||||
|
||||
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000);
|
||||
|
||||
return await createRouter({
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"url-loader": "^4.1.0",
|
||||
"webpack": "^4.41.6",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"yaml": "^1.10.0",
|
||||
"yml-loader": "^2.1.0",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
|
||||
import { buildBundle } from '../../lib/bundler';
|
||||
import { Command } from 'commander';
|
||||
import { loadConfig } from '../../lib/app-config';
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
await buildBundle({
|
||||
entry: 'src/index',
|
||||
statsJsonEnabled: cmd.stats,
|
||||
appConfig: await loadConfig(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { serveBundle } from '../../lib/bundler';
|
||||
import { loadConfig } from '../../lib/app-config';
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
const waitForExit = await serveBundle({
|
||||
entry: 'src/index',
|
||||
checksEnabled: cmd.check,
|
||||
appConfig: await loadConfig(),
|
||||
});
|
||||
|
||||
await waitForExit();
|
||||
|
||||
@@ -24,6 +24,7 @@ export default async function clean() {
|
||||
const packagePath = getPackagePath(cacheOptions.cacheDir);
|
||||
await fs.remove(cacheOptions.output);
|
||||
await fs.remove(packagePath);
|
||||
await fs.remove(paths.resolveTarget('coverage'));
|
||||
}
|
||||
|
||||
function getPackagePath(cacheDir: string) {
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { serveBundle } from '../../lib/bundler';
|
||||
import { loadConfig } from '../../lib/app-config';
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
const waitForExit = await serveBundle({
|
||||
entry: 'dev/index',
|
||||
checksEnabled: cmd.check,
|
||||
appConfig: await loadConfig(),
|
||||
});
|
||||
|
||||
await waitForExit();
|
||||
|
||||
+2
-7
@@ -14,10 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defaultRouter } from '.';
|
||||
|
||||
describe('test', () => {
|
||||
it('unbreaks the test runner', () => {
|
||||
expect(defaultRouter).toBeDefined();
|
||||
});
|
||||
});
|
||||
export type { AppConfig } from './types';
|
||||
export { loadConfig } from './loaders';
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { AppConfig } from './types';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'yaml';
|
||||
import { paths } from '../paths';
|
||||
|
||||
type LoadConfigOptions = {
|
||||
// Config path, defaults to app-config.yaml in project root
|
||||
configPath?: string;
|
||||
};
|
||||
|
||||
export async function loadConfig(
|
||||
options: LoadConfigOptions = {},
|
||||
): Promise<AppConfig[]> {
|
||||
// TODO: We'll want this to be a bit more elaborate, probably adding configs for
|
||||
// specific env, and maybe local config for plugins.
|
||||
const { configPath = paths.resolveTargetRoot('app-config.yaml') } = options;
|
||||
|
||||
try {
|
||||
const configYaml = await fs.readFile(configPath, 'utf8');
|
||||
const config = yaml.parse(configYaml);
|
||||
return [config];
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read static configuration file, ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type AppConfig = any;
|
||||
@@ -51,6 +51,12 @@ export function createConfig(
|
||||
);
|
||||
}
|
||||
|
||||
plugins.push(
|
||||
new webpack.EnvironmentPlugin({
|
||||
APP_CONFIG: options.appConfig,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
mode: isDev ? 'development' : 'production',
|
||||
profile: false,
|
||||
|
||||
@@ -15,16 +15,20 @@
|
||||
*/
|
||||
|
||||
import { BundlingPathsOptions } from './paths';
|
||||
import { AppConfig } from '../app-config';
|
||||
|
||||
export type BundlingOptions = {
|
||||
checksEnabled: boolean;
|
||||
isDev: boolean;
|
||||
appConfig: AppConfig[];
|
||||
};
|
||||
|
||||
export type ServeOptions = BundlingPathsOptions & {
|
||||
checksEnabled: boolean;
|
||||
appConfig: AppConfig[];
|
||||
};
|
||||
|
||||
export type BuildOptions = BundlingPathsOptions & {
|
||||
statsJsonEnabled: boolean;
|
||||
appConfig: AppConfig[];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
app:
|
||||
title: Scaffolded Backstage App
|
||||
|
||||
organization:
|
||||
name: Acme Corporation
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConfigApi, Config } from '../../definitions/ConfigApi';
|
||||
import { AppConfig } from '../../../app';
|
||||
|
||||
const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
|
||||
|
||||
@@ -62,6 +63,18 @@ function validateString(
|
||||
export class ConfigReader implements ConfigApi {
|
||||
static nullReader = new ConfigReader({});
|
||||
|
||||
static fromConfigs(configs: AppConfig[]): ConfigReader {
|
||||
if (configs.length === 0) {
|
||||
return new ConfigReader({});
|
||||
}
|
||||
|
||||
// Merge together all configs info a single config with recursive fallback
|
||||
// readers, giving the first config object in the array the highest priority.
|
||||
return configs.reduceRight((previousReader, nextConfig) => {
|
||||
return new ConfigReader(nextConfig, previousReader);
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly data: JsonObject,
|
||||
private readonly fallback?: ConfigApi,
|
||||
|
||||
@@ -150,7 +150,7 @@ export class PrivateAppImpl implements BackstageApp {
|
||||
const Provider: FC<{}> = ({ children }) => {
|
||||
// Keeping this synchronous when a config loader isn't set simplifies tests a lot
|
||||
const hasConfig = Boolean(this.configLoader);
|
||||
const config = useAsync(this.configLoader || (() => Promise.resolve({})));
|
||||
const config = useAsync(this.configLoader || (() => Promise.resolve([])));
|
||||
|
||||
let childNode = children;
|
||||
|
||||
@@ -164,7 +164,7 @@ export class PrivateAppImpl implements BackstageApp {
|
||||
|
||||
const appApis = ApiRegistry.from([
|
||||
[appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)],
|
||||
[configApiRef, new ConfigReader(config.value ?? {})],
|
||||
[configApiRef, ConfigReader.fromConfigs(config.value ?? [])],
|
||||
]);
|
||||
const apis = new ApiAggregator(this.apis, appApis);
|
||||
|
||||
|
||||
@@ -38,8 +38,11 @@ export type AppConfig = any;
|
||||
|
||||
/**
|
||||
* A function that loads in the App config that will be accessible via the ConfigApi.
|
||||
*
|
||||
* If multiple config objects are returned in the array, values in the earlier configs
|
||||
* will override later ones.
|
||||
*/
|
||||
export type AppConfigLoader = () => Promise<AppConfig>;
|
||||
export type AppConfigLoader = () => Promise<AppConfig[]>;
|
||||
|
||||
export type AppOptions = {
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { defaultConfigLoader } from './createApp';
|
||||
|
||||
describe('defaultConfigLoader', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.APP_CONFIG;
|
||||
});
|
||||
|
||||
it('loads static config', async () => {
|
||||
Object.defineProperty(process.env, 'APP_CONFIG', {
|
||||
configurable: true,
|
||||
value: [{ my: 'config' }, { my: 'override-config' }] as any,
|
||||
});
|
||||
const configs = await defaultConfigLoader();
|
||||
expect(configs).toEqual([{ my: 'config' }, { my: 'override-config' }]);
|
||||
});
|
||||
|
||||
it('loads runtime config', async () => {
|
||||
Object.defineProperty(process.env, 'APP_CONFIG', {
|
||||
configurable: true,
|
||||
value: [{ my: 'override-config' }, { my: 'config' }] as any,
|
||||
});
|
||||
const configs = await (defaultConfigLoader as any)(
|
||||
'{"my":"runtime-config"}',
|
||||
);
|
||||
expect(configs).toEqual([
|
||||
{ my: 'runtime-config' },
|
||||
{ my: 'override-config' },
|
||||
{ my: 'config' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails to load invalid missing config', async () => {
|
||||
await expect(defaultConfigLoader()).rejects.toThrow(
|
||||
'No static configuration provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to load invalid static config', async () => {
|
||||
Object.defineProperty(process.env, 'APP_CONFIG', {
|
||||
configurable: true,
|
||||
value: { my: 'invalid-config' } as any,
|
||||
});
|
||||
await expect(defaultConfigLoader()).rejects.toThrow(
|
||||
'Static configuration has invalid format',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to load bad runtime config', async () => {
|
||||
Object.defineProperty(process.env, 'APP_CONFIG', {
|
||||
configurable: true,
|
||||
value: [{ my: 'config' }] as any,
|
||||
});
|
||||
|
||||
await expect((defaultConfigLoader as any)('}')).rejects.toThrow(
|
||||
'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,8 @@ import privateExports, {
|
||||
ApiRegistry,
|
||||
defaultSystemIcons,
|
||||
BootErrorPageProps,
|
||||
AppConfigLoader,
|
||||
AppConfig,
|
||||
} from '@backstage/core-api';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
@@ -29,6 +31,43 @@ import { lightTheme, darkTheme } from '@backstage/theme';
|
||||
|
||||
const { PrivateAppImpl } = privateExports;
|
||||
|
||||
/**
|
||||
* The default config loader, which expects that config is available at compile-time
|
||||
* in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as
|
||||
* returned by the config loader.
|
||||
*
|
||||
* It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string,
|
||||
* which can be rewritten at runtime to contain an additional JSON config object.
|
||||
* If runtime config is present, it will be placed first in the config array, overriding
|
||||
* other config values.
|
||||
*/
|
||||
export const defaultConfigLoader: AppConfigLoader = async (
|
||||
// This string may be replaced at runtime to provide additional config.
|
||||
// It should be replaced by a JSON-serialized config object.
|
||||
// It's a param so we can test it, but at runtime this will always fall back to default.
|
||||
runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__',
|
||||
) => {
|
||||
const appConfig = process.env.APP_CONFIG;
|
||||
if (!appConfig) {
|
||||
throw new Error('No static configuration provided');
|
||||
}
|
||||
if (!Array.isArray(appConfig)) {
|
||||
throw new Error('Static configuration has invalid format');
|
||||
}
|
||||
const configs = (appConfig.slice() as unknown) as AppConfig[];
|
||||
|
||||
// Avoiding this string also being replaced at runtime
|
||||
if (runtimeConfigJson !== '__app_injected_runtime_config__'.toUpperCase()) {
|
||||
try {
|
||||
configs.unshift(JSON.parse(runtimeConfigJson));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load runtime configuration, ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
// createApp is defined in core, and not core-api, since we need access
|
||||
// to the components inside core to provide defaults.
|
||||
// The actual implementation of the app class still lives in core-api,
|
||||
@@ -77,7 +116,7 @@ export function createApp(options?: AppOptions) {
|
||||
theme: darkTheme,
|
||||
},
|
||||
];
|
||||
const configLoader = options?.configLoader ?? (async () => ({}));
|
||||
const configLoader = options?.configLoader ?? defaultConfigLoader;
|
||||
|
||||
const app = new PrivateAppImpl({
|
||||
apis,
|
||||
|
||||
@@ -19,6 +19,14 @@ read -r AUTH_GOOGLE_CLIENT_SECRET
|
||||
export AUTH_GOOGLE_CLIENT_SECRET
|
||||
run `yarn start` in packages/backend folder
|
||||
|
||||
### SAML
|
||||
|
||||
To try out SAML, you can use the mock identity provider:
|
||||
|
||||
```bash
|
||||
./scripts/start-saml-idp.sh
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- (The Backstage homepage)[https://backstage.io]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@types/passport": "^1.0.3",
|
||||
"@types/passport-github2": "^1.2.4",
|
||||
"@types/passport-google-oauth20": "^2.0.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
@@ -31,11 +32,14 @@
|
||||
"passport": "^0.4.1",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-saml": "^1.3.3",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.6",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/passport-saml": "^1.1.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.pem
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
|
||||
cd "$DIR"
|
||||
|
||||
if [[ ! -f idp-public-cert.pem ]]; then
|
||||
echo "Generating new SAML Certificates"
|
||||
openssl req \
|
||||
-x509 \
|
||||
-newkey rsa:1024 \
|
||||
-days 3650 \
|
||||
-nodes \
|
||||
-subj '/CN=localhost' \
|
||||
-keyout "idp-private-key.pem" \
|
||||
-out "idp-public-cert.pem"
|
||||
fi
|
||||
|
||||
echo "Downloading and starting SAML-IdP"
|
||||
export NPM_CONFIG_REGISTRY=https://registry.npmjs.org
|
||||
exec npx saml-idp --acsUrl "http://localhost:7000/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001
|
||||
@@ -55,7 +55,7 @@ export const executeFrameHandlerStrategy = async (
|
||||
reject(new Error('Unexpected redirect'));
|
||||
};
|
||||
|
||||
strategy.authenticate(req);
|
||||
strategy.authenticate(req, {});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -32,4 +32,12 @@ export const providers = [
|
||||
},
|
||||
disableRefresh: true,
|
||||
},
|
||||
{
|
||||
provider: 'saml',
|
||||
options: {
|
||||
path: '/auth/saml/handler/frame',
|
||||
entryPoint: 'http://localhost:7001/',
|
||||
issuer: 'passport-saml',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,37 +14,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthProviderFactories,
|
||||
AuthProviderRouteHandlers,
|
||||
AuthProviderConfig,
|
||||
} from './types';
|
||||
import { GoogleAuthProvider } from './google';
|
||||
import { GithubAuthProvider } from './github';
|
||||
import { OAuthProvider } from './OAuthProvider';
|
||||
import Router from 'express-promise-router';
|
||||
import { createGithubProvider } from './github';
|
||||
import { createGoogleProvider } from './google';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { AuthProviderFactory, AuthProviderConfig } from './types';
|
||||
|
||||
export class ProviderFactories {
|
||||
private static readonly providerFactories: AuthProviderFactories = {
|
||||
google: GoogleAuthProvider,
|
||||
github: GithubAuthProvider,
|
||||
};
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
github: createGithubProvider,
|
||||
saml: createSamlProvider,
|
||||
};
|
||||
|
||||
public static getProviderFactory(
|
||||
config: AuthProviderConfig,
|
||||
): AuthProviderRouteHandlers {
|
||||
const providerId = config.provider;
|
||||
const ProviderImpl = ProviderFactories.providerFactories[providerId];
|
||||
if (!ProviderImpl) {
|
||||
throw Error(
|
||||
`Provider Implementation missing for : ${providerId} auth provider`,
|
||||
);
|
||||
}
|
||||
const providerInstance = new ProviderImpl(config);
|
||||
const oauthProvider = new OAuthProvider(
|
||||
providerInstance,
|
||||
providerId,
|
||||
config.disableRefresh,
|
||||
);
|
||||
return oauthProvider;
|
||||
export function createAuthProvider(providerId: string, config: any) {
|
||||
const factory = factories[providerId];
|
||||
if (!factory) {
|
||||
throw Error(`No auth provider available for '${providerId}'`);
|
||||
}
|
||||
return factory(config);
|
||||
}
|
||||
|
||||
export const createAuthProviderRouter = (config: AuthProviderConfig) => {
|
||||
const providerId = config.provider;
|
||||
const provider = createAuthProvider(providerId, config);
|
||||
|
||||
const router = Router();
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
router.get('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.post('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.get('/logout', provider.logout.bind(provider));
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
}
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { GithubAuthProvider } from './provider';
|
||||
export { createGithubProvider } from './provider';
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
AuthInfoBase,
|
||||
AuthInfoPrivate,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../OAuthProvider';
|
||||
|
||||
export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
@@ -57,3 +58,9 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
return await executeFrameHandlerStrategy(req, this._strategy);
|
||||
}
|
||||
}
|
||||
|
||||
export function createGithubProvider(config: AuthProviderConfig) {
|
||||
const provider = new GithubAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider, true);
|
||||
return oauthProvider;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { GoogleAuthProvider } from './provider';
|
||||
export { createGoogleProvider } from './provider';
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../OAuthProvider';
|
||||
|
||||
export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
@@ -87,3 +88,9 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createGoogleProvider(config: AuthProviderConfig) {
|
||||
const provider = new GoogleAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider);
|
||||
return oauthProvider;
|
||||
}
|
||||
|
||||
@@ -14,24 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Router from 'express-promise-router';
|
||||
import { AuthProviderRouteHandlers, AuthProviderConfig } from './types';
|
||||
import { ProviderFactories } from './factories';
|
||||
|
||||
export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
|
||||
const router = Router();
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
router.get('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.get('/logout', provider.logout.bind(provider));
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
}
|
||||
return router;
|
||||
};
|
||||
|
||||
export const makeProvider = (config: AuthProviderConfig) => {
|
||||
const providerId = config.provider;
|
||||
const oauthProvider = ProviderFactories.getProviderFactory(config);
|
||||
const providerRouter = defaultRouter(oauthProvider);
|
||||
return { providerId, providerRouter };
|
||||
};
|
||||
export { createAuthProviderRouter } from './factories';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { createSamlProvider } from './provider';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 express from 'express';
|
||||
import { Strategy as SamlStrategy } from 'passport-saml';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
} from '../PassportStrategyHelper';
|
||||
import { AuthProviderConfig, AuthProviderRouteHandlers } from '../types';
|
||||
import { postMessageResponse } from '../OAuthProvider';
|
||||
|
||||
export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly strategy: SamlStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.strategy = new SamlStrategy(
|
||||
{ ...providerConfig.options },
|
||||
(profile: any, done: any) => {
|
||||
// TODO: There's plenty more validation and profile handling to do here,
|
||||
// this provider is currently only intended to validate the provider pattern
|
||||
// for non-oauth auth flows.
|
||||
// TODO: This flow doesn't issue an identity token that can be used to validate
|
||||
// the identity of the user in other backends, which we need in some form.
|
||||
done(undefined, {
|
||||
email: profile.email,
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
displayName: profile.displayName,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async start(req: express.Request, res: express.Response): Promise<any> {
|
||||
const { url } = await executeRedirectStrategy(req, this.strategy, {});
|
||||
res.redirect(url);
|
||||
}
|
||||
|
||||
async frameHandler(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): Promise<any> {
|
||||
try {
|
||||
const { user } = await executeFrameHandlerStrategy(req, this.strategy);
|
||||
|
||||
return postMessageResponse(res, {
|
||||
type: 'auth-result',
|
||||
payload: user,
|
||||
});
|
||||
} catch (error) {
|
||||
return postMessageResponse(res, {
|
||||
type: 'auth-result',
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async logout(_req: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('noop');
|
||||
}
|
||||
}
|
||||
|
||||
export function createSamlProvider(config: AuthProviderConfig) {
|
||||
return new SamlAuthProvider(config);
|
||||
}
|
||||
@@ -37,13 +37,9 @@ export interface AuthProviderRouteHandlers {
|
||||
logout(req: express.Request, res: express.Response): Promise<any>;
|
||||
}
|
||||
|
||||
export type AuthProviderFactories = {
|
||||
[key: string]: AuthProviderFactory;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = {
|
||||
new (providerConfig: any): OAuthProviderHandlers;
|
||||
};
|
||||
export type AuthProviderFactory = (
|
||||
config: AuthProviderConfig,
|
||||
) => AuthProviderRouteHandlers;
|
||||
|
||||
export type AuthInfoBase = {
|
||||
accessToken: string;
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import bodyParser from 'body-parser';
|
||||
import { Logger } from 'winston';
|
||||
import { providers } from './../providers/config';
|
||||
import { makeProvider } from '../providers';
|
||||
import { createAuthProviderRouter } from '../providers';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
@@ -32,12 +33,15 @@ export async function createRouter(
|
||||
const logger = options.logger.child({ plugin: 'auth' });
|
||||
|
||||
router.use(cookieParser());
|
||||
router.use(bodyParser.urlencoded({ extended: false }));
|
||||
router.use(bodyParser.json());
|
||||
|
||||
// configure all the providers
|
||||
for (const providerConfig of providers) {
|
||||
const { providerId, providerRouter } = makeProvider(providerConfig);
|
||||
logger.info(`Configuring provider, ${providerId}`);
|
||||
router.use(`/${providerId}`, providerRouter);
|
||||
const { provider } = providerConfig;
|
||||
const providerRouter = createAuthProviderRouter(providerConfig);
|
||||
logger.info(`Configuring provider, ${provider}`);
|
||||
router.use(`/${provider}`, providerRouter);
|
||||
}
|
||||
|
||||
return router;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon -r esm dist/run.js\"",
|
||||
"start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"cross-env NODE_ENV=development nodemon -r esm dist/run.js\\\"",
|
||||
"build": "tsc",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
|
||||
@@ -38,8 +38,8 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
|
||||
|
||||
async entityByName(
|
||||
kind: string,
|
||||
name: string,
|
||||
namespace: string | undefined,
|
||||
name: string,
|
||||
): Promise<Entity | undefined> {
|
||||
return await this.database.transaction(tx =>
|
||||
this.entityByNameInternal(tx, kind, name, namespace),
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
import { Location } from '@backstage/catalog-model';
|
||||
import type { Database } from '../database';
|
||||
import { DatabaseLocationUpdateLogEvent } from '../database/types';
|
||||
import {
|
||||
DatabaseLocationUpdateLogEvent,
|
||||
DatabaseLocationUpdateLogStatus,
|
||||
} from '../database/types';
|
||||
import { LocationResponse, LocationsCatalog } from './types';
|
||||
|
||||
export class DatabaseLocationsCatalog implements LocationsCatalog {
|
||||
@@ -63,4 +66,28 @@ export class DatabaseLocationsCatalog implements LocationsCatalog {
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async logUpdateSuccess(
|
||||
locationId: string,
|
||||
entityName?: string,
|
||||
): Promise<void> {
|
||||
await this.database.addLocationUpdateLogEvent(
|
||||
locationId,
|
||||
DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
entityName,
|
||||
);
|
||||
}
|
||||
|
||||
async logUpdateFailure(
|
||||
locationId: string,
|
||||
error?: Error,
|
||||
entityName?: string,
|
||||
): Promise<void> {
|
||||
await this.database.addLocationUpdateLogEvent(
|
||||
locationId,
|
||||
DatabaseLocationUpdateLogStatus.FAIL,
|
||||
entityName,
|
||||
error?.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,10 @@ export type LocationsCatalog = {
|
||||
locations(): Promise<LocationResponse[]>;
|
||||
location(id: string): Promise<LocationResponse>;
|
||||
locationHistory(id: string): Promise<LocationUpdateLogEvent[]>;
|
||||
logUpdateSuccess(locationId: string, entityName?: string): Promise<void>;
|
||||
logUpdateFailure(
|
||||
locationId: string,
|
||||
error?: Error,
|
||||
entityName?: string,
|
||||
): Promise<void>;
|
||||
};
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 } from '@backstage/backend-common';
|
||||
import type { Entity, EntityPolicy } from '@backstage/catalog-model';
|
||||
import Knex from 'knex';
|
||||
import type { IngestionModel } from '../ingestion/types';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
import { DatabaseLocationUpdateLogStatus } from './types';
|
||||
import type {
|
||||
Database,
|
||||
DbLocationsRow,
|
||||
DbLocationsRowWithStatus,
|
||||
} from './types';
|
||||
|
||||
describe('DatabaseManager', () => {
|
||||
describe('refreshLocations', () => {
|
||||
it('works with no locations added', async () => {
|
||||
const db = ({
|
||||
locations: jest.fn().mockResolvedValue([]),
|
||||
} as unknown) as Database;
|
||||
const reader: IngestionModel = {
|
||||
readLocation: jest.fn(),
|
||||
};
|
||||
const policy: EntityPolicy = {
|
||||
enforce: jest.fn(),
|
||||
};
|
||||
|
||||
await expect(
|
||||
DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()),
|
||||
).resolves.toBeUndefined();
|
||||
expect(reader.readLocation).not.toHaveBeenCalled();
|
||||
expect(policy.enforce).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can update a single location', async () => {
|
||||
const location: DbLocationsRowWithStatus = {
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
message: '',
|
||||
status: DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
timestamp: new Date(314159265).toISOString(),
|
||||
};
|
||||
const desc: Entity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'c1' },
|
||||
spec: { type: 'service' },
|
||||
};
|
||||
|
||||
const tx = (undefined as unknown) as Knex.Transaction<any, any>;
|
||||
|
||||
const db = ({
|
||||
transaction: jest.fn(f => f(tx)),
|
||||
entity: jest.fn(() => Promise.resolve(undefined)),
|
||||
addEntity: jest.fn(),
|
||||
locations: jest.fn(() => Promise.resolve([location])),
|
||||
addLocationUpdateLogEvent: jest.fn(),
|
||||
} as Partial<Database>) as Database;
|
||||
|
||||
const reader: IngestionModel = {
|
||||
readLocation: jest.fn(() =>
|
||||
Promise.resolve([{ type: 'data', data: desc }]),
|
||||
),
|
||||
};
|
||||
const policy: EntityPolicy = {
|
||||
enforce: jest.fn(() => Promise.resolve(desc)),
|
||||
};
|
||||
|
||||
await expect(
|
||||
DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()),
|
||||
).resolves.toBeUndefined();
|
||||
expect(reader.readLocation).toHaveBeenCalledTimes(1);
|
||||
expect(reader.readLocation).toHaveBeenNthCalledWith(1, 'some', 'thing');
|
||||
expect(db.addEntity).toHaveBeenCalledTimes(1);
|
||||
expect(db.addEntity).toHaveBeenNthCalledWith(1, undefined, {
|
||||
locationId: '123',
|
||||
entity: expect.objectContaining({
|
||||
metadata: expect.objectContaining({ name: 'c1' }),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('logs successful updates', async () => {
|
||||
const tx = (undefined as unknown) as Knex.Transaction<any, any>;
|
||||
|
||||
const db = ({
|
||||
transaction: jest.fn(f => f(tx)),
|
||||
addEntity: jest.fn(),
|
||||
entity: jest.fn(() => Promise.resolve(undefined)),
|
||||
locations: jest.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
} as DbLocationsRow,
|
||||
]),
|
||||
),
|
||||
addLocationUpdateLogEvent: jest.fn(),
|
||||
} as unknown) as Database;
|
||||
|
||||
const desc: Entity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'c1' },
|
||||
spec: { type: 'service' },
|
||||
};
|
||||
const reader: IngestionModel = {
|
||||
readLocation: jest.fn(() =>
|
||||
Promise.resolve([{ type: 'data', data: desc }]),
|
||||
),
|
||||
};
|
||||
const policy: EntityPolicy = {
|
||||
enforce: jest.fn(() => Promise.resolve(desc)),
|
||||
};
|
||||
|
||||
await expect(
|
||||
DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'123',
|
||||
DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
'c1',
|
||||
);
|
||||
|
||||
expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'123',
|
||||
DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('logs unsuccessful updates when parser fails', async () => {
|
||||
const tx = (undefined as unknown) as Knex.Transaction<any, any>;
|
||||
|
||||
const db = ({
|
||||
transaction: jest.fn(f => f(tx)),
|
||||
addEntity: jest.fn(),
|
||||
locations: jest.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
} as DbLocationsRow,
|
||||
]),
|
||||
),
|
||||
addLocationUpdateLogEvent: jest.fn(),
|
||||
} as unknown) as Database;
|
||||
|
||||
const desc: Entity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'c1' },
|
||||
spec: { type: 'service' },
|
||||
};
|
||||
const reader: IngestionModel = {
|
||||
readLocation: jest.fn(() =>
|
||||
Promise.resolve([{ type: 'data', data: desc }]),
|
||||
),
|
||||
};
|
||||
const policy: EntityPolicy = {
|
||||
enforce: jest.fn(() =>
|
||||
Promise.reject(new Error('parser error message')),
|
||||
),
|
||||
};
|
||||
|
||||
await expect(
|
||||
DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'123',
|
||||
DatabaseLocationUpdateLogStatus.FAIL,
|
||||
'c1',
|
||||
'parser error message',
|
||||
);
|
||||
|
||||
expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'123',
|
||||
DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('logs unsuccessful updates when reader fails', async () => {
|
||||
const tx = (undefined as unknown) as Knex.Transaction<any, any>;
|
||||
|
||||
const db = ({
|
||||
transaction: jest.fn(f => f(tx)),
|
||||
addEntity: jest.fn(),
|
||||
locations: jest.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
} as DbLocationsRow,
|
||||
]),
|
||||
),
|
||||
addLocationUpdateLogEvent: jest.fn(),
|
||||
} as unknown) as Database;
|
||||
|
||||
const reader: IngestionModel = {
|
||||
readLocation: jest.fn(() =>
|
||||
Promise.reject([{ type: 'error', error: new Error('test message') }]),
|
||||
),
|
||||
};
|
||||
const policy: EntityPolicy = {
|
||||
enforce: jest.fn(() =>
|
||||
Promise.reject(new Error('parser error message')),
|
||||
),
|
||||
};
|
||||
|
||||
await expect(
|
||||
DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'123',
|
||||
DatabaseLocationUpdateLogStatus.FAIL,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,18 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityPolicy } from '@backstage/catalog-model';
|
||||
import Knex from 'knex';
|
||||
import lodash from 'lodash';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
import { IngestionModel } from '../ingestion/types';
|
||||
import { CommonDatabase } from './CommonDatabase';
|
||||
import {
|
||||
DatabaseLocationUpdateLogStatus,
|
||||
Database,
|
||||
DbEntityRequest,
|
||||
} from './types';
|
||||
import { Database } from './types';
|
||||
|
||||
export class DatabaseManager {
|
||||
public static async createDatabase(
|
||||
@@ -57,182 +50,4 @@ export class DatabaseManager {
|
||||
});
|
||||
return new CommonDatabase(database, logger);
|
||||
}
|
||||
|
||||
private static async logUpdateSuccess(
|
||||
database: Database,
|
||||
locationId: string,
|
||||
entityName?: string,
|
||||
) {
|
||||
return database.addLocationUpdateLogEvent(
|
||||
locationId,
|
||||
DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
entityName,
|
||||
);
|
||||
}
|
||||
|
||||
private static async logUpdateFailure(
|
||||
database: Database,
|
||||
locationId: string,
|
||||
error?: Error,
|
||||
entityName?: string,
|
||||
) {
|
||||
return database.addLocationUpdateLogEvent(
|
||||
locationId,
|
||||
DatabaseLocationUpdateLogStatus.FAIL,
|
||||
entityName,
|
||||
error?.message,
|
||||
);
|
||||
}
|
||||
|
||||
public static async refreshLocations(
|
||||
database: Database,
|
||||
ingestionModel: IngestionModel,
|
||||
entityPolicy: EntityPolicy,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
const locations = await database.locations();
|
||||
for (const location of locations) {
|
||||
try {
|
||||
logger.debug(
|
||||
`Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`,
|
||||
);
|
||||
|
||||
const readerOutput = await ingestionModel.readLocation(
|
||||
location.type,
|
||||
location.target,
|
||||
);
|
||||
|
||||
for (const readerItem of readerOutput) {
|
||||
if (readerItem.type === 'error') {
|
||||
logger.info(readerItem.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const entity = await entityPolicy.enforce(readerItem.data);
|
||||
await DatabaseManager.refreshSingleEntity(
|
||||
database,
|
||||
location.id,
|
||||
entity,
|
||||
logger,
|
||||
);
|
||||
await DatabaseManager.logUpdateSuccess(
|
||||
database,
|
||||
location.id,
|
||||
entity.metadata.name,
|
||||
);
|
||||
} catch (error) {
|
||||
await DatabaseManager.logUpdateFailure(
|
||||
database,
|
||||
location.id,
|
||||
error,
|
||||
readerItem.data.metadata.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
await DatabaseManager.logUpdateSuccess(
|
||||
database,
|
||||
location.id,
|
||||
undefined,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`Failed to refresh location id="${location.id}", ${error}`,
|
||||
);
|
||||
await DatabaseManager.logUpdateFailure(database, location.id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async refreshSingleEntity(
|
||||
database: Database,
|
||||
locationId: string,
|
||||
entity: Entity,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
const { kind } = entity;
|
||||
const { name, namespace } = entity.metadata || {};
|
||||
if (!name) {
|
||||
throw new Error('Entities without names are not yet supported');
|
||||
}
|
||||
|
||||
const request: DbEntityRequest = {
|
||||
locationId: locationId,
|
||||
entity: entity,
|
||||
};
|
||||
|
||||
logger.debug(
|
||||
`Read entity kind="${kind}" name="${name}" namespace="${namespace}"`,
|
||||
);
|
||||
|
||||
await database.transaction(async tx => {
|
||||
const previous = await database.entity(tx, kind, name, namespace);
|
||||
if (!previous) {
|
||||
logger.debug(`No such entity found, adding`);
|
||||
await database.addEntity(tx, request);
|
||||
} else if (
|
||||
!DatabaseManager.entitiesAreEqual(previous.entity, request.entity)
|
||||
) {
|
||||
logger.debug(`Different from existing entity, updating`);
|
||||
await database.updateEntity(tx, request);
|
||||
} else {
|
||||
logger.debug(`Equal to existing entity, skipping update`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static entitiesAreEqual(previous: Entity, next: Entity) {
|
||||
if (
|
||||
previous.apiVersion !== next.apiVersion ||
|
||||
previous.kind !== next.kind ||
|
||||
!lodash.isEqual(previous.spec, next.spec) // Accept that {} !== undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since the next annotations get merged into the previous, extract only
|
||||
// the overlapping keys and check if their values match.
|
||||
if (next.metadata.annotations) {
|
||||
if (!previous.metadata.annotations) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!lodash.isEqual(
|
||||
next.metadata.annotations,
|
||||
lodash.pick(
|
||||
previous.metadata.annotations,
|
||||
Object.keys(next.metadata.annotations),
|
||||
),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const e1 = lodash.cloneDeep(previous);
|
||||
const e2 = lodash.cloneDeep(next);
|
||||
|
||||
if (!e1.metadata.labels) {
|
||||
e1.metadata.labels = {};
|
||||
}
|
||||
if (!e2.metadata.labels) {
|
||||
e2.metadata.labels = {};
|
||||
}
|
||||
|
||||
// Remove generated fields
|
||||
delete e1.metadata.uid;
|
||||
delete e1.metadata.etag;
|
||||
delete e1.metadata.generation;
|
||||
delete e2.metadata.uid;
|
||||
delete e2.metadata.etag;
|
||||
delete e2.metadata.generation;
|
||||
|
||||
// Remove already compared things
|
||||
delete e1.metadata.annotations;
|
||||
delete e1.spec;
|
||||
delete e2.metadata.annotations;
|
||||
delete e2.spec;
|
||||
|
||||
return lodash.isEqual(e1, e2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { Entity, Location } from '@backstage/catalog-model';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { IngestionModel } from './types';
|
||||
import { LocationUpdateStatus } from '../catalog/types';
|
||||
import { DatabaseLocationUpdateLogStatus } from '../database/types';
|
||||
import { HigherOrderOperations } from './HigherOrderOperations';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { IngestionModel } from './types';
|
||||
|
||||
describe('HigherOrderOperations', () => {
|
||||
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
|
||||
@@ -39,6 +42,8 @@ describe('HigherOrderOperations', () => {
|
||||
locations: jest.fn(),
|
||||
location: jest.fn(),
|
||||
locationHistory: jest.fn(),
|
||||
logUpdateSuccess: jest.fn(),
|
||||
logUpdateFailure: jest.fn(),
|
||||
};
|
||||
ingestionModel = {
|
||||
readLocation: jest.fn(),
|
||||
@@ -47,6 +52,7 @@ describe('HigherOrderOperations', () => {
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
ingestionModel,
|
||||
getVoidLogger(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -140,4 +146,147 @@ describe('HigherOrderOperations', () => {
|
||||
expect(locationsCatalog.addLocation).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshLocations', () => {
|
||||
it('works with no locations added', async () => {
|
||||
locationsCatalog.locations.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
higherOrderOperation.refreshAllLocations(),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(locationsCatalog.locations).toHaveBeenCalledTimes(1);
|
||||
expect(ingestionModel.readLocation).not.toHaveBeenCalled();
|
||||
expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can update a single location where a matching entity did not exist', async () => {
|
||||
const locationStatus: LocationUpdateStatus = {
|
||||
message: '',
|
||||
status: DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
timestamp: new Date(314159265).toISOString(),
|
||||
};
|
||||
const location: Location = {
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
};
|
||||
const desc: Entity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'c1' },
|
||||
spec: { type: 'service' },
|
||||
};
|
||||
|
||||
locationsCatalog.locations.mockResolvedValue([
|
||||
{ currentStatus: locationStatus, data: location },
|
||||
]);
|
||||
ingestionModel.readLocation.mockResolvedValue([
|
||||
{ type: 'data', data: desc },
|
||||
]);
|
||||
entitiesCatalog.entityByName.mockResolvedValue(undefined);
|
||||
entitiesCatalog.addOrUpdateEntity.mockResolvedValue(desc);
|
||||
|
||||
await expect(
|
||||
higherOrderOperation.refreshAllLocations(),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(locationsCatalog.locations).toHaveBeenCalledTimes(1);
|
||||
expect(ingestionModel.readLocation).toHaveBeenCalledTimes(1);
|
||||
expect(ingestionModel.readLocation).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'some',
|
||||
'thing',
|
||||
);
|
||||
expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entityByName).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'Component',
|
||||
undefined,
|
||||
'c1',
|
||||
);
|
||||
expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({ name: 'c1' }),
|
||||
}),
|
||||
'123',
|
||||
);
|
||||
});
|
||||
|
||||
it('logs successful updates', async () => {
|
||||
const locationStatus: LocationUpdateStatus = {
|
||||
message: '',
|
||||
status: DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
timestamp: new Date(314159265).toISOString(),
|
||||
};
|
||||
const location: Location = {
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
};
|
||||
const desc: Entity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'c1' },
|
||||
spec: { type: 'service' },
|
||||
};
|
||||
|
||||
locationsCatalog.locations.mockResolvedValue([
|
||||
{ currentStatus: locationStatus, data: location },
|
||||
]);
|
||||
ingestionModel.readLocation.mockResolvedValue([
|
||||
{ type: 'data', data: desc },
|
||||
]);
|
||||
entitiesCatalog.entityByName.mockResolvedValue(undefined);
|
||||
entitiesCatalog.addOrUpdateEntity.mockResolvedValue(desc);
|
||||
|
||||
await expect(
|
||||
higherOrderOperation.refreshAllLocations(),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledTimes(2);
|
||||
expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledWith(
|
||||
'123',
|
||||
undefined,
|
||||
);
|
||||
expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledWith(
|
||||
'123',
|
||||
'c1',
|
||||
);
|
||||
});
|
||||
|
||||
it('logs unsuccessful updates when reader fails', async () => {
|
||||
const locationStatus: LocationUpdateStatus = {
|
||||
message: '',
|
||||
status: DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
timestamp: new Date(314159265).toISOString(),
|
||||
};
|
||||
const location: Location = {
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
};
|
||||
|
||||
locationsCatalog.locations.mockResolvedValue([
|
||||
{ currentStatus: locationStatus, data: location },
|
||||
]);
|
||||
ingestionModel.readLocation.mockRejectedValue(
|
||||
new Error('reader error message'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
higherOrderOperation.refreshAllLocations(),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(ingestionModel.readLocation).toHaveBeenCalledTimes(1);
|
||||
expect(locationsCatalog.logUpdateFailure).toHaveBeenCalledTimes(1);
|
||||
expect(locationsCatalog.logUpdateSuccess).not.toHaveBeenCalled();
|
||||
expect(locationsCatalog.logUpdateFailure).toHaveBeenCalledWith(
|
||||
'123',
|
||||
expect.objectContaining({ message: 'reader error message' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { Entity, Location, LocationSpec } from '@backstage/catalog-model';
|
||||
import lodash from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { IngestionModel } from '../ingestion';
|
||||
import { AddLocationResult, HigherOrderOperation } from './types';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
const LOCATION_ANNOTATION = 'backstage.io/managed-by-location';
|
||||
|
||||
@@ -34,15 +36,18 @@ export class HigherOrderOperations implements HigherOrderOperation {
|
||||
private readonly entitiesCatalog: EntitiesCatalog;
|
||||
private readonly locationsCatalog: LocationsCatalog;
|
||||
private readonly ingestionModel: IngestionModel;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
entitiesCatalog: EntitiesCatalog,
|
||||
locationsCatalog: LocationsCatalog,
|
||||
ingestionModel: IngestionModel,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.entitiesCatalog = entitiesCatalog;
|
||||
this.locationsCatalog = locationsCatalog;
|
||||
this.ingestionModel = ingestionModel;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,4 +116,154 @@ export class HigherOrderOperations implements HigherOrderOperation {
|
||||
|
||||
return { location, entities: outputEntities };
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through all registered locations, and performs a refresh of each one.
|
||||
*
|
||||
* Entities are read from their respective sources, are parsed and validated
|
||||
* according to the entity policy, and get inserted or updated in the catalog.
|
||||
* Entities that have disappeared from their location are left orphaned,
|
||||
* without changes.
|
||||
*/
|
||||
async refreshAllLocations(): Promise<void> {
|
||||
const startTimestamp = new Date().valueOf();
|
||||
this.logger.info('Beginning locations refresh');
|
||||
|
||||
const locations = await this.locationsCatalog.locations();
|
||||
this.logger.info(`Visiting ${locations.length} locations`);
|
||||
|
||||
for (const { data: location } of locations) {
|
||||
this.logger.debug(
|
||||
`Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`,
|
||||
);
|
||||
try {
|
||||
await this.refreshSingleLocation(location);
|
||||
await this.locationsCatalog.logUpdateSuccess(location.id, undefined);
|
||||
} catch (e) {
|
||||
this.logger.debug(
|
||||
`Failed to refresh location id="${location.id}" type="${location.type}" target="${location.target}", ${e}`,
|
||||
);
|
||||
await this.locationsCatalog.logUpdateFailure(location.id, e);
|
||||
}
|
||||
}
|
||||
|
||||
const endTimestamp = new Date().valueOf();
|
||||
const duration = ((endTimestamp - startTimestamp) / 1000).toFixed(1);
|
||||
this.logger.debug(`Completed locations refresh in ${duration} seconds`);
|
||||
}
|
||||
|
||||
// Performs a full refresh of a single location
|
||||
private async refreshSingleLocation(location: Location) {
|
||||
const readerOutput = await this.ingestionModel.readLocation(
|
||||
location.type,
|
||||
location.target,
|
||||
);
|
||||
|
||||
for (const readerItem of readerOutput) {
|
||||
if (readerItem.type === 'error') {
|
||||
this.logger.debug(
|
||||
`Failed item in location id="${location.id}" type="${location.type}" target="${location.target}", ${readerItem.error}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const entity = readerItem.data;
|
||||
this.logger.debug(
|
||||
`Read entity kind="${entity.kind}" name="${
|
||||
entity.metadata.name
|
||||
}" namespace="${entity.metadata.namespace || ''}"`,
|
||||
);
|
||||
|
||||
try {
|
||||
const previous = await this.entitiesCatalog.entityByName(
|
||||
entity.kind,
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
);
|
||||
|
||||
if (!previous) {
|
||||
this.logger.debug(`No such entity found, adding`);
|
||||
await this.entitiesCatalog.addOrUpdateEntity(entity, location.id);
|
||||
} else if (!this.entitiesAreEqual(previous, entity)) {
|
||||
this.logger.debug(`Different from existing entity, updating`);
|
||||
await this.entitiesCatalog.addOrUpdateEntity(entity, location.id);
|
||||
} else {
|
||||
this.logger.debug(`Equal to existing entity, skipping update`);
|
||||
}
|
||||
|
||||
await this.locationsCatalog.logUpdateSuccess(
|
||||
location.id,
|
||||
entity.metadata.name,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.debug(
|
||||
`Failed refresh of entity kind="${entity.kind}" name="${
|
||||
entity.metadata.name
|
||||
}" namespace="${entity.metadata.namespace || ''}", ${error}`,
|
||||
);
|
||||
|
||||
await this.locationsCatalog.logUpdateFailure(
|
||||
location.id,
|
||||
error,
|
||||
entity.metadata.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compares entities, ignoring generated and irrelevant data
|
||||
private entitiesAreEqual(previous: Entity, next: Entity): boolean {
|
||||
if (
|
||||
previous.apiVersion !== next.apiVersion ||
|
||||
previous.kind !== next.kind ||
|
||||
!lodash.isEqual(previous.spec, next.spec) // Accept that {} !== undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since the next annotations get merged into the previous, extract only
|
||||
// the overlapping keys and check if their values match.
|
||||
if (next.metadata.annotations) {
|
||||
if (!previous.metadata.annotations) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!lodash.isEqual(
|
||||
next.metadata.annotations,
|
||||
lodash.pick(
|
||||
previous.metadata.annotations,
|
||||
Object.keys(next.metadata.annotations),
|
||||
),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const e1 = lodash.cloneDeep(previous);
|
||||
const e2 = lodash.cloneDeep(next);
|
||||
|
||||
if (!e1.metadata.labels) {
|
||||
e1.metadata.labels = {};
|
||||
}
|
||||
if (!e2.metadata.labels) {
|
||||
e2.metadata.labels = {};
|
||||
}
|
||||
|
||||
// Remove generated fields
|
||||
delete e1.metadata.uid;
|
||||
delete e1.metadata.etag;
|
||||
delete e1.metadata.generation;
|
||||
delete e2.metadata.uid;
|
||||
delete e2.metadata.etag;
|
||||
delete e2.metadata.generation;
|
||||
|
||||
// Remove already compared things
|
||||
delete e1.metadata.annotations;
|
||||
delete e1.spec;
|
||||
delete e2.metadata.annotations;
|
||||
delete e2.spec;
|
||||
|
||||
return lodash.isEqual(e1, e2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,4 @@ export * from './descriptor';
|
||||
export { HigherOrderOperations } from './HigherOrderOperations';
|
||||
export { IngestionModels } from './IngestionModels';
|
||||
export * from './source';
|
||||
export type { IngestionModel } from './types';
|
||||
export type { HigherOrderOperation, IngestionModel } from './types';
|
||||
|
||||
@@ -28,4 +28,5 @@ export type IngestionModel = {
|
||||
|
||||
export type HigherOrderOperation = {
|
||||
addLocation(spec: LocationSpec): Promise<AddLocationResult>;
|
||||
refreshAllLocations(): Promise<void>;
|
||||
};
|
||||
|
||||
@@ -43,9 +43,12 @@ describe('createRouter', () => {
|
||||
locations: jest.fn(),
|
||||
location: jest.fn(),
|
||||
locationHistory: jest.fn(),
|
||||
logUpdateSuccess: jest.fn(),
|
||||
logUpdateFailure: jest.fn(),
|
||||
};
|
||||
higherOrderOperation = {
|
||||
addLocation: jest.fn(),
|
||||
refreshAllLocations: jest.fn(),
|
||||
};
|
||||
const router = await createRouter({
|
||||
entitiesCatalog,
|
||||
@@ -78,6 +81,7 @@ describe('createRouter', () => {
|
||||
const response = await request(app).get('/entities?a=1&a=&a=3&b=4&c=');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entities).toHaveBeenCalledWith([
|
||||
{ key: 'a', values: ['1', null, '3'] },
|
||||
{ key: 'b', values: ['4'] },
|
||||
@@ -99,14 +103,19 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app).get('/entities/by-uid/zzz');
|
||||
|
||||
expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(expect.objectContaining(entity));
|
||||
});
|
||||
|
||||
it('responds with a 404 for missing entities', async () => {
|
||||
entitiesCatalog.entityByUid.mockResolvedValue(undefined);
|
||||
|
||||
const response = await request(app).get('/entities/by-uid/zzz');
|
||||
|
||||
expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz');
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.text).toMatch(/uid/);
|
||||
});
|
||||
@@ -116,16 +125,18 @@ describe('createRouter', () => {
|
||||
it('can fetch entity by name', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
kind: 'k',
|
||||
metadata: {
|
||||
name: 'c',
|
||||
namespace: 'd',
|
||||
name: 'n',
|
||||
namespace: 'ns',
|
||||
},
|
||||
};
|
||||
entitiesCatalog.entityByName.mockResolvedValue(entity);
|
||||
|
||||
const response = await request(app).get('/entities/by-name/b/d/c');
|
||||
const response = await request(app).get('/entities/by-name/k/ns/n');
|
||||
|
||||
expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('k', 'ns', 'n');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(expect.objectContaining(entity));
|
||||
});
|
||||
@@ -133,8 +144,10 @@ describe('createRouter', () => {
|
||||
it('responds with a 404 for missing entities', async () => {
|
||||
entitiesCatalog.entityByName.mockResolvedValue(undefined);
|
||||
|
||||
const response = await request(app).get('/entities/by-name//b/d/c');
|
||||
const response = await request(app).get('/entities/by-name/b/d/c');
|
||||
|
||||
expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('b', 'd', 'c');
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.text).toMatch(/name/);
|
||||
});
|
||||
@@ -147,9 +160,9 @@ describe('createRouter', () => {
|
||||
.set('Content-Type', 'application/json')
|
||||
.send();
|
||||
|
||||
expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled();
|
||||
expect(response.status).toEqual(400);
|
||||
expect(response.text).toMatch(/body/);
|
||||
expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes the body down', async () => {
|
||||
@@ -169,13 +182,13 @@ describe('createRouter', () => {
|
||||
.send(entity)
|
||||
.set('Content-Type', 'application/json');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(entity);
|
||||
expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
entity,
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(entity);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,8 +198,9 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app).delete('/entities/by-uid/apa');
|
||||
|
||||
expect(response.status).toEqual(204);
|
||||
expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa');
|
||||
expect(response.status).toEqual(204);
|
||||
});
|
||||
|
||||
it('responds with a 404 for missing entities', async () => {
|
||||
@@ -196,8 +210,9 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app).delete('/entities/by-uid/apa');
|
||||
|
||||
expect(response.status).toEqual(404);
|
||||
expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa');
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -227,8 +242,8 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app).post('/locations').send(spec);
|
||||
|
||||
expect(response.status).toEqual(400);
|
||||
expect(higherOrderOperation.addLocation).not.toHaveBeenCalled();
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
it('passes the body down', async () => {
|
||||
@@ -244,9 +259,14 @@ describe('createRouter', () => {
|
||||
|
||||
const response = await request(app).post('/locations').send(spec);
|
||||
|
||||
expect(response.status).toEqual(201);
|
||||
expect(higherOrderOperation.addLocation).toHaveBeenCalledTimes(1);
|
||||
expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec);
|
||||
expect(response.status).toEqual(201);
|
||||
expect(response.body).toEqual(
|
||||
expect.objectContaining({
|
||||
location: { id: 'a', ...spec },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,8 +69,8 @@ export async function createRouter(
|
||||
const { kind, namespace, name } = req.params;
|
||||
const entity = await entitiesCatalog.entityByName(
|
||||
kind,
|
||||
name,
|
||||
namespace,
|
||||
name,
|
||||
);
|
||||
if (!entity) {
|
||||
res
|
||||
|
||||
@@ -25,6 +25,7 @@ import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { Logger } from 'winston';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { HigherOrderOperation } from '../ingestion';
|
||||
import { createRouter } from './router';
|
||||
import { HigherOrderOperation } from '../ingestion/types';
|
||||
|
||||
@@ -43,8 +44,8 @@ export async function createStandaloneApplication(
|
||||
enableCors,
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
logger,
|
||||
higherOrderOperation,
|
||||
logger,
|
||||
} = options;
|
||||
const app = express();
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/codemirror": "^0.0.93",
|
||||
"@types/codemirror": "^0.0.95",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: get yarn cache
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
@@ -1482,14 +1482,14 @@
|
||||
"@types/yargs" "^15.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@lerna/add@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/add/-/add-3.20.0.tgz#bea7edf36fc93fb72ec34cb9ba854c48d4abf309"
|
||||
integrity sha512-AnH1oRIEEg/VDa3SjYq4x1/UglEAvrZuV0WssHUMN81RTZgQk3we+Mv3qZNddrZ/fBcZu2IAdN/EQ3+ie2JxKQ==
|
||||
"@lerna/add@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b"
|
||||
integrity sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A==
|
||||
dependencies:
|
||||
"@evocateur/pacote" "^9.6.3"
|
||||
"@lerna/bootstrap" "3.20.0"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/bootstrap" "3.21.0"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/filter-options" "3.20.0"
|
||||
"@lerna/npm-conf" "3.16.0"
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
@@ -1498,12 +1498,12 @@
|
||||
p-map "^2.1.0"
|
||||
semver "^6.2.0"
|
||||
|
||||
"@lerna/bootstrap@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.20.0.tgz#635d71046830f208e851ab429a63da1747589e37"
|
||||
integrity sha512-Wylullx3uthKE7r4izo09qeRGL20Y5yONlQEjPCfnbxCC2Elu+QcPu4RC6kqKQ7b+g7pdC3OOgcHZjngrwr5XQ==
|
||||
"@lerna/bootstrap@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.21.0.tgz#bcd1b651be5b0970b20d8fae04c864548123aed6"
|
||||
integrity sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw==
|
||||
dependencies:
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/filter-options" "3.20.0"
|
||||
"@lerna/has-npm-version" "3.16.5"
|
||||
"@lerna/npm-install" "3.16.5"
|
||||
@@ -1527,13 +1527,13 @@
|
||||
read-package-tree "^5.1.6"
|
||||
semver "^6.2.0"
|
||||
|
||||
"@lerna/changed@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.20.0.tgz#66b97ebd6c8f8d207152ee524a0791846a9097ae"
|
||||
integrity sha512-+hzMFSldbRPulZ0vbKk6RD9f36gaH3Osjx34wrrZ62VB4pKmjyuS/rxVYkCA3viPLHoiIw2F8zHM5BdYoDSbjw==
|
||||
"@lerna/changed@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.21.0.tgz#108e15f679bfe077af500f58248c634f1044ea0b"
|
||||
integrity sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw==
|
||||
dependencies:
|
||||
"@lerna/collect-updates" "3.20.0"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/listable" "3.18.5"
|
||||
"@lerna/output" "3.13.0"
|
||||
|
||||
@@ -1555,12 +1555,12 @@
|
||||
execa "^1.0.0"
|
||||
strong-log-transformer "^2.0.0"
|
||||
|
||||
"@lerna/clean@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.20.0.tgz#ba777e373ddeae63e57860df75d47a9e5264c5b2"
|
||||
integrity sha512-9ZdYrrjQvR5wNXmHfDsfjWjp0foOkCwKe3hrckTzkAeQA1ibyz5llGwz5e1AeFrV12e2/OLajVqYfe+qdkZUgg==
|
||||
"@lerna/clean@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.21.0.tgz#c0b46b5300cc3dae2cda3bec14b803082da3856d"
|
||||
integrity sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg==
|
||||
dependencies:
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/filter-options" "3.20.0"
|
||||
"@lerna/prompt" "3.18.5"
|
||||
"@lerna/pulse-till-done" "3.13.0"
|
||||
@@ -1600,14 +1600,14 @@
|
||||
npmlog "^4.1.2"
|
||||
slash "^2.0.0"
|
||||
|
||||
"@lerna/command@3.18.5":
|
||||
version "3.18.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/command/-/command-3.18.5.tgz#14c6d2454adbfd365f8027201523e6c289cd3cd9"
|
||||
integrity sha512-36EnqR59yaTU4HrR1C9XDFti2jRx0BgpIUBeWn129LZZB8kAB3ov1/dJNa1KcNRKp91DncoKHLY99FZ6zTNpMQ==
|
||||
"@lerna/command@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/command/-/command-3.21.0.tgz#9a2383759dc7b700dacfa8a22b2f3a6e190121f7"
|
||||
integrity sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ==
|
||||
dependencies:
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/package-graph" "3.18.5"
|
||||
"@lerna/project" "3.18.0"
|
||||
"@lerna/project" "3.21.0"
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
"@lerna/write-log-file" "3.13.0"
|
||||
clone-deep "^4.0.1"
|
||||
@@ -1616,10 +1616,10 @@
|
||||
is-ci "^2.0.0"
|
||||
npmlog "^4.1.2"
|
||||
|
||||
"@lerna/conventional-commits@3.18.5":
|
||||
version "3.18.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.18.5.tgz#08efd2e5b45acfaf3f151a53a3ec7ecade58a7bc"
|
||||
integrity sha512-qcvXIEJ3qSgalxXnQ7Yxp5H9Ta5TVyai6vEor6AAEHc20WiO7UIdbLDCxBtiiHMdGdpH85dTYlsoYUwsCJu3HQ==
|
||||
"@lerna/conventional-commits@3.22.0":
|
||||
version "3.22.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09"
|
||||
integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA==
|
||||
dependencies:
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
conventional-changelog-angular "^5.0.3"
|
||||
@@ -1642,14 +1642,14 @@
|
||||
fs-extra "^8.1.0"
|
||||
npmlog "^4.1.2"
|
||||
|
||||
"@lerna/create@3.18.5":
|
||||
version "3.18.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/create/-/create-3.18.5.tgz#11ac539f069248eaf7bc4c42e237784330f4fc47"
|
||||
integrity sha512-cHpjocbpKmLopCuZFI7cKEM3E/QY8y+yC7VtZ4FQRSaLU8D8i2xXtXmYaP1GOlVNavji0iwoXjuNpnRMInIr2g==
|
||||
"@lerna/create@3.22.0":
|
||||
version "3.22.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619"
|
||||
integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw==
|
||||
dependencies:
|
||||
"@evocateur/pacote" "^9.6.3"
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/npm-conf" "3.16.0"
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
camelcase "^5.0.0"
|
||||
@@ -1674,23 +1674,23 @@
|
||||
"@lerna/child-process" "3.16.5"
|
||||
npmlog "^4.1.2"
|
||||
|
||||
"@lerna/diff@3.18.5":
|
||||
version "3.18.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.18.5.tgz#e9e2cb882f84d5b84f0487c612137305f07accbc"
|
||||
integrity sha512-u90lGs+B8DRA9Z/2xX4YaS3h9X6GbypmGV6ITzx9+1Ga12UWGTVlKaCXBgONMBjzJDzAQOK8qPTwLA57SeBLgA==
|
||||
"@lerna/diff@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.21.0.tgz#e6df0d8b9916167ff5a49fcb02ac06424280a68d"
|
||||
integrity sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw==
|
||||
dependencies:
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
npmlog "^4.1.2"
|
||||
|
||||
"@lerna/exec@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.20.0.tgz#29f0c01aee2340eb46f90706731fef2062a49639"
|
||||
integrity sha512-pS1mmC7kzV668rHLWuv31ClngqeXjeHC8kJuM+W2D6IpUVMGQHLcCTYLudFgQsuKGVpl0DGNYG+sjLhAPiiu6A==
|
||||
"@lerna/exec@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.21.0.tgz#17f07533893cb918a17b41bcc566dc437016db26"
|
||||
integrity sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q==
|
||||
dependencies:
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/filter-options" "3.20.0"
|
||||
"@lerna/profiler" "3.20.0"
|
||||
"@lerna/run-topologically" "3.18.5"
|
||||
@@ -1733,13 +1733,13 @@
|
||||
ssri "^6.0.1"
|
||||
tar "^4.4.8"
|
||||
|
||||
"@lerna/github-client@3.16.5":
|
||||
version "3.16.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.16.5.tgz#2eb0235c3bf7a7e5d92d73e09b3761ab21f35c2e"
|
||||
integrity sha512-rHQdn8Dv/CJrO3VouOP66zAcJzrHsm+wFuZ4uGAai2At2NkgKH+tpNhQy2H1PSC0Ezj9LxvdaHYrUzULqVK5Hw==
|
||||
"@lerna/github-client@3.22.0":
|
||||
version "3.22.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95"
|
||||
integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg==
|
||||
dependencies:
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@octokit/plugin-enterprise-rest" "^3.6.1"
|
||||
"@octokit/plugin-enterprise-rest" "^6.0.1"
|
||||
"@octokit/rest" "^16.28.4"
|
||||
git-url-parse "^11.1.2"
|
||||
npmlog "^4.1.2"
|
||||
@@ -1766,13 +1766,13 @@
|
||||
"@lerna/child-process" "3.16.5"
|
||||
semver "^6.2.0"
|
||||
|
||||
"@lerna/import@3.18.5":
|
||||
version "3.18.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/import/-/import-3.18.5.tgz#a9c7d8601870729851293c10abd18b3707f7ba5e"
|
||||
integrity sha512-PH0WVLEgp+ORyNKbGGwUcrueW89K3Iuk/DDCz8mFyG2IG09l/jOF0vzckEyGyz6PO5CMcz4TI1al/qnp3FrahQ==
|
||||
"@lerna/import@3.22.0":
|
||||
version "3.22.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2"
|
||||
integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg==
|
||||
dependencies:
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/prompt" "3.18.5"
|
||||
"@lerna/pulse-till-done" "3.13.0"
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
@@ -1780,43 +1780,43 @@
|
||||
fs-extra "^8.1.0"
|
||||
p-map-series "^1.0.0"
|
||||
|
||||
"@lerna/info@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/info/-/info-3.20.0.tgz#3a5212f3029f2bc6255f9533bdf4bcb120ef329a"
|
||||
integrity sha512-Rsz+KQF9mczbGUbPTrtOed1N0C+cA08Qz0eX/oI+NNjvsryZIju/o7uedG4I3P55MBiAioNrJI88fHH3eTgYug==
|
||||
"@lerna/info@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/info/-/info-3.21.0.tgz#76696b676fdb0f35d48c83c63c1e32bb5e37814f"
|
||||
integrity sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA==
|
||||
dependencies:
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/output" "3.13.0"
|
||||
envinfo "^7.3.1"
|
||||
|
||||
"@lerna/init@3.18.5":
|
||||
version "3.18.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/init/-/init-3.18.5.tgz#86dd0b2b3290755a96975069b5cb007f775df9f5"
|
||||
integrity sha512-oCwipWrha98EcJAHm8AGd2YFFLNI7AW9AWi0/LbClj1+XY9ah+uifXIgYGfTk63LbgophDd8936ZEpHMxBsbAg==
|
||||
"@lerna/init@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/init/-/init-3.21.0.tgz#1e810934dc8bf4e5386c031041881d3b4096aa5c"
|
||||
integrity sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg==
|
||||
dependencies:
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
fs-extra "^8.1.0"
|
||||
p-map "^2.1.0"
|
||||
write-json-file "^3.2.0"
|
||||
|
||||
"@lerna/link@3.18.5":
|
||||
version "3.18.5"
|
||||
resolved "https://registry.npmjs.org/@lerna/link/-/link-3.18.5.tgz#f24347e4f0b71d54575bd37cfa1794bc8ee91b18"
|
||||
integrity sha512-xTN3vktJpkT7Nqc3QkZRtHO4bT5NvuLMtKNIBDkks0HpGxC9PRyyqwOoCoh1yOGbrWIuDezhfMg3Qow+6I69IQ==
|
||||
"@lerna/link@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/link/-/link-3.21.0.tgz#8be68ff0ccee104b174b5bbd606302c2f06e9d9b"
|
||||
integrity sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ==
|
||||
dependencies:
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/package-graph" "3.18.5"
|
||||
"@lerna/symlink-dependencies" "3.17.0"
|
||||
p-map "^2.1.0"
|
||||
slash "^2.0.0"
|
||||
|
||||
"@lerna/list@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/list/-/list-3.20.0.tgz#7e67cc29c5cf661cfd097e8a7c2d3dcce7a81029"
|
||||
integrity sha512-fXTicPrfioVnRzknyPawmYIVkzDRBaQqk9spejS1S3O1DOidkihK0xxNkr8HCVC0L22w6f92g83qWDp2BYRUbg==
|
||||
"@lerna/list@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/list/-/list-3.21.0.tgz#42f76fafa56dea13b691ec8cab13832691d61da2"
|
||||
integrity sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg==
|
||||
dependencies:
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/filter-options" "3.20.0"
|
||||
"@lerna/listable" "3.18.5"
|
||||
"@lerna/output" "3.13.0"
|
||||
@@ -1962,25 +1962,7 @@
|
||||
npmlog "^4.1.2"
|
||||
upath "^1.2.0"
|
||||
|
||||
"@lerna/project@3.18.0":
|
||||
version "3.18.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/project/-/project-3.18.0.tgz#56feee01daeb42c03cbdf0ed8a2a10cbce32f670"
|
||||
integrity sha512-+LDwvdAp0BurOAWmeHE3uuticsq9hNxBI0+FMHiIai8jrygpJGahaQrBYWpwbshbQyVLeQgx3+YJdW2TbEdFWA==
|
||||
dependencies:
|
||||
"@lerna/package" "3.16.0"
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
cosmiconfig "^5.1.0"
|
||||
dedent "^0.7.0"
|
||||
dot-prop "^4.2.0"
|
||||
glob-parent "^5.0.0"
|
||||
globby "^9.2.0"
|
||||
load-json-file "^5.3.0"
|
||||
npmlog "^4.1.2"
|
||||
p-map "^2.1.0"
|
||||
resolve-from "^4.0.0"
|
||||
write-json-file "^3.2.0"
|
||||
|
||||
"@lerna/project@^3.18.0":
|
||||
"@lerna/project@3.21.0", "@lerna/project@^3.18.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz#5d784d2d10c561a00f20320bcdb040997c10502d"
|
||||
integrity sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A==
|
||||
@@ -2006,10 +1988,10 @@
|
||||
inquirer "^6.2.0"
|
||||
npmlog "^4.1.2"
|
||||
|
||||
"@lerna/publish@3.20.2":
|
||||
version "3.20.2"
|
||||
resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.20.2.tgz#a45d29813099b3249657ea913d0dc3f8ebc5cc2e"
|
||||
integrity sha512-N7Y6PdhJ+tYQPdI1tZum8W25cDlTp4D6brvRacKZusweWexxaopbV8RprBaKexkEX/KIbncuADq7qjDBdQHzaA==
|
||||
"@lerna/publish@3.22.0":
|
||||
version "3.22.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.22.0.tgz#7a3fb61026d3b7425f3b9a1849421f67d795c55d"
|
||||
integrity sha512-8LBeTLBN8NIrCrLGykRu+PKrfrCC16sGCVY0/bzq9TDioR7g6+cY0ZAw653Qt/0Kr7rg3J7XxVNdzj3fvevlwA==
|
||||
dependencies:
|
||||
"@evocateur/libnpmaccess" "^3.1.2"
|
||||
"@evocateur/npm-registry-fetch" "^4.0.0"
|
||||
@@ -2017,7 +1999,7 @@
|
||||
"@lerna/check-working-tree" "3.16.5"
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/collect-updates" "3.20.0"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/describe-ref" "3.16.5"
|
||||
"@lerna/log-packed" "3.16.0"
|
||||
"@lerna/npm-conf" "3.16.0"
|
||||
@@ -2032,7 +2014,7 @@
|
||||
"@lerna/run-lifecycle" "3.16.2"
|
||||
"@lerna/run-topologically" "3.18.5"
|
||||
"@lerna/validation-error" "3.13.0"
|
||||
"@lerna/version" "3.20.2"
|
||||
"@lerna/version" "3.22.0"
|
||||
figgy-pudding "^3.5.1"
|
||||
fs-extra "^8.1.0"
|
||||
npm-package-arg "^6.1.0"
|
||||
@@ -2095,12 +2077,12 @@
|
||||
figgy-pudding "^3.5.1"
|
||||
p-queue "^4.0.0"
|
||||
|
||||
"@lerna/run@3.20.0":
|
||||
version "3.20.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/run/-/run-3.20.0.tgz#a479f7c42bdf9ebabb3a1e5a2bdebb7a8d201151"
|
||||
integrity sha512-9U3AqeaCeB7KsGS9oyKNp62s9vYoULg/B4cqXTKZkc+OKL6QOEjYHYVSBcMK9lUXrMjCjDIuDSX3PnTCPxQ2Dw==
|
||||
"@lerna/run@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/run/-/run-3.21.0.tgz#2a35ec84979e4d6e42474fe148d32e5de1cac891"
|
||||
integrity sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q==
|
||||
dependencies:
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/filter-options" "3.20.0"
|
||||
"@lerna/npm-run-script" "3.16.5"
|
||||
"@lerna/output" "3.13.0"
|
||||
@@ -2145,17 +2127,17 @@
|
||||
dependencies:
|
||||
npmlog "^4.1.2"
|
||||
|
||||
"@lerna/version@3.20.2":
|
||||
version "3.20.2"
|
||||
resolved "https://registry.npmjs.org/@lerna/version/-/version-3.20.2.tgz#3709141c0f537741d9bc10cb24f56897bcb30428"
|
||||
integrity sha512-ckBJMaBWc+xJen0cMyCE7W67QXLLrc0ELvigPIn8p609qkfNM0L0CF803MKxjVOldJAjw84b8ucNWZLvJagP/Q==
|
||||
"@lerna/version@3.22.0":
|
||||
version "3.22.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/version/-/version-3.22.0.tgz#67e1340c1904e9b339becd66429f32dd8ad65a55"
|
||||
integrity sha512-6uhL6RL7/FeW6u1INEgyKjd5dwO8+IsbLfkfC682QuoVLS7VG6OOB+JmTpCvnuyYWI6fqGh1bRk9ww8kPsj+EA==
|
||||
dependencies:
|
||||
"@lerna/check-working-tree" "3.16.5"
|
||||
"@lerna/child-process" "3.16.5"
|
||||
"@lerna/collect-updates" "3.20.0"
|
||||
"@lerna/command" "3.18.5"
|
||||
"@lerna/conventional-commits" "3.18.5"
|
||||
"@lerna/github-client" "3.16.5"
|
||||
"@lerna/command" "3.21.0"
|
||||
"@lerna/conventional-commits" "3.22.0"
|
||||
"@lerna/github-client" "3.22.0"
|
||||
"@lerna/gitlab-client" "3.15.0"
|
||||
"@lerna/output" "3.13.0"
|
||||
"@lerna/prerelease-id-from-version" "3.16.0"
|
||||
@@ -2340,10 +2322,10 @@
|
||||
is-plain-object "^3.0.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/plugin-enterprise-rest@^3.6.1":
|
||||
version "3.6.2"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-3.6.2.tgz#74de25bef21e0182b4fa03a8678cd00a4e67e561"
|
||||
integrity sha512-3wF5eueS5OHQYuAEudkpN+xVeUsg8vYEMMenEzLphUZ7PRZ8OJtDcsreL3ad9zxXmBbaFWzLmFcdob5CLyZftA==
|
||||
"@octokit/plugin-enterprise-rest@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437"
|
||||
integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==
|
||||
|
||||
"@octokit/plugin-paginate-rest@^1.1.1":
|
||||
version "1.1.2"
|
||||
@@ -2585,15 +2567,15 @@
|
||||
ts-dedent "^1.1.0"
|
||||
|
||||
"@storybook/addon-storysource@^5.3.18":
|
||||
version "5.3.18"
|
||||
resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-5.3.18.tgz#003374d76c0898c3b74aa4cd008f444eb6b63c68"
|
||||
integrity sha512-uMSu505rbcNErAYngV5raVRQ/eMiOtGNA88WxNzD/8h/0EVpEXlBAewiGzAmooNhCa/3O3iPuwka8Cky6aWPOA==
|
||||
version "5.3.19"
|
||||
resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-5.3.19.tgz#ae693e88db5d220cb256a9ef4a2366c300e8d88c"
|
||||
integrity sha512-W7mIAHuxYT+b1huaHCHLkBAh2MbeWmF8CxeBCFiOgZaYYQUTDEh018HJF8u2AqiWSouRhcfzhTnGxOo0hNRBgw==
|
||||
dependencies:
|
||||
"@storybook/addons" "5.3.18"
|
||||
"@storybook/components" "5.3.18"
|
||||
"@storybook/router" "5.3.18"
|
||||
"@storybook/source-loader" "5.3.18"
|
||||
"@storybook/theming" "5.3.18"
|
||||
"@storybook/addons" "5.3.19"
|
||||
"@storybook/components" "5.3.19"
|
||||
"@storybook/router" "5.3.19"
|
||||
"@storybook/source-loader" "5.3.19"
|
||||
"@storybook/theming" "5.3.19"
|
||||
core-js "^3.0.1"
|
||||
estraverse "^4.2.0"
|
||||
loader-utils "^1.2.3"
|
||||
@@ -2616,7 +2598,7 @@
|
||||
global "^4.3.2"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/addons@^5.3.17":
|
||||
"@storybook/addons@5.3.19", "@storybook/addons@^5.3.17":
|
||||
version "5.3.19"
|
||||
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.19.tgz#3a7010697afd6df9a41b8c8a7351d9a06ff490a4"
|
||||
integrity sha512-Ky/k22p6i6FVNvs1VhuFyGvYJdcp+FgXqFgnPyY/OXJW/vPDapdElpTpHJZLFI9I2FQBDcygBPU5RXkumQ+KUQ==
|
||||
@@ -2770,6 +2752,33 @@
|
||||
simplebar-react "^1.0.0-alpha.6"
|
||||
ts-dedent "^1.1.0"
|
||||
|
||||
"@storybook/components@5.3.19":
|
||||
version "5.3.19"
|
||||
resolved "https://registry.npmjs.org/@storybook/components/-/components-5.3.19.tgz#aac1f9eea1247cc85bd93b10fca803876fb84a6b"
|
||||
integrity sha512-3g23/+ktlocaHLJKISu9Neu3XKa6aYP2ctDYkRtGchSB0Q55hQsUVGO+BEVuT7Pk2D59mVCxboBjxcRoPUY4pw==
|
||||
dependencies:
|
||||
"@storybook/client-logger" "5.3.19"
|
||||
"@storybook/theming" "5.3.19"
|
||||
"@types/react-syntax-highlighter" "11.0.4"
|
||||
"@types/react-textarea-autosize" "^4.3.3"
|
||||
core-js "^3.0.1"
|
||||
global "^4.3.2"
|
||||
lodash "^4.17.15"
|
||||
markdown-to-jsx "^6.11.4"
|
||||
memoizerific "^1.11.3"
|
||||
polished "^3.3.1"
|
||||
popper.js "^1.14.7"
|
||||
prop-types "^15.7.2"
|
||||
react "^16.8.3"
|
||||
react-dom "^16.8.3"
|
||||
react-focus-lock "^2.1.0"
|
||||
react-helmet-async "^1.0.2"
|
||||
react-popper-tooltip "^2.8.3"
|
||||
react-syntax-highlighter "^11.0.2"
|
||||
react-textarea-autosize "^7.1.0"
|
||||
simplebar-react "^1.0.0-alpha.6"
|
||||
ts-dedent "^1.1.0"
|
||||
|
||||
"@storybook/core-events@5.3.18":
|
||||
version "5.3.18"
|
||||
resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-5.3.18.tgz#e5d335f8a2c7dd46502b8f505006f1e111b46d49"
|
||||
@@ -2939,13 +2948,13 @@
|
||||
qs "^6.6.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/source-loader@5.3.18":
|
||||
version "5.3.18"
|
||||
resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-5.3.18.tgz#39ba28d9664ab8204d6b04ee757772369931e7e5"
|
||||
integrity sha512-oljmLt3KMu17W9FAaEgocsI6wrloWczS26SVXadzbDbL+8Tu6vTeFJSd6HcY+e4JIIKjuze0kmaVhpi6wwPbZQ==
|
||||
"@storybook/source-loader@5.3.19":
|
||||
version "5.3.19"
|
||||
resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-5.3.19.tgz#ff0a00731c24c61721d8b9d84152f8542913a3b7"
|
||||
integrity sha512-srSZRPgEOUse8nRVnlazweB2QGp63mPqM0uofg8zYARyaYSOzkC155ymdeiHsmiBTS3X3I0FQE4+KnwiH7iLtw==
|
||||
dependencies:
|
||||
"@storybook/addons" "5.3.18"
|
||||
"@storybook/client-logger" "5.3.18"
|
||||
"@storybook/addons" "5.3.19"
|
||||
"@storybook/client-logger" "5.3.19"
|
||||
"@storybook/csf" "0.0.1"
|
||||
core-js "^3.0.1"
|
||||
estraverse "^4.2.0"
|
||||
@@ -3402,7 +3411,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/body-parser@*":
|
||||
"@types/body-parser@*", "@types/body-parser@^1.19.0":
|
||||
version "1.19.0"
|
||||
resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
||||
integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
|
||||
@@ -3422,10 +3431,10 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/codemirror@^0.0.93":
|
||||
version "0.0.93"
|
||||
resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.93.tgz#74ce5e1132325cb2c88a9bb91fd819294fe0c8ed"
|
||||
integrity sha512-7F2ksY4U5amV/bIkMTev0n22RWcFO2BBuFuFYmAJIT19gr3kDElr1phgjPsNKV/Pj301bujnMjSy5GlWla+cow==
|
||||
"@types/codemirror@^0.0.95":
|
||||
version "0.0.95"
|
||||
resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.95.tgz#8fe424989cd5a6d7848f124774d42ef34d91adb8"
|
||||
integrity sha512-E7w4HS8/rR7Rxkga6j68n3/Mi4BJ870/OJJKRqytyWiM659KnbviSng/NPfM/FOjg7YL+5ruFF69FqoLChnPBw==
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
@@ -3812,6 +3821,14 @@
|
||||
"@types/oauth" "*"
|
||||
"@types/passport" "*"
|
||||
|
||||
"@types/passport-saml@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/@types/passport-saml/-/passport-saml-1.1.2.tgz#f32ac2321eb25ec7bdbb1f3a5313b596bb0887e6"
|
||||
integrity sha512-vpSdcb7V/bFxrvZJwSqnBr0qEqIhtOnwRBxw+Dvq4UkVbEgcCOkxF4tERCCFfA+FP3lp63VCCAifZLQrF5JkXA==
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
"@types/passport" "*"
|
||||
|
||||
"@types/passport@*", "@types/passport@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.3.tgz#e459ed6c262bf0686684d1b05901be0d0b192a9c"
|
||||
@@ -5453,7 +5470,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
||||
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
|
||||
|
||||
body-parser@1.19.0:
|
||||
body-parser@1.19.0, body-parser@^1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
||||
@@ -6771,7 +6788,7 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@7.0.1, cross-spawn@^7.0.0, cross-spawn@^7.0.1:
|
||||
cross-spawn@7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14"
|
||||
integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==
|
||||
@@ -6789,7 +6806,7 @@ cross-spawn@^5.1.0:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.2:
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
@@ -8065,7 +8082,7 @@ escape-goat@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
|
||||
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
|
||||
|
||||
escape-html@~1.0.3:
|
||||
escape-html@^1.0.3, escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
@@ -12006,26 +12023,26 @@ left-pad@^1.3.0:
|
||||
integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
|
||||
|
||||
lerna@^3.20.2:
|
||||
version "3.20.2"
|
||||
resolved "https://registry.npmjs.org/lerna/-/lerna-3.20.2.tgz#abf84e73055fe84ee21b46e64baf37b496c24864"
|
||||
integrity sha512-bjdL7hPLpU3Y8CBnw/1ys3ynQMUjiK6l9iDWnEGwFtDy48Xh5JboR9ZJwmKGCz9A/sarVVIGwf1tlRNKUG9etA==
|
||||
version "3.22.0"
|
||||
resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.0.tgz#da14d08f183ffe6eec566a4ef3f0e11afa621183"
|
||||
integrity sha512-xWlHdAStcqK/IjKvjsSMHPZjPkBV1lS60PmsIeObU8rLljTepc4Sg/hncw4HWfQxPIewHAUTqhrxPIsqf9L2Eg==
|
||||
dependencies:
|
||||
"@lerna/add" "3.20.0"
|
||||
"@lerna/bootstrap" "3.20.0"
|
||||
"@lerna/changed" "3.20.0"
|
||||
"@lerna/clean" "3.20.0"
|
||||
"@lerna/add" "3.21.0"
|
||||
"@lerna/bootstrap" "3.21.0"
|
||||
"@lerna/changed" "3.21.0"
|
||||
"@lerna/clean" "3.21.0"
|
||||
"@lerna/cli" "3.18.5"
|
||||
"@lerna/create" "3.18.5"
|
||||
"@lerna/diff" "3.18.5"
|
||||
"@lerna/exec" "3.20.0"
|
||||
"@lerna/import" "3.18.5"
|
||||
"@lerna/info" "3.20.0"
|
||||
"@lerna/init" "3.18.5"
|
||||
"@lerna/link" "3.18.5"
|
||||
"@lerna/list" "3.20.0"
|
||||
"@lerna/publish" "3.20.2"
|
||||
"@lerna/run" "3.20.0"
|
||||
"@lerna/version" "3.20.2"
|
||||
"@lerna/create" "3.22.0"
|
||||
"@lerna/diff" "3.21.0"
|
||||
"@lerna/exec" "3.21.0"
|
||||
"@lerna/import" "3.22.0"
|
||||
"@lerna/info" "3.21.0"
|
||||
"@lerna/init" "3.21.0"
|
||||
"@lerna/link" "3.21.0"
|
||||
"@lerna/list" "3.21.0"
|
||||
"@lerna/publish" "3.22.0"
|
||||
"@lerna/run" "3.21.0"
|
||||
"@lerna/version" "3.22.0"
|
||||
import-local "^2.0.0"
|
||||
npmlog "^4.1.2"
|
||||
|
||||
@@ -12553,6 +12570,14 @@ markdown-it@^10.0.0:
|
||||
mdurl "^1.0.1"
|
||||
uc.micro "^1.0.5"
|
||||
|
||||
markdown-to-jsx@^6.11.4:
|
||||
version "6.11.4"
|
||||
resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.4.tgz#b4528b1ab668aef7fe61c1535c27e837819392c5"
|
||||
integrity sha512-3lRCD5Sh+tfA52iGgfs/XZiw33f7fFX9Bn55aNnVNUd2GzLDkOWyKYYD8Yju2B1Vn+feiEdgJs8T6Tg0xNokPw==
|
||||
dependencies:
|
||||
prop-types "^15.6.2"
|
||||
unquote "^1.1.0"
|
||||
|
||||
markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.0.tgz#a2e3f2bc781c3402d8bb0f8e0a12a186474623b0"
|
||||
@@ -13210,6 +13235,11 @@ node-forge@0.9.0:
|
||||
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
|
||||
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
|
||||
|
||||
node-forge@^0.7.0:
|
||||
version "0.7.6"
|
||||
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
|
||||
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
|
||||
|
||||
node-gyp@^5.0.2:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332"
|
||||
@@ -14079,7 +14109,21 @@ passport-oauth2@1.x.x:
|
||||
uid2 "0.0.x"
|
||||
utils-merge "1.x.x"
|
||||
|
||||
passport-strategy@1.x.x:
|
||||
passport-saml@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.3.tgz#cbea1a2b21ff32b3bc4bfd84dc39c3a370df9935"
|
||||
integrity sha512-54ecY/A6UEsyCehJws6a+J6THvwtYnGl9cnAUxx5DjsuKgZrDs0tSy58K4hCk1XG/LOcdQSF1TR3xlRXgTULhA==
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
passport-strategy "*"
|
||||
q "^1.5.0"
|
||||
xml-crypto "^1.4.0"
|
||||
xml-encryption "^1.0.0"
|
||||
xml2js "0.4.x"
|
||||
xmlbuilder "^11.0.0"
|
||||
xmldom "0.1.x"
|
||||
|
||||
passport-strategy@*, passport-strategy@1.x.x:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
|
||||
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
|
||||
@@ -15067,7 +15111,7 @@ pupa@^2.0.1:
|
||||
dependencies:
|
||||
escape-goat "^2.0.0"
|
||||
|
||||
q@^1.1.2, q@^1.5.1:
|
||||
q@^1.1.2, q@^1.5.0, q@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||
@@ -16405,7 +16449,7 @@ sane@^4.0.3:
|
||||
minimist "^1.1.1"
|
||||
walker "~1.0.5"
|
||||
|
||||
sax@^1.2.4, sax@~1.2.4:
|
||||
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
@@ -18083,9 +18127,9 @@ ts-pnp@^1.1.2:
|
||||
integrity sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ==
|
||||
|
||||
tsc-watch@^4.2.3:
|
||||
version "4.2.5"
|
||||
resolved "https://registry.npmjs.org/tsc-watch/-/tsc-watch-4.2.5.tgz#3ee680cfd02087bb76cbecd2d5f290a0d6302454"
|
||||
integrity sha512-scXpL5SFJevvtKOtQIRxJvyEwCKSIZS9bDrO/cy/dWlETTVwSlfRm2WmakdRPM0Rg90DbYyJjkUr2z74HXcxzQ==
|
||||
version "4.2.8"
|
||||
resolved "https://registry.npmjs.org/tsc-watch/-/tsc-watch-4.2.8.tgz#cb8639bf507738dfa64590504f539a2f525d3a7b"
|
||||
integrity sha512-EtWdmVZYFxTxSHqtmpowtvC/son28Hgl3qxYnD6sy+uGQXsWYDQxwmL8EneWca2kzAIYNZq3CGrUdK1jjbL4Vw==
|
||||
dependencies:
|
||||
cross-spawn "^5.1.0"
|
||||
node-cleanup "^2.1.2"
|
||||
@@ -19171,16 +19215,62 @@ xdg-basedir@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
||||
|
||||
xml-crypto@^1.4.0:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34"
|
||||
integrity sha512-uHkmpUtX15xExe5iimPmakAZN+6CqIvjmaJTy4FwqGzaTjrKRBNeqMh8zGEzVNgW0dk6beFYpyQSgqV/J6C5xA==
|
||||
dependencies:
|
||||
xmldom "0.1.27"
|
||||
xpath "0.0.27"
|
||||
|
||||
xml-encryption@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.2.0.tgz#37c8b470beae88b4625ea8cad82f108ea0f9c364"
|
||||
integrity sha512-J3NjGMY8jf6bTo15jURTYBLtsisbnyCeM+MuxtfiAkZEZBnSZpNKjUUORhiOScKvSi6tMOAaZ3r7bZOXOni+Ew==
|
||||
dependencies:
|
||||
escape-html "^1.0.3"
|
||||
node-forge "^0.7.0"
|
||||
xmldom "~0.1.15"
|
||||
xpath "0.0.27"
|
||||
|
||||
xml-name-validator@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
|
||||
|
||||
xml2js@0.4.x:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
|
||||
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
|
||||
dependencies:
|
||||
sax ">=0.6.0"
|
||||
xmlbuilder "~11.0.0"
|
||||
|
||||
xmlbuilder@^11.0.0, xmlbuilder@~11.0.0:
|
||||
version "11.0.1"
|
||||
resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
||||
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
|
||||
|
||||
xmlchars@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
xmldom@0.1.27:
|
||||
version "0.1.27"
|
||||
resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
|
||||
integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
|
||||
|
||||
xmldom@0.1.x, xmldom@~0.1.15:
|
||||
version "0.1.31"
|
||||
resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
|
||||
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
|
||||
|
||||
xpath@0.0.27:
|
||||
version "0.0.27"
|
||||
resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"
|
||||
integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==
|
||||
|
||||
xregexp@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50"
|
||||
@@ -19220,6 +19310,11 @@ yaml@*, yaml@^1.9.2:
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
|
||||
yaml@^1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
|
||||
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
|
||||
|
||||
yaml@^1.7.2:
|
||||
version "1.8.3"
|
||||
resolved "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a"
|
||||
|
||||
Reference in New Issue
Block a user