Implement location management
This commit is contained in:
@@ -39,13 +39,14 @@ import { PluginEnvironment } from './types';
|
||||
|
||||
const DEFAULT_PORT = 7000;
|
||||
const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
|
||||
const pluginEnvironment: PluginEnvironment = {
|
||||
logger: getRootLogger().child({ type: 'plugin' }),
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const database = await buildDatabase(getRootLogger());
|
||||
console.log(await database.select().from('locations'));
|
||||
|
||||
const pluginEnvironment: PluginEnvironment = {
|
||||
logger: getRootLogger().child({ type: 'plugin' }),
|
||||
database,
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
|
||||
@@ -15,22 +15,12 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AggregatorInventory,
|
||||
DatabaseInventory,
|
||||
createRouter,
|
||||
StaticInventory,
|
||||
} from '@backstage/plugin-inventory-backend';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function ({ logger }: PluginEnvironment) {
|
||||
const inventory = new AggregatorInventory();
|
||||
inventory.enlist(
|
||||
new StaticInventory([
|
||||
{ id: 'component1' },
|
||||
{ id: 'component2' },
|
||||
{ id: 'component3' },
|
||||
{ id: 'component4' },
|
||||
]),
|
||||
);
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function({ logger, database }: PluginEnvironment) {
|
||||
const inventory = new DatabaseInventory(database);
|
||||
return await createRouter({ inventory, logger });
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Logger } from 'winston';
|
||||
import Knex from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export type PluginEnvironment = {
|
||||
logger: Logger;
|
||||
database: Knex;
|
||||
};
|
||||
|
||||
@@ -18,11 +18,16 @@
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"helmet": "^3.22.0",
|
||||
"knex": "^0.21.1",
|
||||
"morgan": "^1.10.0",
|
||||
"winston": "^3.2.1"
|
||||
"uuid": "^8.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"yup": "^0.28.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@types/uuid": "^7.0.3",
|
||||
"@types/yup": "^0.28.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
|
||||
@@ -1,47 +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 { NotFoundError } from '@backstage/backend-common';
|
||||
import { Component, Inventory } from './types';
|
||||
|
||||
export class AggregatorInventory implements Inventory {
|
||||
#inventories: Inventory[] = [];
|
||||
|
||||
async components(): Promise<Component[]> {
|
||||
const lists = await Promise.all(
|
||||
this.#inventories.map((i) => i.components()),
|
||||
);
|
||||
return lists.flat();
|
||||
}
|
||||
|
||||
component(id: string): Promise<Component> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const promises = this.#inventories.map((i) =>
|
||||
i.component(id).then(resolve, () => {
|
||||
// For now, just swallow errors in individual inventories;
|
||||
// should handle partial errors better
|
||||
}),
|
||||
);
|
||||
Promise.all(promises).then(() =>
|
||||
reject(new NotFoundError(`Found no component with ID ${id}`)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
enlist(inventory: Inventory) {
|
||||
this.#inventories.push(inventory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { NotFoundError } from '@backstage/backend-common';
|
||||
import Knex from 'knex';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AddLocationRequest, Component, Inventory, Location } from './types';
|
||||
|
||||
export class DatabaseInventory implements Inventory {
|
||||
constructor(private readonly database: Knex) {}
|
||||
|
||||
async components(): Promise<Component[]> {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async component(id: string): Promise<Component> {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
async addLocation(location: AddLocationRequest): Promise<Location> {
|
||||
const id = uuidv4();
|
||||
const { type, target } = location;
|
||||
await this.database('locations').insert({ id, type, target });
|
||||
return await this.location(id);
|
||||
}
|
||||
|
||||
async removeLocation(id: string): Promise<void> {
|
||||
const result = await this.database('locations')
|
||||
.where({ id })
|
||||
.del();
|
||||
|
||||
if (!result) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async location(id: string): Promise<Location> {
|
||||
const items = await this.database('locations')
|
||||
.where({ id })
|
||||
.select();
|
||||
if (!items.length) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
return items[0];
|
||||
}
|
||||
|
||||
async locations(): Promise<Location[]> {
|
||||
return await this.database('locations').select();
|
||||
}
|
||||
}
|
||||
@@ -15,24 +15,49 @@
|
||||
*/
|
||||
|
||||
import { NotFoundError } from '@backstage/backend-common';
|
||||
import { Component, Inventory } from './types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AddLocationRequest, Component, Inventory, Location } from './types';
|
||||
|
||||
export class StaticInventory implements Inventory {
|
||||
#components: Component[];
|
||||
private _components: Component[];
|
||||
private _locations: Location[];
|
||||
|
||||
constructor(components: Component[]) {
|
||||
this.#components = components;
|
||||
constructor(components: Component[], locations: Location[]) {
|
||||
this._components = components;
|
||||
this._locations = locations;
|
||||
}
|
||||
|
||||
async components(): Promise<Component[]> {
|
||||
return this.#components.slice();
|
||||
return this._components.slice();
|
||||
}
|
||||
|
||||
async component(id: string): Promise<Component> {
|
||||
const item = this.#components.find((i) => i.id === id);
|
||||
const item = this._components.find(i => i.id === id);
|
||||
if (!item) {
|
||||
throw new NotFoundError(`Found no component with ID ${id}`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async addLocation(location: AddLocationRequest): Promise<Location> {
|
||||
const l = { id: uuidv4(), type: location.type, target: location.target };
|
||||
this._locations.push(l);
|
||||
return l;
|
||||
}
|
||||
|
||||
async removeLocation(id: string): Promise<void> {
|
||||
this._locations = this._locations.filter(l => l.id !== id);
|
||||
}
|
||||
|
||||
async location(id: string): Promise<Location> {
|
||||
const location = this._locations.find(l => l.id === id);
|
||||
if (!location) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
async locations(): Promise<Location[]> {
|
||||
return this._locations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './AggregatorInventory';
|
||||
export * from './DatabaseInventory';
|
||||
export * from './StaticInventory';
|
||||
export * from './types';
|
||||
|
||||
@@ -14,11 +14,35 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as yup from 'yup';
|
||||
|
||||
export type Component = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
id: string;
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type AddLocationRequest = {
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export const addLocationRequestShape: yup.Schema<AddLocationRequest> = yup
|
||||
.object({
|
||||
type: yup.string().required(),
|
||||
target: yup.string().required(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
export type Inventory = {
|
||||
components(): Promise<Component[]>;
|
||||
component(id: string): Promise<Component>;
|
||||
addLocation(location: AddLocationRequest): Promise<Location>;
|
||||
removeLocation(id: string): Promise<void>;
|
||||
location(id: string): Promise<Location>;
|
||||
locations(): Promise<Location[]>;
|
||||
};
|
||||
|
||||
@@ -14,23 +14,51 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import express, { Request } from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Inventory } from '../inventory';
|
||||
import { Logger } from 'winston';
|
||||
import yup from 'yup';
|
||||
import { addLocationRequestShape, Inventory } from '../inventory';
|
||||
|
||||
export interface RouterOptions {
|
||||
inventory: Inventory;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
async function validateRequestBody<T>(
|
||||
req: Request,
|
||||
schema: yup.Schema<T>,
|
||||
): Promise<T> {
|
||||
const contentType = req.header('content-type');
|
||||
if (!contentType) {
|
||||
throw new InputError('Content-Type missing');
|
||||
} else if (!contentType.match(/^application\/json($|;)/)) {
|
||||
throw new InputError('Illegal Content-Type');
|
||||
}
|
||||
|
||||
const body = req.body;
|
||||
if (!body) {
|
||||
throw new InputError('Missing request body');
|
||||
}
|
||||
|
||||
try {
|
||||
await schema.validate(body, { strict: true });
|
||||
} catch (e) {
|
||||
throw new InputError(`Malformed request: ${e}`);
|
||||
}
|
||||
|
||||
return body as T;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const inventory = options.inventory;
|
||||
const logger = options.logger.child({ plugin: 'inventory' });
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Components
|
||||
router
|
||||
.get('/components', async (req, res) => {
|
||||
const components = await inventory.components();
|
||||
@@ -42,6 +70,28 @@ export async function createRouter(
|
||||
res.status(200).send(component);
|
||||
});
|
||||
|
||||
// Locations
|
||||
router
|
||||
.post('/locations', async (req, res) => {
|
||||
const input = await validateRequestBody(req, addLocationRequestShape);
|
||||
const output = await inventory.addLocation(input);
|
||||
res.status(201).send(output);
|
||||
})
|
||||
.get('/locations', async (req, res) => {
|
||||
const output = await inventory.locations();
|
||||
res.status(200).send(output);
|
||||
})
|
||||
.get('/locations/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const output = await inventory.location(id);
|
||||
res.status(200).send(output);
|
||||
})
|
||||
.delete('/locations/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
await inventory.removeLocation(id);
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.set('logger', logger);
|
||||
app.use('/', router);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { AggregatorInventory, StaticInventory } from '../inventory';
|
||||
import { StaticInventory } from '../inventory';
|
||||
import { createStandaloneApplication } from './standaloneApplication';
|
||||
|
||||
export interface ServerOptions {
|
||||
@@ -30,14 +30,14 @@ export async function startStandaloneServer(
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'inventory-backend' });
|
||||
|
||||
const inventory = new AggregatorInventory();
|
||||
inventory.enlist(
|
||||
new StaticInventory([
|
||||
const inventory = new StaticInventory(
|
||||
[
|
||||
{ id: 'component1' },
|
||||
{ id: 'component2' },
|
||||
{ id: 'component3' },
|
||||
{ id: 'component4' },
|
||||
]),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
logger.debug('Creating application...');
|
||||
|
||||
@@ -956,6 +956,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
|
||||
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.4.0", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||
version "7.8.6"
|
||||
resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
|
||||
@@ -4542,6 +4549,11 @@
|
||||
dependencies:
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@types/uuid@^7.0.3":
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.3.tgz#45cd03e98e758f8581c79c535afbd4fc27ba7ac8"
|
||||
integrity sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==
|
||||
|
||||
"@types/webpack-dev-server@*", "@types/webpack-dev-server@^3.10.0":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.10.1.tgz#93b7133cc9dab1ca1b76659f5ef8b763ad54c28a"
|
||||
@@ -4598,6 +4610,11 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yup@^0.28.2":
|
||||
version "0.28.2"
|
||||
resolved "https://registry.npmjs.org/@types/yup/-/yup-0.28.2.tgz#ea6597d7bd48ecea94b81f8de7fd60fba07a4628"
|
||||
integrity sha512-VvXPMsvUnkDIWHFIalJROljLSv9m9TaXKng5sLFUkmuybgY64AeBbZ5tOPZ9sjSpqhJFeOXIc22JSQDO6SCONg==
|
||||
|
||||
"@types/zen-observable@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
|
||||
@@ -9907,6 +9924,11 @@ flush-write-stream@^1.0.0:
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.3.6"
|
||||
|
||||
fn-name@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
|
||||
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
|
||||
|
||||
focus-lock@^0.6.6:
|
||||
version "0.6.6"
|
||||
resolved "https://registry.npmjs.org/focus-lock/-/focus-lock-0.6.6.tgz#98119a755a38cfdbeda0280eaa77e307eee850c7"
|
||||
@@ -13910,6 +13932,11 @@ lockfile@^1.0.4:
|
||||
dependencies:
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
lodash-es@^4.17.11:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||
|
||||
lodash._baseuniq@~4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
|
||||
@@ -17287,6 +17314,11 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0,
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
property-expr@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330"
|
||||
integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g==
|
||||
|
||||
property-information@^5.0.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.npmjs.org/property-information/-/property-information-5.4.0.tgz#16e08f13f4e5c4a7be2e4ec431c01c4f8dba869a"
|
||||
@@ -20186,6 +20218,11 @@ symbol.prototype.description@^1.0.0:
|
||||
es-abstract "^1.17.0-next.1"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
synchronous-promise@^2.0.10:
|
||||
version "2.0.11"
|
||||
resolved "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.11.tgz#e92022b0754e916f556d3ace1626d24a24214b7e"
|
||||
integrity sha512-8/L5FOCjnlK0FoAfj+NqdCaImMKvEyOEzGmdfcezKp5K9HIukm4akx72endvM87eS/gU8kOxiMQflpC/CdkQAg==
|
||||
|
||||
table@^5.2.3:
|
||||
version "5.4.6"
|
||||
resolved "https://registry.npmjs.org/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
||||
@@ -20580,6 +20617,11 @@ toposort@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
|
||||
integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
|
||||
|
||||
toposort@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
|
||||
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
|
||||
|
||||
touch@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
|
||||
@@ -21220,6 +21262,11 @@ uuid@^7.0.3:
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
|
||||
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
|
||||
|
||||
uuid@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
|
||||
integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
|
||||
@@ -22199,6 +22246,19 @@ yn@3.1.1:
|
||||
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
|
||||
yup@^0.28.5:
|
||||
version "0.28.5"
|
||||
resolved "https://registry.npmjs.org/yup/-/yup-0.28.5.tgz#85cabb4000d3623ef69be81551190692e631a8a5"
|
||||
integrity sha512-7JZcvpUGUxMKoaEtcoMEM8lCWRaueGNH/A3EhL/UWqfbFm3uloiI+x59Yq4nzhbbYWUTwAsCteaZOJ+VbqI1uw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.6"
|
||||
fn-name "~3.0.0"
|
||||
lodash "^4.17.15"
|
||||
lodash-es "^4.17.11"
|
||||
property-expr "^2.0.2"
|
||||
synchronous-promise "^2.0.10"
|
||||
toposort "^2.0.2"
|
||||
|
||||
zen-observable@^0.8.15:
|
||||
version "0.8.15"
|
||||
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
|
||||
|
||||
Reference in New Issue
Block a user