diff --git a/CODEOWNERS b/CODEOWNERS index 483b38b6e33f..2d4bef1249f5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -202,6 +202,7 @@ /packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts @agnes512 @hacksparrow @nabdelgadir /packages/cli/generators/relation @agnes512 @hacksparrow @nabdelgadir /examples/todo-list @agnes512 @hacksparrow @nabdelgadir +/examples/references-many @agnes512 @hacksparrow @nabdelgadir /docs/site/tutorials/todo-list/ @agnes512 @hacksparrow @nabdelgadir # diff --git a/docs/site/MONOREPO.md b/docs/site/MONOREPO.md index b4518e71f7fa..00aaf2ab8f91 100644 --- a/docs/site/MONOREPO.md +++ b/docs/site/MONOREPO.md @@ -40,6 +40,7 @@ one in the monorepo: `npm run update-monorepo-file` | [examples/todo-jwt](https://github.com/loopbackio/loopback-next/tree/master/examples/todo-jwt) | @loopback/example-todo-jwt | A modified Todo tutorial how to build an application with JWT authentication and LoopBack 4 | | [examples/todo-list](https://github.com/loopbackio/loopback-next/tree/master/examples/todo-list) | @loopback/example-todo-list | Continuation of the todo example using relations in LoopBack 4. | | [examples/validation-app](https://github.com/loopbackio/loopback-next/tree/master/examples/validation-app) | @loopback/example-validation-app | An example demonstrating how to add validation in a LoopBack 4 application | +| [examples/references-many](https://github.com/loopbackio/loopback-next/tree/master/examples/references-many) | @loopback/example-references-many | An example of references many relation in a LoopBack 4 | | [examples/webpack](https://github.com/loopbackio/loopback-next/tree/master/examples/webpack) | @loopback/example-webpack | An example to bundle @loopback/core using webpack | | [extensions/apiconnect](https://github.com/loopbackio/loopback-next/tree/master/extensions/apiconnect) | @loopback/apiconnect | An extension for IBM API Connect | | [extensions/authentication-jwt](https://github.com/loopbackio/loopback-next/tree/master/extensions/authentication-jwt) | @loopback/authentication-jwt | Extension for the prototype of JWT authentication | diff --git a/docs/site/ReferencesMany-relation.md b/docs/site/ReferencesMany-relation.md new file mode 100644 index 000000000000..fcee4d0b80f0 --- /dev/null +++ b/docs/site/ReferencesMany-relation.md @@ -0,0 +1,379 @@ +--- +lang: en +title: 'referencesMany Relation' +keywords: LoopBack 4.0, LoopBack 4, Node.js, TypeScript, OpenAPI, Model Relation +sidebar: lb4_sidebar +permalink: /doc/en/lb4/ReferencesMany-relation.html +--- + +## Overview + +{% include important.html content="Please read [Relations](Relations.md) first." %} + +LoopBack relations enable you to create connections between models and provide +navigation APIs to deal with a graph of model instances. In addition to the +traditional ones, LoopBack supports `referencesMany` relation that embeds an +array of foreign keys to reference other objects. For example: + +```json +{ + "id": 1, + "name": "John Smith", + "accountIds": ["saving-01", "checking-01"] +} +``` + +To add a `referencesMany` relation to your LoopBack application, you need to +perform the following steps: + +1. Add a property to your source model to define the array of foreign keys. +2. Modify the source model repository class to provide an accessor function for + obtaining the target model instances. +3. Call the accessor function in your controller methods. + +## Defining a referencesMany relation + +This section describes how to define a `referencesMany` relation at the model +level using the `@referencesMany` decorator to define the array of foreign keys. + +LB4 also provides an CLI tool `lb4 relation` to generate `referencesMany` +relation for you. Before you check out the +[`Relation Generator`](https://loopback.io/doc/en/lb4/Relation-generator.html) +page, read on to learn how you can define relations to meet your requirements. + +### Relation Metadata + +LB4 uses three `keyFrom`, `keyTo` and `name` fields in the `referencesMany` +relation metadata to configure relations. The relation metadata has its own +default values for these three fields: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Field NameDescriptionDefault ValueExample
keyFromthe array property of foreign keysthe target model name appended with `Ids` in camel caseCustomer.accountIds
keyTothe source key of the target modelthe primary key in the target modelAccount.id
namethe name of the relationthe plural name of target modelaccounts
+ +We recommend to use default values. If you'd like to customize the name of +foreign keys or the relation name, you'll need to specify some fields through +the relation decorator. + +The standard naming convention for the property of foreign keys in the source +model is `target name` + `Ids` (for example, Customer.accountIds). + +{% include code-caption.html content="/src/models/customer.model.ts" %} + +```ts +import {referencesMany, Entity, model, property} from '@loopback/repository'; +import {Account} from './account.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @property({ + type: 'string', + }) + name: string; + + @referencesMany(() => Account) + accountIds: number[]; // relation name will default to `accounts` + + constructor(data: Partial) { + super(data); + } +} + +export interface CustomerRelations { + // describe navigational properties here +} + +export type CustomerWithRelations = Customer & CustomerRelations; +``` + +If the property name of foreign keys in the source model has to be customized +(`account_ids` instead of `accountIds` for example), the relation name has to be +explicitly specified in the `name` attribute of the relation definition. +Otherwise the _default relation name_ generates by LB4 (`account_ids` in this +case) will be the same as the customized name of foreign keys, which is invalid. + +{% include warning.html content="Make sure that you have different names for the property name of foreign keys and the relation name in ReferencesMany relations."%} + +```ts +// import statements +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @property({ + type: 'string', + }) + name: string; + + @referencesMany( + () => Account, + {name: 'accounts'}, // specify the relation name if fk is customized + ) + account_ids: number[]; // customized foreign keys + + // other properties, constructor, etc. +} +``` + +If you need to use _different names for models and database columns_, to use +`account_ids` as db column name other than `accountIds` for example, passing the +column name in the third argument to the `referencesMany` decorator would allow +you to do so: + +```ts +class Customer extends Entity { + // constructor, properties, etc. + @referencesMany(() => Account, {keyFrom: 'accountIds'}, {name: 'account_ids'}) + accountIds: number[]; +} +``` + +If you need to use another attribute other than the id property to be the source +key of the target model (joining two tables on non-primary attribute), the +`keyTo` attribute in the relation definition has to be stated explicitly. + +```ts +class Customer extends Entity { + // constructor, properties, etc. + @referencesMany(() => Account, {keyTo: 'customized_target_property'}) + accountIds: number[]; +} + +export interface CustomerRelations { + accounts?: AccountWithRelations[]; +} +``` + +{% include important.html content="LB4 doesn't support composite keys for now. e.g joining two tables with more than one source key. Related GitHub issue: [Composite primary/foreign keys](https://github.com/loopbackio/loopback-next/issues/1830)" %} + +## Configuring a referencesMany relation + +The configuration and resolution of a `referencesMany` relation takes place at +the repository level. Once `referencesMany` relation is defined on the source +model, then there are a couple of steps involved to configure it and use it. On +the source repository, the following are required: + +- In the constructor of your source repository class, use + [Dependency Injection](Dependency-injection.md) to receive a getter function + for obtaining an instance of the target repository. +- Declare a property with the factory function type + `ReferencesManyAccessor` on the + source repository class. +- call the `createReferencesManyAccessorFor` function in the constructor of the + source repository class with the relation name (decorated relation property on + the source model) and target repository instance and assign it the property + mentioned above. + +The following code snippet shows how it would look like: + +{% include code-caption.html +content="/src/repositories/customer.repository.ts" %} + +```ts +import {Getter, inject} from '@loopback/core'; +import { + ReferencesManyAccessor, + DefaultCrudRepository, + juggler, + repository, +} from '@loopback/repository'; +import {Account, Customer, CustomerRelations} from '../models'; +import {AccountRepository} from '../repositories'; + +export class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id, + CustomerRelations +> { + public readonly accounts: ReferencesManyAccessor< + Account, + typeof Customer.prototype.id + >; + + constructor( + @inject('datasources.db') protected db: juggler.DataSource, + @repository.getter('AccountRepository') + accountRepositoryGetter: Getter, + ) { + super(Customer, db); + this.accounts = this.createReferencesManyAccessorFor( + 'accounts', + accountRepositoryGetter, + ); + } +} +``` + +`ReferencesManyAccessor` is a function accepting the primary key (id) of a +source model instance (e.g. `customer.id`) and returning back the related target +model instances (e.g. a `Account[]`). See also +[API Docs](https://loopback.io/doc/en/lb4/apidocs.repository.belongstoaccessor.html) + +{% include note.html content="Notice that `CustomerRepository.create()` expects an `Customer` model only, navigational properties are not expected to be included in the target data. For instance, the following request will be rejected: +`customerRepository.create({` +` id: 1,` +` name: 'John'` +` accountIds: [1]` +` accounts:[{id: 1, balance: 0}] // rejected` +`})`" %} + +## Querying related models + +Different from LB3, LB4 creates a different inclusion resolver for each relation +type to query related models. Each **relation** has its own inclusion resolver +`inclusionResolver`. And each **repository** has a built-in property +`inclusionResolvers` as a registry for its inclusionResolvers. + +A `referencesMany` relation has an `inclusionResolver` function as a property. +It fetches target models for the given list of source model instances. + +### Use the relation between `Customer` and `Account` we show above. + +After setting up the relation in the repository class, the inclusion resolver +allows users to retrieve all customers along with their related accounts through +the following code at the repository level: + +```ts +customerRepo.find({include: ['accounts']}); +``` + +or use APIs with controllers: + +``` +GET http://localhost:3000/customers?filter[include][]=accounts +``` + +### Enable/disable the inclusion resolvers + +- Base repository classes have a public property `inclusionResolvers`, which + maintains a map containing inclusion resolvers for each relation. +- The `inclusionResolver` of a certain relation is built when the source + repository class calls the `createReferencesManyAccessorFor` function in the + constructor with the relation name. +- Call `registerInclusionResolver` to add the resolver of that relation to the + `inclusionResolvers` map. (As we realized in LB3, not all relations are + allowed to be traversed. Users can decide to which resolvers can be added.) + The first parameter is the name of the relation. + +The following code snippet shows how to register the inclusion resolver for the +referencesMany relation 'accounts': + +```ts +export class CustomerRepository extends DefaultCrudRepository { + accounts: ReferencesManyAccessor; + + constructor( + dataSource: juggler.DataSource, + accountRepositoryGetter: Getter, + ) { + super(Customer, dataSource); + + // we already have this line to create a ReferencesManyRepository factory + this.accounts = this.createReferencesManyAccessorFor( + 'accounts', + accountRepositoryGetter, + ); + + // add this line to register inclusion resolver. + this.registerInclusionResolver('accounts', this.accounts.inclusionResolver); + } +} +``` + +- We can simply include the relation in queries via `find()`, `findOne()`, and + `findById()` methods. For example, these queries return all customers with + their accounts: + + if you process data at the repository level: + + ```ts + customerRepository.find({include: ['accounts']}); + ``` + + this is the same as the url: + + ``` + GET http://localhost:3000/customers?filter[include][]=accounts + ``` + + which returns: + + ```ts + [ + { + id: 1, + name: 'John', + accountIds: [12, 16], + accounts: [ + { + id: 12, + balance: 99, + }, + { + id: 16, + balance: 0, + }, + ], + }, + { + id: 2, + name: 'Dave', + accountIds: [4], + accounts: [ + { + id: 4, + balance: 10, + }, + ], + }, + ]; + ``` + +- You can delete a relation from `inclusionResolvers` to disable the inclusion + for a certain relation. e.g + `customerRepository.inclusionResolvers.delete('accounts')` + +### Query multiple relations + +It is possible to query several relations or nested include relations with +custom scope once you have the inclusion resolver of each relation set up. Check +[HasMany - Query multiple relations](HasMany-relation.md#query-multiple-relations) +for the usage and examples. diff --git a/docs/site/Relation-generator.md b/docs/site/Relation-generator.md index f4190c1b1ee3..322b41e8e04b 100644 --- a/docs/site/Relation-generator.md +++ b/docs/site/Relation-generator.md @@ -138,6 +138,7 @@ The tool will prompt you for: - [HasManyThrough](HasManyThrough-relation.md) - [HasOne](HasOne-relation.md) - [BelongsTo](BelongsTo-relation.md) + - [ReferencesMany](ReferencesMany-relation.md) - Name of the source model (`sourceModel`). Prompts a list of available models to choose from as the source model of the relation. diff --git a/docs/site/Relations.md b/docs/site/Relations.md index 9cac7c2401c4..293337fe57f5 100644 --- a/docs/site/Relations.md +++ b/docs/site/Relations.md @@ -45,6 +45,7 @@ Here are the currently supported relations: - [BelongsTo](BelongsTo-relation.md) - [HasOne](HasOne-relation.md) - [HasManyThrough](HasManyThrough-relation.md) +- [ReferencesMany](ReferencesMany-relation.md) {% include note.html content=" The `hasMany` relation may alternatively be implemented using the diff --git a/docs/site/decorators/Decorators_repository.md b/docs/site/decorators/Decorators_repository.md index 84077acc84a3..c44ae41af998 100644 --- a/docs/site/decorators/Decorators_repository.md +++ b/docs/site/decorators/Decorators_repository.md @@ -255,6 +255,31 @@ export class Todo extends Entity { } ``` +#### ReferencesMany Decorator + +Syntax: +`@referencesMany(targetResolver: EntityResolver, definition?: Partial)` + +A ReferencesMany relation embeds an array of foreign keys to reference other +objects. E.g. a `Customer` model references many `Account` objects. See +[ReferencesMany relation](../ReferencesMany-relation.md) for more details. + +{% include code-caption.html content="customer.model.ts" %} + +```ts +import {referencesMany} from '@loopback/repository'; +import {Account} from './account.model'; + +export class Customer extends Entity { + // properties + + @referencesMany(() => Account) + accountIds: number[]; + + // etc +} +``` + #### Other Decorators The following decorators are not implemented yet. To see their progress, please diff --git a/docs/site/migration/models/relations.md b/docs/site/migration/models/relations.md index fa8d7b2abd37..e7e2e1ba63f7 100644 --- a/docs/site/migration/models/relations.md +++ b/docs/site/migration/models/relations.md @@ -69,6 +69,7 @@ LoopBack 3: - [HasOne](../../HasOne-relation.md) - [BelongsTo](../../BelongsTo-relation.md) - [HasManyThrough](../../HasManyThrough-relation.md) +- [ReferencesMany](../../ReferencesMany-relation.md) Other relations types are not supported yet, you can subscribe to our progress in the high-level tracking issue @@ -80,5 +81,3 @@ issue, for example: [loopback-next#2308](https://github.com/loopbackio/loopback-next/issues/2308) - Polymorphic relations - [loopback-next#2487](https://github.com/loopbackio/loopback-next/issues/2487) -- ReferencesMany - - [loopback-next#2488](https://github.com/loopbackio/loopback-next/issues/1450) diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index 725d3645e33c..5f5bc4ed89b1 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -757,6 +757,10 @@ children: url: HasManyThrough-relation.html output: 'web, pdf' + - title: 'ReferencesMany Relation' + url: ReferencesMany-relation.html + output: 'web, pdf' + - title: 'Repository' url: Repository.html output: 'web, pdf' diff --git a/examples/references-many/.dockerignore b/examples/references-many/.dockerignore new file mode 100644 index 000000000000..7aecc7e3dda5 --- /dev/null +++ b/examples/references-many/.dockerignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +/dist +# Cache used by TypeScript's incremental build +*.tsbuildinfo diff --git a/examples/references-many/.eslintrc.js b/examples/references-many/.eslintrc.js new file mode 100644 index 000000000000..980c8cb5bf14 --- /dev/null +++ b/examples/references-many/.eslintrc.js @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = { + extends: ['@loopback/eslint-config'], +}; diff --git a/examples/references-many/.npmignore b/examples/references-many/.npmignore new file mode 100644 index 000000000000..f86f365ab85b --- /dev/null +++ b/examples/references-many/.npmignore @@ -0,0 +1,2 @@ +# Exclude *.tsbuildinfo - cache for tsc incremental builds +*.tsbuildinfo diff --git a/examples/references-many/.npmrc b/examples/references-many/.npmrc new file mode 100644 index 000000000000..cafe685a112d --- /dev/null +++ b/examples/references-many/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/examples/references-many/.prettierignore b/examples/references-many/.prettierignore new file mode 100644 index 000000000000..c6911da9e1e8 --- /dev/null +++ b/examples/references-many/.prettierignore @@ -0,0 +1,2 @@ +dist +*.json diff --git a/examples/references-many/.prettierrc b/examples/references-many/.prettierrc new file mode 100644 index 000000000000..2e48c76c3184 --- /dev/null +++ b/examples/references-many/.prettierrc @@ -0,0 +1,7 @@ +{ + "bracketSpacing": false, + "singleQuote": true, + "printWidth": 80, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/examples/references-many/.vscode/settings.json b/examples/references-many/.vscode/settings.json new file mode 100644 index 000000000000..7c80afccc2b4 --- /dev/null +++ b/examples/references-many/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "editor.rulers": [80], + "editor.tabCompletion": "on", + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "editor.formatOnSave": true, + + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.hg": true, + "**/.svn": true, + "**/CVS": true, + "dist": true, + }, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + + "typescript.tsdk": "./node_modules/typescript/lib" +} diff --git a/examples/references-many/.vscode/tasks.json b/examples/references-many/.vscode/tasks.json new file mode 100644 index 000000000000..555f092d81eb --- /dev/null +++ b/examples/references-many/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Watch and Compile Project", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "build:watch"], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$tsc-watch" + }, + { + "label": "Build, Test and Lint", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "test:dev"], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": ["$tsc", "$eslint-stylish", "$eslint-compact"] + } + ] +} diff --git a/examples/references-many/CHANGELOG.md b/examples/references-many/CHANGELOG.md new file mode 100644 index 000000000000..e4d87c4d45c4 --- /dev/null +++ b/examples/references-many/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/examples/references-many/Dockerfile b/examples/references-many/Dockerfile new file mode 100644 index 000000000000..ae6b17598621 --- /dev/null +++ b/examples/references-many/Dockerfile @@ -0,0 +1,28 @@ +# Check out https://hub.docker.com/_/node to select a new base image +FROM node:16-slim + +# Set to a non-root built-in user `node` +USER node + +# Create app directory (with user `node`) +RUN mkdir -p /home/node/app + +WORKDIR /home/node/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY --chown=node package*.json ./ + +RUN npm install + +# Bundle app source code +COPY --chown=node . . + +RUN npm run build + +# Bind to all network interfaces so that it can be mapped to the host OS +ENV HOST=0.0.0.0 PORT=3000 + +EXPOSE ${PORT} +CMD [ "node", "." ] diff --git a/examples/references-many/LICENSE b/examples/references-many/LICENSE new file mode 100644 index 000000000000..c5ce0c376ceb --- /dev/null +++ b/examples/references-many/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2018,2019. +Node module: @loopback/example-references-many +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/examples/references-many/README.md b/examples/references-many/README.md new file mode 100644 index 000000000000..709c4724a896 --- /dev/null +++ b/examples/references-many/README.md @@ -0,0 +1,49 @@ +# @loopback/example-references-many + +## Try it out + +1. Run the `lb4 example` command to select and clone the repository: + + ```sh + lb4 example references-many + ``` + +2. Switch to the directory. + + ```sh + cd loopback4-example-references-many + ``` + +3. Finally, start the application! + + ```sh + $ npm start + + Server is running at http://127.0.0.1:3000 + ``` + +Feel free to look around in the application's code to get a feel for how it +works. + +### Bugs/Feedback + +Open an issue in [loopback-next](https://github.com/loopbackio/loopback-next) +and we'll take a look! + +## Contributions + +- [Guidelines](https://github.com/loopbackio/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/loopbackio/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/loopbackio/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/examples/references-many/data/db.json b/examples/references-many/data/db.json new file mode 100644 index 000000000000..5a4f28814253 --- /dev/null +++ b/examples/references-many/data/db.json @@ -0,0 +1,18 @@ +{ + "ids": { + "Customer": 5, + "Account": 3 + }, + "models": { + "Customer": { + "1": "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"accountIds\":[\"1\",\"2\"],\"id\":1}", + "2": "{\"firstName\":\"Tommy\",\"lastName\":\"Lee\",\"accountIds\":[\"1\"],\"id\":2}", + "3": "{\"firstName\":\"Julian\",\"lastName\":\"Lennon\",\"accountIds\":[],\"id\":3}", + "4": "{\"firstName\":\"Monica\",\"lastName\":\"Raymund\",\"accountIds\":[],\"id\":4}" + }, + "Account": { + "1": "{\"balance\":0,\"id\":1}", + "2": "{\"balance\":20,\"id\":2}" + } + } +} diff --git a/examples/references-many/package-lock.json b/examples/references-many/package-lock.json new file mode 100644 index 000000000000..c6c6b56850bb --- /dev/null +++ b/examples/references-many/package-lock.json @@ -0,0 +1,3096 @@ +{ + "name": "@loopback/example-references-many", + "version": "5.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@loopback/example-references-many", + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "loopback-connector-rest": "^4.0.1", + "tslib": "^2.3.1" + }, + "devDependencies": { + "@types/lodash": "^4.14.178", + "@types/node": "^10.17.60", + "eslint": "^8.8.0", + "lodash": "^4.17.21", + "typescript": "~4.5.5" + }, + "engines": { + "node": "12 || 14 || 16 || 17" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", + "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.2.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", + "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + }, + "node_modules/accept-language": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", + "integrity": "sha1-9QJfF79lpGaoRYOMz5jNuHfYM4Q=", + "dependencies": { + "bcp47": "^1.1.2", + "stable": "^0.1.6" + } + }, + "node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcp47": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", + "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "engines": { + "node": "*" + } + }, + "node_modules/cldrjs": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.5.tgz", + "integrity": "sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "engines": { + "node": "*" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.8.0.tgz", + "integrity": "sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.0.5", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.2.0", + "espree": "^9.3.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", + "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz", + "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==", + "dev": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.1.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globalize": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.7.0.tgz", + "integrity": "sha512-faR46vTIbFCeAemyuc9E6/d7Wrx9k2ae2L60UhakztFg6VuE42gENVJNuPFtt7Sdjrk9m2w8+py7Jj+JTNy59w==", + "dependencies": { + "cldrjs": "^0.5.4" + } + }, + "node_modules/globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/invert-kv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", + "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sindresorhus/invert-kv?sponsor=1" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/jsonpath-plus": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-4.0.0.tgz", + "integrity": "sha512-e0Jtg4KAzDJKKwzbLaUtinCn0RZseWBVRTRGihSpvFlM3wTR7ExSp+PTdeTsDrLNJUe7L7JYJe8mblHX5SCT6A==", + "engines": { + "node": ">=10.0" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/lcid": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", + "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", + "dependencies": { + "invert-kv": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loopback-connector-rest": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/loopback-connector-rest/-/loopback-connector-rest-4.0.1.tgz", + "integrity": "sha512-iAyTlOMtZqo/oI6lUlUIdA5Q0+1cqms81o3LnHRKGiyt4CjUSvXgtBvJBlhiqvwbL+oDTItYwglpiacrdfUSBw==", + "dependencies": { + "debug": "^4.1.0", + "jsonpath-plus": "^4.0.0", + "lodash": "^4.17.11", + "methods": "^1.1.1", + "mime": "^2.3.1", + "qs": "^6.1.0", + "request": "^2.53.0", + "strong-globalize": "^6.0.5", + "traverse": "^0.6.6" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/mem": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", + "integrity": "sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==", + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^2.1.0", + "p-is-promise": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dependencies": { + "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-locale": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz", + "integrity": "sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==", + "dependencies": { + "execa": "^4.0.0", + "lcid": "^3.0.0", + "mem": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strong-globalize": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-6.0.5.tgz", + "integrity": "sha512-7nfUli41TieV9/TSc0N62ve5Q4nfrpy/T0nNNy6TyD3vst79QWmeylCyd3q1gDxh8dqGEtabLNCdPQP1Iuvecw==", + "dependencies": { + "accept-language": "^3.0.18", + "debug": "^4.2.0", + "globalize": "^1.6.0", + "lodash": "^4.17.20", + "md5": "^2.3.0", + "mkdirp": "^1.0.4", + "os-locale": "^5.0.0", + "yamljs": "^0.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + } + }, + "dependencies": { + "@eslint/eslintrc": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", + "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.2.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", + "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "dev": true + }, + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + }, + "accept-language": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", + "integrity": "sha1-9QJfF79lpGaoRYOMz5jNuHfYM4Q=", + "requires": { + "bcp47": "^1.1.2", + "stable": "^0.1.6" + } + }, + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bcp47": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", + "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "cldrjs": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.5.tgz", + "integrity": "sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.8.0.tgz", + "integrity": "sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.0.5", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.2.0", + "espree": "^9.3.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", + "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "dev": true + }, + "espree": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz", + "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==", + "dev": true, + "requires": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.1.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globalize": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.7.0.tgz", + "integrity": "sha512-faR46vTIbFCeAemyuc9E6/d7Wrx9k2ae2L60UhakztFg6VuE42gENVJNuPFtt7Sdjrk9m2w8+py7Jj+JTNy59w==", + "requires": { + "cldrjs": "^0.5.4" + } + }, + "globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "invert-kv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", + "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonpath-plus": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-4.0.0.tgz", + "integrity": "sha512-e0Jtg4KAzDJKKwzbLaUtinCn0RZseWBVRTRGihSpvFlM3wTR7ExSp+PTdeTsDrLNJUe7L7JYJe8mblHX5SCT6A==" + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "lcid": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", + "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", + "requires": { + "invert-kv": "^3.0.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loopback-connector-rest": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/loopback-connector-rest/-/loopback-connector-rest-4.0.1.tgz", + "integrity": "sha512-iAyTlOMtZqo/oI6lUlUIdA5Q0+1cqms81o3LnHRKGiyt4CjUSvXgtBvJBlhiqvwbL+oDTItYwglpiacrdfUSBw==", + "requires": { + "debug": "^4.1.0", + "jsonpath-plus": "^4.0.0", + "lodash": "^4.17.11", + "methods": "^1.1.1", + "mime": "^2.3.1", + "qs": "^6.1.0", + "request": "^2.53.0", + "strong-globalize": "^6.0.5", + "traverse": "^0.6.6" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "mem": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", + "integrity": "sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==", + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^2.1.0", + "p-is-promise": "^2.1.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + }, + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + }, + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "requires": { + "mime-db": "1.51.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "os-locale": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz", + "integrity": "sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==", + "requires": { + "execa": "^4.0.0", + "lcid": "^3.0.0", + "mem": "^5.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strong-globalize": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-6.0.5.tgz", + "integrity": "sha512-7nfUli41TieV9/TSc0N62ve5Q4nfrpy/T0nNNy6TyD3vst79QWmeylCyd3q1gDxh8dqGEtabLNCdPQP1Iuvecw==", + "requires": { + "accept-language": "^3.0.18", + "debug": "^4.2.0", + "globalize": "^1.6.0", + "lodash": "^4.17.20", + "md5": "^2.3.0", + "mkdirp": "^1.0.4", + "os-locale": "^5.0.0", + "yamljs": "^0.3.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + } + } + } + } +} diff --git a/examples/references-many/package.json b/examples/references-many/package.json new file mode 100644 index 000000000000..89db885b6589 --- /dev/null +++ b/examples/references-many/package.json @@ -0,0 +1,77 @@ +{ + "name": "@loopback/example-references-many", + "description": "Example of the references many relation in LoopBack 4.", + "version": "5.0.1", + "keywords": [ + "loopback", + "LoopBack", + "example", + "tutorial", + "relations", + "CRUD", + "models", + "referenceMany", + "HasMany" + ], + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": "IBM Corp.", + "copyright.owner": "IBM Corp.", + "repository": { + "type": "git", + "url": "https://github.com/loopbackio/loopback-next.git", + "directory": "examples/references-many" + }, + "engines": { + "node": "12 || 14 || 16 || 17" + }, + "scripts": { + "build": "lb-tsc", + "build:watch": "lb-tsc --watch", + "clean": "lb-clean *example-references-many*.tgz dist *.tsbuildinfo package", + "lint": "npm run prettier:check && npm run eslint", + "lint:fix": "npm run eslint:fix && npm run prettier:fix", + "prettier:cli": "lb-prettier \"**/*.ts\"", + "prettier:check": "npm run prettier:cli -- -l", + "prettier:fix": "npm run prettier:cli -- --write", + "eslint": "lb-eslint --report-unused-disable-directives .", + "eslint:fix": "npm run eslint -- --fix", + "pretest": "npm run rebuild", + "test": "lb-mocha \"dist/__tests__/**/*.js\"", + "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", + "verify": "npm pack && tar xf loopback-references-many*.tgz && tree package && npm run clean", + "premigrate": "npm run build ", + "migrate": "node ./dist/migrate", + "preopenapi-spec": "npm run build", + "openapi-spec": "node ./dist/openapi-spec", + "rebuild": "npm run clean && npm run build", + "prestart": "npm run rebuild", + "start": "node ." + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@loopback/boot": "^4.0.1", + "@loopback/core": "^3.0.1", + "@loopback/repository": "^4.0.1", + "@loopback/rest": "^11.0.1", + "@loopback/rest-explorer": "^4.0.1", + "@loopback/service-proxy": "^4.0.1", + "loopback-connector-rest": "^4.0.1", + "tslib": "^2.3.1" + }, + "devDependencies": { + "@loopback/build": "^8.0.1", + "@loopback/eslint-config": "^12.0.1", + "@loopback/http-caching-proxy": "^3.0.1", + "@loopback/repository": "^4.0.1", + "@loopback/testlab": "^4.0.1", + "@types/lodash": "^4.14.178", + "@types/node": "^10.17.60", + "eslint": "^8.8.0", + "lodash": "^4.17.21", + "typescript": "~4.5.5" + } +} diff --git a/examples/references-many/public/index.html b/examples/references-many/public/index.html new file mode 100644 index 000000000000..d93d5e277cb5 --- /dev/null +++ b/examples/references-many/public/index.html @@ -0,0 +1,72 @@ + + + + + myapp + + + + + + + + + + +
+

@loopback/example-references-many

+ +

OpenAPI spec: /openapi.json

+

API Explorer: /explorer

+
+ + + + + diff --git a/examples/references-many/src/__tests__/acceptance/account.acceptance.ts b/examples/references-many/src/__tests__/acceptance/account.acceptance.ts new file mode 100644 index 000000000000..5b990bd9b84b --- /dev/null +++ b/examples/references-many/src/__tests__/acceptance/account.acceptance.ts @@ -0,0 +1,139 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {EntityNotFoundError} from '@loopback/repository'; +import {Client, createRestAppClient, expect, toJSON} from '@loopback/testlab'; +import {ReferencesManyApplication} from '../../application'; +import {Account} from '../../models/'; +import {AccountRepository} from '../../repositories/'; +import { + givenRunningApplicationWithCustomConfiguration, + givenAccount, + givenAccountInstance, + givenAccountRepositories, +} from '../helpers'; + +describe('ReferencesManyApplication', () => { + let app: ReferencesManyApplication; + let client: Client; + let accountRepo: AccountRepository; + + before(async () => { + app = await givenRunningApplicationWithCustomConfiguration(); + }); + after(() => app.stop()); + + before(async () => { + ({accountRepo} = await givenAccountRepositories(app)); + }); + before(() => { + client = createRestAppClient(app); + }); + + beforeEach(async () => { + await accountRepo.deleteAll(); + }); + + it('creates an account', async function () { + const account = givenAccount(); + const response = await client.post('/accounts').send(account).expect(200); + expect(response.body).to.containDeep(account); + const result = await accountRepo.findById(response.body.id); + expect(result).to.containDeep(account); + }); + + it('gets a count of accounts', async function () { + await givenAccountInstance(accountRepo, {balance: 22}); + await givenAccountInstance(accountRepo, {balance: 33}); + await client.get('/accounts/count').expect(200, {count: 2}); + }); + + context('when dealing with a single persisted account', () => { + let persistedAccount: Account; + + beforeEach(async () => { + persistedAccount = await givenAccountInstance(accountRepo); + }); + + it('gets an account by ID', () => { + return client + .get(`/accounts/${persistedAccount.id}`) + .send() + .expect(200, toJSON(persistedAccount)); + }); + + it('returns 404 when getting an account that does not exist', () => { + return client.get('/accounts/99999').expect(404); + }); + + it('replaces the account by ID', async () => { + const updatedAccount = givenAccount({ + balance: 44, + }); + await client + .put(`/accounts/${persistedAccount.id}`) + .send(updatedAccount) + .expect(204); + const result = await accountRepo.findById(persistedAccount.id); + expect(result).to.containEql(updatedAccount); + }); + + it('returns 404 when replacing an account that does not exist', () => { + return client.put('/accounts/99999').send(givenAccount()).expect(404); + }); + + it('updates the account by ID ', async () => { + const updatedAccount = givenAccount({ + balance: 55, + }); + await client + .patch(`/accounts/${persistedAccount.id}`) + .send(updatedAccount) + .expect(204); + const result = await accountRepo.findById(persistedAccount.id); + expect(result).to.containEql(updatedAccount); + }); + + it('returns 404 when updating an account that does not exist', () => { + return client.patch('/account/99999').send(givenAccount()).expect(404); + }); + + it('deletes the account', async () => { + await client.del(`/accounts/${persistedAccount.id}`).send().expect(204); + await expect( + accountRepo.findById(persistedAccount.id), + ).to.be.rejectedWith(EntityNotFoundError); + }); + + it('returns 404 when deleting an account that does not exist', async () => { + await client.del(`/accounts/99999`).expect(404); + }); + }); + + it('queries accounts with a filter', async () => { + await givenAccountInstance(accountRepo, {balance: 77}); + + const emptyAccount = await givenAccountInstance(accountRepo, {balance: 0}); + + await client + .get('/accounts') + .query({filter: {where: {balance: 0}}}) + .expect(200, [toJSON(emptyAccount)]); + }); + + it('updates accounts using a filter', async () => { + await givenAccountInstance(accountRepo, { + balance: 1, + }); + await givenAccountInstance(accountRepo, { + balance: 2, + }); + await client + .patch('/accounts') + .query({where: {balance: 2}}) + .send({balance: 3}) + .expect(200, {count: 1}); + }); +}); diff --git a/examples/references-many/src/__tests__/acceptance/customer.acceptance.ts b/examples/references-many/src/__tests__/acceptance/customer.acceptance.ts new file mode 100644 index 000000000000..a2a127b039a3 --- /dev/null +++ b/examples/references-many/src/__tests__/acceptance/customer.acceptance.ts @@ -0,0 +1,198 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {EntityNotFoundError} from '@loopback/repository'; +import {Client, createRestAppClient, expect, toJSON} from '@loopback/testlab'; +import {ReferencesManyApplication} from '../../application'; +import {Customer} from '../../models/'; +import {AccountRepository, CustomerRepository} from '../../repositories/'; +import { + givenRunningApplicationWithCustomConfiguration, + givenCustomer, + givenCustomerInstance, + givenCustomerRepositories, + givenAccountInstance, +} from '../helpers'; + +describe('ReferencesManyApplication', () => { + let app: ReferencesManyApplication; + let client: Client; + let customerRepo: CustomerRepository; + let accountRepo: AccountRepository; + + before(async () => { + app = await givenRunningApplicationWithCustomConfiguration(); + }); + after(() => app.stop()); + + before(async () => { + ({customerRepo, accountRepo} = await givenCustomerRepositories(app)); + }); + before(() => { + client = createRestAppClient(app); + }); + + beforeEach(async () => { + await customerRepo.deleteAll(); + await accountRepo.deleteAll(); + }); + + it('creates a customer', async function () { + const customer = givenCustomer(); + const response = await client.post('/customers').send(customer).expect(200); + expect(response.body).to.containDeep(customer); + const result = await customerRepo.findById(response.body.id); + expect(result).to.containDeep(customer); + }); + + it('gets a count of customers', async function () { + await givenCustomerInstance(customerRepo, { + firstName: 'Andrew', + lastName: 'Jackson', + }); + await givenCustomerInstance(customerRepo, { + firstName: 'James', + lastName: 'Gunn', + }); + await client.get('/customers/count').expect(200, {count: 2}); + }); + + context('when dealing with a single persisted customer', () => { + let persistedCustomer: Customer; + + beforeEach(async () => { + persistedCustomer = await givenCustomerInstance(customerRepo); + }); + + it('gets a customer by ID', () => { + return client + .get(`/customers/${persistedCustomer.id}`) + .send() + .expect(200, toJSON(persistedCustomer)); + }); + + it('returns 404 when getting a customer that does not exist', () => { + return client.get('/customers/99999').expect(404); + }); + + it('replaces the customer by ID', async () => { + const updatedCustomer = givenCustomer({ + firstName: 'Andrew', + lastName: 'Jackson', + }); + await client + .put(`/customers/${persistedCustomer.id}`) + .send(updatedCustomer) + .expect(204); + const result = await customerRepo.findById(persistedCustomer.id); + expect(result).to.containEql(updatedCustomer); + }); + + it('returns 404 when replacing a customer that does not exist', () => { + return client.put('/customers/99999').send(givenCustomer()).expect(404); + }); + + it('updates the customer by ID ', async () => { + const updatedCustomer = givenCustomer({ + firstName: 'Tommy', + lastName: 'Jeans', + }); + await client + .patch(`/customers/${persistedCustomer.id}`) + .send(updatedCustomer) + .expect(204); + const result = await customerRepo.findById(persistedCustomer.id); + expect(result).to.containEql(updatedCustomer); + }); + + it('returns 404 when updating a customer that does not exist', () => { + return client.patch('/customer/99999').send(givenCustomer()).expect(404); + }); + + it('deletes the customer', async () => { + await client.del(`/customers/${persistedCustomer.id}`).send().expect(204); + await expect( + customerRepo.findById(persistedCustomer.id), + ).to.be.rejectedWith(EntityNotFoundError); + }); + + it('returns 404 when deleting a customer that does not exist', async () => { + await client.del(`/customers/99999`).expect(404); + }); + }); + + it('queries customers with a filter', async () => { + await givenCustomerInstance(customerRepo, { + firstName: 'Andrew', + lastName: 'Jackson', + }); + + const unnamedCustomer = await givenCustomerInstance(customerRepo, { + firstName: '', + lastName: '', + }); + + await client + .get('/customers') + .query({filter: {where: {firstName: ''}}}) + .expect(200, [toJSON(unnamedCustomer)]); + }); + + it('updates customers using a filter', async () => { + await givenCustomerInstance(customerRepo, { + firstName: 'Andrew', + lastName: 'Jackson', + }); + await givenCustomerInstance(customerRepo, { + firstName: 'James', + lastName: 'Gunn', + }); + await client + .patch('/customers') + .query({where: {firstName: 'Andrew'}}) + .send({firstName: 'Tommy'}) + .expect(200, {count: 1}); + }); + + it('includes Accounts in query result', async () => { + const firstAccount = await givenAccountInstance(accountRepo, {balance: 10}); + const secondAccount = await givenAccountInstance(accountRepo, { + balance: 20, + }); + const customer = await givenCustomerInstance(customerRepo); + const customerWithAccounts = await givenCustomerInstance(customerRepo, { + accountIds: [firstAccount.id, secondAccount.id], + }); + const filter = JSON.stringify({include: ['accounts']}); + + const response = await client.get('/customers').query({filter: filter}); + + expect(response.body).to.have.length(2); + expect(response.body[0]).to.deepEqual({ + ...toJSON(customer), + accounts: [], + }); + expect(response.body[1]).to.deepEqual({ + ...toJSON(customerWithAccounts), + accounts: [toJSON(firstAccount), toJSON(secondAccount)], + }); + }); + + it('not includes a not existed Account in query result', async () => { + const notExistedId = 1; + const customer = await givenCustomerInstance(customerRepo, { + accountIds: [notExistedId], + }); + const filter = JSON.stringify({include: ['accounts']}); + + const response = await client.get('/customers').query({filter: filter}); + + expect(response.body).to.have.length(1); + expect(response.body[0]).to.deepEqual({ + ...toJSON(customer), + accounts: [], + }); + }); +}); diff --git a/examples/references-many/src/__tests__/acceptance/home-page.acceptance.ts b/examples/references-many/src/__tests__/acceptance/home-page.acceptance.ts new file mode 100644 index 000000000000..9aeb05412a98 --- /dev/null +++ b/examples/references-many/src/__tests__/acceptance/home-page.acceptance.ts @@ -0,0 +1,36 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Client} from '@loopback/testlab'; +import {ReferencesManyApplication} from '../..'; +import {setupApplication} from './test-helper'; + +describe('HomePage', () => { + let app: ReferencesManyApplication; + let client: Client; + + before('setupApplication', async () => { + ({app, client} = await setupApplication()); + }); + + after(async () => { + await app.stop(); + }); + + it('exposes a default home page', async () => { + await client + .get('/') + .expect(200) + .expect('Content-Type', /text\/html/); + }); + + it('exposes self-hosted explorer', async () => { + await client + .get('/explorer/') + .expect(200) + .expect('Content-Type', /text\/html/) + .expect(/LoopBack API Explorer/); + }); +}); diff --git a/examples/references-many/src/__tests__/acceptance/test-helper.ts b/examples/references-many/src/__tests__/acceptance/test-helper.ts new file mode 100644 index 000000000000..95c7264941cd --- /dev/null +++ b/examples/references-many/src/__tests__/acceptance/test-helper.ts @@ -0,0 +1,29 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Client, + createRestAppClient, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {ReferencesManyApplication} from '../..'; + +export async function setupApplication(): Promise<AppWithClient> { + const app = new ReferencesManyApplication({ + rest: givenHttpServerConfig(), + }); + + await app.boot(); + await app.start(); + + const client = createRestAppClient(app); + + return {app, client}; +} + +export interface AppWithClient { + app: ReferencesManyApplication; + client: Client; +} diff --git a/examples/references-many/src/__tests__/helpers.ts b/examples/references-many/src/__tests__/helpers.ts new file mode 100644 index 000000000000..8a5946f1f256 --- /dev/null +++ b/examples/references-many/src/__tests__/helpers.ts @@ -0,0 +1,119 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {juggler} from '@loopback/repository'; +import {givenHttpServerConfig} from '@loopback/testlab'; +import {ReferencesManyApplication} from '../application'; +import {Account, Customer} from '../models'; +import {AccountRepository, CustomerRepository} from '../repositories'; + +/* + ============================================================================== + HELPER FUNCTIONS + If you find yourself creating the same helper functions across different + test files, then extracting those functions into helper modules is an easy + way to reduce duplication. + + Other tips: + + - Using the super awesome Partial<T> type in conjunction with Object.assign + means you can: + * customize the object you get back based only on what's important + to you during a particular test + * avoid writing test logic that is brittle with respect to the properties + of your object + - Making the input itself optional means you don't need to do anything special + for tests where the particular details of the input don't matter. + ============================================================================== + */ + +/** + * Generate a complete Customer object for use with tests. + * @param customer - A partial (or complete) Customer object. + */ +export function givenCustomer(customer?: Partial<Customer>) { + const data = Object.assign( + { + firstName: 'John', + lastName: 'Doe', + }, + customer, + ); + return new Customer(data); +} + +/** + * Generate a complete Account object for use with tests. + * @param account - A partial (or complete) Account object. + */ +export function givenAccount(account?: Partial<Account>) { + const data = Object.assign({balance: 999}, account); + return new Account(data); +} + +export async function givenRunningApplicationWithCustomConfiguration() { + const app = new ReferencesManyApplication({ + rest: givenHttpServerConfig(), + }); + + await app.boot(); + + /** + * Override default config for DataSource for testing so we don't write + * test data to file when using the memory connector. + */ + app.bind('datasources.config.db').to({ + name: 'db', + connector: 'memory', + }); + + // Start Application + await app.start(); + return app; +} + +export async function givenCustomerRepositories( + app: ReferencesManyApplication, +) { + const customerRepo = await app.getRepository(CustomerRepository); + const accountRepo = await app.getRepository(AccountRepository); + return {customerRepo, accountRepo}; +} + +export async function givenAccountRepositories(app: ReferencesManyApplication) { + const accountRepo = await app.getRepository(AccountRepository); + return {accountRepo}; +} + +export async function givenCustomerInstance( + customerRepo: CustomerRepository, + customer?: Partial<Customer>, +) { + return customerRepo.create(givenCustomer(customer)); +} + +export async function givenAccountInstance( + accountRepo: AccountRepository, + account?: Partial<Account>, +) { + return accountRepo.create(givenAccount(account)); +} + +export async function givenEmptyDatabase() { + const accountRepo: AccountRepository = new AccountRepository(testdb); + const customerRepo: CustomerRepository = new CustomerRepository( + testdb, + async () => accountRepo, + async () => customerRepo, + ); + + await accountRepo.deleteAll(); + await customerRepo.deleteAll(); +} + +export const testdb: juggler.DataSource = new juggler.DataSource({ + name: 'db', + connector: 'memory', +}); diff --git a/examples/references-many/src/__tests__/integration/customer.repository.integration.ts b/examples/references-many/src/__tests__/integration/customer.repository.integration.ts new file mode 100644 index 000000000000..07571b45d590 --- /dev/null +++ b/examples/references-many/src/__tests__/integration/customer.repository.integration.ts @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect, toJSON} from '@loopback/testlab'; +import {AccountRepository, CustomerRepository} from '../../repositories'; +import { + givenEmptyDatabase, + givenAccountInstance, + givenCustomerInstance, + testdb, +} from '../helpers'; + +describe('ReferencesManyRepository', () => { + let accountRepo: AccountRepository; + let customerRepo: CustomerRepository; + + before(async () => { + accountRepo = new AccountRepository(testdb); + customerRepo = new CustomerRepository( + testdb, + async () => accountRepo, + async () => customerRepo, + ); + }); + + beforeEach(givenEmptyDatabase); + + it('includes Accounts in find method result', async () => { + const account = await givenAccountInstance(accountRepo); + const customer = await givenCustomerInstance(customerRepo, { + accountIds: [account.id], + }); + + const response = await customerRepo.find({ + include: ['accounts'], + }); + + expect(toJSON(response)).to.deepEqual([ + { + ...toJSON(customer), + accounts: [toJSON(account)], + }, + ]); + }); + + it('includes Accounts in findById result', async () => { + const account = await givenAccountInstance(accountRepo); + const customer = await givenCustomerInstance(customerRepo, { + accountIds: [account.id], + }); + + const response = await customerRepo.findById(customer.id, { + include: ['accounts'], + }); + + expect(toJSON(response)).to.deepEqual({ + ...toJSON(customer), + accounts: [toJSON(account)], + }); + }); + + it('includes Accounts in findOne method result', async () => { + const account = await givenAccountInstance(accountRepo); + const customer = await givenCustomerInstance(customerRepo, { + accountIds: [account.id], + }); + + const response = await customerRepo.findOne({ + where: {id: customer.id}, + include: ['accounts'], + }); + + expect(toJSON(response)).to.deepEqual({ + ...toJSON(customer), + accounts: [toJSON(account)], + }); + }); +}); diff --git a/examples/references-many/src/__tests__/unit/controllers/account.controller.unit.ts b/examples/references-many/src/__tests__/unit/controllers/account.controller.unit.ts new file mode 100644 index 000000000000..8d2588c68a98 --- /dev/null +++ b/examples/references-many/src/__tests__/unit/controllers/account.controller.unit.ts @@ -0,0 +1,127 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-account-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + createStubInstance, + expect, + sinon, + StubbedInstanceWithSinonAccessor, +} from '@loopback/testlab'; +import {AccountController} from '../../../controllers'; +import {Account} from '../../../models'; +import {AccountRepository} from '../../../repositories'; +import {givenAccount} from '../../helpers'; + +describe('AccountController', () => { + let accountRepo: StubbedInstanceWithSinonAccessor<AccountRepository>; + + /* + ============================================================================= + TEST VARIABLES + Combining top-level objects with our resetRepositories method means we don't + need to duplicate several variable assignments (and generation statements) + in all of our test logic. + + NOTE: If you wanted to parallelize your test runs, you should avoid this + pattern since each of these tests is sharing references. + ============================================================================= + */ + let controller: AccountController; + let aAccount: Account; + let aAccountWithId: Account; + let aChangedAccount: Account; + let aListOfAccounts: Account[]; + + beforeEach(resetRepositories); + + describe('createAccount', () => { + it('creates a Account', async () => { + const create = accountRepo.stubs.create; + create.resolves(aAccountWithId); + const result = await controller.create(aAccount); + expect(result).to.eql(aAccountWithId); + sinon.assert.calledWith(create, aAccount); + }); + }); + + describe('findAccountById', () => { + it('returns a account if it exists', async () => { + const findById = accountRepo.stubs.findById; + findById.resolves(aAccountWithId); + expect(await controller.findById(aAccountWithId.id as number)).to.eql( + aAccountWithId, + ); + sinon.assert.calledWith(findById, aAccountWithId.id); + }); + }); + + describe('findAccounts', () => { + it('returns multiple accounts if they exist', async () => { + const find = accountRepo.stubs.find; + find.resolves(aListOfAccounts); + expect(await controller.find()).to.eql(aListOfAccounts); + sinon.assert.called(find); + }); + + it('returns empty list if no accounts exist', async () => { + const find = accountRepo.stubs.find; + const expected: Account[] = []; + find.resolves(expected); + expect(await controller.find()).to.eql(expected); + sinon.assert.called(find); + }); + }); + + describe('replaceAccount', () => { + it('successfully replaces existing items', async () => { + const replaceById = accountRepo.stubs.replaceById; + replaceById.resolves(); + await controller.replaceById( + aAccountWithId.id as number, + aChangedAccount, + ); + sinon.assert.calledWith(replaceById, aAccountWithId.id, aChangedAccount); + }); + }); + + describe('updateAccount', () => { + it('successfully updates existing items', async () => { + const updateById = accountRepo.stubs.updateById; + updateById.resolves(); + await controller.updateById(aAccountWithId.id as number, aChangedAccount); + sinon.assert.calledWith(updateById, aAccountWithId.id, aChangedAccount); + }); + }); + + describe('deleteAccount', () => { + it('successfully deletes existing items', async () => { + const deleteById = accountRepo.stubs.deleteById; + deleteById.resolves(); + await controller.deleteById(aAccountWithId.id as number); + sinon.assert.calledWith(deleteById, aAccountWithId.id); + }); + }); + + function resetRepositories() { + accountRepo = createStubInstance(AccountRepository); + aAccount = givenAccount(); + aAccountWithId = givenAccount({ + id: 1, + }); + aListOfAccounts = [ + aAccountWithId, + givenAccount({ + id: 2, + balance: 5, + }), + ] as Account[]; + aChangedAccount = givenAccount({ + id: aAccountWithId.id, + balance: 10, + }); + + controller = new AccountController(accountRepo); + } +}); diff --git a/examples/references-many/src/__tests__/unit/controllers/customer.controller.unit.ts b/examples/references-many/src/__tests__/unit/controllers/customer.controller.unit.ts new file mode 100644 index 000000000000..5c2e5303db35 --- /dev/null +++ b/examples/references-many/src/__tests__/unit/controllers/customer.controller.unit.ts @@ -0,0 +1,136 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/example-customer-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + createStubInstance, + expect, + sinon, + StubbedInstanceWithSinonAccessor, +} from '@loopback/testlab'; +import {CustomerController} from '../../../controllers'; +import {Customer} from '../../../models'; +import {CustomerRepository} from '../../../repositories'; +import {givenCustomer} from '../../helpers'; + +describe('CustomerController', () => { + let customerRepo: StubbedInstanceWithSinonAccessor<CustomerRepository>; + + /* + ============================================================================= + TEST VARIABLES + Combining top-level objects with our resetRepositories method means we don't + need to duplicate several variable assignments (and generation statements) + in all of our test logic. + + NOTE: If you wanted to parallelize your test runs, you should avoid this + pattern since each of these tests is sharing references. + ============================================================================= + */ + let controller: CustomerController; + let aCustomer: Customer; + let aCustomerWithId: Customer; + let aChangedCustomer: Customer; + let aListOfCustomers: Customer[]; + + beforeEach(resetRepositories); + + describe('createCustomer', () => { + it('creates a Customer', async () => { + const create = customerRepo.stubs.create; + create.resolves(aCustomerWithId); + const result = await controller.create(aCustomer); + expect(result).to.eql(aCustomerWithId); + sinon.assert.calledWith(create, aCustomer); + }); + }); + + describe('findCustomerById', () => { + it('returns a customer if it exists', async () => { + const findById = customerRepo.stubs.findById; + findById.resolves(aCustomerWithId); + expect(await controller.findById(aCustomerWithId.id as number)).to.eql( + aCustomerWithId, + ); + sinon.assert.calledWith(findById, aCustomerWithId.id); + }); + }); + + describe('findCustomers', () => { + it('returns multiple customers if they exist', async () => { + const find = customerRepo.stubs.find; + find.resolves(aListOfCustomers); + expect(await controller.find()).to.eql(aListOfCustomers); + sinon.assert.called(find); + }); + + it('returns empty list if no customers exist', async () => { + const find = customerRepo.stubs.find; + const expected: Customer[] = []; + find.resolves(expected); + expect(await controller.find()).to.eql(expected); + sinon.assert.called(find); + }); + }); + + describe('replaceCustomer', () => { + it('successfully replaces existing items', async () => { + const replaceById = customerRepo.stubs.replaceById; + replaceById.resolves(); + await controller.replaceById( + aCustomerWithId.id as number, + aChangedCustomer, + ); + sinon.assert.calledWith( + replaceById, + aCustomerWithId.id, + aChangedCustomer, + ); + }); + }); + + describe('updateCustomer', () => { + it('successfully updates existing items', async () => { + const updateById = customerRepo.stubs.updateById; + updateById.resolves(); + await controller.updateById( + aCustomerWithId.id as number, + aChangedCustomer, + ); + sinon.assert.calledWith(updateById, aCustomerWithId.id, aChangedCustomer); + }); + }); + + describe('deleteCustomer', () => { + it('successfully deletes existing items', async () => { + const deleteById = customerRepo.stubs.deleteById; + deleteById.resolves(); + await controller.deleteById(aCustomerWithId.id as number); + sinon.assert.calledWith(deleteById, aCustomerWithId.id); + }); + }); + + function resetRepositories() { + customerRepo = createStubInstance(CustomerRepository); + aCustomer = givenCustomer(); + aCustomerWithId = givenCustomer({ + id: 1, + }); + aListOfCustomers = [ + aCustomerWithId, + givenCustomer({ + id: 2, + firstName: 'Dave', + lastName: 'Brubeck', + }), + ] as Customer[]; + aChangedCustomer = givenCustomer({ + id: aCustomerWithId.id, + firstName: 'Tim', + lastName: 'Benton', + }); + + controller = new CustomerController(customerRepo); + } +}); diff --git a/examples/references-many/src/application.ts b/examples/references-many/src/application.ts new file mode 100644 index 000000000000..5a546f6fc8a9 --- /dev/null +++ b/examples/references-many/src/application.ts @@ -0,0 +1,49 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BootMixin} from '@loopback/boot'; +import {ApplicationConfig} from '@loopback/core'; +import {RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; +import { + RestExplorerBindings, + RestExplorerComponent, +} from '@loopback/rest-explorer'; +import {ServiceMixin} from '@loopback/service-proxy'; +import path from 'path'; +import {MySequence} from './sequence'; + +export {ApplicationConfig}; + +export class ReferencesManyApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options: ApplicationConfig = {}) { + super(options); + + // Set up the custom sequence + this.sequence(MySequence); + + // Set up default home page + this.static('/', path.join(__dirname, '../public')); + + // Customize @loopback/rest-explorer configuration here + this.configure(RestExplorerBindings.COMPONENT).to({ + path: '/explorer', + }); + this.component(RestExplorerComponent); + + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = { + controllers: { + // Customize ControllerBooter Conventions here + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, + }, + }; + } +} diff --git a/examples/references-many/src/controllers/account.controller.ts b/examples/references-many/src/controllers/account.controller.ts new file mode 100644 index 000000000000..14fc6f5d1c78 --- /dev/null +++ b/examples/references-many/src/controllers/account.controller.ts @@ -0,0 +1,177 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Count, + CountSchema, + Filter, + FilterExcludingWhere, + repository, + Where, +} from '@loopback/repository'; +import { + del, + get, + getModelSchemaRef, + param, + patch, + post, + put, + requestBody, +} from '@loopback/rest'; +import {Account} from '../models'; +import {AccountRepository} from '../repositories'; + +export class AccountController { + constructor( + @repository(AccountRepository) + public accountRepository: AccountRepository, + ) {} + + @post('/accounts', { + responses: { + '200': { + description: 'Account model instance', + content: {'application/json': {schema: getModelSchemaRef(Account)}}, + }, + }, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Account, { + title: 'NewAccount', + exclude: ['id'], + }), + }, + }, + }) + account: Omit<Account, 'id'>, + ): Promise<Account> { + return this.accountRepository.create(account); + } + + @get('/accounts/count', { + responses: { + '200': { + description: 'Account model count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async count(@param.where(Account) where?: Where<Account>): Promise<Count> { + return this.accountRepository.count(where); + } + + @get('/accounts', { + responses: { + '200': { + description: 'Array of Account model instances', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(Account, {includeRelations: true}), + }, + }, + }, + }, + }, + }) + async find( + @param.filter(Account) filter?: Filter<Account>, + ): Promise<Account[]> { + return this.accountRepository.find(filter); + } + + @patch('/accounts', { + responses: { + '200': { + description: 'Account PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async updateAll( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Account, {partial: true}), + }, + }, + }) + account: Account, + @param.where(Account) where?: Where<Account>, + ): Promise<Count> { + return this.accountRepository.updateAll(account, where); + } + + @get('/accounts/{id}', { + responses: { + '200': { + description: 'Account model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(Account, {includeRelations: true}), + }, + }, + }, + }, + }) + async findById( + @param.path.number('id') id: number, + @param.filter(Account, {exclude: 'where'}) + filter?: FilterExcludingWhere<Account>, + ): Promise<Account> { + return this.accountRepository.findById(id, filter); + } + + @patch('/accounts/{id}', { + responses: { + '204': { + description: 'Account PATCH success', + }, + }, + }) + async updateById( + @param.path.number('id') id: number, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Account, {partial: true}), + }, + }, + }) + account: Account, + ): Promise<void> { + await this.accountRepository.updateById(id, account); + } + + @del('/accounts/{id}', { + responses: { + '204': { + description: 'Account DELETE success', + }, + }, + }) + async deleteById(@param.path.number('id') id: number): Promise<void> { + await this.accountRepository.deleteById(id); + } + + @put('/accounts/{id}', { + responses: { + '204': { + description: 'Account PUT success', + }, + }, + }) + async replaceById( + @param.path.number('id') id: number, + @requestBody() account: Account, + ): Promise<void> { + await this.accountRepository.replaceById(id, account); + } +} diff --git a/examples/references-many/src/controllers/customer.controller.ts b/examples/references-many/src/controllers/customer.controller.ts new file mode 100644 index 000000000000..4d6d594a7ec2 --- /dev/null +++ b/examples/references-many/src/controllers/customer.controller.ts @@ -0,0 +1,177 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Count, + CountSchema, + Filter, + FilterExcludingWhere, + repository, + Where, +} from '@loopback/repository'; +import { + del, + get, + getModelSchemaRef, + param, + patch, + post, + put, + requestBody, +} from '@loopback/rest'; +import {Customer} from '../models'; +import {CustomerRepository} from '../repositories'; + +export class CustomerController { + constructor( + @repository(CustomerRepository) + public customerRepository: CustomerRepository, + ) {} + + @post('/customers', { + responses: { + '200': { + description: 'Customer model instance', + content: {'application/json': {schema: getModelSchemaRef(Customer)}}, + }, + }, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Customer, { + title: 'NewCustomer', + exclude: ['id'], + }), + }, + }, + }) + customer: Omit<Customer, 'id'>, + ): Promise<Customer> { + return this.customerRepository.create(customer); + } + + @get('/customers/count', { + responses: { + '200': { + description: 'Customer model count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async count(@param.where(Customer) where?: Where<Customer>): Promise<Count> { + return this.customerRepository.count(where); + } + + @get('/customers', { + responses: { + '200': { + description: 'Array of Customer model instances', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(Customer, {includeRelations: true}), + }, + }, + }, + }, + }, + }) + async find( + @param.filter(Customer) filter?: Filter<Customer>, + ): Promise<Customer[]> { + return this.customerRepository.find(filter); + } + + @patch('/customers', { + responses: { + '200': { + description: 'Customer PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }, + }, + }) + async updateAll( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Customer, {partial: true}), + }, + }, + }) + customer: Customer, + @param.where(Customer) where?: Where<Customer>, + ): Promise<Count> { + return this.customerRepository.updateAll(customer, where); + } + + @get('/customers/{id}', { + responses: { + '200': { + description: 'Customer model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(Customer, {includeRelations: true}), + }, + }, + }, + }, + }) + async findById( + @param.path.number('id') id: number, + @param.filter(Customer, {exclude: 'where'}) + filter?: FilterExcludingWhere<Customer>, + ): Promise<Customer> { + return this.customerRepository.findById(id, filter); + } + + @patch('/customers/{id}', { + responses: { + '204': { + description: 'Customer PATCH success', + }, + }, + }) + async updateById( + @param.path.number('id') id: number, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Customer, {partial: true}), + }, + }, + }) + customer: Customer, + ): Promise<void> { + await this.customerRepository.updateById(id, customer); + } + + @del('/customers/{id}', { + responses: { + '204': { + description: 'Customer DELETE success', + }, + }, + }) + async deleteById(@param.path.number('id') id: number): Promise<void> { + await this.customerRepository.deleteById(id); + } + + @put('/customers/{id}', { + responses: { + '204': { + description: 'Customer PUT success', + }, + }, + }) + async replaceById( + @param.path.number('id') id: number, + @requestBody() customer: Customer, + ): Promise<void> { + await this.customerRepository.replaceById(id, customer); + } +} diff --git a/examples/references-many/src/controllers/index.ts b/examples/references-many/src/controllers/index.ts new file mode 100644 index 000000000000..dbafe00058ae --- /dev/null +++ b/examples/references-many/src/controllers/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './customer.controller'; +export * from './account.controller'; diff --git a/examples/references-many/src/datasources/db.datasource.ts b/examples/references-many/src/datasources/db.datasource.ts new file mode 100644 index 000000000000..3a7e823144b9 --- /dev/null +++ b/examples/references-many/src/datasources/db.datasource.ts @@ -0,0 +1,34 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core'; +import {juggler} from '@loopback/repository'; + +const config = { + name: 'db', + connector: 'memory', + localStorage: '', + file: './data/db.json', +}; + +// Observe application's life cycle to disconnect the datasource when +// application is stopped. This allows the application to be shut down +// gracefully. The `stop()` method is inherited from `juggler.DataSource`. +// Learn more at https://loopback.io/doc/en/lb4/Life-cycle.html +@lifeCycleObserver('datasource') +export class DbDataSource + extends juggler.DataSource + implements LifeCycleObserver +{ + static dataSourceName = 'db'; + static readonly defaultConfig = config; + + constructor( + @inject('datasources.config.db', {optional: true}) + dsConfig: object = config, + ) { + super(dsConfig); + } +} diff --git a/examples/references-many/src/datasources/index.ts b/examples/references-many/src/datasources/index.ts new file mode 100644 index 000000000000..603a98d6fe7d --- /dev/null +++ b/examples/references-many/src/datasources/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018,2020 All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './db.datasource'; diff --git a/examples/references-many/src/index.ts b/examples/references-many/src/index.ts new file mode 100644 index 000000000000..16cf6ad27706 --- /dev/null +++ b/examples/references-many/src/index.ts @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig, ReferencesManyApplication} from './application'; + +export * from './application'; + +export async function main(options: ApplicationConfig = {}) { + const app = new ReferencesManyApplication(options); + await app.boot(); + await app.start(); + + const url = app.restServer.url; + console.log(`Server is running at ${url}`); + return app; +} + +if (require.main === module) { + // Run the application + const config = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST, + // The `gracePeriodForClose` provides a graceful close for http/https + // servers with keep-alive clients. The default value is `Infinity` + // (don't force-close). If you want to immediately destroy all sockets + // upon stop, set its value to `0`. + // See https://www.npmjs.com/package/stoppable + gracePeriodForClose: 5000, // 5 seconds + openApiSpec: { + // useful when used with OpenAPI-to-GraphQL to locate your application + setServersFromRequest: true, + }, + }, + }; + main(config).catch(err => { + console.error('Cannot start the application.', err); + process.exit(1); + }); +} diff --git a/examples/references-many/src/migrate.ts b/examples/references-many/src/migrate.ts new file mode 100644 index 000000000000..0771b1197426 --- /dev/null +++ b/examples/references-many/src/migrate.ts @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ReferencesManyApplication} from './application'; + +export async function migrate(args: string[]) { + const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter'; + console.log('Migrating schemas (%s existing schema)', existingSchema); + + const app = new ReferencesManyApplication(); + await app.boot(); + await app.migrateSchema({existingSchema}); + + // Connectors usually keep a pool of opened connections, + // this keeps the process running even after all work is done. + // We need to exit explicitly. + process.exit(0); +} + +migrate(process.argv).catch(err => { + console.error('Cannot migrate database schema', err); + process.exit(1); +}); diff --git a/examples/references-many/src/models/account.model.ts b/examples/references-many/src/models/account.model.ts new file mode 100644 index 000000000000..ae2f4bdb449d --- /dev/null +++ b/examples/references-many/src/models/account.model.ts @@ -0,0 +1,31 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Account extends Entity { + @property({ + type: 'number', + id: true, + generated: false, + }) + id: number; + + @property({ + type: 'number', + default: 0, + }) + balance: number; + + constructor(data?: Partial<Account>) { + super(data); + } +} + +export interface AccountRelations { + // describe navigational properties here +} + +export type AccountWithRelations = Account & AccountRelations; diff --git a/examples/references-many/src/models/customer.model.ts b/examples/references-many/src/models/customer.model.ts new file mode 100644 index 000000000000..1eb769ed358d --- /dev/null +++ b/examples/references-many/src/models/customer.model.ts @@ -0,0 +1,40 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property, referencesMany} from '@loopback/repository'; +import {Account, AccountWithRelations} from './account.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + generated: true, + }) + id: number; + + @property({ + type: 'string', + }) + firstName: string; + + @property({ + type: 'string', + }) + lastName: string; + + @referencesMany(() => Account) + accountIds?: number[]; + + constructor(data?: Partial<Customer>) { + super(data); + } +} + +export interface CustomerRelations { + accounts?: AccountWithRelations; +} + +export type CustomerWithRelations = Customer & CustomerRelations; diff --git a/examples/references-many/src/models/index.ts b/examples/references-many/src/models/index.ts new file mode 100644 index 000000000000..077c70e60c3b --- /dev/null +++ b/examples/references-many/src/models/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2018,2020 All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './customer.model'; +export * from './account.model'; diff --git a/examples/references-many/src/openapi-spec.ts b/examples/references-many/src/openapi-spec.ts new file mode 100644 index 000000000000..8a8c7ce93330 --- /dev/null +++ b/examples/references-many/src/openapi-spec.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {ReferencesManyApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise<void> { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new ReferencesManyApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/examples/references-many/src/repositories/account.repository.ts b/examples/references-many/src/repositories/account.repository.ts new file mode 100644 index 000000000000..fe89ca4a7ae7 --- /dev/null +++ b/examples/references-many/src/repositories/account.repository.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Account, AccountRelations} from '../models'; + +export class AccountRepository extends DefaultCrudRepository< + Account, + typeof Account.prototype.id, + AccountRelations +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(Account, dataSource); + } +} diff --git a/examples/references-many/src/repositories/customer.repository.ts b/examples/references-many/src/repositories/customer.repository.ts new file mode 100644 index 000000000000..810cea55d5a7 --- /dev/null +++ b/examples/references-many/src/repositories/customer.repository.ts @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter, inject} from '@loopback/core'; +import { + DefaultCrudRepository, + ReferencesManyAccessor, + repository, +} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Account, Customer, CustomerRelations} from '../models'; +import {AccountRepository} from './account.repository'; + +export class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id, + CustomerRelations +> { + public readonly accounts: ReferencesManyAccessor< + Account, + typeof Account.prototype.id + >; + + constructor( + @inject('datasources.db') dataSource: DbDataSource, + @repository.getter('AccountRepository') + protected accountRepositoryGetter: Getter<AccountRepository>, + @repository.getter('CustomerRepository') + protected customerRepositoryGetter: Getter<CustomerRepository>, + ) { + super(Customer, dataSource); + + this.accounts = this.createReferencesManyAccessorFor( + 'accounts', + accountRepositoryGetter, + ); + + this.registerInclusionResolver('accounts', this.accounts.inclusionResolver); + } +} diff --git a/examples/references-many/src/repositories/index.ts b/examples/references-many/src/repositories/index.ts new file mode 100644 index 000000000000..3c42b6f7c08e --- /dev/null +++ b/examples/references-many/src/repositories/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './customer.repository'; +export * from './account.repository'; diff --git a/examples/references-many/src/sequence.ts b/examples/references-many/src/sequence.ts new file mode 100644 index 000000000000..3860259150e5 --- /dev/null +++ b/examples/references-many/src/sequence.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2022. All Rights Reserved. +// Node module: @loopback/example-references-many +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {MiddlewareSequence} from '@loopback/rest'; + +export class MySequence extends MiddlewareSequence {} diff --git a/examples/references-many/tsconfig.json b/examples/references-many/tsconfig.json new file mode 100644 index 000000000000..d92a21a77f27 --- /dev/null +++ b/examples/references-many/tsconfig.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": true + }, + "include": [ + "src/**/*", + "src/**/*.json" + ], + "references": [ + { + "path": "../../packages/boot/tsconfig.json" + }, + { + "path": "../../packages/core/tsconfig.json" + }, + { + "path": "../../packages/http-caching-proxy/tsconfig.json" + }, + { + "path": "../../packages/repository/tsconfig.json" + }, + { + "path": "../../packages/rest-explorer/tsconfig.json" + }, + { + "path": "../../packages/rest/tsconfig.json" + }, + { + "path": "../../packages/service-proxy/tsconfig.json" + }, + { + "path": "../../packages/testlab/tsconfig.json" + } + ] +} diff --git a/packages/cli/generators/relation/index.js b/packages/cli/generators/relation/index.js index cefd14c2a97d..7b9d1a769c36 100644 --- a/packages/cli/generators/relation/index.js +++ b/packages/cli/generators/relation/index.js @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -// no translation: HasMany, BelongsTo, HasOne +// no translation: HasMany, BelongsTo, HasOne, ReferencesMany 'use strict'; const ArtifactGenerator = require('../../lib/artifact-generator'); @@ -18,6 +18,7 @@ const BelongsToRelationGenerator = require('./belongs-to-relation.generator'); const HasManyRelationGenerator = require('./has-many-relation.generator'); const HasManyThroughRelationGenerator = require('./has-many-through-relation.generator'); const HasOneRelationGenerator = require('./has-one-relation.generator'); +const ReferencesManyRelationGenerator = require('./references-many-relation.generator'); const g = require('../../lib/globalize'); @@ -39,7 +40,10 @@ const PROMPT_MESSAGE_RELATION_NAME = g.f('Relation name'); const PROMPT_MESSAGE_FOREIGN_KEY_NAME = g.f( 'Foreign key name to define on the target model', ); -const PROMPT_MESSAGE_FOREIGN_KEY_NAME_BELONGSTO = g.f( +const PROMPT_MESSAGE_FOREIGN_KEY_NAME_BELONGS_TO = g.f( + 'Foreign key name to define on the source model', +); +const PROMPT_MESSAGE_FOREIGN_KEY_NAME_REFERENCES_MANY = g.f( 'Foreign key name to define on the source model', ); @@ -200,6 +204,13 @@ module.exports = class RelationGenerator extends ArtifactGenerator { this.artifactInfo.destinationModel, ); break; + case relationUtils.relationType.referencesMany: + // this is how the referencesManyAccessor generates the default relation name + defaultRelationName = this.artifactInfo.foreignKeyName.replace( + /Ids$/, + 's', + ); + break; } return defaultRelationName; @@ -380,7 +391,7 @@ module.exports = class RelationGenerator extends ArtifactGenerator { * 4. Check is foreign key exist in destination model. If not - prompt. * Error - if type is not the same. * - * For belongsTo this is getting source key not fk. + * For belongsTo and referencesMany this is getting source key not fk. */ async promptForeignKey() { if (this.shouldExit()) return false; @@ -525,17 +536,37 @@ module.exports = class RelationGenerator extends ArtifactGenerator { this.artifactInfo.foreignKeyName = this.options.foreignKeyName; } - this.artifactInfo.defaultForeignKeyName = - this.artifactInfo.relationType === 'belongsTo' - ? utils.camelCase(this.artifactInfo.destinationModel) + 'Id' - : utils.camelCase(this.artifactInfo.sourceModel) + 'Id'; + switch (this.artifactInfo.relationType) { + case 'belongsTo': + this.artifactInfo.defaultForeignKeyName = + utils.camelCase(this.artifactInfo.destinationModel) + 'Id'; + break; + case 'referencesMany': + this.artifactInfo.defaultForeignKeyName = + utils.camelCase(this.artifactInfo.destinationModel) + 'Ids'; + break; + default: + this.artifactInfo.defaultForeignKeyName = + utils.camelCase(this.artifactInfo.sourceModel) + 'Id'; + break; + } + + let msg; + switch (this.artifactInfo.relationType) { + case 'belongsTo': + msg = PROMPT_MESSAGE_FOREIGN_KEY_NAME_BELONGS_TO; + break; + case 'referencesMany': + msg = PROMPT_MESSAGE_FOREIGN_KEY_NAME_REFERENCES_MANY; + break; + default: + msg = PROMPT_MESSAGE_FOREIGN_KEY_NAME; + break; + } - const msg = - this.artifactInfo.relationType === 'belongsTo' - ? PROMPT_MESSAGE_FOREIGN_KEY_NAME_BELONGSTO - : PROMPT_MESSAGE_FOREIGN_KEY_NAME; const foreignKeyModel = - this.artifactInfo.relationType === 'belongsTo' + this.artifactInfo.relationType === 'belongsTo' || + this.artifactInfo.relationType === 'referencesMany' ? this.artifactInfo.sourceModel : this.artifactInfo.destinationModel; @@ -563,10 +594,11 @@ module.exports = class RelationGenerator extends ArtifactGenerator { cl, this.artifactInfo.foreignKeyName, ); - // checks if its the case that the fk already exists in source model and decorated by @belongsTo, which should be aborted + // checks if it's the case that the fk already exists in source model and decorated by @belongsTo or @referencesMany, which should be aborted if ( this.artifactInfo.doesForeignKeyExist && - this.artifactInfo.relationType === 'belongsTo' + (this.artifactInfo.relationType === 'belongsTo' || + this.artifactInfo.relationType === 'referencesMany') ) { try { relationUtils.doesRelationExist(cl, this.artifactInfo.foreignKeyName); @@ -637,7 +669,8 @@ module.exports = class RelationGenerator extends ArtifactGenerator { this.artifactInfo.defaultRelationName = this._getDefaultRelationName(); // for hasMany && hasOne, the source key is the same as the relation name const msg = - this.artifactInfo.relationType === 'belongsTo' + this.artifactInfo.relationType === 'belongsTo' || + this.artifactInfo.relationType === 'referencesMany' ? PROMPT_MESSAGE_RELATION_NAME : PROMPT_MESSAGE_PROPERTY_NAME; @@ -740,6 +773,12 @@ module.exports = class RelationGenerator extends ArtifactGenerator { case relationUtils.relationType.hasOne: relationGenerator = new HasOneRelationGenerator(this.args, this.opts); break; + case relationUtils.relationType.referencesMany: + relationGenerator = new ReferencesManyRelationGenerator( + this.args, + this.opts, + ); + break; } try { diff --git a/packages/cli/generators/relation/references-many-relation.generator.js b/packages/cli/generators/relation/references-many-relation.generator.js new file mode 100644 index 000000000000..bbeffae0adbe --- /dev/null +++ b/packages/cli/generators/relation/references-many-relation.generator.js @@ -0,0 +1,142 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/cli +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const BaseRelationGenerator = require('./base-relation.generator'); +const utils = require('../../lib/utils'); +const relationUtils = require('./utils.generator'); + +module.exports = class ReferencesManyRelationGenerator extends ( + BaseRelationGenerator +) { + constructor(args, opts) { + super(args, opts); + } + + async generateControllers(options) { + // no controllers + } + + async generateModels(options) { + // for repo to generate relation name + this.artifactInfo.relationName = options.relationName; + const modelDir = this.artifactInfo.modelDir; + const sourceModel = options.sourceModel; + + const targetModel = options.destinationModel; + const relationType = options.relationType; + const relationName = options.relationName; + const defaultRelationName = options.defaultRelationName; + const foreignKeyName = options.foreignKeyName; + const fkType = options.destinationModelPrimaryKeyType; + + const project = new relationUtils.AstLoopBackProject(); + const sourceFile = relationUtils.addFileToProject( + project, + modelDir, + sourceModel, + ); + const sourceClass = relationUtils.getClassObj(sourceFile, sourceModel); + // this checks if the foreign key already exists, so the 2nd param should be foreignKeyName + relationUtils.doesRelationExist(sourceClass, foreignKeyName); + + const modelProperty = this.getReferencesMany( + targetModel, + relationName, + defaultRelationName, + foreignKeyName, + fkType, + ); + + relationUtils.addProperty(sourceClass, modelProperty); + const imports = relationUtils.getRequiredImports(targetModel, relationType); + relationUtils.addRequiredImports(sourceFile, imports); + + sourceClass.formatText(); + await sourceFile.save(); + } + + getReferencesMany( + className, + relationName, + defaultRelationName, + foreignKeyName, + fkType, + ) { + // checks if relation name is customized + let relationDecorator = [ + { + name: 'referencesMany', + arguments: [`() => ${className}`], + }, + ]; + // already checked if the relation name is the same as the source key before + if (defaultRelationName !== relationName) { + relationDecorator = [ + { + name: 'referencesMany', + arguments: [`() => ${className}, {name: '${relationName}'}`], + }, + ]; + } + return { + decorators: relationDecorator, + name: foreignKeyName, + type: fkType + '[]', + }; + } + + _getRepositoryRequiredImports(dstModelClassName, dstRepositoryClassName) { + const importsArray = super._getRepositoryRequiredImports( + dstModelClassName, + dstRepositoryClassName, + ); + importsArray.push({ + name: 'ReferencesManyAccessor', + module: '@loopback/repository', + }); + return importsArray; + } + + _getRepositoryRelationPropertyName() { + return this.artifactInfo.relationName; + } + + _initializeProperties(options) { + super._initializeProperties(options); + this.artifactInfo.dstModelPrimaryKey = options.destinationModelPrimaryKey; + } + + _getRepositoryRelationPropertyType() { + return ( + `ReferencesManyAccessor<` + + `${utils.toClassName(this.artifactInfo.dstModelClass)}` + + `, typeof ${utils.toClassName(this.artifactInfo.srcModelClass)}` + + `.prototype.${this.artifactInfo.srcModelPrimaryKey}>` + ); + } + + _addCreatorToRepositoryConstructor(classConstructor) { + const relationName = this.artifactInfo.relationName; + const statement = + `this.${relationName} = ` + + `this.createReferencesManyAccessorFor('` + + `${relationName}',` + + ` ${utils.camelCase(this.artifactInfo.dstRepositoryClassName)}` + + `Getter,);`; + classConstructor.insertStatements(1, statement); + } + + _registerInclusionResolverForRelation(classConstructor, options) { + const relationName = this.artifactInfo.relationName; + if (options.registerInclusionResolver) { + const statement = + `this.registerInclusionResolver(` + + `'${relationName}', this.${relationName}.inclusionResolver);`; + classConstructor.insertStatements(2, statement); + } + } +}; diff --git a/packages/cli/generators/relation/utils.generator.js b/packages/cli/generators/relation/utils.generator.js index 8152bf38a823..91a5ec1bdeb6 100644 --- a/packages/cli/generators/relation/utils.generator.js +++ b/packages/cli/generators/relation/utils.generator.js @@ -15,6 +15,7 @@ exports.relationType = { hasMany: 'hasMany', hasManyThrough: 'hasManyThrough', hasOne: 'hasOne', + referencesMany: 'referencesMany', }; class AstLoopBackProject extends ast.Project { diff --git a/packages/cli/snapshots/integration/generators/relation.references-many.integration.snapshots.js b/packages/cli/snapshots/integration/generators/relation.references-many.integration.snapshots.js new file mode 100644 index 000000000000..3e7f987dc1ad --- /dev/null +++ b/packages/cli/snapshots/integration/generators/relation.references-many.integration.snapshots.js @@ -0,0 +1,141 @@ +// IMPORTANT +// This snapshot file is auto-generated, but designed for humans. +// It should be checked into source control and tracked carefully. +// Re-generate by setting UPDATE_SNAPSHOTS=1 and running tests. +// Make sure to inspect the changes in the snapshots below. +// Do not ignore changes! + +'use strict'; + +exports[`lb4 relation checks generated source class repository answers {"relationType":"referencesMany","sourceModel":"Customer","destinationModel":"Account","relationName":"custom_name","registerInclusionResolver":false} generates Customer repository file with different inputs 1`] = ` +import {inject, Getter} from '@loopback/core'; +import {DefaultCrudRepository, repository, ReferencesManyAccessor} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Customer, Account} from '../models'; +import {AccountRepository} from './account.repository'; + +export class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id +> { + + public readonly custom_name: ReferencesManyAccessor<Account, typeof Customer.prototype.id>; + + constructor(@inject('datasources.db') dataSource: DbDataSource, @repository.getter('AccountRepository') protected accountRepositoryGetter: Getter<AccountRepository>,) { + super(Customer, dataSource); + this.custom_name = this.createReferencesManyAccessorFor('custom_name', accountRepositoryGetter,); + } +} + +`; + + +exports[`lb4 relation checks generated source class repository answers {"relationType":"referencesMany","sourceModel":"Customer","destinationModel":"Account"} generates Customer repository file with different inputs 1`] = ` +import {inject, Getter} from '@loopback/core'; +import {DefaultCrudRepository, repository, ReferencesManyAccessor} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Customer, Account} from '../models'; +import {AccountRepository} from './account.repository'; + +export class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id +> { + + public readonly accounts: ReferencesManyAccessor<Account, typeof Customer.prototype.id>; + + constructor(@inject('datasources.db') dataSource: DbDataSource, @repository.getter('AccountRepository') protected accountRepositoryGetter: Getter<AccountRepository>,) { + super(Customer, dataSource); + this.accounts = this.createReferencesManyAccessorFor('accounts', accountRepositoryGetter,); + this.registerInclusionResolver('accounts', this.accounts.inclusionResolver); + } +} + +`; + + +exports[`lb4 relation generates model relation for existing property name verifies that a preexisting property will be overwritten 1`] = ` +import {Entity, model, property, referencesMany} from '@loopback/repository'; +import {Account} from './account.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @referencesMany(() => Account) + accountIds: number[]; + + constructor(data?: Partial<Customer>) { + super(data); + } +} + +`; + + +exports[`lb4 relation generates model relation with custom relation name answers {"relationType":"referencesMany","sourceModel":"Customer","destinationModel":"Account","foreignKeyName":"accountIds","relationName":"my_accounts"} relation name should be my_accounts 1`] = ` +import {Entity, model, property, referencesMany} from '@loopback/repository'; +import {Account} from './account.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @referencesMany(() => Account, {name: 'my_accounts'}) + accountIds: number[]; + + constructor(data?: Partial<Customer>) { + super(data); + } +} + +`; + + +exports[`lb4 relation generates model relation with default values answers {"relationType":"referencesMany","sourceModel":"Customer","destinationModel":"Account"} has correct default imports 1`] = ` +import {Entity, model, property, referencesMany} from '@loopback/repository'; +import {Account} from './account.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @referencesMany(() => Account) + accountIds: number[]; + + constructor(data?: Partial<Customer>) { + super(data); + } +} + +`; diff --git a/packages/cli/test/fixtures/relation/controllers/account.controller.ts b/packages/cli/test/fixtures/relation/controllers/account.controller.ts new file mode 100644 index 000000000000..0f7bfb2b577a --- /dev/null +++ b/packages/cli/test/fixtures/relation/controllers/account.controller.ts @@ -0,0 +1 @@ +export class AccountController {} diff --git a/packages/cli/test/fixtures/relation/index.js b/packages/cli/test/fixtures/relation/index.js index df0ce6c85da2..c54b96b9eb0d 100644 --- a/packages/cli/test/fixtures/relation/index.js +++ b/packages/cli/test/fixtures/relation/index.js @@ -12,6 +12,17 @@ const fs = require('fs'); const {getSourceForDataSourceClassWithConfig} = require('../../test-utils'); const SourceEntries = { + AccountModel: { + path: MODEL_APP_PATH, + file: 'account.model.ts', + content: readSourceFile('./models/account.model.ts'), + }, + AccountRepository: { + path: REPOSITORY_APP_PATH, + file: 'account.repository.ts', + content: readSourceFile('./repositories/account.repository.ts'), + }, + CustomerModel: { path: MODEL_APP_PATH, file: 'customer.model.ts', @@ -27,6 +38,11 @@ const SourceEntries = { file: 'customer.model.ts', content: readSourceFile('./models/customer6.model.ts'), }, + CustomerModelWithAccountIdsProperty: { + path: MODEL_APP_PATH, + file: 'customer.model.ts', + content: readSourceFile('./models/customer7.model.ts'), + }, CustomerRepository: { path: REPOSITORY_APP_PATH, file: 'customer.repository.ts', @@ -175,6 +191,7 @@ exports.SANDBOX_FILES = [ connector: 'loopback-connector-sqlite3', }), }, + SourceEntries.AccountRepository, SourceEntries.CustomerRepository, SourceEntries.OrderRepository, SourceEntries.AddressRepository, @@ -182,6 +199,7 @@ exports.SANDBOX_FILES = [ SourceEntries.PatientRepository, SourceEntries.AppointmentRepository, + SourceEntries.AccountModel, SourceEntries.CustomerModel, SourceEntries.OrderModel, SourceEntries.AddressModel, diff --git a/packages/cli/test/fixtures/relation/models/account.model.ts b/packages/cli/test/fixtures/relation/models/account.model.ts new file mode 100644 index 000000000000..02bc5df5edb2 --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/account.model.ts @@ -0,0 +1,21 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Account extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'number', + default: 0, + }) + balance?: string; + + constructor(data?: Partial<Account>) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/models/customer7.model.ts b/packages/cli/test/fixtures/relation/models/customer7.model.ts new file mode 100644 index 000000000000..fe424b40c73c --- /dev/null +++ b/packages/cli/test/fixtures/relation/models/customer7.model.ts @@ -0,0 +1,25 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + name?: string; + + @property({ + type: 'array', + }) + accountIds: number[]; + + constructor(data?: Partial<Customer>) { + super(data); + } +} diff --git a/packages/cli/test/fixtures/relation/repositories/account.repository.ts b/packages/cli/test/fixtures/relation/repositories/account.repository.ts new file mode 100644 index 000000000000..db7f51605270 --- /dev/null +++ b/packages/cli/test/fixtures/relation/repositories/account.repository.ts @@ -0,0 +1,13 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Account} from '../models'; + +export class AccountRepository extends DefaultCrudRepository< + Account, + typeof Account.prototype.id +> { + constructor(@inject('datasources.db') dataSource: DbDataSource) { + super(Account, dataSource); + } +} diff --git a/packages/cli/test/integration/generators/relation.references-many.integration.js b/packages/cli/test/integration/generators/relation.references-many.integration.js new file mode 100644 index 000000000000..4adb702d0639 --- /dev/null +++ b/packages/cli/test/integration/generators/relation.references-many.integration.js @@ -0,0 +1,236 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/cli +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const path = require('path'); +const assert = require('yeoman-assert'); +const {expect, TestSandbox} = require('@loopback/testlab'); +const {expectFileToMatchSnapshot} = require('../../snapshots'); + +const generator = path.join(__dirname, '../../../generators/relation'); +const {SANDBOX_FILES, SourceEntries} = require('../../fixtures/relation'); +const testUtils = require('../../test-utils'); + +// Test Sandbox +const MODEL_APP_PATH = 'src/models'; +const REPOSITORY_APP_PATH = 'src/repositories'; +const sandbox = new TestSandbox(path.resolve(__dirname, '../.sandbox')); + +const sourceFileName = 'customer.model.ts'; +const repositoryFileName = 'customer.repository.ts'; +// speed up tests by avoiding reading docs +const options = { + sourceModelPrimaryKey: 'id', + sourceModelPrimaryKeyType: 'number', + destinationModelPrimaryKey: 'id', + destinationModelPrimaryKeyType: 'number', +}; + +describe('lb4 relation', /** @this {Mocha.Suite} */ function () { + this.timeout(30000); + + it('rejects relation when models does not exist', async () => { + await sandbox.reset(); + const prompt = { + relationType: 'referencesMany', + sourceModel: 'Customer', + destinationModel: 'NotExistModel', + }; + + return expect( + testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: [ + // no model/repository files in this project + ], + }), + ) + .withPrompts(prompt), + ).to.be.rejectedWith(/No models found/); + }); + + context('generates model relation with default values', () => { + const promptArray = [ + { + relationType: 'referencesMany', + sourceModel: 'Customer', + destinationModel: 'Account', + }, + ]; + + promptArray.forEach(function (multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withPrompts(multiItemPrompt); + }); + + it('has correct default imports', async () => { + const sourceFilePath = path.join( + sandbox.path, + MODEL_APP_PATH, + sourceFileName, + ); + + assert.file(sourceFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }); + } + }); + + context('generates model relation for existing property name', () => { + const promptList = [ + { + relationType: 'referencesMany', + sourceModel: 'Customer', + destinationModel: 'Account', + foreignKeyName: 'accountIds', + relationName: 'accounts', + }, + ]; + + it('verifies that a preexisting property will be overwritten', async () => { + await sandbox.reset(); + + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: [ + SourceEntries.AccountModel, + SourceEntries.CustomerModelWithAccountIdsProperty, + SourceEntries.CustomerRepository, + SourceEntries.AccountRepository, + ], + }), + ) + .withOptions(options) + .withPrompts(promptList[0]); + + const sourceFilePath = path.join( + sandbox.path, + MODEL_APP_PATH, + 'customer.model.ts', + ); + + assert.file(sourceFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }); + }); + + context('generates model relation with custom relation name', () => { + const promptArray = [ + { + relationType: 'referencesMany', + sourceModel: 'Customer', + destinationModel: 'Account', + foreignKeyName: 'accountIds', + relationName: 'my_accounts', + }, + ]; + promptArray.forEach(function (multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(options) + .withPrompts(multiItemPrompt); + }); + + it('relation name should be my_accounts', async () => { + const sourceFilePath = path.join( + sandbox.path, + MODEL_APP_PATH, + sourceFileName, + ); + + assert.file(sourceFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }); + } + }); + + context('checks generated source class repository', () => { + const promptArray = [ + { + relationType: 'referencesMany', + sourceModel: 'Customer', + destinationModel: 'Account', + }, + { + relationType: 'referencesMany', + sourceModel: 'Customer', + destinationModel: 'Account', + relationName: 'custom_name', + registerInclusionResolver: false, + }, + ]; + + const sourceClassnames = ['Customer', 'Customer']; + + promptArray.forEach(function (multiItemPrompt, i) { + describe('answers ' + JSON.stringify(multiItemPrompt), () => { + suite(multiItemPrompt, i); + }); + }); + + function suite(multiItemPrompt, i) { + before(async function runGeneratorWithAnswers() { + await sandbox.reset(); + await testUtils + .executeGenerator(generator) + .inDir(sandbox.path, () => + testUtils.givenLBProject(sandbox.path, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(options) + .withPrompts(multiItemPrompt); + }); + + it( + 'generates ' + + sourceClassnames[i] + + ' repository file with different inputs', + async () => { + const sourceFilePath = path.join( + sandbox.path, + REPOSITORY_APP_PATH, + repositoryFileName, + ); + + assert.file(sourceFilePath); + expectFileToMatchSnapshot(sourceFilePath); + }, + ); + } + }); +}); diff --git a/packages/repository/src/__tests__/unit/decorator/model-and-relation.decorator.unit.ts b/packages/repository/src/__tests__/unit/decorator/model-and-relation.decorator.unit.ts index ae00663fe862..efede998a811 100644 --- a/packages/repository/src/__tests__/unit/decorator/model-and-relation.decorator.unit.ts +++ b/packages/repository/src/__tests__/unit/decorator/model-and-relation.decorator.unit.ts @@ -58,8 +58,13 @@ describe('model decorator', () => { @model() class Account extends Entity { + @property() id: string; + + @property() type: string; + + @property() balance: number; } @@ -116,8 +121,8 @@ describe('model decorator', () => { @embedsMany() phones: Phone[]; - @referencesMany() - accounts: Account[]; + @referencesMany(() => Account) + accountIds: string[]; @referencesOne() profile: Profile; @@ -277,13 +282,19 @@ describe('model decorator', () => { it('adds referencesMany metadata', () => { const meta = - MetadataInspector.getAllPropertyMetadata( + MetadataInspector.getAllPropertyMetadata<RelationMetadata>( RELATIONS_KEY, Customer.prototype, ) ?? /* istanbul ignore next */ {}; - expect(meta.accounts).to.eql({ + const relationDef = meta.accountIds; + expect(relationDef).to.containEql({ type: RelationType.referencesMany, + name: 'accounts', + target: () => Account, + keyFrom: 'accountIds', }); + expect(relationDef.source).to.be.exactly(Customer); + expect(relationDef.target()).to.be.exactly(Account); }); it('adds referencesOne metadata', () => { diff --git a/packages/repository/src/model.ts b/packages/repository/src/model.ts index e5b85b1e28dd..b65e8aad3530 100644 --- a/packages/repository/src/model.ts +++ b/packages/repository/src/model.ts @@ -8,6 +8,7 @@ import { BelongsToDefinition, HasManyDefinition, HasOneDefinition, + ReferencesManyDefinition, JsonSchema, RelationMetadata, RelationType, @@ -240,6 +241,24 @@ export class ModelDefinition { return this.addRelation(meta); } + /** + * Define a new referencesMany relation. + * @param name - The name of the referencesMany relation. + * @param definition - The definition of the referencesMany relation. + */ + referencesMany( + name: string, + definition: Omit<ReferencesManyDefinition, 'name' | 'type' | 'targetsMany'>, + ): this { + const meta: ReferencesManyDefinition = { + ...definition, + name, + type: RelationType.referencesMany, + targetsMany: true, + }; + return this.addRelation(meta); + } + /** * Get an array of names of ID properties, which are specified in * the model settings or properties with `id` attribute. diff --git a/packages/repository/src/relations/index.ts b/packages/repository/src/relations/index.ts index 401af94f0eb9..5cd590b1131c 100644 --- a/packages/repository/src/relations/index.ts +++ b/packages/repository/src/relations/index.ts @@ -6,6 +6,7 @@ export * from './belongs-to'; export * from './has-many'; export * from './has-one'; +export * from './references-many'; export * from './relation.decorator'; export * from './relation.helpers'; export * from './relation.types'; diff --git a/packages/repository/src/relations/references-many/index.ts b/packages/repository/src/relations/references-many/index.ts new file mode 100644 index 000000000000..51ee5fc0e12d --- /dev/null +++ b/packages/repository/src/relations/references-many/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2018,2020. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './references-many.decorator'; +export * from './references-many.repository'; +export * from './references-many.accessor'; +export * from './references-many.inclusion-resolver'; diff --git a/packages/repository/src/relations/references-many/references-many.accessor.ts b/packages/repository/src/relations/references-many/references-many.accessor.ts new file mode 100644 index 000000000000..2f377ba535ba --- /dev/null +++ b/packages/repository/src/relations/references-many/references-many.accessor.ts @@ -0,0 +1,76 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import debugFactory from 'debug'; +import {DataObject} from '../../common-types'; +import {Entity} from '../../model'; +import {EntityCrudRepository} from '../../repositories/repository'; +import { + ReferencesManyDefinition, + Getter, + InclusionResolver, +} from '../relation.types'; +import {resolveReferencesManyMetadata} from './references-many.helpers'; +import {createReferencesManyInclusionResolver} from './references-many.inclusion-resolver'; +import {DefaultReferencesManyRepository} from './references-many.repository'; + +const debug = debugFactory( + 'loopback:repository:relations:references-many:accessor', +); + +export interface ReferencesManyAccessor<Target extends Entity, SourceId> { + /** + * Invoke the function to obtain HasManyRepository. + */ + (sourceId: SourceId): Promise<Target>; + + /** + * Use `resolver` property to obtain an InclusionResolver for this relation. + */ + inclusionResolver: InclusionResolver<Entity, Target>; +} + +/** + * Enforces a ReferencesMany constraint on a repository + */ +export function createReferencesManyAccessor< + Target extends Entity, + TargetIds, + Source extends Entity, + SourceId, +>( + referencesManyMetadata: ReferencesManyDefinition, + targetRepoGetter: Getter<EntityCrudRepository<Target, TargetIds>>, + sourceRepository: EntityCrudRepository<Source, SourceId>, +): ReferencesManyAccessor<Target, SourceId> { + const meta = resolveReferencesManyMetadata(referencesManyMetadata); + debug('Resolved ReferencesMany relation metadata: %o', meta); + const result: ReferencesManyAccessor<Target, SourceId> = + async function getTargetInstancesOfReferencesMany(sourceId: SourceId) { + const foreignKey = meta.keyFrom; + const primaryKey = meta.keyTo; + const sourceModel = await sourceRepository.findById(sourceId); + const foreignKeyValue = sourceModel[foreignKey as keyof Source]; + // workaround to check referential integrity. + // should be removed once the memory connector ref integrity is done + // GH issue: https://github.com/loopbackio/loopback-next/issues/2333 + if (!foreignKeyValue) { + return undefined as unknown as Target; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const constraint: any = {[primaryKey]: foreignKeyValue}; + const constrainedRepo = new DefaultReferencesManyRepository( + targetRepoGetter, + constraint as DataObject<Target>, + ); + return constrainedRepo.get(); + }; + + result.inclusionResolver = createReferencesManyInclusionResolver( + meta, + targetRepoGetter, + ); + return result; +} diff --git a/packages/repository/src/relations/references-many/references-many.decorator.ts b/packages/repository/src/relations/references-many/references-many.decorator.ts new file mode 100644 index 000000000000..ecb066cbf023 --- /dev/null +++ b/packages/repository/src/relations/references-many/references-many.decorator.ts @@ -0,0 +1,100 @@ +// Copyright IBM Corp. 2018,2020. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {DecoratorFactory, MetadataInspector} from '@loopback/core'; +import {property} from '../../decorators'; +import {relation} from '../relation.decorator'; +import {Entity, EntityResolver, PropertyDefinition} from '../../model'; +import {ReferencesManyDefinition, RelationType} from '../relation.types'; + +/** + * Decorator for referencesMany + * @param targetResolver - A resolver function that returns the target model for + * a referencesMany relation + * @param definition - Optional metadata for setting up a referencesMany relation + * @param propertyDefinition - Optional metadata for setting up the property + * @returns A property decorator + */ +export function referencesMany<T extends Entity>( + targetResolver: EntityResolver<T>, + definition?: Partial<ReferencesManyDefinition>, + propertyDefinition?: Partial<PropertyDefinition>, +) { + return function (decoratedTarget: Entity, decoratedKey: string) { + const propType = + MetadataInspector.getDesignTypeForProperty( + decoratedTarget, + decoratedKey, + ) ?? propertyDefinition?.type; + + if (!propType) { + const fullPropName = DecoratorFactory.getTargetName( + decoratedTarget, + decoratedKey, + ); + throw new Error( + `Cannot infer type of model property ${fullPropName} because ` + + 'TypeScript compiler option `emitDecoratorMetadata` is not set. ' + + 'Please enable `emitDecoratorMetadata` or use the third argument of ' + + '`@referencesMany` decorator to specify the property type explicitly.', + ); + } + + const sourceKeyType = MetadataInspector.getDesignTypeForProperty( + targetResolver().prototype, + definition?.keyTo ?? 'id', + ); + + if (!sourceKeyType) { + const fullPropName = DecoratorFactory.getTargetName( + targetResolver().prototype, + definition?.keyTo ?? 'id', + ); + throw new Error( + `Cannot infer type of model property ${fullPropName} because ` + + 'TypeScript compiler option `emitDecoratorMetadata` is not set. ' + + 'Please enable `emitDecoratorMetadata` or use the second argument of ' + + '`@referencesMany` decorator to specify the property type explicitly.', + ); + } + + const propMeta: PropertyDefinition = Object.assign( + {}, + // properties provided by the caller + propertyDefinition, + // properties enforced by the decorator + { + type: propType, + itemType: sourceKeyType, + // TODO(bajtos) Make the foreign key required once our REST API layer + // allows controller methods to exclude required properties + // required: true, + }, + ); + property(propMeta)(decoratedTarget, decoratedKey); + + // @referencesMany() is typically decorating the foreign key property, + // e.g. customerIds. We need to strip the trailing "Ids" suffix from the name. + const relationName = decoratedKey.replace(/Ids$/, 's'); + + const meta: ReferencesManyDefinition = Object.assign( + // default values, can be customized by the caller + { + keyFrom: decoratedKey, + name: relationName, + }, + // properties provided by the caller + definition, + // properties enforced by the decorator + { + type: RelationType.referencesMany, + targetsMany: true, + source: decoratedTarget.constructor, + target: targetResolver, + }, + ); + relation(meta)(decoratedTarget, decoratedKey); + }; +} diff --git a/packages/repository/src/relations/references-many/references-many.helpers.ts b/packages/repository/src/relations/references-many/references-many.helpers.ts new file mode 100644 index 000000000000..2e9257c9f853 --- /dev/null +++ b/packages/repository/src/relations/references-many/references-many.helpers.ts @@ -0,0 +1,82 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import debugFactory from 'debug'; +import {camelCase} from 'lodash'; +import {InvalidRelationError} from '../../errors'; +import {isTypeResolver} from '../../type-resolver'; +import {ReferencesManyDefinition, RelationType} from '../relation.types'; + +const debug = debugFactory( + 'loopback:repository:relations:references-many:helpers', +); + +/** + * Relation definition with optional metadata (e.g. `keyTo`) filled in. + * @internal + */ +export type ReferencesManyResolvedDefinition = ReferencesManyDefinition & { + keyFrom: string; + keyTo: string; +}; + +/** + * Resolves given referencesMany metadata if target is specified to be a resolver. + * Mainly used to infer what the `keyTo` property should be from the target's + * property id metadata + * @param relationMeta - referencesMany metadata to resolve + * @internal + */ +export function resolveReferencesManyMetadata( + relationMeta: ReferencesManyDefinition, +) { + if ((relationMeta.type as RelationType) !== RelationType.referencesMany) { + const reason = 'relation type must be ReferencesMany'; + throw new InvalidRelationError(reason, relationMeta); + } + + if (!isTypeResolver(relationMeta.target)) { + const reason = 'target must be a type resolver'; + throw new InvalidRelationError(reason, relationMeta); + } + + const sourceModel = relationMeta.source; + if (!sourceModel || !sourceModel.modelName) { + const reason = 'source model must be defined'; + throw new InvalidRelationError(reason, relationMeta); + } + + const targetModel = relationMeta.target(); + const targetName = targetModel.modelName; + debug('Resolved model %s from given metadata: %o', targetName, targetModel); + + let keyFrom; + if ( + relationMeta.keyFrom && + relationMeta.source.definition.properties[relationMeta.keyFrom] + ) { + keyFrom = relationMeta.keyFrom; + } else { + keyFrom = camelCase(targetName + '_ids'); + } + + const targetProperties = targetModel.definition.properties; + debug('relation metadata from %o: %o', targetName, targetProperties); + + if (relationMeta.keyTo && targetProperties[relationMeta.keyTo]) { + // The explicit cast is needed because of a limitation of type inference + return Object.assign(relationMeta, { + keyFrom, + }) as ReferencesManyResolvedDefinition; + } + + const targetPrimaryKey = targetModel.definition.idProperties()[0]; + if (!targetPrimaryKey) { + const reason = `${targetName} does not have any primary key (id property)`; + throw new InvalidRelationError(reason, relationMeta); + } + + return Object.assign(relationMeta, {keyFrom, keyTo: targetPrimaryKey}); +} diff --git a/packages/repository/src/relations/references-many/references-many.inclusion-resolver.ts b/packages/repository/src/relations/references-many/references-many.inclusion-resolver.ts new file mode 100644 index 000000000000..55ed090cb36b --- /dev/null +++ b/packages/repository/src/relations/references-many/references-many.inclusion-resolver.ts @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Filter, InclusionFilter} from '@loopback/filter'; +import {AnyObject, Options} from '../../common-types'; +import {Entity} from '../../model'; +import {EntityCrudRepository} from '../../repositories'; +import { + deduplicate, + findByForeignKeys, + flattenTargetsOfOneToOneRelation, + StringKeyOf, +} from '../relation.helpers'; +import { + ReferencesManyDefinition, + Getter, + InclusionResolver, +} from '../relation.types'; +import {resolveReferencesManyMetadata} from './references-many.helpers'; + +/** + * Creates InclusionResolver for ReferencesMany relation. + * Notice that this function only generates the inclusionResolver. + * It doesn't register it for the source repository. + * + * Notice: scope field for inclusion is not supported yet + * + * @param meta - resolved ReferencesManyMetadata + * @param getTargetRepo - target repository i.e where related instances are + */ +export function createReferencesManyInclusionResolver< + Target extends Entity, + TargetIds, + TargetRelations extends object, +>( + meta: ReferencesManyDefinition, + getTargetRepo: Getter< + EntityCrudRepository<Target, TargetIds, TargetRelations> + >, +): InclusionResolver<Entity, Target> { + const relationMeta = resolveReferencesManyMetadata(meta); + + return async function fetchIncludedModels( + entities: Entity[], + inclusion: InclusionFilter, + options?: Options, + ): Promise<(Target & TargetRelations)[][]> { + if (!entities.length) return []; + + const sourceKey = relationMeta.keyFrom; + const sourceMap = entities.map(e => (e as AnyObject)[sourceKey]); + const sourceIds = sourceMap.flat(); + const targetKey = relationMeta.keyTo as StringKeyOf<Target>; + const dedupedSourceIds = deduplicate(sourceIds); + + const scope = + typeof inclusion === 'string' ? {} : (inclusion.scope as Filter<Target>); + + const targetRepo = await getTargetRepo(); + const targetsFound = await findByForeignKeys( + targetRepo, + targetKey, + dedupedSourceIds.filter(e => e), + scope, + options, + ); + + return sourceMap.map(chainIds => { + if (!chainIds) return []; + const targets = flattenTargetsOfOneToOneRelation( + chainIds, + targetsFound, + targetKey, + ); + return targets.filter((v): v is Target & TargetRelations => v != null); + }); + }; +} diff --git a/packages/repository/src/relations/references-many/references-many.repository.ts b/packages/repository/src/relations/references-many/references-many.repository.ts new file mode 100644 index 000000000000..8579268322cf --- /dev/null +++ b/packages/repository/src/relations/references-many/references-many.repository.ts @@ -0,0 +1,55 @@ +// Copyright IBM Corp. 2018,2020. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter} from '@loopback/core'; +import {DataObject, Options} from '../../common-types'; +import {EntityNotFoundError} from '../../errors'; +import {Entity} from '../../model'; +import {constrainFilter, EntityCrudRepository} from '../../repositories'; + +/** + * CRUD operations for a target repository of a ReferencesMany relation + */ +export interface ReferencesManyRepository<Target extends Entity> { + /** + * Gets the target model instance + * @param options + * @returns A promise resolved with the target object or rejected + * with an EntityNotFoundError when target model instance was not found. + */ + get(options?: Options): Promise<Target>; +} + +export class DefaultReferencesManyRepository< + TargetEntity extends Entity, + TargetIds, + TargetRepository extends EntityCrudRepository<TargetEntity, TargetIds>, +> implements ReferencesManyRepository<TargetEntity> +{ + /** + * Constructor of DefaultReferencesManyEntityCrudRepository + * @param getTargetRepository - the getter of the related target model repository instance + * @param constraint - the key value pair representing foreign key name to constrain + * the target repository instance + */ + constructor( + public getTargetRepository: Getter<TargetRepository>, + public constraint: DataObject<TargetEntity>, + ) {} + + async get(options?: Options): Promise<TargetEntity> { + const targetRepo = await this.getTargetRepository(); + const result = await targetRepo.find( + constrainFilter(undefined, this.constraint), + options, + ); + if (!result.length) { + // We don't have a direct access to the foreign key value here :( + const id = 'constraint ' + JSON.stringify(this.constraint); + throw new EntityNotFoundError(targetRepo.entityClass, id); + } + return result[0]; + } +} diff --git a/packages/repository/src/relations/relation.decorator.ts b/packages/repository/src/relations/relation.decorator.ts index 43229b9f207c..83f06eb079fd 100644 --- a/packages/repository/src/relations/relation.decorator.ts +++ b/packages/repository/src/relations/relation.decorator.ts @@ -76,15 +76,3 @@ export function referencesOne(definition?: Object) { decoratorName: '@referencesOne', }); } - -/** - * Decorator for referencesMany - * @param definition - * @returns A property decorator - */ -export function referencesMany(definition?: Object) { - const rel = Object.assign({type: RelationType.referencesMany}, definition); - return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel, { - decoratorName: '@referencesMany', - }); -} diff --git a/packages/repository/src/relations/relation.types.ts b/packages/repository/src/relations/relation.types.ts index ed17b026c193..6dc26a424d8c 100644 --- a/packages/repository/src/relations/relation.types.ts +++ b/packages/repository/src/relations/relation.types.ts @@ -143,6 +143,21 @@ export interface HasOneDefinition extends RelationDefinitionBase { keyFrom?: string; } +export interface ReferencesManyDefinition extends RelationDefinitionBase { + type: RelationType.referencesMany; + targetsMany: true; + + /** + * keyTo: The foreign key used by the target model for this relation. + * keyFrom: The source key used by the source model for this relation. + * + * TODO(bajtos) Add relation description. + * + */ + keyTo?: string; + keyFrom?: string; +} + /** * A union type describing all possible Relation metadata objects. */ @@ -150,6 +165,7 @@ export type RelationMetadata = | HasManyDefinition | BelongsToDefinition | HasOneDefinition + | ReferencesManyDefinition // TODO(bajtos) add other relation types and remove RelationDefinitionBase once // all relation types are covered. | RelationDefinitionBase; diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 51988c28f75f..8988f7cd09b3 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -36,6 +36,7 @@ import { createHasManyRepositoryFactory, createHasManyThroughRepositoryFactory, createHasOneRepositoryFactory, + createReferencesManyAccessor, HasManyDefinition, HasManyRepositoryFactory, HasManyThroughRepositoryFactory, @@ -43,6 +44,8 @@ import { HasOneRepositoryFactory, includeRelatedModels, InclusionResolver, + ReferencesManyAccessor, + ReferencesManyDefinition, } from '../relations'; import {IsolationLevel, Transaction} from '../transaction'; import {isTypeResolver, resolveType} from '../type-resolver'; @@ -423,6 +426,40 @@ export class DefaultCrudRepository< ); } + /** + * @deprecated + * Function to create a references many accessor + * + * Use `this.createReferencesManyAccessorFor()` instead + * + * @param relationName - Name of the relation defined on the source model + * @param targetRepo - Target repository instance + */ + protected _createReferencesManyAccessorFor<Target extends Entity, TargetId>( + relationName: string, + targetRepoGetter: Getter<EntityCrudRepository<Target, TargetId>>, + ): ReferencesManyAccessor<Target, ID> { + return this.createReferencesManyAccessorFor(relationName, targetRepoGetter); + } + + /** + * Function to create a references many accessor + * + * @param relationName - Name of the relation defined on the source model + * @param targetRepo - Target repository instance + */ + protected createReferencesManyAccessorFor<Target extends Entity, TargetId>( + relationName: string, + targetRepoGetter: Getter<EntityCrudRepository<Target, TargetId>>, + ): ReferencesManyAccessor<Target, ID> { + const meta = this.entityClass.definition.relations[relationName]; + return createReferencesManyAccessor<Target, TargetId, T, ID>( + meta as ReferencesManyDefinition, + targetRepoGetter, + this, + ); + } + async create(entity: DataObject<T>, options?: Options): Promise<T> { // perform persist hook const data = await this.entityToData(entity, options); diff --git a/tsconfig.json b/tsconfig.json index 98bca0b9cd75..31bfe4afd0b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -67,6 +67,9 @@ { "path": "examples/passport-login/tsconfig.json" }, + { + "path": "examples/references-many/tsconfig.json" + }, { "path": "examples/rest-crud/tsconfig.json" },