Merge branch 'master' into cdedreuille/ui-neutral-bg-simplification
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli-common': patch
|
||||
---
|
||||
|
||||
The `findOwnRootDir` utility now searches for the monorepo root by traversing up the directory tree looking for a `package.json` with `workspaces`, instead of assuming a fixed `../..` relative path. If no workspaces root is found during this traversal, `findOwnRootDir` now throws to enforce stricter validation of the repository layout.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli-node': patch
|
||||
---
|
||||
|
||||
Added `toString()` method to `Lockfile` for serializing lockfiles back to string format.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Migrated internal versioning utilities to use `@backstage/cli-node` instead of a local implementation.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed `--bui-bg-popover` CSS token. Popover, Tooltip, Menu, and Dialog now use `--bui-bg-app` for their outer shell and `Box bg="neutral-1"` for content areas, providing better theme consistency and eliminating a redundant token.
|
||||
|
||||
**Migration:**
|
||||
|
||||
Replace any usage of `--bui-bg-popover` with `--bui-bg-neutral-1` (for content surfaces) or `--bui-bg-app` (for outer shells):
|
||||
|
||||
```diff
|
||||
- background: var(--bui-bg-popover);
|
||||
+ background: var(--bui-bg-neutral-1);
|
||||
```
|
||||
|
||||
**Affected components:** Popover, Tooltip, Menu, Dialog
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-github': minor
|
||||
---
|
||||
|
||||
The default user transformer now prefers organization verified domain emails over the user's public GitHub email when populating the user entity profile. It also strips plus-addressed routing tags that GitHub adds to these emails.
|
||||
|
||||
If you want to retain the old behavior, you can do so with a custom user transformer using the `githubOrgEntityProviderTransformsExtensionPoint`:
|
||||
|
||||
```ts
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { githubOrgEntityProviderTransformsExtensionPoint } from '@backstage/plugin-catalog-backend-module-github-org';
|
||||
import { defaultUserTransformer } from '@backstage/plugin-catalog-backend-module-github';
|
||||
|
||||
export default createBackendModule({
|
||||
pluginId: 'catalog',
|
||||
moduleId: 'github-org-custom-transforms',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
transforms: githubOrgEntityProviderTransformsExtensionPoint,
|
||||
},
|
||||
async init({ transforms }) {
|
||||
transforms.setUserTransformer(async (item, ctx) => {
|
||||
const entity = await defaultUserTransformer(item, ctx);
|
||||
if (entity && item.email) {
|
||||
entity.spec.profile!.email = item.email;
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Internal refactor of CLI command modules.
|
||||
+39
-1
@@ -210,5 +210,43 @@
|
||||
"@backstage/plugin-user-settings-backend": "0.4.0",
|
||||
"@backstage/plugin-user-settings-common": "0.1.0"
|
||||
},
|
||||
"changesets": []
|
||||
"changesets": [
|
||||
"add-masked-and-hidden-option",
|
||||
"blue-moons-crash",
|
||||
"bright-moons-open",
|
||||
"brown-towns-find",
|
||||
"bump-bfj-v9",
|
||||
"cli-common-cached-paths",
|
||||
"cli-internal-refactor",
|
||||
"cli-node-parallel-helpers",
|
||||
"cli-translations-export-import",
|
||||
"dependabot-d2ec7e9",
|
||||
"fancy-ends-turn",
|
||||
"fix-frontend-feature-compat",
|
||||
"fix-prettier-existence-check",
|
||||
"fluffy-owls-act",
|
||||
"long-hairs-throw",
|
||||
"mean-fans-decide",
|
||||
"metal-humans-move",
|
||||
"migrate-to-target-paths",
|
||||
"ninety-corners-flash",
|
||||
"orange-mugs-post-1",
|
||||
"orange-mugs-post-2",
|
||||
"pink-terms-know",
|
||||
"polite-singers-lead",
|
||||
"pretty-days-taste",
|
||||
"rare-adults-attack",
|
||||
"renovate-8b1c21e",
|
||||
"rude-groups-shout",
|
||||
"scaffolder-export-form-fields-api",
|
||||
"silver-pigs-remain",
|
||||
"sixty-pianos-begin",
|
||||
"stable-translation-plugin-app",
|
||||
"stable-translation-test-utils",
|
||||
"stupid-pans-hope",
|
||||
"swift-flowers-grin",
|
||||
"swift-ravens-jog",
|
||||
"tired-bushes-write",
|
||||
"twenty-worlds-create"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Added predicate-based entity filtering via POST /entities/by-query endpoint.
|
||||
|
||||
Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support.
|
||||
|
||||
The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided.
|
||||
@@ -1 +0,0 @@
|
||||
Follow the instructions at /.github/copilot-instructions.md
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../AGENTS.md
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
description: General project guidelines for Backstage development
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
Follow the instructions at /.github/copilot-instructions.md
|
||||
@@ -1,49 +1,3 @@
|
||||
Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces.
|
||||
|
||||
## Key Directories
|
||||
|
||||
- `/packages`: Core framework packages (prefixed `@backstage/`)
|
||||
- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`)
|
||||
- `/packages/app` and `/packages/backend`: Example app for local development
|
||||
- `/packages/app`: Main example app using the new frontend system
|
||||
- `/packages/app-legacy`: Example app using the old frontend system
|
||||
- `/docs`: Documentation files
|
||||
|
||||
Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system.
|
||||
|
||||
## Code Standards
|
||||
|
||||
The following files contain guidelines for the project:
|
||||
|
||||
- `/CONTRIBUTING.md`: comprehensive contribution guidelines.
|
||||
- `/STYLE.md`: guidelines for code style.
|
||||
- `/REVIEWING.md`: guidelines for pull requests and writing changesets.
|
||||
- `/SECURITY.md`: guidelines for security.
|
||||
- `/docs/architecture-decisions/`: contains the architecture decisions for the project.
|
||||
|
||||
## Development Flow
|
||||
|
||||
Before any of these commands can be run, you need to run `yarn install` in the project root.
|
||||
|
||||
- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline.
|
||||
- Test: Use `yarn test --no-watch <path>` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests.
|
||||
- Type checking: Use `yarn tsc` in the project root to run the type checker.
|
||||
- Code formatting: Use `yarn prettier --write <path>` to format code.
|
||||
- Lint: Use `yarn lint --fix` in the project root to run the linter.
|
||||
- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages.
|
||||
- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007).
|
||||
- Create: Use `yarn new` to scaffold new plugins, packages, or modules.
|
||||
|
||||
You MUST NOT create a release by running `yarn changesets version` or `yarn release` as part of any changes. Releases are created by separate workflows.
|
||||
|
||||
All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher.
|
||||
|
||||
Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
See `/docs/contribute/project-structure.md` for a detailed description of the repository structure.
|
||||
|
||||
## Pull Request Reviews
|
||||
|
||||
When reviewing a pull request you MUST calculate a priority score between 0 and 100, where a higher score indicates higher priority. You MUST include the score as an HTML comment at the very beginning of the "PR Overview" section, using the following format: `<!-- priority: <score> -->`.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces.
|
||||
|
||||
## Key Directories
|
||||
|
||||
- `/packages`: Core framework packages (prefixed `@backstage/`)
|
||||
- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`)
|
||||
- `/packages/app` and `/packages/backend`: Example app for local development
|
||||
- `/packages/app`: Main example app using the new frontend system
|
||||
- `/packages/app-legacy`: Example app using the old frontend system
|
||||
- `/docs`: Documentation files
|
||||
|
||||
Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system.
|
||||
|
||||
## Code Standards
|
||||
|
||||
The following files contain guidelines for the project:
|
||||
|
||||
- `/CONTRIBUTING.md`: comprehensive contribution guidelines.
|
||||
- `/STYLE.md`: guidelines for code style.
|
||||
- `/REVIEWING.md`: guidelines for pull requests and writing changesets.
|
||||
- `/SECURITY.md`: guidelines for security.
|
||||
- `/docs/architecture-decisions/`: contains the architecture decisions for the project.
|
||||
|
||||
When writing or generating code, always match the existing coding style of each individual package and file. Different packages in the monorepo may have different conventions — consistency within a package is more important than consistency across the repo.
|
||||
|
||||
When writing or generating tests, prefer fewer thorough tests with multiple assertions over many small tests. When using React Testing Library, prefer using `screen` and `.findBy*` queries over `waitFor`, and avoid adding test IDs to the implementation.
|
||||
|
||||
## Development Flow
|
||||
|
||||
Before any of these commands can be run, you need to run `yarn install` in the project root.
|
||||
|
||||
- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline.
|
||||
- Test: Use `CI=1 yarn test <path>` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests.
|
||||
- Type checking: Use `yarn tsc` in the project root to run the type checker. Do not try to run it somewhere else than the project root and do not supply any options.
|
||||
- Code formatting: Use `yarn prettier --write <...paths>` to format code. Run it explicitly for file paths that you know are changed, not for entire folders - otherwise it may change formatting of unrelated files.
|
||||
- Lint: Use `yarn lint --fix` in the project root to run the linter.
|
||||
- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages.
|
||||
- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007).
|
||||
- Create: Use `yarn new` to scaffold new plugins, packages, or modules.
|
||||
|
||||
You MUST NOT run builds or create a release by running `yarn build`, `yarn changesets version`, or `yarn release` as part of any changes. Builds and releases are made by separate workflows.
|
||||
|
||||
All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory and should be created by writing changeset files directly — never use the changeset CLI. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher. For non-breaking changes that introduce new APIs or features, use `minor` for packages at version `1.0.0` or higher, and `patch` for packages below `1.0.0`. Each changeset message should be relevant to the specific package it targets and written for Backstage adopters as the audience — avoid referencing internal implementation details. If a change spans multiple packages you often need to create separate changesets to make sure they are tailored to each package.
|
||||
|
||||
When creating pull requests, use the template at `/.github/PULL_REQUEST_TEMPLATE.md`.
|
||||
|
||||
Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested.
|
||||
|
||||
Never make changes to the release notes in `/docs/releases` unless explicitly asked. These document past releases and should not be updated based on newer changes.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
See `/docs/contribute/project-structure.md` for a detailed description of the repository structure.
|
||||
@@ -156,7 +156,6 @@ These colors form a layered neutral scale for your application backgrounds. `--b
|
||||
| Token Name | Description |
|
||||
| ----------------------------- | ------------------------------------------------------------ |
|
||||
| `--bui-bg-app` | The base background color of your Backstage instance. |
|
||||
| `--bui-bg-popover` | The background color used for popovers, tooltips, and menus. |
|
||||
| `--bui-bg-neutral-1` | First elevated layer. Use for cards, dialogs, and panels. |
|
||||
| `--bui-bg-neutral-1-hover` | Hover state for elements on neutral-1. |
|
||||
| `--bui-bg-neutral-1-pressed` | Pressed state for elements on neutral-1. |
|
||||
|
||||
@@ -210,6 +210,191 @@ if `prevCursor` exists, it can be used to retrieve the previous batch of entitie
|
||||
it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters,
|
||||
as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration.
|
||||
|
||||
### `POST /entities/by-query`
|
||||
|
||||
This supports the same features as the `GET` variant, but in a `POST` body to
|
||||
not have to abide by URL length limits. Additionally, it supports advanced, more
|
||||
expressive querying format - see below. The response format is identical.
|
||||
|
||||
#### Querying by filter predicate
|
||||
|
||||
You can pass in a filter predicate to select a subset of entities in the
|
||||
catalog. They are comprised of an optional logical expression tree (using
|
||||
`$all`, `$any`, `$not`), ending in filter sets that can have custom matchers
|
||||
(e.g. `$exists`, `$in`, `$hasPrefix`, `$contains`).
|
||||
|
||||
This is an example of what such a filter predicate expression might look like:
|
||||
|
||||
```js
|
||||
{
|
||||
"query": {
|
||||
"$all": [
|
||||
{
|
||||
"kind": "Component",
|
||||
"spec.type": { "$in": ["service", "website"] }
|
||||
},
|
||||
{
|
||||
"$not": {
|
||||
"metadata.annotations.backstage.io/orphan": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A filter set is an object whose keys are dot separated paths into an object, and
|
||||
the values are either primitives (string, number, or boolean) or custom matchers
|
||||
as per below. An example of a simple such filter set is:
|
||||
|
||||
```js
|
||||
// All of the following must be true for a given entity (there's an
|
||||
// implicit AND between them)
|
||||
{
|
||||
// The kind field is matched against a literal, case insensitively
|
||||
"kind": "Component",
|
||||
// The type field inside the spec is matched using a custom matcher, see below
|
||||
"spec.type": { "$in": ["service", "website"] }
|
||||
}
|
||||
```
|
||||
|
||||
The root of the query is always an object, whether there is a logic expression
|
||||
tree or not. Nodes with a single key that starts with a `$` sign have special
|
||||
meaning.
|
||||
|
||||
- `$not`: Logical negation.
|
||||
|
||||
Its value must be a single expression. Example:
|
||||
|
||||
```js
|
||||
// Matches entities that do NOT have kind Component
|
||||
{
|
||||
"$not": {
|
||||
"kind": "Component",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that `$not` cannot be used in a right hand side value matcher.
|
||||
|
||||
```js
|
||||
// ❌ WRONG
|
||||
{ "kind": { "$not": "Component" } }
|
||||
// ✅ CORRECT
|
||||
{ "$not": { "kind": "Component" } }
|
||||
```
|
||||
|
||||
- `$all`: Require that all given expressions match each entity.
|
||||
|
||||
Its value must be an array of expressions. Example:
|
||||
|
||||
```js
|
||||
// Matches entities that BOTH have kind Component and type website
|
||||
{
|
||||
"$all": [
|
||||
{ "kind": "Component" },
|
||||
{ "spec.type": "website" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
An empty array always matches every entity.
|
||||
|
||||
- `$any`: Require that at least one of a set of expressions match a given entity.
|
||||
|
||||
Its value must be an array of expressions. Example:
|
||||
|
||||
```js
|
||||
// Matches entities that EITHER have kind Component or type website
|
||||
{
|
||||
"$any": [
|
||||
{ "kind": "Component" },
|
||||
{ "spec.type": "website" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
An empty array never matches anything.
|
||||
|
||||
- `$exists`: Assert on the existence of fields.
|
||||
|
||||
Its value is either `true`, meaning that the field must exist on the entity
|
||||
(no matter what its value), or `false`, meaning that it must not exist.
|
||||
Example:
|
||||
|
||||
```js
|
||||
// Matches entities that DO NOT have that annotation, ignoring what the
|
||||
// value might be
|
||||
{
|
||||
"metadata.annotations.backstage.io/orphan": {
|
||||
"$exists": false
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `$in`: Assert that a field has any of a set of primitive values.
|
||||
|
||||
Its value must be an array of string, number, and/or boolean values. Example:
|
||||
|
||||
```js
|
||||
// Matches entities whose type is EITHER service or website
|
||||
{
|
||||
"spec.type": {
|
||||
"$in": ["service", "website"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The matching is case insensitive. An empty array never matches anything.
|
||||
|
||||
- `$hasPrefix`: Assert that a field is a string that starts with a certain prefix text.
|
||||
|
||||
Its value is a string. Example:
|
||||
|
||||
```js
|
||||
// Matches entities whose project slug annotation starts with "backstage/"
|
||||
{
|
||||
"metadata.annotations.github.com/project-slug": {
|
||||
"$hasPrefix": "backstage/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The matching is case insensitive, and captures both exact matches and strings
|
||||
that start with the given prefix.
|
||||
|
||||
- `$contains`: Assert that an array contains an element that matches the given expression.
|
||||
|
||||
There is only limited support for this matcher. One use case is for relations:
|
||||
|
||||
```js
|
||||
{
|
||||
// Specifically type and (optionally) targetRef supported, and only
|
||||
// with equality or "$in" for the targetRef
|
||||
"relations": {
|
||||
"$contains": {
|
||||
"type": "ownedBy",
|
||||
"targetRef": {
|
||||
"$in": ["user:default/foo", "group:default/bar"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The other use case is for arrays where you match with a primitive value, such
|
||||
as tags. Example:
|
||||
|
||||
```js
|
||||
{
|
||||
// Works for array fields whose items are primitive values
|
||||
// (typically strings, but numbers and booleans are also supported)
|
||||
"metadata.tags": {
|
||||
"$contains": "java"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /entities`
|
||||
|
||||
Lists entities.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "root",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0-next.0",
|
||||
"backstage": {
|
||||
"cli": {
|
||||
"new": {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/app-defaults
|
||||
|
||||
## 1.7.6-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/core-app-api@1.19.6-next.0
|
||||
- @backstage/core-components@0.18.8-next.0
|
||||
- @backstage/core-plugin-api@1.12.4-next.0
|
||||
- @backstage/theme@0.7.2
|
||||
- @backstage/plugin-permission-react@0.4.41-next.0
|
||||
|
||||
## 1.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/app-defaults",
|
||||
"version": "1.7.5",
|
||||
"version": "1.7.6-next.0",
|
||||
"description": "Provides the default wiring of a Backstage App",
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# app-example-plugin
|
||||
|
||||
## 0.0.33-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/frontend-plugin-api@0.14.2-next.0
|
||||
- @backstage/core-components@0.18.8-next.0
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-example-plugin",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.33-next.0",
|
||||
"description": "Backstage internal example plugin",
|
||||
"backstage": {
|
||||
"role": "frontend-plugin",
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
# example-app-legacy
|
||||
|
||||
## 0.2.119-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/ui@0.12.1-next.0
|
||||
- @backstage/plugin-search-react@1.10.5-next.0
|
||||
- @backstage/plugin-search@1.6.2-next.0
|
||||
- @backstage/plugin-api-docs@0.13.5-next.0
|
||||
- @backstage/cli@0.35.5-next.0
|
||||
- @backstage/plugin-scaffolder@1.35.5-next.0
|
||||
- @backstage/plugin-catalog@1.33.1-next.0
|
||||
- @backstage/plugin-catalog-react@2.0.1-next.0
|
||||
- @backstage/plugin-mui-to-bui@0.2.5-next.0
|
||||
- @backstage/plugin-techdocs@1.17.1-next.0
|
||||
- @backstage/app-defaults@1.7.6-next.0
|
||||
- @backstage/catalog-model@1.7.6
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/core-app-api@1.19.6-next.0
|
||||
- @backstage/core-components@0.18.8-next.0
|
||||
- @backstage/core-plugin-api@1.12.4-next.0
|
||||
- @backstage/frontend-app-api@0.15.1-next.0
|
||||
- @backstage/integration-react@1.2.16-next.0
|
||||
- @backstage/theme@0.7.2
|
||||
- @backstage/plugin-auth-react@0.1.25-next.0
|
||||
- @backstage/plugin-catalog-common@1.1.8
|
||||
- @backstage/plugin-catalog-graph@0.5.8-next.0
|
||||
- @backstage/plugin-catalog-import@0.13.11-next.0
|
||||
- @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0
|
||||
- @backstage/plugin-devtools@0.1.37-next.0
|
||||
- @backstage/plugin-home@0.9.3-next.0
|
||||
- @backstage/plugin-home-react@0.1.36-next.0
|
||||
- @backstage/plugin-kubernetes@0.12.17-next.0
|
||||
- @backstage/plugin-kubernetes-cluster@0.0.35-next.0
|
||||
- @backstage/plugin-notifications@0.5.15-next.0
|
||||
- @backstage/plugin-org@0.6.50-next.0
|
||||
- @backstage/plugin-permission-react@0.4.41-next.0
|
||||
- @backstage/plugin-scaffolder-react@1.19.8-next.0
|
||||
- @backstage/plugin-search-common@1.2.22
|
||||
- @backstage/plugin-signals@0.0.29-next.0
|
||||
- @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0
|
||||
- @backstage/plugin-techdocs-react@1.3.9-next.0
|
||||
- @backstage/plugin-user-settings@0.9.1-next.0
|
||||
|
||||
## 0.2.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-app-legacy",
|
||||
"version": "0.2.118",
|
||||
"version": "0.2.119-next.0",
|
||||
"backstage": {
|
||||
"role": "frontend"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# example-app
|
||||
|
||||
## 0.0.33-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/ui@0.12.1-next.0
|
||||
- @backstage/plugin-search-react@1.10.5-next.0
|
||||
- @backstage/plugin-search@1.6.2-next.0
|
||||
- @backstage/plugin-api-docs@0.13.5-next.0
|
||||
- @backstage/cli@0.35.5-next.0
|
||||
- @backstage/frontend-plugin-api@0.14.2-next.0
|
||||
- @backstage/plugin-scaffolder@1.35.5-next.0
|
||||
- @backstage/plugin-app@0.4.1-next.0
|
||||
- @backstage/plugin-app-visualizer@0.2.1-next.0
|
||||
- @backstage/plugin-catalog@1.33.1-next.0
|
||||
- @backstage/plugin-catalog-react@2.0.1-next.0
|
||||
- @backstage/plugin-techdocs@1.17.1-next.0
|
||||
- @backstage/app-defaults@1.7.6-next.0
|
||||
- @backstage/catalog-model@1.7.6
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/core-app-api@1.19.6-next.0
|
||||
- @backstage/core-compat-api@0.5.9-next.0
|
||||
- @backstage/core-components@0.18.8-next.0
|
||||
- @backstage/core-plugin-api@1.12.4-next.0
|
||||
- @backstage/frontend-app-api@0.15.1-next.0
|
||||
- @backstage/frontend-defaults@0.4.1-next.0
|
||||
- @backstage/integration-react@1.2.16-next.0
|
||||
- @backstage/theme@0.7.2
|
||||
- @backstage/plugin-app-react@0.2.1-next.0
|
||||
- @backstage/plugin-auth@0.1.6-next.0
|
||||
- @backstage/plugin-auth-react@0.1.25-next.0
|
||||
- @backstage/plugin-catalog-common@1.1.8
|
||||
- @backstage/plugin-catalog-graph@0.5.8-next.0
|
||||
- @backstage/plugin-catalog-import@0.13.11-next.0
|
||||
- @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0
|
||||
- @backstage/plugin-devtools@0.1.37-next.0
|
||||
- @backstage/plugin-home@0.9.3-next.0
|
||||
- @backstage/plugin-home-react@0.1.36-next.0
|
||||
- @backstage/plugin-kubernetes@0.12.17-next.0
|
||||
- @backstage/plugin-kubernetes-cluster@0.0.35-next.0
|
||||
- @backstage/plugin-notifications@0.5.15-next.0
|
||||
- @backstage/plugin-org@0.6.50-next.0
|
||||
- @backstage/plugin-permission-react@0.4.41-next.0
|
||||
- @backstage/plugin-scaffolder-react@1.19.8-next.0
|
||||
- @backstage/plugin-search-common@1.2.22
|
||||
- @backstage/plugin-signals@0.0.29-next.0
|
||||
- @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0
|
||||
- @backstage/plugin-techdocs-react@1.3.9-next.0
|
||||
- @backstage/plugin-user-settings@0.9.1-next.0
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.33-next.0",
|
||||
"backstage": {
|
||||
"role": "frontend"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/backend-app-api
|
||||
|
||||
## 1.5.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/backend-plugin-api@1.7.1-next.0
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
|
||||
## 1.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-app-api",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1-next.0",
|
||||
"description": "Core API used by Backstage backend apps",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# @backstage/backend-defaults
|
||||
|
||||
## 0.15.3-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1
|
||||
- d933f62: Add configurable throttling and retry mechanism for GitLab integration.
|
||||
- b99158a: Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding
|
||||
an optional `default` type discriminator to PostgreSQL connection configuration,
|
||||
allowing `config:check` to properly validate `default` connection configurations.
|
||||
- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins.
|
||||
- Updated dependencies
|
||||
- @backstage/cli-node@0.2.19-next.0
|
||||
- @backstage/integration@1.21.0-next.0
|
||||
- @backstage/config-loader@1.10.9-next.0
|
||||
- @backstage/backend-plugin-api@1.7.1-next.0
|
||||
- @backstage/backend-app-api@1.5.1-next.0
|
||||
- @backstage/backend-dev-utils@0.1.7
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/integration-aws-node@0.1.20
|
||||
- @backstage/types@1.2.2
|
||||
- @backstage/plugin-auth-node@0.6.14-next.0
|
||||
- @backstage/plugin-events-node@0.4.20-next.0
|
||||
- @backstage/plugin-permission-node@0.10.11-next.0
|
||||
|
||||
## 0.15.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-defaults",
|
||||
"version": "0.15.2",
|
||||
"version": "0.15.3-next.0",
|
||||
"description": "Backend defaults used by Backstage backend apps",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @backstage/backend-dynamic-feature-service
|
||||
|
||||
## 0.7.10-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
|
||||
- Updated dependencies
|
||||
- @backstage/cli-common@0.2.0-next.0
|
||||
- @backstage/cli-node@0.2.19-next.0
|
||||
- @backstage/backend-defaults@0.15.3-next.0
|
||||
- @backstage/plugin-catalog-backend@3.5.0-next.0
|
||||
- @backstage/config-loader@1.10.9-next.0
|
||||
- @backstage/backend-plugin-api@1.7.1-next.0
|
||||
- @backstage/backend-openapi-utils@0.6.7-next.0
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/types@1.2.2
|
||||
- @backstage/plugin-app-node@0.1.43-next.0
|
||||
- @backstage/plugin-auth-node@0.6.14-next.0
|
||||
- @backstage/plugin-events-backend@0.5.12-next.0
|
||||
- @backstage/plugin-events-node@0.4.20-next.0
|
||||
- @backstage/plugin-permission-common@0.9.6
|
||||
- @backstage/plugin-permission-node@0.10.11-next.0
|
||||
- @backstage/plugin-scaffolder-node@0.12.6-next.0
|
||||
- @backstage/plugin-search-backend-node@1.4.2-next.0
|
||||
- @backstage/plugin-search-common@1.2.22
|
||||
|
||||
## 0.7.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-dynamic-feature-service",
|
||||
"version": "0.7.9",
|
||||
"version": "0.7.10-next.0",
|
||||
"description": "Backstage dynamic feature service",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/backend-openapi-utils
|
||||
|
||||
## 0.6.7-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/backend-plugin-api@1.7.1-next.0
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/types@1.2.2
|
||||
|
||||
## 0.6.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-openapi-utils",
|
||||
"version": "0.6.6",
|
||||
"version": "0.6.7-next.0",
|
||||
"description": "OpenAPI typescript support.",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @backstage/backend-plugin-api
|
||||
|
||||
## 1.7.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins.
|
||||
- Updated dependencies
|
||||
- @backstage/cli-common@0.2.0-next.0
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/types@1.2.2
|
||||
- @backstage/plugin-auth-node@0.6.14-next.0
|
||||
- @backstage/plugin-permission-common@0.9.6
|
||||
- @backstage/plugin-permission-node@0.10.11-next.0
|
||||
|
||||
## 1.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-plugin-api",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1-next.0",
|
||||
"description": "Core API used by Backstage backend plugins",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @backstage/backend-test-utils
|
||||
|
||||
## 1.11.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1ee5b28: Adds a new metrics service mock to be leveraged in tests
|
||||
- Updated dependencies
|
||||
- @backstage/backend-defaults@0.15.3-next.0
|
||||
- @backstage/backend-plugin-api@1.7.1-next.0
|
||||
- @backstage/backend-app-api@1.5.1-next.0
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/types@1.2.2
|
||||
- @backstage/plugin-auth-node@0.6.14-next.0
|
||||
- @backstage/plugin-events-node@0.4.20-next.0
|
||||
- @backstage/plugin-permission-common@0.9.6
|
||||
|
||||
## 1.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-test-utils",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1-next.0",
|
||||
"description": "Test helpers library for Backstage backends",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
# example-backend
|
||||
|
||||
## 0.0.48-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/backend-defaults@0.15.3-next.0
|
||||
- @backstage/plugin-auth-backend@0.27.1-next.0
|
||||
- @backstage/plugin-catalog-backend@3.5.0-next.0
|
||||
- @backstage/plugin-scaffolder-backend@3.1.4-next.0
|
||||
- @backstage/backend-plugin-api@1.7.1-next.0
|
||||
- @backstage/plugin-mcp-actions-backend@0.1.10-next.0
|
||||
- @backstage/catalog-model@1.7.6
|
||||
- @backstage/plugin-app-backend@0.5.12-next.0
|
||||
- @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0
|
||||
- @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0
|
||||
- @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0
|
||||
- @backstage/plugin-auth-node@0.6.14-next.0
|
||||
- @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0
|
||||
- @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0
|
||||
- @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0
|
||||
- @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0
|
||||
- @backstage/plugin-devtools-backend@0.5.15-next.0
|
||||
- @backstage/plugin-events-backend@0.5.12-next.0
|
||||
- @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0
|
||||
- @backstage/plugin-kubernetes-backend@0.21.2-next.0
|
||||
- @backstage/plugin-notifications-backend@0.6.3-next.0
|
||||
- @backstage/plugin-permission-backend@0.7.10-next.0
|
||||
- @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0
|
||||
- @backstage/plugin-permission-common@0.9.6
|
||||
- @backstage/plugin-permission-node@0.10.11-next.0
|
||||
- @backstage/plugin-proxy-backend@0.6.11-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0
|
||||
- @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0
|
||||
- @backstage/plugin-search-backend@2.0.13-next.0
|
||||
- @backstage/plugin-search-backend-module-catalog@0.3.13-next.0
|
||||
- @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0
|
||||
- @backstage/plugin-search-backend-module-explore@0.3.12-next.0
|
||||
- @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0
|
||||
- @backstage/plugin-search-backend-node@1.4.2-next.0
|
||||
- @backstage/plugin-signals-backend@0.3.13-next.0
|
||||
- @backstage/plugin-techdocs-backend@2.1.6-next.0
|
||||
|
||||
## 0.0.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.0.47",
|
||||
"version": "0.0.48-next.0",
|
||||
"backstage": {
|
||||
"role": "backend"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @backstage/catalog-client
|
||||
|
||||
## 1.13.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d2494d6: Minor update to catalog client docs
|
||||
- Updated dependencies
|
||||
- @backstage/catalog-model@1.7.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/filter-predicates@0.1.0
|
||||
|
||||
## 1.13.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/catalog-client",
|
||||
"version": "1.13.0",
|
||||
"version": "1.13.1-next.0",
|
||||
"description": "An isomorphic client for the catalog backend",
|
||||
"backstage": {
|
||||
"role": "common-library"
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { AnalyzeLocationRequest } from '@backstage/plugin-catalog-common';
|
||||
import type { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { FilterPredicate } from '@backstage/filter-predicates';
|
||||
import { SerializedError } from '@backstage/errors';
|
||||
import type { FilterPredicate } from '@backstage/filter-predicates';
|
||||
import type { SerializedError } from '@backstage/errors';
|
||||
|
||||
// @public
|
||||
export type AddLocationRequest = {
|
||||
@@ -320,6 +320,7 @@ export type QueryEntitiesInitialRequest = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
filter?: EntityFilterQuery;
|
||||
query?: FilterPredicate;
|
||||
orderFields?: EntityOrderQuery;
|
||||
fullTextFilter?: {
|
||||
term: string;
|
||||
|
||||
@@ -540,6 +540,350 @@ describe('CatalogClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryEntities with predicate-based queries (POST endpoint)', () => {
|
||||
const defaultResponse = {
|
||||
items: [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'service-1',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'team-a',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'service-2',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'team-b',
|
||||
},
|
||||
},
|
||||
],
|
||||
totalItems: 2,
|
||||
pageInfo: {},
|
||||
};
|
||||
|
||||
it('should use POST endpoint when query is provided', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.method).toBe('POST');
|
||||
expect(req.body).toMatchObject({
|
||||
query: { kind: 'component' },
|
||||
limit: 20,
|
||||
});
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
const response = await client.queryEntities({
|
||||
query: { kind: 'component' },
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
expect(response.items).toEqual(defaultResponse.items);
|
||||
expect(response.totalItems).toBe(2);
|
||||
});
|
||||
|
||||
it('should support $all operator', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body).toMatchObject({
|
||||
query: {
|
||||
$all: [{ kind: 'component' }, { 'spec.type': 'service' }],
|
||||
},
|
||||
});
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: {
|
||||
$all: [{ kind: 'component' }, { 'spec.type': 'service' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support $any operator', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body).toMatchObject({
|
||||
query: {
|
||||
$any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }],
|
||||
},
|
||||
});
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: {
|
||||
$any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support $not operator', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body).toMatchObject({
|
||||
query: {
|
||||
$not: { 'spec.lifecycle': 'experimental' },
|
||||
},
|
||||
});
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: {
|
||||
$not: { 'spec.lifecycle': 'experimental' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support $exists operator', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body).toMatchObject({
|
||||
query: {
|
||||
'spec.owner': { $exists: true },
|
||||
},
|
||||
});
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: {
|
||||
'spec.owner': { $exists: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support $in operator', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body).toMatchObject({
|
||||
query: {
|
||||
'spec.owner': { $in: ['team-a', 'team-b', 'team-c'] },
|
||||
},
|
||||
});
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: {
|
||||
'spec.owner': { $in: ['team-a', 'team-b', 'team-c'] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support complex nested predicates', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body).toMatchObject({
|
||||
query: {
|
||||
$all: [
|
||||
{ kind: 'component' },
|
||||
{
|
||||
$any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }],
|
||||
},
|
||||
{
|
||||
$not: {
|
||||
'spec.lifecycle': 'experimental',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: {
|
||||
$all: [
|
||||
{ kind: 'component' },
|
||||
{
|
||||
$any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }],
|
||||
},
|
||||
{
|
||||
$not: {
|
||||
'spec.lifecycle': 'experimental',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should send orderFields with correct format', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body.orderBy).toEqual([
|
||||
{ field: 'metadata.name', order: 'asc' },
|
||||
]);
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: { kind: 'component' },
|
||||
orderFields: { field: 'metadata.name', order: 'asc' },
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should send multiple orderFields with correct format', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body.orderBy).toEqual([
|
||||
{ field: 'metadata.name', order: 'asc' },
|
||||
{ field: 'spec.type', order: 'desc' },
|
||||
]);
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: { kind: 'component' },
|
||||
orderFields: [
|
||||
{ field: 'metadata.name', order: 'asc' },
|
||||
{ field: 'spec.type', order: 'desc' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should send limit and offset parameters in the body', async () => {
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.body.limit).toBe(50);
|
||||
return res(ctx.json(defaultResponse));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await client.queryEntities({
|
||||
query: { kind: 'component' },
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should paginate using POST when cursor contains a query', async () => {
|
||||
// Simulate a cursor that contains a query predicate (as the server would encode it)
|
||||
const cursorPayload = Buffer.from(
|
||||
JSON.stringify({
|
||||
orderFields: [],
|
||||
orderFieldValues: [],
|
||||
isPrevious: false,
|
||||
query: { kind: 'component' },
|
||||
totalItems: 100,
|
||||
}),
|
||||
).toString('base64');
|
||||
|
||||
const page2Response = {
|
||||
items: [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'service-3', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
totalItems: 100,
|
||||
pageInfo: {},
|
||||
};
|
||||
|
||||
const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => {
|
||||
expect(req.method).toBe('POST');
|
||||
expect(req.body).toMatchObject({ cursor: cursorPayload });
|
||||
return res(ctx.json(page2Response));
|
||||
});
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
const response = await client.queryEntities({
|
||||
cursor: cursorPayload,
|
||||
});
|
||||
|
||||
expect(mockedEndpoint).toHaveBeenCalledTimes(1);
|
||||
expect(response.items).toEqual(page2Response.items);
|
||||
expect(response.totalItems).toBe(100);
|
||||
});
|
||||
|
||||
it('should use GET endpoint for cursor without query', async () => {
|
||||
// A cursor that does NOT contain a query field should go to GET
|
||||
const cursorPayload = Buffer.from(
|
||||
JSON.stringify({
|
||||
orderFields: [],
|
||||
orderFieldValues: [],
|
||||
isPrevious: false,
|
||||
totalItems: 50,
|
||||
}),
|
||||
).toString('base64');
|
||||
|
||||
const mockedGetEndpoint = jest.fn().mockImplementation((_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
items: [],
|
||||
totalItems: 50,
|
||||
pageInfo: {},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const mockedPostEndpoint = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/entities/by-query`, mockedGetEndpoint),
|
||||
rest.post(`${mockBaseUrl}/entities/by-query`, mockedPostEndpoint),
|
||||
);
|
||||
|
||||
await client.queryEntities({ cursor: cursorPayload });
|
||||
|
||||
expect(mockedGetEndpoint).toHaveBeenCalledTimes(1);
|
||||
expect(mockedPostEndpoint).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors from POST endpoint', async () => {
|
||||
const mockedEndpoint = jest
|
||||
.fn()
|
||||
.mockImplementation((_req, res, ctx) => res(ctx.status(400)));
|
||||
|
||||
server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint));
|
||||
|
||||
await expect(() =>
|
||||
client.queryEntities({ query: { kind: 'component' } }),
|
||||
).rejects.toThrow(/Request failed with 400/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('streamEntities', () => {
|
||||
const defaultResponse: QueryEntitiesResponse = {
|
||||
items: [
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
parseEntityRef,
|
||||
stringifyLocationRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import { InputError, ResponseError } from '@backstage/errors';
|
||||
import { FilterPredicate } from '@backstage/filter-predicates';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
@@ -46,10 +47,17 @@ import {
|
||||
StreamEntitiesRequest,
|
||||
ValidateEntityResponse,
|
||||
} from './types/api';
|
||||
import { isQueryEntitiesInitialRequest, splitRefsIntoChunks } from './utils';
|
||||
import {
|
||||
convertFilterToPredicate,
|
||||
isQueryEntitiesInitialRequest,
|
||||
splitRefsIntoChunks,
|
||||
cursorContainsQuery,
|
||||
} from './utils';
|
||||
import {
|
||||
DefaultApiClient,
|
||||
GetEntitiesByQuery,
|
||||
GetLocationsByQueryRequest,
|
||||
QueryEntitiesByPredicateRequest,
|
||||
TypedResponse,
|
||||
} from './schema/openapi';
|
||||
import type {
|
||||
@@ -266,11 +274,26 @@ export class CatalogClient implements CatalogApi {
|
||||
request: QueryEntitiesRequest = {},
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<QueryEntitiesResponse> {
|
||||
const params: Partial<
|
||||
Parameters<typeof this.apiClient.getEntitiesByQuery>[0]['query']
|
||||
> = {};
|
||||
const isInitialRequest = isQueryEntitiesInitialRequest(request);
|
||||
|
||||
if (isQueryEntitiesInitialRequest(request)) {
|
||||
// Route to POST endpoint if query predicate is provided (initial request)
|
||||
if (isInitialRequest && request.query) {
|
||||
return this.queryEntitiesByPredicate(request, options);
|
||||
}
|
||||
|
||||
// Route to POST endpoint if cursor contains a query predicate (pagination)
|
||||
// TODO(freben): It's costly and non-opaque to have to introspect the cursor
|
||||
// like this. It should be refactored in the future to not need this.
|
||||
// Suggestion: make the GET and POST endpoints understand the same cursor
|
||||
// format, and pick which one to call ONLY based on whether the cursor size
|
||||
// risks hitting url length limits
|
||||
if (!isInitialRequest && cursorContainsQuery(request.cursor)) {
|
||||
return this.queryEntitiesByPredicate(request, options);
|
||||
}
|
||||
|
||||
const params: Partial<GetEntitiesByQuery['query']> = {};
|
||||
|
||||
if (isInitialRequest) {
|
||||
const {
|
||||
fields = [],
|
||||
filter,
|
||||
@@ -320,6 +343,84 @@ export class CatalogClient implements CatalogApi {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query entities using predicate-based filters (POST endpoint).
|
||||
* @internal
|
||||
*/
|
||||
private async queryEntitiesByPredicate(
|
||||
request: QueryEntitiesRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<QueryEntitiesResponse> {
|
||||
const body: QueryEntitiesByPredicateRequest = {};
|
||||
|
||||
if (isQueryEntitiesInitialRequest(request)) {
|
||||
const {
|
||||
filter,
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
orderFields,
|
||||
fullTextFilter,
|
||||
fields,
|
||||
} = request;
|
||||
|
||||
let filterPredicate: FilterPredicate | undefined;
|
||||
if (query !== undefined) {
|
||||
if (
|
||||
typeof query !== 'object' ||
|
||||
query === null ||
|
||||
Array.isArray(query)
|
||||
) {
|
||||
throw new InputError('Query must be an object');
|
||||
}
|
||||
filterPredicate = query;
|
||||
}
|
||||
if (filter !== undefined) {
|
||||
const converted = convertFilterToPredicate(filter);
|
||||
filterPredicate = filterPredicate
|
||||
? { $all: [filterPredicate, converted] }
|
||||
: converted;
|
||||
}
|
||||
if (filterPredicate !== undefined) {
|
||||
body.query = filterPredicate as unknown as { [key: string]: any };
|
||||
}
|
||||
|
||||
if (limit !== undefined) {
|
||||
body.limit = limit;
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
body.offset = offset;
|
||||
}
|
||||
if (orderFields !== undefined) {
|
||||
body.orderBy = [orderFields].flat();
|
||||
}
|
||||
if (fullTextFilter) {
|
||||
body.fullTextFilter = fullTextFilter;
|
||||
}
|
||||
if (fields?.length) {
|
||||
body.fields = fields;
|
||||
}
|
||||
} else {
|
||||
body.cursor = request.cursor;
|
||||
if (request.limit !== undefined) {
|
||||
body.limit = request.limit;
|
||||
}
|
||||
if (request.fields?.length) {
|
||||
body.fields = request.fields;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await this.requestRequired(
|
||||
await this.apiClient.queryEntitiesByPredicate({ body }, options),
|
||||
);
|
||||
|
||||
return {
|
||||
items: res.items,
|
||||
totalItems: res.totalItems,
|
||||
pageInfo: res.pageInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc CatalogApi.getEntityByRef}
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Entity } from '../models/Entity.model';
|
||||
import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model';
|
||||
import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model';
|
||||
import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model';
|
||||
import { QueryEntitiesByPredicateRequest } from '../models/QueryEntitiesByPredicateRequest.model';
|
||||
import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model';
|
||||
import { ValidateEntityRequest } from '../models/ValidateEntityRequest.model';
|
||||
import { AnalyzeLocationRequest } from '../models/AnalyzeLocationRequest.model';
|
||||
@@ -139,6 +140,12 @@ export type GetEntityFacets = {
|
||||
filter?: Array<string>;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type QueryEntitiesByPredicate = {
|
||||
body: QueryEntitiesByPredicateRequest;
|
||||
};
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -449,6 +456,31 @@ export class DefaultApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query entities using predicate-based filters.
|
||||
* @param queryEntitiesByPredicateRequest -
|
||||
*/
|
||||
public async queryEntitiesByPredicate(
|
||||
// @ts-ignore
|
||||
request: QueryEntitiesByPredicate,
|
||||
options?: RequestOptions,
|
||||
): Promise<TypedResponse<EntitiesQueryResponse>> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
|
||||
|
||||
const uriTemplate = `/entities/by-query`;
|
||||
|
||||
const uri = parser.parse(uriTemplate).expand({});
|
||||
|
||||
return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request.body),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the entity related to entityRef.
|
||||
* @param refreshEntityRequest -
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model';
|
||||
import { QueryEntitiesByPredicateRequestOrderByInner } from '../models/QueryEntitiesByPredicateRequestOrderByInner.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface QueryEntitiesByPredicateRequest {
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: Array<QueryEntitiesByPredicateRequestOrderByInner>;
|
||||
fullTextFilter?: QueryEntitiesByPredicateRequestFullTextFilter;
|
||||
fields?: Array<string>;
|
||||
/**
|
||||
* A type representing all allowed JSON object values.
|
||||
*/
|
||||
query?: { [key: string]: any };
|
||||
}
|
||||
+12
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Lockfile } from './Lockfile';
|
||||
export { fetchPackageInfo, mapDependencies } from './packages';
|
||||
export type { YarnInfoInspectData } from './packages';
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface QueryEntitiesByPredicateRequestFullTextFilter {
|
||||
term?: string;
|
||||
fields?: Array<string>;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface QueryEntitiesByPredicateRequestOrderByInner {
|
||||
field: string;
|
||||
order: QueryEntitiesByPredicateRequestOrderByInnerOrderEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type QueryEntitiesByPredicateRequestOrderByInnerOrderEnum =
|
||||
| 'asc'
|
||||
| 'desc';
|
||||
@@ -45,6 +45,9 @@ export * from '../models/LocationsQueryResponse.model';
|
||||
export * from '../models/LocationsQueryResponsePageInfo.model';
|
||||
export * from '../models/ModelError.model';
|
||||
export * from '../models/NullableEntity.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequest.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model';
|
||||
export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model';
|
||||
export * from '../models/RecursivePartialEntity.model';
|
||||
export * from '../models/RecursivePartialEntityMeta.model';
|
||||
export * from '../models/RecursivePartialEntityMetaAllOf.model';
|
||||
|
||||
@@ -683,6 +683,83 @@ describe('InMemoryCatalogClient', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters by predicate query', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
const result = await client.queryEntities({
|
||||
query: { kind: 'CustomKind' },
|
||||
});
|
||||
expect(result.items).toEqual([entity1, entity3]);
|
||||
expect(result.totalItems).toBe(2);
|
||||
});
|
||||
|
||||
it('filters by predicate query with $all', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
const result = await client.queryEntities({
|
||||
query: {
|
||||
$all: [{ kind: 'CustomKind' }, { 'spec.type': 'service' }],
|
||||
},
|
||||
});
|
||||
expect(result.items).toEqual([entity1, entity3]);
|
||||
});
|
||||
|
||||
it('filters by predicate query with $any', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
const result = await client.queryEntities({
|
||||
query: {
|
||||
$any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }],
|
||||
},
|
||||
});
|
||||
expect(result.items).toEqual([entity1, entity3, entity4]);
|
||||
});
|
||||
|
||||
it('filters by predicate query with $not', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
const result = await client.queryEntities({
|
||||
query: {
|
||||
$all: [
|
||||
{ kind: 'CustomKind' },
|
||||
{ $not: { 'spec.lifecycle': 'production' } },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters by predicate query with $in', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
const result = await client.queryEntities({
|
||||
query: { 'spec.type': { $in: ['service', 'library'] } },
|
||||
});
|
||||
expect(result.items).toEqual([entity1, entity2, entity3]);
|
||||
});
|
||||
|
||||
it('filters by predicate query with $exists', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
const result = await client.queryEntities({
|
||||
query: { 'spec.lifecycle': { $exists: false } },
|
||||
});
|
||||
expect(result.items).toEqual([entity4]);
|
||||
});
|
||||
|
||||
it('preserves query predicate through cursor pagination', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
const page1 = await client.queryEntities({
|
||||
query: { kind: 'CustomKind' },
|
||||
orderFields: { field: 'metadata.name', order: 'asc' },
|
||||
limit: 1,
|
||||
});
|
||||
expect(page1.items.map(e => e.metadata.name)).toEqual(['e1']);
|
||||
expect(page1.totalItems).toBe(2);
|
||||
expect(page1.pageInfo.nextCursor).toBeDefined();
|
||||
|
||||
const page2 = await client.queryEntities({
|
||||
cursor: page1.pageInfo.nextCursor!,
|
||||
limit: 1,
|
||||
});
|
||||
expect(page2.items.map(e => e.metadata.name)).toEqual(['e3']);
|
||||
expect(page2.pageInfo.nextCursor).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws InputError for invalid cursor', async () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
await expect(
|
||||
|
||||
@@ -51,6 +51,10 @@ import {
|
||||
NotFoundError,
|
||||
NotImplementedError,
|
||||
} from '@backstage/errors';
|
||||
import {
|
||||
FilterPredicate,
|
||||
filterPredicateToFilterFunction,
|
||||
} from '@backstage/filter-predicates';
|
||||
import lodash from 'lodash';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch';
|
||||
@@ -373,6 +377,7 @@ export class InMemoryCatalogClient implements CatalogApi {
|
||||
): Promise<QueryEntitiesResponse> {
|
||||
// Decode query parameters from cursor or from the request directly
|
||||
let filter: EntityFilterQuery | undefined;
|
||||
let query: FilterPredicate | undefined;
|
||||
let orderFields: EntityOrderQuery | undefined;
|
||||
let fullTextFilter: { term: string; fields?: string[] } | undefined;
|
||||
let offset: number;
|
||||
@@ -386,12 +391,14 @@ export class InMemoryCatalogClient implements CatalogApi {
|
||||
throw new InputError('Invalid cursor');
|
||||
}
|
||||
filter = deserializeFilter(c.filter as any[]);
|
||||
query = c.query as FilterPredicate | undefined;
|
||||
orderFields = c.orderFields as EntityOrderQuery | undefined;
|
||||
fullTextFilter = c.fullTextFilter as typeof fullTextFilter;
|
||||
offset = c.offset as number;
|
||||
limit = request.limit;
|
||||
} else {
|
||||
filter = request?.filter;
|
||||
query = request?.query;
|
||||
orderFields = request?.orderFields;
|
||||
fullTextFilter = request?.fullTextFilter;
|
||||
offset = request?.offset ?? 0;
|
||||
@@ -401,6 +408,11 @@ export class InMemoryCatalogClient implements CatalogApi {
|
||||
// Apply filter
|
||||
let items = this.#entities.filter(createFilter(filter));
|
||||
|
||||
// Apply predicate-based query filter
|
||||
if (query) {
|
||||
items = items.filter(filterPredicateToFilterFunction(query));
|
||||
}
|
||||
|
||||
// Apply full-text filter, defaulting to the sort field or metadata.uid
|
||||
if (fullTextFilter) {
|
||||
const orderFieldsList = orderFields ? [orderFields].flat() : [];
|
||||
@@ -432,6 +444,7 @@ export class InMemoryCatalogClient implements CatalogApi {
|
||||
|
||||
const cursorBase = {
|
||||
filter: serializeFilter(filter),
|
||||
query,
|
||||
orderFields,
|
||||
fullTextFilter,
|
||||
totalItems,
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
import { SerializedError } from '@backstage/errors';
|
||||
import type { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
import type { SerializedError } from '@backstage/errors';
|
||||
import type {
|
||||
AnalyzeLocationRequest,
|
||||
AnalyzeLocationResponse,
|
||||
} from '@backstage/plugin-catalog-common';
|
||||
import { FilterPredicate } from '@backstage/filter-predicates';
|
||||
import type { FilterPredicate } from '@backstage/filter-predicates';
|
||||
|
||||
/**
|
||||
* This symbol can be used in place of a value when passed to filters in e.g.
|
||||
@@ -418,16 +418,43 @@ export type QueryEntitiesRequest =
|
||||
* The method takes this type in an initial pagination request,
|
||||
* when requesting the first batch of entities.
|
||||
*
|
||||
* The properties filter, sortField, query and sortFieldOrder, are going
|
||||
* The properties filter, query, sortField and sortFieldOrder, are going
|
||||
* to be immutable for the entire lifecycle of the following requests.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Either `filter` or `query` can be provided, or even both:
|
||||
* - `filter`: Uses the traditional key-value filter syntax (GET endpoint)
|
||||
* - `query`: Uses the predicate-based filter syntax with logical operators (POST endpoint)
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type QueryEntitiesInitialRequest = {
|
||||
fields?: string[];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
/**
|
||||
* Traditional key-value based filter.
|
||||
*/
|
||||
filter?: EntityFilterQuery;
|
||||
/**
|
||||
* Predicate-based filter with operators for logical expressions (`$all`,
|
||||
* `$any`, and `$not`) and matching (`$exists`, `$in`, `$hasPrefix`, and
|
||||
* (partially) `$contains`).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* {
|
||||
* query: {
|
||||
* $all: [
|
||||
* { kind: 'component' },
|
||||
* { 'spec.type': { $in: ['service', 'website'] } }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
query?: FilterPredicate;
|
||||
orderFields?: EntityOrderQuery;
|
||||
fullTextFilter?: {
|
||||
term: string;
|
||||
@@ -567,6 +594,7 @@ export interface CatalogApi {
|
||||
* const response = await catalogClient.queryEntities({
|
||||
* filter: [{ kind: 'group' }],
|
||||
* limit: 20,
|
||||
* fields: ['metadata', 'kind'],
|
||||
* fullTextFilter: {
|
||||
* term: 'A',
|
||||
* },
|
||||
@@ -583,11 +611,15 @@ export interface CatalogApi {
|
||||
*
|
||||
* ```
|
||||
* const secondBatchResponse = await catalogClient
|
||||
* .queryEntities({ cursor: response.nextCursor });
|
||||
* .queryEntities({
|
||||
* cursor: response.nextCursor,
|
||||
* limit: 20,
|
||||
* fields: ['metadata', 'kind'],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* secondBatchResponse will contain the next batch of (maximum) 20 entities,
|
||||
* together with a prevCursor property, useful to fetch the previous batch.
|
||||
* `secondBatchResponse` will contain the next batch of (maximum) 20 entities,
|
||||
* together with a `prevCursor` property, useful to fetch the previous batch.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
|
||||
@@ -14,7 +14,87 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { splitRefsIntoChunks } from './utils';
|
||||
import { CATALOG_FILTER_EXISTS } from './types/api';
|
||||
import { convertFilterToPredicate, splitRefsIntoChunks } from './utils';
|
||||
|
||||
describe('convertFilterToPredicate', () => {
|
||||
it('converts a single string value', () => {
|
||||
expect(convertFilterToPredicate({ kind: 'component' })).toEqual({
|
||||
kind: 'component',
|
||||
});
|
||||
});
|
||||
|
||||
it('converts multiple keys into $all', () => {
|
||||
expect(
|
||||
convertFilterToPredicate({
|
||||
kind: 'component',
|
||||
'spec.type': 'service',
|
||||
}),
|
||||
).toEqual({
|
||||
$all: [{ kind: 'component' }, { 'spec.type': 'service' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts an array of string values into $in', () => {
|
||||
expect(
|
||||
convertFilterToPredicate({ 'spec.type': ['service', 'website'] }),
|
||||
).toEqual({
|
||||
'spec.type': { $in: ['service', 'website'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('converts CATALOG_FILTER_EXISTS into $exists', () => {
|
||||
expect(
|
||||
convertFilterToPredicate({ 'spec.owner': CATALOG_FILTER_EXISTS }),
|
||||
).toEqual({
|
||||
'spec.owner': { $exists: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('converts an array of records into $any (OR)', () => {
|
||||
expect(
|
||||
convertFilterToPredicate([{ kind: 'component' }, { kind: 'api' }]),
|
||||
).toEqual({
|
||||
$any: [{ kind: 'component' }, { kind: 'api' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts array of records with multiple keys each', () => {
|
||||
expect(
|
||||
convertFilterToPredicate([
|
||||
{ kind: 'component', 'spec.type': 'service' },
|
||||
{ kind: 'api' },
|
||||
]),
|
||||
).toEqual({
|
||||
$any: [
|
||||
{ $all: [{ kind: 'component' }, { 'spec.type': 'service' }] },
|
||||
{ kind: 'api' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('treats CATALOG_FILTER_EXISTS mixed with string values as just existence', () => {
|
||||
expect(
|
||||
convertFilterToPredicate({
|
||||
'spec.owner': [CATALOG_FILTER_EXISTS, 'team-a'],
|
||||
}),
|
||||
).toEqual({
|
||||
'spec.owner': { $exists: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('converts a single-element array filter without wrapping in $any', () => {
|
||||
expect(convertFilterToPredicate([{ kind: 'component' }])).toEqual({
|
||||
kind: 'component',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores entries with no valid values', () => {
|
||||
expect(
|
||||
convertFilterToPredicate({ kind: 'component', other: [] as string[] }),
|
||||
).toEqual({ kind: 'component' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitRefsIntoChunks', () => {
|
||||
it('splits by count limit', () => {
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FilterPredicate,
|
||||
FilterPredicateExpression,
|
||||
} from '@backstage/filter-predicates';
|
||||
import {
|
||||
CATALOG_FILTER_EXISTS,
|
||||
EntityFilterQuery,
|
||||
QueryEntitiesCursorRequest,
|
||||
QueryEntitiesInitialRequest,
|
||||
QueryEntitiesRequest,
|
||||
@@ -26,6 +32,58 @@ export function isQueryEntitiesInitialRequest(
|
||||
return !(request as QueryEntitiesCursorRequest).cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cursor contains a predicate query by attempting to decode it.
|
||||
* @internal
|
||||
*/
|
||||
export function cursorContainsQuery(cursor: string): boolean {
|
||||
try {
|
||||
const decoded = JSON.parse(atob(cursor));
|
||||
return 'query' in decoded;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link EntityFilterQuery} into a predicate query object.
|
||||
* @internal
|
||||
*/
|
||||
export function convertFilterToPredicate(filter: EntityFilterQuery):
|
||||
| FilterPredicateExpression
|
||||
| {
|
||||
$all: FilterPredicate[];
|
||||
}
|
||||
| {
|
||||
$any: FilterPredicate[];
|
||||
} {
|
||||
const records = [filter].flat();
|
||||
|
||||
const clauses = records.map(record => {
|
||||
const parts: FilterPredicateExpression[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
const values = [value].flat();
|
||||
const strings = values.filter((v): v is string => typeof v === 'string');
|
||||
const hasExists = values.some(v => v === CATALOG_FILTER_EXISTS);
|
||||
|
||||
if (hasExists) {
|
||||
// Ignore whether there ALSO were some strings - that would boil down to
|
||||
// just existence anyway since there's effectively an OR between them
|
||||
parts.push({ [key]: { $exists: true } } as FilterPredicateExpression);
|
||||
} else if (strings.length === 1) {
|
||||
parts.push({ [key]: strings[0] } as FilterPredicateExpression);
|
||||
} else if (strings.length > 1) {
|
||||
parts.push({ [key]: { $in: strings } } as FilterPredicateExpression);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length === 1 ? parts[0] : { $all: parts };
|
||||
});
|
||||
|
||||
return clauses.length === 1 ? clauses[0] : { $any: clauses };
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a set of entity refs, and splits them into chunks (groups) such that
|
||||
* the total string length in each chunk does not exceed the default Express.js
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# @backstage/cli-common
|
||||
|
||||
## 0.2.0-next.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 56bd494: Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths.
|
||||
|
||||
To migrate existing `findPaths` usage:
|
||||
|
||||
```ts
|
||||
// Before
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
const paths = findPaths(__dirname);
|
||||
|
||||
// After — for target project paths (cwd-based):
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
// paths.targetDir → targetPaths.dir
|
||||
// paths.targetRoot → targetPaths.rootDir
|
||||
// paths.resolveTarget('src') → targetPaths.resolve('src')
|
||||
// paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock')
|
||||
|
||||
// After — for package-relative paths:
|
||||
import { findOwnPaths } from '@backstage/cli-common';
|
||||
const own = findOwnPaths(__dirname);
|
||||
// paths.ownDir → own.dir
|
||||
// paths.ownRoot → own.rootDir
|
||||
// paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js')
|
||||
// paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json')
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/errors@1.2.7
|
||||
|
||||
## 0.1.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/cli-common",
|
||||
"version": "0.1.18",
|
||||
"version": "0.2.0-next.0",
|
||||
"description": "Common functionality used by cli, backend, and create-app",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths';
|
||||
import { findPaths, findRootPath, findOwnRootDir, findOwnPaths } from './paths';
|
||||
|
||||
describe('paths', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('findOwnDir and findOwnRootDir should find owns paths', () => {
|
||||
const dir = findOwnDir(__dirname);
|
||||
const root = findOwnRootDir(dir);
|
||||
it('findOwnPaths and findOwnRootDir should find own paths', () => {
|
||||
const own = findOwnPaths(__dirname);
|
||||
const root = findOwnRootDir(own.dir);
|
||||
|
||||
expect(dir).toBe(resolvePath(__dirname, '..'));
|
||||
expect(own.dir).toBe(resolvePath(__dirname, '..'));
|
||||
expect(root).toBe(resolvePath(__dirname, '../../..'));
|
||||
});
|
||||
|
||||
|
||||
@@ -119,7 +119,23 @@ export function findOwnRootDir(ownDir: string) {
|
||||
);
|
||||
}
|
||||
|
||||
return resolvePath(ownDir, '../..');
|
||||
const rootDir = findRootPath(ownDir, pkgJsonPath => {
|
||||
try {
|
||||
const content = fs.readFileSync(pkgJsonPath, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
return Boolean(data.workspaces);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to read package.json at '${pkgJsonPath}', ${error}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!rootDir) {
|
||||
throw new Error(`No monorepo root found when searching from '${ownDir}'`);
|
||||
}
|
||||
|
||||
return rootDir;
|
||||
}
|
||||
|
||||
// Hierarchical directory cache shared across all OwnPathsImpl instances.
|
||||
@@ -199,11 +215,6 @@ class OwnPathsImpl implements OwnPaths {
|
||||
};
|
||||
}
|
||||
|
||||
// Finds the root of a given package
|
||||
export function findOwnDir(searchDir: string) {
|
||||
return OwnPathsImpl.findDir(searchDir);
|
||||
}
|
||||
|
||||
// Used by the test utility in testUtils.ts to override targetPaths
|
||||
export let targetPathsOverride: TargetPaths | undefined;
|
||||
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/cli-node
|
||||
|
||||
## 0.2.19-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code.
|
||||
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
|
||||
- Updated dependencies
|
||||
- @backstage/cli-common@0.2.0-next.0
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/types@1.2.2
|
||||
|
||||
## 0.2.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/cli-node",
|
||||
"version": "0.2.18",
|
||||
"version": "0.2.19-next.0",
|
||||
"description": "Node.js library for Backstage CLIs",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
@@ -35,6 +35,7 @@
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"@yarnpkg/parsers": "^3.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"semver": "^7.5.3",
|
||||
@@ -43,6 +44,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@types/yarnpkg__lockfile": "^1.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ export class Lockfile {
|
||||
keys(): IterableIterator<string>;
|
||||
static load(path: string): Promise<Lockfile>;
|
||||
static parse(content: string): Lockfile;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Lockfile } from './Lockfile';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
@@ -29,7 +30,74 @@ __metadata:
|
||||
cacheKey: 8
|
||||
`;
|
||||
|
||||
describe('New Lockfile', () => {
|
||||
const mockLegacy = `${LEGACY_HEADER}
|
||||
a@^1:
|
||||
version "1.0.1"
|
||||
resolved "https://my-registry/a-1.0.01.tgz#abc123"
|
||||
integrity sha512-xyz
|
||||
dependencies:
|
||||
b "^2"
|
||||
|
||||
b@2.0.x:
|
||||
version "2.0.1"
|
||||
|
||||
b@^2:
|
||||
version "2.0.0"
|
||||
`;
|
||||
|
||||
const mockModern = `${MODERN_HEADER}
|
||||
a@^1:
|
||||
version: 1.0.1
|
||||
dependencies:
|
||||
b: ^2
|
||||
integrity: sha512-xyz
|
||||
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
|
||||
|
||||
"b@2.0.x, b@^2.0.1":
|
||||
version: 2.0.1
|
||||
|
||||
b@^2:
|
||||
version: 2.0.0
|
||||
`;
|
||||
|
||||
describe('Lockfile', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
it('should load and serialize a legacy lockfile', async () => {
|
||||
mockDir.setContent({
|
||||
'yarn.lock': mockLegacy,
|
||||
});
|
||||
|
||||
const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock'));
|
||||
expect(lockfile.get('a')).toEqual([
|
||||
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
|
||||
]);
|
||||
expect(lockfile.get('b')).toEqual([
|
||||
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' },
|
||||
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
|
||||
]);
|
||||
expect(lockfile.toString()).toBe(mockLegacy);
|
||||
});
|
||||
|
||||
it('should load and serialize a modern lockfile', async () => {
|
||||
mockDir.setContent({
|
||||
'yarn.lock': mockModern,
|
||||
});
|
||||
|
||||
const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock'));
|
||||
expect(lockfile.get('a')).toEqual([
|
||||
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
|
||||
]);
|
||||
expect(lockfile.get('b')).toEqual([
|
||||
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
|
||||
{ range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
|
||||
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
|
||||
]);
|
||||
expect(lockfile.toString()).toBe(mockModern);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lockfile advanced', () => {
|
||||
describe('diff', () => {
|
||||
const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER}
|
||||
a@^1:
|
||||
|
||||
@@ -14,12 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { parseSyml } from '@yarnpkg/parsers';
|
||||
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
|
||||
import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
|
||||
|
||||
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
|
||||
const NEW_HEADER = `${[
|
||||
`# This file is generated by running "yarn install" inside your project.\n`,
|
||||
`# Manual changes might be lost - proceed with caution!\n`,
|
||||
].join(``)}\n`;
|
||||
|
||||
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
|
||||
const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
|
||||
|
||||
/** @internal */
|
||||
type LockfileData = {
|
||||
[entry: string]: {
|
||||
@@ -97,6 +107,8 @@ export class Lockfile {
|
||||
* @public
|
||||
*/
|
||||
static parse(content: string): Lockfile {
|
||||
const legacy = LEGACY_REGEX.test(content);
|
||||
|
||||
let data: LockfileData;
|
||||
try {
|
||||
data = parseSyml(content);
|
||||
@@ -130,18 +142,21 @@ export class Lockfile {
|
||||
}
|
||||
}
|
||||
|
||||
return new Lockfile(packages, data);
|
||||
return new Lockfile(packages, data, legacy);
|
||||
}
|
||||
|
||||
private readonly packages: Map<string, LockfileQueryEntry[]>;
|
||||
private readonly data: LockfileData;
|
||||
private readonly legacy: boolean;
|
||||
|
||||
private constructor(
|
||||
packages: Map<string, LockfileQueryEntry[]>,
|
||||
data: LockfileData,
|
||||
legacy: boolean = false,
|
||||
) {
|
||||
this.packages = packages;
|
||||
this.data = data;
|
||||
this.legacy = legacy;
|
||||
}
|
||||
|
||||
/** Returns the name of all packages available in the lockfile */
|
||||
@@ -154,6 +169,15 @@ export class Lockfile {
|
||||
return this.packages.keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the lockfile back to a string.
|
||||
*/
|
||||
toString(): string {
|
||||
return this.legacy
|
||||
? legacyStringifyLockfile(this.data)
|
||||
: NEW_HEADER + stringifySyml(this.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simplified dependency graph from the lockfile data, where each
|
||||
* key is a package, and the value is a set of all packages that it depends on
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# @backstage/cli
|
||||
|
||||
## 0.35.5-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 246877a: Updated dependency `bfj` to `^9.0.2`.
|
||||
- bba2e49: Internal refactor to use new concurrency utilities from `@backstage/cli-node`.
|
||||
- fd50cb3: Added `translations export` and `translations import` commands for managing translation files.
|
||||
|
||||
The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app.
|
||||
|
||||
Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping.
|
||||
|
||||
- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1
|
||||
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
|
||||
- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages.
|
||||
- 092b41f: Updated dependency `webpack` to `~5.105.0`.
|
||||
- Updated dependencies
|
||||
- @backstage/cli-common@0.2.0-next.0
|
||||
- @backstage/cli-node@0.2.19-next.0
|
||||
- @backstage/eslint-plugin@0.2.2-next.0
|
||||
- @backstage/integration@1.21.0-next.0
|
||||
- @backstage/config-loader@1.10.9-next.0
|
||||
- @backstage/catalog-model@1.7.6
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/module-federation-common@0.1.0
|
||||
- @backstage/release-manifests@0.0.13
|
||||
- @backstage/types@1.2.2
|
||||
|
||||
## 0.35.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/cli",
|
||||
"version": "0.35.4",
|
||||
"version": "0.35.5-next.0",
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"backstage": {
|
||||
"role": "cli"
|
||||
@@ -77,8 +77,6 @@
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||
"@typescript-eslint/parser": "^8.16.0",
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"@yarnpkg/parsers": "^3.0.0",
|
||||
"bfj": "^9.0.2",
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^4.0.0",
|
||||
@@ -182,7 +180,6 @@
|
||||
"@types/tar": "^6.1.1",
|
||||
"@types/terser-webpack-plugin": "^5.0.4",
|
||||
"@types/webpack-sources": "^3.2.3",
|
||||
"@types/yarnpkg__lockfile": "^1.1.4",
|
||||
"del": "^8.0.0",
|
||||
"esbuild-loader": "^4.0.0",
|
||||
"eslint-webpack-plugin": "^4.2.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { packageVersions, createPackageVersionProvider } from './version';
|
||||
import { Lockfile } from './versioning';
|
||||
import { Lockfile } from '@backstage/cli-node';
|
||||
import corePluginApiPkg from '@backstage/core-plugin-api/package.json';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import fs from 'fs-extra';
|
||||
import semver from 'semver';
|
||||
import { findOwnPaths } from '@backstage/cli-common';
|
||||
import { Lockfile } from './versioning';
|
||||
import { Lockfile } from '@backstage/cli-node';
|
||||
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
const ownPaths = findOwnPaths(__dirname);
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Lockfile } from './Lockfile';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
`;
|
||||
|
||||
const MODERN_HEADER = `# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 6
|
||||
cacheKey: 8
|
||||
`;
|
||||
|
||||
const mockA = `${LEGACY_HEADER}
|
||||
a@^1:
|
||||
version "1.0.1"
|
||||
resolved "https://my-registry/a-1.0.01.tgz#abc123"
|
||||
integrity sha512-xyz
|
||||
dependencies:
|
||||
b "^2"
|
||||
|
||||
b@2.0.x:
|
||||
version "2.0.1"
|
||||
|
||||
b@^2:
|
||||
version "2.0.0"
|
||||
`;
|
||||
|
||||
describe('Lockfile', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
it('should load and serialize mockA', async () => {
|
||||
mockDir.setContent({
|
||||
'yarn.lock': mockA,
|
||||
});
|
||||
|
||||
const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock'));
|
||||
expect(lockfile.get('a')).toEqual([
|
||||
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
|
||||
]);
|
||||
expect(lockfile.get('b')).toEqual([
|
||||
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' },
|
||||
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
|
||||
]);
|
||||
expect(lockfile.toString()).toBe(mockA);
|
||||
});
|
||||
});
|
||||
|
||||
const mockANew = `${MODERN_HEADER}
|
||||
a@^1:
|
||||
version: 1.0.1
|
||||
dependencies:
|
||||
b: ^2
|
||||
integrity: sha512-xyz
|
||||
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
|
||||
|
||||
"b@2.0.x, b@^2.0.1":
|
||||
version: 2.0.1
|
||||
|
||||
b@^2:
|
||||
version: 2.0.0
|
||||
`;
|
||||
|
||||
describe('New Lockfile', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
it('should load and serialize mockANew', async () => {
|
||||
mockDir.setContent({
|
||||
'yarn.lock': mockANew,
|
||||
});
|
||||
|
||||
const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock'));
|
||||
expect(lockfile.get('a')).toEqual([
|
||||
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
|
||||
]);
|
||||
expect(lockfile.get('b')).toEqual([
|
||||
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
|
||||
{ range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
|
||||
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
|
||||
]);
|
||||
expect(lockfile.toString()).toBe(mockANew);
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
|
||||
import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
|
||||
|
||||
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
|
||||
|
||||
type LockfileData = {
|
||||
[entry: string]: {
|
||||
version: string;
|
||||
resolved?: string;
|
||||
integrity?: string /* old */;
|
||||
checksum?: string /* new */;
|
||||
dependencies?: { [name: string]: string };
|
||||
peerDependencies?: { [name: string]: string };
|
||||
};
|
||||
};
|
||||
|
||||
type LockfileQueryEntry = {
|
||||
range: string;
|
||||
version: string;
|
||||
dataKey: string;
|
||||
};
|
||||
|
||||
// the new yarn header is handled out of band of the parsing
|
||||
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
|
||||
const NEW_HEADER = `${[
|
||||
`# This file is generated by running "yarn install" inside your project.\n`,
|
||||
`# Manual changes might be lost - proceed with caution!\n`,
|
||||
].join(``)}\n`;
|
||||
|
||||
// taken from yarn parser package
|
||||
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
|
||||
const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
|
||||
|
||||
// these are special top level yarn keys.
|
||||
// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9
|
||||
const SPECIAL_OBJECT_KEYS = [
|
||||
`__metadata`,
|
||||
`version`,
|
||||
`resolution`,
|
||||
`dependencies`,
|
||||
`peerDependencies`,
|
||||
`dependenciesMeta`,
|
||||
`peerDependenciesMeta`,
|
||||
`binaries`,
|
||||
];
|
||||
|
||||
export class Lockfile {
|
||||
static async load(path: string) {
|
||||
const lockfileContents = await fs.readFile(path, 'utf8');
|
||||
return Lockfile.parse(lockfileContents);
|
||||
}
|
||||
|
||||
static parse(content: string) {
|
||||
const legacy = LEGACY_REGEX.test(content);
|
||||
|
||||
let data: LockfileData;
|
||||
try {
|
||||
data = parseSyml(content);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed yarn.lock parse, ${err}`);
|
||||
}
|
||||
|
||||
const packages = new Map<string, LockfileQueryEntry[]>();
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (SPECIAL_OBJECT_KEYS.includes(key)) continue;
|
||||
|
||||
const [, name, ranges] = ENTRY_PATTERN.exec(key) ?? [];
|
||||
if (!name) {
|
||||
throw new Error(`Failed to parse yarn.lock entry '${key}'`);
|
||||
}
|
||||
|
||||
let queries = packages.get(name);
|
||||
if (!queries) {
|
||||
queries = [];
|
||||
packages.set(name, queries);
|
||||
}
|
||||
for (let range of ranges.split(/\s*,\s*/)) {
|
||||
if (range.startsWith(`${name}@`)) {
|
||||
range = range.slice(`${name}@`.length);
|
||||
}
|
||||
if (range.startsWith('npm:')) {
|
||||
range = range.slice('npm:'.length);
|
||||
}
|
||||
queries.push({ range, version: value.version, dataKey: key });
|
||||
}
|
||||
}
|
||||
|
||||
return new Lockfile(packages, data, legacy);
|
||||
}
|
||||
|
||||
private readonly packages: Map<string, LockfileQueryEntry[]>;
|
||||
private readonly data: LockfileData;
|
||||
private readonly legacy: boolean;
|
||||
|
||||
private constructor(
|
||||
packages: Map<string, LockfileQueryEntry[]>,
|
||||
data: LockfileData,
|
||||
legacy: boolean = false,
|
||||
) {
|
||||
this.packages = packages;
|
||||
this.data = data;
|
||||
this.legacy = legacy;
|
||||
}
|
||||
|
||||
/** Get the entries for a single package in the lockfile */
|
||||
get(name: string): LockfileQueryEntry[] | undefined {
|
||||
return this.packages.get(name);
|
||||
}
|
||||
|
||||
/** Returns the name of all packages available in the lockfile */
|
||||
keys(): IterableIterator<string> {
|
||||
return this.packages.keys();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.legacy
|
||||
? legacyStringifyLockfile(this.data)
|
||||
: NEW_HEADER + stringifySyml(this.data);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
import {
|
||||
productionPack,
|
||||
revertProductionPack,
|
||||
} from '../../../../modules/build/lib/packager/productionPack';
|
||||
} from '../../lib/packager/productionPack';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { Command, Option } from 'commander';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
import { configOption } from '../config';
|
||||
|
||||
export function registerPackageCommands(command: Command) {
|
||||
@@ -197,6 +197,58 @@ export const buildPlugin = createCliPlugin({
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'clean'],
|
||||
description: 'Delete cache directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/clean'), 'default'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'prepack'],
|
||||
description: 'Prepares a package for packaging before publishing',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'pre'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'postpack'],
|
||||
description: 'Restores the changes made by the prepack command',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'post'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'clean'],
|
||||
description: 'Delete cache and output directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/repo/clean'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['build-workspace'],
|
||||
description:
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import yargs from 'yargs';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export const configOption = [
|
||||
'--config <path>',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'new',
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
import { version as cliVersion } from '../../../../package.json';
|
||||
import os from 'node:os';
|
||||
import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common';
|
||||
import { Lockfile } from '../../../lib/versioning';
|
||||
import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
|
||||
import {
|
||||
BackstagePackageJson,
|
||||
Lockfile,
|
||||
PackageGraph,
|
||||
} from '@backstage/cli-node';
|
||||
import { minimatch } from 'minimatch';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import yargs from 'yargs';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'info',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export function registerPackageLintCommand(command: Command) {
|
||||
command.arguments('[directories...]');
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from 'node:path';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import { publishPreflightCheck } from '../../lib/publishing';
|
||||
import { publishPreflightCheck } from '../../../build/lib/publishing';
|
||||
|
||||
const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx', '.json'];
|
||||
|
||||
|
||||
@@ -15,50 +15,11 @@
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'maintenance',
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['package', 'clean'],
|
||||
description: 'Delete cache directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/clean'), 'default'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'prepack'],
|
||||
description: 'Prepares a package for packaging before publishing',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'pre'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'postpack'],
|
||||
description: 'Restores the changes made by the prepack command',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'post'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'fix'],
|
||||
description: 'Automatically fix packages in the project',
|
||||
@@ -79,19 +40,6 @@ export default createCliPlugin({
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'clean'],
|
||||
description: 'Delete cache and output directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/repo/clean'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'list-deprecations'],
|
||||
description: 'List deprecations',
|
||||
|
||||
@@ -19,7 +19,7 @@ import * as runObj from '@backstage/cli-common';
|
||||
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
|
||||
import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump';
|
||||
import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils';
|
||||
import { YarnInfoInspectData } from '../../../../lib/versioning/packages';
|
||||
import { YarnInfoInspectData } from '../../lib/versioning/packages';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
@@ -69,8 +69,8 @@ jest.mock('@backstage/cli-common', () => {
|
||||
});
|
||||
|
||||
const mockFetchPackageInfo = jest.fn();
|
||||
jest.mock('../../../../lib/versioning/packages', () => {
|
||||
const actual = jest.requireActual('../../../../lib/versioning/packages');
|
||||
jest.mock('../../lib/versioning/packages', () => {
|
||||
const actual = jest.requireActual('../../lib/versioning/packages');
|
||||
return {
|
||||
...actual,
|
||||
fetchPackageInfo: (name: string) => mockFetchPackageInfo(name),
|
||||
|
||||
@@ -31,13 +31,12 @@ import { isError, NotFoundError } from '@backstage/errors';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
|
||||
import { getHasYarnPlugin } from '../../../../lib/yarnPlugin';
|
||||
import { Lockfile, runConcurrentTasks } from '@backstage/cli-node';
|
||||
import {
|
||||
fetchPackageInfo,
|
||||
Lockfile,
|
||||
mapDependencies,
|
||||
YarnInfoInspectData,
|
||||
} from '../../../../lib/versioning';
|
||||
import { runConcurrentTasks } from '@backstage/cli-node';
|
||||
} from '../../lib/versioning/packages';
|
||||
import {
|
||||
getManifestByReleaseLine,
|
||||
getManifestByVersion,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'migrate',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
import { NotImplementedError } from '@backstage/errors';
|
||||
|
||||
export default createCliPlugin({
|
||||
|
||||
@@ -24,7 +24,7 @@ import startCase from 'lodash/startCase';
|
||||
import upperCase from 'lodash/upperCase';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import lowerFirst from 'lodash/lowerFirst';
|
||||
import { Lockfile } from '../../../../lib/versioning';
|
||||
import { Lockfile } from '@backstage/cli-node';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
|
||||
import { createPackageVersionProvider } from '../../../../lib/version';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'test',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import yargs from 'yargs';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../lib/lazy';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
import { DEFAULT_MESSAGE_PATTERN } from './lib/messageFilePath';
|
||||
|
||||
export default createCliPlugin({
|
||||
|
||||
@@ -20,7 +20,7 @@ import { CommandRegistry } from './CommandRegistry';
|
||||
import { Command } from 'commander';
|
||||
import { version } from '../lib/version';
|
||||
import chalk from 'chalk';
|
||||
import { exitWithError } from '../lib/errors';
|
||||
import { exitWithError } from './errors';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { isPromise } from 'node:util/types';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { exitWithError } from '../lib/errors';
|
||||
import { exitWithError } from './errors';
|
||||
|
||||
type ActionFunc = (...args: any[]) => Promise<void>;
|
||||
type ActionExports<TModule extends object> = {
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/codemods
|
||||
|
||||
## 0.1.55-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
|
||||
- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages.
|
||||
- Updated dependencies
|
||||
- @backstage/cli-common@0.2.0-next.0
|
||||
|
||||
## 0.1.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/codemods",
|
||||
"version": "0.1.54",
|
||||
"version": "0.1.55-next.0",
|
||||
"description": "A collection of codemods for Backstage projects",
|
||||
"backstage": {
|
||||
"role": "cli"
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/config-loader
|
||||
|
||||
## 1.10.9-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
|
||||
- Updated dependencies
|
||||
- @backstage/cli-common@0.2.0-next.0
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/errors@1.2.7
|
||||
- @backstage/types@1.2.2
|
||||
|
||||
## 1.10.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/config-loader",
|
||||
"version": "1.10.8",
|
||||
"version": "1.10.9-next.0",
|
||||
"description": "Config loading functionality used by Backstage backend, and CLI",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @backstage/core-app-api
|
||||
|
||||
## 1.19.6-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/config@1.3.6
|
||||
- @backstage/core-plugin-api@1.12.4-next.0
|
||||
- @backstage/types@1.2.2
|
||||
- @backstage/version-bridge@1.0.12
|
||||
|
||||
## 1.19.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/core-app-api",
|
||||
"version": "1.19.5",
|
||||
"version": "1.19.6-next.0",
|
||||
"description": "Core app API used by Backstage apps",
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user